diff options
Diffstat (limited to 'collections-debian-merged/ansible_collections/cisco/meraki/plugins')
52 files changed, 17995 insertions, 0 deletions
diff --git a/collections-debian-merged/ansible_collections/cisco/meraki/plugins/doc_fragments/__init__.py b/collections-debian-merged/ansible_collections/cisco/meraki/plugins/doc_fragments/__init__.py new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/collections-debian-merged/ansible_collections/cisco/meraki/plugins/doc_fragments/__init__.py diff --git a/collections-debian-merged/ansible_collections/cisco/meraki/plugins/doc_fragments/meraki.py b/collections-debian-merged/ansible_collections/cisco/meraki/plugins/doc_fragments/meraki.py new file mode 100644 index 00000000..d6d456f1 --- /dev/null +++ b/collections-debian-merged/ansible_collections/cisco/meraki/plugins/doc_fragments/meraki.py @@ -0,0 +1,84 @@ +# -*- 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 + + +class ModuleDocFragment(object): + # Standard files for documentation fragment + DOCUMENTATION = r''' +notes: +- 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. +- As of Ansible 2.9, Meraki modules output keys as snake case. To use camel case, set the C(ANSIBLE_MERAKI_FORMAT) environment variable to C(camelcase). +- Ansible's Meraki modules will stop supporting camel case output in Ansible 2.13. Please update your playbooks. +- Check Mode downloads the current configuration from the dashboard, then compares changes against this download. Check Mode will report changed if + there are differences in the configurations, but does not submit changes to the API for validation of change. +options: + auth_key: + description: + - Authentication key provided by the dashboard. Required if environmental variable C(MERAKI_KEY) is not set. + type: str + required: yes + host: + description: + - Hostname for Meraki dashboard. + - Can be used to access regional Meraki environments, such as China. + type: str + default: api.meraki.com + use_proxy: + description: + - If C(no), it will not use a proxy, even if one is defined in an environment variable on the target hosts. + type: bool + default: False + use_https: + description: + - If C(no), it will use HTTP. Otherwise it will use HTTPS. + - Only useful for internal Meraki developers. + type: bool + default: yes + output_format: + description: + - Instructs module whether response keys should be snake case (ex. C(net_id)) or camel case (ex. C(netId)). + type: str + choices: [snakecase, camelcase] + default: snakecase + output_level: + description: + - Set amount of debug output during module execution. + type: str + choices: [ debug, normal ] + default: normal + timeout: + description: + - Time to timeout for HTTP requests. + type: int + default: 30 + validate_certs: + description: + - Whether to validate HTTP certificates. + type: bool + default: yes + org_name: + description: + - Name of organization. + type: str + aliases: [ organization ] + org_id: + description: + - ID of organization. + type: str + rate_limit_retry_time: + description: + - Number of seconds to retry if rate limiter is triggered. + type: int + default: 165 + internal_error_retry_time: + description: + - Number of seconds to retry if server returns an internal server error. + type: int + default: 60 +''' diff --git a/collections-debian-merged/ansible_collections/cisco/meraki/plugins/module_utils/network/meraki/__init__.py b/collections-debian-merged/ansible_collections/cisco/meraki/plugins/module_utils/network/meraki/__init__.py new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/collections-debian-merged/ansible_collections/cisco/meraki/plugins/module_utils/network/meraki/__init__.py diff --git a/collections-debian-merged/ansible_collections/cisco/meraki/plugins/module_utils/network/meraki/meraki.py b/collections-debian-merged/ansible_collections/cisco/meraki/plugins/module_utils/network/meraki/meraki.py new file mode 100644 index 00000000..53b14caf --- /dev/null +++ b/collections-debian-merged/ansible_collections/cisco/meraki/plugins/module_utils/network/meraki/meraki.py @@ -0,0 +1,517 @@ +# -*- 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 + +import time +import re +from ansible.module_utils.basic import json, env_fallback +from ansible.module_utils.common.dict_transformations import camel_dict_to_snake_dict, snake_dict_to_camel_dict, recursive_diff +from ansible.module_utils.urls import fetch_url +from ansible.module_utils.six.moves.urllib.parse import urlencode +from ansible.module_utils._text import to_native + + +RATE_LIMIT_RETRY_MULTIPLIER = 3 +INTERNAL_ERROR_RETRY_MULTIPLIER = 3 + + +def meraki_argument_spec(): + return dict(auth_key=dict(type='str', no_log=True, fallback=(env_fallback, ['MERAKI_KEY']), required=True), + host=dict(type='str', default='api.meraki.com'), + use_proxy=dict(type='bool', default=False), + use_https=dict(type='bool', default=True), + validate_certs=dict(type='bool', default=True), + output_format=dict(type='str', choices=['camelcase', 'snakecase'], default='snakecase', fallback=(env_fallback, ['ANSIBLE_MERAKI_FORMAT'])), + output_level=dict(type='str', default='normal', choices=['normal', 'debug']), + timeout=dict(type='int', default=30), + org_name=dict(type='str', aliases=['organization']), + org_id=dict(type='str'), + rate_limit_retry_time=dict(type='int', default=165), + internal_error_retry_time=dict(type='int', default=60) + ) + + +class RateLimitException(Exception): + def __init__(self, *args, **kwargs): + Exception.__init__(self, *args, **kwargs) + + +class InternalErrorException(Exception): + def __init__(self, *args, **kwargs): + Exception.__init__(self, *args, **kwargs) + + +class HTTPError(Exception): + def __init__(self, *args, **kwargs): + Exception.__init__(self, *args, **kwargs) + + +class MerakiModule(object): + + def __init__(self, module, function=None): + self.module = module + self.params = module.params + self.result = dict(changed=False) + self.headers = dict() + self.function = function + self.orgs = None + self.nets = None + self.org_id = None + self.net_id = None + self.check_mode = module.check_mode + self.key_map = {} + self.request_attempts = 0 + + # normal output + self.existing = None + + # info output + self.config = dict() + self.original = None + self.proposed = dict() + self.merged = None + self.ignored_keys = ['id', 'organizationId'] + + # debug output + self.filter_string = '' + self.method = None + self.path = None + self.response = None + self.status = None + self.url = None + self.body = None + + # rate limiting statistics + self.retry = 0 + self.retry_time = 0 + + # If URLs need to be modified or added for specific purposes, use .update() on the url_catalog dictionary + self.get_urls = {'organizations': '/organizations', + 'network': '/organizations/{org_id}/networks', + 'admins': '/organizations/{org_id}/admins', + 'configTemplates': '/organizations/{org_id}/configTemplates', + 'samlymbols': '/organizations/{org_id}/samlRoles', + 'ssids': '/networks/{net_id}/ssids', + 'groupPolicies': '/networks/{net_id}/groupPolicies', + 'staticRoutes': '/networks/{net_id}/staticRoutes', + 'vlans': '/networks/{net_id}/vlans', + 'devices': '/networks/{net_id}/devices', + } + + # Used to retrieve only one item + self.get_one_urls = {'organizations': '/organizations/{org_id}', + 'network': '/networks/{net_id}', + } + + # Module should add URLs which are required by the module + self.url_catalog = {'get_all': self.get_urls, + 'get_one': self.get_one_urls, + 'create': None, + 'update': None, + 'delete': None, + 'misc': None, + } + + if self.module._debug or self.params['output_level'] == 'debug': + self.module.warn('Enable debug output because ANSIBLE_DEBUG was set or output_level is set to debug.') + + # TODO: This should be removed as org_name isn't always required + self.module.required_if = [('state', 'present', ['org_name']), + ('state', 'absent', ['org_name']), + ] + # self.module.mutually_exclusive = [('org_id', 'org_name'), + # ] + self.modifiable_methods = ['POST', 'PUT', 'DELETE'] + + self.headers = {'Content-Type': 'application/json', + 'Authorization': 'Bearer {key}'.format(key=module.params['auth_key']), + } + + def define_protocol(self): + """Set protocol based on use_https parameters.""" + if self.params['use_https'] is True: + self.params['protocol'] = 'https' + else: + self.params['protocol'] = 'http' + + def sanitize_keys(self, data): + if isinstance(data, dict): + items = {} + for k, v in data.items(): + try: + new = {self.key_map[k]: data[k]} + items[self.key_map[k]] = self.sanitize_keys(data[k]) + except KeyError: + snake_k = re.sub('([a-z0-9])([A-Z])', r'\1_\2', k).lower() + # new = {snake_k: data[k]} + items[snake_k] = self.sanitize_keys(data[k]) + return items + elif isinstance(data, list): + items = [] + for i in data: + items.append(self.sanitize_keys(i)) + return items + elif isinstance(data, int) or isinstance(data, str) or isinstance(data, float): + return data + + def is_update_required(self, original, proposed, optional_ignore=None, debug=False): + ''' Compare two data-structures ''' + self.ignored_keys.append('net_id') + if optional_ignore is not None: + # self.fail_json(msg="Keys", ignored_keys=self.ignored_keys, optional=optional_ignore) + self.ignored_keys = self.ignored_keys + optional_ignore + + if isinstance(original, list): + if len(original) != len(proposed): + if debug is True: + self.fail_json(msg="Length of lists don't match") + return True + for a, b in zip(original, proposed): + if self.is_update_required(a, b, debug=debug): + if debug is True: + self.fail_json(msg="List doesn't match", a=a, b=b) + return True + elif isinstance(original, dict): + try: + for k, v in proposed.items(): + if k not in self.ignored_keys: + if k in original: + if self.is_update_required(original[k], proposed[k], debug=debug): + return True + else: + if debug is True: + self.fail_json(msg="Key not in original", k=k) + return True + except AttributeError: + return True + else: + if original != proposed: + if debug is True: + self.fail_json(msg="Fallback", original=original, proposed=proposed) + return True + return False + + def generate_diff(self, before, after): + """Creates a diff based on two objects. Applies to the object and returns nothing. + """ + try: + diff = recursive_diff(before, after) + self.result['diff'] = {'before': diff[0], + 'after': diff[1]} + except AttributeError: # Normally for passing a list instead of a dict + diff = recursive_diff({'data': before}, + {'data': after}) + self.result['diff'] = {'before': diff[0]['data'], + 'after': diff[1]['data']} + + def get_orgs(self): + """Downloads all organizations for a user.""" + response = self.request('/organizations', method='GET') + if self.status != 200: + self.fail_json(msg='Organization lookup failed') + self.orgs = response + return self.orgs + + def is_org_valid(self, data, org_name=None, org_id=None): + """Checks whether a specific org exists and is duplicated. + + If 0, doesn't exist. 1, exists and not duplicated. >1 duplicated. + """ + org_count = 0 + if org_name is not None: + for o in data: + if o['name'] == org_name: + org_count += 1 + if org_id is not None: + for o in data: + if o['id'] == org_id: + org_count += 1 + return org_count + + def get_org_id(self, org_name): + """Returns an organization id based on organization name, only if unique. + + If org_id is specified as parameter, return that instead of a lookup. + """ + orgs = self.get_orgs() + # self.fail_json(msg='ogs', orgs=orgs) + if self.params['org_id'] is not None: + if self.is_org_valid(orgs, org_id=self.params['org_id']) is True: + return self.params['org_id'] + org_count = self.is_org_valid(orgs, org_name=org_name) + if org_count == 0: + self.fail_json(msg='There are no organizations with the name {org_name}'.format(org_name=org_name)) + if org_count > 1: + self.fail_json(msg='There are multiple organizations with the name {org_name}'.format(org_name=org_name)) + elif org_count == 1: + for i in orgs: + if org_name == i['name']: + # self.fail_json(msg=i['id']) + return str(i['id']) + + def get_nets(self, org_name=None, org_id=None): + """Downloads all networks in an organization.""" + if org_name: + org_id = self.get_org_id(org_name) + path = self.construct_path('get_all', org_id=org_id, function='network', params={'perPage': '1000'}) + r = self.request(path, method='GET', pagination_items=1000) + if self.status != 200: + self.fail_json(msg='Network lookup failed') + self.nets = r + templates = self.get_config_templates(org_id) + for t in templates: + self.nets.append(t) + return self.nets + + def get_net(self, org_name, net_name=None, org_id=None, data=None, net_id=None): + ''' Return network information ''' + if not data: + if not org_id: + org_id = self.get_org_id(org_name) + data = self.get_nets(org_id=org_id) + for n in data: + if net_id: + if n['id'] == net_id: + return n + elif net_name: + if n['name'] == net_name: + return n + return False + + def get_net_id(self, org_name=None, net_name=None, data=None): + """Return network id from lookup or existing data.""" + if data is None: + self.fail_json(msg='Must implement lookup') + for n in data: + if n['name'] == net_name: + return n['id'] + self.fail_json(msg='No network found with the name {0}'.format(net_name)) + + def get_config_templates(self, org_id): + path = self.construct_path('get_all', function='configTemplates', org_id=org_id) + response = self.request(path, 'GET') + if self.status != 200: + self.fail_json(msg='Unable to get configuration templates') + return response + + def get_template_id(self, name, data): + for template in data: + if name == template['name']: + return template['id'] + self.fail_json(msg='No configuration template named {0} found'.format(name)) + + def convert_camel_to_snake(self, data): + """ + Converts a dictionary or list to snake case from camel case + :type data: dict or list + :return: Converted data structure, if list or dict + """ + + if isinstance(data, dict): + return camel_dict_to_snake_dict(data, ignore_list=('tags', 'tag')) + elif isinstance(data, list): + return [camel_dict_to_snake_dict(item, ignore_list=('tags', 'tag')) for item in data] + else: + return data + + def convert_snake_to_camel(self, data): + """ + Converts a dictionary or list to camel case from snake case + :type data: dict or list + :return: Converted data structure, if list or dict + """ + + if isinstance(data, dict): + return snake_dict_to_camel_dict(data) + elif isinstance(data, list): + return [snake_dict_to_camel_dict(item) for item in data] + else: + return data + + def construct_params_list(self, keys, aliases=None): + qs = {} + for key in keys: + if key in aliases: + qs[aliases[key]] = self.module.params[key] + else: + qs[key] = self.module.params[key] + return qs + + def encode_url_params(self, params): + """Encodes key value pairs for URL""" + return "?{0}".format(urlencode(params)) + + def construct_path(self, + action, + function=None, + org_id=None, + net_id=None, + org_name=None, + custom=None, + params=None): + """Build a path from the URL catalog. + Uses function property from class for catalog lookup. + """ + built_path = None + if function is None: + built_path = self.url_catalog[action][self.function] + else: + built_path = self.url_catalog[action][function] + if org_name: + org_id = self.get_org_id(org_name) + if custom: + built_path = built_path.format(org_id=org_id, net_id=net_id, **custom) + else: + built_path = built_path.format(org_id=org_id, net_id=net_id) + if params: + built_path += self.encode_url_params(params) + return built_path + + def _set_url(self, path, method, params): + self.path = path + self.define_protocol() + + if method is not None: + self.method = method + + self.url = '{protocol}://{host}/api/v1/{path}'.format(path=self.path.lstrip('/'), **self.params) + + @staticmethod + def _parse_pagination_header(link): + rels = {'first': None, + 'next': None, + 'prev': None, + 'last': None + } + for rel in link.split(','): + kv = rel.split('rel=') + rels[kv[1]] = kv[0].split('<')[1].split('>')[0].strip() # This should return just the URL for <url> + return rels + + def _execute_request(self, path, method=None, payload=None, params=None): + """ Execute query """ + try: + resp, info = fetch_url(self.module, self.url, + headers=self.headers, + data=payload, + method=self.method, + timeout=self.params['timeout'], + use_proxy=self.params['use_proxy'], + ) + self.status = info['status'] + + if self.status == 429: + self.retry += 1 + if self.retry <= 10: + # retry-after isn't returned for over 10 concurrent connections per IP + try: + self.module.warn("Rate limiter hit, retry {0}...pausing for {1} seconds".format(self.retry, info['Retry-After'])) + time.sleep(info['Retry-After']) + except KeyError: + self.module.warn("Rate limiter hit, retry {0}...pausing for 5 seconds".format(self.retry)) + time.sleep(5) + return self._execute_request(path, method=method, payload=payload, params=params) + else: + self.fail_json(msg="Rate limit retries failed for {url}".format(url=self.url)) + elif self.status == 500: + self.retry += 1 + self.module.warn("Internal server error 500, retry {0}".format(self.retry)) + if self.retry <= 10: + self.retry_time += self.retry * INTERNAL_ERROR_RETRY_MULTIPLIER + time.sleep(self.retry_time) + return self._execute_request(path, method=method, payload=payload, params=params) + else: + # raise RateLimitException(e) + self.fail_json(msg="Rate limit retries failed for {url}".format(url=self.url)) + elif self.status == 502: + self.module.warn("Internal server error 502, retry {0}".format(self.retry)) + elif self.status == 400: + raise HTTPError("") + elif self.status >= 400: + self.fail_json(msg=self.status, url=self.url) + raise HTTPError("") + except HTTPError: + try: + self.fail_json(msg="HTTP error {0} - {1} - {2}".format(self.status, self.url, json.loads(info['body'])['errors'][0])) + except json.decoder.JSONDecodeError: + self.fail_json(msg="HTTP error {0} - {1}".format(self.status, self.url)) + self.retry = 0 # Needs to reset in case of future retries + return resp, info + + def request(self, path, method=None, payload=None, params=None, pagination_items=None): + """ Submit HTTP request to Meraki API """ + self._set_url(path, method, params) + + try: + # Gather the body (resp) and header (info) + resp, info = self._execute_request(path, method=method, payload=payload, params=params) + except HTTPError: + self.fail_json(msg="HTTP request to {url} failed with error code {code}".format(url=self.url, code=self.status)) + self.response = info['msg'] + self.status = info['status'] + # This needs to be refactored as it's not very clean + # Looping process for pagination + if pagination_items is not None: + data = None + if 'body' in info: + self.body = info['body'] + data = json.loads(to_native(resp.read())) + header_link = self._parse_pagination_header(info['link']) + while header_link['next'] is not None: + self.url = header_link['next'] + try: + # Gather the body (resp) and header (info) + resp, info = self._execute_request(header_link['next'], method=method, payload=payload, params=params) + except HTTPError: + self.fail_json(msg="HTTP request to {url} failed with error code {code}".format(url=self.url, code=self.status)) + header_link = self._parse_pagination_header(info['link']) + data.extend(json.loads(to_native(resp.read()))) + return data + else: # Non-pagination + if 'body' in info: + self.body = info['body'] + try: + return json.loads(to_native(resp.read())) + except json.decoder.JSONDecodeError: + return {} + + def exit_json(self, **kwargs): + """Custom written method to exit from module.""" + self.result['response'] = self.response + self.result['status'] = self.status + if self.retry > 0: + self.module.warn("Rate limiter triggered - retry count {0}".format(self.retry)) + # Return the gory details when we need it + if self.params['output_level'] == 'debug': + self.result['method'] = self.method + self.result['url'] = self.url + self.result.update(**kwargs) + if self.params['output_format'] == 'camelcase': + self.module.deprecate("Update your playbooks to support snake_case format instead of camelCase format.", + date="2022-06-01", + collection_name="cisco.meraki") + else: + if 'data' in self.result: + try: + self.result['data'] = self.convert_camel_to_snake(self.result['data']) + self.result['diff'] = self.convert_camel_to_snake(self.result['diff']) + except (KeyError, AttributeError): + pass + self.module.exit_json(**self.result) + + def fail_json(self, msg, **kwargs): + """Custom written method to return info on failure.""" + self.result['response'] = self.response + self.result['status'] = self.status + + if self.params['output_level'] == 'debug': + if self.url is not None: + self.result['method'] = self.method + self.result['url'] = self.url + + self.result.update(**kwargs) + self.module.fail_json(msg=msg, **self.result) diff --git a/collections-debian-merged/ansible_collections/cisco/meraki/plugins/modules/__init__.py b/collections-debian-merged/ansible_collections/cisco/meraki/plugins/modules/__init__.py new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/collections-debian-merged/ansible_collections/cisco/meraki/plugins/modules/__init__.py diff --git a/collections-debian-merged/ansible_collections/cisco/meraki/plugins/modules/meraki_admin.py b/collections-debian-merged/ansible_collections/cisco/meraki/plugins/modules/meraki_admin.py new file mode 100644 index 00000000..e554bb00 --- /dev/null +++ b/collections-debian-merged/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/collections-debian-merged/ansible_collections/cisco/meraki/plugins/modules/meraki_alert.py b/collections-debian-merged/ansible_collections/cisco/meraki/plugins/modules/meraki_alert.py new file mode 100644 index 00000000..481d8652 --- /dev/null +++ b/collections-debian-merged/ansible_collections/cisco/meraki/plugins/modules/meraki_alert.py @@ -0,0 +1,337 @@ +#!/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 + 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: + - type: "gatewayDown" + enabled: yes + filters: + timeout: 60 + alert_destinations: + emails: + - 'youremail@yourcorp' + - 'youremail2@yourcorp' + all_admins: yes + snmp: no + - 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: + 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 construct_payload(meraki, original): + payload = copy.deepcopy(original) + if meraki.params['default_destinations'] is not None: + payload['defaultDestinations'].update(meraki.params['default_destinations']) + payload['defaultDestinations']['allAdmins'] = meraki.params['default_destinations']['all_admins'] + del payload['defaultDestinations']['all_admins'] + del payload['defaultDestinations']['http_server_ids'] + if meraki.params['alerts'] is not None: + for alert in meraki.params['alerts']: + alert.update(meraki.convert_snake_to_camel(alert)) + del alert['alert_destinations'] + for alert_want in meraki.params['alerts']: + for alert_have in payload['alerts']: + if alert_want['alert_type'] == alert_have['type']: + alert_have.update(alert_want) + del alert_have['alert_type'] + del alert_have['alertType'] + 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', default=[]), + ) + + 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) + # meraki.fail_json(msg="Compare", original=original, payload=payload) + # meraki.fail_json(msg=payload) + 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/collections-debian-merged/ansible_collections/cisco/meraki/plugins/modules/meraki_config_template.py b/collections-debian-merged/ansible_collections/cisco/meraki/plugins/modules/meraki_config_template.py new file mode 100644 index 00000000..8438d41b --- /dev/null +++ b/collections-debian-merged/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/collections-debian-merged/ansible_collections/cisco/meraki/plugins/modules/meraki_content_filtering.py b/collections-debian-merged/ansible_collections/cisco/meraki/plugins/modules/meraki_content_filtering.py new file mode 100644 index 00000000..5bc6b934 --- /dev/null +++ b/collections-debian-merged/ansible_collections/cisco/meraki/plugins/modules/meraki_content_filtering.py @@ -0,0 +1,282 @@ +#!/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 + + +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']: + payload['allowedUrlPatterns'] = meraki.params['allowed_urls'] + if meraki.params['blocked_urls']: + payload['blockedUrlPatterns'] = meraki.params['blocked_urls'] + if meraki.params['blocked_categories']: + if len(meraki.params['blocked_categories']) == 0: # 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) + if meraki.is_update_required(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 + 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/collections-debian-merged/ansible_collections/cisco/meraki/plugins/modules/meraki_device.py b/collections-debian-merged/ansible_collections/cisco/meraki/plugins/modules/meraki_device.py new file mode 100644 index 00000000..ddbd0301 --- /dev/null +++ b/collections-debian-merged/ansible_collections/cisco/meraki/plugins/modules/meraki_device.py @@ -0,0 +1,432 @@ +#!/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_uplink_urls = {'device': '/networks/{net_id}/devices/{serial}/uplink'} + query_device_lldp_urls = {'device': '/networks/{net_id}/devices/{serial}/lldp_cdp'} + 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/collections-debian-merged/ansible_collections/cisco/meraki/plugins/modules/meraki_firewalled_services.py b/collections-debian-merged/ansible_collections/cisco/meraki/plugins/modules/meraki_firewalled_services.py new file mode 100644 index 00000000..ec78c068 --- /dev/null +++ b/collections-debian-merged/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/collections-debian-merged/ansible_collections/cisco/meraki/plugins/modules/meraki_intrusion_prevention.py b/collections-debian-merged/ansible_collections/cisco/meraki/plugins/modules/meraki_intrusion_prevention.py new file mode 100644 index 00000000..c5d0213c --- /dev/null +++ b/collections-debian-merged/ansible_collections/cisco/meraki/plugins/modules/meraki_intrusion_prevention.py @@ -0,0 +1,366 @@ +#!/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 + message: + description: + - Description of rule. + - This is overwritten by the API. + type: str + 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" + 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" + 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', + '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'), + message=dict(type='str'), + ) + + 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['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/collections-debian-merged/ansible_collections/cisco/meraki/plugins/modules/meraki_malware.py b/collections-debian-merged/ansible_collections/cisco/meraki/plugins/modules/meraki_malware.py new file mode 100644 index 00000000..1cbf7e68 --- /dev/null +++ b/collections-debian-merged/ansible_collections/cisco/meraki/plugins/modules/meraki_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/collections-debian-merged/ansible_collections/cisco/meraki/plugins/modules/meraki_management_interface.py b/collections-debian-merged/ansible_collections/cisco/meraki/plugins/modules/meraki_management_interface.py new file mode 100644 index 00000000..c0297861 --- /dev/null +++ b/collections-debian-merged/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/collections-debian-merged/ansible_collections/cisco/meraki/plugins/modules/meraki_mr_l3_firewall.py b/collections-debian-merged/ansible_collections/cisco/meraki/plugins/modules/meraki_mr_l3_firewall.py new file mode 100644 index 00000000..274dbb15 --- /dev/null +++ b/collections-debian-merged/ansible_collections/cisco/meraki/plugins/modules/meraki_mr_l3_firewall.py @@ -0,0 +1,288 @@ +#!/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 response + + +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/collections-debian-merged/ansible_collections/cisco/meraki/plugins/modules/meraki_mr_rf_profile.py b/collections-debian-merged/ansible_collections/cisco/meraki/plugins/modules/meraki_mr_rf_profile.py new file mode 100644 index 00000000..3a31e825 --- /dev/null +++ b/collections-debian-merged/ansible_collections/cisco/meraki/plugins/modules/meraki_mr_rf_profile.py @@ -0,0 +1,663 @@ +#!/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.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 +from re import sub + + +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']) + + 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/collections-debian-merged/ansible_collections/cisco/meraki/plugins/modules/meraki_mr_settings.py b/collections-debian-merged/ansible_collections/cisco/meraki/plugins/modules/meraki_mr_settings.py new file mode 100644 index 00000000..0d0c8897 --- /dev/null +++ b/collections-debian-merged/ansible_collections/cisco/meraki/plugins/modules/meraki_mr_settings.py @@ -0,0 +1,222 @@ +#!/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.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 +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/collections-debian-merged/ansible_collections/cisco/meraki/plugins/modules/meraki_mr_ssid.py b/collections-debian-merged/ansible_collections/cisco/meraki/plugins/modules/meraki_mr_ssid.py new file mode 100644 index 00000000..c9101290 --- /dev/null +++ b/collections-debian-merged/ansible_collections/cisco/meraki/plugins/modules/meraki_mr_ssid.py @@ -0,0 +1,614 @@ +#!/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_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'] + use_vlan_tagging: + description: + - Set whether to use VLAN tagging. + - Requires C(default_vlan_id) to be set. + 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 + 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 +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 + 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', + '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', + 'concentratorNetworkId': 'concentrator_network_id', + 'vlanId': 'vlan_id', + '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', + } + + 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_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'), + 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'), + ) + + # 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.fails_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") + + # 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: + ssid_id = get_available_number(ssids) + if ssid_id is False: + meraki.fail_json(msg='No SSID found by specified name and no number was referenced.') + 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/collections-debian-merged/ansible_collections/cisco/meraki/plugins/modules/meraki_ms_access_list.py b/collections-debian-merged/ansible_collections/cisco/meraki/plugins/modules/meraki_ms_access_list.py new file mode 100644 index 00000000..bd5e9205 --- /dev/null +++ b/collections-debian-merged/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/collections-debian-merged/ansible_collections/cisco/meraki/plugins/modules/meraki_ms_l3_interface.py b/collections-debian-merged/ansible_collections/cisco/meraki/plugins/modules/meraki_ms_l3_interface.py new file mode 100644 index 00000000..716ec8d9 --- /dev/null +++ b/collections-debian-merged/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/collections-debian-merged/ansible_collections/cisco/meraki/plugins/modules/meraki_ms_link_aggregation.py b/collections-debian-merged/ansible_collections/cisco/meraki/plugins/modules/meraki_ms_link_aggregation.py new file mode 100644 index 00000000..a38eda7d --- /dev/null +++ b/collections-debian-merged/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/collections-debian-merged/ansible_collections/cisco/meraki/plugins/modules/meraki_ms_ospf.py b/collections-debian-merged/ansible_collections/cisco/meraki/plugins/modules/meraki_ms_ospf.py new file mode 100644 index 00000000..f071b3f8 --- /dev/null +++ b/collections-debian-merged/ansible_collections/cisco/meraki/plugins/modules/meraki_ms_ospf.py @@ -0,0 +1,322 @@ +#!/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) + 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), + ) + + # 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/collections-debian-merged/ansible_collections/cisco/meraki/plugins/modules/meraki_ms_stack.py b/collections-debian-merged/ansible_collections/cisco/meraki/plugins/modules/meraki_ms_stack.py new file mode 100644 index 00000000..fb0729a1 --- /dev/null +++ b/collections-debian-merged/ansible_collections/cisco/meraki/plugins/modules/meraki_ms_stack.py @@ -0,0 +1,277 @@ +#!/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: + payload = {'serial': meraki.params['serials'][0]} + original = get_stack(stack_id, stacks) + comparable = deepcopy(original) + comparable.update(payload) + if meraki.params['serials'][0] in comparable['serials']: + comparable['serials'].remove(meraki.params['serials'][0]) + 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/collections-debian-merged/ansible_collections/cisco/meraki/plugins/modules/meraki_ms_storm_control.py b/collections-debian-merged/ansible_collections/cisco/meraki/plugins/modules/meraki_ms_storm_control.py new file mode 100644 index 00000000..2048ad5e --- /dev/null +++ b/collections-debian-merged/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/collections-debian-merged/ansible_collections/cisco/meraki/plugins/modules/meraki_ms_switchport.py b/collections-debian-merged/ansible_collections/cisco/meraki/plugins/modules/meraki_ms_switchport.py new file mode 100644 index 00000000..f119c71a --- /dev/null +++ b/collections-debian-merged/ansible_collections/cisco/meraki/plugins/modules/meraki_ms_switchport.py @@ -0,0 +1,424 @@ +#!/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 whitelist, Sticky MAC whitelist] + 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, 100Megabit (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. + type: int + voice_vlan: + description: + - VLAN number assigned to a port for voice traffic. + - Only applicable to access port type. + type: int + +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 +''' + +RETURN = r''' +data: + description: Information queried or updated switchports. + returned: success + type: complex + contains: + 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" +''' + +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', + } + + +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 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'), + 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'), + 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 whitelist', 'Sticky MAC whitelist']), + access_policy_number=dict(type='int'), + link_negotiation=dict(type='str', + choices=['Auto negotiate', '100Megabit (auto)', '100 Megabit full duplex (forced)'], + default='Auto negotiate'), + ) + + # 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') + 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 + + proposed = payload.copy() + query_path = meraki.construct_path('get_one', custom={'serial': meraki.params['serial'], + 'number': meraki.params['number'], + }) + original = meraki.request(query_path, method='GET') + 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'], + }) + # meraki.fail_json(msg=payload) + 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/collections-debian-merged/ansible_collections/cisco/meraki/plugins/modules/meraki_mx_content_filtering.py b/collections-debian-merged/ansible_collections/cisco/meraki/plugins/modules/meraki_mx_content_filtering.py new file mode 100644 index 00000000..5bc6b934 --- /dev/null +++ b/collections-debian-merged/ansible_collections/cisco/meraki/plugins/modules/meraki_mx_content_filtering.py @@ -0,0 +1,282 @@ +#!/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 + + +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']: + payload['allowedUrlPatterns'] = meraki.params['allowed_urls'] + if meraki.params['blocked_urls']: + payload['blockedUrlPatterns'] = meraki.params['blocked_urls'] + if meraki.params['blocked_categories']: + if len(meraki.params['blocked_categories']) == 0: # 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) + if meraki.is_update_required(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 + 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/collections-debian-merged/ansible_collections/cisco/meraki/plugins/modules/meraki_mx_intrusion_prevention.py b/collections-debian-merged/ansible_collections/cisco/meraki/plugins/modules/meraki_mx_intrusion_prevention.py new file mode 100644 index 00000000..c5d0213c --- /dev/null +++ b/collections-debian-merged/ansible_collections/cisco/meraki/plugins/modules/meraki_mx_intrusion_prevention.py @@ -0,0 +1,366 @@ +#!/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 + message: + description: + - Description of rule. + - This is overwritten by the API. + type: str + 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" + 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" + 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', + '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'), + message=dict(type='str'), + ) + + 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['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/collections-debian-merged/ansible_collections/cisco/meraki/plugins/modules/meraki_mx_l2_interface.py b/collections-debian-merged/ansible_collections/cisco/meraki/plugins/modules/meraki_mx_l2_interface.py new file mode 100644 index 00000000..65944b87 --- /dev/null +++ b/collections-debian-merged/ansible_collections/cisco/meraki/plugins/modules/meraki_mx_l2_interface.py @@ -0,0 +1,273 @@ +#!/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.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(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/collections-debian-merged/ansible_collections/cisco/meraki/plugins/modules/meraki_mx_l3_firewall.py b/collections-debian-merged/ansible_collections/cisco/meraki/plugins/modules/meraki_mx_l3_firewall.py new file mode 100644 index 00000000..b9b1e9b0 --- /dev/null +++ b/collections-debian-merged/ansible_collections/cisco/meraki/plugins/modules/meraki_mx_l3_firewall.py @@ -0,0 +1,365 @@ +#!/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: permit + 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 response + + +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/collections-debian-merged/ansible_collections/cisco/meraki/plugins/modules/meraki_mx_l7_firewall.py b/collections-debian-merged/ansible_collections/cisco/meraki/plugins/modules/meraki_mx_l7_firewall.py new file mode 100644 index 00000000..31c9af1c --- /dev/null +++ b/collections-debian-merged/ansible_collections/cisco/meraki/plugins/modules/meraki_mx_l7_firewall.py @@ -0,0 +1,479 @@ +#!/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 rename_id_to_appid(rules): + for rule in rules['rules']: + if rule['type'] == 'application' or rule['type'] == 'applicationCategory': + rule['value']['appId'] = rule['value'].pop('id') + return rules + + +def rename_appid_to_id(rules): + for rule in rules['rules']: + if rule['type'] == 'application' or rule['type'] == 'applicationCategory': + rule['value']['id'] = rule['value'].pop('appId') + return rules + + +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) + if meraki.params['rules']: + payload = {'rules': []} + for rule in meraki.params['rules']: + payload['rules'].append(assemble_payload(meraki, net_id, rule)) + else: + payload = dict() + + ''' + The rename_* functions are needed because the key is id and + is_update_required() by default ignores id. + ''' + rules = rename_id_to_appid(rules) + payload = rename_id_to_appid(payload) + if meraki.is_update_required(rules, payload): + rules = rename_appid_to_id(rules) + payload = rename_appid_to_id(payload) + 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: + rules = rename_appid_to_id(rules) + payload = rename_appid_to_id(payload) + 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/collections-debian-merged/ansible_collections/cisco/meraki/plugins/modules/meraki_mx_malware.py b/collections-debian-merged/ansible_collections/cisco/meraki/plugins/modules/meraki_mx_malware.py new file mode 100644 index 00000000..1cbf7e68 --- /dev/null +++ b/collections-debian-merged/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/collections-debian-merged/ansible_collections/cisco/meraki/plugins/modules/meraki_mx_nat.py b/collections-debian-merged/ansible_collections/cisco/meraki/plugins/modules/meraki_mx_nat.py new file mode 100644 index 00000000..0844d4c1 --- /dev/null +++ b/collections-debian-merged/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/collections-debian-merged/ansible_collections/cisco/meraki/plugins/modules/meraki_mx_site_to_site_firewall.py b/collections-debian-merged/ansible_collections/cisco/meraki/plugins/modules/meraki_mx_site_to_site_firewall.py new file mode 100644 index 00000000..f81ac3a3 --- /dev/null +++ b/collections-debian-merged/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/collections-debian-merged/ansible_collections/cisco/meraki/plugins/modules/meraki_mx_site_to_site_vpn.py b/collections-debian-merged/ansible_collections/cisco/meraki/plugins/modules/meraki_mx_site_to_site_vpn.py new file mode 100644 index 00000000..57a7211d --- /dev/null +++ b/collections-debian-merged/ansible_collections/cisco/meraki/plugins/modules/meraki_mx_site_to_site_vpn.py @@ -0,0 +1,250 @@ +#!/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: 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): + 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/collections-debian-merged/ansible_collections/cisco/meraki/plugins/modules/meraki_mx_static_route.py b/collections-debian-merged/ansible_collections/cisco/meraki/plugins/modules/meraki_mx_static_route.py new file mode 100644 index 00000000..927d4230 --- /dev/null +++ b/collections-debian-merged/ansible_collections/cisco/meraki/plugins/modules/meraki_mx_static_route.py @@ -0,0 +1,392 @@ +#!/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 + 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'), + ) + + # 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'] + + 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/collections-debian-merged/ansible_collections/cisco/meraki/plugins/modules/meraki_mx_uplink.py b/collections-debian-merged/ansible_collections/cisco/meraki/plugins/modules/meraki_mx_uplink.py new file mode 100644 index 00000000..fd66fc9b --- /dev/null +++ b/collections-debian-merged/ansible_collections/cisco/meraki/plugins/modules/meraki_mx_uplink.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(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/collections-debian-merged/ansible_collections/cisco/meraki/plugins/modules/meraki_mx_uplink_bandwidth.py b/collections-debian-merged/ansible_collections/cisco/meraki/plugins/modules/meraki_mx_uplink_bandwidth.py new file mode 100644 index 00000000..fd66fc9b --- /dev/null +++ b/collections-debian-merged/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(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/collections-debian-merged/ansible_collections/cisco/meraki/plugins/modules/meraki_mx_vlan.py b/collections-debian-merged/ansible_collections/cisco/meraki/plugins/modules/meraki_mx_vlan.py new file mode 100644 index 00000000..92a608fe --- /dev/null +++ b/collections-debian-merged/ansible_collections/cisco/meraki/plugins/modules/meraki_mx_vlan.py @@ -0,0 +1,583 @@ +#!/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_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/collections-debian-merged/ansible_collections/cisco/meraki/plugins/modules/meraki_nat.py b/collections-debian-merged/ansible_collections/cisco/meraki/plugins/modules/meraki_nat.py new file mode 100644 index 00000000..0844d4c1 --- /dev/null +++ b/collections-debian-merged/ansible_collections/cisco/meraki/plugins/modules/meraki_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/collections-debian-merged/ansible_collections/cisco/meraki/plugins/modules/meraki_network.py b/collections-debian-merged/ansible_collections/cisco/meraki/plugins/modules/meraki_network.py new file mode 100644 index 00000000..af583f0a --- /dev/null +++ b/collections-debian-merged/ansible_collections/cisco/meraki/plugins/modules/meraki_network.py @@ -0,0 +1,412 @@ +#!/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 ] + 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: > + - 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 + +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: 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'], 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'), + ) + + # 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 True and meraki.params['remote_status_page_enabled'] is False: + 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'] + + # 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'], + meraki.params['net_name'], + data=nets + ) + 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/collections-debian-merged/ansible_collections/cisco/meraki/plugins/modules/meraki_organization.py b/collections-debian-merged/ansible_collections/cisco/meraki/plugins/modules/meraki_organization.py new file mode 100644 index 00000000..45148e89 --- /dev/null +++ b/collections-debian-merged/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/collections-debian-merged/ansible_collections/cisco/meraki/plugins/modules/meraki_site_to_site_vpn.py b/collections-debian-merged/ansible_collections/cisco/meraki/plugins/modules/meraki_site_to_site_vpn.py new file mode 100644 index 00000000..57a7211d --- /dev/null +++ b/collections-debian-merged/ansible_collections/cisco/meraki/plugins/modules/meraki_site_to_site_vpn.py @@ -0,0 +1,250 @@ +#!/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: 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): + 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/collections-debian-merged/ansible_collections/cisco/meraki/plugins/modules/meraki_snmp.py b/collections-debian-merged/ansible_collections/cisco/meraki/plugins/modules/meraki_snmp.py new file mode 100644 index 00000000..0240a168 --- /dev/null +++ b/collections-debian-merged/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/collections-debian-merged/ansible_collections/cisco/meraki/plugins/modules/meraki_ssid.py b/collections-debian-merged/ansible_collections/cisco/meraki/plugins/modules/meraki_ssid.py new file mode 100644 index 00000000..c9101290 --- /dev/null +++ b/collections-debian-merged/ansible_collections/cisco/meraki/plugins/modules/meraki_ssid.py @@ -0,0 +1,614 @@ +#!/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_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'] + use_vlan_tagging: + description: + - Set whether to use VLAN tagging. + - Requires C(default_vlan_id) to be set. + 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 + 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 +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 + 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', + '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', + 'concentratorNetworkId': 'concentrator_network_id', + 'vlanId': 'vlan_id', + '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', + } + + 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_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'), + 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'), + ) + + # 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.fails_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") + + # 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: + ssid_id = get_available_number(ssids) + if ssid_id is False: + meraki.fail_json(msg='No SSID found by specified name and no number was referenced.') + 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/collections-debian-merged/ansible_collections/cisco/meraki/plugins/modules/meraki_static_route.py b/collections-debian-merged/ansible_collections/cisco/meraki/plugins/modules/meraki_static_route.py new file mode 100644 index 00000000..927d4230 --- /dev/null +++ b/collections-debian-merged/ansible_collections/cisco/meraki/plugins/modules/meraki_static_route.py @@ -0,0 +1,392 @@ +#!/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 + 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'), + ) + + # 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'] + + 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/collections-debian-merged/ansible_collections/cisco/meraki/plugins/modules/meraki_switch_access_list.py b/collections-debian-merged/ansible_collections/cisco/meraki/plugins/modules/meraki_switch_access_list.py new file mode 100644 index 00000000..bd5e9205 --- /dev/null +++ b/collections-debian-merged/ansible_collections/cisco/meraki/plugins/modules/meraki_switch_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/collections-debian-merged/ansible_collections/cisco/meraki/plugins/modules/meraki_switch_stack.py b/collections-debian-merged/ansible_collections/cisco/meraki/plugins/modules/meraki_switch_stack.py new file mode 100644 index 00000000..fb0729a1 --- /dev/null +++ b/collections-debian-merged/ansible_collections/cisco/meraki/plugins/modules/meraki_switch_stack.py @@ -0,0 +1,277 @@ +#!/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: + payload = {'serial': meraki.params['serials'][0]} + original = get_stack(stack_id, stacks) + comparable = deepcopy(original) + comparable.update(payload) + if meraki.params['serials'][0] in comparable['serials']: + comparable['serials'].remove(meraki.params['serials'][0]) + 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/collections-debian-merged/ansible_collections/cisco/meraki/plugins/modules/meraki_switch_storm_control.py b/collections-debian-merged/ansible_collections/cisco/meraki/plugins/modules/meraki_switch_storm_control.py new file mode 100644 index 00000000..2048ad5e --- /dev/null +++ b/collections-debian-merged/ansible_collections/cisco/meraki/plugins/modules/meraki_switch_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/collections-debian-merged/ansible_collections/cisco/meraki/plugins/modules/meraki_switchport.py b/collections-debian-merged/ansible_collections/cisco/meraki/plugins/modules/meraki_switchport.py new file mode 100644 index 00000000..f119c71a --- /dev/null +++ b/collections-debian-merged/ansible_collections/cisco/meraki/plugins/modules/meraki_switchport.py @@ -0,0 +1,424 @@ +#!/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 whitelist, Sticky MAC whitelist] + 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, 100Megabit (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. + type: int + voice_vlan: + description: + - VLAN number assigned to a port for voice traffic. + - Only applicable to access port type. + type: int + +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 +''' + +RETURN = r''' +data: + description: Information queried or updated switchports. + returned: success + type: complex + contains: + 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" +''' + +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', + } + + +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 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'), + 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'), + 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 whitelist', 'Sticky MAC whitelist']), + access_policy_number=dict(type='int'), + link_negotiation=dict(type='str', + choices=['Auto negotiate', '100Megabit (auto)', '100 Megabit full duplex (forced)'], + default='Auto negotiate'), + ) + + # 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') + 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 + + proposed = payload.copy() + query_path = meraki.construct_path('get_one', custom={'serial': meraki.params['serial'], + 'number': meraki.params['number'], + }) + original = meraki.request(query_path, method='GET') + 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'], + }) + # meraki.fail_json(msg=payload) + 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/collections-debian-merged/ansible_collections/cisco/meraki/plugins/modules/meraki_syslog.py b/collections-debian-merged/ansible_collections/cisco/meraki/plugins/modules/meraki_syslog.py new file mode 100644 index 00000000..7304199e --- /dev/null +++ b/collections-debian-merged/ansible_collections/cisco/meraki/plugins/modules/meraki_syslog.py @@ -0,0 +1,260 @@ +#!/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: ['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 + status: 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 + status: query + 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 + status: query + 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 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 + + server_arg_spec = dict(host=dict(type='str'), + port=dict(type='int', default="514"), + roles=dict(type='list', elements='str', choices=['Wireless Event log', + 'Appliance event log', + 'Switch event log', + 'Air Marshal events', + 'Flows', + 'URLs', + 'IDS alerts', + 'Security events', + ]), + ) + + 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': + # 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 + + 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('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/collections-debian-merged/ansible_collections/cisco/meraki/plugins/modules/meraki_vlan.py b/collections-debian-merged/ansible_collections/cisco/meraki/plugins/modules/meraki_vlan.py new file mode 100644 index 00000000..92a608fe --- /dev/null +++ b/collections-debian-merged/ansible_collections/cisco/meraki/plugins/modules/meraki_vlan.py @@ -0,0 +1,583 @@ +#!/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_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/collections-debian-merged/ansible_collections/cisco/meraki/plugins/modules/meraki_webhook.py b/collections-debian-merged/ansible_collections/cisco/meraki/plugins/modules/meraki_webhook.py new file mode 100644 index 00000000..ec5574ad --- /dev/null +++ b/collections-debian-merged/ansible_collections/cisco/meraki/plugins/modules/meraki_webhook.py @@ -0,0 +1,347 @@ +#!/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 + 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 + 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 +''' + +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: + meraki.result['data'][0]['shared_secret'] = 'VALUE_SPECIFIED_IN_NO_LOG_PARAMETER' + except KeyError: + pass + try: + meraki.result['data']['shared_secret'] = 'VALUE_SPECIFIED_IN_NO_LOG_PARAMETER' + except (KeyError, TypeError) as e: + pass + + +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'), + ) + + # 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}'} + + 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 + + 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) + 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'], + 'sharedSecret': meraki.params['shared_secret']} + + 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: + 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() |