Migrated to cisco.meraki

pull/68298/head
Ansible Core Team 4 years ago committed by Matt Martz
parent 5aa37733c3
commit 8a3f3e41f8

@ -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)

@ -1,498 +0,0 @@
#!/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: '2.6'
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.
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.
suboptions:
id:
description:
- Network ID 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: 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, env_fallback
from ansible.module_utils.urls import fetch_url
from ansible.module_utils._text import to_native
from ansible.module_utils.common.dict_transformations import recursive_diff
from ansible.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 = json.loads(networks)
networks_new = []
for n in networks:
networks_new.append({'id': meraki.get_net_id(org_name=meraki.params['org_name'],
net_name=n['network'],
data=nets),
'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'] = json.loads(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:
diff = recursive_diff(is_admin_existing, payload)
is_admin_existing.update(payload)
meraki.result['diff'] = {'before': diff[0],
'after': diff[1],
}
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
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='json'),
networks=dict(type='json'),
org_name=dict(type='str', aliases=['organization']),
org_id=dict(type='str'),
)
# seed the result dict in the object
# we primarily care about changed and state
# change is if this module effectively modified the target
# state will include any data that you want your module to pass back
# for consumption, for example, in a subsequent task
result = dict(
changed=False,
)
# 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
payload = None
# 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()

@ -1,331 +0,0 @@
#!/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: "2.7"
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
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']
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: 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
'''
import os
from ansible.module_utils.basic import AnsibleModule, json, env_fallback
from ansible.module_utils.urls import fetch_url
from ansible.module_utils._text import to_native
from ansible.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'),
)
# seed the result dict in the object
# we primarily care about changed and state
# change is if this module effectively modified the target
# state will include any data that you want your module to pass back
# for consumption, for example, in a subsequent task
result = dict(
changed=False,
)
# 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
payload = None
# 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 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 nets is None:
nets = meraki.get_nets(org_id=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()

@ -1,248 +0,0 @@
#!/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_content_filtering
short_description: Edit Meraki MX content filtering policies
version_added: "2.8"
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
blocked_urls:
description:
- List of URL patterns which should be blocked.
type: list
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
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
version_added: '2.9'
author:
- Kevin Breit (@kbreit)
extends_documentation_fragment: 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:
id:
description: Identification string of network.
returned: success
type: str
sample: N_12345
'''
import os
from ansible.module_utils.basic import AnsibleModule, json, env_fallback
from ansible.module_utils.urls import fetch_url
from ansible.module_utils._text import to_native
from ansible.module_utils.common.dict_transformations import recursive_diff
from ansible.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'),
blocked_urls=dict(type='list'),
blocked_categories=dict(type='list'),
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}/contentFiltering/categories'}
policy_urls = {'content_filtering': '/networks/{net_id}/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 = 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']:
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:
meraki.result['diff'] = dict()
diff = recursive_diff(current, payload)
meraki.result['diff']['before'] = diff[0]
meraki.result['diff']['after'] = diff[1]
if module.check_mode:
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
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()

@ -1,430 +0,0 @@
#!/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
version_added: "2.7"
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: 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
serial_lldp_cdp:
description:
- Serial number of device to query LLDP/CDP information from.
type: str
lldp_cdp_timespan:
description:
- Timespan, in seconds, used to query LLDP and CDP information.
- Must be less than 1 month.
type: int
serial_uplink:
description:
- Serial number of device to query uplink information from.
type: str
note:
description:
- Informational notes about a device.
- Limited to 255 characters.
version_added: '2.8'
type: str
author:
- Kevin Breit (@kbreit)
extends_documentation_fragment: 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
'''
import os
from ansible.module_utils.basic import AnsibleModule, json, env_fallback
from ansible.module_utils._text import to_native
from ansible.module_utils.network.meraki.meraki import MerakiModule, meraki_argument_spec
def format_tags(tags):
return " {tags} ".format(tags=tags)
def is_device_valid(meraki, serial, data):
for device in data:
if device['serial'] == serial:
return True
return False
def get_org_devices(meraki, org_id):
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 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'),
serial_uplink=dict(type='str'),
serial_lldp_cdp=dict(type='str'),
lldp_cdp_timespan=dict(type='int'),
hostname=dict(type='str', aliases=['name']),
model=dict(type='str'),
tags=dict(type='str'),
lat=dict(type='float', aliases=['latitude']),
lng=dict(type='float', aliases=['longitude']),
address=dict(type='str'),
move_map_marker=dict(type='bool'),
note=dict(type='str'),
)
# seed the result dict in the object
# we primarily care about changed and state
# change is if this module effectively modified the target
# state will include any data that you want your module to pass back
# for consumption, for example, in a subsequent task
result = dict(
changed=False,
)
# 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='device')
if meraki.params['serial_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}/inventory'}
query_device_urls = {'device': '/networks/{net_id}/devices/'}
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/'}
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['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
# 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
# FIXME: Work with Meraki so they can implement a check mode
if module.check_mode:
meraki.exit_json(**meraki.result)
# 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) + meraki.params['serial']
request = meraki.request(path, method='GET')
device.append(request)
meraki.result['data'] = device
elif meraki.params['serial_uplink']:
path = meraki.construct_path('get_device', net_id=net_id) + meraki.params['serial_uplink'] + '/uplink'
meraki.result['data'] = (meraki.request(path, method='GET'))
elif meraki.params['serial_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', net_id=net_id) + meraki.params['serial_lldp_cdp'] + '/lldp_cdp'
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)
devices = meraki.request(path, method='GET')
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 meraki.params['hostname']:
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):
payload = {'name': meraki.params['hostname'],
'tags': format_tags(meraki.params['tags']),
'lat': meraki.params['lat'],
'lng': meraki.params['lng'],
'address': meraki.params['address'],
'moveMapMarker': meraki.params['move_map_marker'],
'notes': meraki.params['note'],
}
query_path = meraki.construct_path('get_device', net_id=net_id) + meraki.params['serial']
device_data = meraki.request(query_path, method='GET')
ignore_keys = ['lanIp', 'serial', 'mac', 'model', 'networkId', 'moveMapMarker', 'wan1Ip', 'wan2Ip']
# meraki.fail_json(msg="Compare", original=device_data, payload=payload, ignore=ignore_keys)
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:
if net_id is None:
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:
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 = {'serial': 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)
path = path + meraki.params['serial'] + '/remove'
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()

@ -1,238 +0,0 @@
#!/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
version_added: "2.9"
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
author:
- Kevin Breit (@kbreit)
extends_documentation_fragment: 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.module_utils.common.dict_transformations import recursive_diff
from ansible.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}/firewalledServices'}
services_urls = {'firewalled_services': '/networks/{net_id}/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)
diff = recursive_diff(original, diff_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)
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:
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()

@ -1,276 +0,0 @@
#!/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_malware
short_description: Manage Malware Protection in the Meraki cloud
version_added: "2.9"
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.
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.
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]
author:
- Kevin Breit (@kbreit)
extends_documentation_fragment: 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.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
'''
import os
from ansible.module_utils.basic import AnsibleModule, json, env_fallback
from ansible.module_utils._text import to_native
from ansible.module_utils.common.dict_transformations import recursive_diff
from ansible.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),
)
# seed the result dict in the object
# we primarily care about changed and state
# change is if this module effectively modified the target
# state will include any data that you want your module to pass back
# for consumption, for example, in a subsequent task
result = dict(
changed=False,
)
# 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}/security/malwareSettings'}
update_url = {'malware': '/networks/{net_id}/security/malwareSettings'}
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:
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)
path = meraki.construct_path('update', net_id=net_id)
data = meraki.request(path, method='PUT', payload=json.dumps(payload))
if meraki.status == 200:
diff = recursive_diff(original, payload)
meraki.result['diff'] = {'before': diff[0],
'after': diff[1],
}
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()

@ -1,283 +0,0 @@
#!/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
version_added: "2.7"
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: int
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
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: 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'''
'''
import os
from ansible.module_utils.basic import AnsibleModule, json, env_fallback
from ansible.module_utils.urls import fetch_url
from ansible.module_utils._text import to_native
from ansible.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),
)
# seed the result dict in the object
# we primarily care about changed and state
# change is if this module effectively modified the target
# state will include any data that you want your module to pass back
# for consumption, for example, in a subsequent task
result = dict(
changed=False,
)
# 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}/ssids/{number}/l3FirewallRules'}
update_urls = {'mr_l3_firewall': '/networks/{net_id}/ssids/{number}/l3FirewallRules'}
meraki.url_catalog['get_all'].update(query_urls)
meraki.url_catalog['update'] = update_urls
payload = None
# 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
# FIXME: Work with Meraki so they can implement a check mode
if module.check_mode:
meraki.exit_json(**meraki.result)
# 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
if rules[len(rules) - 2] != meraki.params['allow_lan_access']:
update = True
if update is True:
payload['allowLanAccess'] = meraki.params['allow_lan_access']
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()

@ -1,328 +0,0 @@
#!/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
version_added: "2.7"
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
net_name:
description:
- Name of network which MX firewall is in.
net_id:
description:
- ID of network which MX firewall is in.
rules:
description:
- List of firewall rules.
suboptions:
policy:
description:
- Policy to apply if rule is hit.
choices: [allow, deny]
protocol:
description:
- Protocol to match against.
choices: [any, icmp, tcp, udp]
dest_port:
description:
- Comma separated list of destination port numbers to match against.
dest_cidr:
description:
- Comma separated list of CIDR notation destination networks.
src_port:
description:
- Comma separated list of source port numbers to match against.
src_cidr:
description:
- Comma separated list of CIDR notation source networks.
comment:
description:
- Optional comment to describe the firewall rule.
syslog_enabled:
description:
- Whether to log hints against the firewall rule.
- Only applicable if a syslog server is specified against the network.
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
default: no
author:
- Kevin Breit (@kbreit)
extends_documentation_fragment: 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:
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
'''
import os
from ansible.module_utils.basic import AnsibleModule, json, env_fallback
from ansible.module_utils.urls import fetch_url
from ansible.module_utils._text import to_native
from ansible.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 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'),
)
# seed the result dict in the object
# we primarily care about changed and state
# change is if this module effectively modified the target
# state will include any data that you want your module to pass back
# for consumption, for example, in a subsequent task
result = dict(
changed=False,
)
# 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}/l3FirewallRules/'}
update_urls = {'mx_l3_firewall': '/networks/{net_id}/l3FirewallRules/'}
meraki.url_catalog['get_all'].update(query_urls)
meraki.url_catalog['update'] = update_urls
payload = None
# 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
# FIXME: Work with Meraki so they can implement a check mode
if module.check_mode:
meraki.exit_json(**meraki.result)
# 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))
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']:
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 len(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[len(rules) - 1]['syslogEnabled'] != meraki.params['syslog_default_rule']:
update = True
if update is False:
default_rule = rules[len(rules) - 1].copy()
del rules[len(rules) - 1] # Remove default rule for comparison
for r in range(len(rules) - 1):
if meraki.is_update_required(rules[r], payload['rules'][r]) is True:
update = True
rules.append(default_rule)
except KeyError:
pass
if update is True:
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()

@ -1,506 +0,0 @@
#!/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
version_added: "2.9"
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
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,
blacklisted_countries,
host,
ip_range,
port,
whitelisted_countries]
type: str
application:
description:
- Application to filter.
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
application_category:
description:
- Category of applications to filter.
suboptions:
name:
description:
- Name of application category to filter as defined by Meraki.
type: str
id:
description:
- URI of application category 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
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: 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: whitelisted_countries
countries:
- US
- FR
- type: blacklisted_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
whitelistedCountries:
description: Countries to be whitelisted.
returned: success
type: str
sample: CA
blacklistedCountries:
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
import os
from ansible.module_utils.basic import AnsibleModule, json, env_fallback
from ansible.module_utils.common.dict_transformations import recursive_diff
from ansible.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'] == 'blacklisted_countries':
new_rule = {'policy': rule['policy'],
'type': 'blacklistedCountries',
'value': rule['countries']
}
elif rule['type'] == 'whitelisted_countries':
new_rule = {'policy': rule['policy'],
'type': 'whitelistedCountries',
'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']:
print(rule['type'])
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',
'blacklisted_countries',
'host',
'ip_range',
'port',
'whitelisted_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'),
)
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'),
)
# seed the result dict in the object
# we primarily care about changed and state
# change is if this module effectively modified the target
# state will include any data that you want your module to pass back
# for consumption, for example, in a subsequent task
result = dict(
changed=False,
)
# 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'] == 'blacklisted_countries' and rule['countries'] is None:
meraki.fail_json(msg="countries argument is required when type is blacklisted_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'] == 'whitelisted_countries' and rule['countries'] is None:
meraki.fail_json(msg="countries argument is required when type is whitelisted_countries.")
meraki.params['follow_redirects'] = 'all'
query_urls = {'mx_l7_firewall': '/networks/{net_id}/l7FirewallRules/'}
query_category_urls = {'mx_l7_firewall': '/networks/{net_id}/l7FirewallRules/applicationCategories'}
update_urls = {'mx_l7_firewall': '/networks/{net_id}/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)
diff = recursive_diff(restructure_response(rules), response)
meraki.result['diff'] = {'before': diff[0],
'after': diff[1],
}
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:
diff = recursive_diff(restructure_response(rules), response)
meraki.result['diff'] = {'before': diff[0],
'after': diff[1],
}
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()

@ -1,673 +0,0 @@
#!/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_nat
short_description: Manage NAT rules in Meraki cloud
version_added: "2.9"
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
one_to_one:
description:
- List of 1:1 NAT rules.
type: list
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
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
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
one_to_many:
description:
- List of 1:many NAT rules.
type: list
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
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
port_forwarding:
description:
- List of port forwarding rules.
type: list
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
protocol:
description:
- Protocol to forward traffic for.
choices: [tcp, udp]
type: str
author:
- Kevin Breit (@kbreit)
extends_documentation_fragment: 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
'''
import os
from ansible.module_utils.basic import AnsibleModule, json, env_fallback
from ansible.module_utils.urls import fetch_url
from ansible.module_utils._text import to_native
from ansible.module_utils.common.dict_transformations import recursive_diff
from ansible.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', element='str'),
allowed_ips=dict(type='list'),
)
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'),
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', element='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', element='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'),
)
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', 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}/oneToManyNatRules'}
onetoone_urls = {'nat': '/networks/{net_id}/oneToOneNatRules'}
port_forwarding_urls = {'nat': '/networks/{net_id}/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()

@ -1,390 +0,0 @@
#!/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
version_added: "2.6"
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
net_name:
description:
- Name of a network.
aliases: [ name, network ]
net_id:
description:
- ID number of a network.
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
tags:
type: list
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.
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
version_added: '2.9'
disable_my_meraki:
description: >
- Disables 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)).
- Mutually exclusive of C(enable_my_meraki).
- Will be deprecated in Ansible 2.13 in favor of C(enable_my_meraki).
type: bool
version_added: '2.7'
enable_my_meraki:
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)).
- Ansible 2.7 had this parameter as C(disable_my_meraki).
type: bool
version_added: '2.9'
enable_remote_status_page:
description:
- Enables access to the device status page (U(http://device LAN IP)).
- Can only be set if C(enable_my_meraki:) is set to C(yes).
type: bool
version_added: '2.9'
author:
- Kevin Breit (@kbreit)
extends_documentation_fragment: 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
'''
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: str
sample: " production wireless "
time_zone:
description: Timezone where network resides.
returned: success
type: str
sample: America/Chicago
type:
description: Functional type of network.
returned: success
type: str
sample: switch
disable_my_meraki_com:
description: States whether U(my.meraki.com) and other device portals should be disabled.
returned: success
type: bool
sample: true
disableRemoteStatusPage:
description: Disables access to the device status page.
returned: success
type: bool
sample: true
'''
import os
from ansible.module_utils.basic import AnsibleModule, json, env_fallback
from ansible.module_utils.urls import fetch_url
from ansible.module_utils._text import to_native
from ansible.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 construct_tags(tags):
formatted_tags = ' '.join(tags)
return ' {0} '.format(formatted_tags) # Meraki needs space padding
def list_to_string(data):
new_string = str()
for i, item in enumerate(data):
if i == len(new_string) - 1:
new_string += i
else:
new_string = "{0}{1} ".format(new_string, item)
return new_string.strip()
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', choices=['wireless', 'switch', 'appliance'], aliases=['net_type']),
tags=dict(type='list'),
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'),
disable_my_meraki=dict(type='bool', removed_in_version=2.13),
enable_my_meraki=dict(type='bool'),
enable_remote_status_page=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=False,
mutually_exclusive=[('disable_my_meraki', 'enable_my_meraki'),
]
)
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}'}
enable_vlans_urls = {'network': '/networks/{net_id}/vlansEnabledState'}
get_vlan_status_urls = {'network': '/networks/{net_id}/vlansEnabledState'}
meraki.url_catalog['create'] = create_urls
meraki.url_catalog['update'] = update_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['enable_my_meraki'] is True and meraki.params['enable_remote_status_page'] is False:
meraki.fail_json(msg='enable_my_meraki must be true when setting enable_remote_status_page')
# 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
if module.check_mode:
return meraki.result
# 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['type'] = list_to_string(meraki.params['type'])
if meraki.params['tags']:
payload['tags'] = construct_tags(meraki.params['tags'])
if meraki.params['timezone']:
payload['timeZone'] = meraki.params['timezone']
if meraki.params['enable_my_meraki'] is not None:
if meraki.params['enable_my_meraki'] is True:
payload['disableMyMerakiCom'] = False
else:
payload['disableMyMerakiCom'] = True
elif meraki.params['disable_my_meraki'] is not None:
payload['disableMyMerakiCom'] = meraki.params['disable_my_meraki']
if meraki.params['enable_remote_status_page'] is not None:
if meraki.params['enable_remote_status_page'] is True:
payload['disableRemoteStatusPage'] = False
# meraki.fail_json(msg="Debug", payload=payload)
else:
payload['disableRemoteStatusPage'] = 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']
if not org_id:
org_id = meraki.get_org_id(meraki.params['org_name'])
nets = meraki.get_nets(org_id=org_id)
# check if network is created
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:
meraki.result['data'] = meraki.get_net(meraki.params['org_name'],
meraki.params['net_name'],
data=nets
)
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.")
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
# meraki.fail_json(msg="nets", nets=nets, net_id=net_id)
# meraki.fail_json(msg="compare", original=net, payload=payload)
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 = {'enabled': meraki.params['enable_vlans']}
# meraki.fail_json(msg="here", payload=payload)
if meraki.is_update_required(status, payload):
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)
net = meraki.get_net(meraki.params['org_name'], net_id=net_id, data=nets)
if meraki.is_update_required(net, payload):
path = meraki.construct_path('update', net_id=net_id)
# meraki.fail_json(msg="Payload", path=path, payload=payload)
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:
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()

