summaryrefslogtreecommitdiff
path: root/lib/ansible/module_utils/network/meraki/meraki.py
diff options
context:
space:
mode:
Diffstat (limited to 'lib/ansible/module_utils/network/meraki/meraki.py')
-rw-r--r--lib/ansible/module_utils/network/meraki/meraki.py461
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)