summaryrefslogtreecommitdiffstats
path: root/ansible_collections/cisco/meraki/plugins/modules
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-13 12:04:41 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-13 12:04:41 +0000
commit975f66f2eebe9dadba04f275774d4ab83f74cf25 (patch)
tree89bd26a93aaae6a25749145b7e4bca4a1e75b2be /ansible_collections/cisco/meraki/plugins/modules
parentInitial commit. (diff)
downloadansible-975f66f2eebe9dadba04f275774d4ab83f74cf25.tar.xz
ansible-975f66f2eebe9dadba04f275774d4ab83f74cf25.zip
Adding upstream version 7.7.0+dfsg.upstream/7.7.0+dfsg
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'ansible_collections/cisco/meraki/plugins/modules')
-rw-r--r--ansible_collections/cisco/meraki/plugins/modules/__init__.py0
-rw-r--r--ansible_collections/cisco/meraki/plugins/modules/meraki_action_batch.py394
-rw-r--r--ansible_collections/cisco/meraki/plugins/modules/meraki_admin.py504
-rw-r--r--ansible_collections/cisco/meraki/plugins/modules/meraki_alert.py395
-rw-r--r--ansible_collections/cisco/meraki/plugins/modules/meraki_config_template.py331
-rw-r--r--ansible_collections/cisco/meraki/plugins/modules/meraki_device.py431
-rw-r--r--ansible_collections/cisco/meraki/plugins/modules/meraki_firewalled_services.py233
-rw-r--r--ansible_collections/cisco/meraki/plugins/modules/meraki_management_interface.py384
-rw-r--r--ansible_collections/cisco/meraki/plugins/modules/meraki_mr_l3_firewall.py300
-rw-r--r--ansible_collections/cisco/meraki/plugins/modules/meraki_mr_l7_firewall.py503
-rw-r--r--ansible_collections/cisco/meraki/plugins/modules/meraki_mr_radio.py490
-rw-r--r--ansible_collections/cisco/meraki/plugins/modules/meraki_mr_rf_profile.py662
-rw-r--r--ansible_collections/cisco/meraki/plugins/modules/meraki_mr_settings.py221
-rw-r--r--ansible_collections/cisco/meraki/plugins/modules/meraki_mr_ssid.py744
-rw-r--r--ansible_collections/cisco/meraki/plugins/modules/meraki_ms_access_list.py319
-rw-r--r--ansible_collections/cisco/meraki/plugins/modules/meraki_ms_access_policies.py608
-rw-r--r--ansible_collections/cisco/meraki/plugins/modules/meraki_ms_l3_interface.py373
-rw-r--r--ansible_collections/cisco/meraki/plugins/modules/meraki_ms_link_aggregation.py258
-rw-r--r--ansible_collections/cisco/meraki/plugins/modules/meraki_ms_ospf.py323
-rw-r--r--ansible_collections/cisco/meraki/plugins/modules/meraki_ms_stack.py278
-rw-r--r--ansible_collections/cisco/meraki/plugins/modules/meraki_ms_stack_l3_interface.py395
-rw-r--r--ansible_collections/cisco/meraki/plugins/modules/meraki_ms_storm_control.py201
-rw-r--r--ansible_collections/cisco/meraki/plugins/modules/meraki_ms_switchport.py680
-rw-r--r--ansible_collections/cisco/meraki/plugins/modules/meraki_mx_content_filtering.py302
-rw-r--r--ansible_collections/cisco/meraki/plugins/modules/meraki_mx_intrusion_prevention.py371
-rw-r--r--ansible_collections/cisco/meraki/plugins/modules/meraki_mx_l2_interface.py272
-rw-r--r--ansible_collections/cisco/meraki/plugins/modules/meraki_mx_l3_firewall.py377
-rw-r--r--ansible_collections/cisco/meraki/plugins/modules/meraki_mx_l7_firewall.py475
-rw-r--r--ansible_collections/cisco/meraki/plugins/modules/meraki_mx_malware.py264
-rw-r--r--ansible_collections/cisco/meraki/plugins/modules/meraki_mx_nat.py679
-rw-r--r--ansible_collections/cisco/meraki/plugins/modules/meraki_mx_network_vlan_settings.py161
-rw-r--r--ansible_collections/cisco/meraki/plugins/modules/meraki_mx_site_to_site_firewall.py330
-rw-r--r--ansible_collections/cisco/meraki/plugins/modules/meraki_mx_site_to_site_vpn.py270
-rw-r--r--ansible_collections/cisco/meraki/plugins/modules/meraki_mx_static_route.py438
-rw-r--r--ansible_collections/cisco/meraki/plugins/modules/meraki_mx_third_party_vpn_peers.py493
-rw-r--r--ansible_collections/cisco/meraki/plugins/modules/meraki_mx_uplink_bandwidth.py325
-rw-r--r--ansible_collections/cisco/meraki/plugins/modules/meraki_mx_vlan.py585
-rw-r--r--ansible_collections/cisco/meraki/plugins/modules/meraki_network.py469
-rw-r--r--ansible_collections/cisco/meraki/plugins/modules/meraki_network_settings.py337
-rw-r--r--ansible_collections/cisco/meraki/plugins/modules/meraki_organization.py242
-rw-r--r--ansible_collections/cisco/meraki/plugins/modules/meraki_snmp.py387
-rw-r--r--ansible_collections/cisco/meraki/plugins/modules/meraki_syslog.py317
-rw-r--r--ansible_collections/cisco/meraki/plugins/modules/meraki_webhook.py431
-rw-r--r--ansible_collections/cisco/meraki/plugins/modules/meraki_webhook_payload_template.py352
44 files changed, 16904 insertions, 0 deletions
diff --git a/ansible_collections/cisco/meraki/plugins/modules/__init__.py b/ansible_collections/cisco/meraki/plugins/modules/__init__.py
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/ansible_collections/cisco/meraki/plugins/modules/__init__.py
diff --git a/ansible_collections/cisco/meraki/plugins/modules/meraki_action_batch.py b/ansible_collections/cisco/meraki/plugins/modules/meraki_action_batch.py
new file mode 100644
index 000000000..3e0bed937
--- /dev/null
+++ b/ansible_collections/cisco/meraki/plugins/modules/meraki_action_batch.py
@@ -0,0 +1,394 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Copyright: (c) 2021, Kevin Breit (@kbreit) <kevin.breit@kevinbreit.net>
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+
+__metaclass__ = type
+
+ANSIBLE_METADATA = {
+ "metadata_version": "1.1",
+ "status": ["preview"],
+ "supported_by": "community",
+}
+
+DOCUMENTATION = r"""
+---
+module: meraki_action_batch
+short_description: Manage Action Batch jobs within the Meraki Dashboard.
+description:
+- Allows for management of Action Batch jobs for Meraki.
+notes:
+- This module is in active development and the interface may change.
+options:
+ state:
+ description:
+ - Specifies whether to lookup, create, or delete an Action Batch job.
+ choices: ['query', 'present', 'absent']
+ default: present
+ type: str
+ net_name:
+ description:
+ - Name of network, if applicable.
+ type: str
+ net_id:
+ description:
+ - ID of network, if applicable.
+ type: str
+ action_batch_id:
+ description:
+ - ID of an existing Action Batch job.
+ type: str
+ confirmed:
+ description:
+ - Whether job is to be executed.
+ type: bool
+ default: False
+ synchronous:
+ description:
+ - Whether job is a synchronous or asynchronous job.
+ type: bool
+ default: True
+ actions:
+ description:
+ - List of actions the job should execute.
+ type: list
+ elements: dict
+ suboptions:
+ operation:
+ description:
+ - Operation type of action
+ type: str
+ choices: [
+ 'create',
+ 'destroy',
+ 'update',
+ 'claim',
+ 'bind',
+ 'split',
+ 'unbind',
+ 'combine',
+ 'update_order',
+ 'cycle',
+ 'swap',
+ 'assignSeats',
+ 'move',
+ 'moveSeats',
+ 'renewSeats'
+ ]
+ resource:
+ description:
+ - Path to Action Batch resource.
+ type: str
+ body:
+ description:
+ - Required body of action.
+ type: raw
+author:
+- Kevin Breit (@kbreit)
+extends_documentation_fragment: cisco.meraki.meraki
+"""
+
+
+EXAMPLES = r"""
+ - name: Query all Action Batches
+ meraki_action_batch:
+ auth_key: abc123
+ org_name: YourOrg
+ state: query
+ delegate_to: localhost
+
+ - name: Query one Action Batch job
+ meraki_action_batch:
+ auth_key: abc123
+ org_name: YourOrg
+ state: query
+ action_batch_id: 12345
+ delegate_to: localhost
+
+ - name: Create an Action Batch job
+ meraki_action_batch:
+ auth_key: abc123
+ org_name: YourOrg
+ state: present
+ actions:
+ - resource: '/organizations/org_123/networks'
+ operation: 'create'
+ body:
+ name: 'AnsibleActionBatch1'
+ productTypes:
+ - 'switch'
+ delegate_to: localhost
+
+ - name: Update Action Batch job
+ meraki_action_batch:
+ auth_key: abc123
+ org_name: YourOrg
+ state: present
+ action_batch_id: 12345
+ synchronous: false
+
+ - name: Create an Action Batch job with multiple actions
+ meraki_action_batch:
+ auth_key: abc123
+ org_name: YourOrg
+ state: present
+ actions:
+ - resource: '/organizations/org_123/networks'
+ operation: 'create'
+ body:
+ name: 'AnsibleActionBatch2'
+ productTypes:
+ - 'switch'
+ - resource: '/organizations/org_123/networks'
+ operation: 'create'
+ body:
+ name: 'AnsibleActionBatch3'
+ productTypes:
+ - 'switch'
+ delegate_to: localhost
+
+ - name: Delete an Action Batch job
+ meraki_action_batch:
+ auth_key: abc123
+ org_name: YourOrg
+ state: absent
+ action_batch_id: 12345
+ delegate_to: localhost
+"""
+
+RETURN = r"""
+data:
+ description: Information about action batch jobs.
+ type: complex
+ returned: always
+ contains:
+ id:
+ description: Unique ID of action batch job.
+ returned: success
+ type: str
+ sample: 123
+ organization_id:
+ description: Unique ID of organization which owns batch job.
+ returned: success
+ type: str
+ sample: 2930418
+ confirmed:
+ description: Whether action batch job was confirmed for execution.
+ returned: success
+ type: bool
+ synchronous:
+ description: Whether action batch job executes synchronously or asynchronously.
+ returned: success
+ type: bool
+ status:
+ description: Information about the action batch job state.
+ type: complex
+ contains:
+ completed:
+ description: Whether job has completed.
+ type: bool
+ returned: success
+ failed:
+ description: Whether execution of action batch job failed.
+ type: bool
+ returned: success
+ errors:
+ description: List of errors, if any, created during execution.
+ type: list
+ returned: success
+ created_resources:
+ description: List of resources created during execution.
+ type: list
+ returned: success
+ sample: [{"id": 100, "uri": "/networks/L_XXXXX/groupPolicies/100"}]
+ actions:
+ description: List of actions associated to job.
+ type: dict
+"""
+
+from ansible.module_utils.basic import AnsibleModule, json
+from ansible.module_utils.common.dict_transformations import snake_dict_to_camel_dict
+from ansible_collections.cisco.meraki.plugins.module_utils.network.meraki.meraki import (
+ MerakiModule,
+ meraki_argument_spec,
+)
+
+
+def _construct_payload(meraki):
+ payload = dict()
+ payload["confirmed"] = meraki.params["confirmed"]
+ payload["synchronous"] = meraki.params["synchronous"]
+ if meraki.params["actions"] is not None: # No payload is specified for an update
+ payload["actions"] = list()
+ for action in meraki.params["actions"]:
+ action_detail = dict()
+ if action["resource"] is not None:
+ action_detail["resource"] = action["resource"]
+ if action["operation"] is not None:
+ action_detail["operation"] = action["operation"]
+ if action["body"] is not None:
+ action_detail["body"] = action["body"]
+ payload["actions"].append(action_detail)
+ return payload
+
+
+def main():
+
+ # define the available arguments/parameters that a user can pass to
+ # the module
+
+ actions_arg_spec = dict(
+ operation=dict(
+ type="str",
+ choices=[
+ "create",
+ "destroy",
+ "update",
+ "claim",
+ "bind",
+ "split",
+ "unbind",
+ "combine",
+ "update_order",
+ "cycle",
+ "swap",
+ "assignSeats",
+ "move",
+ "moveSeats",
+ "renewSeats",
+ ],
+ ),
+ resource=dict(type="str"),
+ body=dict(type="raw"),
+ )
+
+ argument_spec = meraki_argument_spec()
+ argument_spec.update(
+ state=dict(
+ type="str", choices=["present", "query", "absent"], default="present"
+ ),
+ net_name=dict(type="str"),
+ net_id=dict(type="str"),
+ action_batch_id=dict(type="str", default=None),
+ confirmed=dict(type="bool", default=False),
+ synchronous=dict(type="bool", default=True),
+ actions=dict(
+ type="list", default=None, elements="dict", options=actions_arg_spec
+ ),
+ )
+
+ # the AnsibleModule object will be our abstraction working with Ansible
+ # this includes instantiation, a couple of common attr would be the
+ # args/params passed to the execution, as well as if the module
+ # supports check mode
+ module = AnsibleModule(
+ argument_spec=argument_spec,
+ supports_check_mode=True,
+ )
+ meraki = MerakiModule(module, function="action_batch")
+ meraki.params["follow_redirects"] = "all"
+
+ query_urls = {"action_batch": "/organizations/{org_id}/actionBatches"}
+ query_one_urls = {
+ "action_batch": "/organizations/{org_id}/actionBatches/{action_batch_id}"
+ }
+ create_urls = {"action_batch": "/organizations/{org_id}/actionBatches"}
+ update_urls = {
+ "action_batch": "/organizations/{org_id}/actionBatches/{action_batch_id}"
+ }
+ delete_urls = {
+ "action_batch": "/organizations/{org_id}/actionBatches/{action_batch_id}"
+ }
+
+ meraki.url_catalog["get_all"].update(query_urls)
+ meraki.url_catalog["get_one"].update(query_one_urls)
+ meraki.url_catalog["create"] = create_urls
+ meraki.url_catalog["update"] = update_urls
+ meraki.url_catalog["delete"] = delete_urls
+
+ payload = None
+
+ if not meraki.params["org_name"] and not meraki.params["org_id"]:
+ meraki.fail_json(msg="org_name or org_id is required")
+
+ org_id = meraki.params["org_id"]
+ if org_id is None:
+ org_id = meraki.get_org_id(meraki.params["org_name"])
+
+ if meraki.params["state"] == "query":
+ if meraki.params["action_batch_id"] is None: # Get all Action Batches
+ path = meraki.construct_path("get_all", org_id=org_id)
+ response = meraki.request(path, method="GET")
+ if meraki.status == 200:
+ meraki.result["data"] = response
+ meraki.exit_json(**meraki.result)
+ elif meraki.params["action_batch_id"] is not None: # Query one Action Batch job
+ path = meraki.construct_path(
+ "get_one",
+ org_id=org_id,
+ custom={"action_batch_id": meraki.params["action_batch_id"]},
+ )
+ response = meraki.request(path, method="GET")
+ if meraki.status == 200:
+ meraki.result["data"] = response
+ meraki.exit_json(**meraki.result)
+ elif meraki.params["state"] == "present":
+ if meraki.params["action_batch_id"] is None: # Create a new Action Batch job
+ payload = _construct_payload(meraki)
+ path = meraki.construct_path("create", org_id=org_id)
+ response = meraki.request(path, method="POST", payload=json.dumps(payload))
+ if meraki.status == 201:
+ meraki.result["data"] = response
+ meraki.result["changed"] = True
+ meraki.exit_json(**meraki.result)
+ elif meraki.params["action_batch_id"] is not None:
+ path = meraki.construct_path(
+ "get_one",
+ org_id=org_id,
+ custom={"action_batch_id": meraki.params["action_batch_id"]},
+ )
+ current = meraki.request(path, method="GET")
+ payload = _construct_payload(meraki)
+ if (
+ meraki.params["actions"] is not None
+ ): # Cannot update the body once a job is submitted
+ meraki.fail_json(msg="Body cannot be updated on existing job.")
+ if (
+ meraki.is_update_required(current, payload) is True
+ ): # Job needs to be modified
+ path = meraki.construct_path(
+ "update",
+ org_id=org_id,
+ custom={"action_batch_id": meraki.params["action_batch_id"]},
+ )
+ response = meraki.request(
+ path, method="PUT", payload=json.dumps(payload)
+ )
+ if meraki.status == 200:
+ meraki.result["data"] = response
+ meraki.result["changed"] = True
+ meraki.exit_json(**meraki.result)
+ else: # Idempotent response
+ meraki.result["data"] = current
+ meraki.exit_json(**meraki.result)
+ elif meraki.params["state"] == "absent":
+ path = meraki.construct_path(
+ "delete",
+ org_id=org_id,
+ custom={"action_batch_id": meraki.params["action_batch_id"]},
+ )
+ response = meraki.request(path, method="DELETE")
+ if meraki.status == 204:
+ meraki.result["data"] = response
+ meraki.result["changed"] = True
+
+ # in the event of a successful module execution, you will want to
+ # simple AnsibleModule.exit_json(), passing the key/value results
+ meraki.exit_json(**meraki.result)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/ansible_collections/cisco/meraki/plugins/modules/meraki_admin.py b/ansible_collections/cisco/meraki/plugins/modules/meraki_admin.py
new file mode 100644
index 000000000..e554bb009
--- /dev/null
+++ b/ansible_collections/cisco/meraki/plugins/modules/meraki_admin.py
@@ -0,0 +1,504 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Copyright: (c) 2018, Kevin Breit (@kbreit) <kevin.breit@kevinbreit.net>
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+ANSIBLE_METADATA = {
+ 'metadata_version': '1.1',
+ 'status': ['preview'],
+ 'supported_by': 'community'
+}
+
+DOCUMENTATION = r'''
+---
+module: meraki_admin
+short_description: Manage administrators in the Meraki cloud
+version_added: '1.0.0'
+description:
+- Allows for creation, management, and visibility into administrators within Meraki.
+options:
+ name:
+ description:
+ - Name of the dashboard administrator.
+ - Required when creating a new administrator.
+ type: str
+ email:
+ description:
+ - Email address for the dashboard administrator.
+ - Email cannot be updated.
+ - Required when creating or editing an administrator.
+ type: str
+ org_access:
+ description:
+ - Privileges assigned to the administrator in the organization.
+ aliases: [ orgAccess ]
+ choices: [ full, none, read-only ]
+ type: str
+ tags:
+ description:
+ - Tags the administrator has privileges on.
+ - When creating a new administrator, C(org_name), C(network), or C(tags) must be specified.
+ - If C(none) is specified, C(network) or C(tags) must be specified.
+ type: list
+ elements: dict
+ suboptions:
+ tag:
+ description:
+ - Object tag which privileges should be assigned.
+ type: str
+ access:
+ description:
+ - The privilege of the dashboard administrator for the tag.
+ type: str
+ networks:
+ description:
+ - List of networks the administrator has privileges on.
+ - When creating a new administrator, C(org_name), C(network), or C(tags) must be specified.
+ type: list
+ elements: dict
+ suboptions:
+ id:
+ description:
+ - Network ID for which administrator should have privileges assigned.
+ type: str
+ network:
+ description:
+ - Network name for which administrator should have privileges assigned.
+ type: str
+ access:
+ description:
+ - The privilege of the dashboard administrator on the network.
+ - Valid options are C(full), C(read-only), or C(none).
+ type: str
+ state:
+ description:
+ - Create or modify, or delete an organization
+ - If C(state) is C(absent), name takes priority over email if both are specified.
+ choices: [ absent, present, query ]
+ required: true
+ type: str
+ org_name:
+ description:
+ - Name of organization.
+ - Used when C(name) should refer to another object.
+ - When creating a new administrator, C(org_name), C(network), or C(tags) must be specified.
+ aliases: ['organization']
+ type: str
+author:
+ - Kevin Breit (@kbreit)
+extends_documentation_fragment: cisco.meraki.meraki
+'''
+
+EXAMPLES = r'''
+- name: Query information about all administrators associated to the organization
+ meraki_admin:
+ auth_key: abc12345
+ org_name: YourOrg
+ state: query
+ delegate_to: localhost
+
+- name: Query information about a single administrator by name
+ meraki_admin:
+ auth_key: abc12345
+ org_id: 12345
+ state: query
+ name: Jane Doe
+
+- name: Query information about a single administrator by email
+ meraki_admin:
+ auth_key: abc12345
+ org_name: YourOrg
+ state: query
+ email: jane@doe.com
+
+- name: Create new administrator with organization access
+ meraki_admin:
+ auth_key: abc12345
+ org_name: YourOrg
+ state: present
+ name: Jane Doe
+ org_access: read-only
+ email: jane@doe.com
+
+- name: Create new administrator with organization access
+ meraki_admin:
+ auth_key: abc12345
+ org_name: YourOrg
+ state: present
+ name: Jane Doe
+ org_access: read-only
+ email: jane@doe.com
+
+- name: Create a new administrator with organization access
+ meraki_admin:
+ auth_key: abc12345
+ org_name: YourOrg
+ state: present
+ name: Jane Doe
+ org_access: read-only
+ email: jane@doe.com
+
+- name: Revoke access to an organization for an administrator
+ meraki_admin:
+ auth_key: abc12345
+ org_name: YourOrg
+ state: absent
+ email: jane@doe.com
+
+- name: Create a new administrator with full access to two tags
+ meraki_admin:
+ auth_key: abc12345
+ org_name: YourOrg
+ state: present
+ name: Jane Doe
+ orgAccess: read-only
+ email: jane@doe.com
+ tags:
+ - tag: tenant
+ access: full
+ - tag: corporate
+ access: read-only
+
+- name: Create a new administrator with full access to a network
+ meraki_admin:
+ auth_key: abc12345
+ org_name: YourOrg
+ state: present
+ name: Jane Doe
+ orgAccess: read-only
+ email: jane@doe.com
+ networks:
+ - id: N_12345
+ access: full
+'''
+
+RETURN = r'''
+data:
+ description: List of administrators.
+ returned: success
+ type: complex
+ contains:
+ email:
+ description: Email address of administrator.
+ returned: success
+ type: str
+ sample: your@email.com
+ id:
+ description: Unique identification number of administrator.
+ returned: success
+ type: str
+ sample: 1234567890
+ name:
+ description: Given name of administrator.
+ returned: success
+ type: str
+ sample: John Doe
+ account_status:
+ description: Status of account.
+ returned: success
+ type: str
+ sample: ok
+ two_factor_auth_enabled:
+ description: Enabled state of two-factor authentication for administrator.
+ returned: success
+ type: bool
+ sample: false
+ has_api_key:
+ description: Defines whether administrator has an API assigned to their account.
+ returned: success
+ type: bool
+ sample: false
+ last_active:
+ description: Date and time of time the administrator was active within Dashboard.
+ returned: success
+ type: str
+ sample: 2019-01-28 14:58:56 -0800
+ networks:
+ description: List of networks administrator has access on.
+ returned: success
+ type: complex
+ contains:
+ id:
+ description: The network ID.
+ returned: when network permissions are set
+ type: str
+ sample: N_0123456789
+ access:
+ description: Access level of administrator. Options are 'full', 'read-only', or 'none'.
+ returned: when network permissions are set
+ type: str
+ sample: read-only
+ tags:
+ description: Tags the administrator has access on.
+ returned: success
+ type: complex
+ contains:
+ tag:
+ description: Tag name.
+ returned: when tag permissions are set
+ type: str
+ sample: production
+ access:
+ description: Access level of administrator. Options are 'full', 'read-only', or 'none'.
+ returned: when tag permissions are set
+ type: str
+ sample: full
+ org_access:
+ description: The privilege of the dashboard administrator on the organization. Options are 'full', 'read-only', or 'none'.
+ returned: success
+ type: str
+ sample: full
+
+'''
+
+import os
+from ansible.module_utils.basic import AnsibleModule, json
+from ansible_collections.cisco.meraki.plugins.module_utils.network.meraki.meraki import MerakiModule, meraki_argument_spec
+
+
+def get_admins(meraki, org_id):
+ admins = meraki.request(
+ meraki.construct_path(
+ 'query',
+ function='admin',
+ org_id=org_id
+ ),
+ method='GET'
+ )
+ if meraki.status == 200:
+ return admins
+
+
+def get_admin_id(meraki, data, name=None, email=None):
+ admin_id = None
+ for a in data:
+ if meraki.params['name'] is not None:
+ if meraki.params['name'] == a['name']:
+ if admin_id is not None:
+ meraki.fail_json(msg='There are multiple administrators with the same name')
+ else:
+ admin_id = a['id']
+ elif meraki.params['email']:
+ if meraki.params['email'] == a['email']:
+ return a['id']
+ if admin_id is None:
+ meraki.fail_json(msg='No admin_id found')
+ return admin_id
+
+
+def get_admin(meraki, data, id):
+ for a in data:
+ if a['id'] == id:
+ return a
+ meraki.fail_json(msg='No admin found by specified name or email')
+
+
+def find_admin(meraki, data, email):
+ for a in data:
+ if a['email'] == email:
+ return a
+ return None
+
+
+def delete_admin(meraki, org_id, admin_id):
+ path = meraki.construct_path('revoke', 'admin', org_id=org_id) + admin_id
+ r = meraki.request(path,
+ method='DELETE'
+ )
+ if meraki.status == 204:
+ return r
+
+
+def network_factory(meraki, networks, nets):
+ networks_new = []
+ for n in networks:
+ if 'network' in n and n['network'] is not None:
+ networks_new.append({'id': meraki.get_net_id(org_name=meraki.params['org_name'],
+ net_name=n['network'],
+ data=nets),
+ 'access': n['access']
+ })
+ elif 'id' in n:
+ networks_new.append({'id': n['id'],
+ 'access': n['access']
+ })
+
+ return networks_new
+
+
+def create_admin(meraki, org_id, name, email):
+ payload = dict()
+ payload['name'] = name
+ payload['email'] = email
+
+ is_admin_existing = find_admin(meraki, get_admins(meraki, org_id), email)
+
+ if meraki.params['org_access'] is not None:
+ payload['orgAccess'] = meraki.params['org_access']
+ if meraki.params['tags'] is not None:
+ payload['tags'] = meraki.params['tags']
+ if meraki.params['networks'] is not None:
+ nets = meraki.get_nets(org_id=org_id)
+ networks = network_factory(meraki, meraki.params['networks'], nets)
+ payload['networks'] = networks
+ if is_admin_existing is None: # Create new admin
+ if meraki.module.check_mode is True:
+ meraki.result['data'] = payload
+ meraki.result['changed'] = True
+ meraki.exit_json(**meraki.result)
+ path = meraki.construct_path('create', function='admin', org_id=org_id)
+ r = meraki.request(path,
+ method='POST',
+ payload=json.dumps(payload)
+ )
+ if meraki.status == 201:
+ meraki.result['changed'] = True
+ return r
+ elif is_admin_existing is not None: # Update existing admin
+ if not meraki.params['tags']:
+ payload['tags'] = []
+ if not meraki.params['networks']:
+ payload['networks'] = []
+ if meraki.is_update_required(is_admin_existing, payload) is True:
+ if meraki.module.check_mode is True:
+ meraki.generate_diff(is_admin_existing, payload)
+ is_admin_existing.update(payload)
+ meraki.result['changed'] = True
+ meraki.result['data'] = payload
+ meraki.exit_json(**meraki.result)
+ path = meraki.construct_path('update', function='admin', org_id=org_id) + is_admin_existing['id']
+ r = meraki.request(path,
+ method='PUT',
+ payload=json.dumps(payload)
+ )
+ if meraki.status == 200:
+ meraki.result['changed'] = True
+ return r
+ else:
+ meraki.result['data'] = is_admin_existing
+ if meraki.module.check_mode is True:
+ meraki.result['data'] = payload
+ meraki.exit_json(**meraki.result)
+ return -1
+
+
+def main():
+ # define the available arguments/parameters that a user can pass to
+ # the module
+
+ network_arg_spec = dict(id=dict(type='str'),
+ network=dict(type='str'),
+ access=dict(type='str'),
+ )
+
+ tag_arg_spec = dict(tag=dict(type='str'),
+ access=dict(type='str'),
+ )
+
+ argument_spec = meraki_argument_spec()
+ argument_spec.update(state=dict(type='str', choices=['present', 'query', 'absent'], required=True),
+ name=dict(type='str'),
+ email=dict(type='str'),
+ org_access=dict(type='str', aliases=['orgAccess'], choices=['full', 'read-only', 'none']),
+ tags=dict(type='list', elements='dict', options=tag_arg_spec),
+ networks=dict(type='list', elements='dict', options=network_arg_spec),
+ org_name=dict(type='str', aliases=['organization']),
+ org_id=dict(type='str'),
+ )
+
+ # the AnsibleModule object will be our abstraction working with Ansible
+ # this includes instantiation, a couple of common attr would be the
+ # args/params passed to the execution, as well as if the module
+ # supports check mode
+ module = AnsibleModule(argument_spec=argument_spec,
+ supports_check_mode=True,
+ )
+ meraki = MerakiModule(module, function='admin')
+
+ meraki.function = 'admin'
+ meraki.params['follow_redirects'] = 'all'
+
+ query_urls = {'admin': '/organizations/{org_id}/admins',
+ }
+ create_urls = {'admin': '/organizations/{org_id}/admins',
+ }
+ update_urls = {'admin': '/organizations/{org_id}/admins/',
+ }
+ revoke_urls = {'admin': '/organizations/{org_id}/admins/',
+ }
+
+ meraki.url_catalog['query'] = query_urls
+ meraki.url_catalog['create'] = create_urls
+ meraki.url_catalog['update'] = update_urls
+ meraki.url_catalog['revoke'] = revoke_urls
+
+ try:
+ meraki.params['auth_key'] = os.environ['MERAKI_KEY']
+ except KeyError:
+ pass
+
+ # if the user is working with this module in only check mode we do not
+ # want to make any changes to the environment, just return the current
+ # state with no modifications
+
+ # execute checks for argument completeness
+ if meraki.params['state'] == 'query':
+ meraki.mututally_exclusive = ['name', 'email']
+ if not meraki.params['org_name'] and not meraki.params['org_id']:
+ meraki.fail_json(msg='org_name or org_id required')
+ meraki.required_if = [(['state'], ['absent'], ['email']),
+ ]
+
+ # manipulate or modify the state as needed (this is going to be the
+ # part where your module will do what it needs to do)
+ org_id = meraki.params['org_id']
+ if not meraki.params['org_id']:
+ org_id = meraki.get_org_id(meraki.params['org_name'])
+ if meraki.params['state'] == 'query':
+ admins = get_admins(meraki, org_id)
+ if not meraki.params['name'] and not meraki.params['email']: # Return all admins for org
+ meraki.result['data'] = admins
+ if meraki.params['name'] is not None: # Return a single admin for org
+ admin_id = get_admin_id(meraki, admins, name=meraki.params['name'])
+ meraki.result['data'] = admin_id
+ admin = get_admin(meraki, admins, admin_id)
+ meraki.result['data'] = admin
+ elif meraki.params['email'] is not None:
+ admin_id = get_admin_id(meraki, admins, email=meraki.params['email'])
+ meraki.result['data'] = admin_id
+ admin = get_admin(meraki, admins, admin_id)
+ meraki.result['data'] = admin
+ elif meraki.params['state'] == 'present':
+ r = create_admin(meraki,
+ org_id,
+ meraki.params['name'],
+ meraki.params['email'],
+ )
+ if r != -1:
+ meraki.result['data'] = r
+ elif meraki.params['state'] == 'absent':
+ if meraki.module.check_mode is True:
+ meraki.result['data'] = {}
+ meraki.result['changed'] = True
+ meraki.exit_json(**meraki.result)
+ admin_id = get_admin_id(meraki,
+ get_admins(meraki, org_id),
+ email=meraki.params['email']
+ )
+ r = delete_admin(meraki, org_id, admin_id)
+
+ if r != -1:
+ meraki.result['data'] = r
+ meraki.result['changed'] = True
+
+ # in the event of a successful module execution, you will want to
+ # simple AnsibleModule.exit_json(), passing the key/value results
+ meraki.exit_json(**meraki.result)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/ansible_collections/cisco/meraki/plugins/modules/meraki_alert.py b/ansible_collections/cisco/meraki/plugins/modules/meraki_alert.py
new file mode 100644
index 000000000..e9dfe0f12
--- /dev/null
+++ b/ansible_collections/cisco/meraki/plugins/modules/meraki_alert.py
@@ -0,0 +1,395 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Copyright: (c) 2018, 2019 Kevin Breit (@kbreit) <kevin.breit@kevinbreit.net>
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+
+__metaclass__ = type
+
+ANSIBLE_METADATA = {
+ "metadata_version": "1.1",
+ "status": ["preview"],
+ "supported_by": "community",
+}
+
+DOCUMENTATION = r"""
+---
+module: meraki_alert
+version_added: "2.1.0"
+short_description: Manage alerts in the Meraki cloud
+description:
+- Allows for creation, management, and visibility into alert settings within Meraki.
+options:
+ state:
+ description:
+ - Create or modify an alert.
+ choices: [ present, query ]
+ default: present
+ type: str
+ net_name:
+ description:
+ - Name of a network.
+ aliases: [ name, network ]
+ type: str
+ net_id:
+ description:
+ - ID number of a network.
+ type: str
+ default_destinations:
+ description:
+ - Properties for destinations when alert specific destinations aren't specified.
+ type: dict
+ suboptions:
+ all_admins:
+ description:
+ - If true, all network admins will receive emails.
+ type: bool
+ snmp:
+ description:
+ - If true, then an SNMP trap will be sent if there is an SNMP trap server configured for this network.
+ type: bool
+ emails:
+ description:
+ - A list of emails that will recieve the alert(s).
+ type: list
+ elements: str
+ http_server_ids:
+ description:
+ - A list of HTTP server IDs to send a Webhook to.
+ type: list
+ elements: str
+ alerts:
+ description:
+ - Alert-specific configuration for each type.
+ type: list
+ elements: dict
+ suboptions:
+ alert_type:
+ description:
+ - The type of alert.
+ type: str
+ enabled:
+ description:
+ - A boolean depicting if the alert is turned on or off.
+ type: bool
+ filters:
+ description:
+ - A hash of specific configuration data for the alert. Only filters specific to the alert will be updated.
+ - No validation checks occur against C(filters).
+ type: raw
+ default: {}
+ alert_destinations:
+ description:
+ - A hash of destinations for this specific alert.
+ type: dict
+ suboptions:
+ all_admins:
+ description:
+ - If true, all network admins will receive emails.
+ type: bool
+ snmp:
+ description:
+ - If true, then an SNMP trap will be sent if there is an SNMP trap server configured for this network.
+ type: bool
+ emails:
+ description:
+ - A list of emails that will recieve the alert(s).
+ type: list
+ elements: str
+ http_server_ids:
+ description:
+ - A list of HTTP server IDs to send a Webhook to.
+ type: list
+ elements: str
+
+author:
+ - Kevin Breit (@kbreit)
+extends_documentation_fragment: cisco.meraki.meraki
+"""
+
+EXAMPLES = r"""
+- name: Update settings
+ meraki_alert:
+ auth_key: abc123
+ org_name: YourOrg
+ net_name: YourNet
+ state: present
+ default_destinations:
+ emails:
+ - 'youremail@yourcorp'
+ - 'youremail2@yourcorp'
+ all_admins: yes
+ snmp: no
+ alerts:
+ - alert_type: "gatewayDown"
+ enabled: yes
+ filters:
+ timeout: 60
+ alert_destinations:
+ emails:
+ - 'youremail@yourcorp'
+ - 'youremail2@yourcorp'
+ all_admins: yes
+ snmp: no
+ - alert_type: "usageAlert"
+ enabled: yes
+ filters:
+ period: 1200
+ threshold: 104857600
+ alert_destinations:
+ emails:
+ - 'youremail@yourcorp'
+ - 'youremail2@yourcorp'
+ all_admins: yes
+ snmp: no
+
+- name: Query all settings
+ meraki_alert:
+ auth_key: abc123
+ org_name: YourOrg
+ net_name: YourNet
+ state: query
+ delegate_to: localhost
+"""
+
+RETURN = r"""
+data:
+ description: Information about the created or manipulated object.
+ returned: info
+ type: complex
+ contains:
+ default_destinations:
+ description: Properties for destinations when alert specific destinations aren't specified.
+ returned: success
+ type: complex
+ contains:
+ all_admins:
+ description: If true, all network admins will receive emails.
+ type: bool
+ sample: true
+ returned: success
+ snmp:
+ description: If true, then an SNMP trap will be sent if there is an SNMP trap server configured for this network.
+ type: bool
+ sample: true
+ returned: success
+ emails:
+ description: A list of emails that will recieve the alert(s).
+ type: list
+ returned: success
+ http_server_ids:
+ description: A list of HTTP server IDs to send a Webhook to.
+ type: list
+ returned: success
+ alerts:
+ description: Alert-specific configuration for each type.
+ type: complex
+ contains:
+ alert_type:
+ description: The type of alert.
+ type: str
+ returned: success
+ enabled:
+ description: A boolean depicting if the alert is turned on or off.
+ type: bool
+ returned: success
+ filters:
+ description:
+ - A hash of specific configuration data for the alert. Only filters specific to the alert will be updated.
+ - No validation checks occur against C(filters).
+ type: complex
+ returned: success
+ alert_destinations:
+ description: A hash of destinations for this specific alert.
+ type: complex
+ contains:
+ all_admins:
+ description: If true, all network admins will receive emails.
+ type: bool
+ returned: success
+ snmp:
+ description: If true, then an SNMP trap will be sent if there is an SNMP trap server configured for this network.
+ type: bool
+ returned: success
+ emails:
+ description: A list of emails that will recieve the alert(s).
+ type: list
+ returned: success
+ http_server_ids:
+ description: A list of HTTP server IDs to send a Webhook to.
+ type: list
+ returned: success
+"""
+
+import copy
+from ansible.module_utils.basic import AnsibleModule, json
+from ansible_collections.cisco.meraki.plugins.module_utils.network.meraki.meraki import (
+ MerakiModule,
+ meraki_argument_spec,
+)
+
+
+def get_alert_by_type(type, meraki):
+ for alert in meraki.params["alerts"]:
+ if alert["alert_type"] == type:
+ return alert
+ return None
+
+
+def construct_payload(meraki, current):
+ payload = {}
+ if meraki.params["default_destinations"] is not None:
+ payload["defaultDestinations"] = {}
+ if meraki.params["default_destinations"]["all_admins"] is not None:
+ payload["defaultDestinations"]["allAdmins"] = meraki.params["default_destinations"]["all_admins"]
+ if meraki.params["default_destinations"]["snmp"] is not None:
+ payload["defaultDestinations"]["snmp"] = meraki.params["default_destinations"]["snmp"]
+ if meraki.params["default_destinations"]["emails"] is not None:
+ payload["defaultDestinations"]["emails"] = meraki.params["default_destinations"]["emails"]
+ if len(payload["defaultDestinations"]["emails"]) > 0 and payload["defaultDestinations"]["emails"][0] == "None":
+ # Ansible is setting the first item to be "None" so we need to clear this
+ # This happens when an empty list is provided to clear emails
+ del payload["defaultDestinations"]["emails"][0]
+ if meraki.params["default_destinations"]["http_server_ids"] is not None:
+ payload["defaultDestinations"]["httpServerIds"] = meraki.params["default_destinations"]["http_server_ids"]
+ if len(payload["defaultDestinations"]["httpServerIds"]) > 0 and payload["defaultDestinations"]["httpServerIds"][0] == "None":
+ # Ansible is setting the first item to be "None" so we need to clear this
+ # This happens when an empty list is provided to clear server IDs
+ del payload["defaultDestinations"]["httpServerIds"][0]
+ if meraki.params["alerts"] is not None:
+ payload["alerts"] = []
+ # All data should be resubmitted, otherwise it will clear the alert
+ # Also, the order matters so it should go in the same order as current
+ modified_types = [type["alert_type"] for type in meraki.params["alerts"]]
+
+ # for alert in meraki.params["alerts"]:
+ for current_alert in current["alerts"]:
+ if current_alert["type"] not in modified_types:
+ payload["alerts"].append(current_alert)
+ else:
+ alert = get_alert_by_type(current_alert["type"], meraki)
+ alert_temp = {"type": None}
+ if alert["alert_type"] is not None:
+ alert_temp["type"] = alert["alert_type"]
+ if alert["enabled"] is not None:
+ alert_temp["enabled"] = alert["enabled"]
+ if alert["filters"] is not None:
+ alert_temp["filters"] = alert["filters"]
+ if alert["alert_destinations"] is not None:
+ alert_temp["alertDestinations"] = dict()
+ if alert["alert_destinations"]["all_admins"] is not None:
+ alert_temp["alertDestinations"]["allAdmins"] = alert["alert_destinations"]["all_admins"]
+ if alert["alert_destinations"]["snmp"] is not None:
+ alert_temp["alertDestinations"]["snmp"] = alert["alert_destinations"]["snmp"]
+ if alert["alert_destinations"]["emails"] is not None:
+ alert_temp["alertDestinations"]["emails"] = alert["alert_destinations"]["emails"]
+ if len(alert_temp["alertDestinations"]["emails"]) > 0 and alert_temp["alertDestinations"]["emails"][0] == "None":
+ # Ansible is setting the first item to be "None" so we need to clear this
+ # This happens when an empty list is provided to clear emails
+ del alert_temp["defaultDestinations"]["emails"][0]
+ if alert["alert_destinations"]["http_server_ids"] is not None:
+ alert_temp["alertDestinations"]["httpServerIds"] = alert["alert_destinations"]["http_server_ids"]
+ if len(alert_temp["alertDestinations"]["httpServerIds"]) > 0 and alert_temp["alertDestinations"]["httpServerIds"][0] == "None":
+ # Ansible is setting the first item to be "None" so we need to clear this
+ # This happens when an empty list is provided to clear server IDs
+ del alert_temp["defaultDestinations"]["httpServerIds"][0]
+ payload["alerts"].append(alert_temp)
+ return payload
+
+
+def main():
+
+ # define the available arguments/parameters that a user can pass to
+ # the module
+
+ destinations_arg_spec = dict(
+ all_admins=dict(type="bool"),
+ snmp=dict(type="bool"),
+ emails=dict(type="list", elements="str"),
+ http_server_ids=dict(type="list", elements="str"),
+ )
+
+ alerts_arg_spec = dict(
+ alert_type=dict(type="str"),
+ enabled=dict(type="bool"),
+ alert_destinations=dict(
+ type="dict", default=None, options=destinations_arg_spec
+ ),
+ filters=dict(type="raw", default={}),
+ )
+
+ argument_spec = meraki_argument_spec()
+ argument_spec.update(
+ net_id=dict(type="str"),
+ net_name=dict(type="str", aliases=["name", "network"]),
+ state=dict(type="str", choices=["present", "query"], default="present"),
+ default_destinations=dict(
+ type="dict", default=None, options=destinations_arg_spec
+ ),
+ alerts=dict(type="list", elements="dict", options=alerts_arg_spec),
+ )
+
+ # the AnsibleModule object will be our abstraction working with Ansible
+ # this includes instantiation, a couple of common attr would be the
+ # args/params passed to the execution, as well as if the module
+ # supports check mode
+ module = AnsibleModule(
+ argument_spec=argument_spec,
+ supports_check_mode=True,
+ )
+
+ meraki = MerakiModule(module, function="alert")
+ module.params["follow_redirects"] = "all"
+
+ query_urls = {"alert": "/networks/{net_id}/alerts/settings"}
+ update_urls = {"alert": "/networks/{net_id}/alerts/settings"}
+ meraki.url_catalog["get_all"].update(query_urls)
+ meraki.url_catalog["update"] = update_urls
+
+ # manipulate or modify the state as needed (this is going to be the
+ # part where your module will do what it needs to do)
+
+ org_id = meraki.params["org_id"]
+ if org_id is None:
+ org_id = meraki.get_org_id(meraki.params["org_name"])
+ net_id = meraki.params["net_id"]
+ if net_id is None:
+ nets = meraki.get_nets(org_id=org_id)
+ net_id = meraki.get_net_id(net_name=meraki.params["net_name"], data=nets)
+
+ if meraki.params["state"] == "query":
+ path = meraki.construct_path("get_all", net_id=net_id)
+ response = meraki.request(path, method="GET")
+ if meraki.status == 200:
+ meraki.result["data"] = response
+ meraki.exit_json(**meraki.result)
+ elif meraki.params["state"] == "present":
+ path = meraki.construct_path("get_all", net_id=net_id)
+ original = meraki.request(path, method="GET")
+ payload = construct_payload(meraki, original)
+ if meraki.is_update_required(original, payload):
+ if meraki.check_mode is True:
+ meraki.generate_diff(original, payload)
+ meraki.result["data"] = payload
+ meraki.result["changed"] = True
+ meraki.exit_json(**meraki.result)
+ path = meraki.construct_path("update", net_id=net_id)
+ response = meraki.request(path, method="PUT", payload=json.dumps(payload))
+ if meraki.status == 200:
+ meraki.generate_diff(original, payload)
+ meraki.result["data"] = response
+ meraki.result["changed"] = True
+ meraki.exit_json(**meraki.result)
+ else:
+ meraki.result["data"] = original
+ meraki.exit_json(**meraki.result)
+
+ # in the event of a successful module execution, you will want to
+ # simple AnsibleModule.exit_json(), passing the key/value results
+ meraki.exit_json(**meraki.result)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/ansible_collections/cisco/meraki/plugins/modules/meraki_config_template.py b/ansible_collections/cisco/meraki/plugins/modules/meraki_config_template.py
new file mode 100644
index 000000000..8438d41b2
--- /dev/null
+++ b/ansible_collections/cisco/meraki/plugins/modules/meraki_config_template.py
@@ -0,0 +1,331 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Copyright: (c) 2018, Kevin Breit (@kbreit) <kevin.breit@kevinbreit.net>
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+ANSIBLE_METADATA = {
+ 'metadata_version': '1.1',
+ 'status': ['preview'],
+ 'supported_by': 'community'
+}
+
+DOCUMENTATION = r'''
+---
+module: meraki_config_template
+short_description: Manage configuration templates in the Meraki cloud
+version_added: "1.0.0"
+description:
+- Allows for querying, deleting, binding, and unbinding of configuration templates.
+notes:
+- Module is not idempotent as the Meraki API is limited in what information it provides about configuration templates.
+- Meraki's API does not support creating new configuration templates.
+- To use the configuration template, simply pass its ID via C(net_id) parameters in Meraki modules.
+options:
+ state:
+ description:
+ - Specifies whether configuration template information should be queried, modified, or deleted.
+ choices: ['absent', 'query', 'present']
+ default: query
+ type: str
+ org_name:
+ description:
+ - Name of organization containing the configuration template.
+ type: str
+ org_id:
+ description:
+ - ID of organization associated to a configuration template.
+ type: str
+ config_template:
+ description:
+ - Name of the configuration template within an organization to manipulate.
+ aliases: ['name']
+ type: str
+ net_name:
+ description:
+ - Name of the network to bind or unbind configuration template to.
+ type: str
+ net_id:
+ description:
+ - ID of the network to bind or unbind configuration template to.
+ type: str
+ auto_bind:
+ description:
+ - Optional boolean indicating whether the network's switches should automatically bind to profiles of the same model.
+ - This option only affects switch networks and switch templates.
+ - Auto-bind is not valid unless the switch template has at least one profile and has at most one profile per switch model.
+ type: bool
+
+author:
+- Kevin Breit (@kbreit)
+extends_documentation_fragment: cisco.meraki.meraki
+'''
+
+EXAMPLES = r'''
+- name: Query configuration templates
+ meraki_config_template:
+ auth_key: abc12345
+ org_name: YourOrg
+ state: query
+ delegate_to: localhost
+
+- name: Bind a template from a network
+ meraki_config_template:
+ auth_key: abc123
+ state: present
+ org_name: YourOrg
+ net_name: YourNet
+ config_template: DevConfigTemplate
+ delegate_to: localhost
+
+- name: Unbind a template from a network
+ meraki_config_template:
+ auth_key: abc123
+ state: absent
+ org_name: YourOrg
+ net_name: YourNet
+ config_template: DevConfigTemplate
+ delegate_to: localhost
+
+- name: Delete a configuration template
+ meraki_config_template:
+ auth_key: abc123
+ state: absent
+ org_name: YourOrg
+ config_template: DevConfigTemplate
+ delegate_to: localhost
+'''
+
+RETURN = r'''
+data:
+ description: Information about queried object.
+ returned: success
+ type: complex
+ contains:
+ id:
+ description: Unique identification number of organization.
+ returned: success
+ type: int
+ sample: L_2930418
+ name:
+ description: Name of configuration template.
+ returned: success
+ type: str
+ sample: YourTemplate
+ product_types:
+ description: List of products which can exist in the network.
+ returned: success
+ type: list
+ sample: [ "appliance", "switch" ]
+ time_zone:
+ description: Timezone applied to each associated network.
+ returned: success
+ type: str
+ sample: "America/Chicago"
+
+'''
+
+from ansible.module_utils.basic import AnsibleModule, json
+from ansible_collections.cisco.meraki.plugins.module_utils.network.meraki.meraki import MerakiModule, meraki_argument_spec
+
+
+def get_config_templates(meraki, org_id):
+ path = meraki.construct_path('get_all', org_id=org_id)
+ response = meraki.request(path, 'GET')
+ if meraki.status != 200:
+ meraki.fail_json(msg='Unable to get configuration templates')
+ return response
+
+
+def get_template_id(meraki, name, data):
+ for template in data:
+ if name == template['name']:
+ return template['id']
+ meraki.fail_json(msg='No configuration template named {0} found'.format(name))
+
+
+def is_template_valid(meraki, nets, template_id):
+ for net in nets:
+ if net['id'] == template_id:
+ return True
+ return False
+
+
+def is_network_bound(meraki, nets, net_id, template_id):
+ for net in nets:
+ if net['id'] == net_id:
+ try:
+ if net['configTemplateId'] == template_id:
+ return True
+ except KeyError:
+ pass
+ return False
+
+
+def delete_template(meraki, org_id, name, data):
+ template_id = get_template_id(meraki, name, data)
+ path = meraki.construct_path('delete', org_id=org_id)
+ path = path + '/' + template_id
+ response = meraki.request(path, 'DELETE')
+ if meraki.status != 204:
+ meraki.fail_json(msg='Unable to remove configuration template')
+ return response
+
+
+def bind(meraki, net_id, template_id):
+ path = meraki.construct_path('bind', net_id=net_id)
+ payload = {'configTemplateId': template_id}
+ if meraki.params['auto_bind']:
+ payload['autoBind'] = meraki.params['auto_bind']
+ r = meraki.request(path, method='POST', payload=json.dumps(payload))
+ return r
+
+
+def unbind(meraki, net_id):
+ path = meraki.construct_path('unbind', net_id=net_id)
+ meraki.result['changed'] = True
+ return meraki.request(path, method='POST')
+
+
+def main():
+
+ # define the available arguments/parameters that a user can pass to
+ # the module
+ argument_spec = meraki_argument_spec()
+ argument_spec.update(state=dict(type='str', choices=['absent', 'query', 'present'], default='query'),
+ config_template=dict(type='str', aliases=['name']),
+ net_name=dict(type='str'),
+ net_id=dict(type='str'),
+ # config_template_id=dict(type='str', aliases=['id']),
+ auto_bind=dict(type='bool'),
+ )
+
+ # the AnsibleModule object will be our abstraction working with Ansible
+ # this includes instantiation, a couple of common attr would be the
+ # args/params passed to the execution, as well as if the module
+ # supports check mode
+ module = AnsibleModule(argument_spec=argument_spec,
+ supports_check_mode=True,
+ )
+ meraki = MerakiModule(module, function='config_template')
+ meraki.params['follow_redirects'] = 'all'
+
+ query_urls = {'config_template': '/organizations/{org_id}/configTemplates'}
+ delete_urls = {'config_template': '/organizations/{org_id}/configTemplates'}
+ bind_urls = {'config_template': '/networks/{net_id}/bind'}
+ unbind_urls = {'config_template': '/networks/{net_id}/unbind'}
+
+ meraki.url_catalog['get_all'].update(query_urls)
+ meraki.url_catalog['delete'] = delete_urls
+ meraki.url_catalog['bind'] = bind_urls
+ meraki.url_catalog['unbind'] = unbind_urls
+
+ # if the user is working with this module in only check mode we do not
+ # want to make any changes to the environment, just return the current
+ # state with no modifications
+
+ # execute checks for argument completeness
+
+ # manipulate or modify the state as needed (this is going to be the
+ # part where your module will do what it needs to do)
+ org_id = meraki.params['org_id']
+ if meraki.params['org_name']:
+ org_id = meraki.get_org_id(meraki.params['org_name'])
+ net_id = meraki.params['net_id']
+ nets = None
+ if net_id is None:
+ if meraki.params['net_name'] is not None:
+ nets = meraki.get_nets(org_id=org_id)
+ net_id = meraki.get_net_id(net_name=meraki.params['net_name'], data=nets)
+ else:
+ nets = meraki.get_nets(org_id=org_id)
+
+ if meraki.params['state'] == 'query':
+ meraki.result['data'] = get_config_templates(meraki, org_id)
+ elif meraki.params['state'] == 'present':
+ template_id = get_template_id(meraki,
+ meraki.params['config_template'],
+ get_config_templates(meraki, org_id))
+ if nets is None:
+ nets = meraki.get_nets(org_id=org_id)
+ if is_network_bound(meraki, nets, net_id, template_id) is False: # Bind template
+ if meraki.check_mode is True:
+ meraki.result['data'] = {}
+ meraki.result['changed'] = True
+ meraki.exit_json(**meraki.result)
+ template_bind = bind(meraki,
+ net_id,
+ template_id)
+ if meraki.status != 200:
+ meraki.fail_json(msg='Unable to bind configuration template to network')
+ meraki.result['changed'] = True
+ meraki.result['data'] = template_bind
+ else: # Network is already bound, being explicit
+ if meraki.check_mode is True: # Include to be explicit
+ meraki.result['data'] = {}
+ meraki.result['changed'] = False
+ meraki.exit_json(**meraki.result)
+ meraki.result['data'] = {}
+ meraki.result['changed'] = False
+ meraki.exit_json(**meraki.result)
+ elif meraki.params['state'] == 'absent':
+ template_id = get_template_id(meraki,
+ meraki.params['config_template'],
+ get_config_templates(meraki, org_id))
+ if not meraki.params['net_name'] and not meraki.params['net_id']: # Delete template
+ if is_template_valid(meraki, nets, template_id) is True:
+ if meraki.check_mode is True:
+ meraki.result['data'] = {}
+ meraki.result['changed'] = True
+ meraki.exit_json(**meraki.result)
+ meraki.result['data'] = delete_template(meraki,
+ org_id,
+ meraki.params['config_template'],
+ get_config_templates(meraki, org_id))
+ if meraki.status == 204:
+ meraki.result['data'] = {}
+ meraki.result['changed'] = True
+ else:
+ meraki.fail_json(msg="No template named {0} found.".format(meraki.params['config_template']))
+ else: # Unbind template
+ if nets is None:
+ nets = meraki.get_nets(org_id=org_id)
+ if meraki.check_mode is True:
+ meraki.result['data'] = {}
+ if is_template_valid(meraki, nets, template_id) is True:
+ meraki.result['changed'] = True
+ else:
+ meraki.result['changed'] = False
+ meraki.exit_json(**meraki.result)
+ template_id = get_template_id(meraki,
+ meraki.params['config_template'],
+ get_config_templates(meraki, org_id))
+ if is_network_bound(meraki, nets, net_id, template_id) is True:
+ if meraki.check_mode is True:
+ meraki.result['data'] = {}
+ meraki.result['changed'] = True
+ meraki.exit_json(**meraki.result)
+ config_unbind = unbind(meraki,
+ net_id)
+ if meraki.status != 200:
+ meraki.fail_json(msg='Unable to unbind configuration template from network')
+ meraki.result['changed'] = True
+ meraki.result['data'] = config_unbind
+ else: # No network is bound, nothing to do
+ if meraki.check_mode is True: # Include to be explicit
+ meraki.result['data'] = {}
+ meraki.result['changed'] = False
+ meraki.exit_json(**meraki.result)
+ meraki.result['data'] = {}
+ meraki.result['changed'] = False
+
+ # in the event of a successful module execution, you will want to
+ # simple AnsibleModule.exit_json(), passing the key/value results
+ meraki.exit_json(**meraki.result)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/ansible_collections/cisco/meraki/plugins/modules/meraki_device.py b/ansible_collections/cisco/meraki/plugins/modules/meraki_device.py
new file mode 100644
index 000000000..2152e4949
--- /dev/null
+++ b/ansible_collections/cisco/meraki/plugins/modules/meraki_device.py
@@ -0,0 +1,431 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Copyright: (c) 2018, Kevin Breit (@kbreit) <kevin.breit@kevinbreit.net>
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+ANSIBLE_METADATA = {
+ 'metadata_version': '1.1',
+ 'status': ['preview'],
+ 'supported_by': 'community'
+}
+
+DOCUMENTATION = r'''
+---
+module: meraki_device
+short_description: Manage devices in the Meraki cloud
+description:
+- Visibility into devices associated to a Meraki environment.
+notes:
+- This module does not support claiming of devices or licenses into a Meraki organization.
+- More information about the Meraki API can be found at U(https://dashboard.meraki.com/api_docs).
+- Some of the options are likely only used for developers within Meraki.
+options:
+ state:
+ description:
+ - Query an organization.
+ choices: [absent, present, query]
+ default: query
+ type: str
+ net_name:
+ description:
+ - Name of a network.
+ aliases: [network]
+ type: str
+ net_id:
+ description:
+ - ID of a network.
+ type: str
+ serial:
+ description:
+ - Serial number of a device to query.
+ type: str
+ hostname:
+ description:
+ - Hostname of network device to search for.
+ aliases: [name]
+ type: str
+ model:
+ description:
+ - Model of network device to search for.
+ type: str
+ tags:
+ description:
+ - Space delimited list of tags to assign to device.
+ type: list
+ elements: str
+ lat:
+ description:
+ - Latitude of device's geographic location.
+ - Use negative number for southern hemisphere.
+ aliases: [latitude]
+ type: float
+ lng:
+ description:
+ - Longitude of device's geographic location.
+ - Use negative number for western hemisphere.
+ aliases: [longitude]
+ type: float
+ address:
+ description:
+ - Postal address of device's location.
+ type: str
+ move_map_marker:
+ description:
+ - Whether or not to set the latitude and longitude of a device based on the new address.
+ - Only applies when C(lat) and C(lng) are not specified.
+ type: bool
+ lldp_cdp_timespan:
+ description:
+ - Timespan, in seconds, used to query LLDP and CDP information.
+ - Must be less than 1 month.
+ type: int
+ note:
+ description:
+ - Informational notes about a device.
+ - Limited to 255 characters.
+ type: str
+ query:
+ description:
+ - Specifies what information should be queried.
+ type: str
+ choices: [lldp_cdp, uplink]
+
+
+author:
+- Kevin Breit (@kbreit)
+extends_documentation_fragment: cisco.meraki.meraki
+'''
+
+EXAMPLES = r'''
+- name: Query all devices in an organization.
+ meraki_device:
+ auth_key: abc12345
+ org_name: YourOrg
+ state: query
+ delegate_to: localhost
+
+- name: Query all devices in a network.
+ meraki_device:
+ auth_key: abc12345
+ org_name: YourOrg
+ net_name: YourNet
+ state: query
+ delegate_to: localhost
+
+- name: Query a device by serial number.
+ meraki_device:
+ auth_key: abc12345
+ org_name: YourOrg
+ net_name: YourNet
+ serial: ABC-123
+ state: query
+ delegate_to: localhost
+
+- name: Lookup uplink information about a device.
+ meraki_device:
+ auth_key: abc12345
+ org_name: YourOrg
+ net_name: YourNet
+ serial_uplink: ABC-123
+ state: query
+ delegate_to: localhost
+
+- name: Lookup LLDP and CDP information about devices connected to specified device.
+ meraki_device:
+ auth_key: abc12345
+ org_name: YourOrg
+ net_name: YourNet
+ serial_lldp_cdp: ABC-123
+ state: query
+ delegate_to: localhost
+
+- name: Lookup a device by hostname.
+ meraki_device:
+ auth_key: abc12345
+ org_name: YourOrg
+ net_name: YourNet
+ hostname: main-switch
+ state: query
+ delegate_to: localhost
+
+- name: Query all devices of a specific model.
+ meraki_device:
+ auth_key: abc123
+ org_name: YourOrg
+ net_name: YourNet
+ model: MR26
+ state: query
+ delegate_to: localhost
+
+- name: Update information about a device.
+ meraki_device:
+ auth_key: abc123
+ org_name: YourOrg
+ net_name: YourNet
+ state: present
+ serial: '{{serial}}'
+ name: mr26
+ address: 1060 W. Addison St., Chicago, IL
+ lat: 41.948038
+ lng: -87.65568
+ tags: recently-added
+ delegate_to: localhost
+
+- name: Claim a device into a network.
+ meraki_device:
+ auth_key: abc123
+ org_name: YourOrg
+ net_name: YourNet
+ serial: ABC-123
+ state: present
+ delegate_to: localhost
+
+- name: Remove a device from a network.
+ meraki_device:
+ auth_key: abc123
+ org_name: YourOrg
+ net_name: YourNet
+ serial: ABC-123
+ state: absent
+ delegate_to: localhost
+'''
+
+RETURN = r'''
+response:
+ description: Data returned from Meraki dashboard.
+ type: dict
+ returned: info
+'''
+
+from ansible.module_utils.basic import AnsibleModule, json
+from ansible_collections.cisco.meraki.plugins.module_utils.network.meraki.meraki import MerakiModule, meraki_argument_spec
+
+
+def is_device_valid(meraki, serial, data):
+ """ Parse a list of devices for a serial and return True if it's in the list """
+ for device in data:
+ if device['serial'] == serial:
+ return True
+ return False
+
+
+def get_org_devices(meraki, org_id):
+ """ Get all devices in an organization """
+ path = meraki.construct_path('get_all_org', org_id=org_id)
+ response = meraki.request(path, method='GET')
+ if meraki.status != 200:
+ meraki.fail_json(msg='Failed to query all devices belonging to the organization')
+ return response
+
+
+def get_net_devices(meraki, net_id):
+ """ Get all devices in a network """
+ path = meraki.construct_path('get_all', net_id=net_id)
+ response = meraki.request(path, method='GET')
+ if meraki.status != 200:
+ meraki.fail_json(msg='Failed to query all devices belonging to the network')
+ return response
+
+
+def construct_payload(params):
+ """ Create payload based on inputs """
+ payload = {}
+ if params['hostname'] is not None:
+ payload['name'] = params['hostname']
+ if params['tags'] is not None:
+ payload['tags'] = params['tags']
+ if params['lat'] is not None:
+ payload['lat'] = params['lat']
+ if params['lng'] is not None:
+ payload['lng'] = params['lng']
+ if params['address'] is not None:
+ payload['address'] = params['address']
+ if params['move_map_marker'] is not None:
+ payload['moveMapMarker'] = params['move_map_marker']
+ if params['note'] is not None:
+ payload['notes'] = params['note']
+ return payload
+
+
+def main():
+
+ # define the available arguments/parameters that a user can pass to
+ # the module
+ argument_spec = meraki_argument_spec()
+ argument_spec.update(state=dict(type='str', choices=['absent', 'present', 'query'], default='query'),
+ net_name=dict(type='str', aliases=['network']),
+ net_id=dict(type='str'),
+ serial=dict(type='str'),
+ lldp_cdp_timespan=dict(type='int'),
+ hostname=dict(type='str', aliases=['name']),
+ model=dict(type='str'),
+ tags=dict(type='list', elements='str', default=None),
+ lat=dict(type='float', aliases=['latitude'], default=None),
+ lng=dict(type='float', aliases=['longitude'], default=None),
+ address=dict(type='str', default=None),
+ move_map_marker=dict(type='bool', default=None),
+ note=dict(type='str', default=None),
+ query=dict(type='str', default=None, choices=['lldp_cdp', 'uplink'])
+ )
+
+ # the AnsibleModule object will be our abstraction working with Ansible
+ # this includes instantiation, a couple of common attr would be the
+ # args/params passed to the execution, as well as if the module
+ # supports check mode
+ module = AnsibleModule(argument_spec=argument_spec,
+ supports_check_mode=False,
+ )
+ meraki = MerakiModule(module, function='device')
+
+ if meraki.params['query'] is not None \
+ and meraki.params['query'] == 'lldp_cdp' \
+ and not meraki.params['lldp_cdp_timespan']:
+ meraki.fail_json(msg='lldp_cdp_timespan is required when querying LLDP and CDP information')
+ if meraki.params['net_name'] and meraki.params['net_id']:
+ meraki.fail_json(msg='net_name and net_id are mutually exclusive')
+
+ meraki.params['follow_redirects'] = 'all'
+
+ query_urls = {'device': '/networks/{net_id}/devices'}
+ query_org_urls = {'device': '/organizations/{org_id}/devices'}
+ query_device_urls = {'device': '/networks/{net_id}/devices/{serial}'}
+ query_device_lldp_urls = {'device': '/devices/{serial}/lldpCdp'}
+ claim_device_urls = {'device': '/networks/{net_id}/devices/claim'}
+ bind_org_urls = {'device': '/organizations/{org_id}/claim'}
+ update_device_urls = {'device': '/networks/{net_id}/devices/'}
+ delete_device_urls = {'device': '/networks/{net_id}/devices/{serial}/remove'}
+
+ meraki.url_catalog['get_all'].update(query_urls)
+ meraki.url_catalog['get_all_org'] = query_org_urls
+ meraki.url_catalog['get_device'] = query_device_urls
+ meraki.url_catalog['get_device_uplink'] = query_device_urls
+ meraki.url_catalog['get_device_lldp'] = query_device_lldp_urls
+ meraki.url_catalog['create'] = claim_device_urls
+ meraki.url_catalog['bind_org'] = bind_org_urls
+ meraki.url_catalog['update'] = update_device_urls
+ meraki.url_catalog['delete'] = delete_device_urls
+
+ payload = None
+
+ # execute checks for argument completeness
+
+ # manipulate or modify the state as needed (this is going to be the
+ # part where your module will do what it needs to do)
+ org_id = meraki.params['org_id']
+ if org_id is None:
+ org_id = meraki.get_org_id(meraki.params['org_name'])
+ nets = meraki.get_nets(org_id=org_id)
+ net_id = None
+ if meraki.params['net_id'] or meraki.params['net_name']:
+ net_id = meraki.params['net_id']
+ if net_id is None:
+ net_id = meraki.get_net_id(net_name=meraki.params['net_name'], data=nets)
+
+ if meraki.params['state'] == 'query':
+ if meraki.params['net_name'] or meraki.params['net_id']:
+ device = []
+ if meraki.params['serial']:
+ path = meraki.construct_path('get_device', net_id=net_id, custom={'serial': meraki.params['serial']})
+ request = meraki.request(path, method='GET')
+ device.append(request)
+ meraki.result['data'] = device
+ if meraki.params['query'] == 'uplink':
+ path = meraki.construct_path('get_device_uplink', net_id=net_id, custom={'serial': meraki.params['serial']})
+ meraki.result['data'] = (meraki.request(path, method='GET'))
+ elif meraki.params['query'] == 'lldp_cdp':
+ if meraki.params['lldp_cdp_timespan'] > 2592000:
+ meraki.fail_json(msg='LLDP/CDP timespan must be less than a month (2592000 seconds)')
+ path = meraki.construct_path('get_device_lldp', net_id=net_id, custom={'serial': meraki.params['serial']})
+ path = path + '?timespan=' + str(meraki.params['lldp_cdp_timespan'])
+ device.append(meraki.request(path, method='GET'))
+ meraki.result['data'] = device
+ elif meraki.params['hostname']:
+ path = meraki.construct_path('get_all', net_id=net_id)
+ devices = meraki.request(path, method='GET')
+ for unit in devices:
+ try:
+ if unit['name'] == meraki.params['hostname']:
+ device.append(unit)
+ meraki.result['data'] = device
+ except KeyError:
+ pass
+ elif meraki.params['model']:
+ path = meraki.construct_path('get_all', net_id=net_id)
+ devices = meraki.request(path, method='GET')
+ device_match = []
+ for device in devices:
+ if device['model'] == meraki.params['model']:
+ device_match.append(device)
+ meraki.result['data'] = device_match
+ else:
+ path = meraki.construct_path('get_all', net_id=net_id)
+ request = meraki.request(path, method='GET')
+ meraki.result['data'] = request
+ else:
+ path = meraki.construct_path('get_all_org', org_id=org_id, params={'perPage': '1000'})
+ devices = meraki.request(path, method='GET', pagination_items=1000)
+ if meraki.params['serial']:
+ for device in devices:
+ if device['serial'] == meraki.params['serial']:
+ meraki.result['data'] = device
+ else:
+ meraki.result['data'] = devices
+ elif meraki.params['state'] == 'present':
+ device = []
+ if net_id is None: # Claim a device to an organization
+ device_list = get_org_devices(meraki, org_id)
+ if is_device_valid(meraki, meraki.params['serial'], device_list) is False:
+ payload = {'serial': meraki.params['serial']}
+ path = meraki.construct_path('bind_org', org_id=org_id)
+ created_device = []
+ created_device.append(meraki.request(path, method='POST', payload=json.dumps(payload)))
+ meraki.result['data'] = created_device
+ meraki.result['changed'] = True
+ else: # A device is assumed to be in an organization
+ device_list = get_net_devices(meraki, net_id)
+ if is_device_valid(meraki, meraki.params['serial'], device_list) is True: # Device is in network, update
+ query_path = meraki.construct_path('get_all', net_id=net_id)
+ if is_device_valid(meraki, meraki.params['serial'], device_list):
+ payload = construct_payload(meraki.params)
+ query_path = meraki.construct_path('get_device', net_id=net_id, custom={'serial': meraki.params['serial']})
+ device_data = meraki.request(query_path, method='GET')
+ ignore_keys = ['lanIp', 'serial', 'mac', 'model', 'networkId', 'moveMapMarker', 'wan1Ip', 'wan2Ip']
+ if meraki.is_update_required(device_data, payload, optional_ignore=ignore_keys):
+ path = meraki.construct_path('update', net_id=net_id) + meraki.params['serial']
+ updated_device = []
+ updated_device.append(meraki.request(path, method='PUT', payload=json.dumps(payload)))
+ meraki.result['data'] = updated_device
+ meraki.result['changed'] = True
+ else:
+ meraki.result['data'] = device_data
+ else: # Claim device into network
+ query_path = meraki.construct_path('get_all', net_id=net_id)
+ device_list = meraki.request(query_path, method='GET')
+ if is_device_valid(meraki, meraki.params['serial'], device_list) is False:
+ if net_id:
+ payload = {'serials': [meraki.params['serial']]}
+ path = meraki.construct_path('create', net_id=net_id)
+ created_device = []
+ created_device.append(meraki.request(path, method='POST', payload=json.dumps(payload)))
+ meraki.result['data'] = created_device
+ meraki.result['changed'] = True
+ elif meraki.params['state'] == 'absent':
+ device = []
+ query_path = meraki.construct_path('get_all', net_id=net_id)
+ device_list = meraki.request(query_path, method='GET')
+ if is_device_valid(meraki, meraki.params['serial'], device_list) is True:
+ path = meraki.construct_path('delete', net_id=net_id, custom={'serial': meraki.params['serial']})
+ request = meraki.request(path, method='POST')
+ meraki.result['changed'] = True
+
+ # in the event of a successful module execution, you will want to
+ # simple AnsibleModule.exit_json(), passing the key/value results
+ meraki.exit_json(**meraki.result)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/ansible_collections/cisco/meraki/plugins/modules/meraki_firewalled_services.py b/ansible_collections/cisco/meraki/plugins/modules/meraki_firewalled_services.py
new file mode 100644
index 000000000..ec78c068b
--- /dev/null
+++ b/ansible_collections/cisco/meraki/plugins/modules/meraki_firewalled_services.py
@@ -0,0 +1,233 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Copyright: (c) 2019, Kevin Breit (@kbreit) <kevin.breit@kevinbreit.net>
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+ANSIBLE_METADATA = {
+ 'metadata_version': '1.1',
+ 'status': ['preview'],
+ 'supported_by': 'community'
+}
+
+DOCUMENTATION = r'''
+---
+module: meraki_firewalled_services
+short_description: Edit firewall policies for administrative network services
+description:
+- Allows for setting policy firewalled services for Meraki network devices.
+
+options:
+ auth_key:
+ description:
+ - Authentication key provided by the dashboard. Required if environmental variable MERAKI_KEY is not set.
+ type: str
+ net_name:
+ description:
+ - Name of a network.
+ aliases: [ network ]
+ type: str
+ net_id:
+ description:
+ - ID number of a network.
+ type: str
+ org_name:
+ description:
+ - Name of organization associated to a network.
+ type: str
+ org_id:
+ description:
+ - ID of organization associated to a network.
+ type: str
+ state:
+ description:
+ - States that a policy should be created or modified.
+ choices: [present, query]
+ default: present
+ type: str
+ service:
+ description:
+ - Network service to query or modify.
+ choices: [ICMP, SNMP, web]
+ type: str
+ access:
+ description:
+ - Network service to query or modify.
+ choices: [blocked, restricted, unrestricted]
+ type: str
+ allowed_ips:
+ description:
+ - List of IP addresses allowed to access a service.
+ - Only used when C(access) is set to restricted.
+ type: list
+ elements: str
+
+author:
+ - Kevin Breit (@kbreit)
+extends_documentation_fragment: cisco.meraki.meraki
+'''
+
+EXAMPLES = r'''
+- name: Set icmp service to blocked
+ meraki_firewalled_services:
+ auth_key: '{{ auth_key }}'
+ state: present
+ org_name: '{{test_org_name}}'
+ net_name: IntTestNetworkAppliance
+ service: ICMP
+ access: blocked
+ delegate_to: localhost
+
+- name: Set icmp service to restricted
+ meraki_firewalled_services:
+ auth_key: abc123
+ state: present
+ org_name: YourOrg
+ net_name: YourNet
+ service: web
+ access: restricted
+ allowed_ips:
+ - 192.0.1.1
+ - 192.0.1.2
+ delegate_to: localhost
+
+- name: Query appliance services
+ meraki_firewalled_services:
+ auth_key: abc123
+ state: query
+ org_name: YourOrg
+ net_name: YourNet
+ delegate_to: localhost
+
+- name: Query services
+ meraki_firewalled_services:
+ auth_key: abc123
+ state: query
+ org_name: YourOrg
+ net_name: YourNet
+ service: ICMP
+ delegate_to: localhost
+'''
+
+RETURN = r'''
+data:
+ description: List of network services.
+ returned: info
+ type: complex
+ contains:
+ access:
+ description: Access assigned to a service type.
+ returned: success
+ type: str
+ sample: unrestricted
+ service:
+ description: Service to apply policy to.
+ returned: success
+ type: str
+ sample: ICMP
+ allowed_ips:
+ description: List of IP addresses to have access to service.
+ returned: success
+ type: str
+ sample: 192.0.1.0
+'''
+
+from ansible.module_utils.basic import AnsibleModule, json
+from ansible_collections.cisco.meraki.plugins.module_utils.network.meraki.meraki import MerakiModule, meraki_argument_spec
+
+
+def main():
+
+ # define the available arguments/parameters that a user can pass to
+ # the module
+
+ argument_spec = meraki_argument_spec()
+ argument_spec.update(
+ net_id=dict(type='str'),
+ net_name=dict(type='str', aliases=['network']),
+ state=dict(type='str', default='present', choices=['query', 'present']),
+ service=dict(type='str', default=None, choices=['ICMP', 'SNMP', 'web']),
+ access=dict(type='str', choices=['blocked', 'restricted', 'unrestricted']),
+ allowed_ips=dict(type='list', elements='str'),
+ )
+
+ mutually_exclusive = [('net_name', 'net_id')]
+
+ # the AnsibleModule object will be our abstraction working with Ansible
+ # this includes instantiation, a couple of common attr would be the
+ # args/params passed to the execution, as well as if the module
+ # supports check mode
+ module = AnsibleModule(argument_spec=argument_spec,
+ supports_check_mode=True,
+ mutually_exclusive=mutually_exclusive
+ )
+
+ meraki = MerakiModule(module, function='firewalled_services')
+ module.params['follow_redirects'] = 'all'
+
+ net_services_urls = {'firewalled_services': '/networks/{net_id}/appliance/firewall/firewalledServices'}
+ services_urls = {'firewalled_services': '/networks/{net_id}/appliance/firewall/firewalledServices/{service}'}
+
+ meraki.url_catalog['network_services'] = net_services_urls
+ meraki.url_catalog['service'] = services_urls
+
+ # manipulate or modify the state as needed (this is going to be the
+ # part where your module will do what it needs to do)
+
+ org_id = meraki.params['org_id']
+ if not org_id:
+ org_id = meraki.get_org_id(meraki.params['org_name'])
+ net_id = meraki.params['net_id']
+ if net_id is None:
+ nets = meraki.get_nets(org_id=org_id)
+ net_id = meraki.get_net_id(org_id, meraki.params['net_name'], data=nets)
+
+ if meraki.params['state'] == 'present':
+ if meraki.params['access'] != 'restricted' and meraki.params['allowed_ips'] is not None:
+ meraki.fail_json(msg="allowed_ips is only allowed when access is restricted.")
+ payload = {'access': meraki.params['access']}
+ if meraki.params['access'] == 'restricted':
+ payload['allowedIps'] = meraki.params['allowed_ips']
+
+ if meraki.params['state'] == 'query':
+ if meraki.params['service'] is None:
+ path = meraki.construct_path('network_services', net_id=net_id)
+ response = meraki.request(path, method='GET')
+ meraki.result['data'] = response
+ meraki.exit_json(**meraki.result)
+ else:
+ path = meraki.construct_path('service', net_id=net_id, custom={'service': meraki.params['service']})
+ response = meraki.request(path, method='GET')
+ meraki.result['data'] = response
+ meraki.exit_json(**meraki.result)
+ elif meraki.params['state'] == 'present':
+ path = meraki.construct_path('service', net_id=net_id, custom={'service': meraki.params['service']})
+ original = meraki.request(path, method='GET')
+ if meraki.is_update_required(original, payload, optional_ignore=['service']):
+ if meraki.check_mode is True:
+ diff_payload = {'service': meraki.params['service']} # Need to add service as it's not in payload
+ diff_payload.update(payload)
+ meraki.generate_diff(original, diff_payload)
+ original.update(payload)
+ meraki.result['data'] = original
+ meraki.result['changed'] = True
+ meraki.exit_json(**meraki.result)
+ path = meraki.construct_path('service', net_id=net_id, custom={'service': meraki.params['service']})
+ response = meraki.request(path, method='PUT', payload=json.dumps(payload))
+ if meraki.status == 200:
+ meraki.generate_diff(original, response)
+ meraki.result['data'] = response
+ meraki.result['changed'] = True
+ else:
+ meraki.result['data'] = original
+
+ # in the event of a successful module execution, you will want to
+ # simple AnsibleModule.exit_json(), passing the key/value results
+ meraki.exit_json(**meraki.result)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/ansible_collections/cisco/meraki/plugins/modules/meraki_management_interface.py b/ansible_collections/cisco/meraki/plugins/modules/meraki_management_interface.py
new file mode 100644
index 000000000..c02978611
--- /dev/null
+++ b/ansible_collections/cisco/meraki/plugins/modules/meraki_management_interface.py
@@ -0,0 +1,384 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Copyright: (c) 2019, Kevin Breit (@kbreit) <kevin.breit@kevinbreit.net>
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+ANSIBLE_METADATA = {
+ 'metadata_version': '1.1',
+ 'status': ['preview'],
+ 'supported_by': 'community'
+}
+
+DOCUMENTATION = r'''
+---
+module: meraki_management_interface
+short_description: Configure Meraki management interfaces
+version_added: "1.1.0"
+description:
+- Allows for configuration of management interfaces on Meraki MX, MS, and MR devices.
+notes:
+- C(WAN2) parameter is only valid for MX appliances.
+- C(wan_enabled) should not be provided for non-MX devies.
+options:
+ state:
+ description:
+ - Specifies whether configuration template information should be queried, modified, or deleted.
+ choices: ['absent', 'query', 'present']
+ default: query
+ type: str
+ org_name:
+ description:
+ - Name of organization containing the configuration template.
+ type: str
+ org_id:
+ description:
+ - ID of organization associated to a configuration template.
+ type: str
+ net_name:
+ description:
+ - Name of the network to bind or unbind configuration template to.
+ type: str
+ net_id:
+ description:
+ - ID of the network to bind or unbind configuration template to.
+ type: str
+ serial:
+ description:
+ - serial number of the device to configure.
+ type: str
+ required: true
+ wan1:
+ description:
+ - Management interface details for management interface.
+ aliases: [mgmt1]
+ type: dict
+ suboptions:
+ wan_enabled:
+ description:
+ - States whether the management interface is enabled.
+ - Only valid for MX devices.
+ type: str
+ choices: [disabled, enabled, not configured]
+ using_static_ip:
+ description:
+ - Configures the interface to use static IP or DHCP.
+ type: bool
+ static_ip:
+ description:
+ - IP address assigned to Management interface.
+ - Valid only if C(using_static_ip) is C(True).
+ type: str
+ static_gateway_ip:
+ description:
+ - IP address for default gateway.
+ - Valid only if C(using_static_ip) is C(True).
+ type: str
+ static_subnet_mask:
+ description:
+ - Netmask for static IP address.
+ - Valid only if C(using_static_ip) is C(True).
+ type: str
+ static_dns:
+ description:
+ - DNS servers to use.
+ - Allows for a maximum of 2 addresses.
+ type: list
+ elements: str
+ vlan:
+ description:
+ - VLAN number to use for the management network.
+ type: int
+ wan2:
+ description:
+ - Management interface details for management interface.
+ type: dict
+ aliases: [mgmt2]
+ suboptions:
+ wan_enabled:
+ description:
+ - States whether the management interface is enabled.
+ - Only valid for MX devices.
+ type: str
+ choices: [disabled, enabled, not configured]
+ using_static_ip:
+ description:
+ - Configures the interface to use static IP or DHCP.
+ type: bool
+ static_ip:
+ description:
+ - IP address assigned to Management interface.
+ - Valid only if C(using_static_ip) is C(True).
+ type: str
+ static_gateway_ip:
+ description:
+ - IP address for default gateway.
+ - Valid only if C(using_static_ip) is C(True).
+ type: str
+ static_subnet_mask:
+ description:
+ - Netmask for static IP address.
+ - Valid only if C(using_static_ip) is C(True).
+ type: str
+ static_dns:
+ description:
+ - DNS servers to use.
+ - Allows for a maximum of 2 addresses.
+ type: list
+ elements: str
+ vlan:
+ description:
+ - VLAN number to use for the management network.
+ type: int
+
+author:
+- Kevin Breit (@kbreit)
+extends_documentation_fragment: cisco.meraki.meraki
+'''
+
+EXAMPLES = r'''
+- name: Set WAN2 as static IP
+ meraki_management_interface:
+ auth_key: abc123
+ state: present
+ org_name: YourOrg
+ net_id: YourNetId
+ serial: AAAA-BBBB-CCCC
+ wan2:
+ wan_enabled: enabled
+ using_static_ip: yes
+ static_ip: 192.168.16.195
+ static_gateway_ip: 192.168.16.1
+ static_subnet_mask: 255.255.255.0
+ static_dns:
+ - 1.1.1.1
+ vlan: 1
+ delegate_to: localhost
+
+- name: Query management information
+ meraki_management_interface:
+ auth_key: abc123
+ state: query
+ org_name: YourOrg
+ net_id: YourNetId
+ serial: AAAA-BBBB-CCCC
+ delegate_to: localhost
+'''
+
+RETURN = r'''
+data:
+ description: Information about queried object.
+ returned: success
+ type: complex
+ contains:
+ wan1:
+ description: Management configuration for WAN1 interface
+ returned: success
+ type: complex
+ contains:
+ wan_enabled:
+ description: Enabled state of interface
+ returned: success
+ type: str
+ sample: enabled
+ using_static_ip:
+ description: Boolean value of whether static IP assignment is used on interface
+ returned: success
+ type: bool
+ sample: True
+ static_ip:
+ description: Assigned static IP
+ returned: only if static IP assignment is used
+ type: str
+ sample: 192.0.1.2
+ static_gateway_ip:
+ description: Assigned static gateway IP
+ returned: only if static IP assignment is used
+ type: str
+ sample: 192.0.1.1
+ static_subnet_mask:
+ description: Assigned netmask for static IP
+ returned: only if static IP assignment is used
+ type: str
+ sample: 255.255.255.0
+ static_dns:
+ description: List of DNS IP addresses
+ returned: only if static IP assignment is used
+ type: list
+ sample: ["1.1.1.1"]
+ vlan:
+ description: VLAN tag id of management VLAN
+ returned: success
+ type: int
+ sample: 2
+ wan2:
+ description: Management configuration for WAN1 interface
+ returned: success
+ type: complex
+ contains:
+ wan_enabled:
+ description: Enabled state of interface
+ returned: success
+ type: str
+ sample: enabled
+ using_static_ip:
+ description: Boolean value of whether static IP assignment is used on interface
+ returned: success
+ type: bool
+ sample: True
+ static_ip:
+ description: Assigned static IP
+ returned: only if static IP assignment is used
+ type: str
+ sample: 192.0.1.2
+ static_gateway_ip:
+ description: Assigned static gateway IP
+ returned: only if static IP assignment is used
+ type: str
+ sample: 192.0.1.1
+ static_subnet_mask:
+ description: Assigned netmask for static IP
+ returned: only if static IP assignment is used
+ type: str
+ sample: 255.255.255.0
+ static_dns:
+ description: List of DNS IP addresses
+ returned: only if static IP assignment is used
+ type: list
+ sample: ["1.1.1.1"]
+ vlan:
+ description: VLAN tag id of management VLAN
+ returned: success
+ type: int
+ sample: 2
+'''
+
+from ansible.module_utils.basic import AnsibleModule, json
+from ansible.module_utils.common.dict_transformations import recursive_diff
+from ansible_collections.cisco.meraki.plugins.module_utils.network.meraki.meraki import MerakiModule, meraki_argument_spec
+
+
+def main():
+ # define the available arguments/parameters that a user can pass to
+ # the module
+
+ int_arg_spec = dict(wan_enabled=dict(type='str', choices=['enabled', 'disabled', 'not configured']),
+ using_static_ip=dict(type='bool'),
+ static_ip=dict(type='str'),
+ static_gateway_ip=dict(type='str'),
+ static_subnet_mask=dict(type='str'),
+ static_dns=dict(type='list', elements='str'),
+ vlan=dict(type='int'),
+ )
+
+ argument_spec = meraki_argument_spec()
+ argument_spec.update(state=dict(type='str', choices=['absent', 'query', 'present'], default='query'),
+ net_name=dict(type='str'),
+ net_id=dict(type='str'),
+ serial=dict(type='str', required=True),
+ wan1=dict(type='dict', default=None, options=int_arg_spec, aliases=['mgmt1']),
+ wan2=dict(type='dict', default=None, options=int_arg_spec, aliases=['mgmt2']),
+ )
+
+ # the AnsibleModule object will be our abstraction working with Ansible
+ # this includes instantiation, a couple of common attr would be the
+ # args/params passed to the execution, as well as if the module
+ # supports check mode
+ module = AnsibleModule(argument_spec=argument_spec,
+ supports_check_mode=True,
+ )
+ meraki = MerakiModule(module, function='management_interface')
+ meraki.params['follow_redirects'] = 'all'
+
+ query_urls = {'management_interface': '/devices/{serial}/managementInterface'}
+
+ meraki.url_catalog['get_one'].update(query_urls)
+
+ if meraki.params['net_id'] and meraki.params['net_name']:
+ meraki.fail_json('net_id and net_name are mutually exclusive.')
+ if meraki.params['state'] == 'present':
+ interfaces = ('wan1', 'wan2')
+ for interface in interfaces:
+ if meraki.params[interface] is not None:
+ if meraki.params[interface]['using_static_ip'] is True:
+ if len(meraki.params[interface]['static_dns']) > 2:
+ meraki.fail_json("Maximum number of static DNS addresses is 2.")
+
+ payload = dict()
+
+ if meraki.params['state'] == 'present':
+ interfaces = ('wan1', 'wan2')
+ for interface in interfaces:
+ if meraki.params[interface] is not None:
+ wan_int = dict()
+ if meraki.params[interface]['wan_enabled'] is not None:
+ wan_int['wanEnabled'] = meraki.params[interface]['wan_enabled']
+ if meraki.params[interface]['using_static_ip'] is not None:
+ wan_int['usingStaticIp'] = meraki.params[interface]['using_static_ip']
+ if meraki.params[interface]['vlan'] is not None:
+ wan_int['vlan'] = meraki.params[interface]['vlan']
+ if meraki.params[interface]['using_static_ip'] is True:
+ wan_int['staticIp'] = meraki.params[interface]['static_ip']
+ wan_int['staticGatewayIp'] = meraki.params[interface]['static_gateway_ip']
+ wan_int['staticSubnetMask'] = meraki.params[interface]['static_subnet_mask']
+ wan_int['staticDns'] = meraki.params[interface]['static_dns']
+ payload[interface] = wan_int
+
+ # manipulate or modify the state as needed (this is going to be the
+ # part where your module will do what it needs to do)
+ org_id = meraki.params['org_id']
+ if meraki.params['org_name']:
+ org_id = meraki.get_org_id(meraki.params['org_name'])
+ net_id = meraki.params['net_id']
+ if net_id is None:
+ nets = meraki.get_nets(org_id=org_id)
+ net_id = meraki.get_net_id(net_name=meraki.params['net_name'], data=nets)
+
+ if meraki.params['state'] == 'query':
+ path = meraki.construct_path('get_one', net_id=net_id, custom={'serial': meraki.params['serial']})
+ response = meraki.request(path, method='GET')
+ if meraki.status == 200:
+ meraki.result['data'] = response
+ elif meraki.params['state'] == 'present':
+ path = meraki.construct_path('get_one', custom={'serial': meraki.params['serial']})
+ original = meraki.request(path, method='GET')
+ update_required = False
+ if 'wan1' in original:
+ if 'wanEnabled' in original['wan1']:
+ update_required = meraki.is_update_required(original, payload)
+ else:
+ update_required = meraki.is_update_required(original, payload, optional_ignore=['wanEnabled'])
+ if 'wan2' in original and update_required is False:
+ if 'wanEnabled' in original['wan2']:
+ update_required = meraki.is_update_required(original, payload)
+ else:
+ update_required = meraki.is_update_required(original, payload, optional_ignore=['wanEnabled'])
+ if update_required is True:
+ if meraki.check_mode is True:
+ diff = recursive_diff(original, payload)
+ original.update(payload)
+ meraki.result['diff'] = {'before': diff[0],
+ 'after': diff[1]}
+ meraki.result['data'] = original
+ meraki.result['changed'] = True
+ meraki.exit_json(**meraki.result)
+ response = meraki.request(path, method='PUT', payload=json.dumps(payload))
+ if meraki.status == 200:
+ diff = recursive_diff(original, response)
+ meraki.result['diff'] = {'before': diff[0],
+ 'after': diff[1]}
+ meraki.result['data'] = response
+ meraki.result['changed'] = True
+ else:
+ meraki.result['data'] = original
+
+ # in the event of a successful module execution, you will want to
+ # simple AnsibleModule.exit_json(), passing the key/value results
+ meraki.exit_json(**meraki.result)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/ansible_collections/cisco/meraki/plugins/modules/meraki_mr_l3_firewall.py b/ansible_collections/cisco/meraki/plugins/modules/meraki_mr_l3_firewall.py
new file mode 100644
index 000000000..d2a70052a
--- /dev/null
+++ b/ansible_collections/cisco/meraki/plugins/modules/meraki_mr_l3_firewall.py
@@ -0,0 +1,300 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Copyright: (c) 2018, Kevin Breit (@kbreit) <kevin.breit@kevinbreit.net>
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+ANSIBLE_METADATA = {
+ 'metadata_version': '1.1',
+ 'status': ['preview'],
+ 'supported_by': 'community'
+}
+
+DOCUMENTATION = r'''
+---
+module: meraki_mr_l3_firewall
+short_description: Manage MR access point layer 3 firewalls in the Meraki cloud
+description:
+- Allows for creation, management, and visibility into layer 3 firewalls implemented on Meraki MR access points.
+- Module is not idempotent as of current release.
+options:
+ state:
+ description:
+ - Create or modify an organization.
+ type: str
+ choices: [ present, query ]
+ default: present
+ net_name:
+ description:
+ - Name of network containing access points.
+ type: str
+ net_id:
+ description:
+ - ID of network containing access points.
+ type: str
+ number:
+ description:
+ - Number of SSID to apply firewall rule to.
+ type: str
+ aliases: [ ssid_number ]
+ ssid_name:
+ description:
+ - Name of SSID to apply firewall rule to.
+ type: str
+ aliases: [ ssid ]
+ allow_lan_access:
+ description:
+ - Sets whether devices can talk to other devices on the same LAN.
+ type: bool
+ default: yes
+ rules:
+ description:
+ - List of firewall rules.
+ type: list
+ elements: dict
+ suboptions:
+ policy:
+ description:
+ - Specifies the action that should be taken when rule is hit.
+ type: str
+ choices: [ allow, deny ]
+ protocol:
+ description:
+ - Specifies protocol to match against.
+ type: str
+ choices: [ any, icmp, tcp, udp ]
+ dest_port:
+ description:
+ - Comma-seperated list of destination ports to match.
+ type: str
+ dest_cidr:
+ description:
+ - Comma-separated list of CIDR notation networks to match.
+ type: str
+ comment:
+ description:
+ - Optional comment describing the firewall rule.
+ type: str
+author:
+- Kevin Breit (@kbreit)
+extends_documentation_fragment: cisco.meraki.meraki
+'''
+
+EXAMPLES = r'''
+- name: Create single firewall rule
+ meraki_mr_l3_firewall:
+ auth_key: abc123
+ state: present
+ org_name: YourOrg
+ net_id: 12345
+ number: 1
+ rules:
+ - comment: Integration test rule
+ policy: allow
+ protocol: tcp
+ dest_port: 80
+ dest_cidr: 192.0.2.0/24
+ allow_lan_access: no
+ delegate_to: localhost
+
+- name: Enable local LAN access
+ meraki_mr_l3_firewall:
+ auth_key: abc123
+ state: present
+ org_name: YourOrg
+ net_id: 123
+ number: 1
+ rules:
+ allow_lan_access: yes
+ delegate_to: localhost
+
+- name: Query firewall rules
+ meraki_mr_l3_firewall:
+ auth_key: abc123
+ state: query
+ org_name: YourOrg
+ net_name: YourNet
+ number: 1
+ delegate_to: localhost
+'''
+
+RETURN = r'''
+
+'''
+
+from ansible.module_utils.basic import AnsibleModule, json
+from ansible_collections.cisco.meraki.plugins.module_utils.network.meraki.meraki import MerakiModule, meraki_argument_spec
+
+
+def assemble_payload(meraki):
+ params_map = {'policy': 'policy',
+ 'protocol': 'protocol',
+ 'dest_port': 'destPort',
+ 'dest_cidr': 'destCidr',
+ 'comment': 'comment',
+ }
+ rules = []
+ for rule in meraki.params['rules']:
+ proposed_rule = dict()
+ for k, v in rule.items():
+ proposed_rule[params_map[k]] = v
+ rules.append(proposed_rule)
+ payload = {'rules': rules}
+ return payload
+
+
+def get_rules(meraki, net_id, number):
+ path = meraki.construct_path('get_all', net_id=net_id, custom={'number': number})
+ response = meraki.request(path, method='GET')
+ if meraki.status == 200:
+ return normalize_rule_case(response)
+
+
+def normalize_rule_case(rules):
+ excluded = ['comment']
+ try:
+ for r in rules['rules']:
+ for k in r:
+ if k not in excluded:
+ r[k] = r[k].lower()
+ except KeyError:
+ return rules
+ return rules
+
+
+def get_ssid_number(name, data):
+ for ssid in data:
+ if name == ssid['name']:
+ return ssid['number']
+ return False
+
+
+def get_ssids(meraki, net_id):
+ path = meraki.construct_path('get_all', net_id=net_id)
+ return meraki.request(path, method='GET')
+
+
+def main():
+ # define the available arguments/parameters that a user can pass to
+ # the module
+
+ fw_rules = dict(policy=dict(type='str', choices=['allow', 'deny']),
+ protocol=dict(type='str', choices=['tcp', 'udp', 'icmp', 'any']),
+ dest_port=dict(type='str'),
+ dest_cidr=dict(type='str'),
+ comment=dict(type='str'),
+ )
+
+ argument_spec = meraki_argument_spec()
+ argument_spec.update(state=dict(type='str', choices=['present', 'query'], default='present'),
+ net_name=dict(type='str'),
+ net_id=dict(type='str'),
+ number=dict(type='str', aliases=['ssid_number']),
+ ssid_name=dict(type='str', aliases=['ssid']),
+ rules=dict(type='list', default=None, elements='dict', options=fw_rules),
+ allow_lan_access=dict(type='bool', default=True),
+ )
+
+ # the AnsibleModule object will be our abstraction working with Ansible
+ # this includes instantiation, a couple of common attr would be the
+ # args/params passed to the execution, as well as if the module
+ # supports check mode
+ module = AnsibleModule(argument_spec=argument_spec,
+ supports_check_mode=True,
+ )
+ meraki = MerakiModule(module, function='mr_l3_firewall')
+
+ meraki.params['follow_redirects'] = 'all'
+
+ query_urls = {'mr_l3_firewall': '/networks/{net_id}/wireless/ssids/{number}/firewall/l3FirewallRules'}
+ update_urls = {'mr_l3_firewall': '/networks/{net_id}/wireless/ssids/{number}/firewall/l3FirewallRules'}
+
+ meraki.url_catalog['get_all'].update(query_urls)
+ meraki.url_catalog['update'] = update_urls
+
+ payload = None
+
+ # execute checks for argument completeness
+
+ # manipulate or modify the state as needed (this is going to be the
+ # part where your module will do what it needs to do)
+ org_id = meraki.params['org_id']
+ orgs = None
+ if org_id is None:
+ orgs = meraki.get_orgs()
+ for org in orgs:
+ if org['name'] == meraki.params['org_name']:
+ org_id = org['id']
+ net_id = meraki.params['net_id']
+ if net_id is None:
+ if orgs is None:
+ orgs = meraki.get_orgs()
+ net_id = meraki.get_net_id(net_name=meraki.params['net_name'],
+ data=meraki.get_nets(org_id=org_id))
+ number = meraki.params['number']
+ if meraki.params['ssid_name']:
+ number = get_ssid_number(meraki.params['ssid_name'], get_ssids(meraki, net_id))
+
+ if meraki.params['state'] == 'query':
+ meraki.result['data'] = get_rules(meraki, net_id, number)
+ elif meraki.params['state'] == 'present':
+ rules = get_rules(meraki, net_id, number)
+ path = meraki.construct_path('get_all', net_id=net_id, custom={'number': number})
+ if meraki.params['rules']:
+ payload = assemble_payload(meraki)
+ else:
+ payload = dict()
+ update = False
+ try:
+ if len(rules) != len(payload['rules']): # Quick and simple check to avoid more processing
+ update = True
+ if update is False:
+ for r in range(len(rules) - 2):
+ if meraki.is_update_required(rules[r], payload[r]) is True:
+ update = True
+ except KeyError:
+ pass
+ # meraki.fail_json(msg=rules)
+ if rules['rules'][len(rules['rules']) - 2] != meraki.params['allow_lan_access']:
+ update = True
+ if update is True:
+ payload['allowLanAccess'] = meraki.params['allow_lan_access']
+ if meraki.check_mode is True:
+ # This code is disgusting, rework it at some point
+ if 'rules' in payload:
+ cleansed_payload = payload['rules']
+ cleansed_payload.append(rules['rules'][len(rules['rules']) - 1])
+ cleansed_payload.append(rules['rules'][len(rules['rules']) - 2])
+ if meraki.params['allow_lan_access'] is None:
+ cleansed_payload[len(cleansed_payload) - 2]['policy'] = rules['rules'][len(rules['rules']) - 2]['policy']
+ else:
+ if meraki.params['allow_lan_access'] is True:
+ cleansed_payload[len(cleansed_payload) - 2]['policy'] = 'allow'
+ else:
+ cleansed_payload[len(cleansed_payload) - 2]['policy'] = 'deny'
+ else:
+ if meraki.params['allow_lan_access'] is True:
+ rules['rules'][len(rules['rules']) - 2]['policy'] = 'allow'
+ else:
+ rules['rules'][len(rules['rules']) - 2]['policy'] = 'deny'
+ cleansed_payload = rules
+ meraki.result['data'] = cleansed_payload
+ meraki.result['changed'] = True
+ meraki.exit_json(**meraki.result)
+ response = meraki.request(path, method='PUT', payload=json.dumps(payload))
+ if meraki.status == 200:
+ meraki.result['data'] = response
+ meraki.result['changed'] = True
+ else:
+ meraki.result['data'] = rules
+
+ # in the event of a successful module execution, you will want to
+ # simple AnsibleModule.exit_json(), passing the key/value results
+ meraki.exit_json(**meraki.result)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/ansible_collections/cisco/meraki/plugins/modules/meraki_mr_l7_firewall.py b/ansible_collections/cisco/meraki/plugins/modules/meraki_mr_l7_firewall.py
new file mode 100644
index 000000000..a9584091f
--- /dev/null
+++ b/ansible_collections/cisco/meraki/plugins/modules/meraki_mr_l7_firewall.py
@@ -0,0 +1,503 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Copyright: (c) 2022, Joshua Coronado (@joshuajcoronado) <joshua@coronado.io>
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+
+__metaclass__ = type
+
+ANSIBLE_METADATA = {
+ "metadata_version": "1.1",
+ "status": ["preview"],
+ "supported_by": "community",
+}
+
+DOCUMENTATION = r"""
+---
+module: meraki_mr_l7_firewall
+short_description: Manage MR access point layer 7 firewalls in the Meraki cloud
+description:
+- Allows for creation, management, and visibility into layer 7 firewalls implemented on Meraki MR access points.
+- Module assumes a complete list of firewall rules are passed as a parameter.
+- If there is interest in this module allowing manipulation of a single firewall rule, please submit an issue against this module.
+options:
+ state:
+ description:
+ - Query or modify a firewall rule.
+ choices: ['present', 'query']
+ default: present
+ type: str
+ net_name:
+ description:
+ - Name of network containing access points.
+ type: str
+ net_id:
+ description:
+ - ID of network containing access points.
+ type: str
+ number:
+ description:
+ - Number of SSID to apply firewall rule to.
+ type: str
+ aliases: [ ssid_number ]
+ ssid_name:
+ description:
+ - Name of SSID to apply firewall rule to.
+ type: str
+ aliases: [ ssid ]
+ rules:
+ description:
+ - List of layer 7 firewall rules.
+ type: list
+ elements: dict
+ suboptions:
+ policy:
+ description:
+ - Policy to apply if rule is hit.
+ choices: [deny]
+ default: deny
+ type: str
+ type:
+ description:
+ - Type of policy to apply.
+ choices: [application,
+ application_category,
+ host,
+ ip_range,
+ port]
+ type: str
+ application:
+ description:
+ - Application to filter.
+ type: dict
+ suboptions:
+ name:
+ description:
+ - Name of application to filter as defined by Meraki.
+ type: str
+ id:
+ description:
+ - URI of application as defined by Meraki.
+ type: str
+ host:
+ description:
+ - FQDN of host to filter.
+ type: str
+ ip_range:
+ description:
+ - CIDR notation range of IP addresses to apply rule to.
+ - Port can be appended to range with a C(":").
+ type: str
+ port:
+ description:
+ - TCP or UDP based port to filter.
+ type: str
+ categories:
+ description:
+ - When C(True), specifies that applications and application categories should be queried instead of firewall rules.
+ type: bool
+author:
+- Joshua Coronado (@joshuajcoronado)
+extends_documentation_fragment: cisco.meraki.meraki
+"""
+
+EXAMPLES = r"""
+- name: Query firewall rules
+ meraki_mr_l7_firewall:
+ auth_key: abc123
+ org_name: YourOrg
+ net_name: YourNet
+ state: query
+ number: 1
+ delegate_to: localhost
+
+- name: Query applications and application categories
+ meraki_mr_l7_firewall:
+ auth_key: abc123
+ org_name: YourOrg
+ net_name: YourNet
+ number: 1
+ categories: yes
+ state: query
+ delegate_to: localhost
+
+- name: Set firewall rules
+ meraki_mr_l7_firewall:
+ auth_key: abc123
+ org_name: YourOrg
+ net_name: YourNet
+ number: 1
+ state: present
+ rules:
+ - policy: deny
+ type: port
+ port: 8080
+ - type: port
+ port: 1234
+ - type: host
+ host: asdf.com
+ - type: application
+ application:
+ id: meraki:layer7/application/205
+ - type: application_category
+ application:
+ id: meraki:layer7/category/24
+ delegate_to: localhost
+"""
+
+RETURN = r"""
+data:
+ description: Firewall rules associated to network SSID.
+ returned: success
+ type: complex
+ contains:
+ rules:
+ description: Ordered list of firewall rules.
+ returned: success, when not querying applications
+ type: list
+ contains:
+ policy:
+ description: Action to apply when rule is hit.
+ returned: success
+ type: str
+ sample: deny
+ type:
+ description: Type of rule category.
+ returned: success
+ type: str
+ sample: applications
+ applications:
+ description: List of applications within a category.
+ type: list
+ contains:
+ id:
+ description: URI of application.
+ returned: success
+ type: str
+ sample: Gmail
+ name:
+ description: Descriptive name of application.
+ returned: success
+ type: str
+ sample: meraki:layer7/application/4
+ applicationCategory:
+ description: List of application categories within a category.
+ type: list
+ contains:
+ id:
+ description: URI of application.
+ returned: success
+ type: str
+ sample: Gmail
+ name:
+ description: Descriptive name of application.
+ returned: success
+ type: str
+ sample: meraki:layer7/application/4
+ port:
+ description: Port number in rule.
+ returned: success
+ type: str
+ sample: 23
+ ipRange:
+ description: Range of IP addresses in rule.
+ returned: success
+ type: str
+ sample: 1.1.1.0/23
+ application_categories:
+ description: List of application categories and applications.
+ type: list
+ returned: success, when querying applications
+ contains:
+ applications:
+ description: List of applications within a category.
+ type: list
+ contains:
+ id:
+ description: URI of application.
+ returned: success
+ type: str
+ sample: Gmail
+ name:
+ description: Descriptive name of application.
+ returned: success
+ type: str
+ sample: meraki:layer7/application/4
+ id:
+ description: URI of application category.
+ returned: success
+ type: str
+ sample: Email
+ name:
+ description: Descriptive name of application category.
+ returned: success
+ type: str
+ sample: layer7/category/1
+"""
+
+import copy
+from ansible.module_utils.basic import AnsibleModule, json
+from ansible_collections.cisco.meraki.plugins.module_utils.network.meraki.meraki import (
+ MerakiModule,
+ meraki_argument_spec,
+)
+
+
+def get_applications(meraki, net_id):
+ path = meraki.construct_path("get_categories", net_id=net_id)
+ return meraki.request(path, method="GET")
+
+
+def lookup_application(meraki, net_id, application):
+ response = get_applications(meraki, net_id)
+ for category in response["applicationCategories"]:
+ if category["name"].lower() == application.lower():
+ return category["id"]
+ for app in category["applications"]:
+ if app["name"].lower() == application.lower():
+ return app["id"]
+ meraki.fail_json(
+ msg="No application or category named {0} found".format(application)
+ )
+
+
+def assemble_payload(meraki, net_id, rule):
+ new_rule = {}
+ if rule["type"] == "application":
+ new_rule = {
+ "policy": rule["policy"],
+ "type": "application",
+ }
+ if rule["application"]["id"]:
+ new_rule["value"] = {"id": rule["application"]["id"]}
+ elif rule["application"]["name"]:
+ new_rule["value"] = {
+ "id": lookup_application(meraki, net_id, rule["application"]["name"])
+ }
+ elif rule["type"] == "application_category":
+ new_rule = {
+ "policy": rule["policy"],
+ "type": "applicationCategory",
+ }
+ if rule["application"]["id"]:
+ new_rule["value"] = {"id": rule["application"]["id"]}
+ elif rule["application"]["name"]:
+ new_rule["value"] = {
+ "id": lookup_application(meraki, net_id, rule["application"]["name"])
+ }
+ elif rule["type"] == "ip_range":
+ new_rule = {
+ "policy": rule["policy"],
+ "type": "ipRange",
+ "value": rule["ip_range"],
+ }
+ elif rule["type"] == "host":
+ new_rule = {
+ "policy": rule["policy"],
+ "type": rule["type"],
+ "value": rule["host"],
+ }
+ elif rule["type"] == "port":
+ new_rule = {
+ "policy": rule["policy"],
+ "type": rule["type"],
+ "value": rule["port"],
+ }
+ return new_rule
+
+
+def restructure_response(rules):
+ for rule in rules["rules"]:
+ type = rule["type"]
+ rule[type] = copy.deepcopy(rule["value"])
+ del rule["value"]
+ return rules
+
+
+def get_ssid_number(name, data):
+ for ssid in data:
+ if name == ssid["name"]:
+ return ssid["number"]
+ return False
+
+
+def get_ssids(meraki, net_id):
+ path = meraki.construct_path("get_ssids", net_id=net_id)
+ return meraki.request(path, method="GET")
+
+
+def get_rules(meraki, net_id, number):
+ path = meraki.construct_path("get_all", net_id=net_id, custom={"number": number})
+ response = meraki.request(path, method="GET")
+ if meraki.status == 200:
+ return response
+
+
+def main():
+ # define the available arguments/parameters that a user can pass to
+ # the module
+
+ application_arg_spec = dict(
+ id=dict(type="str"),
+ name=dict(type="str"),
+ )
+
+ rule_arg_spec = dict(
+ policy=dict(type="str", choices=["deny"], default="deny"),
+ type=dict(
+ type="str",
+ choices=["application", "application_category", "host", "ip_range", "port"],
+ ),
+ ip_range=dict(type="str"),
+ application=dict(type="dict", default=None, options=application_arg_spec),
+ host=dict(type="str"),
+ port=dict(type="str"),
+ )
+
+ argument_spec = meraki_argument_spec()
+ argument_spec.update(
+ state=dict(type="str", choices=["present", "query"], default="present"),
+ net_name=dict(type="str"),
+ net_id=dict(type="str"),
+ number=dict(type="str", aliases=["ssid_number"]),
+ ssid_name=dict(type="str", aliases=["ssid"]),
+ rules=dict(type="list", default=None, elements="dict", options=rule_arg_spec),
+ categories=dict(type="bool"),
+ )
+
+ # the AnsibleModule object will be our abstraction working with Ansible
+ # this includes instantiation, a couple of common attr would be the
+ # args/params passed to the execution, as well as if the module
+ # supports check mode
+ module = AnsibleModule(
+ argument_spec=argument_spec,
+ supports_check_mode=True,
+ )
+ meraki = MerakiModule(module, function="mr_l7_firewall")
+
+ # check for argument completeness
+ if meraki.params["rules"]:
+ for rule in meraki.params["rules"]:
+ if rule["type"] == "application" and rule["application"] is None:
+ meraki.fail_json(
+ msg="application argument is required when type is application."
+ )
+ elif rule["type"] == "application_category" and rule["application"] is None:
+ meraki.fail_json(
+ msg="application argument is required when type is application_category."
+ )
+ elif rule["type"] == "host" and rule["host"] is None:
+ meraki.fail_json(msg="host argument is required when type is host.")
+ elif rule["type"] == "port" and rule["port"] is None:
+ meraki.fail_json(msg="port argument is required when type is port.")
+
+ meraki.params["follow_redirects"] = "all"
+ query_ssids_urls = {"mr_l7_firewall": "/networks/{net_id}/wireless/ssids"}
+ query_urls = {
+ "mr_l7_firewall": "/networks/{net_id}/wireless/ssids/{number}/firewall/l7FirewallRules"
+ }
+ update_urls = {
+ "mr_l7_firewall": "/networks/{net_id}/wireless/ssids/{number}/firewall/l7FirewallRules"
+ }
+ query_category_urls = {
+ "mr_l7_firewall": "/networks/{net_id}/trafficShaping/applicationCategories"
+ }
+
+ meraki.url_catalog["get_all"].update(query_urls)
+ meraki.url_catalog["get_categories"] = query_category_urls
+ meraki.url_catalog["get_ssids"] = query_ssids_urls
+ meraki.url_catalog["update"] = update_urls
+
+ payload = None
+
+ # manipulate or modify the state as needed (this is going to be the
+ # part where your module will do what it needs to do)
+ org_id = meraki.params["org_id"]
+ orgs = None
+ if org_id is None:
+ orgs = meraki.get_orgs()
+ for org in orgs:
+ if org["name"] == meraki.params["org_name"]:
+ org_id = org["id"]
+ net_id = meraki.params["net_id"]
+ if net_id is None:
+ if orgs is None:
+ orgs = meraki.get_orgs()
+ net_id = meraki.get_net_id(
+ net_name=meraki.params["net_name"], data=meraki.get_nets(org_id=org_id)
+ )
+ number = meraki.params["number"]
+ if meraki.params["ssid_name"]:
+ ssids = get_ssids(meraki, net_id)
+ number = get_ssid_number(meraki.params["ssid_name"], ssids)
+
+ if meraki.params["state"] == "query":
+ if meraki.params["categories"] is True: # Output only applications
+ meraki.result["data"] = get_applications(meraki, net_id)
+ else:
+ meraki.result["data"] = restructure_response(
+ get_rules(meraki, net_id, number)
+ )
+ elif meraki.params["state"] == "present":
+ rules = get_rules(meraki, net_id, number)
+ path = meraki.construct_path(
+ "get_all", net_id=net_id, custom={"number": number}
+ )
+
+ # Detect if no rules are given, special case
+ if len(meraki.params["rules"]) == 0:
+ # Conditionally wrap parameters in rules makes it comparable
+ if isinstance(meraki.params["rules"], list):
+ param_rules = {"rules": meraki.params["rules"]}
+ else:
+ param_rules = meraki.params["rules"]
+ if meraki.is_update_required(rules, param_rules):
+ if meraki.module.check_mode is True:
+ meraki.result["data"] = meraki.params["rules"]
+ meraki.result["changed"] = True
+ meraki.exit_json(**meraki.result)
+ payload = {"rules": []}
+ response = meraki.request(
+ path, method="PUT", payload=json.dumps(payload)
+ )
+ meraki.result["data"] = response
+ meraki.result["changed"] = True
+ meraki.exit_json(**meraki.result)
+ else:
+ meraki.result["data"] = param_rules
+ meraki.exit_json(**meraki.result)
+ if meraki.params["rules"]:
+ payload = {"rules": []}
+ for rule in meraki.params["rules"]:
+ payload["rules"].append(assemble_payload(meraki, net_id, rule))
+ else:
+ payload = dict()
+ if meraki.is_update_required(rules, payload, force_include="id"):
+ if meraki.module.check_mode is True:
+ response = restructure_response(payload)
+ meraki.generate_diff(restructure_response(rules), response)
+ meraki.result["data"] = response
+ meraki.result["changed"] = True
+ meraki.exit_json(**meraki.result)
+ response = meraki.request(path, method="PUT", payload=json.dumps(payload))
+ response = restructure_response(response)
+ if meraki.status == 200:
+ meraki.generate_diff(restructure_response(rules), response)
+ meraki.result["data"] = response
+ meraki.result["changed"] = True
+ else:
+ if meraki.module.check_mode is True:
+ meraki.result["data"] = rules
+ meraki.result["changed"] = False
+ meraki.exit_json(**meraki.result)
+ meraki.result["data"] = payload
+
+ # in the event of a successful module execution, you will want to
+ # simple AnsibleModule.exit_json(), passing the key/value results
+ meraki.exit_json(**meraki.result)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/ansible_collections/cisco/meraki/plugins/modules/meraki_mr_radio.py b/ansible_collections/cisco/meraki/plugins/modules/meraki_mr_radio.py
new file mode 100644
index 000000000..c61f1e225
--- /dev/null
+++ b/ansible_collections/cisco/meraki/plugins/modules/meraki_mr_radio.py
@@ -0,0 +1,490 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Copyright: (c) 2021, Tyler Christiansen (@supertylerc) <code@tylerc.me>
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+
+__metaclass__ = type
+
+ANSIBLE_METADATA = {
+ "metadata_version": "1.1",
+ "status": ["preview"],
+ "supported_by": "community",
+}
+
+DOCUMENTATION = r"""
+---
+module: meraki_mr_radio
+short_description: Manage device radio settings for Meraki wireless networks
+description:
+- Allows for configuration of radio settings in Meraki MR wireless networks.
+options:
+ state:
+ description:
+ - Query or edit radio settings on a device.
+ type: str
+ choices: [present, query]
+ default: present
+ serial:
+ description:
+ - Serial number of a device to query.
+ type: str
+ rf_profile_id:
+ description:
+ - The ID of an RF profile to assign to the device.
+ - If the value of this parameter is null, the appropriate basic RF profile (indoor or outdoor) will be assigned to the device.
+ - Assigning an RF profile will clear ALL manually configured overrides on the device (channel width, channel, power).
+ type: str
+ rf_profile_name:
+ description:
+ - The name of an RF profile to assign to the device.
+ - Similar to ``rf_profile_id``, but requires ``net_id`` (preferred) or ``net_name``.
+ type: str
+ net_name:
+ description:
+ - Name of a network.
+ aliases: [network]
+ type: str
+ net_id:
+ description:
+ - ID of a network.
+ type: str
+ five_ghz_settings:
+ description:
+ - Manual radio settings for 5 GHz.
+ type: dict
+ default: {}
+ suboptions:
+ target_power:
+ description:
+ - Set a manual target power for 5 GHz.
+ - Can be between '8' or '30' or null for using auto power range.
+ type: int
+ channel_width:
+ description:
+ - Sets a manual channel for 5 GHz.
+ - Can be '0', '20', '40', or '80' or null for using auto channel width.
+ choices:
+ - auto
+ - '20'
+ - '40'
+ - '80'
+ type: str
+ channel:
+ description:
+ - Sets a manual channel for 5 GHz.
+ type: int
+ choices:
+ - 36
+ - 40
+ - 44
+ - 48
+ - 52
+ - 56
+ - 60
+ - 64
+ - 100
+ - 104
+ - 108
+ - 112
+ - 116
+ - 120
+ - 124
+ - 128
+ - 132
+ - 136
+ - 140
+ - 144
+ - 149
+ - 153
+ - 157
+ - 161
+ - 165
+ two_four_ghz_settings:
+ description:
+ - Manual radio settings for 2.4 GHz.
+ type: dict
+ default: {}
+ suboptions:
+ target_power:
+ description:
+ - Set a manual target power for 2.4 GHz.
+ - Can be between '5' or '30' or null for using auto power range.
+ type: int
+ channel:
+ description:
+ - Sets a manual channel for 2.4 GHz.
+ - Can be '1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12', '13' or '14' or null for using auto channel.
+ choices:
+ - 1
+ - 2
+ - 3
+ - 4
+ - 5
+ - 6
+ - 7
+ - 8
+ - 9
+ - 10
+ - 11
+ - 12
+ - 13
+ - 14
+ type: int
+author:
+- Tyler Christiansen (@supertylerc)
+extends_documentation_fragment: cisco.meraki.meraki
+"""
+
+EXAMPLES = r"""
+- name: Query a device's radio configuration
+ meraki_mr_radio:
+ auth_key: abc123
+ org_name: YourOrg
+ net_name: YourNet
+ serial: YourSerialNumber
+ state: query
+ delegate_to: localhost
+- name: Configure a device's radios
+ meraki_mr_radio:
+ auth_key: abc123
+ org_name: YourOrg
+ net_name: YourNet
+ serial: YourSerialNumber
+ state: present
+ five_ghz_settings:
+ channel: 56
+ channel_width: 20
+ target_power: 10
+ two_four_ghz_settings:
+ channel: 6
+ target_power: 12
+ rf_profile_name: Test Profile
+ delegate_to: localhost
+"""
+
+RETURN = r"""
+data:
+ description: RF settings configured on a specific device.
+ returned: success
+ type: complex
+ contains:
+ serial:
+ description:
+ - Serial number of the device that was configured.
+ type: str
+ returned: success
+ sample: xyz
+ rf_profile_id:
+ description:
+ - The ID of an RF profile assigned to the device.
+ - Null indicates the appropriate basic RF profile (indoor or outdoor) is assigned to the device.
+ type: str
+ returned: success
+ sample: null
+ five_ghz_settings:
+ description:
+ - Configured manual radio settings for 5 GHz.
+ type: dict
+ returned: success
+ contains:
+ target_power:
+ description:
+ - Configured manual target power for 5 GHz.
+ - Null indicates auto power.
+ type: int
+ sample: 25
+ channel_width:
+ description:
+ - Configured manual channel for 5 GHz.
+ - Null indicates auto channel width.
+ type: str
+ sample: 40
+ channel:
+ description:
+ - Configured manual channel for 5 GHz.
+ - Null indicates auto channel.
+ type: str
+ sample: 56
+ two_four_ghz_settings:
+ description:
+ - Configured manual radio settings for 2.4 GHz.
+ type: dict
+ returned: success
+ contains:
+ target_power:
+ description:
+ - Configured manual target power for 2.4 GHz.
+ - Null indicates auto power.
+ type: int
+ sample: 15
+ channel:
+ description:
+ - Configured manual channel for 2.4 GHz.
+ - Null indicates auto channel.
+ type: str
+ sample: 11
+"""
+
+from ansible.module_utils.basic import AnsibleModule, json
+from ansible_collections.cisco.meraki.plugins.module_utils.network.meraki.meraki import (
+ MerakiModule,
+ meraki_argument_spec,
+)
+from re import sub
+
+
+def convert_to_camel_case(string):
+ """Convert "snake case" to "camel case"."""
+ string = sub(r"(_|-)+", " ", string).title().replace(" ", "")
+ return string[0].lower() + string[1:]
+
+
+def _construct_payload(params):
+ """Recursively convert key names.
+
+ This function recursively updates all dict key names from the
+ Ansible/Python-style "snake case" to the format the Meraki API expects
+ ("camel case").
+ """
+ payload = {}
+ for k, v in params.items():
+ if isinstance(v, dict):
+ v = _construct_payload(v)
+ payload[convert_to_camel_case(k)] = v
+ return payload
+
+
+def construct_payload(meraki):
+ """Construct API payload dict.
+
+ This function uses a ``valid_params`` variable to filter out keys from
+ ``meraki.params`` that aren't relevant to the Meraki API call.
+ """
+ params = {}
+ valid_params = [
+ "serial",
+ "rf_profile_id",
+ "five_ghz_settings",
+ "two_four_ghz_settings",
+ ]
+ for k, v in meraki.params.items():
+ if k not in valid_params:
+ continue
+ params[k] = v
+ return _construct_payload(params)
+
+
+# Ansible spec for the 'five_ghz_settings' param, based on Meraki API.
+FIVE_GHZ_SETTINGS_SPEC = {
+ "options": {
+ "target_power": {"type": "int"},
+ "channel_width": {"type": "str", "choices": ["auto", "20", "40", "80"]},
+ "channel": {
+ "type": "int",
+ "choices": [
+ 36,
+ 40,
+ 44,
+ 48,
+ 52,
+ 56,
+ 60,
+ 64,
+ 100,
+ 104,
+ 108,
+ 112,
+ 116,
+ 120,
+ 124,
+ 128,
+ 132,
+ 136,
+ 140,
+ 144,
+ 149,
+ 153,
+ 157,
+ 161,
+ 165,
+ ],
+ },
+ },
+ "default": {},
+}
+
+# Ansible spec for the 'two_four_ghz_settings' param, based on Meraki API.
+TWO_FOUR_GHZ_SETTINGS_SPEC = {
+ "options": {
+ "target_power": {"type": "int"},
+ "channel": {
+ "type": "int",
+ "choices": list(range(1, 15)),
+ },
+ },
+ "default": {},
+}
+
+
+def get_org_id(meraki):
+ """Get the Organization ID based on the Organization Name."""
+ org_id = meraki.params["org_id"]
+ if org_id is None:
+ org_id = meraki.get_org_id(meraki.params["org_name"])
+ return org_id
+
+
+def get_net_id(meraki):
+ """Get the Network ID based on a Network Name."""
+ net_id = meraki.params["net_id"]
+ if net_id is None:
+ org_id = get_org_id(meraki)
+ nets = meraki.get_nets(org_id=org_id)
+ net_id = meraki.get_net_id(org_id, meraki.params["net_name"], data=nets)
+ return net_id
+
+
+def get_rf_profile_id(meraki):
+ """Get the RF Profile ID for a given RF Profile Name."""
+ profile_id = meraki.params["rf_profile_id"]
+ profile_name = meraki.params["rf_profile_name"]
+ if profile_id is None and profile_name is not None:
+ net_id = get_net_id(meraki)
+ path = meraki.construct_path("get_all", "mr_rf_profile", net_id=net_id)
+ profiles = meraki.request(path, method="GET")
+ profile_id = next(
+ (
+ profile["id"]
+ for profile in profiles
+ if profile["name"] == meraki.params["rf_profile_name"]
+ ),
+ None,
+ )
+ return profile_id
+
+
+def meraki_get_radio_settings(meraki):
+ """Query the Meraki API for the current radio settings."""
+ path = meraki.construct_path("get_one", custom={"serial": meraki.params["serial"]})
+ return meraki.request(path, method="GET")
+
+
+def _meraki_run_query(meraki):
+ """Get the radio settings on the specified device."""
+ meraki.result["data"] = meraki_get_radio_settings(meraki)
+ meraki.exit_json(**meraki.result)
+
+
+def _meraki_run_present(meraki):
+ """Update / check radio settings for a specified device."""
+ original = meraki_get_radio_settings(meraki)
+ meraki.result["data"] = original
+ meraki.params["rf_profile_id"] = get_rf_profile_id(meraki)
+ payload = construct_payload(meraki)
+ if meraki.is_update_required(original, payload) is True:
+ if meraki.check_mode is True:
+ meraki.result["data"] = payload
+ meraki.result["changed"] = True
+ meraki.result["original"] = original
+ else:
+ path = meraki.construct_path(
+ "update", custom={"serial": meraki.params["serial"]}
+ )
+ response = meraki.request(path, method="PUT", payload=json.dumps(payload))
+ meraki.result["data"] = response
+ meraki.result["changed"] = True
+ meraki.exit_json(**meraki.result)
+
+
+def _meraki_run_func_lookup(state):
+ """Return the function that `meraki_run` will use based on `state`."""
+ return {
+ "query": _meraki_run_query,
+ "present": _meraki_run_present,
+ }[state]
+
+
+def meraki_run(meraki):
+ """Perform API calls and generate responses based on the 'state' param."""
+ meraki_run_func = _meraki_run_func_lookup(meraki.params["state"])
+ meraki_run_func(meraki)
+
+
+def update_url_catalog(meraki):
+ """Update the URL catalog available to the helper."""
+ query_urls = {"mr_radio": "/devices/{serial}/wireless/radio/settings"}
+ update_urls = {"mr_radio": "/devices/{serial}/wireless/radio/settings"}
+ query_all_urls = {"mr_rf_profile": "/networks/{net_id}/wireless/rfProfiles"}
+
+ meraki.url_catalog["get_one"].update(query_urls)
+ meraki.url_catalog["update"] = update_urls
+ meraki.url_catalog["get_all"].update(query_all_urls)
+
+
+def validate_params(params):
+ """Validate parameters passed to this Ansible module.
+
+ When ``rf_profile_name`` is passed, we need to lookup the ID as that's what
+ the API expects. To look up the RF Profile ID, we need the network ID,
+ which might be derived based on the network name, in which case we need the
+ org ID or org name to complete the process.
+ """
+ valid = True
+ msg = None
+
+ if (
+ params["rf_profile_name"] is not None
+ and params["rf_profile_id"] is None
+ and params["net_id"] is None
+ ):
+ if params["net_name"] is None:
+ valid = False
+ msg = "When specifying 'rf_profile_name', either 'net_id' (preferred) or 'net_name' is required."
+ elif params["org_id"] is None and params["org_name"] is None:
+ valid = False
+ msg = "When specifying 'rf_profile_name' and omitting 'net_id', either 'org_id' (preferred) or 'org_name' is required."
+ return (valid, msg)
+
+
+def main():
+ argument_spec = meraki_argument_spec()
+ argument_spec.update(
+ state=dict(type="str", choices=["present", "query"], default="present"),
+ org_name=dict(type="str", aliases=["organization"]),
+ org_id=dict(type="str"),
+ net_name=dict(type="str", aliases=["network"]),
+ net_id=dict(type="str"),
+ serial=dict(type="str"),
+ rf_profile_name=(dict(type="str")),
+ rf_profile_id=dict(type="str"),
+ five_ghz_settings=dict(
+ type="dict",
+ options=FIVE_GHZ_SETTINGS_SPEC["options"],
+ default=FIVE_GHZ_SETTINGS_SPEC["default"],
+ ),
+ two_four_ghz_settings=dict(
+ type="dict",
+ options=TWO_FOUR_GHZ_SETTINGS_SPEC["options"],
+ default=TWO_FOUR_GHZ_SETTINGS_SPEC["default"],
+ ),
+ )
+
+ module = AnsibleModule(
+ argument_spec=argument_spec,
+ supports_check_mode=True,
+ )
+ meraki = MerakiModule(module, function="mr_radio")
+ meraki.params["follow_redirects"] = "all"
+ valid_params, msg = validate_params(meraki.params)
+ if not valid_params:
+ meraki.fail_json(msg=msg)
+
+ update_url_catalog(meraki)
+ meraki_run(meraki)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/ansible_collections/cisco/meraki/plugins/modules/meraki_mr_rf_profile.py b/ansible_collections/cisco/meraki/plugins/modules/meraki_mr_rf_profile.py
new file mode 100644
index 000000000..cd8d9c41e
--- /dev/null
+++ b/ansible_collections/cisco/meraki/plugins/modules/meraki_mr_rf_profile.py
@@ -0,0 +1,662 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Copyright: (c) 2018, Kevin Breit (@kbreit) <kevin.breit@kevinbreit.net>
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+ANSIBLE_METADATA = {
+ 'metadata_version': '1.1',
+ 'status': ['preview'],
+ 'supported_by': 'community'
+}
+
+DOCUMENTATION = r'''
+---
+module: meraki_mr_rf_profile
+short_description: Manage RF profiles for Meraki wireless networks
+description:
+- Allows for configuration of radio frequency (RF) profiles in Meraki MR wireless networks.
+options:
+ state:
+ description:
+ - Query, edit, or delete wireless RF profile settings.
+ type: str
+ choices: [ present, query, absent]
+ default: present
+ net_name:
+ description:
+ - Name of network.
+ type: str
+ net_id:
+ description:
+ - ID of network.
+ type: str
+ profile_id:
+ description:
+ - Unique identifier of existing RF profile.
+ type: str
+ aliases: [ id ]
+ band_selection_type:
+ description:
+ - Sets whether band selection is assigned per access point or SSID.
+ - This param is required on creation.
+ choices: [ ssid, ap ]
+ type: str
+ min_bitrate_type:
+ description:
+ - Type of minimum bitrate.
+ choices: [ band, ssid ]
+ type: str
+ name:
+ description:
+ - The unique name of the new profile.
+ - This param is required on creation.
+ type: str
+ client_balancing_enabled:
+ description:
+ - Steers client to best available access point.
+ type: bool
+ ap_band_settings:
+ description:
+ - Settings that will be enabled if selectionType is set to 'ap'.
+ type: dict
+ suboptions:
+ mode:
+ description:
+ - Sets which RF band the AP will support.
+ choices: [ 2.4ghz, 5ghz, dual ]
+ aliases: [ band_operation_mode ]
+ type: str
+ band_steering_enabled:
+ description:
+ - Steers client to most open band.
+ type: bool
+ five_ghz_settings:
+ description:
+ - Settings related to 5Ghz band.
+ type: dict
+ suboptions:
+ max_power:
+ description:
+ - Sets max power (dBm) of 5Ghz band.
+ - Can be integer between 8 and 30.
+ type: int
+ min_power:
+ description:
+ - Sets minmimum power (dBm) of 5Ghz band.
+ - Can be integer between 8 and 30.
+ type: int
+ min_bitrate:
+ description:
+ - Sets minimum bitrate (Mbps) of 5Ghz band.
+ choices: [ 6, 9, 12, 18, 24, 36, 48, 54 ]
+ type: int
+ rxsop:
+ description:
+ - The RX-SOP level controls the sensitivity of the radio.
+ - It is strongly recommended to use RX-SOP only after consulting a wireless expert.
+ - RX-SOP can be configured in the range of -65 to -95 (dBm).
+ type: int
+ channel_width:
+ description:
+ - Sets channel width (MHz) for 5Ghz band.
+ choices: [ auto, '20', '40', '80' ]
+ type: str
+ valid_auto_channels:
+ description:
+ - Sets valid auto channels for 5Ghz band.
+ type: list
+ elements: int
+ choices: [36,
+ 40,
+ 44,
+ 48,
+ 52,
+ 56,
+ 60,
+ 64,
+ 100,
+ 104,
+ 108,
+ 112,
+ 116,
+ 120,
+ 124,
+ 128,
+ 132,
+ 136,
+ 140,
+ 144,
+ 149,
+ 153,
+ 157,
+ 161,
+ 165]
+ two_four_ghz_settings:
+ description:
+ - Settings related to 2.4Ghz band
+ type: dict
+ suboptions:
+ max_power:
+ description:
+ - Sets max power (dBm) of 2.4Ghz band.
+ - Can be integer between 5 and 30.
+ type: int
+ min_power:
+ description:
+ - Sets minmimum power (dBm) of 2.4Ghz band.
+ - Can be integer between 5 and 30.
+ type: int
+ min_bitrate:
+ description:
+ - Sets minimum bitrate (Mbps) of 2.4Ghz band.
+ choices: [ 1, 2, 5.5, 6, 9, 11, 12, 18, 24, 36, 48, 54 ]
+ type: float
+ rxsop:
+ description:
+ - The RX-SOP level controls the sensitivity of the radio.
+ - It is strongly recommended to use RX-SOP only after consulting a wireless expert.
+ - RX-SOP can be configured in the range of -65 to -95 (dBm).
+ type: int
+ ax_enabled:
+ description:
+ - Determines whether ax radio on 2.4Ghz band is on or off.
+ type: bool
+ valid_auto_channels:
+ description:
+ - Sets valid auto channels for 2.4Ghz band.
+ choices: [ 1, 6, 11 ]
+ type: list
+ elements: int
+author:
+- Kevin Breit (@kbreit)
+extends_documentation_fragment: cisco.meraki.meraki
+'''
+
+EXAMPLES = r'''
+- name: Create RF profile in check mode
+ meraki_mr_rf_profile:
+ auth_key: abc123
+ org_name: YourOrg
+ net_name: YourNet
+ state: present
+ name: Test Profile
+ band_selection_type: ap
+ client_balancing_enabled: True
+ ap_band_settings:
+ mode: dual
+ band_steering_enabled: true
+ five_ghz_settings:
+ max_power: 10
+ min_bitrate: 12
+ min_power: 8
+ rxsop: -65
+ channel_width: 20
+ valid_auto_channels:
+ - 36
+ - 40
+ - 44
+ two_four_ghz_settings:
+ max_power: 10
+ min_bitrate: 12
+ min_power: 8
+ rxsop: -65
+ ax_enabled: false
+ valid_auto_channels:
+ - 1
+ delegate_to: localhost
+
+- name: Query all RF profiles
+ meraki_mr_rf_profile:
+ auth_key: abc123
+ org_name: YourOrg
+ net_name: YourNet
+ state: query
+ delegate_to: localhost
+
+- name: Query one RF profile by ID
+ meraki_mr_rf_profile:
+ auth_key: abc123
+ org_name: YourOrg
+ net_name: YourNet
+ state: query
+ profile_id: '{{ profile_id }}'
+ delegate_to: localhost
+
+- name: Update profile
+ meraki_mr_rf_profile:
+ auth_key: abc123
+ org_name: YourOrg
+ net_name: YourNet
+ state: present
+ profile_id: 12345
+ band_selection_type: ap
+ client_balancing_enabled: True
+ ap_band_settings:
+ mode: dual
+ band_steering_enabled: true
+ five_ghz_settings:
+ max_power: 10
+ min_bitrate: 12
+ min_power: 8
+ rxsop: -65
+ channel_width: 20
+ valid_auto_channels:
+ - 36
+ - 44
+ two_four_ghz_settings:
+ max_power: 10
+ min_bitrate: 12
+ min_power: 8
+ rxsop: -75
+ ax_enabled: false
+ valid_auto_channels:
+ - 1
+ delegate_to: localhost
+
+- name: Delete RF profile
+ meraki_mr_rf_profile:
+ auth_key: abc123
+ org_name: YourOrg
+ net_name: YourNet
+ state: absent
+ profile_id: 12345
+ delegate_to: localhost
+'''
+
+RETURN = r'''
+data:
+ description: List of wireless RF profile settings.
+ returned: success
+ type: complex
+ contains:
+ id:
+ description:
+ - Unique identifier of existing RF profile.
+ type: str
+ returned: success
+ sample: 12345
+ band_selection_type:
+ description:
+ - Sets whether band selection is assigned per access point or SSID.
+ - This param is required on creation.
+ type: str
+ returned: success
+ sample: ap
+ min_bitrate_type:
+ description:
+ - Type of minimum bitrate.
+ type: str
+ returned: success
+ sample: ssid
+ name:
+ description:
+ - The unique name of the new profile.
+ - This param is required on creation.
+ type: str
+ returned: success
+ sample: Guest RF profile
+ client_balancing_enabled:
+ description:
+ - Steers client to best available access point.
+ type: bool
+ returned: success
+ sample: true
+ ap_band_settings:
+ description:
+ - Settings that will be enabled if selectionType is set to 'ap'.
+ type: complex
+ returned: success
+ contains:
+ mode:
+ description:
+ - Sets which RF band the AP will support.
+ type: str
+ returned: success
+ sample: dual
+ band_steering_enabled:
+ description:
+ - Steers client to most open band.
+ type: bool
+ returned: success
+ sample: true
+ five_ghz_settings:
+ description:
+ - Settings related to 5Ghz band.
+ type: complex
+ returned: success
+ contains:
+ max_power:
+ description:
+ - Sets max power (dBm) of 5Ghz band.
+ - Can be integer between 8 and 30.
+ type: int
+ returned: success
+ sample: 12
+ min_power:
+ description:
+ - Sets minmimum power (dBm) of 5Ghz band.
+ - Can be integer between 8 and 30.
+ type: int
+ returned: success
+ sample: 12
+ min_bitrate:
+ description:
+ - Sets minimum bitrate (Mbps) of 5Ghz band.
+ type: int
+ returned: success
+ sample: 6
+ rxsop:
+ description:
+ - The RX-SOP level controls the sensitivity of the radio.
+ type: int
+ returned: success
+ sample: -70
+ channel_width:
+ description:
+ - Sets channel width (MHz) for 5Ghz band.
+ type: str
+ returned: success
+ sample: auto
+ valid_auto_channels:
+ description:
+ - Sets valid auto channels for 5Ghz band.
+ type: list
+ returned: success
+ two_four_ghz_settings:
+ description:
+ - Settings related to 2.4Ghz band
+ type: complex
+ returned: success
+ contains:
+ max_power:
+ description:
+ - Sets max power (dBm) of 2.4Ghz band.
+ type: int
+ returned: success
+ sample: 12
+ min_power:
+ description:
+ - Sets minmimum power (dBm) of 2.4Ghz band.
+ type: int
+ returned: success
+ sample: 12
+ min_bitrate:
+ description:
+ - Sets minimum bitrate (Mbps) of 2.4Ghz band.
+ type: float
+ returned: success
+ sample: 5.5
+ rxsop:
+ description:
+ - The RX-SOP level controls the sensitivity of the radio.
+ type: int
+ returned: success
+ sample: -70
+ ax_enabled:
+ description:
+ - Determines whether ax radio on 2.4Ghz band is on or off.
+ type: bool
+ returned: success
+ sample: true
+ valid_auto_channels:
+ description:
+ - Sets valid auto channels for 2.4Ghz band.
+ type: list
+ returned: success
+ sample: 6
+'''
+
+from ansible.module_utils.basic import AnsibleModule, json
+from ansible_collections.cisco.meraki.plugins.module_utils.network.meraki.meraki import MerakiModule, meraki_argument_spec
+
+
+def get_profile(meraki, profiles, name):
+ for profile in profiles:
+ if profile['name'] == name:
+ return profile
+ return None
+
+
+def construct_payload(meraki):
+ payload = {}
+ if meraki.params['name'] is not None:
+ payload['name'] = meraki.params['name']
+ if meraki.params['band_selection_type'] is not None:
+ payload['bandSelectionType'] = meraki.params['band_selection_type']
+ if meraki.params['min_bitrate_type'] is not None:
+ payload['minBitrateType'] = meraki.params['min_bitrate_type']
+ if meraki.params['client_balancing_enabled'] is not None:
+ payload['clientBalancingEnabled'] = meraki.params['client_balancing_enabled']
+ if meraki.params['ap_band_settings'] is not None:
+ payload['apBandSettings'] = {}
+ if meraki.params['ap_band_settings']['mode'] is not None:
+ payload['apBandSettings']['bandOperationMode'] = meraki.params['ap_band_settings']['mode']
+ if meraki.params['ap_band_settings']['band_steering_enabled'] is not None:
+ payload['apBandSettings']['bandSteeringEnabled'] = meraki.params['ap_band_settings']['band_steering_enabled']
+ if meraki.params['five_ghz_settings'] is not None:
+ payload['fiveGhzSettings'] = {}
+ if meraki.params['five_ghz_settings']['max_power'] is not None:
+ payload['fiveGhzSettings']['maxPower'] = meraki.params['five_ghz_settings']['max_power']
+ if meraki.params['five_ghz_settings']['min_bitrate'] is not None:
+ payload['fiveGhzSettings']['minBitrate'] = meraki.params['five_ghz_settings']['min_bitrate']
+ if meraki.params['five_ghz_settings']['min_power'] is not None:
+ payload['fiveGhzSettings']['minPower'] = meraki.params['five_ghz_settings']['min_power']
+ if meraki.params['five_ghz_settings']['rxsop'] is not None:
+ payload['fiveGhzSettings']['rxsop'] = meraki.params['five_ghz_settings']['rxsop']
+ if meraki.params['five_ghz_settings']['channel_width'] is not None:
+ payload['fiveGhzSettings']['channelWidth'] = meraki.params['five_ghz_settings']['channel_width']
+ if meraki.params['five_ghz_settings']['valid_auto_channels'] is not None:
+ payload['fiveGhzSettings']['validAutoChannels'] = meraki.params['five_ghz_settings']['valid_auto_channels']
+ if meraki.params['two_four_ghz_settings'] is not None:
+ payload['twoFourGhzSettings'] = {}
+ if meraki.params['two_four_ghz_settings']['max_power'] is not None:
+ payload['twoFourGhzSettings']['maxPower'] = meraki.params['two_four_ghz_settings']['max_power']
+ if meraki.params['two_four_ghz_settings']['min_bitrate'] is not None:
+ payload['twoFourGhzSettings']['minBitrate'] = meraki.params['two_four_ghz_settings']['min_bitrate']
+ if meraki.params['two_four_ghz_settings']['min_power'] is not None:
+ payload['twoFourGhzSettings']['minPower'] = meraki.params['two_four_ghz_settings']['min_power']
+ if meraki.params['two_four_ghz_settings']['rxsop'] is not None:
+ payload['twoFourGhzSettings']['rxsop'] = meraki.params['two_four_ghz_settings']['rxsop']
+ if meraki.params['two_four_ghz_settings']['ax_enabled'] is not None:
+ payload['twoFourGhzSettings']['axEnabled'] = meraki.params['two_four_ghz_settings']['ax_enabled']
+ if meraki.params['two_four_ghz_settings']['valid_auto_channels'] is not None:
+ payload['twoFourGhzSettings']['validAutoChannels'] = meraki.params['two_four_ghz_settings']['valid_auto_channels']
+ return payload
+
+
+def main():
+ # define the available arguments/parameters that a user can pass to
+ # the module
+
+ band_arg_spec = dict(mode=dict(type='str', aliases=['band_operation_mode'], choices=['2.4ghz', '5ghz', 'dual']),
+ band_steering_enabled=dict(type='bool'),
+ )
+
+ five_arg_spec = dict(max_power=dict(type='int'),
+ min_bitrate=dict(type='int', choices=[6, 9, 12, 18, 24, 36, 48, 54]),
+ min_power=dict(type='int'),
+ rxsop=dict(type='int'),
+ channel_width=dict(type='str', choices=['auto', '20', '40', '80']),
+ valid_auto_channels=dict(type='list', elements='int', choices=[36,
+ 40,
+ 44,
+ 48,
+ 52,
+ 56,
+ 60,
+ 64,
+ 100,
+ 104,
+ 108,
+ 112,
+ 116,
+ 120,
+ 124,
+ 128,
+ 132,
+ 136,
+ 140,
+ 144,
+ 149,
+ 153,
+ 157,
+ 161,
+ 165]),
+ )
+
+ two_arg_spec = dict(max_power=dict(type='int'),
+ min_bitrate=dict(type='float', choices=[1,
+ 2,
+ 5.5,
+ 6,
+ 9,
+ 11,
+ 12,
+ 18,
+ 24,
+ 36,
+ 48,
+ 54]),
+ min_power=dict(type='int'),
+ rxsop=dict(type='int'),
+ ax_enabled=dict(type='bool'),
+ valid_auto_channels=dict(type='list', elements='int', choices=[1, 6, 11]),
+ )
+
+ argument_spec = meraki_argument_spec()
+ argument_spec.update(state=dict(type='str', choices=['present', 'query', 'absent'], default='present'),
+ org_name=dict(type='str', aliases=['organization']),
+ org_id=dict(type='str'),
+ net_name=dict(type='str'),
+ net_id=dict(type='str'),
+ profile_id=dict(type='str', aliases=['id']),
+ band_selection_type=dict(type='str', choices=['ssid', 'ap']),
+ min_bitrate_type=dict(type='str', choices=['band', 'ssid']),
+ name=dict(type='str'),
+ client_balancing_enabled=dict(type='bool'),
+ ap_band_settings=dict(type='dict', options=band_arg_spec),
+ five_ghz_settings=dict(type='dict', options=five_arg_spec),
+ two_four_ghz_settings=dict(type='dict', options=two_arg_spec),
+ )
+
+ # the AnsibleModule object will be our abstraction working with Ansible
+ # this includes instantiation, a couple of common attr would be the
+ # args/params passed to the execution, as well as if the module
+ # supports check mode
+ module = AnsibleModule(argument_spec=argument_spec,
+ supports_check_mode=True,
+ )
+ meraki = MerakiModule(module, function='mr_rf_profile')
+
+ meraki.params['follow_redirects'] = 'all'
+
+ query_all_urls = {'mr_rf_profile': '/networks/{net_id}/wireless/rfProfiles'}
+ query_urls = {'mr_rf_profile': '/networks/{net_id}/wireless/rfProfiles/{profile_id}'}
+ create_urls = {'mr_rf_profile': '/networks/{net_id}/wireless/rfProfiles'}
+ update_urls = {'mr_rf_profile': '/networks/{net_id}/wireless/rfProfiles/{profile_id}'}
+ delete_urls = {'mr_rf_profile': '/networks/{net_id}/wireless/rfProfiles/{profile_id}'}
+
+ meraki.url_catalog['get_all'].update(query_all_urls)
+ meraki.url_catalog['get_one'].update(query_urls)
+ meraki.url_catalog['create'] = create_urls
+ meraki.url_catalog['update'] = update_urls
+ meraki.url_catalog['delete'] = delete_urls
+
+ if meraki.params['five_ghz_settings'] is not None:
+ if meraki.params['five_ghz_settings']['max_power'] is not None:
+ if meraki.params['five_ghz_settings']['max_power'] < 8 or meraki.params['five_ghz_settings']['max_power'] > 30:
+ meraki.fail_json(msg="5ghz max power must be between 8 and 30.")
+ if meraki.params['five_ghz_settings']['min_power'] is not None:
+ if meraki.params['five_ghz_settings']['min_power'] < 8 or meraki.params['five_ghz_settings']['min_power'] > 30:
+ meraki.fail_json(msg="5ghz min power must be between 8 and 30.")
+ if meraki.params['five_ghz_settings']['rxsop'] is not None:
+ if meraki.params['five_ghz_settings']['rxsop'] < -95 or meraki.params['five_ghz_settings']['rxsop'] > -65:
+ meraki.fail_json(msg="5ghz min power must be between 8 and 30.")
+ if meraki.params['two_four_ghz_settings'] is not None:
+ if meraki.params['two_four_ghz_settings']['max_power'] is not None:
+ if meraki.params['two_four_ghz_settings']['max_power'] < 5 or meraki.params['two_four_ghz_settings']['max_power'] > 30:
+ meraki.fail_json(msg="5ghz max power must be between 5 and 30.")
+ if meraki.params['two_four_ghz_settings']['min_power'] is not None:
+ if meraki.params['two_four_ghz_settings']['min_power'] < 5 or meraki.params['two_four_ghz_settings']['min_power'] > 30:
+ meraki.fail_json(msg="5ghz min power must be between 5 and 30.")
+ if meraki.params['two_four_ghz_settings']['rxsop'] is not None:
+ if meraki.params['two_four_ghz_settings']['rxsop'] < -95 or meraki.params['two_four_ghz_settings']['rxsop'] > -65:
+ meraki.fail_json(msg="5ghz min power must be between 8 and 30.")
+
+ org_id = meraki.params['org_id']
+ net_id = meraki.params['net_id']
+ profile_id = meraki.params['profile_id']
+ profile = None
+ profiles = None
+ if org_id is None:
+ org_id = meraki.get_org_id(meraki.params['org_name'])
+ if net_id is None:
+ nets = meraki.get_nets(org_id=org_id)
+ net_id = meraki.get_net_id(org_id, meraki.params['net_name'], data=nets)
+ if profile_id is None:
+ path = meraki.construct_path('get_all', net_id=net_id)
+ profiles = meraki.request(path, method='GET')
+ # profile = get_profile(meraki, profiles, meraki.params['name'])
+ profile_id = next((profile['id'] for profile in profiles if profile['name'] == meraki.params['name']), None)
+
+ if meraki.params['state'] == 'query':
+ if profile_id is not None:
+ path = meraki.construct_path('get_one', net_id=net_id, custom={'profile_id': profile_id})
+ result = meraki.request(path, method='GET')
+ meraki.result['data'] = result
+ meraki.exit_json(**meraki.result)
+ if profiles is None:
+ path = meraki.construct_path('get_all', net_id=net_id)
+ profiles = meraki.request(path, method='GET')
+ meraki.result['data'] = profiles
+ meraki.exit_json(**meraki.result)
+ elif meraki.params['state'] == 'present':
+ payload = construct_payload(meraki)
+ if profile_id is None: # Create a new RF profile
+ if meraki.check_mode is True:
+ meraki.result['data'] = payload
+ meraki.result['changed'] = True
+ meraki.exit_json(**meraki.result)
+ path = meraki.construct_path('create', net_id=net_id)
+ response = meraki.request(path, method='POST', payload=json.dumps(payload))
+ meraki.result['data'] = response
+ meraki.result['changed'] = True
+ meraki.exit_json(**meraki.result)
+ else:
+ path = meraki.construct_path('get_one', net_id=net_id, custom={'profile_id': profile_id})
+ original = meraki.request(path, method='GET')
+ if meraki.is_update_required(original, payload) is True:
+ if meraki.check_mode is True:
+ meraki.result['data'] = payload
+ meraki.result['changed'] = True
+ meraki.exit_json(**meraki.result)
+ path = meraki.construct_path('update', net_id=net_id, custom={'profile_id': profile_id})
+ response = meraki.request(path, method='PUT', payload=json.dumps(payload))
+ meraki.result['data'] = response
+ meraki.result['changed'] = True
+ meraki.exit_json(**meraki.result)
+ else:
+ meraki.result['data'] = original
+ meraki.exit_json(**meraki.result)
+ elif meraki.params['state'] == 'absent':
+ if meraki.check_mode is True:
+ meraki.result['data'] = {}
+ meraki.result['changed'] = True
+ meraki.exit_json(**meraki.result)
+ path = meraki.construct_path('delete', net_id=net_id, custom={'profile_id': profile_id})
+ response = meraki.request(path, method='DELETE')
+ meraki.result['data'] = {}
+ meraki.result['changed'] = True
+ meraki.exit_json(**meraki.result)
+
+ # in the event of a successful module execution, you will want to
+ # simple AnsibleModule.exit_json(), passing the key/value results
+ meraki.exit_json(**meraki.result)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/ansible_collections/cisco/meraki/plugins/modules/meraki_mr_settings.py b/ansible_collections/cisco/meraki/plugins/modules/meraki_mr_settings.py
new file mode 100644
index 000000000..7858c208c
--- /dev/null
+++ b/ansible_collections/cisco/meraki/plugins/modules/meraki_mr_settings.py
@@ -0,0 +1,221 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Copyright: (c) 2018, Kevin Breit (@kbreit) <kevin.breit@kevinbreit.net>
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+ANSIBLE_METADATA = {
+ 'metadata_version': '1.1',
+ 'status': ['preview'],
+ 'supported_by': 'community'
+}
+
+DOCUMENTATION = r'''
+---
+module: meraki_mr_settings
+short_description: Manage general settings for Meraki wireless networks
+description:
+- Allows for configuration of general settings in Meraki MR wireless networks.
+options:
+ state:
+ description:
+ - Query or edit wireless settings.
+ type: str
+ choices: [ present, query]
+ default: present
+ net_name:
+ description:
+ - Name of network.
+ type: str
+ net_id:
+ description:
+ - ID of network.
+ type: str
+ upgrade_strategy:
+ description:
+ - The upgrade strategy to apply to the network.
+ - Requires firmware version MR 26.8 or higher.
+ choices: [ minimize_upgrade_time, minimize_client_downtime ]
+ type: str
+ ipv6_bridge_enabled:
+ description:
+ - Toggle for enabling or disabling IPv6 bridging in a network.
+ - If enabled, SSIDs must also be configured to use bridge mode.
+ type: bool
+ led_lights_on:
+ description:
+ - Toggle for enabling or disabling LED lights on all APs in the network.
+ type: bool
+ location_analytics_enabled:
+ description:
+ - Toggle for enabling or disabling location analytics for your network.
+ type: bool
+ meshing_enabled:
+ description: Toggle for enabling or disabling meshing in a network.
+ type: bool
+author:
+- Kevin Breit (@kbreit)
+extends_documentation_fragment: cisco.meraki.meraki
+'''
+
+EXAMPLES = r'''
+- name: Query all settings
+ meraki_mr_settings:
+ auth_key: abc123
+ org_name: YourOrg
+ net_name: YourNet
+ state: query
+ delegate_to: localhost
+- name: Configure settings
+ meraki_mr_settings:
+ auth_key: abc123
+ org_name: YourOrg
+ net_name: YourNet
+ state: present
+ upgrade_strategy: minimize_upgrade_time
+ ipv6_bridge_enabled: false
+ led_lights_on: true
+ location_analytics_enabled: true
+ meshing_enabled: true
+ delegate_to: localhost
+'''
+
+RETURN = r'''
+data:
+ description: List of wireless settings.
+ returned: success
+ type: complex
+ contains:
+ upgrade_strategy:
+ description:
+ - The upgrade strategy to apply to the network.
+ - Requires firmware version MR 26.8 or higher.
+ type: str
+ returned: success
+ sample: minimize_upgrade_time
+ ipv6_bridge_enabled:
+ description:
+ - Toggle for enabling or disabling IPv6 bridging in a network.
+ - If enabled, SSIDs must also be configured to use bridge mode.
+ type: bool
+ returned: success
+ sample: True
+ led_lights_on:
+ description:
+ - Toggle for enabling or disabling LED lights on all APs in the network.
+ type: bool
+ returned: success
+ sample: True
+ location_analytics_enabled:
+ description:
+ - Toggle for enabling or disabling location analytics for your network.
+ type: bool
+ returned: success
+ sample: True
+ meshing_enabled:
+ description: Toggle for enabling or disabling meshing in a network.
+ type: bool
+ returned: success
+ sample: True
+'''
+
+from ansible.module_utils.basic import AnsibleModule, json
+from ansible_collections.cisco.meraki.plugins.module_utils.network.meraki.meraki import MerakiModule, meraki_argument_spec
+from re import sub
+
+
+def convert_to_camel_case(string):
+ string = sub(r"(_|-)+", " ", string).title().replace(" ", "")
+ return string[0].lower() + string[1:]
+
+
+def construct_payload(meraki):
+ payload = {}
+ if meraki.params['upgrade_strategy'] is not None:
+ payload['upgradeStrategy'] = convert_to_camel_case(meraki.params['upgrade_strategy'])
+ if meraki.params['ipv6_bridge_enabled'] is not None:
+ payload['ipv6BridgeEnabled'] = meraki.params['ipv6_bridge_enabled']
+ if meraki.params['led_lights_on'] is not None:
+ payload['ledLightsOn'] = meraki.params['led_lights_on']
+ if meraki.params['location_analytics_enabled'] is not None:
+ payload['locationAnalyticsEnabled'] = meraki.params['location_analytics_enabled']
+ if meraki.params['meshing_enabled'] is not None:
+ payload['meshingEnabled'] = meraki.params['meshing_enabled']
+ return payload
+
+
+def main():
+ # define the available arguments/parameters that a user can pass to
+ # the module
+ argument_spec = meraki_argument_spec()
+ argument_spec.update(state=dict(type='str', choices=['present', 'query'], default='present'),
+ org_name=dict(type='str', aliases=['organization']),
+ org_id=dict(type='str'),
+ net_name=dict(type='str'),
+ net_id=dict(type='str'),
+ upgrade_strategy=dict(type='str', choices=['minimize_upgrade_time',
+ 'minimize_client_downtime']),
+ ipv6_bridge_enabled=dict(type='bool'),
+ led_lights_on=dict(type='bool'),
+ location_analytics_enabled=dict(type='bool'),
+ meshing_enabled=dict(type='bool'),
+ )
+
+ # the AnsibleModule object will be our abstraction working with Ansible
+ # this includes instantiation, a couple of common attr would be the
+ # args/params passed to the execution, as well as if the module
+ # supports check mode
+ module = AnsibleModule(argument_spec=argument_spec,
+ supports_check_mode=True,
+ )
+ meraki = MerakiModule(module, function='mr_settings')
+
+ meraki.params['follow_redirects'] = 'all'
+
+ query_urls = {'mr_settings': '/networks/{net_id}/wireless/settings'}
+ update_urls = {'mr_settings': '/networks/{net_id}/wireless/settings'}
+
+ meraki.url_catalog['get_one'].update(query_urls)
+ meraki.url_catalog['update'] = update_urls
+
+ org_id = meraki.params['org_id']
+ net_id = meraki.params['net_id']
+ if org_id is None:
+ org_id = meraki.get_org_id(meraki.params['org_name'])
+ if net_id is None:
+ nets = meraki.get_nets(org_id=org_id)
+ net_id = meraki.get_net_id(org_id, meraki.params['net_name'], data=nets)
+
+ if meraki.params['state'] == 'query':
+ path = meraki.construct_path('get_one', net_id=net_id)
+ response = meraki.request(path, method='GET')
+ meraki.result['data'] = response
+ meraki.exit_json(**meraki.result)
+ elif meraki.params['state'] == 'present':
+ path = meraki.construct_path('get_one', net_id=net_id)
+ original = meraki.request(path, method='GET')
+ payload = construct_payload(meraki)
+ if meraki.is_update_required(original, payload) is True:
+ if meraki.check_mode is True:
+ meraki.result['data'] = payload
+ meraki.result['changed'] = True
+ meraki.exit_json(**meraki.result)
+ path = meraki.construct_path('update', net_id=net_id)
+ response = meraki.request(path, method='PUT', payload=json.dumps(payload))
+ meraki.result['data'] = response
+ meraki.result['changed'] = True
+ meraki.exit_json(**meraki.result)
+ else:
+ meraki.result['data'] = original
+ meraki.exit_json(**meraki.result)
+
+ # in the event of a successful module execution, you will want to
+ # simple AnsibleModule.exit_json(), passing the key/value results
+ meraki.exit_json(**meraki.result)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/ansible_collections/cisco/meraki/plugins/modules/meraki_mr_ssid.py b/ansible_collections/cisco/meraki/plugins/modules/meraki_mr_ssid.py
new file mode 100644
index 000000000..f6c242e0d
--- /dev/null
+++ b/ansible_collections/cisco/meraki/plugins/modules/meraki_mr_ssid.py
@@ -0,0 +1,744 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Copyright: (c) 2018, Kevin Breit (@kbreit) <kevin.breit@kevinbreit.net>
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+
+__metaclass__ = type
+
+ANSIBLE_METADATA = {
+ "metadata_version": "1.1",
+ "status": ["preview"],
+ "supported_by": "community",
+}
+
+DOCUMENTATION = r"""
+---
+module: meraki_mr_ssid
+short_description: Manage wireless SSIDs in the Meraki cloud
+description:
+- Allows for management of SSIDs in a Meraki wireless environment.
+notes:
+- Deleting an SSID does not delete RADIUS servers.
+options:
+ state:
+ description:
+ - Specifies whether SNMP information should be queried or modified.
+ type: str
+ choices: [ absent, query, present ]
+ default: present
+ number:
+ description:
+ - SSID number within network.
+ type: int
+ aliases: [ssid_number]
+ name:
+ description:
+ - Name of SSID.
+ type: str
+ net_name:
+ description:
+ - Name of network.
+ type: str
+ net_id:
+ description:
+ - ID of network.
+ type: str
+ enabled:
+ description:
+ - Enable or disable SSID network.
+ type: bool
+ auth_mode:
+ description:
+ - Set authentication mode of network.
+ type: str
+ choices: [open, psk, open-with-radius, 8021x-meraki, 8021x-radius]
+ encryption_mode:
+ description:
+ - Set encryption mode of network.
+ type: str
+ choices: [wpa, eap, wpa-eap]
+ psk:
+ description:
+ - Password for wireless network.
+ - Requires auth_mode to be set to psk.
+ type: str
+ wpa_encryption_mode:
+ description:
+ - Encryption mode within WPA specification.
+ type: str
+ choices: [WPA1 and WPA2, WPA2 only, WPA3 Transition Mode, WPA3 only]
+ splash_page:
+ description:
+ - Set to enable splash page and specify type of splash.
+ type: str
+ choices: ['None',
+ 'Click-through splash page',
+ 'Billing',
+ 'Password-protected with Meraki RADIUS',
+ 'Password-protected with custom RADIUS',
+ 'Password-protected with Active Directory',
+ 'Password-protected with LDAP',
+ 'SMS authentication',
+ 'Systems Manager Sentry',
+ 'Facebook Wi-Fi',
+ 'Google OAuth',
+ 'Sponsored guest',
+ 'Cisco ISE']
+ radius_servers:
+ description:
+ - List of RADIUS servers.
+ type: list
+ elements: dict
+ suboptions:
+ host:
+ description:
+ - IP address or hostname of RADIUS server.
+ type: str
+ required: true
+ port:
+ description:
+ - Port number RADIUS server is listening to.
+ type: int
+ secret:
+ description:
+ - RADIUS password.
+ - Setting password is not idempotent.
+ type: str
+ radius_proxy_enabled:
+ description:
+ - Enable or disable RADIUS Proxy on SSID.
+ type: bool
+ radius_coa_enabled:
+ description:
+ - Enable or disable RADIUS CoA (Change of Authorization) on SSID.
+ type: bool
+ radius_failover_policy:
+ description:
+ - Set client access policy in case RADIUS servers aren't available.
+ type: str
+ choices: [Deny access, Allow access]
+ radius_load_balancing_policy:
+ description:
+ - Set load balancing policy when multiple RADIUS servers are specified.
+ type: str
+ choices: [Strict priority order, Round robin]
+ radius_accounting_enabled:
+ description:
+ - Enable or disable RADIUS accounting.
+ type: bool
+ radius_accounting_servers:
+ description:
+ - List of RADIUS servers for RADIUS accounting.
+ type: list
+ elements: dict
+ suboptions:
+ host:
+ description:
+ - IP address or hostname of RADIUS server.
+ type: str
+ required: true
+ port:
+ description:
+ - Port number RADIUS server is listening to.
+ type: int
+ secret:
+ description:
+ - RADIUS password.
+ - Setting password is not idempotent.
+ type: str
+ ip_assignment_mode:
+ description:
+ - Method of which SSID uses to assign IP addresses.
+ type: str
+ choices: ['NAT mode',
+ 'Bridge mode',
+ 'Layer 3 roaming',
+ 'Layer 3 roaming with a concentrator',
+ 'VPN']
+ lan_isolation_enabled:
+ description:
+ - Enable or disable Layer 2 Lan isolation.
+ - Requires C(ip_assignment_mode) to be C(Bridge mode).
+ type: bool
+ use_vlan_tagging:
+ description:
+ - Set whether to use VLAN tagging.
+ - Requires C(default_vlan_id) to be set.
+ type: bool
+ visible:
+ description:
+ - Enable or disable whether APs should broadcast this SSID.
+ type: bool
+ default_vlan_id:
+ description:
+ - Default VLAN ID.
+ - Requires C(ip_assignment_mode) to be C(Bridge mode) or C(Layer 3 roaming).
+ type: int
+ vlan_id:
+ description:
+ - ID number of VLAN on SSID.
+ - Requires C(ip_assignment_mode) to be C(ayer 3 roaming with a concentrator) or C(VPN).
+ type: int
+ ap_tags_vlan_ids:
+ description:
+ - List of VLAN tags.
+ - Requires C(ip_assignment_mode) to be C(Bridge mode) or C(Layer 3 roaming).
+ - Requires C(use_vlan_tagging) to be C(True).
+ type: list
+ elements: dict
+ suboptions:
+ tags:
+ description:
+ - List of AP tags.
+ type: list
+ elements: str
+ vlan_id:
+ description:
+ - Numerical identifier that is assigned to the VLAN.
+ type: int
+ walled_garden_enabled:
+ description:
+ - Enable or disable walled garden functionality.
+ type: bool
+ walled_garden_ranges:
+ description:
+ - List of walled garden ranges.
+ type: list
+ elements: str
+ available_on_all_aps:
+ description:
+ - Set whether all APs should broadcast the SSID or if it should be restricted to APs matching any availability tags.
+ - Requires C(ap_availability_tags) to be defined when set to C(False).
+ type: bool
+ ap_availability_tags:
+ description:
+ - Set whether SSID will be broadcast by APs with tags matching any of the tags in this list.
+ - Requires C(available_on_all_aps) to be C(false).
+ type: list
+ elements: str
+ min_bitrate:
+ description:
+ - Minimum bitrate (Mbps) allowed on SSID.
+ type: float
+ choices: [1, 2, 5.5, 6, 9, 11, 12, 18, 24, 36, 48, 54]
+ band_selection:
+ description:
+ - Set band selection mode.
+ type: str
+ choices: ['Dual band operation', '5 GHz band only', 'Dual band operation with Band Steering']
+ per_client_bandwidth_limit_up:
+ description:
+ - Maximum bandwidth in Mbps devices on SSID can upload.
+ type: int
+ per_client_bandwidth_limit_down:
+ description:
+ - Maximum bandwidth in Mbps devices on SSID can download.
+ type: int
+ concentrator_network_id:
+ description:
+ - The concentrator to use for 'Layer 3 roaming with a concentrator' or 'VPN'.
+ type: str
+ enterprise_admin_access:
+ description:
+ - Whether SSID is accessible by enterprise administrators.
+ type: str
+ choices: ['access disabled', 'access enabled']
+ splash_guest_sponsor_domains:
+ description:
+ - List of valid sponsor email domains for sponsored guest portal.
+ type: list
+ elements: str
+author:
+- Kevin Breit (@kbreit)
+extends_documentation_fragment: cisco.meraki.meraki
+"""
+
+EXAMPLES = r"""
+- name: Enable and name SSID
+ meraki_ssid:
+ auth_key: abc123
+ state: present
+ org_name: YourOrg
+ net_name: WiFi
+ name: GuestSSID
+ enabled: true
+ visible: true
+ delegate_to: localhost
+
+- name: Set PSK with invalid encryption mode
+ meraki_ssid:
+ auth_key: abc123
+ state: present
+ org_name: YourOrg
+ net_name: WiFi
+ name: GuestSSID
+ auth_mode: psk
+ psk: abc1234
+ encryption_mode: eap
+ ignore_errors: yes
+ delegate_to: localhost
+
+- name: Configure RADIUS servers
+ meraki_ssid:
+ auth_key: abc123
+ state: present
+ org_name: YourOrg
+ net_name: WiFi
+ name: GuestSSID
+ auth_mode: open-with-radius
+ radius_servers:
+ - host: 192.0.1.200
+ port: 1234
+ secret: abc98765
+ delegate_to: localhost
+
+- name: Enable click-through splash page
+ meraki_ssid:
+ auth_key: abc123
+ state: present
+ org_name: YourOrg
+ net_name: WiFi
+ name: GuestSSID
+ splash_page: Click-through splash page
+ delegate_to: localhost
+"""
+
+RETURN = r"""
+data:
+ description: List of wireless SSIDs.
+ returned: success
+ type: complex
+ contains:
+ number:
+ description: Zero-based index number for SSIDs.
+ returned: success
+ type: int
+ sample: 0
+ name:
+ description:
+ - Name of wireless SSID.
+ - This value is what is broadcasted.
+ returned: success
+ type: str
+ sample: CorpWireless
+ enabled:
+ description: Enabled state of wireless network.
+ returned: success
+ type: bool
+ sample: true
+ splash_page:
+ description: Splash page to show when user authenticates.
+ returned: success
+ type: str
+ sample: Click-through splash page
+ ssid_admin_accessible:
+ description: Whether SSID is administratively accessible.
+ returned: success
+ type: bool
+ sample: true
+ auth_mode:
+ description: Authentication method.
+ returned: success
+ type: str
+ sample: psk
+ psk:
+ description: Secret wireless password.
+ returned: success
+ type: str
+ sample: SecretWiFiPass
+ encryption_mode:
+ description: Wireless traffic encryption method.
+ returned: success
+ type: str
+ sample: wpa
+ wpa_encryption_mode:
+ description: Enabled WPA versions.
+ returned: success
+ type: str
+ sample: WPA2 only
+ ip_assignment_mode:
+ description: Wireless client IP assignment method.
+ returned: success
+ type: str
+ sample: NAT mode
+ min_bitrate:
+ description: Minimum bitrate a wireless client can connect at.
+ returned: success
+ type: int
+ sample: 11
+ band_selection:
+ description: Wireless RF frequency wireless network will be broadcast on.
+ returned: success
+ type: str
+ sample: 5 GHz band only
+ per_client_bandwidth_limit_up:
+ description: Maximum upload bandwidth a client can use.
+ returned: success
+ type: int
+ sample: 1000
+ per_client_bandwidth_limit_down:
+ description: Maximum download bandwidth a client can use.
+ returned: success
+ type: int
+ sample: 0
+"""
+
+from ansible.module_utils.basic import AnsibleModule, json
+from ansible_collections.cisco.meraki.plugins.module_utils.network.meraki.meraki import (
+ MerakiModule,
+ meraki_argument_spec,
+)
+
+
+def get_available_number(data):
+ for item in data:
+ if "Unconfigured SSID" in item["name"]:
+ return item["number"]
+ return False
+
+
+def get_ssid_number(name, data):
+ for ssid in data:
+ if name == ssid["name"]:
+ return ssid["number"]
+ return False
+
+
+def get_ssids(meraki, net_id):
+ path = meraki.construct_path("get_all", net_id=net_id)
+ return meraki.request(path, method="GET")
+
+
+def construct_payload(meraki):
+ param_map = {
+ "name": "name",
+ "enabled": "enabled",
+ "authMode": "auth_mode",
+ "encryptionMode": "encryption_mode",
+ "psk": "psk",
+ "wpaEncryptionMode": "wpa_encryption_mode",
+ "splashPage": "splash_page",
+ "radiusServers": "radius_servers",
+ "radiusProxyEnabled": "radius_proxy_enabled",
+ "radiusCoaEnabled": "radius_coa_enabled",
+ "radiusFailoverPolicy": "radius_failover_policy",
+ "radiusLoadBalancingPolicy": "radius_load_balancing_policy",
+ "radiusAccountingEnabled": "radius_accounting_enabled",
+ "radiusAccountingServers": "radius_accounting_servers",
+ "ipAssignmentMode": "ip_assignment_mode",
+ "useVlanTagging": "use_vlan_tagging",
+ "visible": "visible",
+ "concentratorNetworkId": "concentrator_network_id",
+ "vlanId": "vlan_id",
+ "lanIsolationEnabled": "lan_isolation_enabled",
+ "availableOnAllAps": "available_on_all_aps",
+ "availabilityTags": "ap_availability_tags",
+ "defaultVlanId": "default_vlan_id",
+ "apTagsAndVlanIds": "ap_tags_vlan_ids",
+ "walledGardenEnabled": "walled_garden_enabled",
+ "walledGardenRanges": "walled_garden_ranges",
+ "minBitrate": "min_bitrate",
+ "bandSelection": "band_selection",
+ "perClientBandwidthLimitUp": "per_client_bandwidth_limit_up",
+ "perClientBandwidthLimitDown": "per_client_bandwidth_limit_down",
+ "enterpriseAdminAccess": "enterprise_admin_access",
+ "splashGuestSponsorDomains": "splash_guest_sponsor_domains",
+ }
+
+ payload = dict()
+ for k, v in param_map.items():
+ if meraki.params[v] is not None:
+ payload[k] = meraki.params[v]
+
+ if meraki.params["ap_tags_vlan_ids"] is not None:
+ for i in payload["apTagsAndVlanIds"]:
+ try:
+ i["vlanId"] = i["vlan_id"]
+ del i["vlan_id"]
+ except KeyError:
+ pass
+
+ return payload
+
+
+def per_line_to_str(data):
+ return data.replace("\n", " ")
+
+
+def main():
+ default_payload = {
+ "name": "Unconfigured SSID",
+ "auth_mode": "open",
+ "splashPage": "None",
+ "perClientBandwidthLimitUp": 0,
+ "perClientBandwidthLimitDown": 0,
+ "ipAssignmentMode": "NAT mode",
+ "enabled": False,
+ "bandSelection": "Dual band operation",
+ "minBitrate": 11,
+ }
+
+ # define the available arguments/parameters that a user can pass to
+ # the module
+ radius_arg_spec = dict(
+ host=dict(type="str", required=True),
+ port=dict(type="int"),
+ secret=dict(type="str", no_log=True),
+ )
+ vlan_arg_spec = dict(
+ tags=dict(type="list", elements="str"),
+ vlan_id=dict(type="int"),
+ )
+
+ argument_spec = meraki_argument_spec()
+ argument_spec.update(
+ state=dict(
+ type="str", choices=["absent", "present", "query"], default="present"
+ ),
+ number=dict(type="int", aliases=["ssid_number"]),
+ name=dict(type="str"),
+ org_name=dict(type="str", aliases=["organization"]),
+ org_id=dict(type="str"),
+ net_name=dict(type="str"),
+ net_id=dict(type="str"),
+ enabled=dict(type="bool"),
+ auth_mode=dict(
+ type="str",
+ choices=["open", "psk", "open-with-radius", "8021x-meraki", "8021x-radius"],
+ ),
+ encryption_mode=dict(type="str", choices=["wpa", "eap", "wpa-eap"]),
+ psk=dict(type="str", no_log=True),
+ wpa_encryption_mode=dict(
+ type="str",
+ choices=["WPA1 and WPA2", "WPA2 only", "WPA3 Transition Mode", "WPA3 only"],
+ ),
+ splash_page=dict(
+ type="str",
+ choices=[
+ "None",
+ "Click-through splash page",
+ "Billing",
+ "Password-protected with Meraki RADIUS",
+ "Password-protected with custom RADIUS",
+ "Password-protected with Active Directory",
+ "Password-protected with LDAP",
+ "SMS authentication",
+ "Systems Manager Sentry",
+ "Facebook Wi-Fi",
+ "Google OAuth",
+ "Sponsored guest",
+ "Cisco ISE",
+ ],
+ ),
+ radius_servers=dict(
+ type="list", default=None, elements="dict", options=radius_arg_spec
+ ),
+ radius_proxy_enabled=dict(type="bool"),
+ radius_coa_enabled=dict(type="bool"),
+ radius_failover_policy=dict(
+ type="str", choices=["Deny access", "Allow access"]
+ ),
+ radius_load_balancing_policy=dict(
+ type="str", choices=["Strict priority order", "Round robin"]
+ ),
+ radius_accounting_enabled=dict(type="bool"),
+ radius_accounting_servers=dict(
+ type="list", elements="dict", options=radius_arg_spec
+ ),
+ ip_assignment_mode=dict(
+ type="str",
+ choices=[
+ "NAT mode",
+ "Bridge mode",
+ "Layer 3 roaming",
+ "Layer 3 roaming with a concentrator",
+ "VPN",
+ ],
+ ),
+ use_vlan_tagging=dict(type="bool"),
+ visible=dict(type="bool"),
+ lan_isolation_enabled=dict(type="bool"),
+ available_on_all_aps=dict(type="bool"),
+ ap_availability_tags=dict(type="list", elements="str"),
+ concentrator_network_id=dict(type="str"),
+ vlan_id=dict(type="int"),
+ default_vlan_id=dict(type="int"),
+ ap_tags_vlan_ids=dict(
+ type="list", default=None, elements="dict", options=vlan_arg_spec
+ ),
+ walled_garden_enabled=dict(type="bool"),
+ walled_garden_ranges=dict(type="list", elements="str"),
+ min_bitrate=dict(
+ type="float", choices=[1, 2, 5.5, 6, 9, 11, 12, 18, 24, 36, 48, 54]
+ ),
+ band_selection=dict(
+ type="str",
+ choices=[
+ "Dual band operation",
+ "5 GHz band only",
+ "Dual band operation with Band Steering",
+ ],
+ ),
+ per_client_bandwidth_limit_up=dict(type="int"),
+ per_client_bandwidth_limit_down=dict(type="int"),
+ enterprise_admin_access=dict(
+ type="str", choices=["access disabled", "access enabled"]
+ ),
+ splash_guest_sponsor_domains=dict(type="list", elements="str"),
+ )
+
+ # the AnsibleModule object will be our abstraction working with Ansible
+ # this includes instantiation, a couple of common attr would be the
+ # args/params passed to the execution, as well as if the module
+ # supports check mode
+ module = AnsibleModule(
+ argument_spec=argument_spec,
+ supports_check_mode=True,
+ )
+ meraki = MerakiModule(module, function="ssid")
+ meraki.params["follow_redirects"] = "all"
+
+ query_urls = {"ssid": "/networks/{net_id}/wireless/ssids"}
+ query_url = {"ssid": "/networks/{net_id}/wireless/ssids/{number}"}
+ update_url = {"ssid": "/networks/{net_id}/wireless/ssids/"}
+
+ meraki.url_catalog["get_all"].update(query_urls)
+ meraki.url_catalog["get_one"].update(query_url)
+ meraki.url_catalog["update"] = update_url
+
+ payload = None
+
+ # execute checks for argument completeness
+ if meraki.params["psk"]:
+ if meraki.params["auth_mode"] != "psk":
+ meraki.fail_json(msg="PSK is only allowed when auth_mode is set to psk")
+ if meraki.params["encryption_mode"] != "wpa":
+ meraki.fail_json(msg="PSK requires encryption_mode be set to wpa")
+ if meraki.params["radius_servers"]:
+ if meraki.params["auth_mode"] not in ("open-with-radius", "8021x-radius"):
+ meraki.fail_json(
+ msg="radius_servers requires auth_mode to be open-with-radius or 8021x-radius"
+ )
+ if meraki.params["radius_accounting_enabled"] is True:
+ if meraki.params["auth_mode"] not in ("open-with-radius", "8021x-radius"):
+ meraki.fail_json(
+ msg="radius_accounting_enabled is only allowed when auth_mode is open-with-radius or 8021x-radius"
+ )
+ if meraki.params["radius_accounting_servers"] is True:
+ if (
+ meraki.params["auth_mode"] not in ("open-with-radius", "8021x-radius")
+ or meraki.params["radius_accounting_enabled"] is False
+ ):
+ meraki.fail_json(
+ msg="radius_accounting_servers is only allowed when auth_mode is open_with_radius or 8021x-radius and \
+ radius_accounting_enabled is true"
+ )
+ if meraki.params["use_vlan_tagging"] is True:
+ if meraki.params["default_vlan_id"] is None:
+ meraki.fail_json(
+ msg="default_vlan_id is required when use_vlan_tagging is True"
+ )
+ if meraki.params["lan_isolation_enabled"] is not None:
+ if meraki.params["ip_assignment_mode"] not in ("Bridge mode"):
+ meraki.fail_json(
+ msg="lan_isolation_enabled is only allowed when ip_assignment_mode is Bridge mode"
+ )
+ if meraki.params["available_on_all_aps"] is False:
+ if not meraki.params["ap_availability_tags"]:
+ meraki.fail_json(
+ msg="available_on_all_aps is only allowed to be false when ap_availability_tags is defined"
+ )
+ if meraki.params["ap_availability_tags"]:
+ if meraki.params["available_on_all_aps"] is not False:
+ meraki.fail_json(
+ msg="ap_availability_tags is only allowed when available_on_all_aps is false"
+ )
+ # manipulate or modify the state as needed (this is going to be the
+ # part where your module will do what it needs to do)
+ org_id = meraki.params["org_id"]
+ net_id = meraki.params["net_id"]
+ if org_id is None:
+ org_id = meraki.get_org_id(meraki.params["org_name"])
+ if net_id is None:
+ nets = meraki.get_nets(org_id=org_id)
+ net_id = meraki.get_net_id(org_id, meraki.params["net_name"], data=nets)
+
+ if meraki.params["state"] == "query":
+ if meraki.params["name"]:
+ ssid_id = get_ssid_number(meraki.params["name"], get_ssids(meraki, net_id))
+ path = meraki.construct_path(
+ "get_one", net_id=net_id, custom={"number": ssid_id}
+ )
+ meraki.result["data"] = meraki.request(path, method="GET")
+ elif meraki.params["number"] is not None:
+ path = meraki.construct_path(
+ "get_one", net_id=net_id, custom={"number": meraki.params["number"]}
+ )
+ meraki.result["data"] = meraki.request(path, method="GET")
+ else:
+ meraki.result["data"] = get_ssids(meraki, net_id)
+ elif meraki.params["state"] == "present":
+ payload = construct_payload(meraki)
+ ssids = get_ssids(meraki, net_id)
+ number = meraki.params["number"]
+ if number is None:
+ number = get_ssid_number(meraki.params["name"], ssids)
+ original = ssids[number]
+ if meraki.is_update_required(original, payload, optional_ignore=["secret"]):
+ ssid_id = meraki.params["number"]
+ if ssid_id is None: # Name should be used to lookup number
+ ssid_id = get_ssid_number(meraki.params["name"], ssids)
+ if ssid_id is False:
+ ssid_id = get_available_number(ssids)
+ if ssid_id is False:
+ meraki.fail_json(
+ msg="No unconfigured SSIDs are available. Specify a number."
+ )
+ if meraki.check_mode is True:
+ original.update(payload)
+ meraki.result["data"] = original
+ meraki.result["changed"] = True
+ meraki.exit_json(**meraki.result)
+ path = meraki.construct_path("update", net_id=net_id) + str(ssid_id)
+ result = meraki.request(path, "PUT", payload=json.dumps(payload))
+ meraki.result["data"] = result
+ meraki.result["changed"] = True
+ else:
+ meraki.result["data"] = original
+ elif meraki.params["state"] == "absent":
+ ssids = get_ssids(meraki, net_id)
+ ssid_id = meraki.params["number"]
+ if ssid_id is None: # Name should be used to lookup number
+ ssid_id = get_ssid_number(meraki.params["name"], ssids)
+ if ssid_id is False:
+ # This will return True as long as there's an unclaimed SSID number!
+ ssid_id = get_available_number(ssids)
+ # There are no available SSIDs or SSID numbers
+ if ssid_id is False:
+ meraki.fail_json(
+ msg="No SSID found by specified name and no numbers unclaimed."
+ )
+ meraki.result["changed"] = False
+ meraki.result["data"] = {}
+ meraki.exit_json(**meraki.result)
+ if meraki.check_mode is True:
+ meraki.result["data"] = {}
+ meraki.result["changed"] = True
+ meraki.exit_json(**meraki.result)
+ path = meraki.construct_path("update", net_id=net_id) + str(ssid_id)
+ payload = default_payload
+ payload["name"] = payload["name"] + " " + str(ssid_id + 1)
+ result = meraki.request(path, "PUT", payload=json.dumps(payload))
+ meraki.result["data"] = result
+ meraki.result["changed"] = True
+
+ # in the event of a successful module execution, you will want to
+ # simple AnsibleModule.exit_json(), passing the key/value results
+ meraki.exit_json(**meraki.result)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/ansible_collections/cisco/meraki/plugins/modules/meraki_ms_access_list.py b/ansible_collections/cisco/meraki/plugins/modules/meraki_ms_access_list.py
new file mode 100644
index 000000000..bd5e9205f
--- /dev/null
+++ b/ansible_collections/cisco/meraki/plugins/modules/meraki_ms_access_list.py
@@ -0,0 +1,319 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Copyright: (c) 2019, Kevin Breit (@kbreit) <kevin.breit@kevinbreit.net>
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+ANSIBLE_METADATA = {
+ 'metadata_version': '1.1',
+ 'status': ['preview'],
+ 'supported_by': 'community'
+}
+
+DOCUMENTATION = r'''
+---
+module: meraki_ms_access_list
+short_description: Manage access lists for Meraki switches in the Meraki cloud
+version_added: "0.1.0"
+description:
+- Configure and query information about access lists on Meraki switches within the Meraki cloud.
+notes:
+- Some of the options are likely only used for developers within Meraki.
+options:
+ state:
+ description:
+ - Specifies whether object should be queried, created/modified, or removed.
+ choices: [absent, present, query]
+ default: query
+ type: str
+ net_name:
+ description:
+ - Name of network which configuration is applied to.
+ aliases: [network]
+ type: str
+ net_id:
+ description:
+ - ID of network which configuration is applied to.
+ type: str
+ rules:
+ description:
+ - List of access control rules.
+ type: list
+ elements: dict
+ suboptions:
+ comment:
+ description:
+ - Description of the rule.
+ type: str
+ policy:
+ description:
+ - Action to take on matching traffic.
+ choices: [allow, deny]
+ type: str
+ ip_version:
+ description:
+ - Type of IP packets to match.
+ choices: [any, ipv4, ipv6]
+ type: str
+ protocol:
+ description:
+ - Type of protocol to match.
+ choices: [any, tcp, udp]
+ type: str
+ src_cidr:
+ description:
+ - CIDR notation of source IP address to match.
+ type: str
+ src_port:
+ description:
+ - Port number of source port to match.
+ - May be a port number or 'any'.
+ type: str
+ dst_cidr:
+ description:
+ - CIDR notation of source IP address to match.
+ type: str
+ dst_port:
+ description:
+ - Port number of destination port to match.
+ - May be a port number or 'any'.
+ type: str
+ vlan:
+ description:
+ - Incoming traffic VLAN.
+ - May be any port between 1-4095 or 'any'.
+ type: str
+author:
+ Kevin Breit (@kbreit)
+extends_documentation_fragment: cisco.meraki.meraki
+'''
+
+EXAMPLES = r'''
+- name: Set access list
+ meraki_switch_access_list:
+ auth_key: abc123
+ state: present
+ org_name: YourOrg
+ net_name: YourNet
+ rules:
+ - comment: Fake rule
+ policy: allow
+ ip_version: ipv4
+ protocol: udp
+ src_cidr: 192.0.1.0/24
+ src_port: "4242"
+ dst_cidr: 1.2.3.4/32
+ dst_port: "80"
+ vlan: "100"
+ delegate_to: localhost
+
+- name: Query access lists
+ meraki_switch_access_list:
+ auth_key: abc123
+ state: query
+ org_name: YourOrg
+ net_name: YourNet
+ delegate_to: localhost
+'''
+
+RETURN = r'''
+data:
+ description: List of administrators.
+ returned: success
+ type: complex
+ contains:
+ rules:
+ description:
+ - List of access control rules.
+ type: list
+ contains:
+ comment:
+ description:
+ - Description of the rule.
+ type: str
+ sample: User rule
+ returned: success
+ policy:
+ description:
+ - Action to take on matching traffic.
+ type: str
+ sample: allow
+ returned: success
+ ip_version:
+ description:
+ - Type of IP packets to match.
+ type: str
+ sample: ipv4
+ returned: success
+ protocol:
+ description:
+ - Type of protocol to match.
+ type: str
+ sample: udp
+ returned: success
+ src_cidr:
+ description:
+ - CIDR notation of source IP address to match.
+ type: str
+ sample: 192.0.1.0/24
+ returned: success
+ src_port:
+ description:
+ - Port number of source port to match.
+ type: str
+ sample: 1234
+ returned: success
+ dst_cidr:
+ description:
+ - CIDR notation of source IP address to match.
+ type: str
+ sample: 1.2.3.4/32
+ returned: success
+ dst_port:
+ description:
+ - Port number of destination port to match.
+ type: str
+ sample: 80
+ returned: success
+ vlan:
+ description:
+ - Incoming traffic VLAN.
+ type: str
+ sample: 100
+ returned: success
+'''
+
+from ansible.module_utils.basic import AnsibleModule, json
+from ansible.module_utils.common.dict_transformations import recursive_diff
+from ansible_collections.cisco.meraki.plugins.module_utils.network.meraki.meraki import MerakiModule, meraki_argument_spec
+from copy import deepcopy
+
+
+def construct_payload(params):
+ payload = {'rules': []}
+ for rule in params['rules']:
+ new_rule = dict()
+ if 'comment' in rule:
+ new_rule['comment'] = rule['comment']
+ if 'policy' in rule:
+ new_rule['policy'] = rule['policy']
+ if 'ip_version' in rule:
+ new_rule['ipVersion'] = rule['ip_version']
+ if 'protocol' in rule:
+ new_rule['protocol'] = rule['protocol']
+ if 'src_cidr' in rule:
+ new_rule['srcCidr'] = rule['src_cidr']
+ if 'src_port' in rule:
+ try: # Need to convert to int for comparison later
+ new_rule['srcPort'] = int(rule['src_port'])
+ except ValueError:
+ pass
+ if 'dst_cidr' in rule:
+ new_rule['dstCidr'] = rule['dst_cidr']
+ if 'dst_port' in rule:
+ try: # Need to convert to int for comparison later
+ new_rule['dstPort'] = int(rule['dst_port'])
+ except ValueError:
+ pass
+ if 'vlan' in rule:
+ try: # Need to convert to int for comparison later
+ new_rule['vlan'] = int(rule['vlan'])
+ except ValueError:
+ pass
+ payload['rules'].append(new_rule)
+ return payload
+
+
+def main():
+ # define the available arguments/parameters that a user can pass to
+ # the module
+
+ rules_arg_spec = dict(comment=dict(type='str'),
+ policy=dict(type='str', choices=['allow', 'deny']),
+ ip_version=dict(type='str', choices=['ipv4', 'ipv6', 'any']),
+ protocol=dict(type='str', choices=['tcp', 'udp', 'any']),
+ src_cidr=dict(type='str'),
+ src_port=dict(type='str'),
+ dst_cidr=dict(type='str'),
+ dst_port=dict(type='str'),
+ vlan=dict(type='str'),
+ )
+
+ argument_spec = meraki_argument_spec()
+ argument_spec.update(state=dict(type='str', choices=['absent', 'present', 'query'], default='query'),
+ net_name=dict(type='str', aliases=['network']),
+ net_id=dict(type='str'),
+ rules=dict(type='list', elements='dict', options=rules_arg_spec),
+ )
+
+ # the AnsibleModule object will be our abstraction working with Ansible
+ # this includes instantiation, a couple of common attr would be the
+ # args/params passed to the execution, as well as if the module
+ # supports check mode
+ module = AnsibleModule(argument_spec=argument_spec,
+ supports_check_mode=True,
+ )
+ meraki = MerakiModule(module, function='switch_access_list')
+
+ meraki.params['follow_redirects'] = 'all'
+
+ query_url = {'switch_access_list': '/networks/{net_id}/switch/accessControlLists'}
+ update_url = {'switch_access_list': '/networks/{net_id}/switch/accessControlLists'}
+
+ meraki.url_catalog['get_all'].update(query_url)
+ meraki.url_catalog['update'] = update_url
+
+ org_id = meraki.params['org_id']
+ if org_id is None:
+ org_id = meraki.get_org_id(meraki.params['org_name'])
+ net_id = meraki.params['net_id']
+ if net_id is None:
+ nets = meraki.get_nets(org_id=org_id)
+ net_id = meraki.get_net_id(net_name=meraki.params['net_name'], data=nets)
+
+ if meraki.params['state'] == 'query':
+ path = meraki.construct_path('get_all', net_id=net_id)
+ result = meraki.request(path, method='GET')
+ if meraki.status == 200:
+ meraki.result['data'] = result
+ elif meraki.params['state'] == 'present':
+ path = meraki.construct_path('get_all', net_id=net_id)
+ original = meraki.request(path, method='GET')
+ payload = construct_payload(meraki.params)
+ comparable = deepcopy(original)
+ if len(comparable['rules']) > 1:
+ del comparable['rules'][len(comparable['rules']) - 1] # Delete the default rule for comparison
+ else:
+ del comparable['rules'][0]
+ if meraki.is_update_required(comparable, payload):
+ if meraki.check_mode is True:
+ default_rule = original['rules'][len(original['rules']) - 1]
+ payload['rules'].append(default_rule)
+ new_rules = {'rules': payload['rules']}
+ meraki.result['data'] = new_rules
+ meraki.result['changed'] = True
+ diff = recursive_diff(original, new_rules)
+ meraki.result['diff'] = {'before': diff[0],
+ 'after': diff[1]}
+ meraki.exit_json(**meraki.result)
+ path = meraki.construct_path('update', net_id=net_id)
+ response = meraki.request(path, method='PUT', payload=json.dumps(payload))
+ if meraki.status == 200:
+ diff = recursive_diff(original, payload)
+ meraki.result['data'] = response
+ meraki.result['changed'] = True
+ meraki.result['diff'] = {'before': diff[0],
+ 'after': diff[1]}
+ else:
+ meraki.result['data'] = original
+
+ # in the event of a successful module execution, you will want to
+ # simple AnsibleModule.exit_json(), passing the key/value results
+ meraki.exit_json(**meraki.result)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/ansible_collections/cisco/meraki/plugins/modules/meraki_ms_access_policies.py b/ansible_collections/cisco/meraki/plugins/modules/meraki_ms_access_policies.py
new file mode 100644
index 000000000..ebf35c35a
--- /dev/null
+++ b/ansible_collections/cisco/meraki/plugins/modules/meraki_ms_access_policies.py
@@ -0,0 +1,608 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Copyright: (c) 2022
+# Marcin Woźniak (@y0rune) <y0rune@aol.com>
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+
+__metaclass__ = type
+
+ANSIBLE_METADATA = {
+ "metadata_version": "1.1",
+ "status": ["preview"],
+ "supported_by": "community",
+}
+
+DOCUMENTATION = r"""
+---
+module: meraki_ms_access_policies
+short_description: Manage Switch Access Policies in the Meraki cloud
+description: Module for managing a Switch Access Policies in the Meraki cloud
+options:
+ state:
+ description:
+ - Specifies whether SNMP information should be queried or modified.
+ type: str
+ choices: [ absent, query, present ]
+ default: present
+ number:
+ description:
+ - Number of the access_policy.
+ type: int
+ aliases: [access_policy_number]
+ name:
+ description:
+ - Name of Access Policy.
+ type: str
+ net_id:
+ description:
+ - ID of network.
+ type: str
+ org_id:
+ description:
+ - ID of organization associated to a network.
+ type: str
+ net_name:
+ description:
+ - Name of a network.
+ aliases: [name, network]
+ type: str
+ auth_method:
+ description:
+ - Set authentication method in the policy.
+ type: str
+ choices: ["Meraki authentication", "my RADIUS server"]
+ guest_vlan:
+ description:
+ - Guest Vlan
+ type: int
+ access_policy_type:
+ description:
+ - Set type of the access policy
+ type: str
+ choices: ["802.1x", "MAC authentication bypass", "Hybrid authentication"]
+ systems_management_enrollment:
+ description:
+ - Set if the Systems Management Enrollemnt is enabled or disabled
+ type: bool
+ default: False
+ radius_servers:
+ description:
+ - List of RADIUS servers.
+ type: list
+ elements: dict
+ suboptions:
+ host:
+ description:
+ - IP address or hostname of RADIUS server.
+ type: str
+ required: true
+ port:
+ description:
+ - Port number RADIUS server is listening to.
+ type: int
+ secret:
+ description:
+ - RADIUS password.
+ - Setting password is not idempotent.
+ type: str
+ radius_testing:
+ description:
+ - Set status of testing a radius.
+ type: bool
+ default: True
+ voice_vlan_clients:
+ description:
+ - If is enabled that means Voice VLAN client require authentication
+ type: bool
+ default: True
+ radius_coa_enabled:
+ description:
+ - Enable or disable RADIUS CoA (Change of Authorization).
+ type: bool
+ radius_accounting_enabled:
+ description:
+ - Enable or disable RADIUS accounting.
+ type: bool
+ radius_accounting_servers:
+ description:
+ - List of RADIUS servers for RADIUS accounting.
+ type: list
+ elements: dict
+ suboptions:
+ host:
+ description:
+ - IP address or hostname of RADIUS server.
+ type: str
+ required: true
+ port:
+ description:
+ - Port number RADIUS server is listening to.
+ type: int
+ secret:
+ description:
+ - RADIUS password.
+ type: str
+ host_mode:
+ description:
+ - Choose the Host Mode for the access policy.
+ type: str
+ choices: ["Single-Host", "Multi-Domain", "Multi-Host", "Multi-Auth"]
+ data_vlan_id:
+ description:
+ - Set a Data VLAN ID for Critical Auth VLAN
+ type: int
+ voice_vlan_id:
+ description:
+ - Set a Voice VLAN ID for Critical Auth VLAN
+ type: int
+ suspend_port_bounce:
+ description:
+ - Enable or disable the Suspend Port Bounce when RADIUS servers are unreachable.
+ type: bool
+ default: False
+ radius_attribute_group_policy_name:
+ description:
+ - Enable that attribute for a RADIUS
+ type: str
+ choices: ["Filter-Id", ""]
+ default: ""
+author:
+- Marcin Woźniak (@y0rune)
+extends_documentation_fragment: cisco.meraki.meraki
+"""
+
+EXAMPLES = r"""
+- name: Create access policy with auth_method is "Meraki authentication"
+ cisco.meraki.meraki_ms_access_policies:
+ auth_key: abc123
+ state: present
+ name: "Meraki authentication policy"
+ auth_method: "Meraki authentication"
+ net_name: YourNet
+ org_name: YourOrg
+ delegate_to: localhost
+
+- name: Create access policy with auth_method is "my Radius Server"
+ cisco.meraki.meraki_ms_access_policies:
+ auth_key: abc123
+ access_policy_type: "802.1x"
+ host_mode: "Single-Host"
+ state: present
+ name: "Meraki authentication policy"
+ auth_method: "my RADIUS server"
+ radius_servers:
+ - host: 192.0.1.18
+ port: 7890
+ secret: secret123
+ net_name: YourNet
+ org_name: YourOrg
+ radius_coa_enabled: False
+ radius_accounting_enabled: False
+ guest_vlan: 10
+ voice_vlan_clients: False
+"""
+
+RETURN = r"""
+data:
+ description: List of Access Policies
+ returned: success
+ type: complex
+ contains:
+ number:
+ description: Number of the Access Policy
+ returned: success
+ type: int
+ sample: 1
+ name:
+ description: Name of the Access Policy
+ returned: success
+ type: str
+ sample: Policy with 802.1x
+ access_policy_type:
+ description: Type of the access policy
+ returned: success
+ type: str
+ sample: 802.1x
+ guest_vlan_id:
+ description: ID of the Guest Vlan
+ returned: success
+ type: int
+ sample: 10
+ host_mode:
+ description: Choosen teh Host Mode for the access policy
+ returned: success
+ type: str
+ sample: Single-Host
+ radius:
+ description: List of radius specific list
+ returned: success
+ type: complex
+ contains:
+ critial_auth:
+ description: Critial Auth List
+ returned: success
+ type: complex
+ contains:
+ data_vlan_id:
+ description: VLAN ID for data
+ returned: success
+ type: int
+ sample: 10
+ suspend_port_bounce:
+ description: Enable or disable suspend port bounce
+ returned: success
+ type: bool
+ sample: False
+ voice_vlan_id:
+ description: VLAN ID for voice
+ returned: success
+ type: int
+ sample: 10
+ failed_auth_vlan_id:
+ description: VLAN ID when failed auth
+ returned: success
+ type: int
+ sample: 11
+ re_authentication_interval:
+ description: Interval of re-authentication
+ returned: success
+ type: int
+ sample:
+ radius_coa_enabled:
+ description:
+ - Enable or disable RADIUS CoA (Change of Authorization).
+ type: bool
+ radius_accounting_enabled:
+ description:
+ - Enable or disable RADIUS accounting.
+ type: bool
+ radius_accounting_servers:
+ description:
+ - List of RADIUS servers for RADIUS accounting.
+ type: list
+ elements: dict
+ radius_servers:
+ description:
+ - List of RADIUS servers.
+ type: list
+ elements: dict
+ radius_attribute_group_policy_name:
+ description: Enable the radius group attribute
+ returned: success
+ type: str
+ choices: [ "11", ""]
+ sample: 11
+ radius_testing_enabled:
+ description: Enable or disable Radius Testing
+ returned: success
+ type: bool
+ sample: True
+ voice_vlan_clients:
+ description: Enable or disable Voice Vlan Clients
+ returned: success
+ type: bool
+ sample: False
+"""
+
+from ansible.module_utils.basic import AnsibleModule, json
+from ansible.module_utils.common.dict_transformations import recursive_diff
+from ansible_collections.cisco.meraki.plugins.module_utils.network.meraki.meraki import (
+ MerakiModule,
+ meraki_argument_spec,
+)
+
+
+def convert_vlan_id(vlan_id):
+ if vlan_id == "":
+ return None
+ elif vlan_id == 0:
+ return None
+ elif vlan_id in range(1, 4094):
+ return vlan_id
+
+
+def convert_radius_attribute_group_policy_name(arg):
+ if arg == "Filter-Id":
+ return 11
+ else:
+ return ""
+
+
+def main():
+ argument_spec = meraki_argument_spec()
+
+ radius_arg_spec = dict(
+ host=dict(type="str", required=True),
+ port=dict(type="int"),
+ secret=dict(type="str", no_log=True),
+ )
+
+ argument_spec.update(
+ state=dict(
+ type="str",
+ choices=["present", "query", "absent"],
+ default="present",
+ ),
+ net_id=dict(type="str"),
+ net_name=dict(type="str", aliases=["network"]),
+ number=dict(type="int", aliases=["access_policy_number"]),
+ name=dict(type="str"),
+ auth_method=dict(
+ type="str",
+ choices=["Meraki authentication", "my RADIUS server"],
+ ),
+ guest_vlan=dict(type="int"),
+ access_policy_type=dict(
+ type="str",
+ choices=[
+ "802.1x",
+ "MAC authentication bypass",
+ "Hybrid authentication",
+ ],
+ ),
+ systems_management_enrollment=dict(type="bool", default=False),
+ radius_servers=dict(
+ type="list", default=None, elements="dict", options=radius_arg_spec
+ ),
+ radius_testing=dict(type="bool", default="True"),
+ voice_vlan_clients=dict(type="bool", default="True"),
+ radius_coa_enabled=dict(type="bool"),
+ radius_accounting_enabled=dict(type="bool"),
+ radius_accounting_servers=dict(
+ type="list", elements="dict", options=radius_arg_spec
+ ),
+ host_mode=dict(
+ type="str",
+ choices=["Single-Host", "Multi-Domain", "Multi-Host", "Multi-Auth"],
+ ),
+ data_vlan_id=dict(type="int"),
+ voice_vlan_id=dict(type="int"),
+ suspend_port_bounce=dict(type="bool", default="False"),
+ radius_attribute_group_policy_name=dict(
+ type="str", choices=["Filter-Id", ""], default=""
+ ),
+ )
+
+ module = AnsibleModule(
+ argument_spec=argument_spec,
+ supports_check_mode=True,
+ )
+ meraki = MerakiModule(module, function="access_policies")
+
+ net_id = meraki.params["net_id"]
+ net_name = meraki.params["net_name"]
+
+ org_id = meraki.params["org_id"]
+ org_name = meraki.params["org_name"]
+
+ if meraki.params["net_name"] and meraki.params["net_id"]:
+ meraki.fail_json(msg="net_name and net_id are mutually exclusive")
+
+ if meraki.params["org_name"] and meraki.params["org_id"]:
+ meraki.fail_json(msg="org_name and org_id are mutually exclusive")
+
+ if net_id or net_name:
+ if net_id is None:
+ if org_id is None:
+ org_id = meraki.get_org_id(org_name)
+ nets = meraki.get_nets(org_id=org_id)
+ net_id = meraki.get_net_id(net_name=net_name, data=nets)
+
+ query_urls = {"access_policies": "/networks/{net_id}/switch/accessPolicies"}
+ query_url = {
+ "access_policies": "/networks/{net_id}/switch/accessPolicies/{number}"
+ }
+ update_url = {
+ "access_policies": "/networks/{net_id}/switch/accessPolicies/{number}"
+ }
+ create_url = {"access_policies": "/networks/{net_id}/switch/accessPolicies"}
+
+ meraki.url_catalog["get_all"].update(query_urls)
+ meraki.url_catalog["get_one"].update(query_url)
+ meraki.url_catalog["update"] = update_url
+ meraki.url_catalog["create"] = create_url
+
+ payload_auth = {
+ "name": meraki.params["name"],
+ "radiusServers": [],
+ "radiusTestingEnabled": False,
+ "radiusGroupAttribute": meraki.params[
+ "radius_attribute_group_policy_name"
+ ],
+ "radius": {
+ "criticalAuth": {
+ "dataVlanId": None,
+ "voiceVlanId": None,
+ "suspendPortBounce": False,
+ },
+ "failedAuthVlanId": None,
+ "reAuthenticationInterval": None,
+ },
+ "radiusCoaSupportEnabled": False,
+ "radiusAccountingEnabled": False,
+ "radiusAccountingServers": [],
+ "hostMode": "Single-Host",
+ "accessPolicyType": "802.1x",
+ "voiceVlanClients": True,
+ "systems_management_enrollment": meraki.params[
+ "systems_management_enrollment"
+ ],
+ "guestVlanId": meraki.params["guest_vlan"],
+ "urlRedirectWalledGardenEnabled": False,
+ }
+
+ payload_radius = {
+ "name": meraki.params["name"],
+ "radiusServers": meraki.params["radius_servers"],
+ "radiusTestingEnabled": meraki.params["radius_testing"],
+ "radiusGroupAttribute": convert_radius_attribute_group_policy_name(
+ meraki.params["radius_attribute_group_policy_name"]
+ ),
+ "radius": {
+ "criticalAuth": {
+ "dataVlanId": convert_vlan_id(meraki.params["data_vlan_id"]),
+ "voiceVlanId": convert_vlan_id(meraki.params["voice_vlan_id"]),
+ "suspendPortBounce": meraki.params["suspend_port_bounce"],
+ },
+ "failedAuthVlanId": None,
+ "reAuthenticationInterval": None,
+ },
+ "radiusCoaSupportEnabled": meraki.params["radius_coa_enabled"],
+ "hostMode": meraki.params["host_mode"],
+ "accessPolicyType": meraki.params["access_policy_type"],
+ "guestVlanId": meraki.params["guest_vlan"],
+ "voiceVlanClients": meraki.params["voice_vlan_clients"],
+ "urlRedirectWalledGardenEnabled": False,
+ "radiusAccountingEnabled": meraki.params["radius_accounting_enabled"],
+ "radiusAccountingServers": meraki.params["radius_accounting_servers"],
+ "systems_management_enrollment": meraki.params[
+ "systems_management_enrollment"
+ ],
+ }
+
+ if meraki.params["state"] == "query":
+ if meraki.params["number"]:
+ path = meraki.construct_path(
+ "get_one",
+ net_id=net_id,
+ custom={
+ "number": meraki.params["number"],
+ },
+ )
+ response = meraki.request(path, method="GET")
+ meraki.result["data"] = response
+ else:
+ path = meraki.construct_path("get_all", net_id=net_id)
+ response = meraki.request(path, method="GET")
+ meraki.result["data"] = response
+ elif meraki.params["state"] == "present":
+
+ query_path_all = meraki.construct_path(
+ "get_all",
+ net_id=net_id,
+ )
+
+ original_all = meraki.request(query_path_all, method="GET")
+
+ for i in original_all:
+ if i.get("name") == meraki.params["name"]:
+ meraki.params["number"] = i.get("accessPolicyNumber")
+
+ if meraki.params["number"] is None:
+ path = meraki.construct_path(
+ "create",
+ net_id=net_id,
+ )
+ if meraki.params["auth_method"] == "Meraki authentication":
+ response = meraki.request(
+ path, method="POST", payload=json.dumps(payload_auth)
+ )
+ meraki.result["changed"] = True
+ meraki.result["data"] = response
+ elif meraki.params["auth_method"] == "my RADIUS server":
+ response = meraki.request(
+ path, method="POST", payload=json.dumps(payload_radius)
+ )
+ meraki.result["changed"] = True
+ meraki.result["data"] = response
+ else:
+ query_path = meraki.construct_path(
+ "get_one",
+ net_id=net_id,
+ custom={
+ "number": meraki.params["number"],
+ },
+ )
+
+ update_path = meraki.construct_path(
+ "update",
+ net_id=net_id,
+ custom={
+ "number": meraki.params["number"],
+ },
+ )
+
+ proposed = ""
+
+ if meraki.params["auth_method"] == "Meraki authentication":
+ proposed = payload_auth.copy()
+ elif meraki.params["auth_method"] == "my RADIUS server":
+ proposed = payload_radius.copy()
+
+ original = meraki.request(query_path, method="GET")
+
+ ignored_parameters = [
+ "accessPolicyNumber",
+ "secret",
+ "systems_management_enrollment",
+ ]
+
+ if meraki.params["radius_accounting_enabled"]:
+ proposed.update(
+ {
+ "radiusAccountingServers": meraki.params[
+ "radius_accounting_servers"
+ ],
+ }
+ )
+ else:
+ proposed.update(
+ {
+ "radiusAccountingServers": [],
+ }
+ )
+
+ if meraki.params["radius_servers"]:
+ proposed.update(
+ {
+ "radiusServers": meraki.params["radius_servers"],
+ }
+ )
+ else:
+ proposed.update(
+ {
+ "radiusServers": [],
+ }
+ )
+
+ if meraki.is_update_required(
+ original,
+ proposed,
+ optional_ignore=ignored_parameters,
+ ):
+
+ if meraki.check_mode is True:
+ original.update(proposed)
+ meraki.result["data"] = original
+ meraki.result["changed"] = True
+ meraki.exit_json(**meraki.result)
+
+ response = meraki.request(
+ update_path, method="PUT", payload=json.dumps(proposed)
+ )
+ meraki.result["changed"] = True
+ meraki.result["data"] = response
+ else:
+ meraki.result["data"] = original
+
+ elif meraki.params["state"] == "absent":
+ path = meraki.construct_path(
+ "update",
+ net_id=net_id,
+ custom={
+ "number": meraki.params["number"],
+ },
+ )
+
+ response = meraki.request(path, method="DELETE")
+ meraki.result["changed"] = True
+ meraki.result["data"] = response
+
+ meraki.exit_json(**meraki.result)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/ansible_collections/cisco/meraki/plugins/modules/meraki_ms_l3_interface.py b/ansible_collections/cisco/meraki/plugins/modules/meraki_ms_l3_interface.py
new file mode 100644
index 000000000..716ec8d9b
--- /dev/null
+++ b/ansible_collections/cisco/meraki/plugins/modules/meraki_ms_l3_interface.py
@@ -0,0 +1,373 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Copyright: (c) 2020, Kevin Breit (@kbreit) <kevin.breit@kevinbreit.net>
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+ANSIBLE_METADATA = {
+ 'metadata_version': '1.1',
+ 'status': ['preview'],
+ 'supported_by': 'community'
+}
+
+DOCUMENTATION = r'''
+---
+module: meraki_ms_l3_interface
+short_description: Manage routed interfaces on MS switches
+description:
+- Allows for creation, management, and visibility into routed interfaces on Meraki MS switches.
+notes:
+- Once a layer 3 interface is created, the API does not allow updating the interface and specifying C(default_gateway).
+options:
+ state:
+ description:
+ - Create or modify an organization.
+ type: str
+ choices: [ present, query, absent ]
+ default: present
+ serial:
+ description:
+ - Serial number of MS switch hosting the layer 3 interface.
+ type: str
+ vlan_id:
+ description:
+ - The VLAN this routed interface is on.
+ - VLAN must be between 1 and 4094.
+ type: int
+ default_gateway:
+ description:
+ - The next hop for any traffic that isn't going to a directly connected subnet or over a static route.
+ - This IP address must exist in a subnet with a routed interface.
+ type: str
+ interface_ip:
+ description:
+ - The IP address this switch will use for layer 3 routing on this VLAN or subnet.
+ - This cannot be the same as the switch's management IP.
+ type: str
+ interface_id:
+ description:
+ - Uniqiue identification number for layer 3 interface.
+ type: str
+ multicast_routing:
+ description:
+ - Enable multicast support if multicast routing between VLANs is required.
+ type: str
+ choices: [disabled, enabled, IGMP snooping querier]
+ name:
+ description:
+ - A friendly name or description for the interface or VLAN.
+ type: str
+ subnet:
+ description:
+ - The network that this routed interface is on, in CIDR notation.
+ type: str
+ ospf_settings:
+ description:
+ - The OSPF routing settings of the interface.
+ type: dict
+ suboptions:
+ cost:
+ description:
+ - The path cost for this interface.
+ type: int
+ area:
+ description:
+ - The OSPF area to which this interface should belong.
+ - Can be either 'disabled' or the identifier of an existing OSPF area.
+ type: str
+ is_passive_enabled:
+ description:
+ - When enabled, OSPF will not run on the interface, but the subnet will still be advertised.
+ type: bool
+author:
+- Kevin Breit (@kbreit)
+extends_documentation_fragment: cisco.meraki.meraki
+'''
+
+EXAMPLES = r'''
+- name: Query all l3 interfaces
+ meraki_ms_l3_interface:
+ auth_key: abc123
+ state: query
+ serial: aaa-bbb-ccc
+
+- name: Query one l3 interface
+ meraki_ms_l3_interface:
+ auth_key: abc123
+ state: query
+ serial: aaa-bbb-ccc
+ name: Test L3 interface
+
+- name: Create l3 interface
+ meraki_ms_l3_interface:
+ auth_key: abc123
+ state: present
+ serial: aaa-bbb-ccc
+ name: "Test L3 interface 2"
+ subnet: "192.168.3.0/24"
+ interface_ip: "192.168.3.2"
+ multicast_routing: disabled
+ vlan_id: 11
+ ospf_settings:
+ area: 0
+ cost: 1
+ is_passive_enabled: true
+
+- name: Update l3 interface
+ meraki_ms_l3_interface:
+ auth_key: abc123
+ state: present
+ serial: aaa-bbb-ccc
+ name: "Test L3 interface 2"
+ subnet: "192.168.3.0/24"
+ interface_ip: "192.168.3.2"
+ multicast_routing: disabled
+ vlan_id: 11
+ ospf_settings:
+ area: 0
+ cost: 2
+ is_passive_enabled: true
+
+- name: Delete l3 interface
+ meraki_ms_l3_interface:
+ auth_key: abc123
+ state: absent
+ serial: aaa-bbb-ccc
+ interface_id: abc123344566
+'''
+
+RETURN = r'''
+data:
+ description: Information about the layer 3 interfaces.
+ returned: success
+ type: complex
+ contains:
+ vlan_id:
+ description: The VLAN this routed interface is on.
+ returned: success
+ type: int
+ sample: 10
+ default_gateway:
+ description: The next hop for any traffic that isn't going to a directly connected subnet or over a static route.
+ returned: success
+ type: str
+ sample: 192.168.2.1
+ interface_ip:
+ description: The IP address this switch will use for layer 3 routing on this VLAN or subnet.
+ returned: success
+ type: str
+ sample: 192.168.2.2
+ interface_id:
+ description: Uniqiue identification number for layer 3 interface.
+ returned: success
+ type: str
+ sample: 62487444811111120
+ multicast_routing:
+ description: Enable multicast support if multicast routing between VLANs is required.
+ returned: success
+ type: str
+ sample: disabled
+ name:
+ description: A friendly name or description for the interface or VLAN.
+ returned: success
+ type: str
+ sample: L3 interface
+ subnet:
+ description: The network that this routed interface is on, in CIDR notation.
+ returned: success
+ type: str
+ sample: 192.168.2.0/24
+ ospf_settings:
+ description: The OSPF routing settings of the interface.
+ returned: success
+ type: complex
+ contains:
+ cost:
+ description: The path cost for this interface.
+ returned: success
+ type: int
+ sample: 1
+ area:
+ description: The OSPF area to which this interface should belong.
+ returned: success
+ type: str
+ sample: 0
+ is_passive_enabled:
+ description: When enabled, OSPF will not run on the interface, but the subnet will still be advertised.
+ returned: success
+ type: bool
+ sample: true
+'''
+
+from ansible.module_utils.basic import AnsibleModule, json
+from ansible_collections.cisco.meraki.plugins.module_utils.network.meraki.meraki import MerakiModule, meraki_argument_spec
+
+
+def construct_payload(meraki):
+ payload = {}
+ if meraki.params['name'] is not None:
+ payload['name'] = meraki.params['name']
+ if meraki.params['subnet'] is not None:
+ payload['subnet'] = meraki.params['subnet']
+ if meraki.params['interface_ip'] is not None:
+ payload['interfaceIp'] = meraki.params['interface_ip']
+ if meraki.params['multicast_routing'] is not None:
+ payload['multicastRouting'] = meraki.params['multicast_routing']
+ if meraki.params['vlan_id'] is not None:
+ payload['vlanId'] = meraki.params['vlan_id']
+ if meraki.params['default_gateway'] is not None:
+ payload['defaultGateway'] = meraki.params['default_gateway']
+ if meraki.params['ospf_settings'] is not None:
+ payload['ospfSettings'] = {}
+ if meraki.params['ospf_settings']['area'] is not None:
+ payload['ospfSettings']['area'] = meraki.params['ospf_settings']['area']
+ if meraki.params['ospf_settings']['cost'] is not None:
+ payload['ospfSettings']['cost'] = meraki.params['ospf_settings']['cost']
+ if meraki.params['ospf_settings']['is_passive_enabled'] is not None:
+ payload['ospfSettings']['isPassiveEnabled'] = meraki.params['ospf_settings']['is_passive_enabled']
+ return payload
+
+
+def get_interface_id(meraki, data, name):
+ # meraki.fail_json(msg=data)
+ for interface in data:
+ if interface['name'] == name:
+ return interface['interfaceId']
+ return None
+
+
+def get_interface(interfaces, interface_id):
+ for interface in interfaces:
+ if interface['interfaceId'] == interface_id:
+ return interface
+ return None
+
+
+def main():
+ # define the available arguments/parameters that a user can pass to
+ # the module
+
+ ospf_arg_spec = dict(area=dict(type='str'),
+ cost=dict(type='int'),
+ is_passive_enabled=dict(type='bool'),
+ )
+
+ argument_spec = meraki_argument_spec()
+ argument_spec.update(state=dict(type='str', choices=['present', 'query', 'absent'], default='present'),
+ serial=dict(type='str'),
+ name=dict(type='str'),
+ subnet=dict(type='str'),
+ interface_id=dict(type='str'),
+ interface_ip=dict(type='str'),
+ multicast_routing=dict(type='str', choices=['disabled', 'enabled', 'IGMP snooping querier']),
+ vlan_id=dict(type='int'),
+ default_gateway=dict(type='str'),
+ ospf_settings=dict(type='dict', default=None, options=ospf_arg_spec),
+ )
+
+ # the AnsibleModule object will be our abstraction working with Ansible
+ # this includes instantiation, a couple of common attr would be the
+ # args/params passed to the execution, as well as if the module
+ # supports check mode
+ module = AnsibleModule(argument_spec=argument_spec,
+ supports_check_mode=True,
+ )
+ meraki = MerakiModule(module, function='ms_l3_interfaces')
+
+ meraki.params['follow_redirects'] = 'all'
+
+ query_urls = {'ms_l3_interfaces': '/devices/{serial}/switch/routing/interfaces'}
+ query_one_urls = {'ms_l3_interfaces': '/devices/{serial}/switch/routing/interfaces'}
+ create_urls = {'ms_l3_interfaces': '/devices/{serial}/switch/routing/interfaces'}
+ update_urls = {'ms_l3_interfaces': '/devices/{serial}/switch/routing/interfaces/{interface_id}'}
+ delete_urls = {'ms_l3_interfaces': '/devices/{serial}/switch/routing/interfaces/{interface_id}'}
+
+ meraki.url_catalog['get_all'].update(query_urls)
+ meraki.url_catalog['get_one'].update(query_one_urls)
+ meraki.url_catalog['create'] = create_urls
+ meraki.url_catalog['update'] = update_urls
+ meraki.url_catalog['delete'] = delete_urls
+
+ payload = None
+
+ if meraki.params['vlan_id'] is not None:
+ if meraki.params['vlan_id'] < 1 or meraki.params['vlan_id'] > 4094:
+ meraki.fail_json(msg='vlan_id must be between 1 and 4094')
+
+ interface_id = meraki.params['interface_id']
+ interfaces = None
+ if interface_id is None:
+ if meraki.params['name'] is not None:
+ path = meraki.construct_path('get_all', custom={'serial': meraki.params['serial']})
+ interfaces = meraki.request(path, method='GET')
+ interface_id = get_interface_id(meraki, interfaces, meraki.params['name'])
+
+ # manipulate or modify the state as needed (this is going to be the
+ # part where your module will do what it needs to do)
+
+ if meraki.params['state'] == 'query':
+ if interface_id is not None: # Query one interface
+ path = meraki.construct_path('get_one', custom={'serial': meraki.params['serial'],
+ 'interface_id': interface_id})
+ response = meraki.request(path, method='GET')
+ meraki.result['data'] = response
+ meraki.exit_json(**meraki.result)
+ else: # Query all interfaces
+ path = meraki.construct_path('get_all', custom={'serial': meraki.params['serial']})
+ response = meraki.request(path, method='GET')
+ meraki.result['data'] = response
+ meraki.exit_json(**meraki.result)
+ elif meraki.params['state'] == 'present':
+ if interface_id is None: # Create a new interface
+ payload = construct_payload(meraki)
+ if meraki.check_mode is True:
+ meraki.result['data'] = payload
+ meraki.result['changed'] = True
+ meraki.exit_json(**meraki.result)
+ path = meraki.construct_path('create', custom={'serial': meraki.params['serial']})
+ response = meraki.request(path, method='POST', payload=json.dumps(payload))
+ meraki.result['data'] = response
+ meraki.result['changed'] = True
+ meraki.exit_json(**meraki.result)
+ else:
+ if interfaces is None:
+ path = meraki.construct_path('get_all', custom={'serial': meraki.params['serial']})
+ interfaces = meraki.request(path, method='GET')
+ payload = construct_payload(meraki)
+ interface = get_interface(interfaces, interface_id)
+ if meraki.is_update_required(interface, payload):
+ if meraki.check_mode is True:
+ interface.update(payload)
+ meraki.result['data'] = interface
+ meraki.result['changed'] = True
+ meraki.exit_json(**meraki.result)
+ path = meraki.construct_path('update', custom={'serial': meraki.params['serial'],
+ 'interface_id': interface_id})
+ response = meraki.request(path, method='PUT', payload=json.dumps(payload))
+ meraki.result['data'] = response
+ meraki.result['changed'] = True
+ meraki.exit_json(**meraki.result)
+ else:
+ meraki.result['data'] = interface
+ meraki.exit_json(**meraki.result)
+ elif meraki.params['state'] == 'absent':
+ if meraki.check_mode is True:
+ meraki.result['data'] = {}
+ meraki.result['changed'] = True
+ meraki.exit_json(**meraki.result)
+ path = meraki.construct_path('delete', custom={'serial': meraki.params['serial'],
+ 'interface_id': meraki.params['interface_id']})
+ response = meraki.request(path, method='DELETE')
+ meraki.result['data'] = response
+ meraki.result['changed'] = True
+
+ # in the event of a successful module execution, you will want to
+ # simple AnsibleModule.exit_json(), passing the key/value results
+ meraki.exit_json(**meraki.result)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/ansible_collections/cisco/meraki/plugins/modules/meraki_ms_link_aggregation.py b/ansible_collections/cisco/meraki/plugins/modules/meraki_ms_link_aggregation.py
new file mode 100644
index 000000000..a38eda7dc
--- /dev/null
+++ b/ansible_collections/cisco/meraki/plugins/modules/meraki_ms_link_aggregation.py
@@ -0,0 +1,258 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Copyright: (c) 2020, Kevin Breit (@kbreit) <kevin.breit@kevinbreit.net>
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+ANSIBLE_METADATA = {
+ 'metadata_version': '1.1',
+ 'status': ['preview'],
+ 'supported_by': 'community'
+}
+
+DOCUMENTATION = r'''
+---
+module: meraki_ms_link_aggregation
+short_description: Manage link aggregations on MS switches
+version_added: "1.2.0"
+description:
+- Allows for management of MS switch link aggregations in a Meraki environment.
+notes:
+- Switch profile ports are not supported in this module.
+options:
+ state:
+ description:
+ - Specifies whether SNMP information should be queried or modified.
+ type: str
+ choices: [ absent, query, present ]
+ default: present
+ net_name:
+ description:
+ - Name of network.
+ type: str
+ net_id:
+ description:
+ - ID of network.
+ type: str
+ lag_id:
+ description:
+ - ID of lag to query or modify.
+ type: str
+ switch_ports:
+ description:
+ - List of switchports to include in link aggregation.
+ type: list
+ elements: dict
+ suboptions:
+ serial:
+ description:
+ - Serial number of switch to own link aggregation.
+ type: str
+ port_id:
+ description:
+ - Port number which should be included in link aggregation.
+ type: str
+author:
+- Kevin Breit (@kbreit)
+extends_documentation_fragment: cisco.meraki.meraki
+'''
+
+EXAMPLES = r'''
+- name: Create LAG
+ meraki_ms_link_aggregation:
+ auth_key: '{{auth_key}}'
+ state: present
+ org_name: '{{test_org_name}}'
+ net_name: '{{test_switch_net_name}}'
+ switch_ports:
+ - serial: '{{serial_switch}}'
+ port_id: "1"
+ - serial: '{{serial_switch}}'
+ port_id: "2"
+ delegate_to: localhost
+
+- name: Update LAG
+ meraki_ms_link_aggregation:
+ auth_key: '{{auth_key}}'
+ state: present
+ org_name: '{{test_org_name}}'
+ net_name: '{{test_switch_net_name}}'
+ lag_id: '{{lag_id}}'
+ switch_ports:
+ - serial: '{{serial_switch}}'
+ port_id: "1"
+ - serial: '{{serial_switch}}'
+ port_id: "2"
+ - serial: '{{serial_switch}}'
+ port_id: "3"
+ - serial: '{{serial_switch}}'
+ port_id: "4"
+ delegate_to: localhost
+'''
+
+RETURN = r'''
+data:
+ description: List of aggregated links.
+ returned: success
+ type: complex
+ contains:
+ id:
+ description:
+ - ID of link aggregation.
+ returned: success
+ type: str
+ sample: "MTK3M4A2ZDdfM3=="
+ switch_ports:
+ description:
+ - List of switch ports to be included in link aggregation.
+ returned: success
+ type: complex
+ contains:
+ port_id:
+ description:
+ - Port number.
+ type: str
+ returned: success
+ sample: "1"
+ serial:
+ description:
+ - Serial number of switch on which port resides.
+ type: str
+ returned: success
+ sample: "ABCD-1234-WXYZ"
+'''
+
+from ansible.module_utils.basic import AnsibleModule, json
+from ansible_collections.cisco.meraki.plugins.module_utils.network.meraki.meraki import MerakiModule, meraki_argument_spec
+
+
+def get_lags(meraki, net_id):
+ path = meraki.construct_path('get_all', net_id=net_id)
+ return meraki.request(path, method='GET')
+
+
+def is_lag_valid(lags, lag_id):
+ for lag in lags:
+ if lag['id'] == lag_id:
+ return lag
+ return False
+
+
+def construct_payload(meraki):
+ payload = dict()
+ if meraki.params['switch_ports'] is not None:
+ payload['switchPorts'] = []
+ for port in meraki.params['switch_ports']:
+ port_config = {'serial': port['serial'],
+ 'portId': port['port_id'],
+ }
+ payload['switchPorts'].append(port_config)
+ # if meraki.params['switch_profile_ports'] is not None:
+ # payload['switchProfilePorts'] = []
+ # for port in meraki.params['switch_profile_ports']:
+ # port_config = {'profile': port['profile'],
+ # 'portId': port['port_id'],
+ # }
+ # payload['switchProfilePorts'].append(port_config)
+ return payload
+
+
+def main():
+
+ # define the available arguments/parameters that a user can pass to
+ # the module
+
+ switch_ports_args = dict(serial=dict(type='str'),
+ port_id=dict(type='str'),
+ )
+
+ # switch_profile_ports_args = dict(profile=dict(type='str'),
+ # port_id=dict(type='str'),
+ # )
+
+ argument_spec = meraki_argument_spec()
+ argument_spec.update(state=dict(type='str', choices=['absent', 'present', 'query'], default='present'),
+ org_name=dict(type='str', aliases=['organization']),
+ org_id=dict(type='str'),
+ net_name=dict(type='str'),
+ net_id=dict(type='str'),
+ lag_id=dict(type='str'),
+ switch_ports=dict(type='list', default=None, elements='dict', options=switch_ports_args),
+ # switch_profile_ports=dict(type='list', default=None, elements='dict', options=switch_profile_ports_args),
+ )
+
+ # the AnsibleModule object will be our abstraction working with Ansible
+ # this includes instantiation, a couple of common attr would be the
+ # args/params passed to the execution, as well as if the module
+ # supports check mode
+ module = AnsibleModule(argument_spec=argument_spec,
+ supports_check_mode=True,
+ )
+ meraki = MerakiModule(module, function='ms_link_aggregation')
+ meraki.params['follow_redirects'] = 'all'
+
+ query_urls = {'ms_link_aggregation': '/networks/{net_id}/switch/linkAggregations'}
+ create_url = {'ms_link_aggregation': '/networks/{net_id}/switch/linkAggregations'}
+ update_url = {'ms_link_aggregation': '/networks/{net_id}/switch/linkAggregations/{lag_id}'}
+ delete_url = {'ms_link_aggregation': '/networks/{net_id}/switch/linkAggregations/{lag_id}'}
+
+ meraki.url_catalog['get_all'].update(query_urls)
+ meraki.url_catalog['create'] = create_url
+ meraki.url_catalog['update'] = update_url
+ meraki.url_catalog['delete'] = delete_url
+
+ payload = None
+
+ # execute checks for argument completeness
+ # manipulate or modify the state as needed (this is going to be the
+ # part where your module will do what it needs to do)
+ org_id = meraki.params['org_id']
+ if org_id is None:
+ org_id = meraki.get_org_id(meraki.params['org_name'])
+ net_id = meraki.params['net_id']
+ if net_id is None:
+ nets = meraki.get_nets(org_id=org_id)
+ net_id = meraki.get_net_id(org_id, meraki.params['net_name'], data=nets)
+
+ if meraki.params['state'] == 'query':
+ path = meraki.construct_path('get_all', net_id=net_id)
+ response = meraki.request(path, method='GET')
+ meraki.result['data'] = response
+ meraki.exit_json(**meraki.result)
+ elif meraki.params['state'] == 'present':
+ if meraki.params['lag_id'] is not None: # Need to update
+ lag = is_lag_valid(get_lags(meraki, net_id), meraki.params['lag_id'])
+ if lag is not False: # Lag ID is valid
+ payload = construct_payload(meraki)
+ if meraki.is_update_required(lag, payload) is True:
+ path = meraki.construct_path('update', net_id=net_id, custom={'lag_id': meraki.params['lag_id']})
+ response = meraki.request(path, method='PUT', payload=json.dumps(payload))
+ meraki.result['changed'] = True
+ meraki.result['data'] = response
+ else:
+ meraki.result['data'] = lag
+ else:
+ meraki.fail_json("Provided lag_id is not valid.")
+ else:
+ path = meraki.construct_path('create', net_id=net_id)
+ payload = construct_payload(meraki)
+ response = meraki.request(path, method='POST', payload=json.dumps(payload))
+ meraki.result['changed'] = True
+ meraki.result['data'] = response
+ meraki.exit_json(**meraki.result)
+ elif meraki.params['state'] == 'absent':
+ path = meraki.construct_path('delete', net_id=net_id, custom={'lag_id': meraki.params['lag_id']})
+ response = meraki.request(path, method='DELETE')
+ meraki.result['data'] = {}
+ meraki.result['changed'] = True
+
+ # in the event of a successful module execution, you will want to
+ # simple AnsibleModule.exit_json(), passing the key/value results
+ meraki.exit_json(**meraki.result)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/ansible_collections/cisco/meraki/plugins/modules/meraki_ms_ospf.py b/ansible_collections/cisco/meraki/plugins/modules/meraki_ms_ospf.py
new file mode 100644
index 000000000..a8aaed005
--- /dev/null
+++ b/ansible_collections/cisco/meraki/plugins/modules/meraki_ms_ospf.py
@@ -0,0 +1,323 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Copyright: (c) 2020, Kevin Breit (@kbreit) <kevin.breit@kevinbreit.net>
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+ANSIBLE_METADATA = {
+ 'metadata_version': '1.1',
+ 'status': ['preview'],
+ 'supported_by': 'community'
+}
+
+DOCUMENTATION = r'''
+---
+module: meraki_ms_ospf
+short_description: Manage OSPF configuration on MS switches
+description:
+- Configure OSPF for compatible Meraki MS switches.
+options:
+ state:
+ description:
+ - Read or edit OSPF settings.
+ type: str
+ choices: [ present, query ]
+ default: present
+ net_name:
+ description:
+ - Name of network containing OSPF configuration.
+ type: str
+ aliases: [ name, network ]
+ net_id:
+ description:
+ - ID of network containing OSPF configuration.
+ type: str
+ enabled:
+ description:
+ - Enable or disable OSPF on the network.
+ type: bool
+ hello_timer:
+ description:
+ - Time interval, in seconds, at which hello packets will be sent to OSPF neighbors to maintain connectivity.
+ - Value must be between 1 and 255.
+ - Default is 10 seconds.
+ type: int
+ dead_timer:
+ description:
+ - Time interval to determine when the peer will be declared inactive.
+ - Value must be between 1 and 65535.
+ type: int
+ md5_authentication_enabled:
+ description:
+ - Whether to enable or disable MD5 authentication.
+ type: bool
+ md5_authentication_key:
+ description:
+ - MD5 authentication credentials.
+ type: dict
+ suboptions:
+ id:
+ description:
+ - MD5 authentication key index.
+ - Must be between 1 and 255.
+ type: str
+ passphrase:
+ description:
+ - Plain text authentication passphrase
+ type: str
+ areas:
+ description:
+ - List of areas in OSPF network.
+ type: list
+ elements: dict
+ suboptions:
+ area_id:
+ description:
+ - OSPF area ID
+ type: int
+ aliases: [ id ]
+ area_name:
+ description:
+ - Descriptive name of OSPF area.
+ type: str
+ aliases: [ name ]
+ area_type:
+ description:
+ - OSPF area type.
+ choices: [normal, stub, nssa]
+ type: str
+ aliases: [ type ]
+author:
+- Kevin Breit (@kbreit)
+extends_documentation_fragment: cisco.meraki.meraki
+'''
+
+EXAMPLES = r'''
+ - name: Query OSPF settings
+ meraki_ms_ospf:
+ auth_key: abc123
+ org_name: YourOrg
+ net_name: YourNet
+ state: query
+ delegate_to: localhost
+
+ - name: Enable OSPF with check mode
+ meraki_ms_ospf:
+ auth_key: abc123
+ org_name: YourOrg
+ net_name: YourNet
+ state: present
+ enabled: true
+ hello_timer: 20
+ dead_timer: 60
+ areas:
+ - area_id: 0
+ area_name: Backbone
+ area_type: normal
+ - area_id: 1
+ area_name: Office
+ area_type: nssa
+ md5_authentication_enabled: false
+'''
+
+RETURN = r'''
+data:
+ description: Information about queried object.
+ returned: success
+ type: complex
+ contains:
+ enabled:
+ description:
+ - Enable or disable OSPF on the network.
+ type: bool
+ hello_timer_in_seconds:
+ description:
+ - Time interval, in seconds, at which hello packets will be sent to OSPF neighbors to maintain connectivity.
+ type: int
+ dead_timer_in_seconds:
+ description:
+ - Time interval to determine when the peer will be declared inactive.
+ type: int
+ areas:
+ description:
+ - List of areas in OSPF network.
+ type: complex
+ contains:
+ area_id:
+ description:
+ - OSPF area ID
+ type: int
+ area_name:
+ description:
+ - Descriptive name of OSPF area.
+ type: str
+ area_type:
+ description:
+ - OSPF area type.
+ type: str
+ md5_authentication_enabled:
+ description:
+ - Whether to enable or disable MD5 authentication.
+ type: bool
+ md5_authentication_key:
+ description:
+ - MD5 authentication credentials.
+ type: complex
+ contains:
+ id:
+ description:
+ - MD5 key index.
+ type: int
+ passphrase:
+ description:
+ - Passphrase for MD5 key.
+ type: str
+'''
+
+from ansible.module_utils.basic import AnsibleModule, json
+from ansible_collections.cisco.meraki.plugins.module_utils.network.meraki.meraki import MerakiModule, meraki_argument_spec
+
+
+def construct_payload(meraki):
+ payload_key_mapping = {'enabled': 'enabled',
+ 'hello_timer': 'helloTimerInSeconds',
+ 'dead_timer': 'deadTimerInSeconds',
+ 'areas': 'areas',
+ 'area_id': 'areaId',
+ 'area_name': 'areaName',
+ 'area_type': 'areaType',
+ 'md5_authentication_enabled': 'md5AuthenticationEnabled',
+ 'md5_authentication_key': 'md5AuthenticationKey',
+ 'id': 'id',
+ 'passphrase': 'passphrase',
+ }
+ payload = {}
+
+ # This may need to be reworked to avoid overwiting
+ for snake, camel in payload_key_mapping.items():
+ try:
+ if meraki.params[snake] is not None:
+ payload[camel] = meraki.params[snake]
+ if snake == 'areas':
+ if meraki.params['areas'] is not None and len(meraki.params['areas']) > 0:
+ payload['areas'] = []
+ for area in meraki.params['areas']:
+ area_settings = {'areaName': area['area_name'],
+ 'areaId': area['area_id'],
+ 'areaType': area['area_type'],
+ }
+ payload['areas'].append(area_settings)
+ # TODO: Does this code below have a purpose?
+ # elif snake == 'md5_authentication_key':
+ # if meraki.params['md5_authentication_key'] is not None:
+ # md5_settings = {'id': meraki.params['md5_authentication_key']['id'],
+ # 'passphrase': meraki.params['md5_authentication_key']['passphrase'],
+ # }
+ except KeyError:
+ pass
+
+ return payload
+
+
+def main():
+ # define the available arguments/parameters that a user can pass to
+ # the module
+
+ areas_arg_spec = dict(area_id=dict(type='int', aliases=['id']),
+ area_name=dict(type='str', aliases=['name']),
+ area_type=dict(type='str', aliases=['type'], choices=['normal', 'stub', 'nssa']),
+ )
+
+ md5_auth_arg_spec = dict(id=dict(type='str'),
+ passphrase=dict(type='str', no_log=True),
+ )
+
+ argument_spec = meraki_argument_spec()
+ argument_spec.update(state=dict(type='str', choices=['present', 'query'], default='present'),
+ net_id=dict(type='str'),
+ net_name=dict(type='str', aliases=['name', 'network']),
+ enabled=dict(type='bool'),
+ hello_timer=dict(type='int'),
+ dead_timer=dict(type='int'),
+ areas=dict(type='list', default=None, elements='dict', options=areas_arg_spec),
+ md5_authentication_enabled=dict(type='bool'),
+ md5_authentication_key=dict(type='dict', default=None, options=md5_auth_arg_spec, no_log=True),
+ )
+
+ # the AnsibleModule object will be our abstraction working with Ansible
+ # this includes instantiation, a couple of common attr would be the
+ # args/params passed to the execution, as well as if the module
+ # supports check mode
+ module = AnsibleModule(argument_spec=argument_spec,
+ supports_check_mode=True,
+ )
+ meraki = MerakiModule(module, function='ms_ospf')
+
+ meraki.params['follow_redirects'] = 'all'
+
+ query_urls = {'ms_ospf': '/networks/{net_id}/switch/routing/ospf'}
+ update_urls = {'ms_ospf': '/networks/{net_id}/switch/routing/ospf'}
+
+ meraki.url_catalog['get_all'].update(query_urls)
+ meraki.url_catalog['update'] = update_urls
+
+ payload = None
+
+ # execute checks for argument completeness
+
+ if meraki.params['dead_timer'] is not None:
+ if meraki.params['dead_timer'] < 1 or meraki.params['dead_timer'] > 65535:
+ meraki.fail_json(msg='dead_timer must be between 1 and 65535')
+ if meraki.params['hello_timer'] is not None:
+ if meraki.params['hello_timer'] < 1 or meraki.params['hello_timer'] > 255:
+ meraki.fail_json(msg='hello_timer must be between 1 and 65535')
+ if meraki.params['md5_authentication_enabled'] is False:
+ if meraki.params['md5_authentication_key'] is not None:
+ meraki.fail_json(msg='md5_authentication_key must not be configured when md5_authentication_enabled is false')
+
+ # manipulate or modify the state as needed (this is going to be the
+ # part where your module will do what it needs to do)
+
+ org_id = meraki.params['org_id']
+ if not org_id:
+ org_id = meraki.get_org_id(meraki.params['org_name'])
+ net_id = meraki.params['net_id']
+ if net_id is None and meraki.params['net_name']:
+ nets = meraki.get_nets(org_id=org_id)
+ net_id = meraki.get_net_id(net_name=meraki.params['net_name'], data=nets)
+ if meraki.params['state'] == 'query':
+ path = meraki.construct_path('get_all', net_id=net_id)
+ response = meraki.request(path, method='GET')
+ meraki.result['data'] = response
+ meraki.exit_json(**meraki.result)
+ elif meraki.params['state'] == 'present':
+ original = meraki.request(meraki.construct_path('get_all', net_id=net_id), method='GET')
+ payload = construct_payload(meraki)
+ if meraki.is_update_required(original, payload) is True:
+ if meraki.check_mode is True:
+ meraki.result['data'] = payload
+ meraki.result['changed'] = True
+ meraki.exit_json(**meraki.result)
+ path = meraki.construct_path('update', net_id=net_id)
+ response = meraki.request(path, method='PUT', payload=json.dumps(payload))
+ if 'md5_authentication_key' in response:
+ response['md5_authentication_key']['passphrase'] = 'VALUE_SPECIFIED_IN_NO_LOG_PARAMETER'
+ meraki.result['data'] = response
+ meraki.result['changed'] = True
+ meraki.exit_json(**meraki.result)
+ else:
+ if 'md5_authentication_key' in original:
+ original['md5_authentication_key']['passphrase'] = 'VALUE_SPECIFIED_IN_NO_LOG_PARAMETER'
+ meraki.result['data'] = original
+ meraki.exit_json(**meraki.result)
+
+ # in the event of a successful module execution, you will want to
+ # simple AnsibleModule.exit_json(), passing the key/value results
+ meraki.exit_json(**meraki.result)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/ansible_collections/cisco/meraki/plugins/modules/meraki_ms_stack.py b/ansible_collections/cisco/meraki/plugins/modules/meraki_ms_stack.py
new file mode 100644
index 000000000..bbe6282ac
--- /dev/null
+++ b/ansible_collections/cisco/meraki/plugins/modules/meraki_ms_stack.py
@@ -0,0 +1,278 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Copyright: (c) 2020, Kevin Breit (@kbreit) <kevin.breit@kevinbreit.net>
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+ANSIBLE_METADATA = {
+ 'metadata_version': '1.1',
+ 'status': ['preview'],
+ 'supported_by': 'community'
+}
+
+DOCUMENTATION = r'''
+---
+module: meraki_ms_stack
+short_description: Modify switch stacking configuration in Meraki.
+version_added: "1.3.0"
+description:
+- Allows for modification of Meraki MS switch stacks.
+notes:
+- Not all actions are idempotent. Specifically, creating a new stack will error if any switch is already in a stack.
+options:
+ state:
+ description:
+ - Create or modify an organization.
+ choices: ['present', 'query', 'absent']
+ default: present
+ type: str
+ net_name:
+ description:
+ - Name of network which MX firewall is in.
+ type: str
+ net_id:
+ description:
+ - ID of network which MX firewall is in.
+ type: str
+ stack_id:
+ description:
+ - ID of stack which is to be modified or deleted.
+ type: str
+ serials:
+ description:
+ - List of switch serial numbers which should be included or removed from a stack.
+ type: list
+ elements: str
+ name:
+ description:
+ - Name of stack.
+ type: str
+
+author:
+- Kevin Breit (@kbreit)
+extends_documentation_fragment: cisco.meraki.meraki
+'''
+
+EXAMPLES = r'''
+- name: Create new stack
+ meraki_switch_stack:
+ auth_key: abc123
+ state: present
+ org_name: YourOrg
+ net_name: YourNet
+ name: Test stack
+ serials:
+ - "ABCD-1231-4579"
+ - "ASDF-4321-0987"
+
+- name: Add switch to stack
+ meraki_switch_stack:
+ auth_key: abc123
+ state: present
+ org_name: YourOrg
+ net_name: YourNet
+ stack_id: ABC12340987
+ serials:
+ - "ABCD-1231-4579"
+
+- name: Remove switch from stack
+ meraki_switch_stack:
+ auth_key: abc123
+ state: absent
+ org_name: YourOrg
+ net_name: YourNet
+ stack_id: ABC12340987
+ serials:
+ - "ABCD-1231-4579"
+
+- name: Query one stack
+ meraki_switch_stack:
+ auth_key: abc123
+ state: query
+ org_name: YourOrg
+ net_name: YourNet
+ stack_id: ABC12340987
+'''
+
+RETURN = r'''
+data:
+ description: VPN settings.
+ returned: success
+ type: complex
+ contains:
+ id:
+ description: ID of switch stack.
+ returned: always
+ type: str
+ sample: 7636
+ name:
+ description: Descriptive name of switch stack.
+ returned: always
+ type: str
+ sample: MyStack
+ serials:
+ description: List of serial numbers in switch stack.
+ returned: always
+ type: list
+ sample:
+ - "QBZY-XWVU-TSRQ"
+ - "QBAB-CDEF-GHIJ"
+'''
+
+from ansible.module_utils.basic import AnsibleModule, json
+from ansible_collections.cisco.meraki.plugins.module_utils.network.meraki.meraki import MerakiModule, meraki_argument_spec
+from copy import deepcopy
+
+
+def get_stacks(meraki, net_id):
+ path = meraki.construct_path('get_all', net_id=net_id)
+ return meraki.request(path, method='GET')
+
+
+def get_stack(stack_id, stacks):
+ for stack in stacks:
+ if stack_id == stack['id']:
+ return stack
+ return None
+
+
+def get_stack_id(meraki, net_id):
+ stacks = get_stacks(meraki, net_id)
+ for stack in stacks:
+ if stack['name'] == meraki.params['name']:
+ return stack['id']
+
+
+def does_stack_exist(meraki, stacks):
+ for stack in stacks:
+ have = set(meraki.params['serials'])
+ want = set(stack['serials'])
+ if have == want:
+ return stack
+ return False
+
+
+def main():
+ # define the available arguments/parameters that a user can pass to
+ # the module
+
+ argument_spec = meraki_argument_spec()
+ argument_spec.update(state=dict(type='str', choices=['present', 'query', 'absent'], default='present'),
+ net_name=dict(type='str'),
+ net_id=dict(type='str'),
+ stack_id=dict(type='str'),
+ serials=dict(type='list', elements='str', default=None),
+ name=dict(type='str'),
+ )
+
+ # the AnsibleModule object will be our abstraction working with Ansible
+ # this includes instantiation, a couple of common attr would be the
+ # args/params passed to the execution, as well as if the module
+ # supports check mode
+ module = AnsibleModule(argument_spec=argument_spec,
+ supports_check_mode=True,
+ )
+ meraki = MerakiModule(module, function='switch_stack')
+
+ meraki.params['follow_redirects'] = 'all'
+
+ query_urls = {'switch_stack': '/networks/{net_id}/switch/stacks'}
+ query_url = {'switch_stack': '/networks/{net_id}/switch/stacks/{stack_id}'}
+ add_urls = {'switch_stack': '/networks/{net_id}/switch/stacks/{stack_id}/add'}
+ remove_urls = {'switch_stack': '/networks/{net_id}/switch/stacks/{stack_id}/remove'}
+ create_urls = {'switch_stack': '/networks/{net_id}/switch/stacks'}
+ delete_urls = {'switch_stack': '/networks/{net_id}/switch/stacks/{stack_id}'}
+
+ meraki.url_catalog['get_all'].update(query_urls)
+ meraki.url_catalog['get_one'].update(query_url)
+ meraki.url_catalog['add'] = add_urls
+ meraki.url_catalog['remove'] = remove_urls
+ meraki.url_catalog['create'] = create_urls
+ meraki.url_catalog['delete'] = delete_urls
+
+ payload = None
+
+ # manipulate or modify the state as needed (this is going to be the
+ # part where your module will do what it needs to do)
+ org_id = meraki.params['org_id']
+ if org_id is None:
+ orgs = meraki.get_orgs()
+ for org in orgs:
+ if org['name'] == meraki.params['org_name']:
+ org_id = org['id']
+ net_id = meraki.params['net_id']
+ if net_id is None:
+ net_id = meraki.get_net_id(net_name=meraki.params['net_name'],
+ data=meraki.get_nets(org_id=org_id))
+
+ # assign and lookup stack_id
+ stack_id = meraki.params['stack_id']
+ if stack_id is None and meraki.params['name'] is not None:
+ stack_id = get_stack_id(meraki, net_id)
+ path = meraki.construct_path('get_all', net_id=net_id)
+ stacks = meraki.request(path, method='GET')
+
+ if meraki.params['state'] == 'query':
+ if stack_id is None:
+ meraki.result['data'] = stacks
+ else:
+ meraki.result['data'] = get_stack(stack_id, stacks)
+ elif meraki.params['state'] == 'present':
+ if meraki.params['stack_id'] is None:
+ payload = {'serials': meraki.params['serials'],
+ 'name': meraki.params['name'],
+ }
+ path = meraki.construct_path('create', net_id=net_id)
+ response = meraki.request(path, method='POST', payload=json.dumps(payload))
+ if meraki.status == 201:
+ meraki.result['data'] = response
+ meraki.result['changed'] = True
+ else:
+ payload = {'serial': meraki.params['serials'][0]}
+ original = get_stack(stack_id, stacks)
+ comparable = deepcopy(original)
+ comparable.update(payload)
+ if meraki.params['serials'][0] not in comparable['serials']:
+ comparable['serials'].append(meraki.params['serials'][0])
+ # meraki.fail_json(msg=comparable)
+ if meraki.is_update_required(original, comparable, optional_ignore=['serial']):
+ path = meraki.construct_path('add', net_id=net_id, custom={'stack_id': stack_id})
+ response = meraki.request(path, method='POST', payload=json.dumps(payload))
+ if meraki.status == 200:
+ meraki.result['data'] = response
+ meraki.result['changed'] = True
+ else:
+ meraki.result['data'] = original
+ elif meraki.params['state'] == 'absent':
+ if meraki.params['serials'] is None:
+ path = meraki.construct_path('delete', net_id=net_id, custom={'stack_id': stack_id})
+ response = meraki.request(path, method='DELETE')
+ meraki.result['data'] = {}
+ meraki.result['changed'] = True
+ else:
+ for serial in meraki.params['serials']:
+ payload = {'serial': serial}
+ original = get_stack(stack_id, stacks)
+ comparable = deepcopy(original)
+ comparable.update(payload)
+ if serial in comparable['serials']:
+ comparable['serials'].remove(serial)
+ if meraki.is_update_required(original, comparable, optional_ignore=['serial']):
+ path = meraki.construct_path('remove', net_id=net_id, custom={'stack_id': stack_id})
+ response = meraki.request(path, method='POST', payload=json.dumps(payload))
+ if meraki.status == 200:
+ meraki.result['data'] = response
+ meraki.result['changed'] = True
+ else:
+ meraki.result['data'] = original
+
+ # in the event of a successful module execution, you will want to
+ # simple AnsibleModule.exit_json(), passing the key/value results
+ meraki.exit_json(**meraki.result)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/ansible_collections/cisco/meraki/plugins/modules/meraki_ms_stack_l3_interface.py b/ansible_collections/cisco/meraki/plugins/modules/meraki_ms_stack_l3_interface.py
new file mode 100644
index 000000000..46291c314
--- /dev/null
+++ b/ansible_collections/cisco/meraki/plugins/modules/meraki_ms_stack_l3_interface.py
@@ -0,0 +1,395 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Copyright: (c) 2020, Kevin Breit (@kbreit) <kevin.breit@kevinbreit.net>
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+ANSIBLE_METADATA = {
+ 'metadata_version': '1.1',
+ 'status': ['preview'],
+ 'supported_by': 'community'
+}
+
+DOCUMENTATION = r'''
+---
+module: meraki_ms_stack_l3_interface
+short_description: Manage routed interfaces on MS switches
+description:
+- Allows for creation, management, and visibility into routed interfaces on Meraki MS switches.
+notes:
+- Once a layer 3 interface is created, the API does not allow updating the interface and specifying C(default_gateway).
+options:
+ state:
+ description:
+ - Create or modify an organization.
+ type: str
+ choices: [ present, query, absent ]
+ default: present
+ net_name:
+ description:
+ - Name of network which configuration is applied to.
+ aliases: [network]
+ type: str
+ net_id:
+ description:
+ - ID of network which configuration is applied to.
+ type: str
+ stack_id:
+ description:
+ - The unique identifier of the stack.
+ type: str
+ vlan_id:
+ description:
+ - The VLAN this routed interface is on.
+ - VLAN must be between 1 and 4094.
+ type: int
+ default_gateway:
+ description:
+ - The next hop for any traffic that isn't going to a directly connected subnet or over a static route.
+ - This IP address must exist in a subnet with a routed interface.
+ type: str
+ interface_ip:
+ description:
+ - The IP address this switch will use for layer 3 routing on this VLAN or subnet.
+ - This cannot be the same as the switch's management IP.
+ type: str
+ interface_id:
+ description:
+ - Uniqiue identification number for layer 3 interface.
+ type: str
+ multicast_routing:
+ description:
+ - Enable multicast support if multicast routing between VLANs is required.
+ type: str
+ choices: [disabled, enabled, IGMP snooping querier]
+ name:
+ description:
+ - A friendly name or description for the interface or VLAN.
+ type: str
+ subnet:
+ description:
+ - The network that this routed interface is on, in CIDR notation.
+ type: str
+ ospf_settings:
+ description:
+ - The OSPF routing settings of the interface.
+ type: dict
+ suboptions:
+ cost:
+ description:
+ - The path cost for this interface.
+ type: int
+ area:
+ description:
+ - The OSPF area to which this interface should belong.
+ - Can be either 'disabled' or the identifier of an existing OSPF area.
+ type: str
+ is_passive_enabled:
+ description:
+ - When enabled, OSPF will not run on the interface, but the subnet will still be advertised.
+ type: bool
+author:
+- Kevin Breit (@kbreit)
+extends_documentation_fragment: cisco.meraki.meraki
+'''
+
+EXAMPLES = r'''
+- name: Query all l3 interfaces
+ meraki_ms_stack_l3_interface:
+ auth_key: abc123
+ state: query
+ serial: aaa-bbb-ccc
+
+- name: Query one l3 interface
+ meraki_ms_stack_l3_interface:
+ auth_key: abc123
+ state: query
+ serial: aaa-bbb-ccc
+ name: Test L3 interface
+
+- name: Create l3 interface
+ meraki_ms_stack_l3_interface:
+ auth_key: abc123
+ state: present
+ serial: aaa-bbb-ccc
+ name: "Test L3 interface 2"
+ subnet: "192.168.3.0/24"
+ interface_ip: "192.168.3.2"
+ multicast_routing: disabled
+ vlan_id: 11
+ ospf_settings:
+ area: 0
+ cost: 1
+ is_passive_enabled: true
+
+- name: Update l3 interface
+ meraki_ms_stack_l3_interface:
+ auth_key: abc123
+ state: present
+ serial: aaa-bbb-ccc
+ name: "Test L3 interface 2"
+ subnet: "192.168.3.0/24"
+ interface_ip: "192.168.3.2"
+ multicast_routing: disabled
+ vlan_id: 11
+ ospf_settings:
+ area: 0
+ cost: 2
+ is_passive_enabled: true
+
+- name: Delete l3 interface
+ meraki_ms_stack_l3_interface:
+ auth_key: abc123
+ state: absent
+ serial: aaa-bbb-ccc
+ interface_id: abc123344566
+'''
+
+RETURN = r'''
+data:
+ description: Information about the layer 3 interfaces.
+ returned: success
+ type: complex
+ contains:
+ vlan_id:
+ description: The VLAN this routed interface is on.
+ returned: success
+ type: int
+ sample: 10
+ default_gateway:
+ description: The next hop for any traffic that isn't going to a directly connected subnet or over a static route.
+ returned: success
+ type: str
+ sample: 192.168.2.1
+ interface_ip:
+ description: The IP address this switch will use for layer 3 routing on this VLAN or subnet.
+ returned: success
+ type: str
+ sample: 192.168.2.2
+ interface_id:
+ description: Uniqiue identification number for layer 3 interface.
+ returned: success
+ type: str
+ sample: 62487444811111120
+ multicast_routing:
+ description: Enable multicast support if multicast routing between VLANs is required.
+ returned: success
+ type: str
+ sample: disabled
+ name:
+ description: A friendly name or description for the interface or VLAN.
+ returned: success
+ type: str
+ sample: L3 interface
+ subnet:
+ description: The network that this routed interface is on, in CIDR notation.
+ returned: success
+ type: str
+ sample: 192.168.2.0/24
+ ospf_settings:
+ description: The OSPF routing settings of the interface.
+ returned: success
+ type: complex
+ contains:
+ cost:
+ description: The path cost for this interface.
+ returned: success
+ type: int
+ sample: 1
+ area:
+ description: The OSPF area to which this interface should belong.
+ returned: success
+ type: str
+ sample: 0
+ is_passive_enabled:
+ description: When enabled, OSPF will not run on the interface, but the subnet will still be advertised.
+ returned: success
+ type: bool
+ sample: true
+'''
+
+from ansible.module_utils.basic import AnsibleModule, json
+from ansible_collections.cisco.meraki.plugins.module_utils.network.meraki.meraki import MerakiModule, meraki_argument_spec
+
+
+def construct_payload(meraki):
+ payload = {}
+ if meraki.params['name'] is not None:
+ payload['name'] = meraki.params['name']
+ if meraki.params['subnet'] is not None:
+ payload['subnet'] = meraki.params['subnet']
+ if meraki.params['interface_ip'] is not None:
+ payload['interfaceIp'] = meraki.params['interface_ip']
+ if meraki.params['multicast_routing'] is not None:
+ payload['multicastRouting'] = meraki.params['multicast_routing']
+ if meraki.params['vlan_id'] is not None:
+ payload['vlanId'] = meraki.params['vlan_id']
+ if meraki.params['default_gateway'] is not None:
+ payload['defaultGateway'] = meraki.params['default_gateway']
+ if meraki.params['ospf_settings'] is not None:
+ payload['ospfSettings'] = {}
+ if meraki.params['ospf_settings']['area'] is not None:
+ payload['ospfSettings']['area'] = meraki.params['ospf_settings']['area']
+ if meraki.params['ospf_settings']['cost'] is not None:
+ payload['ospfSettings']['cost'] = meraki.params['ospf_settings']['cost']
+ if meraki.params['ospf_settings']['is_passive_enabled'] is not None:
+ payload['ospfSettings']['isPassiveEnabled'] = meraki.params['ospf_settings']['is_passive_enabled']
+ return payload
+
+
+def get_interface_id(meraki, data, name):
+ # meraki.fail_json(msg=data)
+ for interface in data:
+ if interface['name'] == name:
+ return interface['interfaceId']
+ return None
+
+
+def get_interface(interfaces, interface_id):
+ for interface in interfaces:
+ if interface['interfaceId'] == interface_id:
+ return interface
+ return None
+
+
+def main():
+ # define the available arguments/parameters that a user can pass to
+ # the module
+
+ ospf_arg_spec = dict(area=dict(type='str'),
+ cost=dict(type='int'),
+ is_passive_enabled=dict(type='bool'),
+ )
+
+ argument_spec = meraki_argument_spec()
+ argument_spec.update(state=dict(type='str', choices=['present', 'query', 'absent'], default='present'),
+ net_id=dict(type='str'),
+ net_name=dict(type='str', aliases=['network']),
+ stack_id=dict(type='str'),
+ name=dict(type='str'),
+ subnet=dict(type='str'),
+ interface_id=dict(type='str'),
+ interface_ip=dict(type='str'),
+ multicast_routing=dict(type='str', choices=['disabled', 'enabled', 'IGMP snooping querier']),
+ vlan_id=dict(type='int'),
+ default_gateway=dict(type='str'),
+ ospf_settings=dict(type='dict', default=None, options=ospf_arg_spec),
+ )
+
+ # the AnsibleModule object will be our abstraction working with Ansible
+ # this includes instantiation, a couple of common attr would be the
+ # args/params passed to the execution, as well as if the module
+ # supports check mode
+ module = AnsibleModule(argument_spec=argument_spec,
+ supports_check_mode=True,
+ )
+ meraki = MerakiModule(module, function='ms_stack_l3_interfaces')
+
+ meraki.params['follow_redirects'] = 'all'
+
+ query_urls = {'ms_stack_l3_interfaces': '/networks/{net_id}/switch/stacks/{stack_id}/routing/interfaces'}
+ query_one_urls = {'ms_stack_l3_interfaces': '/networks/{net_id}/switch/stacks/{stack_id}/routing/interfaces'}
+ create_urls = {'ms_stack_l3_interfaces': '/networks/{net_id}/switch/stacks/{stack_id}/routing/interfaces'}
+ update_urls = {'ms_stack_l3_interfaces': '/networks/{net_id}/switch/stacks/{stack_id}/routing/interfaces/{interface_id}'}
+ delete_urls = {'ms_stack_l3_interfaces': '/networks/{net_id}/switch/stacks/{stack_id}/routing/interfaces/{interface_id}'}
+
+ meraki.url_catalog['get_all'].update(query_urls)
+ meraki.url_catalog['get_one'].update(query_one_urls)
+ meraki.url_catalog['create'] = create_urls
+ meraki.url_catalog['update'] = update_urls
+ meraki.url_catalog['delete'] = delete_urls
+
+ payload = None
+
+ if meraki.params['vlan_id'] is not None:
+ if meraki.params['vlan_id'] < 1 or meraki.params['vlan_id'] > 4094:
+ meraki.fail_json(msg='vlan_id must be between 1 and 4094')
+
+ org_id = meraki.params['org_id']
+ if org_id is None:
+ orgs = meraki.get_orgs()
+ for org in orgs:
+ if org['name'] == meraki.params['org_name']:
+ org_id = org['id']
+ net_id = meraki.params['net_id']
+ if net_id is None:
+ net_id = meraki.get_net_id(net_name=meraki.params['net_name'],
+ data=meraki.get_nets(org_id=org_id))
+
+ interface_id = meraki.params['interface_id']
+ interfaces = None
+ if interface_id is None:
+ if meraki.params['name'] is not None:
+ path = meraki.construct_path('get_all', net_id=net_id, custom={'stack_id': meraki.params['stack_id']})
+ interfaces = meraki.request(path, method='GET')
+ interface_id = get_interface_id(meraki, interfaces, meraki.params['name'])
+
+ # manipulate or modify the state as needed (this is going to be the
+ # part where your module will do what it needs to do)
+
+ if meraki.params['state'] == 'query':
+ if interface_id is not None: # Query one interface
+ path = meraki.construct_path('get_one', net_id=net_id, custom={'stack_id': meraki.params['stack_id'],
+ 'interface_id': interface_id})
+ response = meraki.request(path, method='GET')
+ meraki.result['data'] = response
+ meraki.exit_json(**meraki.result)
+ else: # Query all interfaces
+ path = meraki.construct_path('get_all', net_id=net_id, custom={'stack_id': meraki.params['stack_id']})
+ response = meraki.request(path, method='GET')
+ meraki.result['data'] = response
+ meraki.exit_json(**meraki.result)
+ elif meraki.params['state'] == 'present':
+ if interface_id is None: # Create a new interface
+ payload = construct_payload(meraki)
+ if meraki.check_mode is True:
+ meraki.result['data'] = payload
+ meraki.result['changed'] = True
+ meraki.exit_json(**meraki.result)
+ path = meraki.construct_path('create', net_id=net_id, custom={'stack_id': meraki.params['stack_id']})
+ response = meraki.request(path, method='POST', payload=json.dumps(payload))
+ meraki.result['data'] = response
+ meraki.result['changed'] = True
+ meraki.exit_json(**meraki.result)
+ else:
+ if interfaces is None:
+ path = meraki.construct_path('get_all', net_id=net_id, custom={'stack_id': meraki.params['stack_id']})
+ interfaces = meraki.request(path, method='GET')
+ payload = construct_payload(meraki)
+ interface = get_interface(interfaces, interface_id)
+ if meraki.is_update_required(interface, payload):
+ if meraki.check_mode is True:
+ interface.update(payload)
+ meraki.result['data'] = interface
+ meraki.result['changed'] = True
+ meraki.exit_json(**meraki.result)
+ path = meraki.construct_path('update', net_id=net_id, custom={'stack_id': meraki.params['stack_id'],
+ 'interface_id': interface_id})
+ response = meraki.request(path, method='PUT', payload=json.dumps(payload))
+ meraki.result['data'] = response
+ meraki.result['changed'] = True
+ meraki.exit_json(**meraki.result)
+ else:
+ meraki.result['data'] = interface
+ meraki.exit_json(**meraki.result)
+ elif meraki.params['state'] == 'absent':
+ if meraki.check_mode is True:
+ meraki.result['data'] = {}
+ meraki.result['changed'] = True
+ meraki.exit_json(**meraki.result)
+ path = meraki.construct_path('delete', net_id=net_id, custom={'stack_id': meraki.params['stack_id'],
+ 'interface_id': meraki.params['interface_id']})
+ response = meraki.request(path, method='DELETE')
+ meraki.result['data'] = response
+ meraki.result['changed'] = True
+
+ # in the event of a successful module execution, you will want to
+ # simple AnsibleModule.exit_json(), passing the key/value results
+ meraki.exit_json(**meraki.result)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/ansible_collections/cisco/meraki/plugins/modules/meraki_ms_storm_control.py b/ansible_collections/cisco/meraki/plugins/modules/meraki_ms_storm_control.py
new file mode 100644
index 000000000..2048ad5e9
--- /dev/null
+++ b/ansible_collections/cisco/meraki/plugins/modules/meraki_ms_storm_control.py
@@ -0,0 +1,201 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Copyright: (c) 2019, Kevin Breit (@kbreit) <kevin.breit@kevinbreit.net>
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+ANSIBLE_METADATA = {
+ 'metadata_version': '1.1',
+ 'status': ['preview'],
+ 'supported_by': 'community'
+}
+
+DOCUMENTATION = r'''
+---
+module: meraki_ms_storm_control
+short_description: Manage storm control configuration on a switch in the Meraki cloud
+version_added: "0.0.1"
+description:
+- Allows for management of storm control settings for Meraki MS switches.
+options:
+ state:
+ description:
+ - Specifies whether storm control configuration should be queried or modified.
+ choices: [query, present]
+ default: query
+ type: str
+ net_name:
+ description:
+ - Name of network.
+ type: str
+ net_id:
+ description:
+ - ID of network.
+ type: str
+ broadcast_threshold:
+ description:
+ - Percentage (1 to 99) of total available port bandwidth for broadcast traffic type.
+ - Default value 100 percent rate is to clear the configuration.
+ type: int
+ multicast_threshold:
+ description:
+ - Percentage (1 to 99) of total available port bandwidth for multicast traffic type.
+ - Default value 100 percent rate is to clear the configuration.
+ type: int
+ unknown_unicast_threshold:
+ description:
+ - Percentage (1 to 99) of total available port bandwidth for unknown unicast traffic type.
+ - Default value 100 percent rate is to clear the configuration.
+ type: int
+
+author:
+- Kevin Breit (@kbreit)
+extends_documentation_fragment: cisco.meraki.meraki
+'''
+
+EXAMPLES = r'''
+- name: Set broadcast settings
+ meraki_switch_storm_control:
+ auth_key: abc123
+ state: present
+ org_name: YourOrg
+ net_name: YourNet
+ broadcast_threshold: 75
+ multicast_threshold: 70
+ unknown_unicast_threshold: 65
+ delegate_to: localhost
+
+- name: Query storm control settings
+ meraki_switch_storm_control:
+ auth_key: abc123
+ state: query
+ org_name: YourOrg
+ net_name: YourNet
+ delegate_to: localhost
+'''
+
+RETURN = r'''
+data:
+ description: Information queried or updated storm control configuration.
+ returned: success
+ type: complex
+ contains:
+ broadcast_threshold:
+ description:
+ - Percentage (1 to 99) of total available port bandwidth for broadcast traffic type.
+ - Default value 100 percent rate is to clear the configuration.
+ returned: success
+ type: int
+ sample: 42
+ multicast_threshold:
+ description:
+ - Percentage (1 to 99) of total available port bandwidth for multicast traffic type.
+ - Default value 100 percent rate is to clear the configuration.
+ returned: success
+ type: int
+ sample: 42
+ unknown_unicast_threshold:
+ description:
+ - Percentage (1 to 99) of total available port bandwidth for unknown unicast traffic type.
+ - Default value 100 percent rate is to clear the configuration.
+ returned: success
+ type: int
+ sample: 42
+'''
+
+from ansible.module_utils.basic import AnsibleModule, json
+from ansible.module_utils.common.dict_transformations import recursive_diff
+from ansible_collections.cisco.meraki.plugins.module_utils.network.meraki.meraki import MerakiModule, meraki_argument_spec
+
+
+def construct_payload(params):
+ payload = dict()
+ if 'broadcast_threshold' in params:
+ payload['broadcastThreshold'] = params['broadcast_threshold']
+ if 'multicast_threshold' in params:
+ payload['multicastThreshold'] = params['multicast_threshold']
+ if 'unknown_unicast_threshold' in params:
+ payload['unknownUnicastThreshold'] = params['unknown_unicast_threshold']
+ return payload
+
+
+def main():
+ # define the available arguments/parameters that a user can pass to
+ # the module
+ argument_spec = meraki_argument_spec()
+ argument_spec.update(state=dict(type='str', choices=['present', 'query'], default='query'),
+ net_name=dict(type='str'),
+ net_id=dict(type='str'),
+ broadcast_threshold=dict(type='int'),
+ multicast_threshold=dict(type='int'),
+ unknown_unicast_threshold=dict(type='int'),
+ )
+
+ # the AnsibleModule object will be our abstraction working with Ansible
+ # this includes instantiation, a couple of common attr would be the
+ # args/params passed to the execution, as well as if the module
+ # supports check mode
+ module = AnsibleModule(argument_spec=argument_spec,
+ supports_check_mode=True,
+ )
+ meraki = MerakiModule(module, function='switch_storm_control')
+ meraki.params['follow_redirects'] = 'all'
+
+ query_urls = {'switch_storm_control': '/networks/{net_id}/switch/stormControl'}
+ update_url = {'switch_storm_control': '/networks/{net_id}/switch/stormControl'}
+
+ meraki.url_catalog['get_all'].update(query_urls)
+ meraki.url_catalog['update'] = update_url
+
+ payload = None
+
+ org_id = meraki.params['org_id']
+ if not org_id:
+ org_id = meraki.get_org_id(meraki.params['org_name'])
+ net_id = meraki.params['net_id']
+ if net_id is None:
+ nets = meraki.get_nets(org_id=org_id)
+ net_id = meraki.get_net_id(net_name=meraki.params['net_name'], data=nets)
+
+ # execute checks for argument completeness
+
+ # manipulate or modify the state as needed (this is going to be the
+ # part where your module will do what it needs to do)
+ if meraki.params['state'] == 'query':
+ path = meraki.construct_path('get_all', net_id=net_id)
+ response = meraki.request(path, method='GET')
+ if meraki.status == 200:
+ meraki.result['data'] = response
+ elif meraki.params['state'] == 'present':
+ path = meraki.construct_path('get_all', net_id=net_id)
+ original = meraki.request(path, method='GET')
+ payload = construct_payload(meraki.params)
+ if meraki.is_update_required(original, payload) is True:
+ diff = recursive_diff(original, payload)
+ if meraki.check_mode is True:
+ original.update(payload)
+ meraki.result['data'] = original
+ meraki.result['changed'] = True
+ meraki.result['diff'] = {'before': diff[0],
+ 'after': diff[1]}
+ meraki.exit_json(**meraki.result)
+ path = meraki.construct_path('update', net_id=net_id)
+ response = meraki.request(path, method='PUT', payload=json.dumps(payload))
+ if meraki.status == 200:
+ meraki.result['diff'] = {'before': diff[0],
+ 'after': diff[1]}
+ meraki.result['data'] = response
+ meraki.result['changed'] = True
+ else:
+ meraki.result['data'] = original
+
+ # in the event of a successful module execution, you will want to
+ # simple AnsibleModule.exit_json(), passing the key/value results
+ meraki.exit_json(**meraki.result)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/ansible_collections/cisco/meraki/plugins/modules/meraki_ms_switchport.py b/ansible_collections/cisco/meraki/plugins/modules/meraki_ms_switchport.py
new file mode 100644
index 000000000..a8048bf24
--- /dev/null
+++ b/ansible_collections/cisco/meraki/plugins/modules/meraki_ms_switchport.py
@@ -0,0 +1,680 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Copyright: (c) 2018, Kevin Breit (@kbreit) <kevin.breit@kevinbreit.net>
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+
+__metaclass__ = type
+
+ANSIBLE_METADATA = {
+ "metadata_version": "1.1",
+ "status": ["preview"],
+ "supported_by": "community",
+}
+
+DOCUMENTATION = r"""
+---
+module: meraki_ms_switchport
+short_description: Manage switchports on a switch in the Meraki cloud
+description:
+- Allows for management of switchports settings for Meraki MS switches.
+options:
+ state:
+ description:
+ - Specifies whether a switchport should be queried or modified.
+ choices: [query, present]
+ default: query
+ type: str
+ access_policy_type:
+ description:
+ - Type of access policy to apply to port.
+ type: str
+ choices: [Open, Custom access policy, MAC allow list, Sticky MAC allow list]
+ access_policy_number:
+ description:
+ - Number of the access policy to apply.
+ - Only applicable to access port types.
+ type: int
+ allowed_vlans:
+ description:
+ - List of VLAN numbers to be allowed on switchport.
+ default: all
+ type: list
+ elements: str
+ enabled:
+ description:
+ - Whether a switchport should be enabled or disabled.
+ type: bool
+ default: yes
+ isolation_enabled:
+ description:
+ - Isolation status of switchport.
+ default: no
+ type: bool
+ link_negotiation:
+ description:
+ - Link speed for the switchport.
+ default: Auto negotiate
+ choices: [Auto negotiate, 100 Megabit (auto), 100 Megabit full duplex (forced)]
+ type: str
+ name:
+ description:
+ - Switchport description.
+ aliases: [description]
+ type: str
+ number:
+ description:
+ - Port number.
+ type: str
+ poe_enabled:
+ description:
+ - Enable or disable Power Over Ethernet on a port.
+ type: bool
+ default: true
+ rstp_enabled:
+ description:
+ - Enable or disable Rapid Spanning Tree Protocol on a port.
+ type: bool
+ default: true
+ serial:
+ description:
+ - Serial nubmer of the switch.
+ type: str
+ required: true
+ stp_guard:
+ description:
+ - Set state of STP guard.
+ choices: [disabled, root guard, bpdu guard, loop guard]
+ default: disabled
+ type: str
+ tags:
+ description:
+ - List of tags to assign to a port.
+ type: list
+ elements: str
+ type:
+ description:
+ - Set port type.
+ choices: [access, trunk]
+ default: access
+ type: str
+ vlan:
+ description:
+ - VLAN number assigned to port.
+ - If a port is of type trunk, the specified VLAN is the native VLAN.
+ - Setting value to 0 on a trunk will clear the VLAN.
+ type: int
+ voice_vlan:
+ description:
+ - VLAN number assigned to a port for voice traffic.
+ - Only applicable to access port type.
+ - Only applicable if voice_vlan_state is set to present.
+ type: int
+ voice_vlan_state:
+ description:
+ - Specifies whether voice vlan configuration should be present or absent.
+ choices: [absent, present]
+ default: present
+ type: str
+ mac_allow_list:
+ description:
+ - MAC addresses list that are allowed on a port.
+ - Only applicable to access port type.
+ - Only applicable to access_policy_type "MAC allow list".
+ type: dict
+ suboptions:
+ state:
+ description:
+ - The state the configuration should be left in.
+ - Merged, MAC addresses provided will be added to the current allow list.
+ - Replaced, All MAC addresses are overwritten, only the MAC addresses provided with exist in the allow list.
+ - Deleted, Remove the MAC addresses provided from the current allow list.
+ type: str
+ choices: [merged, replaced, deleted]
+ default: replaced
+ macs:
+ description:
+ - List of MAC addresses to update with based on state option.
+ type: list
+ elements: str
+ sticky_mac_allow_list:
+ description:
+ - MAC addresses list that are allowed on a port.
+ - Only applicable to access port type.
+ - Only applicable to access_policy_type "Sticky MAC allow list".
+ type: dict
+ suboptions:
+ state:
+ description:
+ - The state the configuration should be left in.
+ - Merged, MAC addresses provided will be added to the current allow list.
+ - Replaced, All MAC addresses are overwritten, only the MAC addresses provided with exist in the allow list.
+ - Deleted, Remove the MAC addresses provided from the current allow list.
+ type: str
+ choices: ["merged", "replaced", "deleted"]
+ default: replaced
+ macs:
+ description:
+ - List of MAC addresses to update with based on state option.
+ type: list
+ elements: str
+ sticky_mac_allow_list_limit:
+ description:
+ - The number of MAC addresses allowed in the sticky port allow list.
+ - Only applicable to access port type.
+ - Only applicable to access_policy_type "Sticky MAC allow list".
+ - The value must be equal to or greater then the list size of sticky_mac_allow_list. Value will be checked for validity, during processing.
+ type: int
+ flexible_stacking_enabled:
+ description:
+ - Whether flexible stacking capabilities are supported on the port.
+ type: bool
+author:
+- Kevin Breit (@kbreit)
+extends_documentation_fragment: cisco.meraki.meraki
+"""
+
+EXAMPLES = r"""
+- name: Query information about all switchports on a switch
+ meraki_switchport:
+ auth_key: abc12345
+ state: query
+ serial: ABC-123
+ delegate_to: localhost
+
+- name: Query information about all switchports on a switch
+ meraki_switchport:
+ auth_key: abc12345
+ state: query
+ serial: ABC-123
+ number: 2
+ delegate_to: localhost
+
+- name: Name switchport
+ meraki_switchport:
+ auth_key: abc12345
+ state: present
+ serial: ABC-123
+ number: 7
+ name: Test Port
+ delegate_to: localhost
+
+- name: Configure access port with voice VLAN
+ meraki_switchport:
+ auth_key: abc12345
+ state: present
+ serial: ABC-123
+ number: 7
+ enabled: true
+ name: Test Port
+ tags: desktop
+ type: access
+ vlan: 10
+ voice_vlan: 11
+ delegate_to: localhost
+
+- name: Check access port for idempotency
+ meraki_switchport:
+ auth_key: abc12345
+ state: present
+ serial: ABC-123
+ number: 7
+ enabled: true
+ name: Test Port
+ tags: desktop
+ type: access
+ vlan: 10
+ voice_vlan: 11
+ delegate_to: localhost
+
+- name: Configure trunk port with specific VLANs
+ meraki_switchport:
+ auth_key: abc12345
+ state: present
+ serial: ABC-123
+ number: 7
+ enabled: true
+ name: Server port
+ tags: server
+ type: trunk
+ allowed_vlans:
+ - 10
+ - 15
+ - 20
+ delegate_to: localhost
+
+- name: Configure access port with sticky MAC allow list and limit.
+ meraki_switchport:
+ auth_key: abc12345
+ state: present
+ serial: ABC-123
+ number: 5
+ sticky_mac_allow_limit: 3
+ sticky_mac_allow_list:
+ macs:
+ - aa:aa:bb:bb:cc:cc
+ - bb:bb:aa:aa:cc:cc
+ - 11:aa:bb:bb:cc:cc
+ state: replaced
+ delegate_to: localhost
+
+- name: Delete an existing MAC address from the sticky MAC allow list.
+ meraki_switchport:
+ auth_key: abc12345
+ state: present
+ serial: ABC-123
+ number: 5
+ sticky_mac_allow_list:
+ macs:
+ - aa:aa:bb:bb:cc:cc
+ state: deleted
+ delegate_to: localhost
+
+- name: Add a MAC address to sticky MAC allow list.
+ meraki_switchport:
+ auth_key: abc12345
+ state: present
+ serial: ABC-123
+ number: 5
+ sticky_mac_allow_list:
+ macs:
+ - 22:22:bb:bb:cc:cc
+ state: merged
+ delegate_to: localhost
+"""
+
+RETURN = r"""
+data:
+ description: Information queried or updated switchports.
+ returned: success
+ type: complex
+ contains:
+ access_policy_type:
+ description: Type of access policy assigned to port
+ returned: success, when assigned
+ type: str
+ sample: "MAC allow list"
+ allowed_vlans:
+ description: List of VLANs allowed on an access port
+ returned: success, when port is set as access
+ type: str
+ sample: all
+ number:
+ description: Number of port.
+ returned: success
+ type: int
+ sample: 1
+ name:
+ description: Human friendly description of port.
+ returned: success
+ type: str
+ sample: "Jim Phone Port"
+ tags:
+ description: List of tags assigned to port.
+ returned: success
+ type: list
+ sample: ['phone', 'marketing']
+ enabled:
+ description: Enabled state of port.
+ returned: success
+ type: bool
+ sample: true
+ poe_enabled:
+ description: Power Over Ethernet enabled state of port.
+ returned: success
+ type: bool
+ sample: true
+ type:
+ description: Type of switchport.
+ returned: success
+ type: str
+ sample: trunk
+ vlan:
+ description: VLAN assigned to port.
+ returned: success
+ type: int
+ sample: 10
+ voice_vlan:
+ description: VLAN assigned to port with voice VLAN enabled devices.
+ returned: success
+ type: int
+ sample: 20
+ isolation_enabled:
+ description: Port isolation status of port.
+ returned: success
+ type: bool
+ sample: true
+ rstp_enabled:
+ description: Enabled or disabled state of Rapid Spanning Tree Protocol (RSTP)
+ returned: success
+ type: bool
+ sample: true
+ stp_guard:
+ description: State of STP guard
+ returned: success
+ type: str
+ sample: "Root Guard"
+ access_policy_number:
+ description: Number of assigned access policy. Only applicable to access ports.
+ returned: success
+ type: int
+ sample: 1234
+ link_negotiation:
+ description: Link speed for the port.
+ returned: success
+ type: str
+ sample: "Auto negotiate"
+ sticky_mac_allow_list_limit:
+ description: Number of MAC addresses allowed on a sticky port.
+ returned: success
+ type: int
+ sample: 6
+ sticky_mac_allow_list:
+ description: List of MAC addresses currently allowed on a sticky port. Used with access_policy_type of Sticky MAC allow list.
+ returned: success
+ type: list
+ sample: ["11:aa:bb:bb:cc:cc", "22:aa:bb:bb:cc:cc", "33:aa:bb:bb:cc:cc"]
+ mac_allow_list:
+ description: List of MAC addresses currently allowed on a non-sticky port. Used with access_policy_type of MAC allow list.
+ returned: success
+ type: list
+ sample: ["11:aa:bb:bb:cc:cc", "22:aa:bb:bb:cc:cc", "33:aa:bb:bb:cc:cc"]
+ port_schedule_id:
+ description: Unique ID of assigned port schedule
+ returned: success
+ type: str
+ sample: null
+ udld:
+ description: Alert state of UDLD
+ returned: success
+ type: str
+ sample: "Alert only"
+ flexible_stacking_enabled:
+ description: Whether flexible stacking capabilities are enabled on the port.
+ returned: success
+ type: bool
+"""
+
+from ansible.module_utils.basic import AnsibleModule, json
+from ansible_collections.cisco.meraki.plugins.module_utils.network.meraki.meraki import (
+ MerakiModule,
+ meraki_argument_spec,
+)
+
+param_map = {
+ "access_policy_number": "accessPolicyNumber",
+ "access_policy_type": "accessPolicyType",
+ "allowed_vlans": "allowedVlans",
+ "enabled": "enabled",
+ "isolation_enabled": "isolationEnabled",
+ "link_negotiation": "linkNegotiation",
+ "name": "name",
+ "number": "number",
+ "poe_enabled": "poeEnabled",
+ "rstp_enabled": "rstpEnabled",
+ "stp_guard": "stpGuard",
+ "tags": "tags",
+ "type": "type",
+ "vlan": "vlan",
+ "voice_vlan": "voiceVlan",
+ "mac_allow_list": "macAllowList",
+ "sticky_mac_allow_list": "stickyMacAllowList",
+ "sticky_mac_allow_list_limit": "stickyMacAllowListLimit",
+ "adaptive_policy_group_id": "adaptivePolicyGroupId",
+ "peer_sgt_capable": "peerSgtCapable",
+ "flexible_stacking_enabled": "flexibleStackingEnabled",
+}
+
+
+def sort_vlans(meraki, vlans):
+ converted = set()
+ for vlan in vlans:
+ converted.add(int(vlan))
+ vlans_sorted = sorted(converted)
+ vlans_str = []
+ for vlan in vlans_sorted:
+ vlans_str.append(str(vlan))
+ return ",".join(vlans_str)
+
+
+def assemble_payload(meraki):
+ payload = dict()
+ # if meraki.params['enabled'] is not None:
+ # payload['enabled'] = meraki.params['enabled']
+
+ for k, v in meraki.params.items():
+ try:
+ if meraki.params[k] is not None:
+ if k == "access_policy_number":
+ if meraki.params["access_policy_type"] is not None:
+ payload[param_map[k]] = v
+ else:
+ payload[param_map[k]] = v
+ except KeyError:
+ pass
+ return payload
+
+
+def get_mac_list(original_allowed, new_mac_list, state):
+ if state == "deleted":
+ return [entry for entry in original_allowed if entry not in new_mac_list]
+ if state == "merged":
+ return original_allowed + list(set(new_mac_list) - set(original_allowed))
+ return new_mac_list
+
+
+def clear_vlan(params, payload):
+ if params["vlan"] == 0:
+ payload["vlan"] = None
+ return payload
+
+
+def main():
+ # define the available arguments/parameters that a user can pass to
+ # the module
+ argument_spec = meraki_argument_spec()
+
+ policy_data_arg_spec = dict(
+ macs=dict(type="list", elements="str"),
+ state=dict(
+ type="str", choices=["merged", "replaced", "deleted"], default="replaced"
+ ),
+ )
+
+ argument_spec.update(
+ state=dict(type="str", choices=["present", "query"], default="query"),
+ serial=dict(type="str", required=True),
+ number=dict(type="str"),
+ name=dict(type="str", aliases=["description"]),
+ tags=dict(type="list", elements="str"),
+ enabled=dict(type="bool", default=True),
+ type=dict(type="str", choices=["access", "trunk"], default="access"),
+ vlan=dict(type="int"),
+ voice_vlan=dict(type="int"),
+ voice_vlan_state=dict(
+ type="str", choices=["present", "absent"], default="present"
+ ),
+ allowed_vlans=dict(type="list", elements="str", default="all"),
+ poe_enabled=dict(type="bool", default=True),
+ isolation_enabled=dict(type="bool", default=False),
+ rstp_enabled=dict(type="bool", default=True),
+ stp_guard=dict(
+ type="str",
+ choices=["disabled", "root guard", "bpdu guard", "loop guard"],
+ default="disabled",
+ ),
+ access_policy_type=dict(
+ type="str",
+ choices=[
+ "Open",
+ "Custom access policy",
+ "MAC allow list",
+ "Sticky MAC allow list",
+ ],
+ ),
+ access_policy_number=dict(type="int"),
+ link_negotiation=dict(
+ type="str",
+ choices=[
+ "Auto negotiate",
+ "100 Megabit (auto)",
+ "100 Megabit full duplex (forced)",
+ ],
+ default="Auto negotiate",
+ ),
+ mac_allow_list=dict(type="dict", options=policy_data_arg_spec),
+ sticky_mac_allow_list=dict(type="dict", options=policy_data_arg_spec),
+ sticky_mac_allow_list_limit=dict(type="int"),
+ # adaptive_policy_group_id=dict(type=str),
+ # peer_sgt_capable=dict(type=bool),
+ flexible_stacking_enabled=dict(type="bool"),
+ )
+
+ # the AnsibleModule object will be our abstraction working with Ansible
+ # this includes instantiation, a couple of common attr would be the
+ # args/params passed to the execution, as well as if the module
+ # supports check mode
+ module = AnsibleModule(
+ argument_spec=argument_spec,
+ supports_check_mode=True,
+ )
+ meraki = MerakiModule(module, function="switchport")
+ if meraki.params.get("voice_vlan_state") == "absent" and meraki.params.get(
+ "voice_vlan"
+ ):
+ meraki.fail_json(
+ msg="voice_vlan_state cant be `absent` while voice_vlan is also defined."
+ )
+
+ meraki.params["follow_redirects"] = "all"
+
+ if meraki.params["type"] == "trunk":
+ if not meraki.params["allowed_vlans"]:
+ meraki.params["allowed_vlans"] = [
+ "all"
+ ] # Backdoor way to set default without conflicting on access
+
+ query_urls = {"switchport": "/devices/{serial}/switch/ports"}
+ query_url = {"switchport": "/devices/{serial}/switch/ports/{number}"}
+ update_url = {"switchport": "/devices/{serial}/switch/ports/{number}"}
+
+ meraki.url_catalog["get_all"].update(query_urls)
+ meraki.url_catalog["get_one"].update(query_url)
+ meraki.url_catalog["update"] = update_url
+
+ # execute checks for argument completeness
+
+ # manipulate or modify the state as needed (this is going to be the
+ # part where your module will do what it needs to do)
+ if meraki.params["state"] == "query":
+ if meraki.params["number"]:
+ path = meraki.construct_path(
+ "get_one",
+ custom={
+ "serial": meraki.params["serial"],
+ "number": meraki.params["number"],
+ },
+ )
+ response = meraki.request(path, method="GET")
+ meraki.result["data"] = response
+ else:
+ path = meraki.construct_path(
+ "get_all", custom={"serial": meraki.params["serial"]}
+ )
+ response = meraki.request(path, method="GET")
+ meraki.result["data"] = response
+ elif meraki.params["state"] == "present":
+ payload = assemble_payload(meraki)
+ # meraki.fail_json(msg='payload', payload=payload)
+ allowed = set() # Use a set to remove duplicate items
+ if meraki.params["allowed_vlans"][0] == "all":
+ allowed.add("all")
+ else:
+ for vlan in meraki.params["allowed_vlans"]:
+ allowed.add(str(vlan))
+ if meraki.params["vlan"] is not None:
+ allowed.add(str(meraki.params["vlan"]))
+ if len(allowed) > 1: # Convert from list to comma separated
+ payload["allowedVlans"] = sort_vlans(meraki, allowed)
+ else:
+ payload["allowedVlans"] = next(iter(allowed))
+
+ # Exceptions need to be made for idempotency check based on how Meraki returns
+ if meraki.params["type"] == "access":
+ if not meraki.params[
+ "vlan"
+ ]: # VLAN needs to be specified in access ports, but can't default to it
+ payload["vlan"] = 1
+ query_path = meraki.construct_path(
+ "get_one",
+ custom={
+ "serial": meraki.params["serial"],
+ "number": meraki.params["number"],
+ },
+ )
+ original = meraki.request(query_path, method="GET")
+ # Check voiceVlan to see if state is absent to remove the vlan.
+ if meraki.params.get("voice_vlan_state"):
+ if meraki.params.get("voice_vlan_state") == "absent":
+ payload["voiceVlan"] = None
+ else:
+ payload["voiceVlan"] = meraki.params.get("voice_vlan")
+ if meraki.params.get("mac_allow_list"):
+ macs = get_mac_list(
+ original.get("macAllowList"),
+ meraki.params["mac_allow_list"].get("macs"),
+ meraki.params["mac_allow_list"].get("state"),
+ )
+ payload["macAllowList"] = macs
+ # Evaluate Sticky Limit whether it was passed in or what is currently configured and was returned in GET call.
+ if meraki.params.get("sticky_mac_allow_list_limit"):
+ sticky_mac_limit = meraki.params.get("sticky_mac_allow_list_limit")
+ else:
+ sticky_mac_limit = original.get("stickyMacAllowListLimit")
+ if meraki.params.get("sticky_mac_allow_list"):
+ macs = get_mac_list(
+ original.get("stickyMacAllowList"),
+ meraki.params["sticky_mac_allow_list"].get("macs"),
+ meraki.params["sticky_mac_allow_list"].get("state"),
+ )
+ if int(sticky_mac_limit) < len(macs):
+ meraki.fail_json(
+ msg="Stick MAC Allow List Limit must be equal to or greater than length of Sticky MAC Allow List."
+ )
+ payload["stickyMacAllowList"] = macs
+ payload["stickyMacAllowListLimit"] = sticky_mac_limit
+ payload = clear_vlan(meraki.params, payload)
+ proposed = payload.copy()
+ if meraki.params["type"] == "trunk":
+ proposed["voiceVlan"] = original[
+ "voiceVlan"
+ ] # API shouldn't include voice VLAN on a trunk port
+ # meraki.fail_json(msg='Compare', original=original, payload=payload)
+ if meraki.is_update_required(original, proposed, optional_ignore=["number"]):
+ if meraki.check_mode is True:
+ original.update(proposed)
+ meraki.result["data"] = original
+ meraki.result["changed"] = True
+ meraki.exit_json(**meraki.result)
+ path = meraki.construct_path(
+ "update",
+ custom={
+ "serial": meraki.params["serial"],
+ "number": meraki.params["number"],
+ },
+ )
+ response = meraki.request(path, method="PUT", payload=json.dumps(payload))
+ meraki.result["data"] = response
+ meraki.result["changed"] = True
+ else:
+ meraki.result["data"] = original
+
+ # in the event of a successful module execution, you will want to
+ # simple AnsibleModule.exit_json(), passing the key/value results
+ meraki.exit_json(**meraki.result)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/ansible_collections/cisco/meraki/plugins/modules/meraki_mx_content_filtering.py b/ansible_collections/cisco/meraki/plugins/modules/meraki_mx_content_filtering.py
new file mode 100644
index 000000000..d189a4e44
--- /dev/null
+++ b/ansible_collections/cisco/meraki/plugins/modules/meraki_mx_content_filtering.py
@@ -0,0 +1,302 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Copyright: (c) 2019, Kevin Breit (@kbreit) <kevin.breit@kevinbreit.net>
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+ANSIBLE_METADATA = {
+ 'metadata_version': '1.1',
+ 'status': ['preview'],
+ 'supported_by': 'community'
+}
+
+DOCUMENTATION = r'''
+---
+module: meraki_mx_content_filtering
+short_description: Edit Meraki MX content filtering policies
+description:
+- Allows for setting policy on content filtering.
+options:
+ auth_key:
+ description:
+ - Authentication key provided by the dashboard. Required if environmental variable MERAKI_KEY is not set.
+ type: str
+ net_name:
+ description:
+ - Name of a network.
+ aliases: [ network ]
+ type: str
+ net_id:
+ description:
+ - ID number of a network.
+ type: str
+ state:
+ description:
+ - States that a policy should be created or modified.
+ choices: [present, query]
+ default: present
+ type: str
+ allowed_urls:
+ description:
+ - List of URL patterns which should be allowed.
+ type: list
+ elements: str
+ blocked_urls:
+ description:
+ - List of URL patterns which should be blocked.
+ type: list
+ elements: str
+ blocked_categories:
+ description:
+ - List of content categories which should be blocked.
+ - Use the C(meraki_content_filtering_facts) module for a full list of categories.
+ type: list
+ elements: str
+ category_list_size:
+ description:
+ - Determines whether a network filters fo rall URLs in a category or only the list of top blocked sites.
+ choices: [ top sites, full list ]
+ type: str
+ subset:
+ description:
+ - Display only certain facts.
+ choices: [categories, policy]
+ type: str
+author:
+ - Kevin Breit (@kbreit)
+extends_documentation_fragment: cisco.meraki.meraki
+'''
+
+EXAMPLES = r'''
+ - name: Set single allowed URL pattern
+ meraki_content_filtering:
+ auth_key: abc123
+ org_name: YourOrg
+ net_name: YourMXNet
+ allowed_urls:
+ - "http://www.ansible.com/*"
+
+ - name: Set blocked URL category
+ meraki_content_filtering:
+ auth_key: abc123
+ org_name: YourOrg
+ net_name: YourMXNet
+ state: present
+ category_list_size: full list
+ blocked_categories:
+ - "Adult and Pornography"
+
+ - name: Remove match patterns and categories
+ meraki_content_filtering:
+ auth_key: abc123
+ org_name: YourOrg
+ net_name: YourMXNet
+ state: present
+ category_list_size: full list
+ allowed_urls: []
+ blocked_urls: []
+'''
+
+RETURN = r'''
+data:
+ description: Information about the created or manipulated object.
+ returned: info
+ type: complex
+ contains:
+ categories:
+ description: List of available content filtering categories.
+ returned: query for categories
+ type: complex
+ contains:
+ id:
+ description: Unique ID of content filtering category.
+ returned: query for categories
+ type: str
+ sample: "meraki:contentFiltering/category/1"
+ name:
+ description: Name of content filtering category.
+ returned: query for categories
+ type: str
+ sample: "Real Estate"
+ allowed_url_patterns:
+ description: Explicitly permitted URL patterns
+ returned: query for policy
+ type: list
+ sample: ["http://www.ansible.com"]
+ blocked_url_patterns:
+ description: Explicitly denied URL patterns
+ returned: query for policy
+ type: list
+ sample: ["http://www.ansible.net"]
+ blocked_url_categories:
+ description: List of blocked URL categories
+ returned: query for policy
+ type: complex
+ contains:
+ id:
+ description: Unique ID of category to filter
+ returned: query for policy
+ type: list
+ sample: ["meraki:contentFiltering/category/1"]
+ name:
+ description: Name of category to filter
+ returned: query for policy
+ type: list
+ sample: ["Real Estate"]
+ url_cateogory_list_size:
+ description: Size of categories to cache on MX appliance
+ returned: query for policy
+ type: str
+ sample: "topSites"
+'''
+
+
+from ansible.module_utils.basic import AnsibleModule, json
+from ansible_collections.cisco.meraki.plugins.module_utils.network.meraki.meraki import MerakiModule, meraki_argument_spec, recursive_diff
+from copy import deepcopy
+
+
+def get_category_dict(meraki, full_list, category):
+ for i in full_list['categories']:
+ if i['name'] == category:
+ return i['id']
+ meraki.fail_json(msg="{0} is not a valid content filtering category".format(category))
+
+
+def main():
+
+ # define the available arguments/parameters that a user can pass to
+ # the module
+
+ argument_spec = meraki_argument_spec()
+ argument_spec.update(
+ net_id=dict(type='str'),
+ net_name=dict(type='str', aliases=['network']),
+ state=dict(type='str', default='present', choices=['present', 'query']),
+ allowed_urls=dict(type='list', elements='str'),
+ blocked_urls=dict(type='list', elements='str'),
+ blocked_categories=dict(type='list', elements='str'),
+ category_list_size=dict(type='str', choices=['top sites', 'full list']),
+ subset=dict(type='str', choices=['categories', 'policy']),
+ )
+
+ # the AnsibleModule object will be our abstraction working with Ansible
+ # this includes instantiation, a couple of common attr would be the
+ # args/params passed to the execution, as well as if the module
+ # supports check mode
+ module = AnsibleModule(argument_spec=argument_spec,
+ supports_check_mode=True,
+ )
+
+ meraki = MerakiModule(module, function='content_filtering')
+ module.params['follow_redirects'] = 'all'
+
+ category_urls = {'content_filtering': '/networks/{net_id}/appliance/contentFiltering/categories'}
+ policy_urls = {'content_filtering': '/networks/{net_id}/appliance/contentFiltering'}
+
+ meraki.url_catalog['categories'] = category_urls
+ meraki.url_catalog['policy'] = policy_urls
+
+ if meraki.params['net_name'] and meraki.params['net_id']:
+ meraki.fail_json(msg='net_name and net_id are mutually exclusive')
+
+ # manipulate or modify the state as needed (this is going to be the
+ # part where your module will do what it needs to do)
+
+ org_id = meraki.params['org_id']
+ if not org_id:
+ org_id = meraki.get_org_id(meraki.params['org_name'])
+ net_id = None
+ if net_id is None:
+ nets = meraki.get_nets(org_id=org_id)
+ net_id = meraki.get_net_id(org_id, meraki.params['net_name'], data=nets)
+
+ if meraki.params['state'] == 'query':
+ if meraki.params['subset']:
+ if meraki.params['subset'] == 'categories':
+ path = meraki.construct_path('categories', net_id=net_id)
+ elif meraki.params['subset'] == 'policy':
+ path = meraki.construct_path('policy', net_id=net_id)
+ meraki.result['data'] = meraki.request(path, method='GET')
+ else:
+ response_data = {'categories': None,
+ 'policy': None,
+ }
+ path = meraki.construct_path('categories', net_id=net_id)
+ response_data['categories'] = meraki.request(path, method='GET')
+ path = meraki.construct_path('policy', net_id=net_id)
+ response_data['policy'] = meraki.request(path, method='GET')
+ meraki.result['data'] = response_data
+ if module.params['state'] == 'present':
+ payload = dict()
+ if meraki.params['allowed_urls']:
+ if meraki.params['allowed_urls'] == ['None']: # Corner case for resetting
+ payload['allowedUrlPatterns'] = []
+ else:
+ payload['allowedUrlPatterns'] = meraki.params['allowed_urls']
+ if meraki.params['blocked_urls']:
+ if meraki.params['blocked_urls'] == ['None']: # Corner case for resetting
+ payload['blockedUrlPatterns'] = []
+ else:
+ payload['blockedUrlPatterns'] = meraki.params['blocked_urls']
+ if meraki.params['blocked_categories']:
+ if meraki.params['blocked_categories'] == ['None']: # Corner case for resetting
+ payload['blockedUrlCategories'] = []
+ else:
+ category_path = meraki.construct_path('categories', net_id=net_id)
+ categories = meraki.request(category_path, method='GET')
+ payload['blockedUrlCategories'] = []
+ for category in meraki.params['blocked_categories']:
+ payload['blockedUrlCategories'].append(get_category_dict(meraki,
+ categories,
+ category))
+ if meraki.params['category_list_size']:
+ if meraki.params['category_list_size'].lower() == 'top sites':
+ payload['urlCategoryListSize'] = "topSites"
+ elif meraki.params['category_list_size'].lower() == 'full list':
+ payload['urlCategoryListSize'] = "fullList"
+ path = meraki.construct_path('policy', net_id=net_id)
+ current = meraki.request(path, method='GET')
+ proposed = current.copy()
+ proposed.update(payload)
+ temp_current = deepcopy(current)
+ temp_blocked = []
+ for blocked_category in current['blockedUrlCategories']:
+ temp_blocked.append(blocked_category['id'])
+ temp_current.pop('blockedUrlCategories')
+ if 'blockedUrlCategories' in payload:
+ payload['blockedUrlCategories'].sort()
+ temp_current['blockedUrlCategories'] = temp_blocked
+ if meraki.is_update_required(temp_current, payload) is True:
+ if module.check_mode:
+ meraki.generate_diff(current, payload)
+ current.update(payload)
+ meraki.result['changed'] = True
+ meraki.result['data'] = current
+ meraki.exit_json(**meraki.result)
+ response = meraki.request(path, method='PUT', payload=json.dumps(payload))
+ meraki.result['data'] = response
+ if recursive_diff(current, response) is None:
+ meraki.result['changed'] = False
+ meraki.result['data'] = str(recursive_diff(current, response))
+ else:
+ meraki.result['changed'] = True
+ meraki.generate_diff(current, response)
+ else:
+ meraki.result['data'] = current
+ if module.check_mode:
+ meraki.result['data'] = current
+ meraki.exit_json(**meraki.result)
+ meraki.result['data'] = current
+ meraki.exit_json(**meraki.result)
+
+ # in the event of a successful module execution, you will want to
+ # simple AnsibleModule.exit_json(), passing the key/value results
+ meraki.exit_json(**meraki.result)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/ansible_collections/cisco/meraki/plugins/modules/meraki_mx_intrusion_prevention.py b/ansible_collections/cisco/meraki/plugins/modules/meraki_mx_intrusion_prevention.py
new file mode 100644
index 000000000..0acd6beaa
--- /dev/null
+++ b/ansible_collections/cisco/meraki/plugins/modules/meraki_mx_intrusion_prevention.py
@@ -0,0 +1,371 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Copyright: (c) 2019 Kevin Breit (@kbreit) <kevin.breit@kevinbreit.net>
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+ANSIBLE_METADATA = {
+ 'metadata_version': '1.1',
+ 'status': ['preview'],
+ 'supported_by': 'community'
+}
+
+DOCUMENTATION = r'''
+---
+module: meraki_mx_intrusion_prevention
+short_description: Manage intrustion prevention in the Meraki cloud
+description:
+- Allows for management of intrusion prevention rules networks within Meraki MX networks.
+
+options:
+ state:
+ description:
+ - Create or modify an organization.
+ choices: [ absent, present, query ]
+ default: present
+ type: str
+ net_name:
+ description:
+ - Name of a network.
+ aliases: [ name, network ]
+ type: str
+ net_id:
+ description:
+ - ID number of a network.
+ type: str
+ mode:
+ description:
+ - Operational mode of Intrusion Prevention system.
+ choices: [ detection, disabled, prevention ]
+ type: str
+ ids_rulesets:
+ description:
+ - Ruleset complexity setting.
+ choices: [ connectivity, balanced, security ]
+ type: str
+ allowed_rules:
+ description:
+ - List of IDs related to rules which are allowed for the organization.
+ type: list
+ elements: dict
+ suboptions:
+ rule_id:
+ description:
+ - ID of rule as defined by Snort.
+ type: str
+ rule_message:
+ description:
+ - Description of rule.
+ - This is overwritten by the API.
+ - Formerly C(message) which was deprecated but still maintained as an alias.
+ type: str
+ aliases: [ message ]
+ version_added: "2.3.0"
+ protected_networks:
+ description:
+ - Set included/excluded networks for Intrusion Prevention.
+ type: dict
+ suboptions:
+ use_default:
+ description:
+ - Whether to use special IPv4 addresses per RFC 5735.
+ type: bool
+ included_cidr:
+ description:
+ - List of network IP ranges to include in scanning.
+ type: list
+ elements: str
+ excluded_cidr:
+ description:
+ - List of network IP ranges to exclude from scanning.
+ type: list
+ elements: str
+
+author:
+ - Kevin Breit (@kbreit)
+extends_documentation_fragment: cisco.meraki.meraki
+'''
+
+EXAMPLES = r'''
+- name: Set whitelist for organization
+ meraki_intrusion_prevention:
+ auth_key: '{{auth_key}}'
+ state: present
+ org_id: '{{test_org_id}}'
+ allowed_rules:
+ - rule_id: "meraki:intrusion/snort/GID/01/SID/5805"
+ rule_message: Test rule
+ delegate_to: localhost
+
+- name: Query IPS info for organization
+ meraki_intrusion_prevention:
+ auth_key: '{{auth_key}}'
+ state: query
+ org_name: '{{test_org_name}}'
+ delegate_to: localhost
+ register: query_org
+
+- name: Set full ruleset with check mode
+ meraki_intrusion_prevention:
+ auth_key: '{{auth_key}}'
+ state: present
+ org_name: '{{test_org_name}}'
+ net_name: '{{test_net_name}} - IPS'
+ mode: prevention
+ ids_rulesets: security
+ protected_networks:
+ use_default: true
+ included_cidr:
+ - 192.0.1.0/24
+ excluded_cidr:
+ - 10.0.1.0/24
+ delegate_to: localhost
+
+- name: Clear rules from organization
+ meraki_intrusion_prevention:
+ auth_key: '{{auth_key}}'
+ state: absent
+ org_name: '{{test_org_name}}'
+ allowed_rules: []
+ delegate_to: localhost
+'''
+
+RETURN = r'''
+data:
+ description: Information about the Threat Protection settings.
+ returned: success
+ type: complex
+ contains:
+ whitelistedRules:
+ description: List of whitelisted IPS rules.
+ returned: success, when organization is queried or modified
+ type: complex
+ contains:
+ ruleId:
+ description: A rule identifier for an IPS rule.
+ returned: success, when organization is queried or modified
+ type: str
+ sample: "meraki:intrusion/snort/GID/01/SID/5805"
+ rule_message:
+ description: Description of rule.
+ returned: success, when organization is queried or modified
+ type: str
+ sample: "MALWARE-OTHER Trackware myway speedbar runtime detection - switch engines"
+ mode:
+ description: Enabled setting of intrusion prevention.
+ returned: success, when network is queried or modified
+ type: str
+ sample: enabled
+ idsRulesets:
+ description: Setting of selected ruleset.
+ returned: success, when network is queried or modified
+ type: str
+ sample: balanced
+ protectedNetworks:
+ description: Networks protected by IPS.
+ returned: success, when network is queried or modified
+ type: complex
+ contains:
+ useDefault:
+ description: Whether to use special IPv4 addresses.
+ returned: success, when network is queried or modified
+ type: bool
+ sample: true
+ includedCidr:
+ description: List of CIDR notiation networks to protect.
+ returned: success, when network is queried or modified
+ type: str
+ sample: 192.0.1.0/24
+ excludedCidr:
+ description: List of CIDR notiation networks to exclude from protection.
+ returned: success, when network is queried or modified
+ type: str
+ sample: 192.0.1.0/24
+
+'''
+
+from ansible.module_utils.basic import AnsibleModule, json
+from ansible_collections.cisco.meraki.plugins.module_utils.network.meraki.meraki import MerakiModule, meraki_argument_spec
+
+param_map = {'allowed_rules': 'allowedrules',
+ 'rule_id': 'ruleId',
+ 'rule_message': 'message',
+ 'mode': 'mode',
+ 'protected_networks': 'protectedNetworks',
+ 'use_default': 'useDefault',
+ 'included_cidr': 'includedCidr',
+ }
+
+
+def main():
+
+ # define the available arguments/parameters that a user can pass to
+ # the module
+
+ allowedrules_arg_spec = dict(rule_id=dict(type='str'),
+ rule_message=dict(type='str',
+ aliases=['message'],
+ deprecated_aliases=[dict(name='message', version='3.0.0', collection_name='cisco.meraki')]),
+ )
+
+ protected_nets_arg_spec = dict(use_default=dict(type='bool'),
+ included_cidr=dict(type='list', elements='str'),
+ excluded_cidr=dict(type='list', elements='str'),
+ )
+
+ argument_spec = meraki_argument_spec()
+ argument_spec.update(
+ net_id=dict(type='str'),
+ net_name=dict(type='str', aliases=['name', 'network']),
+ state=dict(type='str', choices=['absent', 'present', 'query'], default='present'),
+ allowed_rules=dict(type='list', default=None, elements='dict', options=allowedrules_arg_spec),
+ mode=dict(type='str', choices=['detection', 'disabled', 'prevention']),
+ ids_rulesets=dict(type='str', choices=['connectivity', 'balanced', 'security']),
+ protected_networks=dict(type='dict', default=None, options=protected_nets_arg_spec),
+ )
+
+ # the AnsibleModule object will be our abstraction working with Ansible
+ # this includes instantiation, a couple of common attr would be the
+ # args/params passed to the execution, as well as if the module
+ # supports check mode
+ module = AnsibleModule(argument_spec=argument_spec,
+ supports_check_mode=True,
+ )
+
+ meraki = MerakiModule(module, function='intrusion_prevention')
+ module.params['follow_redirects'] = 'all'
+ payload = None
+
+ query_org_urls = {'intrusion_prevention': '/organizations/{org_id}/appliance/security/intrusion'}
+ query_net_urls = {'intrusion_prevention': '/networks/{net_id}/appliance/security/intrusion'}
+ set_org_urls = {'intrusion_prevention': '/organizations/{org_id}/appliance/security/intrusion'}
+ set_net_urls = {'intrusion_prevention': '/networks/{net_id}/appliance/security/intrusion'}
+ meraki.url_catalog['query_org'] = query_org_urls
+ meraki.url_catalog['query_net'] = query_net_urls
+ meraki.url_catalog['set_org'] = set_org_urls
+ meraki.url_catalog['set_net'] = set_net_urls
+
+ if not meraki.params['org_name'] and not meraki.params['org_id']:
+ meraki.fail_json(msg='org_name or org_id parameters are required')
+ if meraki.params['net_name'] and meraki.params['net_id']:
+ meraki.fail_json(msg='net_name and net_id are mutually exclusive')
+ if meraki.params['net_name'] is None and meraki.params['net_id'] is None: # Organization param check
+ if meraki.params['state'] == 'present':
+ if meraki.params['allowed_rules'] is None:
+ meraki.fail_json(msg='allowed_rules is required when state is present and no network is specified.')
+ if meraki.params['net_name'] or meraki.params['net_id']: # Network param check
+ if meraki.params['state'] == 'present':
+ if meraki.params['protected_networks'] is not None:
+ if meraki.params['protected_networks']['use_default'] is False and meraki.params['protected_networks']['included_cidr'] is None:
+ meraki.fail_json(msg="included_cidr is required when use_default is False.")
+ if meraki.params['protected_networks']['use_default'] is False and meraki.params['protected_networks']['excluded_cidr'] is None:
+ meraki.fail_json(msg="excluded_cidr is required when use_default is False.")
+
+ org_id = meraki.params['org_id']
+ if not org_id:
+ org_id = meraki.get_org_id(meraki.params['org_name'])
+ net_id = meraki.params['net_id']
+ if net_id is None and meraki.params['net_name']:
+ nets = meraki.get_nets(org_id=org_id)
+ net_id = meraki.get_net_id(net_name=meraki.params['net_name'], data=nets)
+
+ # Assemble payload
+ if meraki.params['state'] == 'present':
+ if net_id is None: # Create payload for organization
+ rules = []
+ for rule in meraki.params['allowed_rules']:
+ rules.append({'ruleId': rule['rule_id'],
+ 'message': rule['rule_message'],
+ })
+ payload = {'allowedRules': rules}
+ else: # Create payload for network
+ payload = dict()
+ if meraki.params['mode']:
+ payload['mode'] = meraki.params['mode']
+ if meraki.params['ids_rulesets']:
+ payload['idsRulesets'] = meraki.params['ids_rulesets']
+ if meraki.params['protected_networks']:
+ payload['protectedNetworks'] = {}
+ if meraki.params['protected_networks']['use_default']:
+ payload['protectedNetworks'].update({'useDefault': meraki.params['protected_networks']['use_default']})
+ if meraki.params['protected_networks']['included_cidr']:
+ payload['protectedNetworks'].update({'includedCidr': meraki.params['protected_networks']['included_cidr']})
+ if meraki.params['protected_networks']['excluded_cidr']:
+ payload['protectedNetworks'].update({'excludedCidr': meraki.params['protected_networks']['excluded_cidr']})
+ elif meraki.params['state'] == 'absent':
+ if net_id is None: # Create payload for organization
+ payload = {'allowedRules': []}
+
+ if meraki.params['state'] == 'query':
+ if net_id is None: # Query settings for organization
+ path = meraki.construct_path('query_org', org_id=org_id)
+ data = meraki.request(path, method='GET')
+ if meraki.status == 200:
+ meraki.result['data'] = data
+ else: # Query settings for network
+ path = meraki.construct_path('query_net', net_id=net_id)
+ data = meraki.request(path, method='GET')
+ elif meraki.params['state'] == 'present':
+ path = meraki.construct_path('query_org', org_id=org_id)
+ original = meraki.request(path, method='GET')
+ if net_id is None: # Set configuration for organization
+ if meraki.is_update_required(original, payload, optional_ignore=['message']):
+ if meraki.module.check_mode is True:
+ original.update(payload)
+ meraki.result['data'] = original
+ meraki.result['changed'] = True
+ meraki.exit_json(**meraki.result)
+ path = meraki.construct_path('set_org', org_id=org_id)
+ data = meraki.request(path, method='PUT', payload=json.dumps(payload))
+ if meraki.status == 200:
+ meraki.result['data'] = data
+ meraki.result['changed'] = True
+ else:
+ meraki.result['data'] = original
+ meraki.result['changed'] = False
+ else: # Set configuration for network
+ path = meraki.construct_path('query_net', net_id=net_id)
+ original = meraki.request(path, method='GET')
+ if meraki.is_update_required(original, payload):
+ if meraki.module.check_mode is True:
+ payload.update(original)
+ meraki.result['data'] = payload
+ meraki.result['changed'] = True
+ meraki.exit_json(**meraki.result)
+ path = meraki.construct_path('set_net', net_id=net_id)
+ data = meraki.request(path, method='PUT', payload=json.dumps(payload))
+ if meraki.status == 200:
+ meraki.result['data'] = data
+ meraki.result['changed'] = True
+ else:
+ meraki.result['data'] = original
+ meraki.result['changed'] = False
+ elif meraki.params['state'] == 'absent':
+ if net_id is None:
+ path = meraki.construct_path('query_org', org_id=org_id)
+ original = meraki.request(path, method='GET')
+ if meraki.is_update_required(original, payload):
+ if meraki.module.check_mode is True:
+ payload.update(original)
+ meraki.result['data'] = payload
+ meraki.result['changed'] = True
+ meraki.exit_json(**meraki.result)
+ path = meraki.construct_path('set_org', org_id=org_id)
+ data = meraki.request(path, method='PUT', payload=json.dumps(payload))
+ if meraki.status == 200:
+ meraki.result['data'] = data
+ meraki.result['changed'] = True
+ else:
+ meraki.result['data'] = original
+ meraki.result['changed'] = False
+
+ # in the event of a successful module execution, you will want to
+ # simple AnsibleModule.exit_json(), passing the key/value results
+ meraki.exit_json(**meraki.result)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/ansible_collections/cisco/meraki/plugins/modules/meraki_mx_l2_interface.py b/ansible_collections/cisco/meraki/plugins/modules/meraki_mx_l2_interface.py
new file mode 100644
index 000000000..48e48642b
--- /dev/null
+++ b/ansible_collections/cisco/meraki/plugins/modules/meraki_mx_l2_interface.py
@@ -0,0 +1,272 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Copyright: (c) 2020, Kevin Breit (@kbreit) <kevin.breit@kevinbreit.net>
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+ANSIBLE_METADATA = {
+ 'metadata_version': '1.1',
+ 'status': ['preview'],
+ 'supported_by': 'community'
+}
+
+DOCUMENTATION = r'''
+---
+module: meraki_mx_l2_interface
+short_description: Configure MX layer 2 interfaces
+version_added: "2.1.0"
+description:
+- Allows for management and visibility of Merkai MX layer 2 ports.
+
+options:
+ state:
+ description:
+ - Modify or query an port.
+ choices: [present, query]
+ default: present
+ type: str
+ net_name:
+ description:
+ - Name of a network.
+ aliases: [name, network]
+ type: str
+ net_id:
+ description:
+ - ID number of a network.
+ type: str
+ org_id:
+ description:
+ - ID of organization associated to a network.
+ type: str
+ number:
+ description:
+ - ID number of MX port.
+ aliases: [port, port_id]
+ type: int
+ vlan:
+ description:
+ - Native VLAN when the port is in Trunk mode.
+ - Access VLAN when the port is in Access mode.
+ type: int
+ access_policy:
+ description:
+ - The name of the policy. Only applicable to access ports.
+ choices: [open, 8021x-radius, mac-radius, hybris-radius]
+ type: str
+ allowed_vlans:
+ description:
+ - Comma-delimited list of the VLAN ID's allowed on the port, or 'all' to permit all VLAN's on the port.
+ type: str
+ port_type:
+ description:
+ - Type of port.
+ choices: [access, trunk]
+ type: str
+ drop_untagged_traffic:
+ description:
+ - Trunk port can Drop all Untagged traffic. When true, no VLAN is required.
+ - Access ports cannot have dropUntaggedTraffic set to true.
+ type: bool
+ enabled:
+ description:
+ - Enabled state of port.
+ type: bool
+
+author:
+ - Kevin Breit (@kbreit)
+extends_documentation_fragment: cisco.meraki.meraki
+'''
+
+EXAMPLES = r'''
+- name: Query layer 2 interface settings
+ meraki_mx_l2_interface:
+ auth_key: abc123
+ org_name: YourOrg
+ net_name: YourNet
+ state: query
+ delegate_to: localhost
+
+- name: Query a single layer 2 interface settings
+ meraki_mx_l2_interface:
+ auth_key: abc123
+ org_name: YourOrg
+ net_name: YourNet
+ state: query
+ number: 2
+ delegate_to: localhost
+
+- name: Update interface configuration
+ meraki_mx_l2_interface:
+ auth_key: abc123
+ org_name: YourOrg
+ net_name: YourNet
+ state: present
+ number: 2
+ port_type: access
+ vlan: 10
+ delegate_to: localhost
+'''
+
+RETURN = r'''
+data:
+ description: Information about the created or manipulated object.
+ returned: success
+ type: complex
+ contains:
+ number:
+ description:
+ - ID number of MX port.
+ type: int
+ returned: success
+ sample: 4
+ vlan:
+ description:
+ - Native VLAN when the port is in Trunk mode.
+ - Access VLAN when the port is in Access mode.
+ type: int
+ returned: success
+ sample: 1
+ access_policy:
+ description:
+ - The name of the policy. Only applicable to access ports.
+ type: str
+ returned: success
+ sample: guestUsers
+ allowed_vlans:
+ description:
+ - Comma-delimited list of the VLAN ID's allowed on the port, or 'all' to permit all VLAN's on the port.
+ type: str
+ returned: success
+ sample: 1,5,10
+ type:
+ description:
+ - Type of port.
+ type: str
+ returned: success
+ sample: access
+ drop_untagged_traffic:
+ description:
+ - Trunk port can Drop all Untagged traffic. When true, no VLAN is required.
+ - Access ports cannot have dropUntaggedTraffic set to true.
+ type: bool
+ returned: success
+ sample: true
+ enabled:
+ description:
+ - Enabled state of port.
+ type: bool
+ returned: success
+ sample: true
+'''
+
+from ansible.module_utils.basic import AnsibleModule, json
+from ansible_collections.cisco.meraki.plugins.module_utils.network.meraki.meraki import MerakiModule, meraki_argument_spec
+
+
+def construct_payload(meraki):
+ payload = {}
+ if meraki.params['vlan'] is not None:
+ payload['vlan'] = meraki.params['vlan']
+ if meraki.params['access_policy'] is not None:
+ payload['accessPolicy'] = meraki.params['access_policy']
+ if meraki.params['allowed_vlans'] is not None:
+ payload['allowedVlans'] = meraki.params['allowed_vlans']
+ if meraki.params['port_type'] is not None:
+ payload['type'] = meraki.params['port_type']
+ if meraki.params['drop_untagged_traffic'] is not None:
+ payload['dropUntaggedTraffic'] = meraki.params['drop_untagged_traffic']
+ if meraki.params['enabled'] is not None:
+ payload['enabled'] = meraki.params['enabled']
+ return payload
+
+
+def main():
+
+ # define the available arguments/parameters that a user can pass to
+ # the module
+
+ argument_spec = meraki_argument_spec()
+ argument_spec.update(
+ net_id=dict(type='str'),
+ net_name=dict(type='str', aliases=['name', 'network']),
+ state=dict(type='str', choices=['present', 'query'], default='present'),
+ number=dict(type='int', aliases=['port', 'port_id']),
+ vlan=dict(type='int'),
+ access_policy=dict(type='str', choices=['open', '8021x-radius', 'mac-radius', 'hybris-radius']),
+ allowed_vlans=dict(type='str'),
+ port_type=dict(type='str', choices=['access', 'trunk']),
+ drop_untagged_traffic=dict(type='bool'),
+ enabled=dict(type='bool'),
+ )
+
+ # the AnsibleModule object will be our abstraction working with Ansible
+ # this includes instantiation, a couple of common attr would be the
+ # args/params passed to the execution, as well as if the module
+ # supports check mode
+ module = AnsibleModule(argument_spec=argument_spec,
+ supports_check_mode=True,
+ )
+
+ meraki = MerakiModule(module, function='mx_l2_interface')
+ module.params['follow_redirects'] = 'all'
+
+ get_all_urls = {'mx_l2_interface': '/networks/{net_id}/appliance/ports'}
+ get_one_urls = {'mx_l2_interface': '/networks/{net_id}/appliance/ports/{port_id}'}
+ update_urls = {'mx_l2_interface': '/networks/{net_id}/appliance/ports/{port_id}'}
+ meraki.url_catalog['query_all'] = get_all_urls
+ meraki.url_catalog['query_one'] = get_one_urls
+ meraki.url_catalog['update'] = update_urls
+
+ if meraki.params['net_name'] and meraki.params['net_id']:
+ meraki.fail_json(msg='net_name and net_id are mutually exclusive.')
+ if meraki.params['port_type'] == 'access':
+ if meraki.params['allowed_vlans'] is not None:
+ meraki.meraki.fail_json(msg='allowed_vlans is mutually exclusive with port type trunk.')
+
+ org_id = meraki.params['org_id']
+ if not org_id:
+ org_id = meraki.get_org_id(meraki.params['org_name'])
+ net_id = meraki.params['net_id']
+ if net_id is None:
+ nets = meraki.get_nets(org_id=org_id)
+ net_id = meraki.get_net_id(org_id, meraki.params['net_name'], data=nets)
+
+ if meraki.params['state'] == 'query':
+ if meraki.params['number'] is not None:
+ path = meraki.construct_path('query_one', net_id=net_id, custom={'port_id': meraki.params['number']})
+ else:
+ path = meraki.construct_path('query_all', net_id=net_id)
+ response = meraki.request(path, method='GET')
+ meraki.result['data'] = response
+ meraki.exit_json(**meraki.result)
+ elif meraki.params['state'] == 'present':
+ path = meraki.construct_path('query_one', net_id=net_id, custom={'port_id': meraki.params['number']})
+ original = meraki.request(path, method='GET')
+ payload = construct_payload(meraki)
+ if meraki.is_update_required(original, payload):
+ meraki.generate_diff(original, payload)
+ if meraki.check_mode is True:
+ original.update(payload)
+ meraki.result['data'] = original
+ meraki.result['changed'] = True
+ meraki.exit_json(**meraki.result)
+ path = meraki.construct_path('update', net_id=net_id, custom={'port_id': meraki.params['number']})
+ response = meraki.request(path, method='PUT', payload=json.dumps(payload))
+ if meraki.status == 200:
+ meraki.result['data'] = response
+ meraki.result['changed'] = True
+ meraki.exit_json(**meraki.result)
+ else:
+ meraki.result['data'] = original
+ meraki.exit_json(**meraki.result)
+
+ # in the event of a successful module execution, you will want to
+ # simple AnsibleModule.exit_json(), passing the key/value results
+ meraki.exit_json(**meraki.result)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/ansible_collections/cisco/meraki/plugins/modules/meraki_mx_l3_firewall.py b/ansible_collections/cisco/meraki/plugins/modules/meraki_mx_l3_firewall.py
new file mode 100644
index 000000000..f8fa3f919
--- /dev/null
+++ b/ansible_collections/cisco/meraki/plugins/modules/meraki_mx_l3_firewall.py
@@ -0,0 +1,377 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Copyright: (c) 2018, Kevin Breit (@kbreit) <kevin.breit@kevinbreit.net>
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+ANSIBLE_METADATA = {
+ 'metadata_version': '1.1',
+ 'status': ['preview'],
+ 'supported_by': 'community'
+}
+
+DOCUMENTATION = r'''
+---
+module: meraki_mx_l3_firewall
+short_description: Manage MX appliance layer 3 firewalls in the Meraki cloud
+description:
+- Allows for creation, management, and visibility into layer 3 firewalls implemented on Meraki MX firewalls.
+notes:
+- Module assumes a complete list of firewall rules are passed as a parameter.
+- If there is interest in this module allowing manipulation of a single firewall rule, please submit an issue against this module.
+options:
+ state:
+ description:
+ - Create or modify an organization.
+ choices: ['present', 'query']
+ default: present
+ type: str
+ net_name:
+ description:
+ - Name of network which MX firewall is in.
+ type: str
+ net_id:
+ description:
+ - ID of network which MX firewall is in.
+ type: str
+ rules:
+ description:
+ - List of firewall rules.
+ type: list
+ elements: dict
+ suboptions:
+ policy:
+ description:
+ - Policy to apply if rule is hit.
+ choices: [allow, deny]
+ type: str
+ protocol:
+ description:
+ - Protocol to match against.
+ choices: [any, icmp, tcp, udp]
+ type: str
+ dest_port:
+ description:
+ - Comma separated list of destination port numbers to match against.
+ - C(Any) must be capitalized.
+ type: str
+ dest_cidr:
+ description:
+ - Comma separated list of CIDR notation destination networks.
+ - C(Any) must be capitalized.
+ type: str
+ src_port:
+ description:
+ - Comma separated list of source port numbers to match against.
+ - C(Any) must be capitalized.
+ type: str
+ src_cidr:
+ description:
+ - Comma separated list of CIDR notation source networks.
+ - C(Any) must be capitalized.
+ type: str
+ comment:
+ description:
+ - Optional comment to describe the firewall rule.
+ type: str
+ syslog_enabled:
+ description:
+ - Whether to log hints against the firewall rule.
+ - Only applicable if a syslog server is specified against the network.
+ type: bool
+ default: False
+ syslog_default_rule:
+ description:
+ - Whether to log hits against the default firewall rule.
+ - Only applicable if a syslog server is specified against the network.
+ - This is not shown in response from Meraki. Instead, refer to the C(syslog_enabled) value in the default rule.
+ type: bool
+author:
+- Kevin Breit (@kbreit)
+extends_documentation_fragment: cisco.meraki.meraki
+'''
+
+EXAMPLES = r'''
+- name: Query firewall rules
+ meraki_mx_l3_firewall:
+ auth_key: abc123
+ org_name: YourOrg
+ net_name: YourNet
+ state: query
+ delegate_to: localhost
+
+- name: Set two firewall rules
+ meraki_mx_l3_firewall:
+ auth_key: abc123
+ org_name: YourOrg
+ net_name: YourNet
+ state: present
+ rules:
+ - comment: Block traffic to server
+ src_cidr: 192.0.1.0/24
+ src_port: any
+ dest_cidr: 192.0.2.2/32
+ dest_port: any
+ protocol: any
+ policy: deny
+ - comment: Allow traffic to group of servers
+ src_cidr: 192.0.1.0/24
+ src_port: any
+ dest_cidr: 192.0.2.0/24
+ dest_port: any
+ protocol: any
+ policy: allow
+ delegate_to: localhost
+
+- name: Set one firewall rule and enable logging of the default rule
+ meraki_mx_l3_firewall:
+ auth_key: abc123
+ org_name: YourOrg
+ net_name: YourNet
+ state: present
+ rules:
+ - comment: Block traffic to server
+ src_cidr: 192.0.1.0/24
+ src_port: any
+ dest_cidr: 192.0.2.2/32
+ dest_port: any
+ protocol: any
+ policy: deny
+ syslog_default_rule: yes
+ delegate_to: localhost
+'''
+
+RETURN = r'''
+data:
+ description: Firewall rules associated to network.
+ returned: success
+ type: complex
+ contains:
+ rules:
+ description: List of firewall rules.
+ returned: success
+ type: complex
+ contains:
+ comment:
+ description: Comment to describe the firewall rule.
+ returned: always
+ type: str
+ sample: Block traffic to server
+ src_cidr:
+ description: Comma separated list of CIDR notation source networks.
+ returned: always
+ type: str
+ sample: 192.0.1.1/32,192.0.1.2/32
+ src_port:
+ description: Comma separated list of source ports.
+ returned: always
+ type: str
+ sample: 80,443
+ dest_cidr:
+ description: Comma separated list of CIDR notation destination networks.
+ returned: always
+ type: str
+ sample: 192.0.1.1/32,192.0.1.2/32
+ dest_port:
+ description: Comma separated list of destination ports.
+ returned: always
+ type: str
+ sample: 80,443
+ protocol:
+ description: Network protocol for which to match against.
+ returned: always
+ type: str
+ sample: tcp
+ policy:
+ description: Action to take when rule is matched.
+ returned: always
+ type: str
+ syslog_enabled:
+ description: Whether to log to syslog when rule is matched.
+ returned: always
+ type: bool
+ sample: true
+'''
+
+from ansible.module_utils.basic import AnsibleModule, json
+from ansible_collections.cisco.meraki.plugins.module_utils.network.meraki.meraki import MerakiModule, meraki_argument_spec
+
+
+def assemble_payload(meraki):
+ params_map = {'policy': 'policy',
+ 'protocol': 'protocol',
+ 'dest_port': 'destPort',
+ 'dest_cidr': 'destCidr',
+ 'src_port': 'srcPort',
+ 'src_cidr': 'srcCidr',
+ 'syslog_enabled': 'syslogEnabled',
+ 'comment': 'comment',
+ }
+ rules = []
+ for rule in meraki.params['rules']:
+ proposed_rule = dict()
+ for k, v in rule.items():
+ proposed_rule[params_map[k]] = v
+ rules.append(proposed_rule)
+ payload = {'rules': rules}
+ return payload
+
+
+def get_rules(meraki, net_id):
+ path = meraki.construct_path('get_all', net_id=net_id)
+ response = meraki.request(path, method='GET')
+ if meraki.status == 200:
+ return normalize_rule_case(response)
+
+
+def normalize_rule_case(rules):
+ excluded = ['comment', 'syslogEnabled']
+ try:
+ for r in rules['rules']:
+ for k in r:
+ if k not in excluded:
+ r[k] = r[k].lower()
+ except KeyError:
+ return rules
+ return rules
+
+
+def normalize_case(rule):
+ any = ['any', 'Any', 'ANY']
+ if 'srcPort' in rule:
+ if rule['srcPort'] in any:
+ rule['srcPort'] = 'Any'
+ if 'srcCidr' in rule:
+ if rule['srcCidr'] in any:
+ rule['srcCidr'] = 'Any'
+ if 'destPort' in rule:
+ if rule['destPort'] in any:
+ rule['destPort'] = 'Any'
+ if 'destCidr' in rule:
+ if rule['destCidr'] in any:
+ rule['destCidr'] = 'Any'
+
+
+def main():
+ # define the available arguments/parameters that a user can pass to
+ # the module
+
+ fw_rules = dict(policy=dict(type='str', choices=['allow', 'deny']),
+ protocol=dict(type='str', choices=['tcp', 'udp', 'icmp', 'any']),
+ dest_port=dict(type='str'),
+ dest_cidr=dict(type='str'),
+ src_port=dict(type='str'),
+ src_cidr=dict(type='str'),
+ comment=dict(type='str'),
+ syslog_enabled=dict(type='bool', default=False),
+ )
+
+ argument_spec = meraki_argument_spec()
+ argument_spec.update(state=dict(type='str', choices=['present', 'query'], default='present'),
+ net_name=dict(type='str'),
+ net_id=dict(type='str'),
+ rules=dict(type='list', default=None, elements='dict', options=fw_rules),
+ syslog_default_rule=dict(type='bool'),
+ )
+
+ # the AnsibleModule object will be our abstraction working with Ansible
+ # this includes instantiation, a couple of common attr would be the
+ # args/params passed to the execution, as well as if the module
+ # supports check mode
+ module = AnsibleModule(argument_spec=argument_spec,
+ supports_check_mode=True,
+ )
+ meraki = MerakiModule(module, function='mx_l3_firewall')
+
+ meraki.params['follow_redirects'] = 'all'
+
+ query_urls = {'mx_l3_firewall': '/networks/{net_id}/appliance/firewall/l3FirewallRules/'}
+ update_urls = {'mx_l3_firewall': '/networks/{net_id}/appliance/firewall/l3FirewallRules/'}
+
+ meraki.url_catalog['get_all'].update(query_urls)
+ meraki.url_catalog['update'] = update_urls
+
+ payload = None
+
+ # execute checks for argument completeness
+
+ # manipulate or modify the state as needed (this is going to be the
+ # part where your module will do what it needs to do)
+ org_id = meraki.params['org_id']
+ if org_id is None:
+ orgs = meraki.get_orgs()
+ for org in orgs:
+ if org['name'] == meraki.params['org_name']:
+ org_id = org['id']
+ net_id = meraki.params['net_id']
+ if net_id is None:
+ net_id = meraki.get_net_id(net_name=meraki.params['net_name'],
+ data=meraki.get_nets(org_id=org_id))
+
+ if meraki.params['state'] == 'query':
+ meraki.result['data'] = get_rules(meraki, net_id)
+ elif meraki.params['state'] == 'present':
+ rules = get_rules(meraki, net_id)
+ path = meraki.construct_path('get_all', net_id=net_id)
+ if meraki.params['rules'] is not None:
+ payload = assemble_payload(meraki)
+ else:
+ payload = dict()
+ update = False
+ if meraki.params['syslog_default_rule'] is not None:
+ payload['syslogDefaultRule'] = meraki.params['syslog_default_rule']
+ try:
+ if meraki.params['rules'] is not None:
+ if len(rules['rules']) - 1 != len(payload['rules']): # Quick and simple check to avoid more processing
+ update = True
+ if meraki.params['syslog_default_rule'] is not None:
+ if rules['rules'][len(rules['rules']) - 1]['syslogEnabled'] != meraki.params['syslog_default_rule']:
+ update = True
+ if update is False:
+ default_rule = rules['rules'][len(rules['rules']) - 1].copy()
+ del rules['rules'][len(rules['rules']) - 1] # Remove default rule for comparison
+ if len(rules['rules']) - 1 == 0: # There is only a single rule
+ normalize_case(rules['rules'][0])
+ normalize_case(payload['rules'][0])
+ if meraki.is_update_required(rules['rules'][0], payload['rules'][0]) is True:
+ update = True
+ else:
+ for r in range(len(rules['rules']) - 1):
+ normalize_case(rules[r])
+ normalize_case(payload['rules'][r])
+ if meraki.is_update_required(rules[r], payload['rules'][r]) is True:
+ update = True
+ rules['rules'].append(default_rule)
+ except KeyError:
+ pass
+ if update is True:
+ if meraki.check_mode is True:
+ if meraki.params['rules'] is not None:
+ data = payload['rules']
+ data.append(rules['rules'][len(rules['rules']) - 1]) # Append the default rule
+ if meraki.params['syslog_default_rule'] is not None:
+ data[len(payload) - 1]['syslog_enabled'] = meraki.params['syslog_default_rule']
+ else:
+ if meraki.params['syslog_default_rule'] is not None:
+ data = rules
+ data['rules'][len(data['rules']) - 1]['syslogEnabled'] = meraki.params['syslog_default_rule']
+ meraki.result['data'] = data
+ meraki.result['changed'] = True
+ meraki.exit_json(**meraki.result)
+ response = meraki.request(path, method='PUT', payload=json.dumps(payload))
+ if meraki.status == 200:
+ meraki.result['data'] = response
+ meraki.result['changed'] = True
+ else:
+ meraki.result['data'] = rules
+
+ # in the event of a successful module execution, you will want to
+ # simple AnsibleModule.exit_json(), passing the key/value results
+ meraki.exit_json(**meraki.result)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/ansible_collections/cisco/meraki/plugins/modules/meraki_mx_l7_firewall.py b/ansible_collections/cisco/meraki/plugins/modules/meraki_mx_l7_firewall.py
new file mode 100644
index 000000000..efe4bda88
--- /dev/null
+++ b/ansible_collections/cisco/meraki/plugins/modules/meraki_mx_l7_firewall.py
@@ -0,0 +1,475 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Copyright: (c) 2019, Kevin Breit (@kbreit) <kevin.breit@kevinbreit.net>
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+ANSIBLE_METADATA = {
+ 'metadata_version': '1.1',
+ 'status': ['preview'],
+ 'supported_by': 'community'
+}
+
+DOCUMENTATION = r'''
+---
+module: meraki_mx_l7_firewall
+short_description: Manage MX appliance layer 7 firewalls in the Meraki cloud
+description:
+- Allows for creation, management, and visibility into layer 7 firewalls implemented on Meraki MX firewalls.
+notes:
+- Module assumes a complete list of firewall rules are passed as a parameter.
+- If there is interest in this module allowing manipulation of a single firewall rule, please submit an issue against this module.
+options:
+ state:
+ description:
+ - Query or modify a firewall rule.
+ choices: ['present', 'query']
+ default: present
+ type: str
+ net_name:
+ description:
+ - Name of network which MX firewall is in.
+ type: str
+ net_id:
+ description:
+ - ID of network which MX firewall is in.
+ type: str
+ rules:
+ description:
+ - List of layer 7 firewall rules.
+ type: list
+ elements: dict
+ suboptions:
+ policy:
+ description:
+ - Policy to apply if rule is hit.
+ choices: [deny]
+ default: deny
+ type: str
+ type:
+ description:
+ - Type of policy to apply.
+ choices: [application,
+ application_category,
+ blocked_countries,
+ host,
+ ip_range,
+ port,
+ allowed_countries]
+ type: str
+ application:
+ description:
+ - Application to filter.
+ type: dict
+ suboptions:
+ name:
+ description:
+ - Name of application to filter as defined by Meraki.
+ type: str
+ id:
+ description:
+ - URI of application as defined by Meraki.
+ type: str
+ host:
+ description:
+ - FQDN of host to filter.
+ type: str
+ ip_range:
+ description:
+ - CIDR notation range of IP addresses to apply rule to.
+ - Port can be appended to range with a C(":").
+ type: str
+ port:
+ description:
+ - TCP or UDP based port to filter.
+ type: str
+ countries:
+ description:
+ - List of countries to whitelist or blacklist.
+ - The countries follow the two-letter ISO 3166-1 alpha-2 format.
+ type: list
+ elements: str
+ categories:
+ description:
+ - When C(True), specifies that applications and application categories should be queried instead of firewall rules.
+ type: bool
+author:
+- Kevin Breit (@kbreit)
+extends_documentation_fragment: cisco.meraki.meraki
+'''
+
+EXAMPLES = r'''
+- name: Query firewall rules
+ meraki_mx_l7_firewall:
+ auth_key: abc123
+ org_name: YourOrg
+ net_name: YourNet
+ state: query
+ delegate_to: localhost
+
+- name: Query applications and application categories
+ meraki_mx_l7_firewall:
+ auth_key: abc123
+ org_name: YourOrg
+ net_name: YourNet
+ categories: yes
+ state: query
+ delegate_to: localhost
+
+- name: Set firewall rules
+ meraki_mx_l7_firewall:
+ auth_key: abc123
+ org_name: YourOrg
+ net_name: YourNet
+ state: present
+ rules:
+ - type: allowed_countries
+ countries:
+ - US
+ - FR
+ - type: blocked_countries
+ countries:
+ - CN
+ - policy: deny
+ type: port
+ port: 8080
+ - type: port
+ port: 1234
+ - type: host
+ host: asdf.com
+ - type: application
+ application:
+ id: meraki:layer7/application/205
+ - type: application_category
+ application:
+ id: meraki:layer7/category/24
+ delegate_to: localhost
+'''
+
+RETURN = r'''
+data:
+ description: Firewall rules associated to network.
+ returned: success
+ type: complex
+ contains:
+ rules:
+ description: Ordered list of firewall rules.
+ returned: success, when not querying applications
+ type: list
+ contains:
+ policy:
+ description: Action to apply when rule is hit.
+ returned: success
+ type: str
+ sample: deny
+ type:
+ description: Type of rule category.
+ returned: success
+ type: str
+ sample: applications
+ applications:
+ description: List of applications within a category.
+ type: list
+ contains:
+ id:
+ description: URI of application.
+ returned: success
+ type: str
+ sample: Gmail
+ name:
+ description: Descriptive name of application.
+ returned: success
+ type: str
+ sample: meraki:layer7/application/4
+ applicationCategory:
+ description: List of application categories within a category.
+ type: list
+ contains:
+ id:
+ description: URI of application.
+ returned: success
+ type: str
+ sample: Gmail
+ name:
+ description: Descriptive name of application.
+ returned: success
+ type: str
+ sample: meraki:layer7/application/4
+ port:
+ description: Port number in rule.
+ returned: success
+ type: str
+ sample: 23
+ ipRange:
+ description: Range of IP addresses in rule.
+ returned: success
+ type: str
+ sample: 1.1.1.0/23
+ allowedCountries:
+ description: Countries to be allowed.
+ returned: success
+ type: str
+ sample: CA
+ blockedCountries:
+ description: Countries to be blacklisted.
+ returned: success
+ type: str
+ sample: RU
+ application_categories:
+ description: List of application categories and applications.
+ type: list
+ returned: success, when querying applications
+ contains:
+ applications:
+ description: List of applications within a category.
+ type: list
+ contains:
+ id:
+ description: URI of application.
+ returned: success
+ type: str
+ sample: Gmail
+ name:
+ description: Descriptive name of application.
+ returned: success
+ type: str
+ sample: meraki:layer7/application/4
+ id:
+ description: URI of application category.
+ returned: success
+ type: str
+ sample: Email
+ name:
+ description: Descriptive name of application category.
+ returned: success
+ type: str
+ sample: layer7/category/1
+'''
+
+import copy
+from ansible.module_utils.basic import AnsibleModule, json
+from ansible_collections.cisco.meraki.plugins.module_utils.network.meraki.meraki import MerakiModule, meraki_argument_spec
+
+
+def get_applications(meraki, net_id):
+ path = meraki.construct_path('get_categories', net_id=net_id)
+ return meraki.request(path, method='GET')
+
+
+def lookup_application(meraki, net_id, application):
+ response = get_applications(meraki, net_id)
+ for category in response['applicationCategories']:
+ if category['name'].lower() == application.lower():
+ return category['id']
+ for app in category['applications']:
+ if app['name'].lower() == application.lower():
+ return app['id']
+ meraki.fail_json(msg="No application or category named {0} found".format(application))
+
+
+def assemble_payload(meraki, net_id, rule):
+ if rule['type'] == 'application':
+ new_rule = {'policy': rule['policy'],
+ 'type': 'application',
+ }
+ if rule['application']['id']:
+ new_rule['value'] = {'id': rule['application']['id']}
+ elif rule['application']['name']:
+ new_rule['value'] = {'id': lookup_application(meraki, net_id, rule['application']['name'])}
+ elif rule['type'] == 'application_category':
+ new_rule = {'policy': rule['policy'],
+ 'type': 'applicationCategory',
+ }
+ if rule['application']['id']:
+ new_rule['value'] = {'id': rule['application']['id']}
+ elif rule['application']['name']:
+ new_rule['value'] = {'id': lookup_application(meraki, net_id, rule['application']['name'])}
+ elif rule['type'] == 'ip_range':
+ new_rule = {'policy': rule['policy'],
+ 'type': 'ipRange',
+ 'value': rule['ip_range']}
+ elif rule['type'] == 'host':
+ new_rule = {'policy': rule['policy'],
+ 'type': rule['type'],
+ 'value': rule['host']}
+ elif rule['type'] == 'port':
+ new_rule = {'policy': rule['policy'],
+ 'type': rule['type'],
+ 'value': rule['port']}
+ elif rule['type'] == 'blocked_countries':
+ new_rule = {'policy': rule['policy'],
+ 'type': 'blockedCountries',
+ 'value': rule['countries']
+ }
+ elif rule['type'] == 'allowed_countries':
+ new_rule = {'policy': rule['policy'],
+ 'type': 'allowedCountries',
+ 'value': rule['countries']
+ }
+ return new_rule
+
+
+def restructure_response(rules):
+ for rule in rules['rules']:
+ type = rule['type']
+ rule[type] = copy.deepcopy(rule['value'])
+ del rule['value']
+ return rules
+
+
+def get_rules(meraki, net_id):
+ path = meraki.construct_path('get_all', net_id=net_id)
+ response = meraki.request(path, method='GET')
+ if meraki.status == 200:
+ return response
+
+
+def main():
+ # define the available arguments/parameters that a user can pass to
+ # the module
+
+ application_arg_spec = dict(id=dict(type='str'),
+ name=dict(type='str'),
+ )
+
+ rule_arg_spec = dict(policy=dict(type='str', choices=['deny'], default='deny'),
+ type=dict(type='str', choices=['application',
+ 'application_category',
+ 'blocked_countries',
+ 'host',
+ 'ip_range',
+ 'port',
+ 'allowed_countries']),
+ ip_range=dict(type='str'),
+ application=dict(type='dict', default=None, options=application_arg_spec),
+ host=dict(type='str'),
+ port=dict(type='str'),
+ countries=dict(type='list', elements='str'),
+ )
+
+ argument_spec = meraki_argument_spec()
+ argument_spec.update(state=dict(type='str', choices=['present', 'query'], default='present'),
+ net_name=dict(type='str'),
+ net_id=dict(type='str'),
+ rules=dict(type='list', default=None, elements='dict', options=rule_arg_spec),
+ categories=dict(type='bool'),
+ )
+
+ # the AnsibleModule object will be our abstraction working with Ansible
+ # this includes instantiation, a couple of common attr would be the
+ # args/params passed to the execution, as well as if the module
+ # supports check mode
+ module = AnsibleModule(argument_spec=argument_spec,
+ supports_check_mode=True,
+ )
+ meraki = MerakiModule(module, function='mx_l7_firewall')
+
+ # check for argument completeness
+ if meraki.params['rules']:
+ for rule in meraki.params['rules']:
+ if rule['type'] == 'application' and rule['application'] is None:
+ meraki.fail_json(msg="application argument is required when type is application.")
+ elif rule['type'] == 'application_category' and rule['application'] is None:
+ meraki.fail_json(msg="application argument is required when type is application_category.")
+ elif rule['type'] == 'blocked_countries' and rule['countries'] is None:
+ meraki.fail_json(msg="countries argument is required when type is blocked_countries.")
+ elif rule['type'] == 'host' and rule['host'] is None:
+ meraki.fail_json(msg="host argument is required when type is host.")
+ elif rule['type'] == 'port' and rule['port'] is None:
+ meraki.fail_json(msg="port argument is required when type is port.")
+ elif rule['type'] == 'allowed_countries' and rule['countries'] is None:
+ meraki.fail_json(msg="countries argument is required when type is allowed_countries.")
+
+ meraki.params['follow_redirects'] = 'all'
+
+ query_urls = {'mx_l7_firewall': '/networks/{net_id}/appliance/firewall/l7FirewallRules/'}
+ query_category_urls = {'mx_l7_firewall': '/networks/{net_id}/appliance/firewall/l7FirewallRules/applicationCategories'}
+ update_urls = {'mx_l7_firewall': '/networks/{net_id}/appliance/firewall/l7FirewallRules/'}
+
+ meraki.url_catalog['get_all'].update(query_urls)
+ meraki.url_catalog['get_categories'] = (query_category_urls)
+ meraki.url_catalog['update'] = update_urls
+
+ payload = None
+
+ # manipulate or modify the state as needed (this is going to be the
+ # part where your module will do what it needs to do)
+ org_id = meraki.params['org_id']
+ orgs = None
+ if org_id is None:
+ orgs = meraki.get_orgs()
+ for org in orgs:
+ if org['name'] == meraki.params['org_name']:
+ org_id = org['id']
+ net_id = meraki.params['net_id']
+ if net_id is None:
+ if orgs is None:
+ orgs = meraki.get_orgs()
+ net_id = meraki.get_net_id(net_name=meraki.params['net_name'],
+ data=meraki.get_nets(org_id=org_id))
+
+ if meraki.params['state'] == 'query':
+ if meraki.params['categories'] is True: # Output only applications
+ meraki.result['data'] = get_applications(meraki, net_id)
+ else:
+ meraki.result['data'] = restructure_response(get_rules(meraki, net_id))
+ elif meraki.params['state'] == 'present':
+ rules = get_rules(meraki, net_id)
+ path = meraki.construct_path('get_all', net_id=net_id)
+
+ # Detect if no rules are given, special case
+ if len(meraki.params['rules']) == 0:
+ # Conditionally wrap parameters in rules makes it comparable
+ if isinstance(meraki.params['rules'], list):
+ param_rules = {'rules': meraki.params['rules']}
+ else:
+ param_rules = meraki.params['rules']
+ if meraki.is_update_required(rules, param_rules):
+ if meraki.module.check_mode is True:
+ meraki.result['data'] = meraki.params['rules']
+ meraki.result['changed'] = True
+ meraki.exit_json(**meraki.result)
+ payload = {'rules': []}
+ response = meraki.request(path, method='PUT', payload=json.dumps(payload))
+ meraki.result['data'] = response
+ meraki.result['changed'] = True
+ meraki.exit_json(**meraki.result)
+ else:
+ meraki.result['data'] = param_rules
+ meraki.exit_json(**meraki.result)
+ if meraki.params['rules']:
+ payload = {'rules': []}
+ for rule in meraki.params['rules']:
+ payload['rules'].append(assemble_payload(meraki, net_id, rule))
+ else:
+ payload = dict()
+ if meraki.is_update_required(rules, payload, force_include='id'):
+ if meraki.module.check_mode is True:
+ response = restructure_response(payload)
+ meraki.generate_diff(restructure_response(rules), response)
+ meraki.result['data'] = response
+ meraki.result['changed'] = True
+ meraki.exit_json(**meraki.result)
+ response = meraki.request(path, method='PUT', payload=json.dumps(payload))
+ response = restructure_response(response)
+ if meraki.status == 200:
+ meraki.generate_diff(restructure_response(rules), response)
+ meraki.result['data'] = response
+ meraki.result['changed'] = True
+ else:
+ if meraki.module.check_mode is True:
+ meraki.result['data'] = rules
+ meraki.result['changed'] = False
+ meraki.exit_json(**meraki.result)
+ meraki.result['data'] = payload
+
+ # in the event of a successful module execution, you will want to
+ # simple AnsibleModule.exit_json(), passing the key/value results
+ meraki.exit_json(**meraki.result)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/ansible_collections/cisco/meraki/plugins/modules/meraki_mx_malware.py b/ansible_collections/cisco/meraki/plugins/modules/meraki_mx_malware.py
new file mode 100644
index 000000000..1cbf7e680
--- /dev/null
+++ b/ansible_collections/cisco/meraki/plugins/modules/meraki_mx_malware.py
@@ -0,0 +1,264 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Copyright: (c) 2019, Kevin Breit (@kbreit) <kevin.breit@kevinbreit.net>
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+ANSIBLE_METADATA = {
+ 'metadata_version': '1.1',
+ 'status': ['preview'],
+ 'supported_by': 'community'
+}
+
+DOCUMENTATION = r'''
+---
+module: meraki_mx_malware
+short_description: Manage Malware Protection in the Meraki cloud
+description:
+- Fully configure malware protection in a Meraki environment.
+notes:
+- Some of the options are likely only used for developers within Meraki.
+options:
+ state:
+ description:
+ - Specifies whether object should be queried, created/modified, or removed.
+ choices: [absent, present, query]
+ default: query
+ type: str
+ net_name:
+ description:
+ - Name of network which configuration is applied to.
+ aliases: [network]
+ type: str
+ net_id:
+ description:
+ - ID of network which configuration is applied to.
+ type: str
+ allowed_urls:
+ description:
+ - List of URLs to whitelist.
+ type: list
+ elements: dict
+ suboptions:
+ url:
+ description:
+ - URL string to allow.
+ type: str
+ comment:
+ description:
+ - Human readable information about URL.
+ type: str
+ allowed_files:
+ description:
+ - List of files to whitelist.
+ type: list
+ elements: dict
+ suboptions:
+ sha256:
+ description:
+ - 256-bit hash of file.
+ type: str
+ aliases: [ hash ]
+ comment:
+ description:
+ - Human readable information about file.
+ type: str
+ mode:
+ description:
+ - Enabled or disabled state of malware protection.
+ choices: [disabled, enabled]
+ type: str
+
+author:
+- Kevin Breit (@kbreit)
+extends_documentation_fragment: cisco.meraki.meraki
+'''
+
+EXAMPLES = r'''
+ - name: Enable malware protection
+ meraki_malware:
+ auth_key: abc123
+ state: present
+ org_name: YourOrg
+ net_name: YourNet
+ mode: enabled
+ delegate_to: localhost
+
+ - name: Set whitelisted url
+ meraki_malware:
+ auth_key: abc123
+ state: present
+ org_name: YourOrg
+ net_name: YourNet
+ mode: enabled
+ allowed_urls:
+ - url: www.ansible.com
+ comment: Ansible
+ - url: www.google.com
+ comment: Google
+ delegate_to: localhost
+
+ - name: Set whitelisted file
+ meraki_malware:
+ auth_key: abc123
+ state: present
+ org_name: YourOrg
+ net_name: YourNet
+ mode: enabled
+ allowed_files:
+ - sha256: e82c5f7d75004727e1f3b94426b9a11c8bc4c312a9170ac9a73abace40aef503
+ comment: random zip
+ delegate_to: localhost
+
+ - name: Get malware settings
+ meraki_malware:
+ auth_key: abc123
+ state: query
+ org_name: YourNet
+ net_name: YourOrg
+ delegate_to: localhost
+'''
+
+RETURN = r'''
+data:
+ description: List of administrators.
+ returned: success
+ type: complex
+ contains:
+ mode:
+ description: Mode to enable or disable malware scanning.
+ returned: success
+ type: str
+ sample: enabled
+ allowed_files:
+ description: List of files which are whitelisted.
+ returned: success
+ type: complex
+ contains:
+ sha256:
+ description: sha256 hash of whitelisted file.
+ returned: success
+ type: str
+ sample: e82c5f7d75004727e1f3b94426b9a11c8bc4c312a9170ac9a73abace40aef503
+ comment:
+ description: Comment about the whitelisted entity
+ returned: success
+ type: str
+ sample: TPS report
+ allowed_urls:
+ description: List of URLs which are whitelisted.
+ returned: success
+ type: complex
+ contains:
+ url:
+ description: URL of whitelisted site.
+ returned: success
+ type: str
+ sample: site.com
+ comment:
+ description: Comment about the whitelisted entity
+ returned: success
+ type: str
+ sample: Corporate HQ
+'''
+
+from ansible.module_utils.basic import AnsibleModule, json
+from ansible_collections.cisco.meraki.plugins.module_utils.network.meraki.meraki import MerakiModule, meraki_argument_spec
+
+
+def main():
+ # define the available arguments/parameters that a user can pass to
+ # the module
+
+ urls_arg_spec = dict(url=dict(type='str'),
+ comment=dict(type='str'),
+ )
+
+ files_arg_spec = dict(sha256=dict(type='str', aliases=['hash']),
+ comment=dict(type='str'),
+ )
+
+ argument_spec = meraki_argument_spec()
+ argument_spec.update(state=dict(type='str', choices=['absent', 'present', 'query'], default='query'),
+ net_name=dict(type='str', aliases=['network']),
+ net_id=dict(type='str'),
+ mode=dict(type='str', choices=['enabled', 'disabled']),
+ allowed_urls=dict(type='list', default=None, elements='dict', options=urls_arg_spec),
+ allowed_files=dict(type='list', default=None, elements='dict', options=files_arg_spec),
+ )
+
+ # the AnsibleModule object will be our abstraction working with Ansible
+ # this includes instantiation, a couple of common attr would be the
+ # args/params passed to the execution, as well as if the module
+ # supports check mode
+ module = AnsibleModule(argument_spec=argument_spec,
+ supports_check_mode=True,
+ )
+ meraki = MerakiModule(module, function='malware')
+
+ meraki.params['follow_redirects'] = 'all'
+
+ query_url = {'malware': '/networks/{net_id}/appliance/security/malware'}
+ update_url = {'malware': '/networks/{net_id}/appliance/security/malware'}
+
+ meraki.url_catalog['get_one'].update(query_url)
+ meraki.url_catalog['update'] = update_url
+
+ org_id = meraki.params['org_id']
+ if org_id is None:
+ org_id = meraki.get_org_id(meraki.params['org_name'])
+ net_id = meraki.params['net_id']
+ if net_id is None:
+ nets = meraki.get_nets(org_id=org_id)
+ net_id = meraki.get_net_id(net_name=meraki.params['net_name'], data=nets)
+
+ # Check for argument completeness
+ if meraki.params['state'] == 'present':
+ if meraki.params['allowed_files'] is not None or meraki.params['allowed_urls'] is not None:
+ if meraki.params['mode'] is None:
+ meraki.fail_json(msg="mode must be set when allowed_files or allowed_urls is set.")
+
+ # Assemble payload
+ if meraki.params['state'] == 'present':
+ payload = dict()
+ if meraki.params['mode'] is not None:
+ payload['mode'] = meraki.params['mode']
+ if meraki.params['allowed_urls'] is not None:
+ payload['allowedUrls'] = meraki.params['allowed_urls']
+ if meraki.params['allowed_files'] is not None:
+ payload['allowedFiles'] = meraki.params['allowed_files']
+
+ if meraki.params['state'] == 'query':
+ path = meraki.construct_path('get_one', net_id=net_id)
+ data = meraki.request(path, method='GET')
+ if meraki.status == 200:
+ meraki.result['data'] = data
+ elif meraki.params['state'] == 'present':
+ path = meraki.construct_path('get_one', net_id=net_id)
+ original = meraki.request(path, method='GET')
+ if meraki.is_update_required(original, payload):
+ if meraki.module.check_mode is True:
+ meraki.generate_diff(original, payload)
+ original.update(payload)
+ meraki.result['data'] = original
+ meraki.result['changed'] = True
+ meraki.exit_json(**meraki.result)
+ path = meraki.construct_path('update', net_id=net_id)
+ data = meraki.request(path, method='PUT', payload=json.dumps(payload))
+ if meraki.status == 200:
+ meraki.generate_diff(original, data)
+ meraki.result['data'] = data
+ meraki.result['changed'] = True
+ else:
+ meraki.result['data'] = original
+
+ # in the event of a successful module execution, you will want to
+ # simple AnsibleModule.exit_json(), passing the key/value results
+ meraki.exit_json(**meraki.result)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/ansible_collections/cisco/meraki/plugins/modules/meraki_mx_nat.py b/ansible_collections/cisco/meraki/plugins/modules/meraki_mx_nat.py
new file mode 100644
index 000000000..0844d4c1f
--- /dev/null
+++ b/ansible_collections/cisco/meraki/plugins/modules/meraki_mx_nat.py
@@ -0,0 +1,679 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Copyright: (c) 2019, Kevin Breit (@kbreit) <kevin.breit@kevinbreit.net>
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+ANSIBLE_METADATA = {
+ 'metadata_version': '1.1',
+ 'status': ['preview'],
+ 'supported_by': 'community'
+}
+
+DOCUMENTATION = r'''
+---
+module: meraki_mx_nat
+short_description: Manage NAT rules in Meraki cloud
+description:
+- Allows for creation, management, and visibility of NAT rules (1:1, 1:many, port forwarding) within Meraki.
+
+options:
+ state:
+ description:
+ - Create or modify an organization.
+ choices: [present, query]
+ default: present
+ type: str
+ net_name:
+ description:
+ - Name of a network.
+ aliases: [name, network]
+ type: str
+ net_id:
+ description:
+ - ID number of a network.
+ type: str
+ org_id:
+ description:
+ - ID of organization associated to a network.
+ type: str
+ subset:
+ description:
+ - Specifies which NAT components to query.
+ choices: ['1:1', '1:many', all, port_forwarding]
+ default: all
+ type: list
+ elements: str
+ one_to_one:
+ description:
+ - List of 1:1 NAT rules.
+ type: list
+ elements: dict
+ suboptions:
+ name:
+ description:
+ - A descriptive name for the rule.
+ type: str
+ public_ip:
+ description:
+ - The IP address that will be used to access the internal resource from the WAN.
+ type: str
+ lan_ip:
+ description:
+ - The IP address of the server or device that hosts the internal resource that you wish to make available on the WAN.
+ type: str
+ uplink:
+ description:
+ - The physical WAN interface on which the traffic will arrive.
+ choices: [both, internet1, internet2]
+ type: str
+ allowed_inbound:
+ description:
+ - The ports this mapping will provide access on, and the remote IPs that will be allowed access to the resource.
+ type: list
+ elements: dict
+ suboptions:
+ protocol:
+ description:
+ - Protocol to apply NAT rule to.
+ choices: [any, icmp-ping, tcp, udp]
+ type: str
+ default: any
+ destination_ports:
+ description:
+ - List of ports or port ranges that will be forwarded to the host on the LAN.
+ type: list
+ elements: str
+ allowed_ips:
+ description:
+ - ranges of WAN IP addresses that are allowed to make inbound connections on the specified ports or port ranges, or 'any'.
+ type: list
+ elements: str
+ one_to_many:
+ description:
+ - List of 1:many NAT rules.
+ type: list
+ elements: dict
+ suboptions:
+ public_ip:
+ description:
+ - The IP address that will be used to access the internal resource from the WAN.
+ type: str
+ uplink:
+ description:
+ - The physical WAN interface on which the traffic will arrive.
+ choices: [both, internet1, internet2]
+ type: str
+ port_rules:
+ description:
+ - List of associated port rules.
+ type: list
+ elements: dict
+ suboptions:
+ name:
+ description:
+ - A description of the rule.
+ type: str
+ protocol:
+ description:
+ - Protocol to apply NAT rule to.
+ choices: [tcp, udp]
+ type: str
+ public_port:
+ description:
+ - Destination port of the traffic that is arriving on the WAN.
+ type: str
+ local_ip:
+ description:
+ - Local IP address to which traffic will be forwarded.
+ type: str
+ local_port:
+ description:
+ - Destination port of the forwarded traffic that will be sent from the MX to the specified host on the LAN.
+ - If you simply wish to forward the traffic without translating the port, this should be the same as the Public port.
+ type: str
+ allowed_ips:
+ description:
+ - Remote IP addresses or ranges that are permitted to access the internal resource via this port forwarding rule, or 'any'.
+ type: list
+ elements: str
+ port_forwarding:
+ description:
+ - List of port forwarding rules.
+ type: list
+ elements: dict
+ suboptions:
+ name:
+ description:
+ - A descriptive name for the rule.
+ type: str
+ lan_ip:
+ description:
+ - The IP address of the server or device that hosts the internal resource that you wish to make available on the WAN.
+ type: str
+ uplink:
+ description:
+ - The physical WAN interface on which the traffic will arrive.
+ choices: [both, internet1, internet2]
+ type: str
+ public_port:
+ description:
+ - A port or port ranges that will be forwarded to the host on the LAN.
+ type: int
+ local_port:
+ description:
+ - A port or port ranges that will receive the forwarded traffic from the WAN.
+ type: int
+ allowed_ips:
+ description:
+ - List of ranges of WAN IP addresses that are allowed to make inbound connections on the specified ports or port ranges (or any).
+ type: list
+ elements: str
+ protocol:
+ description:
+ - Protocol to forward traffic for.
+ choices: [tcp, udp]
+ type: str
+
+author:
+ - Kevin Breit (@kbreit)
+extends_documentation_fragment: cisco.meraki.meraki
+'''
+
+EXAMPLES = r'''
+- name: Query all NAT rules
+ meraki_nat:
+ auth_key: abc123
+ org_name: YourOrg
+ net_name: YourNet
+ state: query
+ subset: all
+ delegate_to: localhost
+
+- name: Query 1:1 NAT rules
+ meraki_nat:
+ auth_key: abc123
+ org_name: YourOrg
+ net_name: YourNet
+ state: query
+ subset: '1:1'
+ delegate_to: localhost
+
+- name: Create 1:1 rule
+ meraki_nat:
+ auth_key: abc123
+ org_name: YourOrg
+ net_name: YourNet
+ state: present
+ one_to_one:
+ - name: Service behind NAT
+ public_ip: 1.2.1.2
+ lan_ip: 192.168.128.1
+ uplink: internet1
+ allowed_inbound:
+ - protocol: tcp
+ destination_ports:
+ - 80
+ allowed_ips:
+ - 10.10.10.10
+ delegate_to: localhost
+
+- name: Create 1:many rule
+ meraki_nat:
+ auth_key: abc123
+ org_name: YourOrg
+ net_name: YourNet
+ state: present
+ one_to_many:
+ - public_ip: 1.1.1.1
+ uplink: internet1
+ port_rules:
+ - name: Test rule
+ protocol: tcp
+ public_port: 10
+ local_ip: 192.168.128.1
+ local_port: 11
+ allowed_ips:
+ - any
+ delegate_to: localhost
+
+- name: Create port forwarding rule
+ meraki_nat:
+ auth_key: abc123
+ org_name: YourOrg
+ net_name: YourNet
+ state: present
+ port_forwarding:
+ - name: Test map
+ lan_ip: 192.168.128.1
+ uplink: both
+ protocol: tcp
+ allowed_ips:
+ - 1.1.1.1
+ public_port: 10
+ local_port: 11
+ delegate_to: localhost
+'''
+
+RETURN = r'''
+data:
+ description: Information about the created or manipulated object.
+ returned: success
+ type: complex
+ contains:
+ one_to_one:
+ description: Information about 1:1 NAT object.
+ returned: success, when 1:1 NAT object is in task
+ type: complex
+ contains:
+ rules:
+ description: List of 1:1 NAT rules.
+ returned: success, when 1:1 NAT object is in task
+ type: complex
+ contains:
+ name:
+ description: Name of NAT object.
+ returned: success, when 1:1 NAT object is in task
+ type: str
+ example: Web server behind NAT
+ lanIp:
+ description: Local IP address to be mapped.
+ returned: success, when 1:1 NAT object is in task
+ type: str
+ example: 192.168.128.22
+ publicIp:
+ description: Public IP address to be mapped.
+ returned: success, when 1:1 NAT object is in task
+ type: str
+ example: 148.2.5.100
+ uplink:
+ description: Internet port where rule is applied.
+ returned: success, when 1:1 NAT object is in task
+ type: str
+ example: internet1
+ allowedInbound:
+ description: List of inbound forwarding rules.
+ returned: success, when 1:1 NAT object is in task
+ type: complex
+ contains:
+ protocol:
+ description: Protocol to apply NAT rule to.
+ returned: success, when 1:1 NAT object is in task
+ type: str
+ example: tcp
+ destinationPorts:
+ description: Ports to apply NAT rule to.
+ returned: success, when 1:1 NAT object is in task
+ type: str
+ example: 80
+ allowedIps:
+ description: List of IP addresses to be forwarded.
+ returned: success, when 1:1 NAT object is in task
+ type: list
+ example: 10.80.100.0/24
+ one_to_many:
+ description: Information about 1:many NAT object.
+ returned: success, when 1:many NAT object is in task
+ type: complex
+ contains:
+ rules:
+ description: List of 1:many NAT rules.
+ returned: success, when 1:many NAT object is in task
+ type: complex
+ contains:
+ publicIp:
+ description: Public IP address to be mapped.
+ returned: success, when 1:many NAT object is in task
+ type: str
+ example: 148.2.5.100
+ uplink:
+ description: Internet port where rule is applied.
+ returned: success, when 1:many NAT object is in task
+ type: str
+ example: internet1
+ portRules:
+ description: List of NAT port rules.
+ returned: success, when 1:many NAT object is in task
+ type: complex
+ contains:
+ name:
+ description: Name of NAT object.
+ returned: success, when 1:many NAT object is in task
+ type: str
+ example: Web server behind NAT
+ protocol:
+ description: Protocol to apply NAT rule to.
+ returned: success, when 1:1 NAT object is in task
+ type: str
+ example: tcp
+ publicPort:
+ description: Destination port of the traffic that is arriving on WAN.
+ returned: success, when 1:1 NAT object is in task
+ type: int
+ example: 9443
+ localIp:
+ description: Local IP address traffic will be forwarded.
+ returned: success, when 1:1 NAT object is in task
+ type: str
+ example: 192.0.2.10
+ localPort:
+ description: Destination port to be forwarded to.
+ returned: success, when 1:1 NAT object is in task
+ type: int
+ example: 443
+ allowedIps:
+ description: List of IP addresses to be forwarded.
+ returned: success, when 1:1 NAT object is in task
+ type: list
+ example: 10.80.100.0/24
+ port_forwarding:
+ description: Information about port forwarding rules.
+ returned: success, when port forwarding is in task
+ type: complex
+ contains:
+ rules:
+ description: List of port forwarding rules.
+ returned: success, when port forwarding is in task
+ type: complex
+ contains:
+ lanIp:
+ description: Local IP address to be mapped.
+ returned: success, when port forwarding is in task
+ type: str
+ example: 192.168.128.22
+ allowedIps:
+ description: List of IP addresses to be forwarded.
+ returned: success, when port forwarding is in task
+ type: list
+ example: 10.80.100.0/24
+ name:
+ description: Name of NAT object.
+ returned: success, when port forwarding is in task
+ type: str
+ example: Web server behind NAT
+ protocol:
+ description: Protocol to apply NAT rule to.
+ returned: success, when port forwarding is in task
+ type: str
+ example: tcp
+ publicPort:
+ description: Destination port of the traffic that is arriving on WAN.
+ returned: success, when port forwarding is in task
+ type: int
+ example: 9443
+ localPort:
+ description: Destination port to be forwarded to.
+ returned: success, when port forwarding is in task
+ type: int
+ example: 443
+ uplink:
+ description: Internet port where rule is applied.
+ returned: success, when port forwarding is in task
+ type: str
+ example: internet1
+'''
+
+from ansible.module_utils.basic import AnsibleModule, json
+from ansible.module_utils.common.dict_transformations import recursive_diff
+from ansible_collections.cisco.meraki.plugins.module_utils.network.meraki.meraki import MerakiModule, meraki_argument_spec
+
+key_map = {'name': 'name',
+ 'public_ip': 'publicIp',
+ 'lan_ip': 'lanIp',
+ 'uplink': 'uplink',
+ 'allowed_inbound': 'allowedInbound',
+ 'protocol': 'protocol',
+ 'destination_ports': 'destinationPorts',
+ 'allowed_ips': 'allowedIps',
+ 'port_rules': 'portRules',
+ 'public_port': 'publicPort',
+ 'local_ip': 'localIp',
+ 'local_port': 'localPort',
+ }
+
+
+def construct_payload(params):
+ if isinstance(params, list):
+ items = []
+ for item in params:
+ items.append(construct_payload(item))
+ return items
+ elif isinstance(params, dict):
+ info = {}
+ for param in params:
+ info[key_map[param]] = construct_payload(params[param])
+ return info
+ elif isinstance(params, str) or isinstance(params, int):
+ return params
+
+
+def list_int_to_str(data):
+ return [str(item) for item in data]
+
+
+def main():
+
+ # define the available arguments/parameters that a user can pass to
+ # the module
+
+ one_to_one_allowed_inbound_spec = dict(protocol=dict(type='str', choices=['tcp', 'udp', 'icmp-ping', 'any'], default='any'),
+ destination_ports=dict(type='list', elements='str'),
+ allowed_ips=dict(type='list', elements='str'),
+ )
+
+ one_to_many_port_inbound_spec = dict(protocol=dict(type='str', choices=['tcp', 'udp']),
+ name=dict(type='str'),
+ local_ip=dict(type='str'),
+ local_port=dict(type='str'),
+ allowed_ips=dict(type='list', elements='str'),
+ public_port=dict(type='str'),
+ )
+
+ one_to_one_spec = dict(name=dict(type='str'),
+ public_ip=dict(type='str'),
+ lan_ip=dict(type='str'),
+ uplink=dict(type='str', choices=['internet1', 'internet2', 'both']),
+ allowed_inbound=dict(type='list', elements='dict', options=one_to_one_allowed_inbound_spec),
+ )
+
+ one_to_many_spec = dict(public_ip=dict(type='str'),
+ uplink=dict(type='str', choices=['internet1', 'internet2', 'both']),
+ port_rules=dict(type='list', elements='dict', options=one_to_many_port_inbound_spec),
+ )
+
+ port_forwarding_spec = dict(name=dict(type='str'),
+ lan_ip=dict(type='str'),
+ uplink=dict(type='str', choices=['internet1', 'internet2', 'both']),
+ protocol=dict(type='str', choices=['tcp', 'udp']),
+ public_port=dict(type='int'),
+ local_port=dict(type='int'),
+ allowed_ips=dict(type='list', elements='str'),
+ )
+
+ argument_spec = meraki_argument_spec()
+ argument_spec.update(
+ net_id=dict(type='str'),
+ net_name=dict(type='str', aliases=['name', 'network']),
+ state=dict(type='str', choices=['present', 'query'], default='present'),
+ subset=dict(type='list', elements='str', choices=['1:1', '1:many', 'all', 'port_forwarding'], default='all'),
+ one_to_one=dict(type='list', elements='dict', options=one_to_one_spec),
+ one_to_many=dict(type='list', elements='dict', options=one_to_many_spec),
+ port_forwarding=dict(type='list', elements='dict', options=port_forwarding_spec),
+ )
+
+ # the AnsibleModule object will be our abstraction working with Ansible
+ # this includes instantiation, a couple of common attr would be the
+ # args/params passed to the execution, as well as if the module
+ # supports check mode
+ module = AnsibleModule(argument_spec=argument_spec,
+ supports_check_mode=True,
+ )
+
+ meraki = MerakiModule(module, function='nat')
+ module.params['follow_redirects'] = 'all'
+
+ one_to_one_payload = None
+ one_to_many_payload = None
+ port_forwarding_payload = None
+ if meraki.params['state'] == 'present':
+ if meraki.params['one_to_one'] is not None:
+ rules = []
+ for i in meraki.params['one_to_one']:
+ data = {'name': i['name'],
+ 'publicIp': i['public_ip'],
+ 'uplink': i['uplink'],
+ 'lanIp': i['lan_ip'],
+ 'allowedInbound': construct_payload(i['allowed_inbound'])
+ }
+ for inbound in data['allowedInbound']:
+ inbound['destinationPorts'] = list_int_to_str(inbound['destinationPorts'])
+ rules.append(data)
+ one_to_one_payload = {'rules': rules}
+ if meraki.params['one_to_many'] is not None:
+ rules = []
+ for i in meraki.params['one_to_many']:
+ data = {'publicIp': i['public_ip'],
+ 'uplink': i['uplink'],
+ }
+ port_rules = []
+ for port_rule in i['port_rules']:
+ rule = {'name': port_rule['name'],
+ 'protocol': port_rule['protocol'],
+ 'publicPort': str(port_rule['public_port']),
+ 'localIp': port_rule['local_ip'],
+ 'localPort': str(port_rule['local_port']),
+ 'allowedIps': port_rule['allowed_ips'],
+ }
+ port_rules.append(rule)
+ data['portRules'] = port_rules
+ rules.append(data)
+ one_to_many_payload = {'rules': rules}
+ if meraki.params['port_forwarding'] is not None:
+ port_forwarding_payload = {'rules': construct_payload(meraki.params['port_forwarding'])}
+ for rule in port_forwarding_payload['rules']:
+ rule['localPort'] = str(rule['localPort'])
+ rule['publicPort'] = str(rule['publicPort'])
+
+ onetomany_urls = {'nat': '/networks/{net_id}/appliance/firewall/oneToManyNatRules'}
+ onetoone_urls = {'nat': '/networks/{net_id}/appliance/firewall/oneToOneNatRules'}
+ port_forwarding_urls = {'nat': '/networks/{net_id}/appliance/firewall/portForwardingRules'}
+ meraki.url_catalog['1:many'] = onetomany_urls
+ meraki.url_catalog['1:1'] = onetoone_urls
+ meraki.url_catalog['port_forwarding'] = port_forwarding_urls
+
+ if meraki.params['net_name'] and meraki.params['net_id']:
+ meraki.fail_json(msg='net_name and net_id are mutually exclusive')
+
+ org_id = meraki.params['org_id']
+ if not org_id:
+ org_id = meraki.get_org_id(meraki.params['org_name'])
+ net_id = meraki.params['net_id']
+ if net_id is None:
+ nets = meraki.get_nets(org_id=org_id)
+ net_id = meraki.get_net_id(org_id, meraki.params['net_name'], data=nets)
+
+ if meraki.params['state'] == 'query':
+ if meraki.params['subset'][0] == 'all':
+ path = meraki.construct_path('1:many', net_id=net_id)
+ data = {'1:many': meraki.request(path, method='GET')}
+ path = meraki.construct_path('1:1', net_id=net_id)
+ data['1:1'] = meraki.request(path, method='GET')
+ path = meraki.construct_path('port_forwarding', net_id=net_id)
+ data['port_forwarding'] = meraki.request(path, method='GET')
+ meraki.result['data'] = data
+ else:
+ for subset in meraki.params['subset']:
+ path = meraki.construct_path(subset, net_id=net_id)
+ data = {subset: meraki.request(path, method='GET')}
+ try:
+ meraki.result['data'][subset] = data
+ except KeyError:
+ meraki.result['data'] = {subset: data}
+ elif meraki.params['state'] == 'present':
+ meraki.result['data'] = dict()
+ if one_to_one_payload is not None:
+ path = meraki.construct_path('1:1', net_id=net_id)
+ current = meraki.request(path, method='GET')
+ if meraki.is_update_required(current, one_to_one_payload):
+ if meraki.module.check_mode is True:
+ diff = recursive_diff(current, one_to_one_payload)
+ current.update(one_to_one_payload)
+ if 'diff' not in meraki.result:
+ meraki.result['diff'] = {'before': {}, 'after': {}}
+ meraki.result['diff']['before'].update({'one_to_one': diff[0]})
+ meraki.result['diff']['after'].update({'one_to_one': diff[1]})
+ meraki.result['data'] = {'one_to_one': current}
+ meraki.result['changed'] = True
+ else:
+ r = meraki.request(path, method='PUT', payload=json.dumps(one_to_one_payload))
+ if meraki.status == 200:
+ diff = recursive_diff(current, one_to_one_payload)
+ if 'diff' not in meraki.result:
+ meraki.result['diff'] = {'before': {}, 'after': {}}
+ meraki.result['diff']['before'].update({'one_to_one': diff[0]})
+ meraki.result['diff']['after'].update({'one_to_one': diff[1]})
+ meraki.result['data'] = {'one_to_one': r}
+ meraki.result['changed'] = True
+ else:
+ meraki.result['data']['one_to_one'] = current
+ if one_to_many_payload is not None:
+ path = meraki.construct_path('1:many', net_id=net_id)
+ current = meraki.request(path, method='GET')
+ if meraki.is_update_required(current, one_to_many_payload):
+ if meraki.module.check_mode is True:
+ diff = recursive_diff(current, one_to_many_payload)
+ current.update(one_to_many_payload)
+ if 'diff' not in meraki.result:
+ meraki.result['diff'] = {'before': {}, 'after': {}}
+ meraki.result['diff']['before'].update({'one_to_many': diff[0]})
+ meraki.result['diff']['after'].update({'one_to_many': diff[1]})
+ meraki.result['data']['one_to_many'] = current
+ meraki.result['changed'] = True
+ else:
+ r = meraki.request(path, method='PUT', payload=json.dumps(one_to_many_payload))
+ if meraki.status == 200:
+ diff = recursive_diff(current, one_to_many_payload)
+ if 'diff' not in meraki.result:
+ meraki.result['diff'] = {'before': {}, 'after': {}}
+ meraki.result['diff']['before'].update({'one_to_many': diff[0]})
+ meraki.result['diff']['after'].update({'one_to_many': diff[1]})
+ meraki.result['data'].update({'one_to_many': r})
+ meraki.result['changed'] = True
+ else:
+ meraki.result['data']['one_to_many'] = current
+ if port_forwarding_payload is not None:
+ path = meraki.construct_path('port_forwarding', net_id=net_id)
+ current = meraki.request(path, method='GET')
+ if meraki.is_update_required(current, port_forwarding_payload):
+ if meraki.module.check_mode is True:
+ diff = recursive_diff(current, port_forwarding_payload)
+ current.update(port_forwarding_payload)
+ if 'diff' not in meraki.result:
+ meraki.result['diff'] = {'before': {}, 'after': {}}
+ meraki.result['diff']['before'].update({'port_forwarding': diff[0]})
+ meraki.result['diff']['after'].update({'port_forwarding': diff[1]})
+ meraki.result['data']['port_forwarding'] = current
+ meraki.result['changed'] = True
+ else:
+ r = meraki.request(path, method='PUT', payload=json.dumps(port_forwarding_payload))
+ if meraki.status == 200:
+ if 'diff' not in meraki.result:
+ meraki.result['diff'] = {'before': {}, 'after': {}}
+ diff = recursive_diff(current, port_forwarding_payload)
+ meraki.result['diff']['before'].update({'port_forwarding': diff[0]})
+ meraki.result['diff']['after'].update({'port_forwarding': diff[1]})
+ meraki.result['data'].update({'port_forwarding': r})
+ meraki.result['changed'] = True
+ else:
+ meraki.result['data']['port_forwarding'] = current
+
+ # in the event of a successful module execution, you will want to
+ # simple AnsibleModule.exit_json(), passing the key/value results
+ meraki.exit_json(**meraki.result)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/ansible_collections/cisco/meraki/plugins/modules/meraki_mx_network_vlan_settings.py b/ansible_collections/cisco/meraki/plugins/modules/meraki_mx_network_vlan_settings.py
new file mode 100644
index 000000000..105114e54
--- /dev/null
+++ b/ansible_collections/cisco/meraki/plugins/modules/meraki_mx_network_vlan_settings.py
@@ -0,0 +1,161 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Copyright: (c) 2022 Kevin Breit (@kbreit) <kevin.breit@kevinbreit.net>
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+
+__metaclass__ = type
+
+ANSIBLE_METADATA = {
+ "metadata_version": "1.1",
+ "status": ["preview"],
+ "supported_by": "community",
+}
+
+DOCUMENTATION = r"""
+---
+module: meraki_mx_network_vlan_settings
+short_description: Manage VLAN settings for Meraki Networks
+description:
+- Edits VLAN enabled status on a network within Meraki.
+options:
+ state:
+ description:
+ - Create or modify an alert.
+ choices: [ present, query ]
+ type: str
+ net_name:
+ description:
+ - Name of a network.
+ aliases: [ name, network ]
+ type: str
+ net_id:
+ description:
+ - ID number of a network.
+ type: str
+ vlans_enabled:
+ description:
+ - Whether VLANs are enabled on the network.
+ type: bool
+
+author:
+ - Kevin Breit (@kbreit)
+extends_documentation_fragment: cisco.meraki.meraki
+"""
+
+EXAMPLES = r"""
+- name: Update settings
+ meraki_mx_network_vlan_settings:
+ auth_key: abc123
+ org_name: YourOrg
+ net_name: YourNet
+ state: present
+ vlans_enabled: true
+"""
+
+RETURN = r"""
+data:
+ description: Information about the created or manipulated object.
+ returned: info
+ type: complex
+ contains:
+ vlans_enabled:
+ description: Whether VLANs are enabled for this network.
+ returned: success
+ type: bool
+"""
+
+import copy
+from ansible.module_utils.basic import AnsibleModule, json
+from ansible_collections.cisco.meraki.plugins.module_utils.network.meraki.meraki import (
+ MerakiModule,
+ meraki_argument_spec,
+)
+
+
+def construct_payload(meraki):
+ payload = {"vlansEnabled": meraki.params["vlans_enabled"]}
+ return payload
+
+
+def main():
+
+ # define the available arguments/parameters that a user can pass to
+ # the module
+
+ argument_spec = meraki_argument_spec()
+ argument_spec.update(
+ net_id=dict(type="str"),
+ net_name=dict(type="str", aliases=["name", "network"]),
+ state=dict(type="str", choices=["query", "present"]),
+ vlans_enabled=dict(type="bool"),
+ )
+
+ # the AnsibleModule object will be our abstraction working with Ansible
+ # this includes instantiation, a couple of common attr would be the
+ # args/params passed to the execution, as well as if the module
+ # supports check mode
+ module = AnsibleModule(
+ argument_spec=argument_spec,
+ supports_check_mode=True,
+ )
+
+ meraki = MerakiModule(module, function="network_vlan_settings")
+ module.params["follow_redirects"] = "all"
+
+ query_urls = {
+ "network_vlan_settings": "/networks/{net_id}/appliance/vlans/settings"
+ }
+ update_urls = {
+ "network_vlan_settings": "/networks/{net_id}/appliance/vlans/settings"
+ }
+ meraki.url_catalog["get_all"].update(query_urls)
+ meraki.url_catalog["update"] = update_urls
+
+ # manipulate or modify the state as needed (this is going to be the
+ # part where your module will do what it needs to do)
+
+ org_id = meraki.params["org_id"]
+ if org_id is None:
+ org_id = meraki.get_org_id(meraki.params["org_name"])
+ net_id = meraki.params["net_id"]
+ if net_id is None:
+ nets = meraki.get_nets(org_id=org_id)
+ net_id = meraki.get_net_id(net_name=meraki.params["net_name"], data=nets)
+
+ if meraki.params["state"] == "query":
+ path = meraki.construct_path("get_all", net_id=net_id)
+ response = meraki.request(path, method="GET")
+ if meraki.status == 200:
+ meraki.result["data"] = response
+ meraki.exit_json(**meraki.result)
+ elif meraki.params["state"] == "present":
+ path = meraki.construct_path("get_all", net_id=net_id)
+ original = meraki.request(path, method="GET")
+ payload = construct_payload(meraki)
+ if meraki.is_update_required(original, payload):
+ if meraki.check_mode is True:
+ meraki.generate_diff(original, payload)
+ meraki.result["data"] = payload
+ meraki.result["changed"] = True
+ meraki.exit_json(**meraki.result)
+ path = meraki.construct_path("update", net_id=net_id)
+ response = meraki.request(path, method="PUT", payload=json.dumps(payload))
+ if meraki.status == 200:
+ meraki.generate_diff(original, payload)
+ meraki.result["data"] = response
+ meraki.result["changed"] = True
+ meraki.exit_json(**meraki.result)
+ else:
+ meraki.result["data"] = original
+ meraki.exit_json(**meraki.result)
+
+ # in the event of a successful module execution, you will want to
+ # simple AnsibleModule.exit_json(), passing the key/value results
+ meraki.exit_json(**meraki.result)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/ansible_collections/cisco/meraki/plugins/modules/meraki_mx_site_to_site_firewall.py b/ansible_collections/cisco/meraki/plugins/modules/meraki_mx_site_to_site_firewall.py
new file mode 100644
index 000000000..f81ac3a3c
--- /dev/null
+++ b/ansible_collections/cisco/meraki/plugins/modules/meraki_mx_site_to_site_firewall.py
@@ -0,0 +1,330 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Copyright: (c) 2018, Kevin Breit (@kbreit) <kevin.breit@kevinbreit.net>
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+ANSIBLE_METADATA = {
+ 'metadata_version': '1.1',
+ 'status': ['preview'],
+ 'supported_by': 'community'
+}
+
+DOCUMENTATION = r'''
+---
+module: meraki_mx_site_to_site_firewall
+short_description: Manage MX appliance firewall rules for site-to-site VPNs
+version_added: "1.0.0"
+description:
+- Allows for creation, management, and visibility into firewall rules for site-to-site VPNs implemented on Meraki MX firewalls.
+notes:
+- Module assumes a complete list of firewall rules are passed as a parameter.
+options:
+ state:
+ description:
+ - Create or modify an organization.
+ choices: ['present', 'query']
+ default: present
+ type: str
+ rules:
+ description:
+ - List of firewall rules.
+ type: list
+ elements: dict
+ suboptions:
+ policy:
+ description:
+ - Policy to apply if rule is hit.
+ choices: [allow, deny]
+ type: str
+ protocol:
+ description:
+ - Protocol to match against.
+ choices: [any, icmp, tcp, udp]
+ type: str
+ dest_port:
+ description:
+ - Comma separated list of destination port numbers to match against.
+ - C(Any) must be capitalized.
+ type: str
+ dest_cidr:
+ description:
+ - Comma separated list of CIDR notation destination networks.
+ - C(Any) must be capitalized.
+ type: str
+ src_port:
+ description:
+ - Comma separated list of source port numbers to match against.
+ - C(Any) must be capitalized.
+ type: str
+ src_cidr:
+ description:
+ - Comma separated list of CIDR notation source networks.
+ - C(Any) must be capitalized.
+ type: str
+ comment:
+ description:
+ - Optional comment to describe the firewall rule.
+ type: str
+ syslog_enabled:
+ description:
+ - Whether to log hints against the firewall rule.
+ - Only applicable if a syslog server is specified against the network.
+ type: bool
+ default: False
+ syslog_default_rule:
+ description:
+ - Whether to log hits against the default firewall rule.
+ - Only applicable if a syslog server is specified against the network.
+ - This is not shown in response from Meraki. Instead, refer to the C(syslog_enabled) value in the default rule.
+ type: bool
+author:
+- Kevin Breit (@kbreit)
+extends_documentation_fragment: cisco.meraki.meraki
+'''
+
+EXAMPLES = r'''
+- name: Query firewall rules
+ meraki_mx_site_to_site_firewall:
+ auth_key: abc123
+ org_name: YourOrg
+ state: query
+ delegate_to: localhost
+
+- name: Set two firewall rules
+ meraki_mx_site_to_site_firewall:
+ auth_key: abc123
+ org_name: YourOrg
+ state: present
+ rules:
+ - comment: Block traffic to server
+ src_cidr: 192.0.1.0/24
+ src_port: any
+ dest_cidr: 192.0.2.2/32
+ dest_port: any
+ protocol: any
+ policy: deny
+ - comment: Allow traffic to group of servers
+ src_cidr: 192.0.1.0/24
+ src_port: any
+ dest_cidr: 192.0.2.0/24
+ dest_port: any
+ protocol: any
+ policy: permit
+ delegate_to: localhost
+
+- name: Set one firewall rule and enable logging of the default rule
+ meraki_mx_site_to_site_firewall:
+ auth_key: abc123
+ org_name: YourOrg
+ state: present
+ rules:
+ - comment: Block traffic to server
+ src_cidr: 192.0.1.0/24
+ src_port: any
+ dest_cidr: 192.0.2.2/32
+ dest_port: any
+ protocol: any
+ policy: deny
+ syslog_default_rule: yes
+ delegate_to: localhost
+'''
+
+RETURN = r'''
+data:
+ description: Firewall rules associated to network.
+ returned: success
+ type: complex
+ contains:
+ rules:
+ description: List of firewall rules associated to network.
+ returned: success
+ type: complex
+ contains:
+ comment:
+ description: Comment to describe the firewall rule.
+ returned: always
+ type: str
+ sample: Block traffic to server
+ src_cidr:
+ description: Comma separated list of CIDR notation source networks.
+ returned: always
+ type: str
+ sample: 192.0.1.1/32,192.0.1.2/32
+ src_port:
+ description: Comma separated list of source ports.
+ returned: always
+ type: str
+ sample: 80,443
+ dest_cidr:
+ description: Comma separated list of CIDR notation destination networks.
+ returned: always
+ type: str
+ sample: 192.0.1.1/32,192.0.1.2/32
+ dest_port:
+ description: Comma separated list of destination ports.
+ returned: always
+ type: str
+ sample: 80,443
+ protocol:
+ description: Network protocol for which to match against.
+ returned: always
+ type: str
+ sample: tcp
+ policy:
+ description: Action to take when rule is matched.
+ returned: always
+ type: str
+ syslog_enabled:
+ description: Whether to log to syslog when rule is matched.
+ returned: always
+ type: bool
+ sample: true
+'''
+
+from ansible.module_utils.basic import AnsibleModule, json
+from ansible_collections.cisco.meraki.plugins.module_utils.network.meraki.meraki import MerakiModule, meraki_argument_spec
+
+
+def assemble_payload(meraki):
+ params_map = {'policy': 'policy',
+ 'protocol': 'protocol',
+ 'dest_port': 'destPort',
+ 'dest_cidr': 'destCidr',
+ 'src_port': 'srcPort',
+ 'src_cidr': 'srcCidr',
+ 'syslog_enabled': 'syslogEnabled',
+ 'comment': 'comment',
+ }
+ rules = []
+ for rule in meraki.params['rules']:
+ proposed_rule = dict()
+ for k, v in rule.items():
+ proposed_rule[params_map[k]] = v
+ rules.append(proposed_rule)
+ payload = {'rules': rules}
+ return payload
+
+
+def get_rules(meraki, org_id):
+ path = meraki.construct_path('get_all', org_id=org_id)
+ response = meraki.request(path, method='GET')
+ if meraki.status == 200:
+ return response
+
+
+def main():
+ # define the available arguments/parameters that a user can pass to
+ # the module
+
+ fw_rules = dict(policy=dict(type='str', choices=['allow', 'deny']),
+ protocol=dict(type='str', choices=['tcp', 'udp', 'icmp', 'any']),
+ dest_port=dict(type='str'),
+ dest_cidr=dict(type='str'),
+ src_port=dict(type='str'),
+ src_cidr=dict(type='str'),
+ comment=dict(type='str'),
+ syslog_enabled=dict(type='bool', default=False),
+ )
+
+ argument_spec = meraki_argument_spec()
+ argument_spec.update(state=dict(type='str', choices=['present', 'query'], default='present'),
+ rules=dict(type='list', default=None, elements='dict', options=fw_rules),
+ syslog_default_rule=dict(type='bool'),
+ )
+
+ # the AnsibleModule object will be our abstraction working with Ansible
+ # this includes instantiation, a couple of common attr would be the
+ # args/params passed to the execution, as well as if the module
+ # supports check mode
+ module = AnsibleModule(argument_spec=argument_spec,
+ supports_check_mode=True,
+ )
+ meraki = MerakiModule(module, function='mx_site_to_site_firewall')
+
+ meraki.params['follow_redirects'] = 'all'
+
+ query_urls = {'mx_site_to_site_firewall': '/organizations/{org_id}/appliance/vpn/vpnFirewallRules/'}
+ update_urls = {'mx_site_to_site_firewall': '/organizations/{org_id}/appliance/vpn/vpnFirewallRules/'}
+
+ meraki.url_catalog['get_all'].update(query_urls)
+ meraki.url_catalog['update'] = update_urls
+
+ payload = None
+
+ # execute checks for argument completeness
+
+ # manipulate or modify the state as needed (this is going to be the
+ # part where your module will do what it needs to do)
+ org_id = meraki.params['org_id']
+ orgs = None
+ if org_id is None:
+ orgs = meraki.get_orgs()
+ for org in orgs:
+ if org['name'] == meraki.params['org_name']:
+ org_id = org['id']
+
+ if meraki.params['state'] == 'query':
+ meraki.result['data'] = get_rules(meraki, org_id)
+ elif meraki.params['state'] == 'present':
+ rules = get_rules(meraki, org_id)
+ path = meraki.construct_path('get_all', org_id=org_id)
+ if meraki.params['rules'] is not None:
+ payload = assemble_payload(meraki)
+ else:
+ payload = dict()
+ update = False
+ if meraki.params['syslog_default_rule'] is not None:
+ payload['syslogDefaultRule'] = meraki.params['syslog_default_rule']
+ try:
+ if meraki.params['rules'] is not None:
+ if len(rules['rules']) - 1 != len(payload['rules']): # Quick and simple check to avoid more processing
+ update = True
+ if meraki.params['syslog_default_rule'] is not None:
+ if rules['rules'][len(rules['rules']) - 1]['syslogEnabled'] != meraki.params['syslog_default_rule']:
+ update = True
+ if update is False:
+ default_rule = rules['rules'][len(rules['rules']) - 1].copy()
+ # meraki.fail_json(msg=update)
+ del rules['rules'][len(rules['rules']) - 1] # Remove default rule for comparison
+ if len(rules['rules']) - 1 == 0:
+ if meraki.is_update_required(rules['rules'][0], payload['rules'][0]) is True:
+ update = True
+ else:
+ for r in range(len(rules) - 1):
+ if meraki.is_update_required(rules['rules'][r], payload['rules'][r]) is True:
+ update = True
+ rules['rules'].append(default_rule)
+ except KeyError:
+ pass
+ if update is True:
+ if meraki.check_mode is True:
+ if meraki.params['rules'] is not None:
+ data = payload
+ data['rules'].append(rules['rules'][len(rules['rules']) - 1]) # Append the default rule
+ if meraki.params['syslog_default_rule'] is not None:
+ data['rules'][len(payload['rules']) - 1]['syslog_enabled'] = meraki.params['syslog_default_rule']
+ else:
+ if meraki.params['syslog_default_rule'] is not None:
+ data = rules
+ data['rules'][len(data['rules']) - 1]['syslogEnabled'] = meraki.params['syslog_default_rule']
+ meraki.result['data'] = data
+ meraki.result['changed'] = True
+ meraki.exit_json(**meraki.result)
+ response = meraki.request(path, method='PUT', payload=json.dumps(payload))
+ if meraki.status == 200:
+ meraki.result['data'] = response
+ meraki.result['changed'] = True
+ else:
+ meraki.result['data'] = rules
+
+ # in the event of a successful module execution, you will want to
+ # simple AnsibleModule.exit_json(), passing the key/value results
+ meraki.exit_json(**meraki.result)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/ansible_collections/cisco/meraki/plugins/modules/meraki_mx_site_to_site_vpn.py b/ansible_collections/cisco/meraki/plugins/modules/meraki_mx_site_to_site_vpn.py
new file mode 100644
index 000000000..2c1711adc
--- /dev/null
+++ b/ansible_collections/cisco/meraki/plugins/modules/meraki_mx_site_to_site_vpn.py
@@ -0,0 +1,270 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Copyright: (c) 2018, Kevin Breit (@kbreit) <kevin.breit@kevinbreit.net>
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+ANSIBLE_METADATA = {
+ 'metadata_version': '1.1',
+ 'status': ['preview'],
+ 'supported_by': 'community'
+}
+
+DOCUMENTATION = r'''
+---
+module: meraki_mx_site_to_site_vpn
+short_description: Manage AutoVPN connections in Meraki
+version_added: "1.1.0"
+description:
+- Allows for creation, management, and visibility into AutoVPNs implemented on Meraki MX firewalls.
+options:
+ state:
+ description:
+ - Create or modify an organization.
+ choices: ['present', 'query']
+ default: present
+ type: str
+ net_name:
+ description:
+ - Name of network which MX firewall is in.
+ type: str
+ net_id:
+ description:
+ - ID of network which MX firewall is in.
+ type: str
+ mode:
+ description:
+ - Set VPN mode for network
+ choices: ['none', 'hub', 'spoke']
+ type: str
+ hubs:
+ description:
+ - List of hubs to assign to a spoke.
+ type: list
+ elements: dict
+ suboptions:
+ hub_id:
+ description:
+ - Network ID of hub
+ type: str
+ use_default_route:
+ description:
+ - Indicates whether deafult troute traffic should be sent to this hub.
+ - Only valid in spoke mode.
+ type: bool
+ subnets:
+ description:
+ - List of subnets to advertise over VPN.
+ type: list
+ elements: dict
+ suboptions:
+ local_subnet:
+ description:
+ - CIDR formatted subnet.
+ type: str
+ use_vpn:
+ description:
+ - Whether to advertise over VPN.
+ type: bool
+author:
+- Kevin Breit (@kbreit)
+extends_documentation_fragment: cisco.meraki.meraki
+'''
+
+EXAMPLES = r'''
+- name: Set hub mode
+ meraki_site_to_site_vpn:
+ auth_key: abc123
+ state: present
+ org_name: YourOrg
+ net_name: hub_network
+ mode: hub
+ delegate_to: localhost
+ register: set_hub
+
+- name: Set spoke mode
+ meraki_site_to_site_vpn:
+ auth_key: abc123
+ state: present
+ org_name: YourOrg
+ net_name: spoke_network
+ mode: spoke
+ hubs:
+ - hub_id: N_1234
+ use_default_route: false
+ delegate_to: localhost
+ register: set_spoke
+
+- name: Add subnet to hub for VPN. Hub is required.
+ meraki_site_to_site_vpn:
+ auth_key: abc123
+ state: present
+ org_name: YourOrg
+ net_name: hub_network
+ mode: hub
+ hubs:
+ - hub_id: N_1234
+ use_default_route: false
+ subnets:
+ - local_subnet: 192.168.1.0/24
+ use_vpn: true
+ delegate_to: localhost
+ register: set_hub
+
+- name: Query rules for hub
+ meraki_site_to_site_vpn:
+ auth_key: abc123
+ state: query
+ org_name: YourOrg
+ net_name: hub_network
+ delegate_to: localhost
+ register: query_all_hub
+'''
+
+RETURN = r'''
+data:
+ description: VPN settings.
+ returned: success
+ type: complex
+ contains:
+ mode:
+ description: Mode assigned to network.
+ returned: always
+ type: str
+ sample: spoke
+ hubs:
+ description: Hub networks to associate to.
+ returned: always
+ type: complex
+ contains:
+ hub_id:
+ description: ID of hub network.
+ returned: always
+ type: complex
+ sample: N_12345
+ use_default_route:
+ description: Whether to send all default route traffic over VPN.
+ returned: always
+ type: bool
+ sample: true
+ subnets:
+ description: List of subnets to advertise over VPN.
+ returned: always
+ type: complex
+ contains:
+ local_subnet:
+ description: CIDR formatted subnet.
+ returned: always
+ type: str
+ sample: 192.168.1.0/24
+ use_vpn:
+ description: Whether subnet should use the VPN.
+ returned: always
+ type: bool
+ sample: true
+'''
+
+from ansible.module_utils.basic import AnsibleModule, json
+from ansible_collections.cisco.meraki.plugins.module_utils.network.meraki.meraki import MerakiModule, meraki_argument_spec
+from copy import deepcopy
+
+
+def assemble_payload(meraki):
+ payload = {'mode': meraki.params['mode']}
+ if meraki.params['hubs'] is not None:
+ payload['hubs'] = meraki.params['hubs']
+ for hub in payload['hubs']:
+ hub['hubId'] = hub.pop('hub_id')
+ hub['useDefaultRoute'] = hub.pop('use_default_route')
+ if meraki.params['subnets'] is not None:
+ payload['subnets'] = meraki.params['subnets']
+ for subnet in payload['subnets']:
+ subnet['localSubnet'] = subnet.pop('local_subnet')
+ subnet['useVpn'] = subnet.pop('use_vpn')
+ return payload
+
+
+def main():
+ # define the available arguments/parameters that a user can pass to
+ # the module
+
+ hubs_args = dict(hub_id=dict(type='str'),
+ use_default_route=dict(type='bool'),
+ )
+ subnets_args = dict(local_subnet=dict(type='str'),
+ use_vpn=dict(type='bool'),
+ )
+
+ argument_spec = meraki_argument_spec()
+ argument_spec.update(state=dict(type='str', choices=['present', 'query'], default='present'),
+ net_name=dict(type='str'),
+ net_id=dict(type='str'),
+ hubs=dict(type='list', default=None, elements='dict', options=hubs_args),
+ subnets=dict(type='list', default=None, elements='dict', options=subnets_args),
+ mode=dict(type='str', choices=['none', 'hub', 'spoke']),
+ )
+
+ # the AnsibleModule object will be our abstraction working with Ansible
+ # this includes instantiation, a couple of common attr would be the
+ # args/params passed to the execution, as well as if the module
+ # supports check mode
+ module = AnsibleModule(argument_spec=argument_spec,
+ supports_check_mode=True,
+ )
+ meraki = MerakiModule(module, function='site_to_site_vpn')
+
+ meraki.params['follow_redirects'] = 'all'
+
+ query_urls = {'site_to_site_vpn': '/networks/{net_id}/appliance/vpn/siteToSiteVpn/'}
+ update_urls = {'site_to_site_vpn': '/networks/{net_id}/appliance/vpn/siteToSiteVpn/'}
+
+ meraki.url_catalog['get_all'].update(query_urls)
+ meraki.url_catalog['update'] = update_urls
+
+ payload = None
+
+ # manipulate or modify the state as needed (this is going to be the
+ # part where your module will do what it needs to do)
+ org_id = meraki.params['org_id']
+ if org_id is None:
+ orgs = meraki.get_orgs()
+ for org in orgs:
+ if org['name'] == meraki.params['org_name']:
+ org_id = org['id']
+ net_id = meraki.params['net_id']
+ if net_id is None:
+ net_id = meraki.get_net_id(net_name=meraki.params['net_name'],
+ data=meraki.get_nets(org_id=org_id))
+
+ if meraki.params['state'] == 'query':
+ path = meraki.construct_path('get_all', net_id=net_id)
+ response = meraki.request(path, method='GET')
+ meraki.result['data'] = response
+ elif meraki.params['state'] == 'present':
+ path = meraki.construct_path('get_all', net_id=net_id)
+ original = meraki.request(path, method='GET')
+ payload = assemble_payload(meraki)
+ comparable = deepcopy(original)
+ comparable.update(payload)
+ if meraki.is_update_required(original, payload):
+ if meraki.check_mode is True:
+ meraki.result['changed'] = True
+ meraki.result['data'] = payload
+ meraki.exit_json(**meraki.result)
+ path = meraki.construct_path('update', net_id=net_id)
+ response = meraki.request(path, method='PUT', payload=json.dumps(payload))
+ meraki.result['changed'] = True
+ meraki.result['data'] = response
+ else:
+ meraki.result['data'] = original
+
+ # in the event of a successful module execution, you will want to
+ # simple AnsibleModule.exit_json(), passing the key/value results
+ meraki.exit_json(**meraki.result)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/ansible_collections/cisco/meraki/plugins/modules/meraki_mx_static_route.py b/ansible_collections/cisco/meraki/plugins/modules/meraki_mx_static_route.py
new file mode 100644
index 000000000..51aef2656
--- /dev/null
+++ b/ansible_collections/cisco/meraki/plugins/modules/meraki_mx_static_route.py
@@ -0,0 +1,438 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Copyright: (c) 2018, 2019 Kevin Breit (@kbreit) <kevin.breit@kevinbreit.net>
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+
+__metaclass__ = type
+
+ANSIBLE_METADATA = {
+ "metadata_version": "1.1",
+ "status": ["preview"],
+ "supported_by": "community",
+}
+
+DOCUMENTATION = r"""
+---
+module: meraki_mx_static_route
+short_description: Manage static routes in the Meraki cloud
+description:
+- Allows for creation, management, and visibility into static routes within Meraki.
+
+options:
+ state:
+ description:
+ - Create or modify an organization.
+ choices: [ absent, query, present ]
+ default: present
+ type: str
+ net_name:
+ description:
+ - Name of a network.
+ type: str
+ net_id:
+ description:
+ - ID number of a network.
+ type: str
+ name:
+ description:
+ - Descriptive name of the static route.
+ type: str
+ subnet:
+ description:
+ - CIDR notation based subnet for static route.
+ type: str
+ gateway_ip:
+ description:
+ - IP address of the gateway for the subnet.
+ type: str
+ route_id:
+ description:
+ - Unique ID of static route.
+ type: str
+ gateway_vlan_id:
+ description:
+ - The gateway IP (next hop) VLAN ID of the static route.
+ type: int
+ fixed_ip_assignments:
+ description:
+ - List of fixed MAC to IP bindings for DHCP.
+ type: list
+ elements: dict
+ suboptions:
+ mac:
+ description:
+ - MAC address of endpoint.
+ type: str
+ ip:
+ description:
+ - IP address of endpoint.
+ type: str
+ name:
+ description:
+ - Hostname of endpoint.
+ type: str
+ reserved_ip_ranges:
+ description:
+ - List of IP ranges reserved for static IP assignments.
+ type: list
+ elements: dict
+ suboptions:
+ start:
+ description:
+ - First IP address of reserved range.
+ type: str
+ end:
+ description:
+ - Last IP address of reserved range.
+ type: str
+ comment:
+ description:
+ - Human readable description of reservation range.
+ type: str
+ enabled:
+ description:
+ - Indicates whether static route is enabled within a network.
+ type: bool
+
+
+author:
+ - Kevin Breit (@kbreit)
+extends_documentation_fragment: cisco.meraki.meraki
+"""
+
+EXAMPLES = r"""
+- name: Create static_route
+ meraki_static_route:
+ auth_key: abc123
+ state: present
+ org_name: YourOrg
+ net_name: YourNet
+ name: Test Route
+ subnet: 192.0.1.0/24
+ gateway_ip: 192.168.128.1
+ delegate_to: localhost
+
+- name: Update static route with fixed IP assignment
+ meraki_static_route:
+ auth_key: abc123
+ state: present
+ org_name: YourOrg
+ net_name: YourNet
+ route_id: d6fa4821-1234-4dfa-af6b-ae8b16c20c39
+ fixed_ip_assignments:
+ - mac: aa:bb:cc:dd:ee:ff
+ ip: 192.0.1.11
+ comment: Server
+ delegate_to: localhost
+
+- name: Query static routes
+ meraki_static_route:
+ auth_key: abc123
+ state: query
+ org_name: YourOrg
+ net_name: YourNet
+ delegate_to: localhost
+
+- name: Delete static routes
+ meraki_static_route:
+ auth_key: abc123
+ state: absent
+ org_name: YourOrg
+ net_name: YourNet
+ route_id: '{{item}}'
+ delegate_to: localhost
+"""
+
+RETURN = r"""
+data:
+ description: Information about the created or manipulated object.
+ returned: info
+ type: complex
+ contains:
+ id:
+ description: Unique identification string assigned to each static route.
+ returned: success
+ type: str
+ sample: d6fa4821-1234-4dfa-af6b-ae8b16c20c39
+ net_id:
+ description: Identification string of network.
+ returned: query or update
+ type: str
+ sample: N_12345
+ name:
+ description: Name of static route.
+ returned: success
+ type: str
+ sample: Data Center static route
+ subnet:
+ description: CIDR notation subnet for static route.
+ returned: success
+ type: str
+ sample: 192.0.1.0/24
+ gatewayIp:
+ description: Next hop IP address.
+ returned: success
+ type: str
+ sample: 192.1.1.1
+ enabled:
+ description: Enabled state of static route.
+ returned: query or update
+ type: bool
+ sample: True
+ reservedIpRanges:
+ description: List of IP address ranges which are reserved for static assignment.
+ returned: query or update
+ type: complex
+ contains:
+ start:
+ description: First address in reservation range, inclusive.
+ returned: query or update
+ type: str
+ sample: 192.0.1.2
+ end:
+ description: Last address in reservation range, inclusive.
+ returned: query or update
+ type: str
+ sample: 192.0.1.10
+ comment:
+ description: Human readable description of range.
+ returned: query or update
+ type: str
+ sample: Server range
+ fixedIpAssignments:
+ description: List of static MAC to IP address bindings.
+ returned: query or update
+ type: complex
+ contains:
+ mac:
+ description: Key is MAC address of endpoint.
+ returned: query or update
+ type: complex
+ contains:
+ ip:
+ description: IP address to be bound to the endpoint.
+ returned: query or update
+ type: str
+ sample: 192.0.1.11
+ name:
+ description: Hostname given to the endpoint.
+ returned: query or update
+ type: str
+ sample: JimLaptop
+"""
+
+from ansible.module_utils.basic import AnsibleModule, json
+from ansible_collections.cisco.meraki.plugins.module_utils.network.meraki.meraki import (
+ MerakiModule,
+ meraki_argument_spec,
+)
+
+
+def fixed_ip_factory(meraki, data):
+ fixed_ips = dict()
+ for item in data:
+ fixed_ips[item["mac"]] = {"ip": item["ip"], "name": item["name"]}
+ return fixed_ips
+
+
+def get_static_routes(meraki, net_id):
+ path = meraki.construct_path("get_all", net_id=net_id)
+ r = meraki.request(path, method="GET")
+ return r
+
+
+def get_static_route(meraki, net_id, route_id):
+ path = meraki.construct_path(
+ "get_one", net_id=net_id, custom={"route_id": route_id}
+ )
+ r = meraki.request(path, method="GET")
+ return r
+
+
+def does_route_exist(name, routes):
+ for route in routes:
+ if name == route["name"]:
+ return route
+ return None
+
+
+def update_dict(original, proposed):
+ for k, v in proposed.items():
+ if v is not None:
+ original[k] = v
+ return original
+
+
+def main():
+
+ # define the available arguments/parameters that a user can pass to
+ # the module
+
+ fixed_ip_arg_spec = dict(
+ mac=dict(type="str"),
+ ip=dict(type="str"),
+ name=dict(type="str"),
+ )
+
+ reserved_ip_arg_spec = dict(
+ start=dict(type="str"),
+ end=dict(type="str"),
+ comment=dict(type="str"),
+ )
+
+ argument_spec = meraki_argument_spec()
+ argument_spec.update(
+ net_id=dict(type="str"),
+ net_name=dict(type="str"),
+ name=dict(type="str"),
+ subnet=dict(type="str"),
+ gateway_ip=dict(type="str"),
+ state=dict(
+ type="str", default="present", choices=["absent", "present", "query"]
+ ),
+ fixed_ip_assignments=dict(
+ type="list", elements="dict", options=fixed_ip_arg_spec
+ ),
+ reserved_ip_ranges=dict(
+ type="list", elements="dict", options=reserved_ip_arg_spec
+ ),
+ route_id=dict(type="str"),
+ enabled=dict(type="bool"),
+ gateway_vlan_id=dict(type="int"),
+ )
+
+ # the AnsibleModule object will be our abstraction working with Ansible
+ # this includes instantiation, a couple of common attr would be the
+ # args/params passed to the execution, as well as if the module
+ # supports check mode
+ module = AnsibleModule(
+ argument_spec=argument_spec,
+ supports_check_mode=True,
+ )
+
+ meraki = MerakiModule(module, function="static_route")
+ module.params["follow_redirects"] = "all"
+ payload = None
+
+ query_urls = {"static_route": "/networks/{net_id}/appliance/staticRoutes"}
+ query_one_urls = {
+ "static_route": "/networks/{net_id}/appliance/staticRoutes/{route_id}"
+ }
+ create_urls = {"static_route": "/networks/{net_id}/appliance/staticRoutes/"}
+ update_urls = {
+ "static_route": "/networks/{net_id}/appliance/staticRoutes/{route_id}"
+ }
+ delete_urls = {
+ "static_route": "/networks/{net_id}/appliance/staticRoutes/{route_id}"
+ }
+ meraki.url_catalog["get_all"].update(query_urls)
+ meraki.url_catalog["get_one"].update(query_one_urls)
+ meraki.url_catalog["create"] = create_urls
+ meraki.url_catalog["update"] = update_urls
+ meraki.url_catalog["delete"] = delete_urls
+
+ if not meraki.params["org_name"] and not meraki.params["org_id"]:
+ meraki.fail_json(
+ msg="Parameters 'org_name' or 'org_id' parameters are required"
+ )
+ if not meraki.params["net_name"] and not meraki.params["net_id"]:
+ meraki.fail_json(
+ msg="Parameters 'net_name' or 'net_id' parameters are required"
+ )
+ if meraki.params["net_name"] and meraki.params["net_id"]:
+ meraki.fail_json(msg="'net_name' and 'net_id' are mutually exclusive")
+
+ # Construct payload
+ if meraki.params["state"] == "present":
+ payload = dict()
+ if meraki.params["net_name"]:
+ payload["name"] = meraki.params["net_name"]
+
+ # manipulate or modify the state as needed (this is going to be the
+ # part where your module will do what it needs to do)
+
+ org_id = meraki.params["org_id"]
+ if not org_id:
+ org_id = meraki.get_org_id(meraki.params["org_name"])
+ net_id = meraki.params["net_id"]
+ if net_id is None:
+ nets = meraki.get_nets(org_id=org_id)
+ net_id = meraki.get_net_id(net_name=meraki.params["net_name"], data=nets)
+
+ if meraki.params["state"] == "query":
+ if meraki.params["route_id"] is not None:
+ meraki.result["data"] = get_static_route(
+ meraki, net_id, meraki.params["route_id"]
+ )
+ else:
+ meraki.result["data"] = get_static_routes(meraki, net_id)
+ elif meraki.params["state"] == "present":
+ payload = {
+ "name": meraki.params["name"],
+ "subnet": meraki.params["subnet"],
+ "gatewayIp": meraki.params["gateway_ip"],
+ }
+ if meraki.params["fixed_ip_assignments"] is not None:
+ payload["fixedIpAssignments"] = fixed_ip_factory(
+ meraki, meraki.params["fixed_ip_assignments"]
+ )
+ if meraki.params["reserved_ip_ranges"] is not None:
+ payload["reservedIpRanges"] = meraki.params["reserved_ip_ranges"]
+ if meraki.params["enabled"] is not None:
+ payload["enabled"] = meraki.params["enabled"]
+ if meraki.params["gateway_vlan_id"] is not None:
+ payload["gatewayVlanId"] = meraki.params["gateway_vlan_id"]
+
+ route_id = meraki.params["route_id"]
+ if meraki.params["name"] is not None and route_id is None:
+ route_status = does_route_exist(
+ meraki.params["name"], get_static_routes(meraki, net_id)
+ )
+ if route_status is not None: # Route exists, assign route_id
+ route_id = route_status["id"]
+
+ if route_id is not None:
+ existing_route = get_static_route(meraki, net_id, route_id)
+ original = existing_route.copy()
+ payload = update_dict(existing_route, payload)
+ if module.check_mode:
+ meraki.result["data"] = payload
+ meraki.exit_json(**meraki.result)
+ if meraki.is_update_required(original, payload, optional_ignore=["id"]):
+ path = meraki.construct_path(
+ "update", net_id=net_id, custom={"route_id": route_id}
+ )
+ meraki.result["data"] = meraki.request(
+ path, method="PUT", payload=json.dumps(payload)
+ )
+ meraki.result["changed"] = True
+ else:
+ meraki.result["data"] = original
+ else:
+ if module.check_mode:
+ meraki.result["data"] = payload
+ meraki.exit_json(**meraki.result)
+ path = meraki.construct_path("create", net_id=net_id)
+ meraki.result["data"] = meraki.request(
+ path, method="POST", payload=json.dumps(payload)
+ )
+ meraki.result["changed"] = True
+ elif meraki.params["state"] == "absent":
+ if module.check_mode:
+ meraki.exit_json(**meraki.result)
+ path = meraki.construct_path(
+ "delete", net_id=net_id, custom={"route_id": meraki.params["route_id"]}
+ )
+ meraki.result["data"] = meraki.request(path, method="DELETE")
+ meraki.result["changed"] = True
+
+ # in the event of a successful module execution, you will want to
+ # simple AnsibleModule.exit_json(), passing the key/value results
+ meraki.exit_json(**meraki.result)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/ansible_collections/cisco/meraki/plugins/modules/meraki_mx_third_party_vpn_peers.py b/ansible_collections/cisco/meraki/plugins/modules/meraki_mx_third_party_vpn_peers.py
new file mode 100644
index 000000000..c504911c0
--- /dev/null
+++ b/ansible_collections/cisco/meraki/plugins/modules/meraki_mx_third_party_vpn_peers.py
@@ -0,0 +1,493 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Copyright: (c) 2022, Kevin Breit (@kbreit) <kevin.breit@kevinbreit.net>
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+
+__metaclass__ = type
+
+ANSIBLE_METADATA = {
+ "metadata_version": "1.1",
+ "status": ["preview"],
+ "supported_by": "community",
+}
+
+DOCUMENTATION = r"""
+---
+module: meraki_mx_third_party_vpn_peers
+short_description: Manage third party (IPSec) VPN peers for MX devices
+description:
+- Create, edit, query, or delete third party VPN peers in a Meraki environment.
+options:
+ state:
+ description:
+ - Specifies whether object should be queried, created/modified, or removed.
+ choices: [absent, present, query]
+ default: query
+ type: str
+ peers:
+ description:
+ - The list of VPN peers.
+ type: list
+ elements: dict
+ suboptions:
+ name:
+ description:
+ - The name of the VPN peer.
+ - Required when state is present.
+ type: str
+ public_ip:
+ description:
+ - The public IP of the VPN peer.
+ - Required when state is present.
+ type: str
+ secret:
+ description:
+ - The shared secret with the VPN peer.
+ - Required when state is present.
+ type: str
+ private_subnets:
+ description:
+ - The list of the private subnets of the VPN peer.
+ - Required when state is present.
+ type: list
+ elements: str
+ ike_version:
+ description:
+ - The IKE version to be used for the IPsec VPN peer configuration.
+ default: "1"
+ type: str
+ choices: ["1", "2"]
+ ipsec_policies_preset:
+ description:
+ - Specifies IPsec preset values. If this is provided, the 'ipsecPolicies' parameter is ignored.
+ type: str
+ choices: ["default", "aws", "azure"]
+ remote_id:
+ description:
+ - The remote ID is used to identify the connecting VPN peer. This can either be a valid IPv4 Address, FQDN or User FQDN.
+ type: str
+ network_tags:
+ description:
+ - A list of network tags that will connect with this peer. If not included, the default is ['all'].
+ type: list
+ elements: str
+ ipsec_policies:
+ description:
+ - Custom IPSec policies for the VPN peer. If not included and a preset has not been chosen, the default preset for IPSec policies will be used.
+ type: dict
+ suboptions:
+ child_lifetime:
+ description:
+ - The lifetime of the Phase 2 SA in seconds.
+ type: int
+ ike_lifetime:
+ description:
+ - The lifetime of the Phase 1 SA in seconds.
+ type: int
+ child_auth_algo:
+ description:
+ - This is the authentication algorithms to be used in Phase 2.
+ type: list
+ elements: str
+ choices: ['sha256', 'sha1', 'md5']
+ child_cipher_algo:
+ description:
+ - This is the cipher algorithms to be used in Phase 2.
+ choices: ['aes256', 'aes192', 'aes128', 'tripledes', 'des', 'null']
+ type: list
+ elements: str
+ child_pfs_group:
+ description:
+ - This is the Diffie-Hellman group to be used for Perfect Forward Secrecy in Phase 2.
+ type: list
+ elements: str
+ choices: ['disabled','group14', 'group5', 'group2', 'group1']
+ ike_auth_algo:
+ description:
+ - This is the authentication algorithm to be used in Phase 1.
+ type: list
+ elements: str
+ choices: ['sha256', 'sha1', 'md5']
+ ike_cipher_algo:
+ description:
+ - This is the cipher algorithm to be used in Phase 1.
+ type: list
+ elements: str
+ choices: ['aes256', 'aes192', 'aes128', 'tripledes', 'des']
+ ike_diffie_hellman_group:
+ description:
+ - This is the Diffie-Hellman group to be used in Phase 1.
+ type: list
+ elements: str
+ choices: ['group14', 'group5', 'group2', 'group1']
+ ike_prf_algo:
+ description:
+ - This is the pseudo-random function to be used in IKE_SA.
+ type: list
+ elements: str
+ choices: ['prfsha256', 'prfsha1', 'prfmd5', 'default']
+
+author:
+- Kevin Breit (@kbreit)
+extends_documentation_fragment: cisco.meraki.meraki
+"""
+
+EXAMPLES = r"""
+- name: Query all VPN peers
+ meraki_mx_third_party_vpn_peers:
+ auth_key: abc123
+ state: query
+ org_name: orgName
+
+- name: Create VPN peer with an IPsec policy
+ meraki_mx_third_party_vpn_peers:
+ auth_key: abc123
+ state: present
+ org_name: orgName
+ peers:
+ - name: "Test peer"
+ public_ip: "198.51.100.1"
+ secret: "s3cret"
+ private_subnets:
+ - "192.0.2.0/24"
+ ike_version: "2"
+ network_tags:
+ - none
+ remote_id: "192.0.2.0"
+ ipsec_policies:
+ child_lifetime: 600
+ ike_lifetime: 600
+ child_auth_algo:
+ - "md5"
+ child_cipher_algo:
+ - "tripledes"
+ - "aes192"
+ child_pfs_group:
+ - "disabled"
+ ike_auth_algo:
+ - "sha256"
+ ike_cipher_algo:
+ - "tripledes"
+ ike_diffie_hellman_group:
+ - "group2"
+ ike_prf_algo:
+ - "prfmd5"
+"""
+
+RETURN = r"""
+
+response:
+ description: Information about the organization which was created or modified
+ returned: success
+ type: complex
+ contains:
+ appliance_ip:
+ description: IP address of Meraki appliance in the VLAN
+ returned: success
+ type: str
+ sample: 192.0.1.1
+ dnsnamservers:
+ description: IP address or Meraki defined DNS servers which VLAN should use by default
+ returned: success
+ type: str
+ sample: upstream_dns
+ peers:
+ description: The list of VPN peers.
+ returned: success
+ type: complex
+ contains:
+ ike_version:
+ description: The IKE version to be used for the IPsec VPN peer configuration.
+ returned: success
+ type: str
+ sample: "1"
+ ipsec_policies_preset:
+ description: Preconfigured IPsec settings.
+ returned: success
+ type: str
+ sample: "aws"
+ name:
+ description: The name of the VPN peer.
+ returned: success
+ type: str
+ sample: "MyVPNPeer"
+ public_ip:
+ description: The public IP of the VPN peer.
+ returned: success
+ type: str
+ sample: "198.51.100.1"
+ remote_id:
+ description: "The remote ID is used to identify the connecting VPN peer."
+ returned: success
+ type: str
+ sample: "s3cret"
+ network_tags:
+ description: A list of network tags that will connect with this peer.
+ returned: success
+ type: list
+ sample: ["all"]
+ private_subnets:
+ description: The list of the private subnets of the VPN peer.
+ returned: success
+ type: list
+ sample: ["192.0.2.0/24"]
+ ipsec_policies:
+ description: Custom IPSec policies for the VPN peer.
+ returned: success
+ type: complex
+ contains:
+ child_lifetime:
+ description: The lifetime of the Phase 2 SA in seconds.
+ returned: success
+ type: str
+ sample: "60"
+ ike_lifetime:
+ description: The lifetime of the Phase 1 SA in seconds.
+ returned: success
+ type: str
+ sample: "60"
+ child_auth_algo:
+ description: This is the authentication algorithms to be used in Phase 2.
+ returned: success
+ type: list
+ sample: ["sha1"]
+ child_cipher_algo:
+ description: This is the cipher algorithms to be used in Phase 2.
+ returned: success
+ type: list
+ sample: ["aes192"]
+ child_pfs_group:
+ description: This is the Diffie-Hellman group to be used for Perfect Forward Secrecy in Phase 2.
+ returned: success
+ type: list
+ sample: ["group14"]
+ ike_auth_algo:
+ description: This is the authentication algorithm to be used in Phase 1.
+ returned: success
+ type: list
+ sample: ["sha1"]
+ ike_cipher_algo:
+ description: This is the cipher algorithm to be used in Phase 1.
+ returned: success
+ type: list
+ sample: ["aes128"]
+ ike_diffie_hellman_group:
+ description: This is the Diffie-Hellman group to be used in Phase 1.
+ returned: success
+ type: list
+ sample: ["group14"]
+ ike_prf_algo:
+ description: This is the pseudo-random function to be used in IKE_SA.
+ returned: success
+ type: list
+ sample: ["prfmd5"]
+"""
+
+from ansible.module_utils.basic import AnsibleModule
+from ansible_collections.cisco.meraki.plugins.module_utils.network.meraki.meraki import (
+ MerakiModule,
+ meraki_argument_spec,
+)
+import json
+
+
+def validate_payload(meraki):
+ for peer in meraki.params["peers"]:
+ if peer["name"] is None:
+ meraki.fail_json(msg="Peer name must be specified")
+ elif peer["public_ip"] is None:
+ meraki.fail_json(msg="Peer public IP must be specified")
+ elif peer["secret"] is None:
+ meraki.fail_json(msg="Peer secret must be specified")
+ elif peer["private_subnets"] is None:
+ meraki.fail_json(msg="Peer private subnets must be specified")
+
+
+def construct_payload(meraki):
+ validate_payload(meraki)
+ peer_list = []
+ for peer in meraki.params["peers"]:
+ current_peer = dict()
+ current_peer["name"] = peer["name"]
+ current_peer["publicIp"] = peer["public_ip"]
+ current_peer["secret"] = peer["secret"]
+ current_peer["privateSubnets"] = peer["private_subnets"]
+ if peer["ike_version"] is not None:
+ current_peer["ikeVersion"] = peer["ike_version"]
+ if peer["ipsec_policies_preset"] is not None:
+ current_peer["ipsecPoliciesPreset"] = peer["ipsec_policies_preset"]
+ if peer["remote_id"] is not None:
+ current_peer["remoteId"] = peer["remote_id"]
+ if peer["network_tags"] is not None:
+ current_peer["networkTags"] = peer["network_tags"]
+ if peer["ipsec_policies"] is not None:
+ current_peer["ipsecPolicies"] = dict()
+ if peer["ipsec_policies"]["child_lifetime"] is not None:
+ current_peer["ipsecPolicies"]["childLifetime"] = peer["ipsec_policies"][
+ "child_lifetime"
+ ]
+ if peer["ipsec_policies"]["ike_lifetime"] is not None:
+ current_peer["ipsecPolicies"]["ikeLifetime"] = peer["ipsec_policies"][
+ "ike_lifetime"
+ ]
+ if peer["ipsec_policies"]["child_auth_algo"] is not None:
+ current_peer["ipsecPolicies"]["childAuthAlgo"] = peer["ipsec_policies"][
+ "child_auth_algo"
+ ]
+ if peer["ipsec_policies"]["child_cipher_algo"] is not None:
+ current_peer["ipsecPolicies"]["childCipherAlgo"] = peer[
+ "ipsec_policies"
+ ]["child_cipher_algo"]
+ if peer["ipsec_policies"]["child_pfs_group"] is not None:
+ current_peer["ipsecPolicies"]["childPfsGroup"] = peer["ipsec_policies"][
+ "child_pfs_group"
+ ]
+ if peer["ipsec_policies"]["ike_auth_algo"] is not None:
+ current_peer["ipsecPolicies"]["ikeAuthAlgo"] = peer["ipsec_policies"][
+ "ike_auth_algo"
+ ]
+ if peer["ipsec_policies"]["ike_cipher_algo"] is not None:
+ current_peer["ipsecPolicies"]["ikeCipherAlgo"] = peer["ipsec_policies"][
+ "ike_cipher_algo"
+ ]
+ if peer["ipsec_policies"]["ike_diffie_hellman_group"] is not None:
+ current_peer["ipsecPolicies"]["ikeDiffieHellmanGroup"] = peer[
+ "ipsec_policies"
+ ]["ike_diffie_hellman_group"]
+ if peer["ipsec_policies"]["ike_prf_algo"] is not None:
+ current_peer["ipsecPolicies"]["ikePrfAlgo"] = peer["ipsec_policies"][
+ "ike_prf_algo"
+ ]
+
+ peer_list.append(current_peer)
+ payload = {"peers": peer_list}
+ return payload
+
+
+def main():
+ # define the available arguments/parameters that a user can pass to
+ # the module
+
+ ipsec_policies_arg_spec = dict(
+ child_lifetime=dict(type="int", default=None),
+ ike_lifetime=dict(type="int", default=None),
+ child_auth_algo=dict(
+ type="list", elements="str", default=None, choices=["sha256", "sha1", "md5"]
+ ),
+ child_cipher_algo=dict(
+ type="list",
+ elements="str",
+ default=None,
+ choices=["aes256", "aes192", "aes128", "tripledes", "des", "null"],
+ ),
+ child_pfs_group=dict(
+ type="list",
+ elements="str",
+ default=None,
+ choices=["disabled", "group14", "group5", "group2", "group1"],
+ ),
+ ike_auth_algo=dict(
+ type="list", elements="str", default=None, choices=["sha256", "sha1", "md5"]
+ ),
+ ike_cipher_algo=dict(
+ type="list",
+ elements="str",
+ default=None,
+ choices=["aes256", "aes192", "aes128", "tripledes", "des"],
+ ),
+ ike_diffie_hellman_group=dict(
+ type="list",
+ elements="str",
+ default=None,
+ choices=["group14", "group5", "group2", "group1"],
+ ),
+ ike_prf_algo=dict(
+ type="list",
+ elements="str",
+ default=None,
+ choices=["prfsha256", "prfsha1", "prfmd5", "default"],
+ ),
+ )
+
+ peers_arg_spec = dict(
+ name=dict(type="str"),
+ public_ip=dict(type="str"),
+ secret=dict(type="str", no_log=True),
+ private_subnets=dict(type="list", elements="str"),
+ ike_version=dict(type="str", choices=["1", "2"], default="1"),
+ ipsec_policies_preset=dict(
+ type="str", choices=["default", "aws", "azure"], default=None
+ ),
+ remote_id=dict(type="str", default=None),
+ network_tags=dict(type="list", elements="str", default=None),
+ ipsec_policies=dict(type="dict", options=ipsec_policies_arg_spec, default=None),
+ )
+
+ argument_spec = meraki_argument_spec()
+ argument_spec.update(
+ state=dict(type="str", choices=["absent", "present", "query"], default="query"),
+ peers=dict(type="list", elements="dict", options=peers_arg_spec),
+ )
+
+ # the AnsibleModule object will be our abstraction working with Ansible
+ # this includes instantiation, a couple of common attr would be the
+ # args/params passed to the execution, as well as if the module
+ # supports check mode
+ module = AnsibleModule(
+ argument_spec=argument_spec,
+ supports_check_mode=True,
+ )
+ meraki = MerakiModule(module, function="third_party_vpn_peer")
+
+ meraki.params["follow_redirects"] = "all"
+
+ query_urls = {
+ "third_party_vpn_peer": "/organizations/{org_id}/appliance/vpn/thirdPartyVPNPeers"
+ }
+ update_url = {
+ "third_party_vpn_peer": "/organizations/{org_id}/appliance/vpn/thirdPartyVPNPeers"
+ }
+
+ meraki.url_catalog["get_all"].update(query_urls)
+ meraki.url_catalog["update"] = update_url
+
+ payload = None
+ if meraki.params["org_id"] is None and meraki.params["org_name"] is None:
+ meraki.fail_json(msg="Organization must be specified via org_name or org_id")
+
+ org_id = meraki.params["org_id"]
+ if org_id is None:
+ org_id = meraki.get_org_id(meraki.params["org_name"])
+
+ if meraki.params["state"] == "query":
+ path = meraki.construct_path("get_all", org_id=org_id)
+ response = meraki.request(path, "GET")
+ meraki.result["data"] = response
+ elif meraki.params["state"] == "present":
+ payload = construct_payload(meraki)
+ have = meraki.request(meraki.construct_path("get_all", org_id=org_id), "GET")
+ # meraki.fail_json(msg="Compare", have=have, payload=payload)
+ if meraki.is_update_required(have, payload):
+ meraki.generate_diff(have, payload)
+ path = meraki.construct_path("update", org_id=org_id)
+ if meraki.module.check_mode is False:
+ response = meraki.request(path, "PUT", payload=json.dumps(payload))
+ meraki.result["data"] = response
+ else:
+ meraki.result["data"] = payload
+ meraki.result["changed"] = True
+ meraki.exit_json(**meraki.result)
+ meraki.result["data"] = have
+ elif meraki.params["state"] == "absent":
+ return
+
+ # in the event of a successful module execution, you will want to
+ # simple AnsibleModule.exit_json(), passing the key/value results
+ meraki.exit_json(**meraki.result)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/ansible_collections/cisco/meraki/plugins/modules/meraki_mx_uplink_bandwidth.py b/ansible_collections/cisco/meraki/plugins/modules/meraki_mx_uplink_bandwidth.py
new file mode 100644
index 000000000..cb2ef07e5
--- /dev/null
+++ b/ansible_collections/cisco/meraki/plugins/modules/meraki_mx_uplink_bandwidth.py
@@ -0,0 +1,325 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Copyright: (c) 2019, Kevin Breit (@kbreit) <kevin.breit@kevinbreit.net>
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+ANSIBLE_METADATA = {
+ 'metadata_version': '1.1',
+ 'status': ['preview'],
+ 'supported_by': 'community'
+}
+
+DOCUMENTATION = r'''
+---
+module: meraki_mx_uplink_bandwidth
+short_description: Manage uplinks on Meraki MX appliances
+version_added: "1.1.0"
+description:
+- Configure and query information about uplinks on Meraki MX appliances.
+notes:
+- Some of the options are likely only used for developers within Meraki.
+- Module was formerly named M(cisco.meraki.meraki_mx_uplink).
+options:
+ state:
+ description:
+ - Specifies whether object should be queried, created/modified, or removed.
+ choices: [absent, present, query]
+ default: query
+ type: str
+ org_name:
+ description:
+ - Name of organization associated to a network.
+ type: str
+ org_id:
+ description:
+ - ID of organization associated to a network.
+ type: str
+ net_name:
+ description:
+ - Name of network which VLAN is in or should be in.
+ aliases: [network]
+ type: str
+ net_id:
+ description:
+ - ID of network which VLAN is in or should be in.
+ type: str
+ wan1:
+ description:
+ - Configuration of WAN1 uplink
+ type: dict
+ suboptions:
+ bandwidth_limits:
+ description:
+ - Structure for configuring bandwidth limits
+ type: dict
+ suboptions:
+ limit_up:
+ description:
+ - Maximum upload speed for interface
+ type: int
+ limit_down:
+ description:
+ - Maximum download speed for interface
+ type: int
+ wan2:
+ description:
+ - Configuration of WAN2 uplink
+ type: dict
+ suboptions:
+ bandwidth_limits:
+ description:
+ - Structure for configuring bandwidth limits
+ type: dict
+ suboptions:
+ limit_up:
+ description:
+ - Maximum upload speed for interface
+ type: int
+ limit_down:
+ description:
+ - Maximum download speed for interface
+ type: int
+ cellular:
+ description:
+ - Configuration of cellular uplink
+ type: dict
+ suboptions:
+ bandwidth_limits:
+ description:
+ - Structure for configuring bandwidth limits
+ type: dict
+ suboptions:
+ limit_up:
+ description:
+ - Maximum upload speed for interface
+ type: int
+ limit_down:
+ description:
+ - Maximum download speed for interface
+ type: int
+author:
+- Kevin Breit (@kbreit)
+extends_documentation_fragment: cisco.meraki.meraki
+'''
+
+EXAMPLES = r'''
+- name: Set MX uplink settings
+ meraki_mx_uplink_bandwidth:
+ auth_key: '{{auth_key}}'
+ state: present
+ org_name: '{{test_org_name}}'
+ net_name: '{{test_net_name}} - Uplink'
+ wan1:
+ bandwidth_limits:
+ limit_down: 1000000
+ limit_up: 1000
+ cellular:
+ bandwidth_limits:
+ limit_down: 0
+ limit_up: 0
+ delegate_to: localhost
+
+- name: Query MX uplink settings
+ meraki_mx_uplink_bandwidth:
+ auth_key: '{{auth_key}}'
+ state: query
+ org_name: '{{test_org_name}}'
+ net_name: '{{test_net_name}} - Uplink'
+ delegate_to: localhost
+
+'''
+
+RETURN = r'''
+
+data:
+ description: Information about the organization which was created or modified
+ returned: success
+ type: complex
+ contains:
+ wan1:
+ description: WAN1 interface
+ returned: success
+ type: complex
+ contains:
+ bandwidth_limits:
+ description: Structure for uplink bandwidth limits
+ returned: success
+ type: complex
+ contains:
+ limit_up:
+ description: Upload bandwidth limit
+ returned: success
+ type: int
+ limit_down:
+ description: Download bandwidth limit
+ returned: success
+ type: int
+ wan2:
+ description: WAN2 interface
+ returned: success
+ type: complex
+ contains:
+ bandwidth_limits:
+ description: Structure for uplink bandwidth limits
+ returned: success
+ type: complex
+ contains:
+ limit_up:
+ description: Upload bandwidth limit
+ returned: success
+ type: int
+ limit_down:
+ description: Download bandwidth limit
+ returned: success
+ type: int
+ cellular:
+ description: cellular interface
+ returned: success
+ type: complex
+ contains:
+ bandwidth_limits:
+ description: Structure for uplink bandwidth limits
+ returned: success
+ type: complex
+ contains:
+ limit_up:
+ description: Upload bandwidth limit
+ returned: success
+ type: int
+ limit_down:
+ description: Download bandwidth limit
+ returned: success
+ type: int
+'''
+
+from ansible.module_utils.basic import AnsibleModule, json
+from ansible.module_utils.common.dict_transformations import recursive_diff
+from ansible_collections.cisco.meraki.plugins.module_utils.network.meraki.meraki import MerakiModule, meraki_argument_spec
+
+INT_NAMES = ('wan1', 'wan2', 'cellular')
+
+
+def clean_custom_format(data):
+ for interface in data:
+ if data[interface]['bandwidth_limits']['limit_up'] is None:
+ data[interface]['bandwidth_limits']['limit_up'] = 0
+ if data[interface]['bandwidth_limits']['limit_down'] is None:
+ data[interface]['bandwidth_limits']['limit_down'] = 0
+ return data
+
+
+def meraki_struct_to_custom_format(data):
+ new_struct = {}
+ for interface in INT_NAMES:
+ if interface in data['bandwidthLimits']:
+ new_struct[interface] = {'bandwidth_limits': {'limit_up': data['bandwidthLimits'][interface]['limitUp'],
+ 'limit_down': data['bandwidthLimits'][interface]['limitDown'],
+ }
+ }
+ # return snake_dict_to_camel_dict(new_struct)
+ return new_struct
+
+
+def main():
+ # define the available arguments/parameters that a user can pass to
+ # the module
+
+ bandwidth_arg_spec = dict(limit_up=dict(type='int'),
+ limit_down=dict(type='int'),
+ )
+
+ interface_arg_spec = dict(bandwidth_limits=dict(type='dict', default=None, options=bandwidth_arg_spec),
+ )
+
+ argument_spec = meraki_argument_spec()
+ argument_spec.update(state=dict(type='str', choices=['absent', 'present', 'query'], default='query'),
+ net_name=dict(type='str', aliases=['network']),
+ net_id=dict(type='str'),
+ wan1=dict(type='dict', default=None, options=interface_arg_spec),
+ wan2=dict(type='dict', default=None, options=interface_arg_spec),
+ cellular=dict(type='dict', default=None, options=interface_arg_spec),
+ )
+
+ # the AnsibleModule object will be our abstraction working with Ansible
+ # this includes instantiation, a couple of common attr would be the
+ # args/params passed to the execution, as well as if the module
+ # supports check mode
+ module = AnsibleModule(argument_spec=argument_spec,
+ supports_check_mode=True,
+ )
+ meraki = MerakiModule(module, function='mx_uplink')
+
+ meraki.params['follow_redirects'] = 'all'
+
+ query_urls = {'mx_uplink': '/networks/{net_id}/appliance/trafficShaping/uplinkBandwidth'}
+ update_bw_url = {'mx_uplink': '/networks/{net_id}/appliance/trafficShaping/uplinkBandwidth'}
+
+ meraki.url_catalog['get_all'].update(query_urls)
+ meraki.url_catalog['update_bw'] = update_bw_url
+
+ payload = dict()
+
+ org_id = meraki.params['org_id']
+ if org_id is None:
+ org_id = meraki.get_org_id(meraki.params['org_name'])
+ net_id = meraki.params['net_id']
+ if net_id is None:
+ nets = meraki.get_nets(org_id=org_id)
+ net_id = meraki.get_net_id(net_name=meraki.params['net_name'], data=nets)
+
+ if meraki.params['state'] == 'query':
+ path = meraki.construct_path('get_all', net_id=net_id)
+ response = meraki.request(path, method='GET')
+ data = clean_custom_format(meraki_struct_to_custom_format(response))
+ meraki.result['data'] = data
+ elif meraki.params['state'] == 'present':
+ path = meraki.construct_path('get_all', net_id=net_id)
+ original = meraki.request(path, method='GET')
+ payload = {'bandwidthLimits': {}}
+ for interface in INT_NAMES:
+ if meraki.params[interface] is not None:
+ if meraki.params[interface]['bandwidth_limits'] is not None:
+ payload['bandwidthLimits'][interface] = None
+ payload['bandwidthLimits'][interface] = {'limitUp': meraki.params[interface]['bandwidth_limits']['limit_up'],
+ 'limitDown': meraki.params[interface]['bandwidth_limits']['limit_down'],
+ }
+ if payload['bandwidthLimits'][interface]['limitUp'] == 0:
+ payload['bandwidthLimits'][interface]['limitUp'] = None
+ if payload['bandwidthLimits'][interface]['limitDown'] == 0:
+ payload['bandwidthLimits'][interface]['limitDown'] = None
+ if meraki.is_update_required(original, payload):
+ if meraki.module.check_mode is True:
+ diff = recursive_diff(clean_custom_format(meraki_struct_to_custom_format(original)),
+ clean_custom_format(meraki_struct_to_custom_format(payload)))
+ original.update(payload)
+ meraki.result['data'] = clean_custom_format(meraki_struct_to_custom_format(original))
+ meraki.result['diff'] = {'before': diff[0],
+ 'after': diff[1],
+ }
+ meraki.result['changed'] = True
+ meraki.exit_json(**meraki.result)
+ path = meraki.construct_path('update_bw', net_id=net_id)
+ response = meraki.request(path, method='PUT', payload=json.dumps(payload))
+ if meraki.status == 200:
+ formatted_original = clean_custom_format(meraki_struct_to_custom_format(original))
+ formatted_response = clean_custom_format(meraki_struct_to_custom_format(response))
+ diff = recursive_diff(formatted_original, formatted_response)
+ meraki.result['diff'] = {'before': diff[0],
+ 'after': diff[1],
+ }
+ meraki.result['data'] = formatted_response
+ meraki.result['changed'] = True
+ else:
+ meraki.result['data'] = clean_custom_format(meraki_struct_to_custom_format(original))
+
+ # in the event of a successful module execution, you will want to
+ # simple AnsibleModule.exit_json(), passing the key/value results
+ meraki.exit_json(**meraki.result)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/ansible_collections/cisco/meraki/plugins/modules/meraki_mx_vlan.py b/ansible_collections/cisco/meraki/plugins/modules/meraki_mx_vlan.py
new file mode 100644
index 000000000..2b6c8adc6
--- /dev/null
+++ b/ansible_collections/cisco/meraki/plugins/modules/meraki_mx_vlan.py
@@ -0,0 +1,585 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Copyright: (c) 2018, Kevin Breit (@kbreit) <kevin.breit@kevinbreit.net>
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+ANSIBLE_METADATA = {
+ 'metadata_version': '1.1',
+ 'status': ['preview'],
+ 'supported_by': 'community'
+}
+
+DOCUMENTATION = r'''
+---
+module: meraki_mx_vlan
+short_description: Manage VLANs in the Meraki cloud
+description:
+- Create, edit, query, or delete VLANs in a Meraki environment.
+notes:
+- Meraki's API will return an error if VLANs aren't enabled on a network. VLANs are returned properly if VLANs are enabled on a network.
+- Some of the options are likely only used for developers within Meraki.
+- Meraki's API defaults to networks having VLAN support disabled and there is no way to enable VLANs support in the API. VLAN support must be enabled manually.
+options:
+ state:
+ description:
+ - Specifies whether object should be queried, created/modified, or removed.
+ choices: [absent, present, query]
+ default: query
+ type: str
+ net_name:
+ description:
+ - Name of network which VLAN is in or should be in.
+ aliases: [network]
+ type: str
+ net_id:
+ description:
+ - ID of network which VLAN is in or should be in.
+ type: str
+ vlan_id:
+ description:
+ - ID number of VLAN.
+ - ID should be between 1-4096.
+ type: int
+ name:
+ description:
+ - Name of VLAN.
+ aliases: [vlan_name]
+ type: str
+ subnet:
+ description:
+ - CIDR notation of network subnet.
+ type: str
+ appliance_ip:
+ description:
+ - IP address of appliance.
+ - Address must be within subnet specified in C(subnet) parameter.
+ type: str
+ dns_nameservers:
+ description:
+ - Semi-colon delimited list of DNS IP addresses.
+ - Specify one of the following options for preprogrammed DNS entries opendns, google_dns, upstream_dns
+ type: str
+ reserved_ip_range:
+ description:
+ - IP address ranges which should be reserve and not distributed via DHCP.
+ type: list
+ elements: dict
+ suboptions:
+ start:
+ description: First IP address of reserved IP address range, inclusive.
+ type: str
+ end:
+ description: Last IP address of reserved IP address range, inclusive.
+ type: str
+ comment:
+ description: Description of IP addresses reservation
+ type: str
+ vpn_nat_subnet:
+ description:
+ - The translated VPN subnet if VPN and VPN subnet translation are enabled on the VLAN.
+ type: str
+ fixed_ip_assignments:
+ description:
+ - Static IP address assignments to be distributed via DHCP by MAC address.
+ type: list
+ elements: dict
+ suboptions:
+ mac:
+ description: MAC address for fixed IP assignment binding.
+ type: str
+ ip:
+ description: IP address for fixed IP assignment binding.
+ type: str
+ name:
+ description: Descriptive name of IP assignment binding.
+ type: str
+ dhcp_handling:
+ description:
+ - How to handle DHCP packets on network.
+ type: str
+ choices: ['Run a DHCP server',
+ 'Relay DHCP to another server',
+ 'Do not respond to DHCP requests',
+ 'none',
+ 'server',
+ 'relay']
+ dhcp_relay_server_ips:
+ description:
+ - IP addresses to forward DHCP packets to.
+ type: list
+ elements: str
+ dhcp_lease_time:
+ description:
+ - DHCP lease timer setting
+ type: str
+ choices: ['30 minutes',
+ '1 hour',
+ '4 hours',
+ '12 hours',
+ '1 day',
+ '1 week']
+ dhcp_boot_options_enabled:
+ description:
+ - Enable DHCP boot options
+ type: bool
+ dhcp_boot_next_server:
+ description:
+ - DHCP boot option to direct boot clients to the server to load boot file from.
+ type: str
+ dhcp_boot_filename:
+ description:
+ - Filename to boot from for DHCP boot
+ type: str
+ dhcp_options:
+ description:
+ - List of DHCP option values
+ type: list
+ elements: dict
+ suboptions:
+ code:
+ description:
+ - DHCP option number.
+ type: int
+ type:
+ description:
+ - Type of value for DHCP option.
+ type: str
+ choices: ['text', 'ip', 'hex', 'integer']
+ value:
+ description:
+ - Value for DHCP option.
+ type: str
+author:
+- Kevin Breit (@kbreit)
+extends_documentation_fragment: cisco.meraki.meraki
+'''
+
+EXAMPLES = r'''
+- name: Query all VLANs in a network.
+ meraki_vlan:
+ auth_key: abc12345
+ org_name: YourOrg
+ net_name: YourNet
+ state: query
+ delegate_to: localhost
+
+- name: Query information about a single VLAN by ID.
+ meraki_vlan:
+ auth_key: abc12345
+ org_name: YourOrg
+ net_name: YourNet
+ vlan_id: 2
+ state: query
+ delegate_to: localhost
+
+- name: Create a VLAN.
+ meraki_vlan:
+ auth_key: abc12345
+ org_name: YourOrg
+ net_name: YourNet
+ state: present
+ vlan_id: 2
+ name: TestVLAN
+ subnet: 192.0.1.0/24
+ appliance_ip: 192.0.1.1
+ delegate_to: localhost
+
+- name: Update a VLAN.
+ meraki_vlan:
+ auth_key: abc12345
+ org_name: YourOrg
+ net_name: YourNet
+ state: present
+ vlan_id: 2
+ name: TestVLAN
+ subnet: 192.0.1.0/24
+ appliance_ip: 192.168.250.2
+ fixed_ip_assignments:
+ - mac: "13:37:de:ad:be:ef"
+ ip: 192.168.250.10
+ name: fixed_ip
+ reserved_ip_range:
+ - start: 192.168.250.10
+ end: 192.168.250.20
+ comment: reserved_range
+ dns_nameservers: opendns
+ delegate_to: localhost
+
+- name: Enable DHCP on VLAN with options
+ meraki_vlan:
+ auth_key: abc123
+ state: present
+ org_name: YourOrg
+ net_name: YourNet
+ vlan_id: 2
+ name: TestVLAN
+ subnet: 192.168.250.0/24
+ appliance_ip: 192.168.250.2
+ dhcp_handling: server
+ dhcp_lease_time: 1 hour
+ dhcp_boot_options_enabled: false
+ dhcp_options:
+ - code: 5
+ type: ip
+ value: 192.0.1.1
+ delegate_to: localhost
+
+- name: Delete a VLAN.
+ meraki_vlan:
+ auth_key: abc12345
+ org_name: YourOrg
+ net_name: YourNet
+ state: absent
+ vlan_id: 2
+ delegate_to: localhost
+'''
+
+RETURN = r'''
+
+response:
+ description: Information about the organization which was created or modified
+ returned: success
+ type: complex
+ contains:
+ appliance_ip:
+ description: IP address of Meraki appliance in the VLAN
+ returned: success
+ type: str
+ sample: 192.0.1.1
+ dnsnamservers:
+ description: IP address or Meraki defined DNS servers which VLAN should use by default
+ returned: success
+ type: str
+ sample: upstream_dns
+ fixed_ip_assignments:
+ description: List of MAC addresses which have IP addresses assigned.
+ returned: success
+ type: complex
+ contains:
+ macaddress:
+ description: MAC address which has IP address assigned to it. Key value is the actual MAC address.
+ returned: success
+ type: complex
+ contains:
+ ip:
+ description: IP address which is assigned to the MAC address.
+ returned: success
+ type: str
+ sample: 192.0.1.4
+ name:
+ description: Descriptive name for binding.
+ returned: success
+ type: str
+ sample: fixed_ip
+ reserved_ip_ranges:
+ description: List of IP address ranges which are reserved for static assignment.
+ returned: success
+ type: complex
+ contains:
+ comment:
+ description: Description for IP address reservation.
+ returned: success
+ type: str
+ sample: reserved_range
+ end:
+ description: Last IP address in reservation range.
+ returned: success
+ type: str
+ sample: 192.0.1.10
+ start:
+ description: First IP address in reservation range.
+ returned: success
+ type: str
+ sample: 192.0.1.5
+ id:
+ description: VLAN ID number.
+ returned: success
+ type: int
+ sample: 2
+ name:
+ description: Descriptive name of VLAN.
+ returned: success
+ type: str
+ sample: TestVLAN
+ networkId:
+ description: ID number of Meraki network which VLAN is associated to.
+ returned: success
+ type: str
+ sample: N_12345
+ subnet:
+ description: CIDR notation IP subnet of VLAN.
+ returned: success
+ type: str
+ sample: "192.0.1.0/24"
+ dhcp_handling:
+ description: Status of DHCP server on VLAN.
+ returned: success
+ type: str
+ sample: Run a DHCP server
+ dhcp_lease_time:
+ description: DHCP lease time when server is active.
+ returned: success
+ type: str
+ sample: 1 day
+ dhcp_boot_options_enabled:
+ description: Whether DHCP boot options are enabled.
+ returned: success
+ type: bool
+ sample: no
+ dhcp_boot_next_server:
+ description: DHCP boot option to direct boot clients to the server to load the boot file from.
+ returned: success
+ type: str
+ sample: 192.0.1.2
+ dhcp_boot_filename:
+ description: Filename for boot file.
+ returned: success
+ type: str
+ sample: boot.txt
+ dhcp_options:
+ description: DHCP options.
+ returned: success
+ type: complex
+ contains:
+ code:
+ description:
+ - Code for DHCP option.
+ - Integer between 2 and 254.
+ returned: success
+ type: int
+ sample: 43
+ type:
+ description:
+ - Type for DHCP option.
+ - Choices are C(text), C(ip), C(hex), C(integer).
+ returned: success
+ type: str
+ sample: text
+ value:
+ description: Value for the DHCP option.
+ returned: success
+ type: str
+ sample: 192.0.1.2
+'''
+
+from ansible.module_utils.basic import AnsibleModule
+from ansible_collections.cisco.meraki.plugins.module_utils.network.meraki.meraki import MerakiModule, meraki_argument_spec
+import json
+
+
+def fixed_ip_factory(meraki, data):
+ fixed_ips = dict()
+ for item in data:
+ fixed_ips[item['mac']] = {'ip': item['ip'], 'name': item['name']}
+ return fixed_ips
+
+
+def get_vlans(meraki, net_id):
+ path = meraki.construct_path('get_all', net_id=net_id)
+ return meraki.request(path, method='GET')
+
+
+# TODO: Allow method to return actual item if True to reduce number of calls needed
+def is_vlan_valid(meraki, net_id, vlan_id):
+ vlans = get_vlans(meraki, net_id)
+ for vlan in vlans:
+ if vlan_id == vlan['id']:
+ return True
+ return False
+
+
+def construct_payload(meraki):
+ payload = {'id': meraki.params['vlan_id'],
+ 'name': meraki.params['name'],
+ 'subnet': meraki.params['subnet'],
+ 'applianceIp': meraki.params['appliance_ip'],
+ }
+ if meraki.params['dns_nameservers']:
+ if meraki.params['dns_nameservers'] not in ('opendns', 'google_dns', 'upstream_dns'):
+ payload['dnsNameservers'] = format_dns(meraki.params['dns_nameservers'])
+ else:
+ payload['dnsNameservers'] = meraki.params['dns_nameservers']
+ if meraki.params['fixed_ip_assignments']:
+ payload['fixedIpAssignments'] = fixed_ip_factory(meraki, meraki.params['fixed_ip_assignments'])
+ if meraki.params['reserved_ip_range']:
+ payload['reservedIpRanges'] = meraki.params['reserved_ip_range']
+ if meraki.params['vpn_nat_subnet']:
+ payload['vpnNatSubnet'] = meraki.params['vpn_nat_subnet']
+ if meraki.params['dhcp_handling']:
+ payload['dhcpHandling'] = normalize_dhcp_handling(meraki.params['dhcp_handling'])
+ if meraki.params['dhcp_relay_server_ips']:
+ payload['dhcpRelayServerIps'] = meraki.params['dhcp_relay_server_ips']
+ if meraki.params['dhcp_lease_time']:
+ payload['dhcpLeaseTime'] = meraki.params['dhcp_lease_time']
+ if meraki.params['dhcp_boot_options_enabled']:
+ payload['dhcpBootOptionsEnabled'] = meraki.params['dhcp_boot_options_enabled']
+ if meraki.params['dhcp_boot_next_server']:
+ payload['dhcpBootNextServer'] = meraki.params['dhcp_boot_next_server']
+ if meraki.params['dhcp_boot_filename']:
+ payload['dhcpBootFilename'] = meraki.params['dhcp_boot_filename']
+ if meraki.params['dhcp_options']:
+ payload['dhcpOptions'] = meraki.params['dhcp_options']
+ # if meraki.params['dhcp_handling']:
+ # meraki.fail_json(payload)
+
+ return payload
+
+
+def format_dns(nameservers):
+ return nameservers.replace(';', '\n')
+
+
+def normalize_dhcp_handling(parameter):
+ if parameter == 'none':
+ return 'Do not respond to DHCP requests'
+ elif parameter == 'server':
+ return 'Run a DHCP server'
+ elif parameter == 'relay':
+ return 'Relay DHCP to another server'
+
+
+def main():
+ # define the available arguments/parameters that a user can pass to
+ # the module
+
+ fixed_ip_arg_spec = dict(mac=dict(type='str'),
+ ip=dict(type='str'),
+ name=dict(type='str'),
+ )
+
+ reserved_ip_arg_spec = dict(start=dict(type='str'),
+ end=dict(type='str'),
+ comment=dict(type='str'),
+ )
+
+ dhcp_options_arg_spec = dict(code=dict(type='int'),
+ type=dict(type='str', choices=['text', 'ip', 'hex', 'integer']),
+ value=dict(type='str'),
+ )
+
+ argument_spec = meraki_argument_spec()
+ argument_spec.update(state=dict(type='str', choices=['absent', 'present', 'query'], default='query'),
+ net_name=dict(type='str', aliases=['network']),
+ net_id=dict(type='str'),
+ vlan_id=dict(type='int'),
+ name=dict(type='str', aliases=['vlan_name']),
+ subnet=dict(type='str'),
+ appliance_ip=dict(type='str'),
+ fixed_ip_assignments=dict(type='list', default=None, elements='dict', options=fixed_ip_arg_spec),
+ reserved_ip_range=dict(type='list', default=None, elements='dict', options=reserved_ip_arg_spec),
+ vpn_nat_subnet=dict(type='str'),
+ dns_nameservers=dict(type='str'),
+ dhcp_handling=dict(type='str', choices=['Run a DHCP server',
+ 'Relay DHCP to another server',
+ 'Do not respond to DHCP requests',
+ 'none',
+ 'server',
+ 'relay'],
+ ),
+ dhcp_relay_server_ips=dict(type='list', default=None, elements='str'),
+ dhcp_lease_time=dict(type='str', choices=['30 minutes',
+ '1 hour',
+ '4 hours',
+ '12 hours',
+ '1 day',
+ '1 week']),
+ dhcp_boot_options_enabled=dict(type='bool'),
+ dhcp_boot_next_server=dict(type='str'),
+ dhcp_boot_filename=dict(type='str'),
+ dhcp_options=dict(type='list', default=None, elements='dict', options=dhcp_options_arg_spec),
+ )
+
+ # the AnsibleModule object will be our abstraction working with Ansible
+ # this includes instantiation, a couple of common attr would be the
+ # args/params passed to the execution, as well as if the module
+ # supports check mode
+ module = AnsibleModule(argument_spec=argument_spec,
+ supports_check_mode=True,
+ )
+ meraki = MerakiModule(module, function='vlan')
+
+ meraki.params['follow_redirects'] = 'all'
+
+ query_urls = {'vlan': '/networks/{net_id}/appliance/vlans'}
+ query_url = {'vlan': '/networks/{net_id}/appliance/vlans/{vlan_id}'}
+ create_url = {'vlan': '/networks/{net_id}/appliance/vlans'}
+ update_url = {'vlan': '/networks/{net_id}/appliance/vlans/'}
+ delete_url = {'vlan': '/networks/{net_id}/appliance/vlans/'}
+
+ meraki.url_catalog['get_all'].update(query_urls)
+ meraki.url_catalog['get_one'].update(query_url)
+ meraki.url_catalog['create'] = create_url
+ meraki.url_catalog['update'] = update_url
+ meraki.url_catalog['delete'] = delete_url
+
+ payload = None
+
+ org_id = meraki.params['org_id']
+ if org_id is None:
+ org_id = meraki.get_org_id(meraki.params['org_name'])
+ net_id = meraki.params['net_id']
+ if net_id is None:
+ nets = meraki.get_nets(org_id=org_id)
+ net_id = meraki.get_net_id(net_name=meraki.params['net_name'], data=nets)
+
+ if meraki.params['state'] == 'query':
+ if not meraki.params['vlan_id']:
+ meraki.result['data'] = get_vlans(meraki, net_id)
+ else:
+ path = meraki.construct_path('get_one', net_id=net_id, custom={'vlan_id': meraki.params['vlan_id']})
+ response = meraki.request(path, method='GET')
+ meraki.result['data'] = response
+ elif meraki.params['state'] == 'present':
+ payload = construct_payload(meraki)
+ if is_vlan_valid(meraki, net_id, meraki.params['vlan_id']) is False: # Create new VLAN
+ if meraki.module.check_mode is True:
+ meraki.result['data'] = payload
+ meraki.result['changed'] = True
+ meraki.exit_json(**meraki.result)
+ path = meraki.construct_path('create', net_id=net_id)
+ response = meraki.request(path, method='POST', payload=json.dumps(payload))
+ meraki.result['changed'] = True
+ meraki.result['data'] = response
+ else: # Update existing VLAN
+ path = meraki.construct_path('get_one', net_id=net_id, custom={'vlan_id': meraki.params['vlan_id']})
+ original = meraki.request(path, method='GET')
+ ignored = ['networkId']
+ if meraki.is_update_required(original, payload, optional_ignore=ignored):
+ meraki.generate_diff(original, payload)
+ if meraki.module.check_mode is True:
+ original.update(payload)
+ meraki.result['changed'] = True
+ meraki.result['data'] = original
+ meraki.exit_json(**meraki.result)
+ path = meraki.construct_path('update', net_id=net_id) + str(meraki.params['vlan_id'])
+ response = meraki.request(path, method='PUT', payload=json.dumps(payload))
+ meraki.result['changed'] = True
+ meraki.result['data'] = response
+ meraki.generate_diff(original, response)
+ else:
+ if meraki.module.check_mode is True:
+ meraki.result['data'] = original
+ meraki.exit_json(**meraki.result)
+ meraki.result['data'] = original
+ elif meraki.params['state'] == 'absent':
+ if is_vlan_valid(meraki, net_id, meraki.params['vlan_id']):
+ if meraki.module.check_mode is True:
+ meraki.result['data'] = {}
+ meraki.result['changed'] = True
+ meraki.exit_json(**meraki.result)
+ path = meraki.construct_path('delete', net_id=net_id) + str(meraki.params['vlan_id'])
+ response = meraki.request(path, 'DELETE')
+ meraki.result['changed'] = True
+ meraki.result['data'] = response
+
+ # in the event of a successful module execution, you will want to
+ # simple AnsibleModule.exit_json(), passing the key/value results
+ meraki.exit_json(**meraki.result)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/ansible_collections/cisco/meraki/plugins/modules/meraki_network.py b/ansible_collections/cisco/meraki/plugins/modules/meraki_network.py
new file mode 100644
index 000000000..84e5a2fc9
--- /dev/null
+++ b/ansible_collections/cisco/meraki/plugins/modules/meraki_network.py
@@ -0,0 +1,469 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Copyright: (c) 2018, 2019 Kevin Breit (@kbreit) <kevin.breit@kevinbreit.net>
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+
+__metaclass__ = type
+
+ANSIBLE_METADATA = {
+ "metadata_version": "1.1",
+ "status": ["preview"],
+ "supported_by": "community",
+}
+
+DOCUMENTATION = r"""
+---
+module: meraki_network
+short_description: Manage networks in the Meraki cloud
+description:
+- Allows for creation, management, and visibility into networks within Meraki.
+options:
+ state:
+ description:
+ - Create or modify an organization.
+ choices: [ absent, present, query ]
+ default: present
+ type: str
+ net_name:
+ description:
+ - Name of a network.
+ aliases: [ name, network ]
+ type: str
+ net_id:
+ description:
+ - ID number of a network.
+ type: str
+ type:
+ description:
+ - Type of network device network manages.
+ - Required when creating a network.
+ - As of Ansible 2.8, C(combined) type is no longer accepted.
+ - As of Ansible 2.8, changes to this parameter are no longer idempotent.
+ choices: [ appliance, switch, wireless, sensor, systemsManager, camera, cellularGateway ]
+ aliases: [ net_type ]
+ type: list
+ elements: str
+ tags:
+ type: list
+ elements: str
+ description:
+ - List of tags to assign to network.
+ - C(tags) name conflicts with the tags parameter in Ansible. Indentation problems may cause unexpected behaviors.
+ - Ansible 2.8 converts this to a list from a comma separated list.
+ timezone:
+ description:
+ - Timezone associated to network.
+ - See U(https://en.wikipedia.org/wiki/List_of_tz_database_time_zones) for a list of valid timezones.
+ type: str
+ enable_vlans:
+ description:
+ - Boolean value specifying whether VLANs should be supported on a network.
+ - Requires C(net_name) or C(net_id) to be specified.
+ type: bool
+ local_status_page_enabled:
+ description: >
+ - This no longer works and will likely be moved to a separate module.
+ - Enables the local device status pages (U[my.meraki.com](my.meraki.com), U[ap.meraki.com](ap.meraki.com), U[switch.meraki.com](switch.meraki.com),
+ U[wired.meraki.com](wired.meraki.com)).
+ - Only can be specified on its own or with C(remote_status_page_enabled).
+ type: bool
+ remote_status_page_enabled:
+ description:
+ - This no longer works and will likely be moved to a separate module.
+ - Enables access to the device status page (U(http://device LAN IP)).
+ - Can only be set if C(local_status_page_enabled:) is set to C(yes).
+ - Only can be specified on its own or with C(local_status_page_enabled).
+ type: bool
+ copy_from_network_id:
+ description:
+ - New network inherits properties from this network ID.
+ - Other provided parameters will override the copied configuration.
+ - Type which must match this network's type exactly.
+ type: str
+
+author:
+ - Kevin Breit (@kbreit)
+extends_documentation_fragment: cisco.meraki.meraki
+"""
+
+EXAMPLES = r"""
+- delegate_to: localhost
+ block:
+ - name: List all networks associated to the YourOrg organization
+ meraki_network:
+ auth_key: abc12345
+ state: query
+ org_name: YourOrg
+ - name: Query network named MyNet in the YourOrg organization
+ meraki_network:
+ auth_key: abc12345
+ state: query
+ org_name: YourOrg
+ net_name: MyNet
+ - name: Create network named MyNet in the YourOrg organization
+ meraki_network:
+ auth_key: abc12345
+ state: present
+ org_name: YourOrg
+ net_name: MyNet
+ type: switch
+ timezone: America/Chicago
+ tags: production, chicago
+ - name: Create combined network named MyNet in the YourOrg organization
+ meraki_network:
+ auth_key: abc12345
+ state: present
+ org_name: YourOrg
+ net_name: MyNet
+ type:
+ - switch
+ - appliance
+ timezone: America/Chicago
+ tags: production, chicago
+ - name: Create new network based on an existing network
+ meraki_network:
+ auth_key: abc12345
+ state: present
+ org_name: YourOrg
+ net_name: MyNet
+ type:
+ - switch
+ - appliance
+ copy_from_network_id: N_1234
+ - name: Enable VLANs on a network
+ meraki_network:
+ auth_key: abc12345
+ state: query
+ org_name: YourOrg
+ net_name: MyNet
+ enable_vlans: yes
+ - name: Modify local status page enabled state
+ meraki_network:
+ auth_key: abc12345
+ state: query
+ org_name: YourOrg
+ net_name: MyNet
+ local_status_page_enabled: yes
+"""
+
+RETURN = r"""
+data:
+ description: Information about the created or manipulated object.
+ returned: info
+ type: complex
+ contains:
+ id:
+ description: Identification string of network.
+ returned: success
+ type: str
+ sample: N_12345
+ name:
+ description: Written name of network.
+ returned: success
+ type: str
+ sample: YourNet
+ organization_id:
+ description: Organization ID which owns the network.
+ returned: success
+ type: str
+ sample: 0987654321
+ tags:
+ description: Space delimited tags assigned to network.
+ returned: success
+ type: list
+ sample: ['production']
+ time_zone:
+ description: Timezone where network resides.
+ returned: success
+ type: str
+ sample: America/Chicago
+ type:
+ description: Functional type of network.
+ returned: success
+ type: list
+ sample: ['switch']
+ local_status_page_enabled:
+ description: States whether U(my.meraki.com) and other device portals should be enabled.
+ returned: success
+ type: bool
+ sample: true
+ remote_status_page_enabled:
+ description: Enables access to the device status page.
+ returned: success
+ type: bool
+ sample: true
+"""
+
+from ansible.module_utils.basic import AnsibleModule, json
+from ansible_collections.cisco.meraki.plugins.module_utils.network.meraki.meraki import (
+ MerakiModule,
+ meraki_argument_spec,
+)
+
+
+def is_net_valid(data, net_name=None, net_id=None):
+ if net_name is None and net_id is None:
+ return False
+ for n in data:
+ if net_name:
+ if n["name"] == net_name:
+ return True
+ elif net_id:
+ if n["id"] == net_id:
+ return True
+ return False
+
+
+def get_network_settings(meraki, net_id):
+ path = meraki.construct_path("get_settings", net_id=net_id)
+ response = meraki.request(path, method="GET")
+ return response
+
+
+def main():
+
+ # define the available arguments/parameters that a user can pass to
+ # the module
+
+ argument_spec = meraki_argument_spec()
+ argument_spec.update(
+ net_id=dict(type="str"),
+ type=dict(
+ type="list",
+ elements="str",
+ choices=[
+ "wireless",
+ "switch",
+ "appliance",
+ "sensor",
+ "systemsManager",
+ "camera",
+ "cellularGateway",
+ ],
+ aliases=["net_type"],
+ ),
+ tags=dict(type="list", elements="str"),
+ timezone=dict(type="str"),
+ net_name=dict(type="str", aliases=["name", "network"]),
+ state=dict(
+ type="str", choices=["present", "query", "absent"], default="present"
+ ),
+ enable_vlans=dict(type="bool"),
+ local_status_page_enabled=dict(type="bool"),
+ remote_status_page_enabled=dict(type="bool"),
+ copy_from_network_id=dict(type="str"),
+ )
+
+ # the AnsibleModule object will be our abstraction working with Ansible
+ # this includes instantiation, a couple of common attr would be the
+ # args/params passed to the execution, as well as if the module
+ # supports check mode
+ module = AnsibleModule(
+ argument_spec=argument_spec,
+ supports_check_mode=True,
+ )
+
+ meraki = MerakiModule(module, function="network")
+ module.params["follow_redirects"] = "all"
+ payload = None
+
+ create_urls = {"network": "/organizations/{org_id}/networks"}
+ update_urls = {"network": "/networks/{net_id}"}
+ delete_urls = {"network": "/networks/{net_id}"}
+ update_settings_urls = {"network": "/networks/{net_id}/settings"}
+ get_settings_urls = {"network": "/networks/{net_id}/settings"}
+ enable_vlans_urls = {"network": "/networks/{net_id}/appliance/vlans/settings"}
+ get_vlan_status_urls = {"network": "/networks/{net_id}/appliance/vlans/settings"}
+ meraki.url_catalog["create"] = create_urls
+ meraki.url_catalog["update"] = update_urls
+ meraki.url_catalog["update_settings"] = update_settings_urls
+ meraki.url_catalog["get_settings"] = get_settings_urls
+ meraki.url_catalog["delete"] = delete_urls
+ meraki.url_catalog["enable_vlans"] = enable_vlans_urls
+ meraki.url_catalog["status_vlans"] = get_vlan_status_urls
+
+ if not meraki.params["org_name"] and not meraki.params["org_id"]:
+ meraki.fail_json(msg="org_name or org_id parameters are required")
+ if meraki.params["state"] != "query":
+ if not meraki.params["net_name"] and not meraki.params["net_id"]:
+ meraki.fail_json(
+ msg="net_name or net_id is required for present or absent states"
+ )
+ if meraki.params["net_name"] and meraki.params["net_id"]:
+ meraki.fail_json(msg="net_name and net_id are mutually exclusive")
+ if not meraki.params["net_name"] and not meraki.params["net_id"]:
+ if meraki.params["enable_vlans"]:
+ meraki.fail_json(
+ msg="The parameter 'enable_vlans' requires 'net_name' or 'net_id' to be specified"
+ )
+ if (
+ meraki.params["local_status_page_enabled"] is False
+ and meraki.params["remote_status_page_enabled"] is True
+ ):
+ meraki.fail_json(
+ msg="local_status_page_enabled must be true when setting remote_status_page_enabled"
+ )
+
+ # Construct payload
+ if meraki.params["state"] == "present":
+ payload = dict()
+ if meraki.params["net_name"]:
+ payload["name"] = meraki.params["net_name"]
+ if meraki.params["type"]:
+ payload["productTypes"] = meraki.params["type"]
+ if meraki.params["tags"]:
+ payload["tags"] = meraki.params["tags"]
+ if meraki.params["timezone"]:
+ payload["timeZone"] = meraki.params["timezone"]
+ if meraki.params["local_status_page_enabled"] is not None:
+ payload["localStatusPageEnabled"] = meraki.params[
+ "local_status_page_enabled"
+ ]
+ if meraki.params["remote_status_page_enabled"] is not None:
+ payload["remoteStatusPageEnabled"] = meraki.params[
+ "remote_status_page_enabled"
+ ]
+ if meraki.params["copy_from_network_id"] is not None:
+ payload["copyFromNetworkId"] = meraki.params["copy_from_network_id"]
+
+ # manipulate or modify the state as needed (this is going to be the
+ # part where your module will do what it needs to do)
+
+ org_id = meraki.params["org_id"]
+ if not org_id:
+ org_id = meraki.get_org_id(meraki.params["org_name"])
+ nets = meraki.get_nets(org_id=org_id)
+ net_id = meraki.params["net_id"]
+ net_exists = False
+ if net_id is not None:
+ if is_net_valid(nets, net_id=net_id) is False:
+ meraki.fail_json(msg="Network specified by net_id does not exist.")
+ net_exists = True
+ elif meraki.params["net_name"]:
+ if is_net_valid(nets, net_name=meraki.params["net_name"]) is True:
+ net_id = meraki.get_net_id(net_name=meraki.params["net_name"], data=nets)
+ net_exists = True
+
+ if meraki.params["state"] == "query":
+ if not meraki.params["net_name"] and not meraki.params["net_id"]:
+ meraki.result["data"] = nets
+ elif meraki.params["net_name"] or meraki.params["net_id"] is not None:
+ if (
+ meraki.params["local_status_page_enabled"] is not None
+ or meraki.params["remote_status_page_enabled"] is not None
+ ):
+ meraki.result["data"] = get_network_settings(meraki, net_id)
+ meraki.exit_json(**meraki.result)
+ else:
+ meraki.result["data"] = meraki.get_net(
+ meraki.params["org_name"], net_name=meraki.params["net_name"], data=nets, net_id=meraki.params["net_id"],
+ )
+ meraki.exit_json(**meraki.result)
+ elif meraki.params["state"] == "present":
+ if net_exists is False: # Network needs to be created
+ if "type" not in meraki.params or meraki.params["type"] is None:
+ meraki.fail_json(
+ msg="type parameter is required when creating a network."
+ )
+ if meraki.check_mode is True:
+ data = payload
+ data["id"] = "N_12345"
+ data["organization_id"] = org_id
+ meraki.result["data"] = data
+ meraki.result["changed"] = True
+ meraki.exit_json(**meraki.result)
+ path = meraki.construct_path("create", org_id=org_id)
+ r = meraki.request(path, method="POST", payload=json.dumps(payload))
+ if meraki.status == 201:
+ meraki.result["data"] = r
+ meraki.result["changed"] = True
+ else: # Network exists, make changes
+ if meraki.params["enable_vlans"] is not None: # Modify VLANs configuration
+ status_path = meraki.construct_path("status_vlans", net_id=net_id)
+ status = meraki.request(status_path, method="GET")
+ payload = {"vlansEnabled": meraki.params["enable_vlans"]}
+ if meraki.is_update_required(status, payload):
+ if meraki.check_mode is True:
+ data = {
+ "vlansEnabled": meraki.params["enable_vlans"],
+ "network_id": net_id,
+ }
+ meraki.result["data"] = data
+ meraki.result["changed"] = True
+ meraki.exit_json(**meraki.result)
+ path = meraki.construct_path("enable_vlans", net_id=net_id)
+ r = meraki.request(path, method="PUT", payload=json.dumps(payload))
+ if meraki.status == 200:
+ meraki.result["data"] = r
+ meraki.result["changed"] = True
+ meraki.exit_json(**meraki.result)
+ else:
+ meraki.result["data"] = status
+ meraki.exit_json(**meraki.result)
+ elif (
+ meraki.params["local_status_page_enabled"] is not None
+ or meraki.params["remote_status_page_enabled"] is not None
+ ):
+ path = meraki.construct_path("get_settings", net_id=net_id)
+ original = meraki.request(path, method="GET")
+ payload = {}
+ if meraki.params["local_status_page_enabled"] is not None:
+ payload["localStatusPageEnabled"] = meraki.params[
+ "local_status_page_enabled"
+ ]
+ if meraki.params["remote_status_page_enabled"] is not None:
+ payload["remoteStatusPageEnabled"] = meraki.params[
+ "remote_status_page_enabled"
+ ]
+ if meraki.is_update_required(original, payload):
+ if meraki.check_mode is True:
+ original.update(payload)
+ meraki.result["data"] = original
+ meraki.result["changed"] = True
+ meraki.exit_json(**meraki.result)
+ path = meraki.construct_path("update_settings", net_id=net_id)
+ response = meraki.request(
+ path, method="PUT", payload=json.dumps(payload)
+ )
+ meraki.result["data"] = response
+ meraki.result["changed"] = True
+ meraki.exit_json(**meraki.result)
+ else:
+ meraki.result["data"] = original
+ meraki.exit_json(**meraki.result)
+ net = meraki.get_net(meraki.params["org_name"], net_id=net_id, data=nets)
+ if meraki.is_update_required(net, payload):
+ if meraki.check_mode is True:
+ data = net
+ net.update(payload)
+ meraki.result["data"] = net
+ meraki.result["changed"] = True
+ meraki.exit_json(**meraki.result)
+ path = meraki.construct_path("update", net_id=net_id)
+ r = meraki.request(path, method="PUT", payload=json.dumps(payload))
+ if meraki.status == 200:
+ meraki.result["data"] = r
+ meraki.result["changed"] = True
+ else:
+ meraki.result["data"] = net
+ elif meraki.params["state"] == "absent":
+ if is_net_valid(nets, net_id=net_id) is True:
+ if meraki.check_mode is True:
+ meraki.result["data"] = {}
+ meraki.result["changed"] = True
+ meraki.exit_json(**meraki.result)
+ path = meraki.construct_path("delete", net_id=net_id)
+ r = meraki.request(path, method="DELETE")
+ if meraki.status == 204:
+ meraki.result["changed"] = True
+
+ # in the event of a successful module execution, you will want to
+ # simple AnsibleModule.exit_json(), passing the key/value results
+ meraki.exit_json(**meraki.result)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/ansible_collections/cisco/meraki/plugins/modules/meraki_network_settings.py b/ansible_collections/cisco/meraki/plugins/modules/meraki_network_settings.py
new file mode 100644
index 000000000..098b7729d
--- /dev/null
+++ b/ansible_collections/cisco/meraki/plugins/modules/meraki_network_settings.py
@@ -0,0 +1,337 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Copyright: (c) 2023 Kevin Breit (@kbreit) <kevin.breit@kevinbreit.net>
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+
+__metaclass__ = type
+
+ANSIBLE_METADATA = {
+ "metadata_version": "1.1",
+ "status": ["preview"],
+ "supported_by": "community",
+}
+
+DOCUMENTATION = r"""
+---
+module: meraki_network_settings
+short_description: Manage the settings of networks in the Meraki cloud
+description:
+- Allows for management of settings of networks within Meraki.
+options:
+ state:
+ description:
+ - Create or modify an organization.
+ choices: [present, query]
+ type: str
+ default: query
+ net_name:
+ description:
+ - Name of a network.
+ aliases: [ name, network ]
+ type: str
+ net_id:
+ description:
+ - ID number of a network.
+ type: str
+ local_status_page_enabled:
+ description: >
+ - Enables the local device status pages (U[my.meraki.com](my.meraki.com), U[ap.meraki.com](ap.meraki.com), U[switch.meraki.com](switch.meraki.com),
+ U[wired.meraki.com](wired.meraki.com)).
+ - Only can be specified on its own or with C(remote_status_page_enabled).
+ type: bool
+ remote_status_page_enabled:
+ description:
+ - Enables access to the device status page (U(http://device LAN IP)).
+ - Can only be set if C(local_status_page_enabled:) is set to C(yes).
+ - Only can be specified on its own or with C(local_status_page_enabled).
+ type: bool
+ local_status_page:
+ description:
+ - Configuration stanza of the local status page.
+ type: dict
+ suboptions:
+ authentication:
+ description:
+ - Local status page authentication settings.
+ type: dict
+ suboptions:
+ enabled:
+ description:
+ - Set whether local status page authentication is enabled.
+ type: bool
+ password:
+ description:
+ - Set password on local status page.
+ type: str
+ secure_port:
+ description:
+ - Configuration of SecureConnect options applied to the network.
+ type: dict
+ suboptions:
+ enabled:
+ description:
+ - Set whether SecureConnect is enabled on the network.
+ type: bool
+author:
+ - Kevin Breit (@kbreit)
+extends_documentation_fragment: cisco.meraki.meraki
+"""
+
+EXAMPLES = r"""
+ - name: Get network settings
+ cisco.meraki.meraki_network_settings:
+ auth_key: '{{ auth_key }}'
+ state: query
+ org_name: '{{test_org_name}}'
+ net_name: NetworkSettingsTestNet
+ delegate_to: localhost
+
+ - name: Update network settings
+ cisco.meraki.meraki_network_settings:
+ auth_key: '{{ auth_key }}'
+ state: present
+ org_name: '{{test_org_name}}'
+ net_name: NetworkSettingsTestNet
+ local_status_page_enabled: false
+ delegate_to: localhost
+
+ - name: Enable password on local page
+ cisco.meraki.meraki_network_settings:
+ auth_key: '{{ auth_key }}'
+ state: present
+ org_name: '{{test_org_name}}'
+ net_name: NetworkSettingsTestNet
+ local_status_page_enabled: true
+ local_status_page:
+ authentication:
+ enabled: true
+ password: abc123
+ delegate_to: localhost
+"""
+
+RETURN = r"""
+data:
+ description: Information about the created or manipulated object.
+ returned: info
+ type: complex
+ contains:
+ local_status_page_enabled:
+ description: States whether U(my.meraki.com) and other device portals should be enabled.
+ returned: success
+ type: bool
+ sample: true
+ remote_status_page_enabled:
+ description: Enables access to the device status page.
+ returned: success
+ type: bool
+ sample: true
+ expire_data_older_than:
+ description: The number of days, weeks, or months in Epoch time to expire the data before
+ returned: success
+ type: int
+ sample: 1234
+ fips:
+ description: A hash of FIPS options applied to the Network.
+ returned: success
+ type: complex
+ contains:
+ enabled:
+ description: Enables/disables FIPS on the network.
+ returned: success
+ type: bool
+ sample: true
+ local_status_page:
+ description: A hash of Local Status Page(s) authentication options applied to the Network.
+ returned: success
+ type: complex
+ contains:
+ authentication:
+ description: A hash of Local Status Pages' authentication options applied to the Network.
+ type: complex
+ contains:
+ username:
+ description: The username used for Local Status Pages.
+ type: str
+ returned: success
+ sample: admin
+ enabled:
+ description: Enables/Disables the authenticaiton on Local Status Pages.
+ type: bool
+ returned: success
+ sample: true
+ secure_port:
+ description: A hash of SecureConnect options applied to the Network.
+ type: complex
+ contains:
+ enabled:
+ description: Enables/disables SecureConnect on the network.
+ type: bool
+ returned: success
+ sample: true
+ named_vlans:
+ description: A hash of Named VLANs options applied to the Network.
+ type: complex
+ contains:
+ enabled:
+ description: Enables/disables Named VLANs on the network.
+ type: bool
+ returned: success
+ sample: true
+"""
+
+from ansible.module_utils.basic import AnsibleModule, json
+from ansible_collections.cisco.meraki.plugins.module_utils.network.meraki.meraki import (
+ MerakiModule,
+ meraki_argument_spec,
+)
+
+
+def is_net_valid(data, net_name=None, net_id=None):
+ if net_name is None and net_id is None:
+ return False
+ for n in data:
+ if net_name:
+ if n["name"] == net_name:
+ return True
+ elif net_id:
+ if n["id"] == net_id:
+ return True
+ return False
+
+
+def get_network_settings(meraki, net_id):
+ path = meraki.construct_path("get_settings", net_id=net_id)
+ response = meraki.request(path, method="GET")
+ return response
+
+
+def construct_payload(params):
+ payload = dict()
+ if params["local_status_page_enabled"] is not None:
+ payload["localStatusPageEnabled"] = params["local_status_page_enabled"]
+ if params["remote_status_page_enabled"] is not None:
+ payload["remoteStatusPageEnabled"] = params["remote_status_page_enabled"]
+ if params["local_status_page"] is not None:
+ payload["localStatusPage"] = dict()
+ if params["local_status_page"]["authentication"] is not None:
+ payload["localStatusPage"]["authentication"] = {}
+ if params["local_status_page"]["authentication"]["enabled"] is not None:
+ payload["localStatusPage"]["authentication"]["enabled"] = params["local_status_page"]["authentication"]["enabled"]
+ if params["local_status_page"]["authentication"]["password"] is not None:
+ payload["localStatusPage"]["authentication"]["password"] = params["local_status_page"]["authentication"]["password"]
+ if params["secure_port"] is not None:
+ payload["securePort"] = dict()
+ if params["secure_port"]["enabled"] is not None:
+ payload["securePort"]["enabled"] = params["secure_port"]["enabled"]
+ return payload
+
+
+def main():
+
+ # define the available arguments/parameters that a user can pass to
+ # the module
+
+ auth_args = dict(
+ enabled=dict(type="bool"),
+ password=dict(type="str", no_log=True),
+ )
+
+ local_status_page_args = dict(
+ authentication=dict(type="dict", default=None, options=auth_args),
+ )
+
+ secure_port_args = dict(
+ enabled=dict(type="bool"),
+ )
+
+ argument_spec = meraki_argument_spec()
+ argument_spec.update(
+ state=dict(type="str", choices=["query", "present"], default="query"),
+ net_name=dict(type="str", aliases=["name", "network"]),
+ net_id=dict(type="str"),
+ local_status_page_enabled=dict(type="bool"),
+ remote_status_page_enabled=dict(type="bool"),
+ local_status_page=dict(type="dict", default=None, options=local_status_page_args),
+ secure_port=dict(type="dict", default=None, options=secure_port_args)
+ )
+
+ # the AnsibleModule object will be our abstraction working with Ansible
+ # this includes instantiation, a couple of common attr would be the
+ # args/params passed to the execution, as well as if the module
+ # supports check mode
+ module = AnsibleModule(
+ argument_spec=argument_spec,
+ supports_check_mode=True,
+ )
+
+ meraki = MerakiModule(module, function="network")
+ module.params["follow_redirects"] = "all"
+ payload = None
+
+ update_settings_urls = {"network": "/networks/{net_id}/settings"}
+ get_settings_urls = {"network": "/networks/{net_id}/settings"}
+ meraki.url_catalog["update_settings"] = update_settings_urls
+ meraki.url_catalog["get_settings"] = get_settings_urls
+
+ if not meraki.params["org_name"] and not meraki.params["org_id"]:
+ meraki.fail_json(msg="org_name or org_id parameters are required")
+ if meraki.params["state"] != "query":
+ if not meraki.params["net_name"] and not meraki.params["net_id"]:
+ meraki.fail_json(
+ msg="net_name or net_id is required for present or absent states"
+ )
+ if meraki.params["net_name"] and meraki.params["net_id"]:
+ meraki.fail_json(msg="net_name and net_id are mutually exclusive")
+ if (
+ meraki.params["local_status_page_enabled"] is False
+ and meraki.params["remote_status_page_enabled"] is True
+ ):
+ meraki.fail_json(
+ msg="local_status_page_enabled must be true when setting remote_status_page_enabled"
+ )
+
+ # manipulate or modify the state as needed (this is going to be the
+ # part where your module will do what it needs to do)
+
+ org_id = meraki.params["org_id"]
+ net_id = meraki.params["net_id"]
+ if not org_id:
+ org_id = meraki.get_org_id(meraki.params["org_name"])
+ if net_id is None:
+ nets = meraki.get_nets(org_id=org_id)
+ net_id = meraki.get_net_id(org_id, meraki.params["net_name"], data=nets)
+
+ if meraki.params["state"] == "query":
+ path = meraki.construct_path("get_settings", net_id=net_id)
+ meraki.result["data"] = meraki.request(path, method="GET")
+ meraki.exit_json(**meraki.result)
+ elif meraki.params["state"] == "present":
+ path = meraki.construct_path("get_settings", net_id=net_id)
+ current = meraki.request(path, method="GET")
+ payload = construct_payload(meraki.params)
+ if meraki.is_update_required(current, payload, optional_ignore=["password"]):
+ if meraki.check_mode is True:
+ try:
+ del payload["local_status_page"]["authentication"]["password"]
+ except KeyError:
+ pass
+ meraki.result["data"] = payload
+ meraki.result["changed"] = True
+ meraki.exit_json(**meraki.result)
+ path = meraki.construct_path("update_settings", net_id=net_id)
+ response = meraki.request(path, method="PUT", payload=json.dumps(payload))
+ if meraki.status == 200:
+ meraki.result["changed"] = True
+ meraki.result["data"] = response
+ meraki.exit_json(**meraki.result)
+ meraki.result["data"] = current
+ # in the event of a successful module execution, you will want to
+ # simple AnsibleModule.exit_json(), passing the key/value results
+ meraki.exit_json(**meraki.result)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/ansible_collections/cisco/meraki/plugins/modules/meraki_organization.py b/ansible_collections/cisco/meraki/plugins/modules/meraki_organization.py
new file mode 100644
index 000000000..45148e89a
--- /dev/null
+++ b/ansible_collections/cisco/meraki/plugins/modules/meraki_organization.py
@@ -0,0 +1,242 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Copyright: (c) 2018, Kevin Breit (@kbreit) <kevin.breit@kevinbreit.net>
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+ANSIBLE_METADATA = {
+ 'metadata_version': '1.1',
+ 'status': ['preview'],
+ 'supported_by': 'community'
+}
+
+DOCUMENTATION = r'''
+---
+module: meraki_organization
+short_description: Manage organizations in the Meraki cloud
+description:
+- Allows for creation, management, and visibility into organizations within Meraki.
+options:
+ state:
+ description:
+ - Create or modify an organization.
+ - C(org_id) must be specified if multiple organizations of the same name exist.
+ - C(absent) WILL DELETE YOUR ENTIRE ORGANIZATION, AND ALL ASSOCIATED OBJECTS, WITHOUT CONFIRMATION. USE WITH CAUTION.
+ choices: ['absent', 'present', 'query']
+ default: present
+ type: str
+ clone:
+ description:
+ - Organization to clone to a new organization.
+ type: str
+ org_name:
+ description:
+ - Name of organization.
+ - If C(clone) is specified, C(org_name) is the name of the new organization.
+ aliases: [ name, organization ]
+ type: str
+ org_id:
+ description:
+ - ID of organization.
+ aliases: [ id ]
+ type: str
+ delete_confirm:
+ description:
+ - ID of organization required for confirmation before deletion.
+ type: str
+author:
+- Kevin Breit (@kbreit)
+extends_documentation_fragment: cisco.meraki.meraki
+'''
+
+EXAMPLES = r'''
+- name: Create a new organization named YourOrg
+ meraki_organization:
+ auth_key: abc12345
+ org_name: YourOrg
+ state: present
+ delegate_to: localhost
+
+- name: Delete an organization named YourOrg
+ meraki_organization:
+ auth_key: abc12345
+ org_name: YourOrg
+ state: absent
+ delegate_to: localhost
+
+- name: Query information about all organizations associated to the user
+ meraki_organization:
+ auth_key: abc12345
+ state: query
+ delegate_to: localhost
+
+- name: Query information about a single organization named YourOrg
+ meraki_organization:
+ auth_key: abc12345
+ org_name: YourOrg
+ state: query
+ delegate_to: localhost
+
+- name: Rename an organization to RenamedOrg
+ meraki_organization:
+ auth_key: abc12345
+ org_id: 987654321
+ org_name: RenamedOrg
+ state: present
+ delegate_to: localhost
+
+- name: Clone an organization named Org to a new one called ClonedOrg
+ meraki_organization:
+ auth_key: abc12345
+ clone: Org
+ org_name: ClonedOrg
+ state: present
+ delegate_to: localhost
+'''
+
+RETURN = r'''
+data:
+ description: Information about the organization which was created or modified
+ returned: success
+ type: complex
+ contains:
+ id:
+ description: Unique identification number of organization
+ returned: success
+ type: int
+ sample: 2930418
+ name:
+ description: Name of organization
+ returned: success
+ type: str
+ sample: YourOrg
+
+'''
+
+from ansible.module_utils.basic import AnsibleModule, json
+from ansible_collections.cisco.meraki.plugins.module_utils.network.meraki.meraki import MerakiModule, meraki_argument_spec
+
+
+def get_org(meraki, org_id, data):
+ # meraki.fail_json(msg=str(org_id), data=data, oid0=data[0]['id'], oid1=data[1]['id'])
+ for o in data:
+ # meraki.fail_json(msg='o', data=o['id'], type=str(type(o['id'])))
+ if o['id'] == org_id:
+ return o
+ return -1
+
+
+def main():
+
+ # define the available arguments/parameters that a user can pass to
+ # the module
+ argument_spec = meraki_argument_spec()
+ argument_spec.update(clone=dict(type='str'),
+ state=dict(type='str', choices=['absent', 'present', 'query'], default='present'),
+ org_name=dict(type='str', aliases=['name', 'organization']),
+ org_id=dict(type='str', aliases=['id']),
+ delete_confirm=dict(type='str'),
+ )
+
+ # the AnsibleModule object will be our abstraction working with Ansible
+ # this includes instantiation, a couple of common attr would be the
+ # args/params passed to the execution, as well as if the module
+ # supports check mode
+ module = AnsibleModule(argument_spec=argument_spec,
+ supports_check_mode=True,
+ )
+ meraki = MerakiModule(module, function='organizations')
+
+ meraki.params['follow_redirects'] = 'all'
+
+ create_urls = {'organizations': '/organizations'}
+ update_urls = {'organizations': '/organizations/{org_id}'}
+ delete_urls = {'organizations': '/organizations/{org_id}'}
+ clone_urls = {'organizations': '/organizations/{org_id}/clone'}
+
+ meraki.url_catalog['create'] = create_urls
+ meraki.url_catalog['update'] = update_urls
+ meraki.url_catalog['clone'] = clone_urls
+ meraki.url_catalog['delete'] = delete_urls
+
+ payload = None
+
+ # manipulate or modify the state as needed (this is going to be the
+ # part where your module will do what it needs to do)
+ orgs = meraki.get_orgs()
+ if meraki.params['state'] == 'query':
+ if meraki.params['org_name']: # Query by organization name
+ module.warn('All matching organizations will be returned, even if there are duplicate named organizations')
+ for o in orgs:
+ if o['name'] == meraki.params['org_name']:
+ meraki.result['data'] = o
+ elif meraki.params['org_id']:
+ for o in orgs:
+ if o['id'] == meraki.params['org_id']:
+ meraki.result['data'] = o
+ else: # Query all organizations, no matter what
+ meraki.result['data'] = orgs
+ elif meraki.params['state'] == 'present':
+ if meraki.params['clone']: # Cloning
+ payload = {'name': meraki.params['org_name']}
+ response = meraki.request(meraki.construct_path('clone',
+ org_name=meraki.params['clone']
+ ),
+ payload=json.dumps(payload),
+ method='POST')
+ if meraki.status != 201:
+ meraki.fail_json(msg='Organization clone failed')
+ meraki.result['data'] = response
+ meraki.result['changed'] = True
+ elif not meraki.params['org_id'] and meraki.params['org_name']: # Create new organization
+ payload = {'name': meraki.params['org_name']}
+ response = meraki.request(meraki.construct_path('create'),
+ method='POST',
+ payload=json.dumps(payload))
+ if meraki.status == 201:
+ meraki.result['data'] = response
+ meraki.result['changed'] = True
+ elif meraki.params['org_id'] and meraki.params['org_name']: # Update an existing organization
+ payload = {'name': meraki.params['org_name'],
+ 'id': meraki.params['org_id'],
+ }
+ original = get_org(meraki, meraki.params['org_id'], orgs)
+ if meraki.is_update_required(original, payload, optional_ignore=['url']):
+ response = meraki.request(meraki.construct_path('update',
+ org_id=meraki.params['org_id']
+ ),
+ method='PUT',
+ payload=json.dumps(payload))
+ if meraki.status != 200:
+ meraki.fail_json(msg='Organization update failed')
+ meraki.result['data'] = response
+ meraki.result['changed'] = True
+ else:
+ meraki.result['data'] = original
+ elif meraki.params['state'] == 'absent':
+ if meraki.params['org_name'] is not None:
+ org_id = meraki.get_org_id(meraki.params['org_name'])
+ elif meraki.params['org_id'] is not None:
+ org_id = meraki.params['org_id']
+ if meraki.params['delete_confirm'] != org_id:
+ meraki.fail_json(msg="delete_confirm must match the network ID of the network to be deleted.")
+ if meraki.check_mode is True:
+ meraki.result['data'] = {}
+ meraki.result['changed'] = True
+ meraki.exit_json(**meraki.result)
+ path = meraki.construct_path('delete', org_id=org_id)
+ response = meraki.request(path, method='DELETE')
+ if meraki.status == 204:
+ meraki.result['data'] = {}
+ meraki.result['changed'] = True
+
+ # in the event of a successful module execution, you will want to
+ # simple AnsibleModule.exit_json(), passing the key/value results
+ meraki.exit_json(**meraki.result)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/ansible_collections/cisco/meraki/plugins/modules/meraki_snmp.py b/ansible_collections/cisco/meraki/plugins/modules/meraki_snmp.py
new file mode 100644
index 000000000..0240a168b
--- /dev/null
+++ b/ansible_collections/cisco/meraki/plugins/modules/meraki_snmp.py
@@ -0,0 +1,387 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Copyright: (c) 2018, Kevin Breit (@kbreit) <kevin.breit@kevinbreit.net>
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+ANSIBLE_METADATA = {
+ 'metadata_version': '1.1',
+ 'status': ['preview'],
+ 'supported_by': 'community'
+}
+
+DOCUMENTATION = r'''
+---
+module: meraki_snmp
+short_description: Manage organizations in the Meraki cloud
+description:
+- Allows for management of SNMP settings for Meraki.
+options:
+ state:
+ description:
+ - Specifies whether SNMP information should be queried or modified.
+ choices: ['query', 'present']
+ default: present
+ type: str
+ v2c_enabled:
+ description:
+ - Specifies whether SNMPv2c is enabled.
+ type: bool
+ v3_enabled:
+ description:
+ - Specifies whether SNMPv3 is enabled.
+ type: bool
+ v3_auth_mode:
+ description:
+ - Sets authentication mode for SNMPv3.
+ choices: ['MD5', 'SHA']
+ type: str
+ v3_auth_pass:
+ description:
+ - Authentication password for SNMPv3.
+ - Must be at least 8 characters long.
+ type: str
+ v3_priv_mode:
+ description:
+ - Specifies privacy mode for SNMPv3.
+ choices: ['DES', 'AES128']
+ type: str
+ v3_priv_pass:
+ description:
+ - Privacy password for SNMPv3.
+ - Must be at least 8 characters long.
+ type: str
+ peer_ips:
+ description:
+ - List of IP addresses which can perform SNMP queries.
+ type: list
+ elements: str
+ net_name:
+ description:
+ - Name of network.
+ type: str
+ net_id:
+ description:
+ - ID of network.
+ type: str
+ access:
+ description:
+ - Type of SNMP access.
+ choices: [community, none, users]
+ type: str
+ community_string:
+ description:
+ - SNMP community string.
+ - Only relevant if C(access) is set to C(community).
+ type: str
+ users:
+ description:
+ - Information about users with access to SNMP.
+ - Only relevant if C(access) is set to C(users).
+ type: list
+ elements: dict
+ suboptions:
+ username:
+ description: Username of user with access.
+ type: str
+ passphrase:
+ description: Passphrase for user SNMP access.
+ type: str
+author:
+- Kevin Breit (@kbreit)
+extends_documentation_fragment: cisco.meraki.meraki
+'''
+
+EXAMPLES = r'''
+- name: Query SNMP values
+ meraki_snmp:
+ auth_key: abc12345
+ org_name: YourOrg
+ state: query
+ delegate_to: localhost
+
+- name: Enable SNMPv2
+ meraki_snmp:
+ auth_key: abc12345
+ org_name: YourOrg
+ state: present
+ v2c_enabled: yes
+ delegate_to: localhost
+
+- name: Disable SNMPv2
+ meraki_snmp:
+ auth_key: abc12345
+ org_name: YourOrg
+ state: present
+ v2c_enabled: no
+ delegate_to: localhost
+
+- name: Enable SNMPv3
+ meraki_snmp:
+ auth_key: abc12345
+ org_name: YourOrg
+ state: present
+ v3_enabled: true
+ v3_auth_mode: SHA
+ v3_auth_pass: ansiblepass
+ v3_priv_mode: AES128
+ v3_priv_pass: ansiblepass
+ peer_ips: 192.0.1.1;192.0.1.2
+ delegate_to: localhost
+
+- name: Set network access type to community string
+ meraki_snmp:
+ auth_key: abc1235
+ org_name: YourOrg
+ net_name: YourNet
+ state: present
+ access: community
+ community_string: abc123
+ delegate_to: localhost
+
+- name: Set network access type to username
+ meraki_snmp:
+ auth_key: abc1235
+ org_name: YourOrg
+ net_name: YourNet
+ state: present
+ access: users
+ users:
+ - username: ansibleuser
+ passphrase: ansiblepass
+ delegate_to: localhost
+'''
+
+RETURN = r'''
+data:
+ description: Information about SNMP settings.
+ type: complex
+ returned: always
+ contains:
+ hostname:
+ description: Hostname of SNMP server.
+ returned: success and no network specified.
+ type: str
+ sample: n1.meraki.com
+ peer_ips:
+ description: Semi-colon delimited list of IPs which can poll SNMP information.
+ returned: success and no network specified.
+ type: str
+ sample: 192.0.1.1
+ port:
+ description: Port number of SNMP.
+ returned: success and no network specified.
+ type: str
+ sample: 16100
+ v2c_enabled:
+ description: Shows enabled state of SNMPv2c
+ returned: success and no network specified.
+ type: bool
+ sample: true
+ v3_enabled:
+ description: Shows enabled state of SNMPv3
+ returned: success and no network specified.
+ type: bool
+ sample: true
+ v3_auth_mode:
+ description: The SNMP version 3 authentication mode either MD5 or SHA.
+ returned: success and no network specified.
+ type: str
+ sample: SHA
+ v3_priv_mode:
+ description: The SNMP version 3 privacy mode DES or AES128.
+ returned: success and no network specified.
+ type: str
+ sample: AES128
+ v2_community_string:
+ description: Automatically generated community string for SNMPv2c.
+ returned: When SNMPv2c is enabled and no network specified.
+ type: str
+ sample: o/8zd-JaSb
+ v3_user:
+ description: Automatically generated username for SNMPv3.
+ returned: When SNMPv3c is enabled and no network specified.
+ type: str
+ sample: o/8zd-JaSb
+ access:
+ description: Type of SNMP access.
+ type: str
+ returned: success, when network specified
+ community_string:
+ description: SNMP community string. Only relevant if C(access) is set to C(community).
+ type: str
+ returned: success, when network specified
+ users:
+ description: Information about users with access to SNMP. Only relevant if C(access) is set to C(users).
+ type: complex
+ contains:
+ username:
+ description: Username of user with access.
+ type: str
+ returned: success, when network specified
+ passphrase:
+ description: Passphrase for user SNMP access.
+ type: str
+ returned: success, when network specified
+'''
+
+from ansible.module_utils.basic import AnsibleModule, json
+from ansible.module_utils.common.dict_transformations import snake_dict_to_camel_dict
+from ansible_collections.cisco.meraki.plugins.module_utils.network.meraki.meraki import MerakiModule, meraki_argument_spec
+
+
+def get_snmp(meraki, org_id):
+ path = meraki.construct_path('get_all', org_id=org_id)
+ r = meraki.request(path,
+ method='GET',
+ )
+ if meraki.status == 200:
+ return r
+
+
+def set_snmp(meraki, org_id):
+ payload = dict()
+ if meraki.params['v2c_enabled'] is not None:
+ payload = {'v2cEnabled': meraki.params['v2c_enabled'],
+ }
+ if meraki.params['v3_enabled'] is True:
+ if len(meraki.params['v3_auth_pass']) < 8 or len(meraki.params['v3_priv_pass']) < 8:
+ meraki.fail_json(msg='v3_auth_pass and v3_priv_pass must both be at least 8 characters long.')
+ if meraki.params['v3_auth_mode'] is None or \
+ meraki.params['v3_auth_pass'] is None or \
+ meraki.params['v3_priv_mode'] is None or \
+ meraki.params['v3_priv_pass'] is None:
+ meraki.fail_json(msg='v3_auth_mode, v3_auth_pass, v3_priv_mode, and v3_auth_pass are required')
+ payload = {'v3Enabled': meraki.params['v3_enabled'],
+ 'v3AuthMode': meraki.params['v3_auth_mode'].upper(),
+ 'v3AuthPass': meraki.params['v3_auth_pass'],
+ 'v3PrivMode': meraki.params['v3_priv_mode'].upper(),
+ 'v3PrivPass': meraki.params['v3_priv_pass'],
+ }
+ if meraki.params['peer_ips'] is not None:
+ payload['peerIps'] = meraki.params['peer_ips']
+ elif meraki.params['v3_enabled'] is False:
+ payload = {'v3Enabled': False}
+ full_compare = snake_dict_to_camel_dict(payload)
+ path = meraki.construct_path('create', org_id=org_id)
+ snmp = get_snmp(meraki, org_id)
+ ignored_parameters = ['v3AuthPass', 'v3PrivPass', 'hostname', 'port', 'v2CommunityString', 'v3User']
+ if meraki.is_update_required(snmp, full_compare, optional_ignore=ignored_parameters):
+ if meraki.module.check_mode is True:
+ meraki.generate_diff(snmp, full_compare)
+ snmp.update(payload)
+ meraki.result['data'] = snmp
+ meraki.result['changed'] = True
+ meraki.exit_json(**meraki.result)
+ r = meraki.request(path,
+ method='PUT',
+ payload=json.dumps(payload))
+ if meraki.status == 200:
+ meraki.generate_diff(snmp, r)
+ meraki.result['changed'] = True
+ return r
+ else:
+ return snmp
+
+
+def main():
+
+ # define the available arguments/parameters that a user can pass to
+ # the module
+ user_arg_spec = dict(username=dict(type='str'),
+ passphrase=dict(type='str', no_log=True),
+ )
+
+ argument_spec = meraki_argument_spec()
+ argument_spec.update(state=dict(type='str', choices=['present', 'query'], default='present'),
+ v2c_enabled=dict(type='bool'),
+ v3_enabled=dict(type='bool'),
+ v3_auth_mode=dict(type='str', choices=['SHA', 'MD5']),
+ v3_auth_pass=dict(type='str', no_log=True),
+ v3_priv_mode=dict(type='str', choices=['DES', 'AES128']),
+ v3_priv_pass=dict(type='str', no_log=True),
+ peer_ips=dict(type='list', default=None, elements='str'),
+ access=dict(type='str', choices=['none', 'community', 'users']),
+ community_string=dict(type='str', no_log=True),
+ users=dict(type='list', default=None, elements='dict', options=user_arg_spec),
+ net_name=dict(type='str'),
+ net_id=dict(type='str'),
+ )
+
+ # the AnsibleModule object will be our abstraction working with Ansible
+ # this includes instantiation, a couple of common attr would be the
+ # args/params passed to the execution, as well as if the module
+ # supports check mode
+ module = AnsibleModule(argument_spec=argument_spec,
+ supports_check_mode=True,
+ )
+ meraki = MerakiModule(module, function='snmp')
+ meraki.params['follow_redirects'] = 'all'
+
+ query_urls = {'snmp': '/organizations/{org_id}/snmp'}
+ query_net_urls = {'snmp': '/networks/{net_id}/snmp'}
+ update_urls = {'snmp': '/organizations/{org_id}/snmp'}
+ update_net_urls = {'snmp': '/networks/{net_id}/snmp'}
+
+ meraki.url_catalog['get_all'].update(query_urls)
+ meraki.url_catalog['query_net_all'] = query_net_urls
+ meraki.url_catalog['create'] = update_urls
+ meraki.url_catalog['create_net'] = update_net_urls
+
+ payload = None
+
+ if not meraki.params['org_name'] and not meraki.params['org_id']:
+ meraki.fail_json(msg='org_name or org_id is required')
+
+ org_id = meraki.params['org_id']
+ if org_id is None:
+ org_id = meraki.get_org_id(meraki.params['org_name'])
+ net_id = meraki.params['net_id']
+ if net_id is None and meraki.params['net_name']:
+ nets = meraki.get_nets(org_id=org_id)
+ net_id = meraki.get_net_id(org_id, meraki.params['net_name'], data=nets)
+
+ if meraki.params['state'] == 'present':
+ if net_id is not None:
+ payload = {'access': meraki.params['access']}
+ if meraki.params['community_string'] is not None:
+ payload['communityString'] = meraki.params['community_string']
+ elif meraki.params['users'] is not None:
+ payload['users'] = meraki.params['users']
+
+ if meraki.params['state'] == 'query':
+ if net_id is None:
+ meraki.result['data'] = get_snmp(meraki, org_id)
+ else:
+ path = meraki.construct_path('query_net_all', net_id=net_id)
+ response = meraki.request(path, method='GET')
+ if meraki.status == 200:
+ meraki.result['data'] = response
+ elif meraki.params['state'] == 'present':
+ if net_id is None:
+ meraki.result['data'] = set_snmp(meraki, org_id)
+ else:
+ path = meraki.construct_path('query_net_all', net_id=net_id)
+ original = meraki.request(path, method='GET')
+ if meraki.is_update_required(original, payload):
+ path = meraki.construct_path('create_net', net_id=net_id)
+ response = meraki.request(path, method='PUT', payload=json.dumps(payload))
+ if meraki.status == 200:
+ if response['access'] == 'none':
+ meraki.result['data'] = {}
+ else:
+ meraki.result['data'] = response
+ meraki.result['changed'] = True
+ else:
+ meraki.result['data'] = original
+
+ # in the event of a successful module execution, you will want to
+ # simple AnsibleModule.exit_json(), passing the key/value results
+ meraki.exit_json(**meraki.result)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/ansible_collections/cisco/meraki/plugins/modules/meraki_syslog.py b/ansible_collections/cisco/meraki/plugins/modules/meraki_syslog.py
new file mode 100644
index 000000000..e94238750
--- /dev/null
+++ b/ansible_collections/cisco/meraki/plugins/modules/meraki_syslog.py
@@ -0,0 +1,317 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Copyright: (c) 2018, Kevin Breit (@kbreit) <kevin.breit@kevinbreit.net>
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+
+__metaclass__ = type
+
+ANSIBLE_METADATA = {
+ "metadata_version": "1.1",
+ "status": ["preview"],
+ "supported_by": "community",
+}
+
+DOCUMENTATION = r"""
+---
+module: meraki_syslog
+short_description: Manage syslog server settings in the Meraki cloud.
+description:
+- Allows for creation and management of Syslog servers within Meraki.
+notes:
+- Changes to existing syslog servers replaces existing configuration. If you need to add to an
+ existing configuration set state to query to gather the existing configuration and then modify or add.
+options:
+ auth_key:
+ description:
+ - Authentication key provided by the dashboard. Required if environmental variable MERAKI_KEY is not set.
+ type: str
+ state:
+ description:
+ - Query or edit syslog servers
+ - To delete a syslog server, do not include server in list of servers
+ choices: [present, query]
+ default: present
+ type: str
+ net_name:
+ description:
+ - Name of a network.
+ aliases: [name, network]
+ type: str
+ net_id:
+ description:
+ - ID number of a network.
+ type: str
+ servers:
+ description:
+ - List of syslog server settings
+ type: list
+ elements: dict
+ suboptions:
+ host:
+ description:
+ - IP address or hostname of Syslog server.
+ type: str
+ port:
+ description:
+ - Port number Syslog server is listening on.
+ default: "514"
+ type: int
+ roles:
+ description:
+ - List of applicable Syslog server roles.
+ - Choices can be one of Wireless Event log, Appliance event log, Switch event log, Air Marshal events, Flows, URLs, IDS alerts, Security events
+ type: list
+ elements: str
+
+author:
+ - Kevin Breit (@kbreit)
+extends_documentation_fragment: cisco.meraki.meraki
+"""
+
+EXAMPLES = r"""
+- name: Query syslog configurations on network named MyNet in the YourOrg organization
+ meraki_syslog:
+ auth_key: abc12345
+ state: query
+ org_name: YourOrg
+ net_name: MyNet
+ delegate_to: localhost
+
+- name: Add single syslog server with Appliance event log role
+ meraki_syslog:
+ auth_key: abc12345
+ state: present
+ org_name: YourOrg
+ net_name: MyNet
+ servers:
+ - host: 192.0.1.2
+ port: 514
+ roles:
+ - Appliance event log
+ delegate_to: localhost
+
+- name: Add multiple syslog servers
+ meraki_syslog:
+ auth_key: abc12345
+ state: present
+ org_name: YourOrg
+ net_name: MyNet
+ servers:
+ - host: 192.0.1.2
+ port: 514
+ roles:
+ - Appliance event log
+ - host: 192.0.1.3
+ port: 514
+ roles:
+ - Appliance event log
+ - Flows
+ delegate_to: localhost
+"""
+
+RETURN = r"""
+data:
+ description: Information about the created or manipulated object.
+ returned: info
+ type: complex
+ contains:
+ servers:
+ description: List of syslog servers.
+ returned: info
+ type: complex
+ contains:
+ host:
+ description: Hostname or IP address of syslog server.
+ returned: success
+ type: str
+ sample: 192.0.1.1
+ port:
+ description: Port number for syslog communication.
+ returned: success
+ type: str
+ sample: 443
+ roles:
+ description: List of roles assigned to syslog server.
+ returned: success
+ type: list
+ sample: "Wireless event log, URLs"
+"""
+
+from copy import deepcopy
+from ansible.module_utils.basic import AnsibleModule, json
+from ansible_collections.cisco.meraki.plugins.module_utils.network.meraki.meraki import (
+ MerakiModule,
+ meraki_argument_spec,
+)
+
+
+def sort_roles(syslog_servers):
+ """Accept a full payload and sort roles"""
+ # sorted_servers = list()
+ for i, server in enumerate(syslog_servers):
+ syslog_servers["servers"][i]["roles"] = sorted(
+ syslog_servers["servers"][i]["roles"]
+ )
+ # return sorted_servers
+
+
+def validate_role_choices(meraki, servers):
+ choices = [
+ "Wireless event log",
+ "Appliance event log",
+ "Switch event log",
+ "Air Marshal events",
+ "Flows",
+ "URLs",
+ "IDS alerts",
+ "Security events",
+ ]
+
+ # Change all choices to lowercase for comparison
+ for i in range(len(choices)):
+ choices[i] = choices[i].lower()
+ for server in range(len(servers)):
+ for role in servers[server]["roles"]:
+ if role.lower() not in choices:
+ meraki.fail_json(
+ msg="Invalid role found in {0}.".format(servers[server]["host"])
+ )
+
+
+def normalize_roles(meraki, servers):
+ if len(servers["servers"]) > 0:
+ for server in range(len(servers)):
+ for role in range(len(servers["servers"][server]["roles"])):
+ servers["servers"][server]["roles"][role] = servers["servers"][server][
+ "roles"
+ ][role].lower()
+ return servers
+
+
+def main():
+
+ # define the available arguments/parameters that a user can pass to
+ # the module
+
+ server_arg_spec = dict(
+ host=dict(type="str"),
+ port=dict(type="int", default="514"),
+ roles=dict(
+ type="list",
+ elements="str",
+ ),
+ )
+
+ argument_spec = meraki_argument_spec()
+ argument_spec.update(
+ net_id=dict(type="str"),
+ servers=dict(type="list", elements="dict", options=server_arg_spec),
+ state=dict(type="str", choices=["present", "query"], default="present"),
+ net_name=dict(type="str", aliases=["name", "network"]),
+ )
+
+ # the AnsibleModule object will be our abstraction working with Ansible
+ # this includes instantiation, a couple of common attr would be the
+ # args/params passed to the execution, as well as if the module
+ # supports check mode
+ module = AnsibleModule(
+ argument_spec=argument_spec,
+ supports_check_mode=True,
+ )
+
+ meraki = MerakiModule(module, function="syslog")
+ module.params["follow_redirects"] = "all"
+ payload = None
+
+ syslog_urls = {"syslog": "/networks/{net_id}/syslogServers"}
+ meraki.url_catalog["query_update"] = syslog_urls
+
+ if not meraki.params["org_name"] and not meraki.params["org_id"]:
+ meraki.fail_json(msg="org_name or org_id parameters are required")
+ if meraki.params["state"] != "query":
+ if not meraki.params["net_name"] and not meraki.params["net_id"]:
+ meraki.fail_json(
+ msg="net_name or net_id is required for present or absent states"
+ )
+ if meraki.params["net_name"] and meraki.params["net_id"]:
+ meraki.fail_json(msg="net_name and net_id are mutually exclusive")
+
+ # if the user is working with this module in only check mode we do not
+ # want to make any changes to the environment, just return the current
+ # state with no modifications
+
+ # manipulate or modify the state as needed (this is going to be the
+ # part where your module will do what it needs to do)
+
+ org_id = meraki.params["org_id"]
+ if not org_id:
+ org_id = meraki.get_org_id(meraki.params["org_name"])
+ net_id = meraki.params["net_id"]
+ if net_id is None:
+ nets = meraki.get_nets(org_id=org_id)
+ net_id = meraki.get_net_id(net_name=meraki.params["net_name"], data=nets)
+
+ if meraki.params["state"] == "query":
+ path = meraki.construct_path("query_update", net_id=net_id)
+ r = meraki.request(path, method="GET")
+ if meraki.status == 200:
+ meraki.result["data"] = r
+ elif meraki.params["state"] == "present":
+ # Validate roles
+ validate_role_choices(meraki, meraki.params["servers"])
+
+ # Construct payload
+ payload = dict()
+ payload["servers"] = meraki.params["servers"]
+
+ # Convert port numbers to string for idempotency checks
+ for server in payload["servers"]:
+ if server["port"]:
+ server["port"] = str(server["port"])
+ path = meraki.construct_path("query_update", net_id=net_id)
+ r = meraki.request(path, method="GET")
+ if meraki.status == 200:
+ original = r
+
+ # Roles must be sorted since out of order responses may break idempotency check
+ # Need to check to make sure servers are actually defined
+ if original is not None:
+ if len(original["servers"]) > 0:
+ sort_roles(original)
+ if payload is not None:
+ if len(payload["servers"]) > 0:
+ sort_roles(payload)
+
+ # Sanitize roles for comparison
+ sanitized_original = normalize_roles(meraki, deepcopy(original))
+ sanitized_payload = normalize_roles(meraki, deepcopy(payload))
+
+ if meraki.is_update_required(sanitized_original, sanitized_payload):
+ if meraki.module.check_mode is True:
+ meraki.generate_diff(original, payload)
+ original.update(payload)
+ meraki.result["data"] = original
+ meraki.result["changed"] = True
+ meraki.exit_json(**meraki.result)
+ path = meraki.construct_path("query_update", net_id=net_id)
+ r = meraki.request(path, method="PUT", payload=json.dumps(payload))
+ if meraki.status == 200:
+ meraki.generate_diff(original, r)
+ meraki.result["data"] = r
+ meraki.result["changed"] = True
+ else:
+ if meraki.module.check_mode is True:
+ meraki.result["data"] = original
+ meraki.exit_json(**meraki.result)
+ meraki.result["data"] = original
+
+ # in the event of a successful module execution, you will want to
+ # simple AnsibleModule.exit_json(), passing the key/value results
+ meraki.exit_json(**meraki.result)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/ansible_collections/cisco/meraki/plugins/modules/meraki_webhook.py b/ansible_collections/cisco/meraki/plugins/modules/meraki_webhook.py
new file mode 100644
index 000000000..9b8ce2f67
--- /dev/null
+++ b/ansible_collections/cisco/meraki/plugins/modules/meraki_webhook.py
@@ -0,0 +1,431 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Copyright: (c) 2019, Kevin Breit (@kbreit) <kevin.breit@kevinbreit.net>
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+
+__metaclass__ = type
+
+ANSIBLE_METADATA = {
+ "metadata_version": "1.1",
+ "status": ["preview"],
+ "supported_by": "community",
+}
+
+DOCUMENTATION = r"""
+---
+module: meraki_webhook
+short_description: Manage webhooks configured in the Meraki cloud
+description:
+- Configure and query information about webhooks within the Meraki cloud.
+notes:
+- Some of the options are likely only used for developers within Meraki.
+options:
+ state:
+ description:
+ - Specifies whether object should be queried, created/modified, or removed.
+ choices: [absent, present, query]
+ default: query
+ type: str
+ net_name:
+ description:
+ - Name of network which configuration is applied to.
+ aliases: [network]
+ type: str
+ net_id:
+ description:
+ - ID of network which configuration is applied to.
+ type: str
+ name:
+ description:
+ - Name of webhook.
+ type: str
+ shared_secret:
+ description:
+ - Secret password to use when accessing webhook.
+ type: str
+ url:
+ description:
+ - URL to access when calling webhook.
+ type: str
+ webhook_id:
+ description:
+ - Unique ID of webhook.
+ type: str
+ payload_template_name:
+ description:
+ - The name of the payload template
+ type: str
+ payload_template_id:
+ description:
+ - The ID of the payload template. Overrides payload_template_name if passed too.
+ type: str
+ test:
+ description:
+ - Indicates whether to test or query status.
+ type: str
+ choices: [test]
+ test_id:
+ description:
+ - ID of webhook test query.
+ type: str
+author:
+- Kevin Breit (@kbreit)
+extends_documentation_fragment: cisco.meraki.meraki
+"""
+
+EXAMPLES = r"""
+- name: Create webhook
+ meraki_webhook:
+ auth_key: abc123
+ state: present
+ org_name: YourOrg
+ net_name: YourNet
+ name: Test_Hook
+ url: https://webhook.url/
+ shared_secret: shhhdonttellanyone
+ payload_template_name: 'Slack (included)'
+ delegate_to: localhost
+
+- name: Query one webhook
+ meraki_webhook:
+ auth_key: abc123
+ state: query
+ org_name: YourOrg
+ net_name: YourNet
+ name: Test_Hook
+ delegate_to: localhost
+
+- name: Query all webhooks
+ meraki_webhook:
+ auth_key: abc123
+ state: query
+ org_name: YourOrg
+ net_name: YourNet
+ delegate_to: localhost
+
+- name: Delete webhook
+ meraki_webhook:
+ auth_key: abc123
+ state: absent
+ org_name: YourOrg
+ net_name: YourNet
+ name: Test_Hook
+ delegate_to: localhost
+
+- name: Test webhook
+ meraki_webhook:
+ auth_key: abc123
+ state: present
+ org_name: YourOrg
+ net_name: YourNet
+ test: test
+ url: https://webhook.url/abc123
+ delegate_to: localhost
+
+- name: Get webhook status
+ meraki_webhook:
+ auth_key: abc123
+ state: present
+ org_name: YourOrg
+ net_name: YourNet
+ test: status
+ test_id: abc123531234
+ delegate_to: localhost
+"""
+
+RETURN = r"""
+data:
+ description: List of administrators.
+ returned: success
+ type: complex
+ contains:
+ id:
+ description: Unique ID of webhook.
+ returned: success
+ type: str
+ sample: aHR0cHM6Ly93ZWJob22LnvpdGUvOGViNWI3NmYtYjE2Ny00Y2I4LTlmYzQtND32Mj3F5NzIaMjQ0
+ name:
+ description: Descriptive name of webhook.
+ returned: success
+ type: str
+ sample: Test_Hook
+ networkId:
+ description: ID of network containing webhook object.
+ returned: success
+ type: str
+ sample: N_12345
+ shared_secret:
+ description: Password for webhook.
+ returned: success
+ type: str
+ sample: VALUE_SPECIFIED_IN_NO_LOG_PARAMETER
+ url:
+ description: URL of webhook endpoint.
+ returned: success
+ type: str
+ sample: https://webhook.url/abc123
+ status:
+ description: Status of webhook test.
+ returned: success, when testing webhook
+ type: str
+ sample: enqueued
+ payloadTemplate:
+ description: The payload template used when posting data to the HTTP server.
+ returned: success
+ type: str
+ sample:
+ payloadTemplateId: wpt_00001
+ name: Meraki (included)
+"""
+
+from ansible.module_utils.basic import AnsibleModule, json
+from ansible_collections.cisco.meraki.plugins.module_utils.network.meraki.meraki import (
+ MerakiModule,
+ meraki_argument_spec,
+)
+
+
+def get_webhook_id(name, webhooks):
+ for webhook in webhooks:
+ if name == webhook["name"]:
+ return webhook["id"]
+ return None
+
+
+def get_all_webhooks(meraki, net_id):
+ path = meraki.construct_path("get_all", net_id=net_id)
+ response = meraki.request(path, method="GET")
+ if meraki.status == 200:
+ return response
+
+
+def sanitize_no_log_values(meraki):
+ try:
+ meraki.result["diff"]["before"][
+ "shared_secret"
+ ] = "VALUE_SPECIFIED_IN_NO_LOG_PARAMETER"
+ except KeyError:
+ pass
+ try:
+ for i in meraki.result['data']:
+ i[
+ "shared_secret"
+ ] = "VALUE_SPECIFIED_IN_NO_LOG_PARAMETER"
+ except (KeyError, TypeError):
+ pass
+ try:
+ meraki.result["data"]["shared_secret"] = "VALUE_SPECIFIED_IN_NO_LOG_PARAMETER"
+ except (KeyError, TypeError):
+ pass
+
+
+def get_webhook_payload_templates(meraki, net_id):
+ path = meraki.construct_path("get_payload_templates", net_id=net_id)
+ response = meraki.request(path, "GET")
+ if meraki.status != 200:
+ meraki.fail_json(msg="Unable to get webhook payload templates")
+ return response
+
+
+def get_webhook_payload_template_id(meraki, templates, name):
+ for template in templates:
+ if template["name"] == name:
+ return template["payloadTemplateId"]
+ meraki.fail_json(msg="No payload template found with the name {0}".format(name))
+
+
+def main():
+ # define the available arguments/parameters that a user can pass to
+ # the module
+
+ argument_spec = meraki_argument_spec()
+ argument_spec.update(
+ state=dict(type="str", choices=["absent", "present", "query"], default="query"),
+ net_name=dict(type="str", aliases=["network"]),
+ net_id=dict(type="str"),
+ name=dict(type="str"),
+ url=dict(type="str"),
+ shared_secret=dict(type="str", no_log=True),
+ webhook_id=dict(type="str"),
+ test=dict(type="str", choices=["test"]),
+ test_id=dict(type="str"),
+ payload_template_name=dict(type="str"),
+ payload_template_id=dict(type="str"),
+ )
+
+ # the AnsibleModule object will be our abstraction working with Ansible
+ # this includes instantiation, a couple of common attr would be the
+ # args/params passed to the execution, as well as if the module
+ # supports check mode
+ module = AnsibleModule(
+ argument_spec=argument_spec,
+ supports_check_mode=True,
+ )
+ meraki = MerakiModule(module, function="webhooks")
+
+ meraki.params["follow_redirects"] = "all"
+
+ query_url = {"webhooks": "/networks/{net_id}/webhooks/httpServers"}
+ query_one_url = {"webhooks": "/networks/{net_id}/webhooks/httpServers/{hookid}"}
+ create_url = {"webhooks": "/networks/{net_id}/webhooks/httpServers"}
+ update_url = {"webhooks": "/networks/{net_id}/webhooks/httpServers/{hookid}"}
+ delete_url = {"webhooks": "/networks/{net_id}/webhooks/httpServers/{hookid}"}
+ test_url = {"webhooks": "/networks/{net_id}/webhooks/webhookTests"}
+ test_status_url = {"webhooks": "/networks/{net_id}/webhooks/webhookTests/{testid}"}
+ query_payload_templates = {
+ "webhooks": "/networks/{net_id}/webhooks/payloadTemplates"
+ }
+ meraki.url_catalog["get_all"].update(query_url)
+ meraki.url_catalog["get_one"].update(query_one_url)
+ meraki.url_catalog["create"] = create_url
+ meraki.url_catalog["update"] = update_url
+ meraki.url_catalog["delete"] = delete_url
+ meraki.url_catalog["test"] = test_url
+ meraki.url_catalog["test_status"] = test_status_url
+ meraki.url_catalog["get_payload_templates"] = query_payload_templates
+
+ org_id = meraki.params["org_id"]
+ if org_id is None:
+ org_id = meraki.get_org_id(meraki.params["org_name"])
+
+ net_id = meraki.params["net_id"]
+ if net_id is None:
+ nets = meraki.get_nets(org_id=org_id)
+ net_id = meraki.get_net_id(net_name=meraki.params["net_name"], data=nets)
+
+ templates = get_webhook_payload_templates(meraki, net_id)
+ payload_template_id = meraki.params["payload_template_id"]
+ if (
+ payload_template_id is None
+ and meraki.params["payload_template_name"] is not None
+ ):
+ payload_template_id = get_webhook_payload_template_id(
+ meraki, templates, name=meraki.params["payload_template_name"]
+ )
+
+ webhook_id = meraki.params["webhook_id"]
+ webhooks = None
+ if webhook_id is None and meraki.params["name"]:
+ webhooks = get_all_webhooks(meraki, net_id)
+ webhook_id = get_webhook_id(meraki.params["name"], webhooks)
+
+ if meraki.params["state"] == "present" and meraki.params["test"] is None:
+ payload = {
+ "name": meraki.params["name"],
+ "url": meraki.params["url"],
+ }
+ if meraki.params["shared_secret"] is not None:
+ payload["sharedSecret"] = meraki.params["shared_secret"]
+ if payload_template_id is not None:
+ payload["payloadTemplate"] = {"payloadTemplateId": payload_template_id}
+
+ if meraki.params["state"] == "query":
+ if webhook_id is not None: # Query a single webhook
+ path = meraki.construct_path(
+ "get_one", net_id=net_id, custom={"hookid": webhook_id}
+ )
+ response = meraki.request(path, method="GET")
+ if meraki.status == 200:
+ meraki.result["data"] = response
+ sanitize_no_log_values(meraki)
+ meraki.exit_json(**meraki.result)
+ elif meraki.params["test_id"] is not None:
+ path = meraki.construct_path(
+ "test_status",
+ net_id=net_id,
+ custom={"testid": meraki.params["test_id"]},
+ )
+ response = meraki.request(path, method="GET")
+ if meraki.status == 200:
+ meraki.result["data"] = response
+ sanitize_no_log_values(meraki)
+ meraki.exit_json(**meraki.result)
+ else:
+ path = meraki.construct_path("get_all", net_id=net_id)
+ response = meraki.request(path, method="GET")
+ if meraki.status == 200:
+ meraki.result["data"] = response
+ # meraki.fail_json(msg=meraki.result)
+ sanitize_no_log_values(meraki)
+ meraki.exit_json(**meraki.result)
+ elif meraki.params["state"] == "present":
+ if meraki.params["test"] == "test":
+ payload = {"url": meraki.params["url"]}
+ path = meraki.construct_path("test", net_id=net_id)
+ response = meraki.request(path, method="POST", payload=json.dumps(payload))
+ if meraki.status == 201:
+ meraki.result["data"] = response
+ meraki.exit_json(**meraki.result)
+ if webhook_id is None: # New webhook needs to be created
+ if meraki.check_mode is True:
+ meraki.result["data"] = payload
+ meraki.result["data"]["networkId"] = net_id
+ meraki.result["changed"] = True
+ meraki.exit_json(**meraki.result)
+ path = meraki.construct_path("create", net_id=net_id)
+ response = meraki.request(path, method="POST", payload=json.dumps(payload))
+ if meraki.status == 201:
+ meraki.result["data"] = response
+ meraki.result["changed"] = True
+ else: # Need to update
+ path = meraki.construct_path(
+ "get_one", net_id=net_id, custom={"hookid": webhook_id}
+ )
+ original = meraki.request(path, method="GET")
+ if meraki.is_update_required(original, payload):
+ if meraki.check_mode is True:
+ meraki.generate_diff(original, payload)
+ sanitize_no_log_values(meraki)
+ original.update(payload)
+ meraki.result["data"] = original
+ meraki.result["changed"] = True
+ meraki.exit_json(**meraki.result)
+ path = meraki.construct_path(
+ "update", net_id=net_id, custom={"hookid": webhook_id}
+ )
+ response = meraki.request(
+ path, method="PUT", payload=json.dumps(payload)
+ )
+ if meraki.status == 200:
+ # Not all fields are included so it needs to be checked to avoid comparing same thing
+ if meraki.is_update_required(original, response):
+ meraki.generate_diff(original, response)
+ sanitize_no_log_values(meraki)
+ meraki.result["data"] = response
+ meraki.result["changed"] = True
+ else:
+ meraki.result["data"] = original
+ elif meraki.params["state"] == "absent":
+ if webhook_id is None: # Make sure it is downloaded
+ if webhooks is None:
+ webhooks = get_all_webhooks(meraki, net_id)
+ webhook_id = get_webhook_id(meraki.params["name"], webhooks)
+ if webhook_id is None:
+ meraki.fail_json(
+ msg="There is no webhook with the name {0}".format(
+ meraki.params["name"]
+ )
+ )
+ if webhook_id: # Test to see if it exists
+ if meraki.module.check_mode is True:
+ meraki.result["data"] = None
+ meraki.result["changed"] = True
+ meraki.exit_json(**meraki.result)
+ path = meraki.construct_path(
+ "delete", net_id=net_id, custom={"hookid": webhook_id}
+ )
+ response = meraki.request(path, method="DELETE")
+ if meraki.status == 204:
+ meraki.result["data"] = response
+ meraki.result["changed"] = True
+
+ # in the event of a successful module execution, you will want to
+ # simple AnsibleModule.exit_json(), passing the key/value results
+ meraki.exit_json(**meraki.result)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/ansible_collections/cisco/meraki/plugins/modules/meraki_webhook_payload_template.py b/ansible_collections/cisco/meraki/plugins/modules/meraki_webhook_payload_template.py
new file mode 100644
index 000000000..d7cc5a190
--- /dev/null
+++ b/ansible_collections/cisco/meraki/plugins/modules/meraki_webhook_payload_template.py
@@ -0,0 +1,352 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Copyright: (c) 2022, Joshua Coronado (@joshuajcoronado) <joshua@coronado.io>
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+
+__metaclass__ = type
+
+ANSIBLE_METADATA = {
+ "metadata_version": "1.1",
+ "status": ["preview"],
+ "supported_by": "community",
+}
+
+DOCUMENTATION = r"""
+---
+module: meraki_webhook_payload_template
+short_description: Manage webhook payload templates for a network in the Meraki cloud
+description:
+- Allows for querying, deleting, creating, and updating of webhook payload templates.
+options:
+ state:
+ description:
+ - Specifies whether payload template should be queried, created, modified, or deleted.
+ choices: ['absent', 'query', 'present']
+ default: query
+ type: str
+ name:
+ description:
+ - Name of the template.
+ type: str
+ net_name:
+ description:
+ - Name of network containing access points.
+ type: str
+ net_id:
+ description:
+ - ID of network containing access points.
+ type: str
+ body:
+ description:
+ - The liquid template used for the body of the webhook message.
+ type: str
+ headers:
+ description:
+ - List of the liquid templates used with the webhook headers.
+ type: list
+ elements: dict
+ default: []
+ suboptions:
+ name:
+ description:
+ - The name of the header template.
+ type: str
+ template:
+ description:
+ - The liquid template for the headers
+ type: str
+author:
+- Joshua Coronado (@joshuajcoronado)
+extends_documentation_fragment: cisco.meraki.meraki
+"""
+
+EXAMPLES = r"""
+- name: Query all configuration templates
+ meraki_webhook_payload_template:
+ auth_key: abc12345
+ org_name: YourOrg
+ state: query
+ delegate_to: localhost
+
+- name: Query specific configuration templates
+ meraki_webhook_payload_template:
+ auth_key: abc12345
+ org_name: YourOrg
+ state: query
+ name: Twitter
+ delegate_to: localhost
+
+- name: Create payload template
+ meraki_webhook_payload_template:
+ auth_key: abc12345
+ org_name: YourOrg
+ state: query
+ name: TestTemplate
+ body: Testbody
+ headers:
+ - name: testheader
+ template: testheadertemplate
+ delegate_to: localhost
+
+- name: Delete a configuration template
+ meraki_config_template:
+ auth_key: abc123
+ state: absent
+ org_name: YourOrg
+ name: TestTemplate
+ delegate_to: localhost
+"""
+
+RETURN = r"""
+data:
+ description: Information about queried object.
+ returned: success
+ type: complex
+ contains:
+ name:
+ description:
+ - The name of the template
+ returned: success
+ type: str
+ sample: testTemplate
+ body:
+ description:
+ - The liquid template used for the body of the webhook message.
+ returned: success
+ type: str
+ sample: {"event_type":"{{alertTypeId}}","client_payload":{"text":"{{alertData}}"}}
+ headers:
+ description: List of the liquid templates used with the webhook headers.
+ returned: success
+ type: list
+ contains:
+ name:
+ description:
+ - The name of the template
+ returned: success
+ type: str
+ sample: testTemplate
+ template:
+ description:
+ - The liquid template for the header
+ returned: success
+ type: str
+ sample: "Bearer {{sharedSecret}}"
+"""
+
+from ansible.module_utils.basic import AnsibleModule, json
+from ansible_collections.cisco.meraki.plugins.module_utils.network.meraki.meraki import (
+ MerakiModule,
+ meraki_argument_spec,
+)
+
+
+def get_webhook_payload_templates(meraki, net_id):
+ path = meraki.construct_path("get_all", net_id=net_id)
+ response = meraki.request(path, "GET")
+ if meraki.status != 200:
+ meraki.fail_json(msg="Unable to get webhook payload templates")
+ return response
+
+
+def delete_template(meraki, net_id, template_id):
+ changed = True
+ if meraki.check_mode:
+ return {}, changed
+ else:
+ path = meraki.construct_path(
+ "update", net_id=net_id, custom={"template_id": template_id}
+ )
+ response = meraki.request(path, method="DELETE")
+ if meraki.status != 204:
+ meraki.fail_json(msg="Unable to remove webhook payload templates")
+ return response, changed
+
+
+def create_template(meraki, net_id, template):
+ changed = True
+
+ if meraki.check_mode:
+ return template, changed
+ else:
+ path = meraki.construct_path("get_all", net_id=net_id)
+ response = meraki.request(path, "POST", payload=json.dumps(template))
+ if meraki.status != 201:
+ meraki.fail_json(msg="Unable to create webhook payload template")
+ return response, changed
+
+
+def update_template(meraki, net_id, template, payload):
+ changed = False
+
+ if template["body"] != payload["body"]:
+ changed = True
+
+ if meraki.is_update_required(template["headers"], payload["headers"]):
+ changed = True
+
+ if changed:
+ meraki.generate_diff(template, payload)
+ if meraki.check_mode:
+ return payload, changed
+ else:
+ path = meraki.construct_path(
+ "update",
+ net_id=net_id,
+ custom={"template_id": template["payloadTemplateId"]},
+ )
+ response = meraki.request(
+ path, method="PUT", payload=json.dumps(payload)
+ )
+ if meraki.status != 200:
+ meraki.fail_json(
+ msg="Unable to update webhook payload template"
+ )
+ return response, changed
+
+ return template, changed
+
+
+def main():
+
+ # define the available arguments/parameters that a user can pass to
+ # the module
+ argument_spec = meraki_argument_spec()
+ argument_spec.update(
+ state=dict(
+ type="str", choices=["absent", "query", "present"], default="query"
+ ),
+ name=dict(type="str", default=None),
+ net_name=dict(type="str"),
+ net_id=dict(type="str"),
+ body=dict(type="str", default=None),
+ headers=dict(
+ type="list",
+ default=[],
+ elements="dict",
+ options=dict(
+ name=dict(type="str"),
+ template=dict(type="str"),
+ ),
+ ),
+ )
+
+ module = AnsibleModule(
+ argument_spec=argument_spec,
+ supports_check_mode=True,
+ )
+ meraki = MerakiModule(module, function="webhook_payload_template")
+ meraki.params["follow_redirects"] = "all"
+
+ query_all_urls = {
+ "webhook_payload_template": "/networks/{net_id}/webhooks/payloadTemplates"
+ }
+ update_urls = {
+ "webhook_payload_template": "/networks/{net_id}/webhooks/payloadTemplates/{template_id}"
+ }
+
+ meraki.url_catalog["get_all"].update(query_all_urls)
+ meraki.url_catalog["update"] = update_urls
+
+ org_id = meraki.params["org_id"]
+ if meraki.params["org_name"]:
+ org_id = meraki.get_org_id(meraki.params["org_name"])
+ net_id = meraki.params["net_id"]
+
+ if net_id is None:
+ if meraki.params["net_name"] is not None:
+ nets = meraki.get_nets(org_id=org_id)
+ net_id = meraki.get_net_id(
+ net_name=meraki.params["net_name"], data=nets
+ )
+
+ templates = {
+ template["name"]: template
+ for template in get_webhook_payload_templates(meraki, net_id)
+ }
+
+ if meraki.params["state"] == "query":
+ meraki.result["changed"] = False
+
+ if meraki.params["name"]:
+ if meraki.params["name"] in templates:
+ meraki.result["data"] = templates[meraki.params["name"]]
+ else:
+ meraki.fail_json(
+ msg="Unable to get webhook payload template named: {0}".format(
+ meraki.params["name"]
+ )
+ )
+ else:
+ meraki.result["data"] = templates
+
+ elif meraki.params["state"] == "present":
+ if meraki.params["name"] is None:
+ meraki.fail_json(msg="name is a required parameter")
+
+ if meraki.params["body"] is None:
+ meraki.fail_json(
+ msg="body is a required parameter when state is present"
+ )
+
+ headers = []
+
+ for header in meraki.params["headers"]:
+ for key in ["name", "template"]:
+ if key not in header:
+ meraki.fail_json(
+ msg="{0} is a required parameter for a header".format(
+ key
+ )
+ )
+ if not header[key]:
+ meraki.fail_json(
+ msg="{0} in header must be a string".format(key)
+ )
+ headers.append(
+ dict(name=header["name"], template=header["template"])
+ )
+
+ payload = {
+ "name": meraki.params["name"],
+ "body": meraki.params["body"],
+ "headers": meraki.params["headers"],
+ }
+
+ if meraki.params["name"] in templates:
+ (
+ meraki.result["data"],
+ meraki.result["changed"],
+ ) = update_template(
+ meraki, net_id, templates[meraki.params["name"]], payload
+ )
+ else:
+ (
+ meraki.result["data"],
+ meraki.result["changed"],
+ ) = create_template(meraki, net_id, payload)
+
+ elif meraki.params["state"] == "absent":
+ if meraki.params["name"] in templates:
+ (
+ meraki.result["data"],
+ meraki.result["changed"],
+ ) = delete_template(
+ meraki,
+ net_id,
+ templates[meraki.params["name"]]["payloadTemplateId"],
+ )
+ else:
+ meraki.result["changed"] = False
+ meraki.result["data"] = {}
+
+ # in the event of a successful module execution, you will want to
+ # simple AnsibleModule.exit_json(), passing the key/value results
+ meraki.exit_json(**meraki.result)
+
+
+if __name__ == "__main__":
+ main()