diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-13 12:04:41 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-13 12:04:41 +0000 |
commit | 975f66f2eebe9dadba04f275774d4ab83f74cf25 (patch) | |
tree | 89bd26a93aaae6a25749145b7e4bca4a1e75b2be /ansible_collections/cisco/meraki/plugins/modules | |
parent | Initial commit. (diff) | |
download | ansible-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')
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() |