diff options
Diffstat (limited to 'lib/ansible/module_utils/network/meraki/meraki.py')
-rw-r--r-- | lib/ansible/module_utils/network/meraki/meraki.py | 461 |
1 files changed, 0 insertions, 461 deletions
diff --git a/lib/ansible/module_utils/network/meraki/meraki.py b/lib/ansible/module_utils/network/meraki/meraki.py deleted file mode 100644 index 50567562c1..0000000000 --- a/lib/ansible/module_utils/network/meraki/meraki.py +++ /dev/null @@ -1,461 +0,0 @@ -# -*- coding: utf-8 -*- - -# This code is part of Ansible, but is an independent component - -# This particular file snippet, and this file snippet only, is BSD licensed. -# Modules you write using this snippet, which is embedded dynamically by Ansible -# still belong to the author of the module, and may assign their own license -# to the complete work. - -# Copyright: (c) 2018, Kevin Breit <kevin.breit@kevinbreit.net> -# All rights reserved. - -# Redistribution and use in source and binary forms, with or without modification, -# are permitted provided that the following conditions are met: -# -# * Redistributions of source code must retain the above copyright -# notice, this list of conditions and the following disclaimer. -# * Redistributions in binary form must reproduce the above copyright notice, -# this list of conditions and the following disclaimer in the documentation -# and/or other materials provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND -# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. -# IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, -# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, -# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS -# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE -# USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -import time -import os -import re -from ansible.module_utils.basic import AnsibleModule, json, env_fallback -from ansible.module_utils.common.dict_transformations import camel_dict_to_snake_dict -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, to_bytes, to_text - - -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) - - -def _error_report(function): - def inner(self, *args, **kwargs): - while True: - try: - response = function(self, *args, **kwargs) - if self.status == 429: - raise RateLimitException( - "Rate limiter hit, retry {0}".format(self.retry)) - elif self.status == 500: - raise InternalErrorException( - "Internal server error 500, retry {0}".format(self.retry)) - elif self.status == 502: - raise InternalErrorException( - "Internal server error 502, retry {0}".format(self.retry)) - elif self.status >= 400: - raise HTTPError("HTTP error {0} - {1}".format(self.status, response)) - self.retry = 0 # Needs to reset in case of future retries - return response - except RateLimitException as e: - self.retry += 1 - if self.retry <= 10: - self.retry_time += self.retry * RATE_LIMIT_RETRY_MULTIPLIER - time.sleep(self.retry * RATE_LIMIT_RETRY_MULTIPLIER) - else: - self.retry_time += 30 - time.sleep(30) - if self.retry_time > self.params['rate_limit_retry_time']: - raise RateLimitException(e) - except InternalErrorException as e: - self.retry += 1 - if self.retry <= 10: - self.retry_time += self.retry * INTERNAL_ERROR_RETRY_MULTIPLIER - time.sleep(self.retry * INTERNAL_ERROR_RETRY_MULTIPLIER) - else: - self.retry_time += 9 - time.sleep(9) - if self.retry_time > self.params['internal_error_retry_time']: - raise InternalErrorException(e) - except HTTPError as e: - raise HTTPError(e) - return inner - - -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 - - # 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', - 'X-Cisco-Meraki-API-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): - ''' Compare two data-structures ''' - self.ignored_keys.append('net_id') - if optional_ignore is not None: - self.ignored_keys = self.ignored_keys + optional_ignore - - if isinstance(original, list): - if len(original) != len(proposed): - # 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): - # self.fail_json(msg="List doesn't match", a=a, b=b) - return True - elif isinstance(original, dict): - 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]): - return True - else: - # self.fail_json(msg="Key not in original", k=k) - return True - else: - if original != proposed: - # self.fail_json(msg="Fallback", original=original, proposed=proposed) - return True - return False - - 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') - r = self.request(path, method='GET') - 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 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 - - @_error_report - def request(self, path, method=None, payload=None): - """Generic HTTP method for Meraki requests.""" - self.path = path - self.define_protocol() - - if method is not None: - self.method = method - self.url = '{protocol}://{host}/api/v0/{path}'.format(path=self.path.lstrip('/'), **self.params) - 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.response = info['msg'] - self.status = info['status'] - - try: - return json.loads(to_native(resp.read())) - except Exception: - pass - - 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.", version=2.13) - else: - if 'data' in self.result: - try: - self.result['data'] = self.convert_camel_to_snake(self.result['data']) - 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) |