@ -1,244 +0,0 @@
#!/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
version_added: "2.6"
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
clone:
description:
- Organization to clone to a new organization.
org_name:
description:
- Name of organization.
- If C(clone) is specified, C(org_name) is the name of the new organization.
aliases: [ name, organization ]
org_id:
description:
- ID of organization.
aliases: [ id ]
type: str
author:
- Kevin Breit (@kbreit)
extends_documentation_fragment: 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
'''
import os
from ansible.module_utils.basic import AnsibleModule, json, env_fallback
from ansible.module_utils.urls import fetch_url
from ansible.module_utils._text import to_native
from ansible.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']),
)
# seed the result dict in the object
# we primarily care about changed and state
# change is if this module effectively modified the target
# state will include any data that you want your module to pass back
# for consumption, for example, in a subsequent task
result = dict(
changed=False,
)
# 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.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()

@ -1,401 +0,0 @@
#!/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
version_added: "2.6"
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:
- Semi-colon delimited IP addresses which can perform SNMP queries.
type: str
net_name:
description:
- Name of network.
type: str
version_added: '2.9'
net_id:
description:
- ID of network.
type: str
version_added: '2.9'
access:
description:
- Type of SNMP access.
choices: [community, none, users]
type: str
version_added: '2.9'
community_string:
description:
- SNMP community string.
- Only relevant if C(access) is set to C(community).
type: str
version_added: '2.9'
users:
description:
- Information about users with access to SNMP.
- Only relevant if C(access) is set to C(users).
type: list
version_added: '2.9'
suboptions:
username:
description: Username of user with access.
type: str
version_added: '2.9'
passphrase:
description: Passphrase for user SNMP access.
type: str
version_added: '2.9'
author:
- Kevin Breit (@kbreit)
extends_documentation_fragment: 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
peerIps:
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 recursive_diff, snake_dict_to_camel_dict
from ansible.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['peer_ips']:
if len(meraki.params['peer_ips']) > 7:
if ';' not in meraki.params['peer_ips']:
meraki.fail_json(msg='Peer IP addresses are semi-colon delimited.')
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:
diff = recursive_diff(snmp, full_compare)
snmp.update(payload)
meraki.result['data'] = snmp
meraki.result['changed'] = True
meraki.result['diff'] = {'before': diff[0],
'after': diff[1]}
meraki.exit_json(**meraki.result)
r = meraki.request(path,
method='PUT',
payload=json.dumps(payload))
if meraki.status == 200:
diff = recursive_diff(snmp, r)
meraki.result['diff'] = {'before': diff[0],
'after': diff[1]}
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='str'),
access=dict(type='str', choices=['none', 'community', 'users']),
community_string=dict(type='str', no_log=True),
users=dict(type='list', default=None, elements='', 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}/snmpSettings'}
update_urls = {'snmp': '/organizations/{org_id}/snmp'}
update_net_urls = {'snmp': '/networks/{net_id}/snmpSettings'}
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()

@ -1,606 +0,0 @@
#!/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_ssid
short_description: Manage wireless SSIDs in the Meraki cloud
version_added: "2.7"
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 WPA2 specification.
type: str
choices: [WPA1 and WPA2, WPA2 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']
radius_servers:
description:
- List of RADIUS servers.
type: list
suboptions:
host:
description:
- IP address or hostname of RADIUS server.
type: str
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
suboptions:
host:
description:
- IP address or hostname of RADIUS server.
type: str
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
suboptions:
tags:
description:
- List of AP tags.
type: list
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
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: 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.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 main():
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',
}
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']),
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']),
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'),
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'),
)
# seed the result dict in the object
# we primarily care about changed and state
# change is if this module effectively modified the target
# state will include any data that you want your module to pass back
# for consumption, for example, in a subsequent task
result = dict(
changed=False,
)
# 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}/ssids'}
query_url = {'ssid': '/networks/{net_id}/ssids/{number}'}
update_url = {'ssid': '/networks/{net_id}/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
# 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
# FIXME: Work with Meraki so they can implement a check mode
if module.check_mode:
meraki.exit_json(**meraki.result)
# 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 = dict()
for k, v in param_map.items():
if meraki.params[v] is not None:
payload[k] = meraki.params[v]
# Short term solution for camelCase/snake_case differences
# Will be addressed later with a method in module utils
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
try:
tags = ','.join(i['tags'])
del i['tags']
i['tags'] = tags
except KeyError:
pass
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.')
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.')
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()

@ -1,375 +0,0 @@
#!/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_static_route
short_description: Manage static routes in the Meraki cloud
version_added: "2.8"
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
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
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: 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
'''
import os
from ansible.module_utils.basic import AnsibleModule, json, env_fallback
from ansible.module_utils.urls import fetch_url
from ansible.module_utils._text import to_native
from ansible.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': meraki.params['route_id']})
r = meraki.request(path, method='GET')
return r
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}/staticRoutes'}
query_one_urls = {'static_route': '/networks/{net_id}/staticRoutes/{route_id}'}
create_urls = {'static_route': '/networks/{net_id}/staticRoutes/'}
update_urls = {'static_route': '/networks/{net_id}/staticRoutes/{route_id}'}
delete_urls = {'static_route': '/networks/{net_id}/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']
# meraki.fail_json(msg="payload", payload=payload)
if meraki.params['enabled'] is not None:
payload['enabled'] = meraki.params['enabled']
if meraki.params['route_id']:
existing_route = get_static_route(meraki, net_id, meraki.params['route_id'])
proposed = existing_route.copy()
proposed.update(payload)
if module.check_mode:
meraki.result['data'] = proposed
meraki.result['data'].update(payload)
meraki.exit_json(**meraki.result)
if meraki.is_update_required(existing_route, proposed, optional_ignore=['id']):
path = meraki.construct_path('update', net_id=net_id, custom={'route_id': meraki.params['route_id']})
meraki.result['data'] = meraki.request(path, method="PUT", payload=json.dumps(payload))
meraki.result['changed'] = True
else:
meraki.result['data'] = existing_route
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()

@ -1,397 +0,0 @@
#!/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_switchport
short_description: Manage switchports on a switch in the Meraki cloud
version_added: "2.7"
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_number:
description:
- Number of the access policy to apply.
- Only applicable to access port types.
allowed_vlans:
description:
- List of VLAN numbers to be allowed on switchport.
default: all
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)]
name:
description:
- Switchport description.
aliases: [description]
number:
description:
- Port number.
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.
stp_guard:
description:
- Set state of STP guard.
choices: [disabled, root guard, bpdu guard, loop guard]
default: disabled
tags:
description:
- Space delimited list of tags to assign to a port.
type:
description:
- Set port type.
choices: [access, trunk]
default: access
vlan:
description:
- VLAN number assigned to port.
- If a port is of type trunk, the specified VLAN is the native VLAN.
voice_vlan:
description:
- VLAN number assigned to a port for voice traffic.
- Only applicable to access port type.
author:
- Kevin Breit (@kbreit)
extends_documentation_fragment: 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: Space delimited list of tags assigned to port.
returned: success
type: str
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"
'''
import os
from ansible.module_utils.basic import AnsibleModule, json, env_fallback
from ansible.module_utils.urls import fetch_url
from ansible.module_utils._text import to_native
from ansible.module_utils.network.meraki.meraki import MerakiModule, meraki_argument_spec
param_map = {'access_policy_number': 'accessPolicyNumber',
'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 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='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', 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_number=dict(type='str'),
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}/switchPorts'}
query_url = {'switchport': '/devices/{serial}/switchPorts/{number}'}
update_url = {'switchport': '/devices/{serial}/switchPorts/{number}'}
meraki.url_catalog['get_all'].update(query_urls)
meraki.url_catalog['get_one'].update(query_url)
meraki.url_catalog['update'] = update_url
payload = None
# 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
# FIXME: Work with Meraki so they can implement a check mode
if module.check_mode:
meraki.exit_json(**meraki.result)
# 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 = dict()
for k, v in meraki.params.items():
try:
payload[param_map[k]] = v
except KeyError:
pass
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
if meraki.is_update_required(original, proposed, optional_ignore=['number']):
path = meraki.construct_path('update', custom={'serial': meraki.params['serial'],
'number': meraki.params['number'],
})
response = meraki.request(path, method='PUT', payload=json.dumps(payload))
meraki.result['data'] = response
meraki.result['changed'] = True
else:
meraki.result['data'] = original
# in the event of a successful module execution, you will want to
# simple AnsibleModule.exit_json(), passing the key/value results
meraki.exit_json(**meraki.result)
if __name__ == '__main__':
main()

@ -1,257 +0,0 @@
#!/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.
version_added: "2.8"
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
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
author:
- Kevin Breit (@kbreit)
extends_documentation_fragment: 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:
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.module_utils.common.dict_transformations import recursive_diff
from ansible.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', 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 = dict()
original['servers'] = r
if meraki.is_update_required(original, payload):
if meraki.module.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)
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.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()

@ -1,446 +0,0 @@
#!/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_vlan
short_description: Manage VLANs in the Meraki cloud
version_added: "2.7"
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
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
author:
- Kevin Breit (@kbreit)
extends_documentation_fragment: 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: 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, json, env_fallback
from ansible.module_utils._text import to_native
from ansible.module_utils.common.dict_transformations import recursive_diff
from ansible.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_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 format_dns(nameservers):
return nameservers.replace(';', '\n')
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(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'),
)
# seed the result dict in the object
# we primarily care about changed and state
# change is if this module effectively modified the target
# state will include any data that you want your module to pass back
# for consumption, for example, in a subsequent task
result = dict(
changed=False,
)
# 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}/vlans'}
query_url = {'vlan': '/networks/{net_id}/vlans/{vlan_id}'}
create_url = {'vlan': '/networks/{net_id}/vlans'}
update_url = {'vlan': '/networks/{net_id}/vlans/'}
delete_url = {'vlan': '/networks/{net_id}/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 = {'id': meraki.params['vlan_id'],
'name': meraki.params['name'],
'subnet': meraki.params['subnet'],
'applianceIp': meraki.params['appliance_ip'],
}
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')
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']
ignored = ['networkId']
if meraki.is_update_required(original, payload, optional_ignore=ignored):
meraki.result['diff'] = dict()
diff = recursive_diff(original, payload)
meraki.result['diff']['before'] = diff[0]
meraki.result['diff']['after'] = diff[1]
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
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()

@ -1,342 +0,0 @@
#!/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
version_added: "2.9"
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, status]
test_id:
description:
- ID of webhook test query.
type: str
author:
- Kevin Breit (@kbreit)
extends_documentation_fragment: 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
'''
import os
from ansible.module_utils.basic import AnsibleModule, json, env_fallback
from ansible.module_utils._text import to_native
from ansible.module_utils.common.dict_transformations import recursive_diff
from ansible.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 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', 'status']),
test_id=dict(type='str'),
)
# seed the result dict in the object
# we primarily care about changed and state
# change is if this module effectively modified the target
# state will include any data that you want your module to pass back
# for consumption, for example, in a subsequent task
result = dict(
changed=False,
)
# 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}/httpServers'}
query_one_url = {'webhooks': '/networks/{net_id}/httpServers/{hookid}'}
create_url = {'webhooks': '/networks/{net_id}/httpServers'}
update_url = {'webhooks': '/networks/{net_id}/httpServers/{hookid}'}
delete_url = {'webhooks': '/networks/{net_id}/httpServers/{hookid}'}
test_url = {'webhooks': '/networks/{net_id}/httpServers/webhookTests'}
test_status_url = {'webhooks': '/networks/{net_id}/httpServers/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']
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
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
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 == 200:
meraki.result['data'] = response
meraki.exit_json(**meraki.result)
elif meraki.params['test'] == 'status':
if meraki.params['test_id'] is None:
meraki.fail_json("test_id is required when querying test status.")
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
meraki.exit_json(**meraki.result)
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: # Test to see if it 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:
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)
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.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()

@ -1,78 +0,0 @@
# -*- 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)
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.
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
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
'''

@ -1,384 +0,0 @@
# Test code for the Meraki Admin module
# Copyright: (c) 2018, Kevin Breit (@kbreit)
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
---
- block:
- name: Create new administrator in check mode
meraki_admin:
auth_key: '{{auth_key}}'
state: present
org_name: '{{test_org_name}}'
name: Jane Doe
email: '{{email_prefix}}+janedoe@{{email_domain}}'
org_access: read-only
delegate_to: localhost
check_mode: yes
register: create_org_check
- name: Create new admin check mode assertion
assert:
that:
- create_org_check is changed
- 'create_org_check.data.name == "Jane Doe"'
- name: Create new administrator
meraki_admin:
auth_key: '{{auth_key}}'
state: present
org_name: '{{test_org_name}}'
name: Jane Doe
email: '{{email_prefix}}+janedoe@{{email_domain}}'
org_access: read-only
delegate_to: localhost
register: create_orgaccess
- name: Create new admin assertion
assert:
that:
- create_orgaccess.changed == true
- 'create_orgaccess.data.name == "Jane Doe"'
- name: Delete recently created administrator with check mode
meraki_admin:
auth_key: '{{auth_key}}'
state: absent
org_name: '{{test_org_name}}'
email: '{{email_prefix}}+janedoe@{{email_domain}}'
delegate_to: localhost
register: delete_one_check
check_mode: yes
- assert:
that:
- delete_one_check is changed
- name: Delete recently created administrator
meraki_admin:
auth_key: '{{auth_key}}'
state: absent
org_name: '{{test_org_name}}'
email: '{{email_prefix}}+janedoe@{{email_domain}}'
delegate_to: localhost
register: delete_one
- name: Create new administrator with org_id
meraki_admin:
auth_key: '{{auth_key}}'
state: present
org_id: '{{test_org_id}}'
name: Jane Doe
email: '{{email_prefix}}+janedoe@{{email_domain}}'
orgAccess: read-only
delegate_to: localhost
register: create_orgaccess_id
- name: Create new admin assertion
assert:
that:
- create_orgaccess_id.changed == true
- 'create_orgaccess_id.data.name == "Jane Doe"'
- name: Create administrator with tags with check mode
meraki_admin:
auth_key: '{{auth_key}}'
state: present
org_name: '{{test_org_name}}'
name: John Doe
email: '{{email_prefix}}+johndoe@{{email_domain}}'
orgAccess: none
tags:
- { "tag": "production", "access": "read-only" }
- tag: beta
access: full
delegate_to: localhost
register: create_tags_check
check_mode: yes
- debug:
var: create_tags_check
- assert:
that:
- create_tags_check is changed
- create_tags_check.data.name == "John Doe"
- create_tags_check.data.tags | length == 2
- name: Create administrator with tags
meraki_admin:
auth_key: '{{auth_key}}'
state: present
org_name: '{{test_org_name}}'
name: John Doe
email: '{{email_prefix}}+johndoe@{{email_domain}}'
orgAccess: none
tags:
- { "tag": "production", "access": "read-only" }
- tag: beta
access: full
delegate_to: localhost
register: create_tags
- assert:
that:
- create_tags.changed == true
- create_tags.data.name == "John Doe"
- create_tags.data.tags | length == 2
- name: Create administrator with invalid tags
meraki_admin:
auth_key: '{{auth_key}}'
state: present
org_name: '{{test_org_name}}'
name: Jake Doe
email: '{{email_prefix}}+jakedoe@{{email_domain}}'
orgAccess: none
tags:
- { "tag": "production", "access": "read-only" }
- { "tag": "alpha", "access": "invalid" }
delegate_to: localhost
register: create_tags_invalid
ignore_errors: yes
- assert:
that:
- '"400" in create_tags_invalid.msg'
# - '"must contain only valid tags" in create_tags_invalid.msg'
- name: Create administrator with invalid tag permission
meraki_admin:
auth_key: '{{auth_key}}'
state: present
org_name: '{{test_org_name}}'
name: Jake Doe
email: '{{email_prefix}}+jakedoe@{{email_domain}}'
orgAccess: none
tags:
- { "tag": "production", "access": "read-only" }
- { "tag": "beta", "access": "invalid" }
delegate_to: localhost
register: create_tags_invalid_permission
ignore_errors: yes
- assert:
that:
- '"400" in create_tags_invalid_permission.msg'
# - '"Invalid permission type" in create_tags_invalid_permission.msg'
- name: Make sure TestNet and TestNet2 are created
meraki_network:
auth_key: '{{auth_key}}'
state: present
org_name: '{{test_org_name}}'
net_name: '{{item}}'
type: switch
loop:
- TestNet
- TestNet2
- name: Create administrator with networks with check mode
meraki_admin:
auth_key: '{{auth_key}}'
state: present
org_name: '{{test_org_name}}'
name: Jim Doe
email: '{{email_prefix}}+jimdoe@{{email_domain}}'
orgAccess: none
networks:
- { "network": "TestNet", "access": "read-only" }
- { "network": "TestNet2", "access": "full" }
delegate_to: localhost
register: create_network_check
check_mode: yes
- assert:
that:
- create_network_check is changed
- create_network_check.data.name == "Jim Doe"
- create_network_check.data.networks | length == 2
- name: Create administrator with networks
meraki_admin:
auth_key: '{{auth_key}}'
state: present
org_name: '{{test_org_name}}'
name: Jim Doe
email: '{{email_prefix}}+jimdoe@{{email_domain}}'
orgAccess: none
networks:
- { "network": "TestNet", "access": "read-only" }
- { "network": "TestNet2", "access": "full" }
delegate_to: localhost
register: create_network
- assert:
that:
- create_network.changed == true
- create_network.data.name == "Jim Doe"
- create_network.data.networks | length == 2
- name: Update administrator with check mode
meraki_admin:
auth_key: '{{auth_key}}'
state: present
org_name: '{{test_org_name}}'
name: Jim Doe
email: '{{email_prefix}}+jimdoe@{{email_domain}}'
orgAccess: none
networks:
- { "network": "TestNet", "access": "full" }
delegate_to: localhost
register: update_network_check
check_mode: yes
- debug:
var: update_network_check
- assert:
that:
- update_network_check is changed
- update_network_check.data.networks.0.access == "full"
- update_network_check.data.networks | length == 1
- name: Update administrator
meraki_admin:
auth_key: '{{auth_key}}'
state: present
org_name: '{{test_org_name}}'
name: Jim Doe
email: '{{email_prefix}}+jimdoe@{{email_domain}}'
orgAccess: none
networks:
- { "network": "TestNet", "access": "full" }
delegate_to: localhost
register: update_network
- assert:
that:
- update_network.changed == true
- update_network.data.networks.0.access == "full"
- update_network.data.networks | length == 1
- name: Update administrator for idempotency check with check mode
meraki_admin:
auth_key: '{{auth_key}}'
state: present
org_name: '{{test_org_name}}'
name: Jim Doe
email: '{{email_prefix}}+jimdoe@{{email_domain}}'
orgAccess: none
networks:
- { "network": "TestNet", "access": "full" }
delegate_to: localhost
register: update_network_idempotent_check
check_mode: yes
- debug:
var: update_network_idempotent_check
- assert:
that:
- update_network_idempotent_check is not changed
- name: Update administrator for idempotency
meraki_admin:
auth_key: '{{auth_key}}'
state: present
org_name: '{{test_org_name}}'
name: Jim Doe
email: '{{email_prefix}}+jimdoe@{{email_domain}}'
orgAccess: none
networks:
- { "network": "TestNet", "access": "full" }
delegate_to: localhost
register: update_network_idempotent
- assert:
that:
- update_network_idempotent.changed == false
- update_network_idempotent.data is defined
- name: Create administrator with invalid network
meraki_admin:
auth_key: '{{auth_key}}'
state: present
org_name: '{{test_org_name}}'
name: John Doe
email: '{{email_prefix}}+John@{{email_domain}}'
orgAccess: none
networks:
- { "network": "readnet", "access": "read-only" }
delegate_to: localhost
register: create_network_invalid
ignore_errors: yes
- assert:
that:
- '"No network found with the name" in create_network_invalid.msg'
# - '"400" in create_network_invalid.msg'
- name: Query all administrators
meraki_admin:
auth_key: '{{auth_key}}'
state: query
org_name: '{{test_org_name}}'
delegate_to: localhost
register: query_all
- assert:
that:
- query_all.data | length == 4
- query_all.changed == False
- name: Query admin by name
meraki_admin:
auth_key: '{{auth_key}}'
state: query
org_name: '{{test_org_name}}'
name: Jane Doe
delegate_to: localhost
register: query_name
- name: Query admin by email
meraki_admin:
auth_key: '{{auth_key}}'
state: query
org_name: '{{test_org_name}}'
email: '{{email_prefix}}+janedoe@{{email_domain}}'
delegate_to: localhost
register: query_email
- assert:
that:
- query_name.data.name == "Jane Doe"
- 'query_email.data.email == "{{email_prefix}}+janedoe@{{email_domain}}"'
always:
#############################################################################
# Tear down starts here
#############################################################################
- name: Delete administrators
meraki_admin:
auth_key: '{{auth_key}}'
state: absent
org_name: '{{test_org_name}}'
email: '{{item}}'
delegate_to: localhost
register: delete_all
ignore_errors: yes
loop:
- '{{email_prefix}}+janedoe@{{email_domain}}'
- '{{email_prefix}}+johndoe@{{email_domain}}'
- '{{email_prefix}}+jimdoe@{{email_domain}}'
- name: Query all administrators
meraki_admin:
auth_key: '{{auth_key}}'
state: query
org_name: '{{test_org_name}}'
delegate_to: localhost
register: query_all_deleted
- assert:
that:
- query_all_deleted.data | length == 1

@ -1,196 +0,0 @@
# Test code for the Meraki Organization module
# Copyright: (c) 2018, Kevin Breit (@kbreit)
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
---
- block:
- name: Test an API key is provided
fail:
msg: Please define an API key
when: auth_key is not defined
- name: Query all configuration templates
meraki_config_template:
auth_key: '{{auth_key}}'
state: query
org_name: '{{test_org_name}}'
register: get_all
- name: Delete non-existant configuration template
meraki_config_template:
auth_key: '{{auth_key}}'
state: absent
org_name: '{{test_org_name}}'
config_template: FakeConfigTemplate
register: deleted
ignore_errors: yes
- assert:
that:
- '"No configuration template named" in deleted.msg'
- name: Create a network
meraki_network:
auth_key: '{{auth_key}}'
state: present
org_name: '{{ test_org_name }}'
net_name: '{{ test_net_name }}'
type: appliance
delegate_to: localhost
register: net_info
- set_fact:
net_id: '{{net_info.data.id}}'
- name: Bind a template to a network with check mode
meraki_config_template:
auth_key: '{{auth_key}}'
state: present
org_name: '{{ test_org_name }}'
net_name: '{{ test_net_name }}'
config_template: '{{test_template_name}}'
check_mode: yes
register: bind_check
- name: Bind a template to a network
meraki_config_template:
auth_key: '{{auth_key}}'
state: present
org_name: '{{ test_org_name }}'
net_name: '{{ test_net_name }}'
config_template: '{{test_template_name}}'
register: bind
- assert:
that:
bind.changed == True
- assert:
that:
bind_check is changed
- name: Bind a template to a network when it's already bound
meraki_config_template:
auth_key: '{{auth_key}}'
state: present
org_name: '{{ test_org_name }}'
net_name: '{{ test_net_name }}'
config_template: '{{test_template_name}}'
register: bind_invalid
ignore_errors: yes
- assert:
that:
- bind_invalid.changed == False
- name: Unbind a template from a network
meraki_config_template:
auth_key: '{{auth_key}}'
state: absent
org_name: '{{ test_org_name }}'
net_name: '{{ test_net_name }}'
config_template: '{{test_template_name}}'
register: unbind
- assert:
that:
unbind.changed == True
- name: Unbind a template from a network when it's not bound
meraki_config_template:
auth_key: '{{auth_key}}'
state: absent
org_name: '{{ test_org_name }}'
net_name: '{{ test_net_name }}'
config_template: '{{test_template_name}}'
register: unbind_invalid
- assert:
that:
unbind_invalid.changed == False
- name: Bind a template to a network via id
meraki_config_template:
auth_key: '{{auth_key}}'
state: present
org_name: '{{test_org_name}}'
net_id: '{{net_id}}'
config_template: '{{test_template_name}}'
register: bind_id
- assert:
that:
bind_id.changed == True
- name: Bind a template to a network via id for idempotency
meraki_config_template:
auth_key: '{{auth_key}}'
state: present
org_name: '{{test_org_name}}'
net_id: '{{net_id}}'
config_template: '{{test_template_name}}'
register: bind_id_idempotent
- assert:
that:
- bind_id_idempotent.changed == False
- bind_id_idempotent.data is defined
- name: Unbind a template from a network via id with check mode
meraki_config_template:
auth_key: '{{auth_key}}'
state: absent
org_name: '{{test_org_name}}'
net_id: '{{net_id}}'
config_template: '{{test_template_name}}'
check_mode: yes
register: unbind_id_check
- assert:
that:
unbind_id_check is changed
- name: Unbind a template from a network via id
meraki_config_template:
auth_key: '{{auth_key}}'
state: absent
org_name: '{{test_org_name}}'
net_id: '{{net_id}}'
config_template: '{{test_template_name}}'
register: unbind_id
- assert:
that:
unbind_id.changed == True
# This is disabled by default since they can't be created via API
- name: Delete sacrificial template with check mode
meraki_config_template:
auth_key: '{{auth_key}}'
state: absent
org_name: '{{test_org_name}}'
config_template: sacrificial_template
check_mode: yes
register: delete_template_check
# This is disabled by default since they can't be created via API
- name: Delete sacrificial template
meraki_config_template:
auth_key: '{{auth_key}}'
state: absent
org_name: '{{test_org_name}}'
config_template: sacrificial_template
output_level: debug
register: delete_template
- debug:
var: delete_template
always:
- name: Delete network
meraki_network:
auth_key: '{{auth_key}}'
state: absent
org_name: '{{ test_org_name }}'
net_name: '{{ test_net_name }}'
delegate_to: localhost

@ -1,117 +0,0 @@
# Test code for the Meraki Organization module
# Copyright: (c) 2018, Kevin Breit (@kbreit)
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
---
- block:
- name: Test an API key is provided
fail:
msg: Please define an API key
when: auth_key is not defined
- name: Use an invalid domain
meraki_config_template:
auth_key: '{{ auth_key }}'
host: marrrraki.com
state: query
org_name: DevTestOrg
output_level: debug
delegate_to: localhost
register: invalid_domain
ignore_errors: yes
- name: Connection assertions
assert:
that:
- '"Failed to connect to" in invalid_domain.msg'
- name: Query all configuration templates
meraki_config_template:
auth_key: '{{auth_key}}'
state: query
org_name: DevTestOrg
register: get_all
- name: Delete non-existant configuration template
meraki_config_template:
auth_key: '{{auth_key}}'
state: absent
org_name: DevTestOrg
config_template: DevConfigTemplateInvalid
register: deleted
ignore_errors: yes
- assert:
that:
- '"No configuration template named" in deleted.msg'
- name: Create a network
meraki_network:
auth_key: '{{auth_key}}'
state: present
org_name: '{{ test_org_name }}'
net_name: '{{ test_net_name }}'
type: appliance
delegate_to: localhost
- name: Bind a template to a network
meraki_config_template:
auth_key: '{{auth_key}}'
state: present
org_name: '{{ test_org_name }}'
net_name: '{{ test_net_name }}'
config_template: DevConfigTemplate
register: bind
- assert:
that:
bind.changed == True
- name: Bind a template to a network when it's already bound
meraki_config_template:
auth_key: '{{auth_key}}'
state: present
org_name: '{{ test_org_name }}'
net_name: '{{ test_net_name }}'
config_template: DevConfigTemplate
register: bind_invalid
ignore_errors: yes
- assert:
that:
- bind_invalid.changed == False
- name: Unbind a template from a network
meraki_config_template:
auth_key: '{{auth_key}}'
state: absent
org_name: '{{ test_org_name }}'
net_name: '{{ test_net_name }}'
config_template: DevConfigTemplate
register: unbind
- assert:
that:
unbind.changed == True
- name: Unbind a template from a network when it's not bound
meraki_config_template:
auth_key: '{{auth_key}}'
state: absent
org_name: '{{ test_org_name }}'
net_name: '{{ test_net_name }}'
config_template: DevConfigTemplate
register: unbind_invalid
- assert:
that:
unbind_invalid.changed == False
always:
- name: Delete network
meraki_network:
auth_key: '{{auth_key}}'
state: absent
org_name: '{{ test_org_name }}'
net_name: '{{ test_net_name }}'
delegate_to: localhost

@ -1,247 +0,0 @@
# Test code for the Meraki Content Filteringmodule
# Copyright: (c) 2019, Kevin Breit (@kbreit)
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
---
- block:
- name: Test an API key is provided
fail:
msg: Please define an API key
when: auth_key is not defined
- name: Create network
meraki_network:
auth_key: '{{ auth_key }}'
state: present
org_name: '{{test_org_name}}'
net_name: '{{test_net_name}}'
type: appliance
timezone: America/Chicago
delegate_to: localhost
register: create_net_appliance
- name: Test net_name and id exclusivity
meraki_content_filtering:
auth_key: '{{auth_key}}'
org_name: '{{test_org_name}}'
net_name: '{{test_net_name}}'
net_id: 12345
state: present
allowed_urls:
- "http://www.ansible.com/*"
register: net_exclusive
ignore_errors: yes
- assert:
that:
- 'net_exclusive.msg == "net_name and net_id are mutually exclusive"'
- name: Set single allowed URL pattern with check mode
meraki_content_filtering:
auth_key: '{{auth_key}}'
org_name: '{{test_org_name}}'
net_name: '{{test_net_name}}'
state: present
allowed_urls:
- "http://www.ansible.com/*"
register: single_allowed_check
check_mode: yes
- assert:
that:
- single_allowed_check.data.allowed_url_patterns | length == 1
- single_allowed_check is changed
- name: Set single allowed URL pattern
meraki_content_filtering:
auth_key: '{{auth_key}}'
org_name: '{{test_org_name}}'
net_name: '{{test_net_name}}'
state: present
allowed_urls:
- "http://www.ansible.com/*"
register: single_allowed
- assert:
that:
- single_allowed.data.allowed_url_patterns | length == 1
- name: Set single allowed URL pattern for idempotency with check mode
meraki_content_filtering:
auth_key: '{{auth_key}}'
org_name: '{{test_org_name}}'
net_name: '{{test_net_name}}'
state: present
allowed_urls:
- "http://www.ansible.com/*"
register: single_allowed_idempotent_check
check_mode: yes
- debug:
var: single_allowed_idempotent_check
- assert:
that:
- single_allowed_idempotent_check is not changed
- single_allowed.data.allowed_url_patterns | length == 1
- name: Set single allowed URL pattern for idempotency
meraki_content_filtering:
auth_key: '{{auth_key}}'
org_name: '{{test_org_name}}'
net_name: '{{test_net_name}}'
state: present
allowed_urls:
- "http://www.ansible.com/*"
register: single_allowed_idempotent
- debug:
var: single_allowed_idempotent
- assert:
that:
- single_allowed_idempotent.changed == False
- single_allowed_idempotent.data is defined
- name: Set single blocked URL pattern
meraki_content_filtering:
auth_key: '{{auth_key}}'
org_name: '{{test_org_name}}'
net_name: '{{test_net_name}}'
state: present
blocked_urls:
- "http://www.ansible.com/*"
register: single_blocked
- debug:
var: single_blocked
- assert:
that:
- single_blocked.data.blocked_url_patterns | length == 1
- name: Set two allowed URL pattern
meraki_content_filtering:
auth_key: '{{auth_key}}'
org_name: '{{test_org_name}}'
net_name: '{{test_net_name}}'
state: present
allowed_urls:
- "http://www.ansible.com/*"
- "http://www.redhat.com"
register: two_allowed
- debug:
var: two_allowed
- assert:
that:
- two_allowed.changed == True
- two_allowed.data.allowed_url_patterns | length == 2
- name: Set blocked URL category
meraki_content_filtering:
auth_key: '{{auth_key}}'
org_name: '{{test_org_name}}'
net_name: '{{test_net_name}}'
state: present
category_list_size: full list
blocked_categories:
- "Adult and Pornography"
register: blocked_category
- debug:
var: blocked_category
- assert:
that:
- blocked_category.changed == True
- blocked_category.data.blocked_url_categories | length == 1
- blocked_category.data.url_category_list_size == "fullList"
- name: Set blocked URL category with top sites
meraki_content_filtering:
auth_key: '{{auth_key}}'
org_name: '{{test_org_name}}'
net_name: '{{test_net_name}}'
state: present
category_list_size: top sites
blocked_categories:
- "Adult and Pornography"
register: blocked_category
- debug:
var: blocked_category
- assert:
that:
- blocked_category.changed == True
- blocked_category.data.blocked_url_categories | length == 1
- blocked_category.data.url_category_list_size == "topSites"
- name: Query all content filtering information
meraki_content_filtering:
auth_key: '{{auth_key}}'
org_name: '{{test_org_name}}'
net_name: '{{test_net_name}}'
state: query
delegate_to: localhost
register: query_all
- debug:
var: query_all
- name: Query all content filtering assertion
assert:
that:
- query_all.data.categories is defined
- query_all.data.policy is defined
- name: Query categories
meraki_content_filtering:
auth_key: '{{auth_key}}'
org_name: '{{test_org_name}}'
net_name: '{{test_net_name}}'
state: query
subset: categories
delegate_to: localhost
register: query_categories
- debug:
var: query_categories
- name: Query categories assertion
assert:
that:
- query_categories.data is defined
- name: Query content filtering policies
meraki_content_filtering:
auth_key: '{{auth_key}}'
org_name: '{{test_org_name}}'
net_name: '{{test_net_name}}'
subset: policy
state: query
delegate_to: localhost
register: query_policy
- debug:
var: query_policy
- name: Query contnet filtering policy assertion
assert:
that:
- query_policy.data is defined
always:
- name: Reset policies
meraki_content_filtering:
auth_key: '{{auth_key}}'
org_name: '{{test_org_name}}'
net_name: '{{test_net_name}}'
state: present
category_list_size: full list
allowed_urls:
-
blocked_urls:
-

@ -1,215 +0,0 @@
---
- block:
# This is commented out because a device cannot be unclaimed via API
# - name: Claim a device into an organization
# meraki_device:
# auth_key: '{{auth_key}}'
# org_name: '{{test_org_name}}'
# serial: '{{serial}}'
# state: present
# delegate_to: localhost
# register: claim_device_org
# - assert:
# that:
# - claim_device_org.changed == true
- name: Query status of all devices in an organization
meraki_device:
auth_key: '{{auth_key}}'
org_name: '{{test_org_name}}'
state: query
delegate_to: localhost
register: query_device_org
- debug:
msg: '{{query_device_org}}'
- name: Claim a device into a network
meraki_device:
auth_key: '{{auth_key}}'
org_name: '{{test_org_name}}'
net_name: '{{test_net_name}}'
serial: '{{serial}}'
state: present
delegate_to: localhost
register: claim_device
- debug:
msg: '{{claim_device}}'
- assert:
that:
- claim_device.changed == true
- name: Query all devices in one network by network ID
meraki_device:
auth_key: '{{auth_key}}'
org_name: '{{test_org_name}}'
net_id: '{{test_net_id}}'
state: query
delegate_to: localhost
register: query_one_net_id
- debug:
msg: '{{query_one_net_id}}'
- name: Query all devices in one network
meraki_device:
auth_key: '{{auth_key}}'
org_name: '{{test_org_name}}'
net_name: '{{test_net_name}}'
state: query
delegate_to: localhost
register: query_one_net
- debug:
msg: '{{query_one_net}}'
- name: Query device by serial
meraki_device:
auth_key: '{{auth_key}}'
org_name: '{{test_org_name}}'
serial: '{{serial}}'
state: query
delegate_to: localhost
register: query_serial_no_net
- debug:
msg: '{{query_serial_no_net}}'
- name: Query device by serial
meraki_device:
auth_key: '{{auth_key}}'
org_name: '{{test_org_name}}'
net_name: '{{test_net_name}}'
serial: '{{serial}}'
state: query
delegate_to: localhost
register: query_serial
- debug:
msg: '{{query_serial}}'
- assert:
that:
- query_serial.changed == False
- name: Query uplink information for a device
meraki_device:
auth_key: '{{auth_key}}'
org_name: '{{test_org_name}}'
net_name: '{{test_net_name}}'
serial_uplink: '{{serial}}'
state: query
delegate_to: localhost
register: query_serial_uplink
- debug:
msg: '{{query_serial_uplink}}'
- name: Query LLDP/CDP information about a device
meraki_device:
auth_key: '{{auth_key}}'
org_name: '{{test_org_name}}'
net_name: '{{test_net_name}}'
serial_lldp_cdp: '{{serial}}'
lldp_cdp_timespan: 6000
state: query
delegate_to: localhost
register: query_serial_lldp_cdp
- debug:
msg: '{{query_serial_lldp_cdp}}'
- name: Query a device by hostname
meraki_device:
auth_key: '{{auth_key}}'
org_name: '{{test_org_name}}'
net_name: '{{test_net_name}}'
hostname: test-hostname
state: query
delegate_to: localhost
register: query_hostname
- debug:
msg: '{{query_hostname}}'
- name: Query a device by model
meraki_device:
auth_key: '{{auth_key}}'
org_name: '{{test_org_name}}'
net_name: '{{test_net_name}}'
model: MR26
state: query
delegate_to: localhost
register: query_model
- debug:
msg: '{{query_model}}'
- name: Update a device
meraki_device:
auth_key: '{{auth_key}}'
org_name: '{{test_org_name}}'
net_name: '{{test_net_name}}'
serial: '{{serial}}'
name: mr26
address: 1060 W. Addison St., Chicago, IL
lat: 41.948038
lng: -87.65568
tags: recently-added
state: present
move_map_marker: True
note: Test device notes
delegate_to: localhost
register: update_device
- assert:
that:
- update_device.changed == true
- update_device.data.0.notes == "Test device notes"
- '"1060 W. Addison St., Chicago, IL" in update_device.data.0.address'
- name: Update a device with idempotency
meraki_device:
auth_key: '{{auth_key}}'
org_name: '{{test_org_name}}'
net_name: '{{test_net_name}}'
serial: '{{serial}}'
name: mr26
address: 1060 W. Addison St., Chicago, IL
lat: 41.948038
lng: -87.65568
tags: recently-added
state: present
move_map_marker: True
note: Test device notes
delegate_to: localhost
register: update_device_idempotent
- debug:
msg: '{{update_device_idempotent}}'
- assert:
that:
- update_device_idempotent.changed == False
- update_device_idempotent.data is defined
always:
- name: Remove a device from a network
meraki_device:
auth_key: '{{auth_key}}'
org_name: '{{test_org_name}}'
net_name: '{{test_net_name}}'
serial: '{{serial}}'
state: absent
delegate_to: localhost
register: delete_device
- debug:
msg: '{{delete_device}}'
- assert:
that:
- delete_device.changed == true

@ -1,7 +0,0 @@
# Test code for the Meraki Firewalled Services module
# Copyright: (c) 2018, Kevin Breit (@kbreit)
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
---
- name: Run test cases
include: tests.yml ansible_connection=local

@ -1,196 +0,0 @@
# Test code for the Meraki modules
# Copyright: (c) 2019, Kevin Breit (@kbreit)
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
---
- block:
- name: Create network
meraki_network:
auth_key: '{{ auth_key }}'
state: present
org_name: '{{test_org_name}}'
net_name: IntTestNetworkAppliance
type: appliance
register: create
- set_fact:
net_id: create.data.id
- name: Set icmp service to blocked with check mode
meraki_firewalled_services:
auth_key: '{{ auth_key }}'
state: present
org_name: '{{test_org_name}}'
net_name: IntTestNetworkAppliance
service: ICMP
access: blocked
register: icmp_blocked_check
check_mode: yes
- debug:
var: icmp_blocked_check
- assert:
that:
- icmp_blocked_check.data is defined
- icmp_blocked_check is changed
- 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
register: icmp_blocked
- debug:
var: icmp_blocked
- assert:
that:
- icmp_blocked.data is defined
- icmp_blocked is changed
- name: Set icmp service to blocked with idempotency
meraki_firewalled_services:
auth_key: '{{ auth_key }}'
state: present
org_name: '{{test_org_name}}'
net_name: IntTestNetworkAppliance
service: ICMP
access: blocked
register: icmp_blocked_idempotent
- debug:
var: icmp_blocked_idempotent
- assert:
that:
- icmp_blocked_idempotent.data is defined
- icmp_blocked_idempotent is not changed
- name: Set icmp service to restricted with check mode
meraki_firewalled_services:
auth_key: '{{ auth_key }}'
state: present
org_name: '{{test_org_name}}'
net_name: IntTestNetworkAppliance
service: web
access: restricted
allowed_ips:
- 192.0.1.1
- 192.0.1.2
check_mode: yes
register: web_restricted_check
- debug:
var: web_restricted_check
- assert:
that:
- web_restricted_check.data is defined
- web_restricted_check is changed
- name: Set icmp service to restricted
meraki_firewalled_services:
auth_key: '{{ auth_key }}'
state: present
org_name: '{{test_org_name}}'
net_name: IntTestNetworkAppliance
service: web
access: restricted
allowed_ips:
- 192.0.1.1
- 192.0.1.2
register: web_restricted
- debug:
var: web_restricted
- assert:
that:
- web_restricted.data is defined
- web_restricted is changed
- name: Set icmp service to restricted with idempotency
meraki_firewalled_services:
auth_key: '{{ auth_key }}'
state: present
org_name: '{{test_org_name}}'
net_name: IntTestNetworkAppliance
service: web
access: restricted
allowed_ips:
- 192.0.1.1
- 192.0.1.2
register: web_restricted_idempotent
- debug:
var: web_restricted_idempotent
- assert:
that:
- web_restricted_idempotent.data is defined
- web_restricted_idempotent is not changed
- name: Test error for access restricted and allowed_ips
meraki_firewalled_services:
auth_key: '{{ auth_key }}'
state: present
org_name: '{{test_org_name}}'
net_name: IntTestNetworkAppliance
service: web
access: unrestricted
allowed_ips:
- 192.0.1.1
- 192.0.1.2
register: access_error
ignore_errors: yes
- assert:
that:
- 'access_error.msg == "allowed_ips is only allowed when access is restricted."'
- name: Query appliance services
meraki_firewalled_services:
auth_key: '{{ auth_key }}'
state: query
org_name: '{{test_org_name}}'
net_name: IntTestNetworkAppliance
register: query_appliance
- debug:
var: query_appliance
- assert:
that:
- query_appliance.data is defined
- name: Query services
meraki_firewalled_services:
auth_key: '{{ auth_key }}'
state: query
org_name: '{{test_org_name}}'
net_name: IntTestNetworkAppliance
service: ICMP
register: query_service
- debug:
var: query_service
- assert:
that:
- query_service.data is defined
#############################################################################
# Tear down starts here
#############################################################################
always:
- name: Delete all networks
meraki_network:
auth_key: '{{ auth_key }}'
state: absent
org_name: '{{test_org_name}}'
net_name: IntTestNetworkAppliance

@ -1,247 +0,0 @@
# Test code for the Meraki VLAN module
# Copyright: (c) 2018, Kevin Breit (@kbreit)
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
---
- block:
- name: Test an API key is provided
fail:
msg: Please define an API key
when: auth_key is not defined
- name: Create test network
meraki_network:
auth_key: '{{auth_key}}'
state: present
org_name: '{{test_org_name}}'
net_name: '{{test_net_name}} - Malware'
type: appliance
delegate_to: localhost
register: net
- set_fact:
net_id: '{{net.data.id}}'
- name: Enable malware protection with check mode
meraki_malware:
auth_key: '{{auth_key}}'
state: present
org_name: '{{test_org_name}}'
net_name: '{{test_net_name}} - Malware'
mode: enabled
delegate_to: localhost
check_mode: yes
register: get_malware_check
- assert:
that:
- get_malware_check is changed
- get_malware_check.data is defined
- name: Enable malware protection
meraki_malware:
auth_key: '{{auth_key}}'
state: present
org_name: '{{test_org_name}}'
net_name: '{{test_net_name}} - Malware'
mode: enabled
delegate_to: localhost
register: get_malware
- debug:
var: get_malware
- assert:
that:
- get_malware is changed
- get_malware.data.mode is defined
- name: Enable malware protection with idempotency
meraki_malware:
auth_key: '{{auth_key}}'
state: present
org_name: '{{test_org_name}}'
net_name: '{{test_net_name}} - Malware'
mode: enabled
delegate_to: localhost
register: get_malware_idempotent
- debug:
var: get_malware_idempotent
- assert:
that:
- get_malware_idempotent is not changed
- get_malware_idempotent.data is defined
- name: Test error when mode is not set
meraki_malware:
auth_key: '{{auth_key}}'
state: present
org_name: '{{test_org_name}}'
net_name: '{{test_net_name}} - Malware'
allowed_files:
- sha256: e82c5f7d75004727e1f3b94426b9a11c8bc4c312a9170ac9a73abace40aef503
comment: random zip
delegate_to: localhost
register: test_mode_err
ignore_errors: yes
- assert:
that:
- test_mode_err.msg == "mode must be set when allowed_files or allowed_urls is set."
- name: Set whitelisted file with check mode
meraki_malware:
auth_key: '{{auth_key}}'
state: present
org_name: '{{test_org_name}}'
net_name: '{{test_net_name}} - Malware'
mode: enabled
allowed_files:
- sha256: e82c5f7d75004727e1f3b94426b9a11c8bc4c312a9170ac9a73abace40aef503
comment: random zip
delegate_to: localhost
check_mode: yes
register: set_file_check
- debug:
var:
set_file_check
- assert:
that:
- set_file_check is changed
- set_file_check.data is defined
- name: Set whitelisted file
meraki_malware:
auth_key: '{{auth_key}}'
state: present
org_name: '{{test_org_name}}'
net_id: '{{net_id}}'
mode: enabled
allowed_files:
- sha256: e82c5f7d75004727e1f3b94426b9a11c8bc4c312a9170ac9a73abace40aef503
comment: random zip
delegate_to: localhost
register: set_file
- debug:
var: set_file
- assert:
that:
- set_file is changed
- set_file.data.mode is defined
- name: Set whitelisted file with idempotency
meraki_malware:
auth_key: '{{auth_key}}'
state: present
org_name: '{{test_org_name}}'
net_name: '{{test_net_name}} - Malware'
mode: enabled
allowed_files:
- sha256: e82c5f7d75004727e1f3b94426b9a11c8bc4c312a9170ac9a73abace40aef503
comment: random zip
delegate_to: localhost
register: set_file_idempotent
- debug:
var: set_file_idempotent
- assert:
that:
- set_file_idempotent is not changed
- set_file_idempotent.data is defined
- name: Set whitelisted url with check mode
meraki_malware:
auth_key: '{{auth_key}}'
state: present
org_name: '{{test_org_name}}'
net_name: '{{test_net_name}} - Malware'
mode: enabled
allowed_urls:
- url: www.google.com
comment: Google
delegate_to: localhost
check_mode: yes
register: set_url_check
- debug:
var:
set_url_check
- assert:
that:
- set_url_check is changed
- set_url_check.data is defined
- name: Set whitelisted url
meraki_malware:
auth_key: '{{auth_key}}'
state: present
org_name: '{{test_org_name}}'
net_name: '{{test_net_name}} - Malware'
mode: enabled
allowed_urls:
- url: www.google.com
comment: Google
delegate_to: localhost
register: set_url
- debug:
var: set_url
- assert:
that:
- set_url is changed
- set_url.data.mode is defined
- name: Set whitelisted url with idempotency
meraki_malware:
auth_key: '{{auth_key}}'
state: present
org_name: '{{test_org_name}}'
net_name: '{{test_net_name}} - Malware'
mode: enabled
allowed_urls:
- url: www.google.com
comment: Google
delegate_to: localhost
register: set_url_idempotent
- debug:
var: set_url_idempotent
- assert:
that:
- set_url_idempotent is not changed
- set_url_idempotent.data is defined
- name: Get malware settings
meraki_malware:
auth_key: '{{auth_key}}'
state: query
org_name: '{{test_org_name}}'
net_name: '{{test_net_name}} - Malware'
delegate_to: localhost
register: get_malware
- assert:
that:
- get_malware.data is defined
#############################################################################
# Tear down starts here
#############################################################################
always:
- name: Delete test network
meraki_network:
auth_key: '{{auth_key}}'
state: absent
org_name: '{{test_org_name}}'
net_name: '{{test_net_name}} - Malware'
delegate_to: localhost

@ -1,100 +0,0 @@
# Test code for the Meraki modules
# Copyright: (c) 2018, Kevin Breit (@kbreit)
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
---
- block:
- name: Create wireless network
meraki_network:
auth_key: '{{ auth_key }}'
state: present
org_name: '{{test_org_name}}'
net_name: TestNetWireless
type: wireless
delegate_to: localhost
register: new_net
- set_fact:
net: '{{new_net.data.id}}'
- name: Create single firewall rule
meraki_mr_l3_firewall:
auth_key: '{{ auth_key }}'
state: present
org_name: '{{test_org_name}}'
net_id: '{{net}}'
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
register: create_one
- debug:
msg: '{{create_one}}'
- assert:
that:
- create_one.data.0.comment == 'Integration test rule'
- create_one.data.1.policy == 'deny'
- create_one.data is defined
- name: Enable local LAN access
meraki_mr_l3_firewall:
auth_key: '{{ auth_key }}'
state: present
org_name: '{{test_org_name}}'
net_id: '{{net}}'
number: 1
rules:
allow_lan_access: yes
delegate_to: localhost
register: enable_lan
- assert:
that:
- enable_lan.data.1.policy == 'allow'
- name: Query firewall rules
meraki_mr_l3_firewall:
auth_key: '{{ auth_key }}'
state: query
org_name: '{{test_org_name}}'
net_id: '{{net}}'
number: 1
delegate_to: localhost
register: query
- debug:
msg: '{{query}}'
- assert:
that:
- query.data.1.comment == 'Wireless clients accessing LAN'
- query.data.2.comment == 'Default rule'
- query.changed == False
############################################################################
# Tear down starts here
############################################################################
always:
- name: Delete wireless SSID
meraki_ssid:
auth_key: '{{ auth_key }}'
state: absent
org_name: '{{test_org_name}}'
net_id: '{{net}}'
number: 1
delegate_to: localhost
- name: Delete wireless network
meraki_network:
auth_key: '{{ auth_key }}'
state: absent
org_name: '{{test_org_name}}'
net_id: '{{net}}'
delegate_to: localhost

@ -1,203 +0,0 @@
# Test code for the Meraki Organization module
# Copyright: (c) 2018, Kevin Breit (@kbreit)
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
---
- block:
- name: Test an API key is provided
fail:
msg: Please define an API key
when: auth_key is not defined
- name: Create network
meraki_network:
auth_key: '{{ auth_key }}'
org_name: '{{test_org_name}}'
net_name: TestNetAppliance
state: present
type: appliance
delegate_to: localhost
- name: Query firewall rules
meraki_mx_l3_firewall:
auth_key: '{{ auth_key }}'
org_name: '{{test_org_name}}'
net_name: TestNetAppliance
state: query
delegate_to: localhost
register: query
- assert:
that:
- query.data|length == 1
- name: Set one firewall rule
meraki_mx_l3_firewall:
auth_key: '{{ auth_key }}'
org_name: '{{test_org_name}}'
net_name: TestNetAppliance
state: present
rules:
- comment: Deny to documentation address
src_port: any
src_cidr: any
dest_port: 80,443
dest_cidr: 192.0.1.1/32
protocol: tcp
policy: deny
delegate_to: localhost
register: create_one
- debug:
var: create_one
- assert:
that:
- create_one.data|length == 2
- create_one.data.0.dest_cidr == '192.0.1.1/32'
- create_one.data.0.protocol == 'tcp'
- create_one.data.0.policy == 'deny'
- create_one.changed == True
- create_one.data is defined
- name: Check for idempotency
meraki_mx_l3_firewall:
auth_key: '{{ auth_key }}'
org_name: '{{test_org_name}}'
net_name: TestNetAppliance
state: present
rules:
- comment: Deny to documentation address
src_port: any
src_cidr: any
dest_port: 80,443
dest_cidr: 192.0.1.1/32
protocol: tcp
policy: deny
delegate_to: localhost
register: create_one_idempotent
- debug:
msg: '{{create_one_idempotent}}'
- assert:
that:
- create_one_idempotent.changed == False
- create_one_idempotent.data is defined
- name: Create syslog in network
meraki_syslog:
auth_key: '{{ auth_key }}'
org_name: '{{test_org_name}}'
net_name: TestNetAppliance
state: present
servers:
- host: 192.0.2.10
port: 514
roles:
- Appliance event log
- Flows
delegate_to: localhost
- name: Enable syslog for default rule
meraki_mx_l3_firewall:
auth_key: '{{ auth_key }}'
org_name: '{{test_org_name}}'
net_name: TestNetAppliance
state: present
rules:
- comment: Deny to documentation address
src_port: any
src_cidr: any
dest_port: 80,443
dest_cidr: 192.0.1.1/32
protocol: tcp
policy: deny
syslog_default_rule: yes
delegate_to: localhost
register: default_syslog
- debug:
msg: '{{default_syslog}}'
- assert:
that:
- default_syslog.data is defined
- name: Query firewall rules
meraki_mx_l3_firewall:
auth_key: '{{ auth_key }}'
org_name: '{{test_org_name}}'
net_name: TestNetAppliance
state: query
delegate_to: localhost
register: query
- debug:
msg: '{{query.data.1}}'
- assert:
that:
- query.data.1.syslog_enabled == True
- default_syslog.changed == True
- name: Disable syslog for default rule
meraki_mx_l3_firewall:
auth_key: '{{ auth_key }}'
org_name: '{{test_org_name}}'
net_name: TestNetAppliance
state: present
rules:
- comment: Deny to documentation address
src_port: any
src_cidr: any
dest_port: 80,443
dest_cidr: 192.0.1.1/32
protocol: tcp
policy: deny
syslog_default_rule: no
delegate_to: localhost
register: disable_syslog
- debug:
msg: '{{disable_syslog}}'
- assert:
that:
- disable_syslog.data is defined
- name: Query firewall rules
meraki_mx_l3_firewall:
auth_key: '{{ auth_key }}'
org_name: '{{test_org_name}}'
net_name: TestNetAppliance
state: query
delegate_to: localhost
register: query
- debug:
msg: '{{query.data.1}}'
- assert:
that:
- query.data.1.syslog_enabled == False
- disable_syslog.changed == True
always:
- name: Delete all firewall rules
meraki_mx_l3_firewall:
auth_key: '{{ auth_key }}'
org_name: '{{test_org_name}}'
net_name: TestNetAppliance
state: present
rules: []
delegate_to: localhost
register: delete_all
- name: Delete network
meraki_network:
auth_key: '{{ auth_key }}'
org_name: '{{test_org_name}}'
net_name: TestNetAppliance
state: absent
delegate_to: localhost

@ -1,7 +0,0 @@
# Test code for the Meraki Organization module
# Copyright: (c) 2018, Kevin Breit (@kbreit)
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
---
- name: Run test cases
include: tests.yml ansible_connection=local

@ -1,494 +0,0 @@
# Test code for the Meraki Organization module
# Copyright: (c) 2018, Kevin Breit (@kbreit)
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
---
- block:
- name: Test an API key is provided
fail:
msg: Please define an API key
when: auth_key is not defined
- name: Create network
meraki_network:
auth_key: '{{ auth_key }}'
org_name: '{{test_org_name}}'
net_name: TestNetAppliance
state: present
type: appliance
- name: Query firewall rules
meraki_mx_l7_firewall:
auth_key: '{{ auth_key }}'
org_name: '{{test_org_name}}'
net_name: TestNetAppliance
state: query
register: query
- debug:
var: query
- assert:
that:
- query.data is defined
- name: Query firewall application categories
meraki_mx_l7_firewall:
auth_key: '{{ auth_key }}'
org_name: '{{test_org_name}}'
net_name: TestNetAppliance
state: query
categories: yes
register: query_categories
- assert:
that:
- query_categories.data is defined
- name: Create firewall rule for IP range in check mode
meraki_mx_l7_firewall:
auth_key: '{{ auth_key }}'
org_name: '{{test_org_name}}'
net_name: TestNetAppliance
state: present
rules:
- type: ip_range
ip_range: 10.11.12.0/24
register: create_ip_range_check
check_mode: yes
- debug:
var: create_ip_range_check
- assert:
that:
- create_ip_range_check is changed
- name: Create firewall rule for IP range
meraki_mx_l7_firewall:
auth_key: '{{ auth_key }}'
org_name: '{{test_org_name}}'
net_name: TestNetAppliance
state: present
rules:
- type: ip_range
ip_range: 10.11.12.0/24
register: create_ip_range
- debug:
var: create_ip_range
- assert:
that:
- create_ip_range is changed
- create_ip_range.data.rules | length == 1
- name: Create firewall rule for IP range with idempotency with check mode
meraki_mx_l7_firewall:
auth_key: '{{ auth_key }}'
org_name: '{{test_org_name}}'
net_name: TestNetAppliance
state: present
rules:
- type: ip_range
ip_range: 10.11.12.0/24
register: create_ip_range_idempotent_check
check_mode: yes
- assert:
that:
- create_ip_range_idempotent_check is not changed
- name: Create firewall rule for IP range with idempotency
meraki_mx_l7_firewall:
auth_key: '{{ auth_key }}'
org_name: '{{test_org_name}}'
net_name: TestNetAppliance
state: present
rules:
- type: ip_range
ip_range: 10.11.12.0/24
register: create_ip_range_idempotent
- assert:
that:
- create_ip_range_idempotent is not changed
- name: Create firewall rule for IP and port
meraki_mx_l7_firewall:
auth_key: '{{ auth_key }}'
org_name: '{{test_org_name}}'
net_name: TestNetAppliance
state: present
rules:
- type: ip_range
ip_range: 10.11.12.1:23
register: create_ip_range_port
- debug:
var: create_ip_range_port
- assert:
that:
- create_ip_range_port is changed
- name: Create firewall rule for IP range
meraki_mx_l7_firewall:
auth_key: '{{ auth_key }}'
org_name: '{{test_org_name}}'
net_name: TestNetAppliance
state: present
rules:
- type: ip_range
ip_range: 10.11.12.0/24
register: create_ip_range
- debug:
var: create_ip_range
- assert:
that:
- create_ip_range is changed
- create_ip_range.data.rules | length == 1
- name: Create firewall rule for IP range with idempotency with check mode
meraki_mx_l7_firewall:
auth_key: '{{ auth_key }}'
org_name: '{{test_org_name}}'
net_name: TestNetAppliance
state: present
rules:
- type: ip_range
ip_range: 10.11.12.0/24
register: create_ip_range_idempotent_check
check_mode: yes
- assert:
that:
- create_ip_range_idempotent_check is not changed
- name: Create firewall rule for IP range with idempotency
meraki_mx_l7_firewall:
auth_key: '{{ auth_key }}'
org_name: '{{test_org_name}}'
net_name: TestNetAppliance
state: present
rules:
- type: ip_range
ip_range: 10.11.12.0/24
register: create_ip_range_idempotent
- assert:
that:
- create_ip_range_idempotent is not changed
- name: Create firewall rule for application
meraki_mx_l7_firewall:
auth_key: '{{ auth_key }}'
org_name: '{{test_org_name}}'
net_name: TestNetAppliance
state: present
rules:
- type: application
application:
name: facebook
register: application_rule
- assert:
that:
- application_rule is changed
- application_rule.data.rules is defined
- name: Create firewall rule for application via ID
meraki_mx_l7_firewall:
auth_key: '{{ auth_key }}'
org_name: '{{test_org_name}}'
net_name: TestNetAppliance
state: present
rules:
- type: application
application:
id: meraki:layer7/application/205
register: application_rule_id
- assert:
that:
- application_rule_id is changed
- name: Create firewall rule for invalid application
meraki_mx_l7_firewall:
auth_key: '{{ auth_key }}'
org_name: '{{test_org_name}}'
net_name: TestNetAppliance
state: present
rules:
- type: application
application:
name: ansible
register: application_rule_invalid
ignore_errors: yes
- name: Create firewall rule for application category
meraki_mx_l7_firewall:
auth_key: '{{ auth_key }}'
org_name: '{{test_org_name}}'
net_name: TestNetAppliance
state: present
rules:
- type: application_category
application:
name: Advertising
register: application_category_rule
- debug:
var: application_category_rule
- assert:
that:
- application_category_rule is changed
- name: Create firewall rule for application category with ID and conflict
meraki_mx_l7_firewall:
auth_key: '{{ auth_key }}'
org_name: '{{test_org_name}}'
net_name: TestNetAppliance
state: present
rules:
- type: application_category
application:
id: meraki:layer7/category/27
register: application_category_rule_id_conflict
- assert:
that:
- application_category_rule_id_conflict is not changed
- name: Create firewall rule for application category with ID
meraki_mx_l7_firewall:
auth_key: '{{ auth_key }}'
org_name: '{{test_org_name}}'
net_name: TestNetAppliance
state: present
rules:
- type: application_category
application:
id: meraki:layer7/category/24
register: application_category_rule_id
- assert:
that:
- application_category_rule_id is changed
- name: Create firewall rule for host
meraki_mx_l7_firewall:
auth_key: '{{ auth_key }}'
org_name: '{{test_org_name}}'
net_name: TestNetAppliance
state: present
rules:
- type: host
host: asdf.com
register: host_rule
- assert:
that:
- host_rule is changed
- name: Create firewall rule for port
meraki_mx_l7_firewall:
auth_key: '{{ auth_key }}'
org_name: '{{test_org_name}}'
net_name: TestNetAppliance
state: present
rules:
- type: port
port: 1234
register: port_rule
- assert:
that:
- port_rule is changed
- name: Create firewall rule for blacklisted countries
meraki_mx_l7_firewall:
auth_key: '{{ auth_key }}'
org_name: '{{test_org_name}}'
net_name: TestNetAppliance
state: present
rules:
- type: blacklisted_countries
countries:
- CA
- AX
register: blacklist_countries
- assert:
that:
- blacklist_countries is changed
- name: Create firewall rule for whitelisted countries
meraki_mx_l7_firewall:
auth_key: '{{ auth_key }}'
org_name: '{{test_org_name}}'
net_name: TestNetAppliance
state: present
rules:
- type: whitelisted_countries
countries:
- US
- FR
register: whitelist_countries
- assert:
that:
- whitelist_countries is changed
- name: Create firewall rule for whitelisted countries with idempotency
meraki_mx_l7_firewall:
auth_key: '{{ auth_key }}'
org_name: '{{test_org_name}}'
net_name: TestNetAppliance
state: present
rules:
- type: whitelisted_countries
countries:
- US
- FR
register: whitelist_countries_idempotent
- assert:
that:
- whitelist_countries_idempotent is not changed
- name: Create multiple firewall rules
meraki_mx_l7_firewall:
auth_key: '{{ auth_key }}'
org_name: '{{test_org_name}}'
net_name: TestNetAppliance
state: present
rules:
- type: application_category
application:
id: meraki:layer7/category/27
- type: blacklisted_countries
countries:
- CN
- policy: deny
type: port
port: 8080
register: multiple_rules
- debug:
var: multiple_rules
- assert:
that:
- multiple_rules.data.rules | length == 3
- multiple_rules is changed
#########################################
## Tests for argument completeness ##
#########################################
- name: Test whitelisted_countries incomplete arguments
meraki_mx_l7_firewall:
auth_key: '{{ auth_key }}'
org_name: '{{test_org_name}}'
net_name: TestNetAppliance
state: present
rules:
- type: whitelisted_countries
register: error_whitelist
ignore_errors: yes
- assert:
that:
- 'error_whitelist.msg == "countries argument is required when type is whitelisted_countries."'
- name: Test blacklisted_countries incomplete arguments
meraki_mx_l7_firewall:
auth_key: '{{ auth_key }}'
org_name: '{{test_org_name}}'
net_name: TestNetAppliance
state: present
rules:
- type: blacklisted_countries
register: error_blacklist
ignore_errors: yes
- assert:
that:
- 'error_blacklist.msg == "countries argument is required when type is blacklisted_countries."'
- name: Test application_category incomplete arguments
meraki_mx_l7_firewall:
auth_key: '{{ auth_key }}'
org_name: '{{test_org_name}}'
net_name: TestNetAppliance
state: present
rules:
- type: application_category
register: error_app_cat
ignore_errors: yes
- assert:
that:
- 'error_app_cat.msg == "application argument is required when type is application_category."'
- name: Test application incomplete arguments
meraki_mx_l7_firewall:
auth_key: '{{ auth_key }}'
org_name: '{{test_org_name}}'
net_name: TestNetAppliance
state: present
rules:
- type: application
register: error_app_cat
ignore_errors: yes
- assert:
that:
- 'error_app_cat.msg == "application argument is required when type is application."'
- name: Test host incomplete arguments
meraki_mx_l7_firewall:
auth_key: '{{ auth_key }}'
org_name: '{{test_org_name}}'
net_name: TestNetAppliance
state: present
rules:
- type: host
register: error_app_cat
ignore_errors: yes
- assert:
that:
- 'error_app_cat.msg == "host argument is required when type is host."'
- name: Test port incomplete arguments
meraki_mx_l7_firewall:
auth_key: '{{ auth_key }}'
org_name: '{{test_org_name}}'
net_name: TestNetAppliance
state: present
rules:
- type: port
register: error_app_cat
ignore_errors: yes
- assert:
that:
- 'error_app_cat.msg == "port argument is required when type is port."'
#################
## Cleanup ##
#################
# always:
# - name: Delete network
# meraki_network:
# auth_key: '{{ auth_key }}'
# org_name: '{{test_org_name}}'
# net_name: TestNetAppliance
# state: absent
# delegate_to: localhost

@ -1,7 +0,0 @@
# Test code for the Meraki Organization module
# Copyright: (c) 2018, Kevin Breit (@kbreit)
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
---
- name: Run test cases
include: tests.yml ansible_connection=local

@ -1,363 +0,0 @@
# Test code for the Meraki NAT module
# Copyright: (c) 2019, Kevin Breit (@kbreit)
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
---
- block:
- name: Create test network
meraki_network:
auth_key: '{{auth_key}}'
org_name: '{{test_org_name}}'
net_name: '{{test_net_name}}'
state: present
type: appliance
- name: Create 1:1 rule with check mode
meraki_nat:
auth_key: '{{auth_key}}'
org_name: '{{test_org_name}}'
net_name: '{{test_net_name}}'
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
register: create_one_one_check
check_mode: yes
- debug:
var: create_one_one_check
- assert:
that:
- create_one_one_check is changed
- name: Create 1:1 rule
meraki_nat:
auth_key: '{{auth_key}}'
org_name: '{{test_org_name}}'
net_name: '{{test_net_name}}'
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
register: create_one_one
- debug:
var: create_one_one
- assert:
that:
- create_one_one is changed
- name: Create 1:1 rule with idempotency
meraki_nat:
auth_key: '{{auth_key}}'
org_name: '{{test_org_name}}'
net_name: '{{test_net_name}}'
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
register: create_one_one_idempotent
- debug:
var: create_one_one_idempotent
- assert:
that:
- create_one_one_idempotent is not changed
- name: Create 1:many rule with check mode
meraki_nat:
auth_key: '{{auth_key}}'
org_name: '{{test_org_name}}'
net_name: '{{test_net_name}}'
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
register: create_one_many_check
check_mode: yes
- debug:
var: create_one_many_check
- assert:
that:
- create_one_many_check is changed
- name: Create 1:many rule
meraki_nat:
auth_key: '{{auth_key}}'
org_name: '{{test_org_name}}'
net_name: '{{test_net_name}}'
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
register: create_one_many
- debug:
var: create_one_many
- assert:
that:
- create_one_many is changed
- name: Create 1:many rule with idempotency
meraki_nat:
auth_key: '{{auth_key}}'
org_name: '{{test_org_name}}'
net_name: '{{test_net_name}}'
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
register: create_one_many_idempotent
- debug:
var: create_one_many_idempotent
- assert:
that:
- create_one_many_idempotent is not changed
- name: Create port forwarding rule with check mode
meraki_nat:
auth_key: '{{auth_key}}'
org_name: '{{test_org_name}}'
net_name: '{{test_net_name}}'
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
register: create_pf_check
check_mode: yes
- debug:
var: create_pf_check
- assert:
that:
- create_pf_check is changed
- name: Create port forwarding rule
meraki_nat:
auth_key: '{{auth_key}}'
org_name: '{{test_org_name}}'
net_name: '{{test_net_name}}'
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
register: create_pf
- debug:
var: create_pf
- assert:
that:
- create_pf is changed
- name: Create port forwarding rule with idempotency
meraki_nat:
auth_key: '{{auth_key}}'
org_name: '{{test_org_name}}'
net_name: '{{test_net_name}}'
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
register: create_pf_idempotent
- debug:
var: create_pf_idempotent
- assert:
that:
- create_pf_idempotent is not changed
- create_pf_idempotent.data.port_forwarding is defined
- name: Create multiple rules
meraki_nat:
auth_key: '{{auth_key}}'
org_name: '{{test_org_name}}'
net_name: '{{test_net_name}}'
state: present
port_forwarding:
- name: Test map
lan_ip: 192.168.128.1
uplink: both
protocol: tcp
allowed_ips:
- 1.1.1.2
public_port: 10
local_port: 11
one_to_many:
- public_ip: 1.1.1.3
uplink: internet1
port_rules:
- name: Test rule
protocol: tcp
public_port: 10
local_ip: 192.168.128.1
local_port: 11
allowed_ips:
- any
register: create_multiple
- debug:
var: create_multiple
- assert:
that:
- create_multiple is changed
- create_multiple.data.one_to_many is defined
- create_multiple.data.port_forwarding is defined
- assert:
that:
- create_multiple is changed
- create_multiple.data.one_to_many is defined
- create_multiple.data.port_forwarding is defined
- create_multiple.diff.before.one_to_many is defined
- create_multiple.diff.before.port_forwarding is defined
- create_multiple.diff.after.one_to_many is defined
- create_multiple.diff.after.port_forwarding is defined
- name: Query all NAT rules
meraki_nat:
auth_key: '{{auth_key}}'
org_name: '{{test_org_name}}'
net_name: '{{test_net_name}}'
state: query
subset: all
register: query_all
- debug:
var: query_all
- name: Query 1:1 NAT rules
meraki_nat:
auth_key: '{{auth_key}}'
org_name: '{{test_org_name}}'
net_name: '{{test_net_name}}'
state: query
subset: '1:1'
register: query_1to1
- debug:
var: query_1to1
- name: Query 1:many NAT rules
meraki_nat:
auth_key: '{{auth_key}}'
org_name: '{{test_org_name}}'
net_name: '{{test_net_name}}'
state: query
subset: '1:many'
register: query_1tomany
- debug:
var: query_1tomany
- name: Query port forwarding rules
meraki_nat:
auth_key: '{{auth_key}}'
org_name: '{{test_org_name}}'
net_name: '{{test_net_name}}'
state: query
subset: port_forwarding
register: query_pf
- debug:
var: query_pf
- name: Query multiple rules
meraki_nat:
auth_key: '{{auth_key}}'
org_name: '{{test_org_name}}'
net_name: '{{test_net_name}}'
state: query
subset:
- '1:1'
- '1:many'
register: query_multiple
- debug:
var: query_multiple
always:
- name: Delete test network
meraki_network:
auth_key: '{{auth_key}}'
org_name: '{{test_org_name}}'
net_name: '{{test_net_name}}'
state: absent

@ -1,402 +0,0 @@
# Test code for the Meraki modules
# Copyright: (c) 2018, Kevin Breit (@kbreit)
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
---
- block:
- name: Create network without type
meraki_network:
auth_key: '{{ auth_key }}'
state: present
org_name: '{{test_org_name}}'
net_name: IntTestNetwork
timezone: America/Chicago
delegate_to: localhost
register: create_net_no_type
ignore_errors: yes
- assert:
that:
- create_net_no_type.msg == 'type parameter is required when creating a network.'
- name: Create network without organization
meraki_network:
auth_key: '{{ auth_key }}'
state: present
net_name: IntTestNetwork
timezone: America/Chicago
delegate_to: localhost
register: create_net_no_org
ignore_errors: yes
- name: Create network with type switch
meraki_network:
auth_key: '{{ auth_key }}'
state: present
org_name: '{{test_org_name}}'
net_name: IntTestNetworkSwitch
type: switch
timezone: America/Chicago
delegate_to: localhost
register: create_net_switch
- name: Create network with type switch by org ID
meraki_network:
auth_key: '{{ auth_key }}'
state: present
org_id: '{{test_org_id}}'
net_name: IntTestNetworkSwitchOrgID
type: switch
timezone: America/Chicago
delegate_to: localhost
register: create_net_switch_org_id
- name: Create network with type appliance and no timezone
meraki_network:
auth_key: '{{ auth_key }}'
state: present
org_name: '{{test_org_name}}'
net_name: IntTestNetworkAppliance
type: appliance
delegate_to: localhost
register: create_net_appliance_no_tz
- name: Enable VLAN support on appliance network
meraki_network:
auth_key: '{{ auth_key }}'
state: present
org_name: '{{ test_org_name }}'
net_name: IntTestNetworkAppliance
enable_vlans: yes
delegate_to: localhost
register: enable_vlan
- assert:
that:
- enable_vlan.data.enabled == True
- name: Enable VLAN support on appliance network with idempotency
meraki_network:
auth_key: '{{ auth_key }}'
state: present
org_name: '{{ test_org_name }}'
net_name: IntTestNetworkAppliance
enable_vlans: yes
delegate_to: localhost
register: enable_vlan_idempotent
- debug:
var: enable_vlan_idempotent
- assert:
that:
- enable_vlan_idempotent is not changed
- enable_vlan_idempotent.data is defined
- name: Disable VLAN support on appliance network
meraki_network:
auth_key: '{{ auth_key }}'
state: present
org_name: '{{ test_org_name }}'
net_name: IntTestNetworkAppliance
enable_vlans: no
delegate_to: localhost
register: disable_vlan
- assert:
that:
- disable_vlan.data.enabled == False
- name: Disable VLAN support on appliance network with idempotency
meraki_network:
auth_key: '{{ auth_key }}'
state: present
org_name: '{{ test_org_name }}'
net_name: IntTestNetworkAppliance
enable_vlans: no
delegate_to: localhost
register: disable_vlan_idempotent
- assert:
that:
- disable_vlan_idempotent is not changed
- disable_vlan_idempotent.data is defined
- name: Create network with type wireless and disable my.meraki.com
meraki_network:
auth_key: '{{ auth_key }}'
state: present
org_name: '{{test_org_name}}'
net_name: IntTestNetworkWireless
type: wireless
timezone: America/Chicago
disable_my_meraki: yes
delegate_to: localhost
register: create_net_wireless
- name: Create network with type wireless, disable my.meraki.com, and check for idempotency
meraki_network:
auth_key: '{{ auth_key }}'
state: present
org_name: '{{test_org_name}}'
net_name: IntTestNetworkWireless
type: wireless
timezone: America/Chicago
disable_my_meraki: yes
delegate_to: localhost
register: create_net_wireless_idempotent
- assert:
that:
- create_net_wireless_idempotent.data is defined
- name: Create network with type combined and disable my.meraki.com
meraki_network:
auth_key: '{{ auth_key }}'
state: present
org_name: '{{ test_org_name }}'
net_name: IntTestNetworkCombined
type:
- appliance
- switch
timezone: America/Chicago
enable_my_meraki: no
delegate_to: localhost
register: create_net_combined
- name: Reenable my.meraki.com
meraki_network:
auth_key: '{{ auth_key }}'
state: present
org_name: '{{test_org_name}}'
net_name: IntTestNetworkCombined
enable_my_meraki: yes
delegate_to: localhost
register: enable_meraki_com
- name: Disable my.meraki.com for next test
meraki_network:
auth_key: '{{ auth_key }}'
state: present
org_name: '{{test_org_name}}'
net_name: IntTestNetworkCombined
enable_my_meraki: no
delegate_to: localhost
- name: Enable remote status page
meraki_network:
auth_key: '{{ auth_key }}'
state: present
org_name: '{{test_org_name}}'
net_name: IntTestNetworkCombined
enable_remote_status_page: yes
delegate_to: localhost
register: disable_remote_status
- debug:
msg: '{{disable_remote_status}}'
- assert:
that:
- disable_remote_status.data.disable_remote_status_page == False
- name: Disable remote status page
meraki_network:
auth_key: '{{ auth_key }}'
state: present
org_name: '{{test_org_name}}'
net_name: IntTestNetworkCombined
enable_remote_status_page: no
delegate_to: localhost
register: enable_remote_status
- debug:
msg: '{{enable_remote_status}}'
- assert:
that:
- enable_remote_status.data.disable_remote_status_page == True
- name: Test status pages are mutually exclusive when on
meraki_network:
auth_key: '{{ auth_key }}'
state: present
org_name: '{{test_org_name}}'
net_name: IntTestNetworkCombined
enable_my_meraki: yes
enable_remote_status_page: no
delegate_to: localhost
register: status_exclusivity
ignore_errors: yes
- assert:
that:
- '"must be true when setting" in status_exclusivity.msg'
- name: Create network with one tag
meraki_network:
auth_key: '{{ auth_key }}'
state: present
org_name: '{{test_org_name}}'
net_name: IntTestNetworkTag
type: switch
timezone: America/Chicago
tags: first_tag
delegate_to: localhost
register: create_net_tag
- name: Create network with two tags
meraki_network:
auth_key: '{{ auth_key }}'
state: present
org_name: '{{test_org_name}}'
net_name: IntTestNetworkTags
type: switch
timezone: America/Chicago
tags:
- first_tag
- second_tag
delegate_to: localhost
register: create_net_tags
- set_fact:
tag_net_id: '{{create_net_tags.data.id}}'
- name: Modify network by net_id
meraki_network:
auth_key: '{{ auth_key }}'
state: present
org_name: '{{test_org_name}}'
net_id: '{{tag_net_id}}'
type: switch
timezone: America/Chicago
tags:
- first_tag
- second_tag
- third_tag
delegate_to: localhost
register: create_net_modified
- name: Modify network with idempotency
meraki_network:
auth_key: '{{ auth_key }}'
state: present
org_name: '{{test_org_name}}'
net_name: IntTestNetworkTags
type: switch
timezone: America/Chicago
tags:
- first_tag
- second_tag
- third_tag
delegate_to: localhost
register: create_net_modified_idempotent
- assert:
that:
- create_net_modified_idempotent.data is defined
- name: Present assertions
assert:
that:
- create_net_combined.data.type == 'combined'
- create_net_combined.data.disable_my_meraki_com == True
- enable_meraki_com.data.disable_my_meraki_com == False
- '"org_name or org_id parameters are required" in create_net_no_org.msg'
- '"IntTestNetworkAppliance" in create_net_appliance_no_tz.data.name'
- create_net_appliance_no_tz.changed == True
- '"IntTestNetworkSwitch" in create_net_switch.data.name'
- '"IntTestNetworkSwitchOrgID" in create_net_switch_org_id.data.name'
- '"IntTestNetworkWireless" in create_net_wireless.data.name'
- create_net_wireless.data.disable_my_meraki_com == True
- create_net_wireless_idempotent.changed == False
- create_net_wireless_idempotent.data is defined
- '"first_tag" in create_net_tag.data.tags'
- '"second_tag" in create_net_tags.data.tags'
- '"third_tag" in create_net_modified.data.tags'
- create_net_modified.changed == True
- create_net_modified_idempotent.changed == False
- create_net_modified_idempotent.data is defined
- name: Query all networks
meraki_network:
auth_key: '{{ auth_key }}'
state: query
org_name: '{{test_org_name}}'
delegate_to: localhost
register: net_query_all
- name: Query a configuration template
meraki_network:
auth_key: '{{auth_key}}'
state: query
org_name: '{{test_org_name}}'
net_name: '{{test_template_name}}'
delegate_to: localhost
register: query_config_template
- name: Query one network
meraki_network:
auth_key: '{{ auth_key }}'
state: query
org_name: '{{test_org_name}}'
net_name: IntTestNetworkSwitch
delegate_to: localhost
register: net_query_one
- name: Query assertions
assert:
that:
- 'net_query_one.data.name == "IntTestNetworkSwitch"'
- 'query_config_template.data.name == "{{ test_template_name }}"'
#############################################################################
# Tear down starts here
#############################################################################
always:
- name: Delete network without org
meraki_network:
auth_key: '{{ auth_key }}'
state: absent
net_name: IntTestNetworkSwitch
delegate_to: localhost
register: delete_all_no_org
ignore_errors: yes
- name: Delete network by org ID
meraki_network:
auth_key: '{{ auth_key }}'
state: absent
org_id: '{{test_org_id}}'
net_name: IntTestNetworkSwitchOrgID
delegate_to: localhost
register: delete_net_org_id
- name: Query after delete with org ID
meraki_network:
auth_key: '{{ auth_key }}'
state: query
org_name: '{{test_org_name}}'
delegate_to: localhost
register: query_deleted_org_id
- name: Delete all networks
meraki_network:
auth_key: '{{ auth_key }}'
state: absent
org_name: '{{test_org_name}}'
net_name: '{{ item }}'
delegate_to: localhost
register: delete_all
ignore_errors: yes
loop:
- IntTestNetworkSwitch
- IntTestNetworkWireless
- IntTestNetworkAppliance
- IntTestNetworkCombined
- IntTestNetworkTag
- IntTestNetworkTags
- assert:
that:
- 'delete_all_no_org.msg == "org_name or org_id parameters are required"'

@ -1,8 +0,0 @@
# Test code for the Meraki Organization module
# Copyright: (c) 2018, Kevin Breit (@kbreit)
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
---
- name: Run test cases
include: tests.yml ansible_connection=local

@ -1,127 +0,0 @@
# Test code for the Meraki Organization module
# Copyright: (c) 2018, Kevin Breit (@kbreit)
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
---
- block:
- name: Test an API key is provided
fail:
msg: Please define an API key
when: auth_key is not defined
- name: Create a new organization named IntTestOrg
meraki_organization:
auth_key: '{{ auth_key }}'
org_name: IntTestOrg
state: present
output_level: debug
register: new_org
- debug:
msg: '{{new_org}}'
- name: Clone IntTestOrg
meraki_organization:
auth_key: '{{ auth_key }}'
clone: IntTestOrg
org_name: IntTestOrgCloned
state: present
register: cloned_org
- debug:
msg: '{{cloned_org}}'
- name: Rename IntTestOrg
meraki_organization:
auth_key: '{{ auth_key }}'
org_name: IntTestOrgRenamed
org_id: '{{ new_org.data.id }}'
state: present
register: modify_org
- debug:
msg: '{{ modify_org }}'
- set_fact:
renamed_org_id: '{{modify_org.data.id}}'
- name: Rename IntTestOrg idempotent
meraki_organization:
auth_key: '{{ auth_key }}'
org_name: IntTestOrgRenamed
org_id: '{{ new_org.data.id }}'
state: present
register: modify_org_idempotent
- name: Present assertions
assert:
that:
- '"https" in new_org.url'
- new_org.changed == True
- new_org.data.id is defined
- cloned_org.changed == True
- cloned_org.data.id is defined
- modify_org.changed == True
- 'modify_org.data.name == "IntTestOrgRenamed"'
- modify_org_idempotent.changed == False
- modify_org_idempotent.data is defined
- name: List all organizations
meraki_organization:
auth_key: '{{ auth_key }}'
state: query
register: query_all
- name: Query information about a single organization named IntTestOrg
meraki_organization:
auth_key: '{{ auth_key }}'
org_name: IntTestOrgRenamed
state: query
register: query_org
- name: Query information about IntTestOrg by organization ID
meraki_organization:
auth_key: '{{ auth_key }}'
org_id: '{{ query_org.data.id }}'
state: query
register: query_org_id
- name: Query assertions
assert:
that:
- query_org.data.id is defined
- query_all.changed == False
- query_all.data | length >= 1
- 'query_org.data.name == "IntTestOrgRenamed"'
- 'query_org_id.data.id == query_org.data.id'
always:
- name: Delete cloned organizations with check mode
meraki_organization:
auth_key: '{{ auth_key }}'
state: absent
org_name: IntTestOrgCloned
register: deleted_org_check
check_mode: yes
- assert:
that:
- deleted_org_check is changed
- name: Delete cloned organizations
meraki_organization:
auth_key: '{{ auth_key }}'
state: absent
org_name: IntTestOrgCloned
register: deleted_org
- name: Delete renamed organization by id
meraki_organization:
auth_key: '{{ auth_key }}'
state: absent
org_id: '{{renamed_org_id}}'
register: deleted_org_id
- assert:
that:
- deleted_org_id is changed

@ -1,306 +0,0 @@
# Test code for the Meraki Organization module
# Copyright: (c) 2018, Kevin Breit (@kbreit)
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
---
- block:
- name: Test an API key is provided
fail:
msg: Please define an API key
when: auth_key is not defined
- name: Create SNMP network
meraki_network:
auth_key: '{{auth_key}}'
org_name: '{{test_org_name}}'
net_name: '{{test_net_name}}'
state: present
type: appliance
delegate_to: localhost
register: new_net
- set_fact:
net_id: new_net.data.id
- name: Query all SNMP settings
meraki_snmp:
auth_key: '{{auth_key}}'
org_name: '{{test_org_name}}'
state: query
delegate_to: localhost
register: snmp_query
- debug:
msg: '{{snmp_query}}'
- name: Enable SNMPv2c
meraki_snmp:
auth_key: '{{auth_key}}'
org_name: '{{test_org_name}}'
state: present
v2c_enabled: true
delegate_to: localhost
register: snmp_v2_enable
- debug:
msg: '{{snmp_v2_enable}}'
- assert:
that:
- snmp_v2_enable.data.v2_community_string is defined
- snmp_v2_enable.data.v2c_enabled == true
- name: Disable SNMPv2c
meraki_snmp:
auth_key: '{{auth_key}}'
org_name: '{{test_org_name}}'
state: present
v2c_enabled: False
delegate_to: localhost
register: snmp_v2_disable
- assert:
that:
- snmp_v2_disable.data.v2_community_string is not defined
- snmp_v2_disable.data.v2c_enabled == False
- name: Enable SNMPv2c with org_id
meraki_snmp:
auth_key: '{{auth_key}}'
org_id: '{{test_org_id}}'
state: present
v2c_enabled: true
delegate_to: localhost
register: snmp_v2_enable_id
- debug:
msg: '{{snmp_v2_enable_id}}'
- assert:
that:
- snmp_v2_enable_id.data.v2_community_string is defined
- snmp_v2_enable_id.data.v2c_enabled == true
- name: Disable SNMPv2c with org_id
meraki_snmp:
auth_key: '{{auth_key}}'
org_id: '{{test_org_id}}'
state: present
v2c_enabled: False
delegate_to: localhost
register: snmp_v2_disable_id
- assert:
that:
- snmp_v2_disable_id.data.v2_community_string is not defined
- snmp_v2_disable_id.data.v2c_enabled == False
- name: Enable SNMPv3 with check mode
meraki_snmp:
auth_key: '{{auth_key}}'
org_name: '{{test_org_name}}'
state: present
v3_enabled: true
v3_auth_mode: SHA
v3_auth_pass: ansiblepass
v3_priv_mode: AES128
v3_priv_pass: ansiblepass
delegate_to: localhost
check_mode: yes
register: snmp_v3_enable_check
- assert:
that:
- snmp_v3_enable_check.data.v3_enabled == True
- snmp_v3_enable_check.changed == True
- name: Enable SNMPv3
meraki_snmp:
auth_key: '{{auth_key}}'
org_name: '{{test_org_name}}'
state: present
v3_enabled: true
v3_auth_mode: SHA
v3_auth_pass: ansiblepass
v3_priv_mode: AES128
v3_priv_pass: ansiblepass
delegate_to: localhost
register: snmp_v3_enable
- assert:
that:
- snmp_v3_enable.data.v3_enabled == True
- snmp_v3_enable.changed == True
- name: Check for idempotency
meraki_snmp:
auth_key: '{{auth_key}}'
org_name: '{{test_org_name}}'
state: present
v3_enabled: true
v3_auth_mode: SHA
v3_auth_pass: ansiblepass
v3_priv_mode: AES128
v3_priv_pass: ansiblepass
delegate_to: localhost
register: snmp_idempotent
- debug:
msg: '{{snmp_idempotent}}'
- assert:
that:
- snmp_idempotent.changed == False
- snmp_idempotent.data is defined
- name: Add peer IPs
meraki_snmp:
auth_key: '{{auth_key}}'
org_name: '{{test_org_name}}'
state: present
v3_enabled: true
v3_auth_mode: SHA
v3_auth_pass: ansiblepass
v3_priv_mode: AES128
v3_priv_pass: ansiblepass
peer_ips: 1.1.1.1;2.2.2.2
delegate_to: localhost
register: peers
- debug:
msg: '{{peers}}'
- assert:
that:
- peers.data.peer_ips is defined
- name: Add invalid peer IPs
meraki_snmp:
auth_key: '{{auth_key}}'
org_name: '{{test_org_name}}'
state: present
peer_ips: 1.1.1.1 2.2.2.2
delegate_to: localhost
register: invalid_peers
ignore_errors: yes
- assert:
that:
'"Peer IP addresses are semi-colon delimited." in invalid_peers.msg'
- name: Set short password
meraki_snmp:
auth_key: '{{auth_key}}'
org_name: '{{test_org_name}}'
state: present
v3_enabled: true
v3_auth_mode: SHA
v3_auth_pass: ansible
v3_priv_mode: AES128
v3_priv_pass: ansible
peer_ips: 1.1.1.1;2.2.2.2
delegate_to: localhost
register: short_password
ignore_errors: yes
- debug:
msg: '{{short_password}}'
- assert:
that:
- '"at least 8" in short_password.msg'
- name: Set network access type to community string
meraki_snmp:
auth_key: '{{auth_key}}'
org_name: '{{test_org_name}}'
net_name: '{{test_net_name}}'
state: present
access: community
community_string: abc123
delegate_to: localhost
register: set_net_community
- debug:
var: set_net_community
- assert:
that:
- set_net_community is changed
- set_net_community.data is defined
- name: Set network access type to username
meraki_snmp:
auth_key: '{{auth_key}}'
org_name: '{{test_org_name}}'
net_name: '{{test_net_name}}'
state: present
access: users
users:
- username: ansibleuser
passphrase: ansiblepass
delegate_to: localhost
register: set_net_user
- debug:
var: set_net_user
- assert:
that:
- set_net_user is changed
- set_net_user.data is defined
- name: Set network access type to none
meraki_snmp:
auth_key: '{{auth_key}}'
org_name: '{{test_org_name}}'
net_name: '{{test_net_name}}'
state: present
access: none
delegate_to: localhost
register: set_net_none
- debug:
var: set_net_none
- assert:
that:
- set_net_none is changed
- set_net_none.data is defined
- name: Query network SNMP settings
meraki_snmp:
auth_key: '{{auth_key}}'
org_name: '{{test_org_name}}'
net_name: '{{test_net_name}}'
state: query
delegate_to: localhost
register: get_net
- debug:
var: get_net
- assert:
that:
- get_net.data is defined
always:
- name: Disable SNMPv3
meraki_snmp:
auth_key: '{{auth_key}}'
org_name: '{{test_org_name}}'
state: present
v3_enabled: no
v3_auth_mode: SHA
v3_auth_pass: ansiblepass
v3_priv_mode: AES128
v3_priv_pass: ansiblepass
delegate_to: localhost
- name: Delete SNMP network
meraki_network:
auth_key: '{{auth_key}}'
org_name: '{{test_org_name}}'
net_name: '{{test_net_name}}'
state: absent
delegate_to: localhost

@ -1,408 +0,0 @@
# Test code for the Meraki SSID module
# Copyright: (c) 2018, Kevin Breit (@kbreit)
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
---
- block:
- name: Test an API key is provided
fail:
msg: Please define an API key
when: auth_key is not defined
- name: Create test network
meraki_network:
auth_key: '{{auth_key}}'
state: present
org_name: '{{test_org_name}}'
net_name: TestNetSSID
type: wireless
register: test_net
- debug:
msg: '{{test_net}}'
- name: Query all SSIDs
meraki_ssid:
auth_key: '{{auth_key}}'
state: query
org_name: '{{test_org_name}}'
net_name: TestNetSSID
delegate_to: localhost
register: query_all
- name: Enable and name SSID
meraki_ssid:
auth_key: '{{auth_key}}'
state: present
org_name: '{{test_org_name}}'
net_name: TestNetSSID
name: AnsibleSSID
enabled: true
delegate_to: localhost
register: enable_name_ssid
- debug:
msg: '{{ enable_name_ssid }}'
- assert:
that:
- query_all.data | length == 15
- query_all.data.0.name == 'TestNetSSID WiFi'
- enable_name_ssid.data.name == 'AnsibleSSID'
- name: Check for idempotency
meraki_ssid:
auth_key: '{{auth_key}}'
state: present
org_name: '{{test_org_name}}'
net_name: TestNetSSID
name: AnsibleSSID
enabled: true
delegate_to: localhost
register: enable_name_ssid_idempotent
- debug:
msg: '{{ enable_name_ssid_idempotent }}'
- assert:
that:
- enable_name_ssid_idempotent.changed == False
- enable_name_ssid_idempotent.data is defined
- name: Query one SSIDs
meraki_ssid:
auth_key: '{{auth_key}}'
state: query
org_name: '{{test_org_name}}'
net_name: TestNetSSID
name: AnsibleSSID
delegate_to: localhost
register: query_one
- debug:
msg: '{{query_one}}'
- assert:
that:
- query_one.data.name == 'AnsibleSSID'
- name: Query one SSID with number
meraki_ssid:
auth_key: '{{auth_key}}'
state: query
org_name: '{{test_org_name}}'
net_name: TestNetSSID
number: 1
delegate_to: localhost
register: query_one_number
- debug:
msg: '{{query_one_number}}'
- assert:
that:
- query_one_number.data.name == 'AnsibleSSID'
- name: Disable SSID without specifying number
meraki_ssid:
auth_key: '{{auth_key}}'
state: present
org_name: '{{test_org_name}}'
net_name: TestNetSSID
name: AnsibleSSID
enabled: false
delegate_to: localhost
register: disable_ssid
- debug:
msg: '{{ disable_ssid.data.enabled }}'
- assert:
that:
- disable_ssid.data.enabled == False
- name: Enable SSID with number
meraki_ssid:
auth_key: '{{auth_key}}'
state: present
org_name: '{{test_org_name}}'
net_name: TestNetSSID
number: 1
enabled: true
delegate_to: localhost
register: enable_ssid_number
- debug:
msg: '{{ enable_ssid_number.data.enabled }}'
- assert:
that:
- enable_ssid_number.data.enabled == True
- name: Set VLAN arg spec
meraki_ssid:
auth_key: '{{auth_key}}'
state: present
org_name: '{{test_org_name}}'
net_name: TestNetSSID
number: 1
use_vlan_tagging: yes
ip_assignment_mode: Bridge mode
default_vlan_id: 1
ap_tags_vlan_ids:
- tags: wifi
vlan_id: 2
delegate_to: localhost
register: set_vlan_arg
- debug:
var: set_vlan_arg
- assert:
that: set_vlan_arg is changed
- name: Set VLAN arg spec
meraki_ssid:
auth_key: '{{auth_key}}'
state: present
org_name: '{{test_org_name}}'
net_name: TestNetSSID
number: 1
use_vlan_tagging: yes
ip_assignment_mode: Bridge mode
default_vlan_id: 1
ap_tags_vlan_ids:
- tags: wifi
vlan_id: 2
delegate_to: localhost
register: set_vlan_arg_idempotent
- debug:
var: set_vlan_arg_idempotent
- assert:
that: set_vlan_arg_idempotent is not changed
- name: Set PSK
meraki_ssid:
auth_key: '{{auth_key}}'
state: present
org_name: '{{test_org_name}}'
net_name: TestNetSSID
name: AnsibleSSID
auth_mode: psk
psk: abc1234567890
encryption_mode: wpa
delegate_to: localhost
register: psk
- debug:
msg: '{{ psk }}'
- assert:
that:
- psk.data.auth_mode == 'psk'
- psk.data.encryption_mode == 'wpa'
- psk.data.wpa_encryption_mode == 'WPA2 only'
- name: Set PSK with idempotency
meraki_ssid:
auth_key: '{{auth_key}}'
state: present
org_name: '{{test_org_name}}'
net_name: TestNetSSID
name: AnsibleSSID
auth_mode: psk
psk: abc1234567890
encryption_mode: wpa
delegate_to: localhost
register: psk_idempotent
- debug:
msg: '{{ psk_idempotent }}'
- assert:
that:
- psk_idempotent is not changed
- name: Enable click-through splash page
meraki_ssid:
auth_key: '{{auth_key}}'
state: present
org_name: '{{test_org_name}}'
net_name: TestNetSSID
name: AnsibleSSID
splash_page: Click-through splash page
delegate_to: localhost
register: splash_click
- debug:
msg: '{{ splash_click }}'
- assert:
that:
- splash_click.data.splash_page == 'Click-through splash page'
- name: Configure RADIUS servers
meraki_ssid:
auth_key: '{{auth_key}}'
state: present
org_name: '{{test_org_name}}'
net_name: TestNetSSID
name: AnsibleSSID
auth_mode: open-with-radius
radius_servers:
- host: 192.0.1.200
port: 1234
secret: abc98765
delegate_to: localhost
register: set_radius_server
- debug:
msg: '{{ set_radius_server }}'
- assert:
that:
- set_radius_server.data.radius_servers.0.host == '192.0.1.200'
- name: Configure RADIUS servers with idempotency
meraki_ssid:
auth_key: '{{auth_key}}'
state: present
org_name: '{{test_org_name}}'
net_name: TestNetSSID
name: AnsibleSSID
auth_mode: open-with-radius
radius_servers:
- host: 192.0.1.200
port: 1234
secret: abc98765
delegate_to: localhost
register: set_radius_server_idempotent
- debug:
var: set_radius_server_idempotent
- assert:
that:
- set_radius_server_idempotent is not changed
#################
# Error testing #
#################
- name: Set PSK with wrong mode
meraki_ssid:
auth_key: '{{auth_key}}'
state: present
org_name: '{{test_org_name}}'
net_name: TestNetSSID
name: AnsibleSSID
auth_mode: open
psk: abc1234
delegate_to: localhost
register: psk_invalid
ignore_errors: yes
- debug:
msg: '{{ psk_invalid }}'
- assert:
that:
- psk_invalid.msg == 'PSK is only allowed when auth_mode is set to psk'
- name: Set PSK with invalid encryption mode
meraki_ssid:
auth_key: '{{auth_key}}'
state: present
org_name: '{{test_org_name}}'
net_name: TestNetSSID
name: AnsibleSSID
auth_mode: psk
psk: abc1234
encryption_mode: eap
delegate_to: localhost
register: psk_invalid_mode
ignore_errors: yes
- debug:
msg: '{{ psk_invalid_mode }}'
- assert:
that:
- psk_invalid_mode.msg == 'PSK requires encryption_mode be set to wpa'
- name: Error for PSK and RADIUS servers
meraki_ssid:
auth_key: '{{auth_key}}'
state: present
org_name: '{{test_org_name}}'
net_name: TestNetSSID
name: AnsibleSSID
auth_mode: psk
radius_servers:
- host: 192.0.1.200
port: 1234
secret: abc98765
delegate_to: localhost
register: err_radius_server_psk
ignore_errors: yes
- debug:
var: err_radius_server_psk
- assert:
that:
- 'err_radius_server_psk.msg == "radius_servers requires auth_mode to be open-with-radius or 8021x-radius"'
- name: Set VLAN arg without default VLAN error
meraki_ssid:
auth_key: '{{auth_key}}'
state: present
org_name: '{{test_org_name}}'
net_name: TestNetSSID
number: 1
use_vlan_tagging: yes
ip_assignment_mode: Bridge mode
ap_tags_vlan_ids:
- tags: wifi
vlan_id: 2
delegate_to: localhost
register: set_vlan_arg_err
ignore_errors: yes
- debug:
var: set_vlan_arg_err
- assert:
that:
- 'set_vlan_arg_err.msg == "default_vlan_id is required when use_vlan_tagging is True"'
always:
- name: Delete SSID
meraki_ssid:
auth_key: '{{auth_key}}'
state: absent
org_name: '{{test_org_name}}'
net_name: TestNetSSID
name: AnsibleSSID
delegate_to: localhost
register: delete_ssid
- debug:
msg: '{{ delete_ssid }}'
- assert:
that:
- delete_ssid.data.name == 'Unconfigured SSID 2'
- name: Delete test network
meraki_network:
auth_key: '{{auth_key}}'
state: absent
org_name: '{{test_org_name}}'
net_name: TestNetSSID
register: delete_net
- debug:
msg: '{{delete_net}}'

@ -1,170 +0,0 @@
# Test code for the Meraki modules
# Copyright: (c) 2018, Kevin Breit (@kbreit)
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
---
- block:
- name: Create appliance network
meraki_network:
auth_key: '{{ auth_key }}'
state: present
org_name: '{{test_org_name}}'
net_name: IntTestNetwork
timezone: America/Chicago
type: appliance
delegate_to: localhost
register: net
- set_fact:
net_id: '{{net.data.id}}'
- name: Initialize static route id list
set_fact:
route_ids: []
- name: Create static_route
meraki_static_route:
auth_key: '{{auth_key}}'
state: present
org_name: '{{test_org_name}}'
net_name: IntTestNetwork
name: Test Route
subnet: 192.0.1.0/24
gateway_ip: 192.168.128.1
delegate_to: localhost
register: create_route
- set_fact:
route_ids: "{{ route_ids + [create_route.data.id] }}"
- name: Create second static_route
meraki_static_route:
auth_key: '{{auth_key}}'
state: present
org_name: '{{test_org_name}}'
net_name: IntTestNetwork
name: Test Route 2
subnet: 192.0.2.0/24
gateway_ip: 192.168.128.1
delegate_to: localhost
register: second_create
- set_fact:
route_ids: "{{ route_ids + [second_create.data.id] }}"
- assert:
that:
- create_route.changed == True
- create_route.data.id is defined
- name: Update static route
meraki_static_route:
auth_key: '{{auth_key}}'
state: present
org_name: '{{test_org_name}}'
net_name: IntTestNetwork
route_id: '{{create_route.data.id}}'
subnet: 192.0.3.0/24
enabled: yes
delegate_to: localhost
register: update
- assert:
that:
- update.data.subnet == "192.0.3.0/24"
- name: Query static routes
meraki_static_route:
auth_key: '{{auth_key}}'
state: query
org_name: '{{test_org_name}}'
net_name: IntTestNetwork
delegate_to: localhost
register: query_all
- debug:
var: query_all
- assert:
that:
- query_all.data | length >= 2
- name: Update static route with idempotency
meraki_static_route:
auth_key: '{{auth_key}}'
state: present
org_name: '{{test_org_name}}'
net_name: IntTestNetwork
route_id: '{{create_route.data.id}}'
name: Test Route
gateway_ip: 192.168.128.1
subnet: 192.0.3.0/24
enabled: yes
delegate_to: localhost
register: update_idempotent
- assert:
that:
- update_idempotent.changed == False
- update_idempotent.data is defined
- name: Update static route with fixed IP assignment and reservation
meraki_static_route:
auth_key: '{{auth_key}}'
state: present
org_name: '{{test_org_name}}'
net_name: IntTestNetwork
route_id: '{{create_route.data.id}}'
fixed_ip_assignments:
- mac: aa:bb:cc:dd:ee:ff
ip: 192.0.3.11
name: WebServer
reserved_ip_ranges:
- start: 192.168.3.2
end: 192.168.3.10
comment: Printers
delegate_to: localhost
register: fixed_ip
- debug:
var: fixed_ip
- assert:
that:
- fixed_ip.data.fixedIpAssignments | length == 1
- fixed_ip.data.reservedIpRanges | length == 1
- name: Query single static route
meraki_static_route:
auth_key: '{{auth_key}}'
state: query
org_name: '{{test_org_name}}'
net_name: IntTestNetwork
route_id: '{{create_route.data.id}}'
delegate_to: localhost
register: query_one
- assert:
that:
- query_one.data.name == "Test Route"
- name: Delete static routes
meraki_static_route:
auth_key: '{{auth_key}}'
state: absent
org_name: '{{test_org_name}}'
net_name: IntTestNetwork
route_id: '{{item}}'
delegate_to: localhost
loop: '{{route_ids}}'
register: delete_all
always:
- name: Delete appliance network
meraki_network:
auth_key: '{{ auth_key }}'
state: absent
org_name: '{{test_org_name}}'
net_name: IntTestNetwork
delegate_to: localhost

@ -1,297 +0,0 @@
# Test code for the Meraki Organization module
# Copyright: (c) 2018, Kevin Breit (@kbreit)
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
---
- name: Test an API key is provided
fail:
msg: Please define an API key
when: auth_key is not defined
- name: Query all switchports
meraki_switchport:
auth_key: '{{auth_key}}'
state: query
serial: Q2HP-2C6E-GTLD
delegate_to: localhost
register: query_all
- debug:
msg: '{{query_all}}'
- name: Query one switchport
meraki_switchport:
auth_key: '{{auth_key}}'
state: query
serial: Q2HP-2C6E-GTLD
number: 1
delegate_to: localhost
register: query_one
- debug:
msg: '{{query_one}}'
- name: Enable switchport
meraki_switchport:
auth_key: '{{auth_key}}'
state: present
serial: Q2HP-2C6E-GTLD
number: 7
enabled: true
delegate_to: localhost
register: update_port_true
- debug:
msg: '{{update_port_true}}'
- assert:
that:
- update_port_true.data.enabled == True
- name: Disable switchport
meraki_switchport:
auth_key: '{{auth_key}}'
state: present
serial: Q2HP-2C6E-GTLD
number: 7
enabled: false
delegate_to: localhost
register: update_port_false
- debug:
msg: '{{update_port_false}}'
- assert:
that:
- update_port_false.data.enabled == False
- name: Name switchport
meraki_switchport:
auth_key: '{{auth_key}}'
state: present
serial: Q2HP-2C6E-GTLD
number: 7
name: Test Port
delegate_to: localhost
register: update_port_name
- debug:
msg: '{{update_port_name}}'
- assert:
that:
- update_port_name.data.name == 'Test Port'
- name: Configure access port
meraki_switchport:
auth_key: '{{auth_key}}'
state: present
serial: Q2HP-2C6E-GTLD
number: 7
enabled: true
name: Test Port
tags: desktop
type: access
vlan: 10
delegate_to: localhost
register: update_access_port
- debug:
msg: '{{update_access_port}}'
- assert:
that:
- update_access_port.data.vlan == 10
- name: Configure port as trunk
meraki_switchport:
auth_key: '{{auth_key}}'
state: present
serial: Q2HP-2C6E-GTLD
number: 8
enabled: true
name: Test Port
type: trunk
vlan: 10
allowed_vlans: 10, 100, 200
delegate_to: localhost
- name: Convert trunk port to access
meraki_switchport:
auth_key: '{{auth_key}}'
state: present
serial: Q2HP-2C6E-GTLD
number: 8
enabled: true
name: Test Port
type: access
vlan: 10
delegate_to: localhost
- name: Test converted port for idempotency
meraki_switchport:
auth_key: '{{auth_key}}'
state: present
serial: Q2HP-2C6E-GTLD
number: 8
enabled: true
name: Test Port
type: access
vlan: 10
delegate_to: localhost
register: convert_idempotent
- assert:
that:
- convert_idempotent.changed == False
- name: Configure access port with voice VLAN
meraki_switchport:
auth_key: '{{auth_key}}'
state: present
serial: Q2HP-2C6E-GTLD
number: 7
enabled: true
name: Test Port
tags: desktop
type: access
vlan: 10
voice_vlan: 11
delegate_to: localhost
register: update_port_vvlan
- debug:
msg: '{{update_port_vvlan}}'
- assert:
that:
- update_port_vvlan.data.voice_vlan == 11
- update_port_vvlan.changed == True
- name: Check access port for idempotenty
meraki_switchport:
auth_key: '{{auth_key}}'
state: present
serial: Q2HP-2C6E-GTLD
number: 7
enabled: true
name: Test Port
tags: desktop
type: access
vlan: 10
voice_vlan: 11
delegate_to: localhost
register: update_port_access_idempotent
- debug:
msg: '{{update_port_access_idempotent}}'
- assert:
that:
- update_port_access_idempotent.changed == False
- update_port_access_idempotent.data is defined
- name: Configure trunk port
meraki_switchport:
auth_key: '{{auth_key}}'
state: present
serial: Q2HP-2C6E-GTLD
number: 7
enabled: true
name: Server port
tags: server
type: trunk
allowed_vlans: all
vlan: 8
delegate_to: localhost
register: update_trunk
- debug:
msg: '{{update_trunk}}'
- assert:
that:
- update_trunk.data.tags == 'server'
- update_trunk.data.type == 'trunk'
- update_trunk.data.allowed_vlans == 'all'
- name: Configure trunk port with specific VLANs
meraki_switchport:
auth_key: '{{auth_key}}'
state: present
serial: Q2HP-2C6E-GTLD
number: 7
enabled: true
name: Server port
tags: server
type: trunk
vlan: 8
allowed_vlans:
- 10
- 15
- 20
delegate_to: localhost
register: update_trunk
- debug:
msg: '{{update_trunk}}'
- assert:
that:
- update_trunk.data.tags == 'server'
- update_trunk.data.type == 'trunk'
- update_trunk.data.allowed_vlans == '8,10,15,20'
- name: Configure trunk port with specific VLANs and native VLAN
meraki_switchport:
auth_key: '{{auth_key}}'
state: present
serial: Q2HP-2C6E-GTLD
number: 7
enabled: true
name: Server port
tags: server
type: trunk
vlan: 2
allowed_vlans:
- 10
- 15
- 20
delegate_to: localhost
register: update_trunk
- debug:
msg: '{{update_trunk}}'
- assert:
that:
- update_trunk.data.tags == 'server'
- update_trunk.data.type == 'trunk'
- update_trunk.data.allowed_vlans == '2,10,15,20'
- name: Check for idempotency on trunk port
meraki_switchport:
auth_key: '{{auth_key}}'
state: present
serial: Q2HP-2C6E-GTLD
number: 7
enabled: true
name: Server port
tags: server
type: trunk
vlan: 2
allowed_vlans:
- 10
- 15
- 20
delegate_to: localhost
register: update_trunk_idempotent
- debug:
msg: '{{update_trunk_idempotent}}'
- assert:
that:
- update_trunk_idempotent.changed == False
- update_trunk_idempotent.data is defined

@ -1,169 +0,0 @@
# Test code for the Meraki Organization module
# Copyright: (c) 2018, Kevin Breit (@kbreit)
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
---
- block:
- name: Test an API key is provided
fail:
msg: Please define an API key
when: auth_key is not defined
- set_fact:
syslog_test_net_name: 'syslog_{{test_net_name}}'
- name: Create network with type appliance and no timezone
meraki_network:
auth_key: '{{ auth_key }}'
state: present
org_name: '{{test_org_name}}'
net_name: '{{test_net_name}}'
type: appliance
delegate_to: localhost
register: new_net
- set_fact:
net_id: '{{new_net.data.id}}'
- name: Query syslog settings
meraki_syslog:
auth_key: '{{auth_key}}'
org_name: '{{test_org_name}}'
net_name: '{{test_net_name}}'
state: query
delegate_to: localhost
register: query_all
- debug:
msg: '{{query_all}}'
- name: Set syslog server
meraki_syslog:
auth_key: '{{auth_key}}'
org_name: '{{test_org_name}}'
net_name: '{{test_net_name}}'
state: present
servers:
- host: 192.0.1.2
port: 514
roles:
- Appliance event log
delegate_to: localhost
register: create_server
- debug:
msg: '{{create_server.data}}'
- assert:
that:
- create_server['data'][0]['host'] == "192.0.1.2"
- name: Set syslog server with idempotency
meraki_syslog:
auth_key: '{{auth_key}}'
org_name: '{{test_org_name}}'
net_name: '{{test_net_name}}'
state: present
servers:
- host: 192.0.1.2
port: 514
roles:
- Appliance event log
delegate_to: localhost
register: create_server_idempotency
- debug:
msg: '{{create_server_idempotency}}'
- assert:
that:
- create_server_idempotency.changed == False
- create_server_idempotency.data is defined
- name: Set multiple syslog servers
meraki_syslog:
auth_key: '{{auth_key}}'
org_name: '{{test_org_name}}'
net_id: '{{net_id}}'
state: present
servers:
- host: 192.0.1.3
port: 514
roles:
- Appliance event log
- host: 192.0.1.4
port: 514
roles:
- Appliance Event log
- Flows
- host: 192.0.1.5
port: 514
roles:
- Flows
delegate_to: localhost
register: create_multiple_servers
- debug:
msg: '{{create_multiple_servers}}'
- assert:
that:
- create_multiple_servers['data'][0]['host'] == "192.0.1.3"
- create_multiple_servers['data'][1]['host'] == "192.0.1.4"
- create_multiple_servers['data'][2]['host'] == "192.0.1.5"
- create_multiple_servers['data'] | length == 3
- name: Create syslog server with bad name
meraki_syslog:
auth_key: '{{auth_key}}'
org_name: '{{test_org_name}}'
net_name: '{{test_net_name}}'
state: present
servers:
- host: 192.0.1.6
port: 514
roles:
- Invalid role
delegate_to: localhost
register: invalid_role
ignore_errors: yes
# - debug:
# msg: '{{invalid_role.body.errors.0}}'
- assert:
that:
- '"Please select at least one valid role" in invalid_role.body.errors.0'
- name: Add role to existing syslog server # Adding doesn't work, just creation
meraki_syslog:
auth_key: '{{auth_key}}'
org_name: '{{test_org_name}}'
net_name: '{{test_net_name}}'
state: present
servers:
- host: 192.0.1.2
port: 514
roles:
- flows
delegate_to: localhost
register: add_role
- debug:
msg: '{{add_role.data.0.roles}}'
- assert:
that:
- add_role.data.0.roles.0 == 'Flows'
# - add_role.data.0.roles | length == 2
always:
- name: Delete syslog test network
meraki_network:
auth_key: '{{ auth_key }}'
state: absent
org_name: '{{test_org_name}}'
net_name: '{{test_net_name}}'
delegate_to: localhost
register: delete_all
ignore_errors: yes

@ -1,412 +0,0 @@
# Test code for the Meraki VLAN module
# Copyright: (c) 2018, Kevin Breit (@kbreit)
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
---
- block:
- name: Test an API key is provided
fail:
msg: Please define an API key
when: auth_key is not defined
- name: Test play without auth_key
meraki_network:
state: present
org_name: '{{test_org_name}}'
net_name: '{{test_net_name}}'
type: appliance
delegate_to: localhost
ignore_errors: yes
register: no_key
- assert:
that:
- '"missing required arguments" in no_key.msg'
- name: Create test network
meraki_network:
auth_key: '{{auth_key}}'
state: present
org_name: '{{test_org_name}}'
net_name: '{{test_net_name}}'
type: appliance
delegate_to: localhost
- name: Enable VLANs on network
meraki_network:
auth_key: '{{auth_key}}'
state: present
org_name: '{{test_org_name}}'
net_name: '{{test_net_name}}'
enable_vlans: yes
delegate_to: localhost
- name: Create VLAN in check mode
meraki_vlan:
auth_key: '{{auth_key}}'
state: present
org_name: '{{test_org_name}}'
net_name: '{{test_net_name}}'
vlan_id: 2
name: TestVLAN
subnet: 192.168.250.0/24
appliance_ip: 192.168.250.1
delegate_to: localhost
register: create_vlan_check
check_mode: yes
- debug:
var: create_vlan_check
- assert:
that:
- create_vlan_check is changed
- name: Create VLAN
meraki_vlan:
auth_key: '{{auth_key}}'
state: present
org_name: '{{test_org_name}}'
net_name: '{{test_net_name}}'
vlan_id: 2
name: TestVLAN
subnet: 192.168.250.0/24
appliance_ip: 192.168.250.1
delegate_to: localhost
register: create_vlan
environment:
ANSIBLE_MERAKI_FORMAT: camelcase
- debug:
msg: '{{create_vlan}}'
- assert:
that:
- create_vlan.data.id == 2
- create_vlan.changed == True
- create_vlan.data.networkId is defined
- name: Update VLAN with check mode
meraki_vlan:
auth_key: '{{auth_key}}'
state: present
org_name: '{{test_org_name}}'
net_name: '{{test_net_name}}'
vlan_id: 2
name: TestVLAN
subnet: 192.168.250.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
register: update_vlan_check
check_mode: yes
- debug:
var: update_vlan_check
- assert:
that:
- update_vlan_check is changed
- name: Update VLAN
meraki_vlan:
auth_key: '{{auth_key}}'
state: present
org_name: '{{test_org_name}}'
net_name: '{{test_net_name}}'
vlan_id: 2
name: TestVLAN
subnet: 192.168.250.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
register: update_vlan
- debug:
msg: '{{update_vlan}}'
- assert:
that:
- update_vlan.data.appliance_ip == '192.168.250.2'
- update_vlan.changed == True
- name: Update VLAN with idempotency and check mode
meraki_vlan:
auth_key: '{{auth_key}}'
state: present
org_name: '{{test_org_name}}'
net_name: '{{test_net_name}}'
vlan_id: 2
name: TestVLAN
subnet: 192.168.250.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
register: update_vlan_idempotent_check
check_mode: yes
- debug:
var: update_vlan_idempotent_check
- assert:
that:
- update_vlan_idempotent_check is not changed
- name: Update VLAN with idempotency
meraki_vlan:
auth_key: '{{auth_key}}'
state: present
org_name: '{{test_org_name}}'
net_name: '{{test_net_name}}'
vlan_id: 2
name: TestVLAN
subnet: 192.168.250.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
register: update_vlan_idempotent
- debug:
msg: '{{update_vlan_idempotent}}'
- assert:
that:
- update_vlan_idempotent.changed == False
- update_vlan_idempotent.data is defined
- name: Add IP assignments and reserved IP ranges
meraki_vlan:
auth_key: '{{auth_key}}'
state: present
org_name: '{{test_org_name}}'
net_name: '{{test_net_name}}'
vlan_id: 2
name: TestVLAN
subnet: 192.168.250.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
- mac: "12:34:56:78:90:12"
ip: 192.168.250.11
name: another_fixed_ip
reserved_ip_range:
- start: 192.168.250.10
end: 192.168.250.20
comment: reserved_range
- start: 192.168.250.100
end: 192.168.250.120
comment: reserved_range_high
dns_nameservers: opendns
delegate_to: localhost
register: update_vlan_add_ip
- debug:
msg: '{{update_vlan_add_ip}}'
- assert:
that:
- update_vlan_add_ip.changed == True
- update_vlan_add_ip.data.fixed_ip_assignments | length == 2
- update_vlan_add_ip.data.reserved_ip_ranges | length == 2
- name: Remove IP assignments and reserved IP ranges
meraki_vlan:
auth_key: '{{auth_key}}'
state: present
org_name: '{{test_org_name}}'
net_name: '{{test_net_name}}'
vlan_id: 2
name: TestVLAN
subnet: 192.168.250.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
register: update_vlan_remove_ip
- debug:
msg: '{{update_vlan_remove_ip}}'
- assert:
that:
- update_vlan_remove_ip.changed == True
- update_vlan_remove_ip.data.fixed_ip_assignments | length == 1
- update_vlan_remove_ip.data.reserved_ip_ranges | length == 1
- name: Update VLAN with idempotency
meraki_vlan:
auth_key: '{{auth_key}}'
state: present
org_name: '{{test_org_name}}'
net_name: '{{test_net_name}}'
vlan_id: 2
name: TestVLAN
subnet: 192.168.250.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
register: update_vlan_idempotent
- debug:
msg: '{{update_vlan_idempotent}}'
- assert:
that:
- update_vlan_idempotent.changed == False
- update_vlan_idempotent.data is defined
- name: Update VLAN with list of DNS entries
meraki_vlan:
auth_key: '{{auth_key}}'
state: present
org_name: '{{test_org_name}}'
net_name: '{{test_net_name}}'
vlan_id: 2
name: TestVLAN
subnet: 192.168.250.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: 1.1.1.1;8.8.8.8
delegate_to: localhost
register: update_vlan_dns_list
- debug:
msg: '{{update_vlan_dns_list}}'
- assert:
that:
- '"1.1.1.1" in update_vlan_dns_list.data.dns_nameservers'
- update_vlan_dns_list.changed == True
- name: Query all VLANs in network
meraki_vlan:
auth_key: '{{ auth_key }}'
org_name: '{{test_org_name}}'
net_name: '{{test_net_name}}'
state: query
delegate_to: localhost
register: query_vlans
- debug:
msg: '{{query_vlans}}'
- assert:
that:
- query_vlans.data | length >= 2
- query_vlans.data.1.id == 2
- query_vlans.changed == False
- name: Query single VLAN
meraki_vlan:
auth_key: '{{ auth_key }}'
org_name: '{{test_org_name}}'
net_name: '{{test_net_name}}'
vlan_id: 2
state: query
output_level: debug
delegate_to: localhost
register: query_vlan
- debug:
msg: '{{query_vlan}}'
- assert:
that:
- query_vlan.data.id == 2
- query_vlan.changed == False
always:
#############################################################################
# Tear down starts here
#############################################################################
- name: Delete VLAN with check mode
meraki_vlan:
auth_key: '{{auth_key}}'
state: absent
org_name: '{{test_org_name}}'
net_name: '{{test_net_name}}'
vlan_id: 2
delegate_to: localhost
register: delete_vlan_check
check_mode: yes
- assert:
that:
delete_vlan_check is changed
- name: Delete VLAN
meraki_vlan:
auth_key: '{{auth_key}}'
state: absent
org_name: '{{test_org_name}}'
net_name: '{{test_net_name}}'
vlan_id: 2
delegate_to: localhost
register: delete_vlan
- debug:
msg: '{{delete_vlan}}'
- name: Delete test network
meraki_network:
auth_key: '{{auth_key}}'
state: absent
org_name: '{{test_org_name}}'
net_name: '{{test_net_name}}'
delegate_to: localhost

@ -1,7 +0,0 @@
# Test code for the Meraki Webhooks module
# Copyright: (c) 2018, Kevin Breit (@kbreit)
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
---
- name: Run test cases
include: tests.yml ansible_connection=local

@ -1,273 +0,0 @@
# Test code for the Meraki Webhook module
# Copyright: (c) 2019, Kevin Breit (@kbreit)
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
---
- block:
- name: Test an API key is provided
fail:
msg: Please define an API key
when: auth_key is not defined
- name: Create test network
meraki_network:
auth_key: '{{auth_key}}'
state: present
org_name: '{{test_org_name}}'
net_name: '{{test_net_name}}'
type: appliance
- name: Create webhook with check mode
meraki_webhook:
auth_key: '{{auth_key}}'
state: present
org_name: '{{test_org_name}}'
net_name: '{{test_net_name}}'
name: Test_Hook
url: https://webhook.site/8eb5b76f-b167-4cb8-9fc4-42621b724244
shared_secret: shhhdonttellanyone
check_mode: yes
register: create_one_check
- debug:
var: create_one_check
- assert:
that:
- create_one_check is changed
- create_one_check.data is defined
- name: Create webhook
meraki_webhook:
auth_key: '{{auth_key}}'
state: present
org_name: '{{test_org_name}}'
net_name: '{{test_net_name}}'
name: Test_Hook
url: https://webhook.site/8eb5b76f-b167-4cb8-9fc4-42621b724244
shared_secret: shhhdonttellanyone
register: create_one
- debug:
var: create_one
- assert:
that:
- create_one is changed
- create_one.data is defined
- set_fact:
webhook_id: '{{create_one.data.id}}'
- name: Query one webhook
meraki_webhook:
auth_key: '{{auth_key}}'
state: query
org_name: '{{test_org_name}}'
net_name: '{{test_net_name}}'
name: Test_Hook
register: query_one
- debug:
var: query_one
- assert:
that:
- query_one.data is defined
- name: Query one webhook with id
meraki_webhook:
auth_key: '{{auth_key}}'
state: query
org_name: '{{test_org_name}}'
net_name: '{{test_net_name}}'
webhook_id: '{{webhook_id}}'
register: query_one_id
- debug:
var: query_one_id
- assert:
that:
- query_one_id.data is defined
- name: Update webhook with check mode
meraki_webhook:
auth_key: '{{auth_key}}'
state: present
org_name: '{{test_org_name}}'
net_name: '{{test_net_name}}'
name: Test_Hook
url: https://webhook.site/8eb5b76f-b167-4cb8-9fc4-42621b724244
shared_secret: shhhdonttellanyonehere
check_mode: yes
register: update_check
- assert:
that:
- update_check is changed
- update_check.data is defined
- name: Update webhook
meraki_webhook:
auth_key: '{{auth_key}}'
state: present
org_name: '{{test_org_name}}'
net_name: '{{test_net_name}}'
name: Test_Hook
url: https://webhook.site/8eb5b76f-b167-4cb8-9fc4-42621b724244
shared_secret: shhhdonttellanyonehere
register: update
- debug:
var: update
- assert:
that:
- update is changed
- update.data is defined
- name: Update webhook with idempotency
meraki_webhook:
auth_key: '{{auth_key}}'
state: present
org_name: '{{test_org_name}}'
net_name: '{{test_net_name}}'
name: Test_Hook
url: https://webhook.site/8eb5b76f-b167-4cb8-9fc4-42621b724244
shared_secret: shhhdonttellanyonehere
register: update_idempotent
- debug:
var: update_idempotent
- assert:
that:
- update_idempotent is not changed
- update_idempotent.data is defined
- name: Update webhook with id
meraki_webhook:
auth_key: '{{auth_key}}'
state: present
org_name: '{{test_org_name}}'
net_name: '{{test_net_name}}'
webhook_id: '{{webhook_id}}'
name: Test_Hook
url: https://webhook.site/8eb5b76f-b167-4cb8-9fc4-42621b724244
shared_secret: shhhdonttellanyonehereid
register: update_id
- debug:
var: update_id
- assert:
that:
- update_id is changed
- update_id.data is defined
- name: Test webhook
meraki_webhook:
auth_key: '{{auth_key}}'
state: present
org_name: '{{test_org_name}}'
net_name: '{{test_net_name}}'
test: test
url: https://webhook.site/8eb5b76f-b167-4cb8-9fc4-42621b724244
register: webhook_test
- debug:
var: webhook_test
- set_fact:
test_id: '{{webhook_test.data.id}}'
- name: Get webhook status
meraki_webhook:
auth_key: '{{auth_key}}'
state: present
org_name: '{{test_org_name}}'
net_name: '{{test_net_name}}'
test: status
test_id: '{{test_id}}'
register: webhook_test_status
- debug:
var: webhook_test_status
- assert:
that:
- webhook_test_status.data is defined
- name: Query all webhooks
meraki_webhook:
auth_key: '{{auth_key}}'
state: query
org_name: '{{test_org_name}}'
net_name: '{{test_net_name}}'
register: query_all
- debug:
var: query_all
- name: Delete webhook invalid webhook
meraki_webhook:
auth_key: '{{auth_key}}'
state: absent
org_name: '{{test_org_name}}'
net_name: '{{test_net_name}}'
name: Test_Hook_Invalid
check_mode: yes
register: delete_invalid
ignore_errors: yes
- debug:
var: delete_invalid
- assert:
that:
- 'delete_invalid.msg == "There is no webhook with the name Test_Hook_Invalid"'
- name: Delete webhook in check mode
meraki_webhook:
auth_key: '{{auth_key}}'
state: absent
org_name: '{{test_org_name}}'
net_name: '{{test_net_name}}'
name: Test_Hook
check_mode: yes
register: delete_check
- debug:
var: delete_check
- assert:
that:
- delete_check is changed
- name: Delete webhook
meraki_webhook:
auth_key: '{{auth_key}}'
state: absent
org_name: '{{test_org_name}}'
net_name: '{{test_net_name}}'
name: Test_Hook
register: delete
- debug:
var: delete
- assert:
that:
- delete is changed
always:
#############################################################################
# Tear down starts here
#############################################################################
- name: Delete test network
meraki_network:
auth_key: '{{auth_key}}'
state: absent
org_name: '{{test_org_name}}'
net_name: '{{test_net_name}}'

@ -100,8 +100,6 @@ lib/ansible/module_utils/network/junos/facts/legacy/base.py future-import-boiler
lib/ansible/module_utils/network/junos/facts/legacy/base.py metaclass-boilerplate
lib/ansible/module_utils/network/junos/junos.py future-import-boilerplate
lib/ansible/module_utils/network/junos/junos.py metaclass-boilerplate
lib/ansible/module_utils/network/meraki/meraki.py future-import-boilerplate
lib/ansible/module_utils/network/meraki/meraki.py metaclass-boilerplate
lib/ansible/module_utils/network/nxos/argspec/facts/facts.py future-import-boilerplate
lib/ansible/module_utils/network/nxos/argspec/facts/facts.py metaclass-boilerplate
lib/ansible/module_utils/network/nxos/facts/facts.py future-import-boilerplate
@ -1960,42 +1958,6 @@ lib/ansible/modules/network/junos/junos_vrf.py validate-modules:missing-suboptio
lib/ansible/modules/network/junos/junos_vrf.py validate-modules:parameter-list-no-elements
lib/ansible/modules/network/junos/junos_vrf.py validate-modules:parameter-type-not-in-doc
lib/ansible/modules/network/junos/junos_vrf.py validate-modules:undocumented-parameter
lib/ansible/modules/network/meraki/meraki_admin.py validate-modules:parameter-type-not-in-doc
lib/ansible/modules/network/meraki/meraki_config_template.py validate-modules:parameter-type-not-in-doc
lib/ansible/modules/network/meraki/meraki_content_filtering.py validate-modules:parameter-list-no-elements
lib/ansible/modules/network/meraki/meraki_firewalled_services.py validate-modules:doc-elements-mismatch
lib/ansible/modules/network/meraki/meraki_malware.py validate-modules:doc-elements-mismatch
lib/ansible/modules/network/meraki/meraki_malware.py validate-modules:parameter-type-not-in-doc
lib/ansible/modules/network/meraki/meraki_mr_l3_firewall.py validate-modules:doc-elements-mismatch
lib/ansible/modules/network/meraki/meraki_mr_l3_firewall.py validate-modules:doc-type-does-not-match-spec
lib/ansible/modules/network/meraki/meraki_mx_l3_firewall.py validate-modules:doc-elements-mismatch
lib/ansible/modules/network/meraki/meraki_mx_l3_firewall.py validate-modules:parameter-type-not-in-doc
lib/ansible/modules/network/meraki/meraki_mx_l7_firewall.py pylint:ansible-bad-function
lib/ansible/modules/network/meraki/meraki_mx_l7_firewall.py validate-modules:doc-elements-mismatch
lib/ansible/modules/network/meraki/meraki_mx_l7_firewall.py validate-modules:nonexistent-parameter-documented
lib/ansible/modules/network/meraki/meraki_mx_l7_firewall.py validate-modules:parameter-list-no-elements
lib/ansible/modules/network/meraki/meraki_mx_l7_firewall.py validate-modules:parameter-type-not-in-doc
lib/ansible/modules/network/meraki/meraki_nat.py validate-modules:doc-elements-mismatch
lib/ansible/modules/network/meraki/meraki_nat.py validate-modules:invalid-ansiblemodule-schema
lib/ansible/modules/network/meraki/meraki_nat.py validate-modules:parameter-list-no-elements
lib/ansible/modules/network/meraki/meraki_network.py validate-modules:parameter-list-no-elements
lib/ansible/modules/network/meraki/meraki_network.py validate-modules:parameter-type-not-in-doc
lib/ansible/modules/network/meraki/meraki_organization.py validate-modules:parameter-type-not-in-doc
lib/ansible/modules/network/meraki/meraki_snmp.py validate-modules:invalid-ansiblemodule-schema
lib/ansible/modules/network/meraki/meraki_snmp.py validate-modules:parameter-list-no-elements
lib/ansible/modules/network/meraki/meraki_ssid.py validate-modules:doc-elements-mismatch
lib/ansible/modules/network/meraki/meraki_ssid.py validate-modules:doc-required-mismatch
lib/ansible/modules/network/meraki/meraki_ssid.py validate-modules:parameter-list-no-elements
lib/ansible/modules/network/meraki/meraki_static_route.py validate-modules:doc-elements-mismatch
lib/ansible/modules/network/meraki/meraki_switchport.py validate-modules:doc-required-mismatch
lib/ansible/modules/network/meraki/meraki_switchport.py validate-modules:parameter-list-no-elements
lib/ansible/modules/network/meraki/meraki_switchport.py validate-modules:parameter-type-not-in-doc
lib/ansible/modules/network/meraki/meraki_syslog.py validate-modules:doc-elements-mismatch
lib/ansible/modules/network/meraki/meraki_syslog.py validate-modules:parameter-list-no-elements
lib/ansible/modules/network/meraki/meraki_vlan.py validate-modules:doc-elements-mismatch
lib/ansible/modules/network/meraki/meraki_vlan.py validate-modules:missing-suboption-docs
lib/ansible/modules/network/meraki/meraki_vlan.py validate-modules:parameter-type-not-in-doc
lib/ansible/modules/network/meraki/meraki_vlan.py validate-modules:undocumented-parameter
lib/ansible/modules/network/nxos/_nxos_interface.py validate-modules:doc-choices-do-not-match-spec
lib/ansible/modules/network/nxos/_nxos_interface.py validate-modules:doc-default-does-not-match-spec
lib/ansible/modules/network/nxos/_nxos_interface.py validate-modules:doc-default-incompatible-type
@ -2925,8 +2887,6 @@ lib/ansible/plugins/doc_fragments/inventory_cache.py future-import-boilerplate
lib/ansible/plugins/doc_fragments/inventory_cache.py metaclass-boilerplate
lib/ansible/plugins/doc_fragments/junos.py future-import-boilerplate
lib/ansible/plugins/doc_fragments/junos.py metaclass-boilerplate
lib/ansible/plugins/doc_fragments/meraki.py future-import-boilerplate
lib/ansible/plugins/doc_fragments/meraki.py metaclass-boilerplate
lib/ansible/plugins/doc_fragments/nxos.py future-import-boilerplate
lib/ansible/plugins/doc_fragments/nxos.py metaclass-boilerplate
lib/ansible/plugins/doc_fragments/openstack.py future-import-boilerplate

@ -1,6 +0,0 @@
[
{
"id": 2930418,
"name": "My organization"
}
]

@ -1,160 +0,0 @@
# -*- coding: utf-8 -*-
# Copyright 2019 Kevin Breit <kevin.breit@kevinbreit.net>
# This file is part of Ansible by Red Hat
#
# Ansible is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Ansible is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
import json
import os
import pytest
from units.compat import unittest, mock
from ansible.module_utils.basic import AnsibleModule
from ansible.module_utils.network.meraki.meraki import MerakiModule, meraki_argument_spec, HTTPError, RateLimitException
from ansible.module_utils.six import PY2, PY3
from ansible.module_utils._text import to_native, to_bytes
from units.modules.utils import set_module_args
fixture_path = os.path.join(os.path.dirname(__file__), 'fixtures')
fixture_data = {}
testcase_data = {
"params": {'orgs': ['orgs.json'],
}
}
def load_fixture(name):
path = os.path.join(fixture_path, name)
if path in fixture_data:
return fixture_data[path]
with open(path) as f:
data = f.read()
# try:
data = json.loads(data)
# except Exception:
# pass
fixture_data[path] = data
return data
@pytest.fixture(scope="module")
def module():
argument_spec = meraki_argument_spec()
set_module_args({'auth_key': 'abc123',
})
module = AnsibleModule(argument_spec=argument_spec,
supports_check_mode=False)
return MerakiModule(module)
def mocked_fetch_url(*args, **kwargs):
print(args)
if args[1] == 'https://api.meraki.com/api/v0/404':
info = {'status': 404,
'msg': '404 - Page is missing',
'url': 'https://api.meraki.com/api/v0/404',
}
info['body'] = '404'
elif args[1] == 'https://api.meraki.com/api/v0/429':
info = {'status': 429,
'msg': '429 - Rate limit hit',
'url': 'https://api.meraki.com/api/v0/429',
}
info['body'] = '429'
return (None, info)
def mocked_fetch_url_rate_success(module, *args, **kwargs):
if module.retry_count == 5:
info = {'status': 200,
'url': 'https://api.meraki.com/api/organization',
}
resp = {'body': 'Succeeded'}
else:
info = {'status': 429,
'msg': '429 - Rate limit hit',
'url': 'https://api.meraki.com/api/v0/429',
}
info['body'] = '429'
return (resp, info)
def mocked_fail_json(*args, **kwargs):
pass
def mocked_sleep(*args, **kwargs):
pass
def test_fetch_url_404(module, mocker):
url = '404'
mocker.patch('ansible.module_utils.network.meraki.meraki.fetch_url', side_effect=mocked_fetch_url)
mocker.patch('ansible.module_utils.network.meraki.meraki.MerakiModule.fail_json', side_effect=mocked_fail_json)
with pytest.raises(HTTPError):
data = module.request(url, method='GET')
assert module.status == 404
def test_fetch_url_429(module, mocker):
url = '429'
mocker.patch('ansible.module_utils.network.meraki.meraki.fetch_url', side_effect=mocked_fetch_url)
mocker.patch('ansible.module_utils.network.meraki.meraki.MerakiModule.fail_json', side_effect=mocked_fail_json)
mocker.patch('time.sleep', return_value=None)
with pytest.raises(RateLimitException):
data = module.request(url, method='GET')
assert module.status == 429
def test_fetch_url_429_success(module, mocker):
url = '429'
mocker.patch('ansible.module_utils.network.meraki.meraki.fetch_url', side_effect=mocked_fetch_url_rate_success)
mocker.patch('ansible.module_utils.network.meraki.meraki.MerakiModule.fail_json', side_effect=mocked_fail_json)
mocker.patch('time.sleep', return_value=None)
# assert module.status == 200
def test_define_protocol_https(module):
module.params['use_https'] = True
module.define_protocol()
testdata = module.params['protocol']
assert testdata == 'https'
def test_define_protocol_http(module):
module.params['use_https'] = False
module.define_protocol()
testdata = module.params['protocol']
assert testdata == 'http'
def test_is_org_valid_org_name(module):
data = load_fixture('orgs.json')
org_count = module.is_org_valid(data, org_name="My organization")
assert org_count == 1
def test_is_org_valid_org_id(module):
data = load_fixture('orgs.json')
org_count = module.is_org_valid(data, org_id=2930418)
assert org_count == 1
Loading…
Cancel
Save