diff --git a/lib/ansible/module_utils/network/meraki/meraki.py b/lib/ansible/module_utils/network/meraki/meraki.py index 4825a9c933..df76773511 100644 --- a/lib/ansible/module_utils/network/meraki/meraki.py +++ b/lib/ansible/module_utils/network/meraki/meraki.py @@ -76,7 +76,7 @@ class MerakiModule(object): # If URLs need to be modified or added for specific purposes, use .update() on the url_catalog dictionary self.get_urls = {'organizations': '/organizations', - 'networks': '/organizations/{org_id}/networks', + 'network': '/organizations/{org_id}/networks', 'admins': '/organizations/{org_id}/admins', 'configTemplates': '/organizations/{org_id}/configTemplates', 'samlRoles': '/organizations/{org_id}/samlRoles', @@ -88,7 +88,7 @@ class MerakiModule(object): } self.get_one_urls = {'organizations': '/organizations/{org_id}', - 'networks': '/networks/{net_id}', + 'network': '/networks/{net_id}', } # Module should add URLs which are required by the module @@ -184,16 +184,30 @@ class MerakiModule(object): # self.fail_json(msg=i['id']) return str(i['id']) + def get_nets(self, org_name=None, org_id=None): + if org_name: + org_id = self.get_org_id(org_name) + path = self.construct_path('get_all', org_id=org_id) + r = self.request(path, method='GET') + return json.loads(r) + def get_net(self, org_name, net_name, data=None): ''' Return network information ''' - if not data: - org_id = self.get_org_id(org_name) - path = '/organizations/{org_id}/networks/{net_id}'.format(org_id=org_id, net_id=self.get_net_id(org_name=org_name, net_name=net_name, data=data)) - return json.loads(self.request('GET', path)) - else: - for n in data: - if n['name'] == net_name: - return n + # if not data: + # org_id = self.get_org_id(org_name) + # path = '/organizations/{org_id}/networks/{net_id}'.format( + # org_id=org_id, + # net_id=self.get_net_id( + # org_name=org_name, + # net_name=net_name, + # data=data) + # ) + # return json.loads(self.request('GET', path)) + # else: + for n in data: + 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 ''' diff --git a/lib/ansible/modules/network/meraki/meraki_network.py b/lib/ansible/modules/network/meraki/meraki_network.py new file mode 100644 index 0000000000..244c28de44 --- /dev/null +++ b/lib/ansible/modules/network/meraki/meraki_network.py @@ -0,0 +1,259 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: (c) 2018, Kevin Breit (@kbreit) +# 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. + +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. + +options: + auth_key: + description: + - Authentication key provided by the dashboard. Required if environmental variable MERAKI_KEY is not set. + 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. + org_name: + description: + - Name of organization associated to a network. + org_id: + description: + - ID of organization associated to a network. + type: + description: + - Type of network device network manages. + - Required when creating a network. + choices: [appliance, combined, switch, wireless] + aliases: [net_type] + tags: + description: + - Comma delimited list of tags to assign to network. + 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. + +author: + - Kevin Breit (@kbreit) +extends_documentation_fragment: meraki +''' + +EXAMPLES = r''' +- name: List all networks associated to the YourOrg organization + meraki_network: + auth_key: abc12345 + status: query + org_name: YourOrg + delegate_to: localhost +- name: Query network named MyNet in the YourOrg organization + meraki_network: + auth_key: abc12345 + status: query + org_name: YourOrg + net_name: MyNet + delegate_to: localhost +- name: Create network named MyNet in the YourOrg organization + meraki_network: + auth_key: abc12345 + status: present + org_name: YourOrg + net_name: MyNet + type: switch + timezone: America/Chicago + tags: production, chicago +''' + +RETURN = r''' +data: + description: Information about the created or manipulated object. + returned: info + type: list + sample: + [ + { + "id": "N_12345", + "name": "YourNetwork", + "organizationId": "0987654321", + "tags": " production ", + "timeZone": "America/Chicago", + "type": "switch" + } + ] +''' + +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(meraki, net_name, data): + for n in data: + if n['name'] == net_name: + return True + return False + + +def construct_tags(tags): + ''' Assumes tags are a comma separated list ''' + if tags is not None: + tags = tags.replace(' ', '') + tags = tags.split(',') + tag_list = str() + for t in tags: + tag_list = tag_list + " " + t + tag_list = tag_list + " " + return tag_list + return None + + +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='str', choices=['wireless', 'switch', 'appliance', 'combined'], aliases=['net_type']), + tags=dict(type='str'), + timezone=dict(type='str'), + net_name=dict(type='str', aliases=['name', 'network']), + state=dict(type='str', choices=['present', 'query', 'absent'], default='present'), + ) + + # the AnsibleModule object will be our abstraction working with Ansible + # this includes instantiation, a couple of common attr would be the + # args/params passed to the execution, as well as if the module + # supports check mode + module = AnsibleModule(argument_spec=argument_spec, + supports_check_mode=False, + ) + + meraki = MerakiModule(module, function='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}'} + 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='org_name or org_id parameters are required') + if meraki.params['state'] != 'query': + if not meraki.params['net_name'] or 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 + if module.check_mode: + return meraki.result + + # Construct payload + if meraki.params['state'] == 'present': + payload = {'name': meraki.params['net_name'], + 'type': 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['type'] == 'combined': + payload['type'] = 'switch wireless appliance' + + # 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['org_name']: + nets = meraki.get_nets(org_name=meraki.params['org_name']) + elif meraki.params['org_id']: + nets = meraki.get_nets(org_id=meraki.params['org_id']) + + 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'], + nets + ) + elif meraki.params['state'] == 'present': + if meraki.params['net_name']: # FIXME: Idempotency check is ugly here, improve + if is_net_valid(meraki, meraki.params['net_name'], nets) is False: + if meraki.params['org_name']: # FIXME: This can be cleaned up...maybe + path = meraki.construct_path('create', + org_name=meraki.params['org_name'] + ) + elif meraki.params['org_id']: + path = meraki.construct_path('create', + org_id=meraki.params['org_id'] + ) + r = meraki.request(path, + method='POST', + payload=json.dumps(payload) + ) + meraki.result['data'] = json.loads(r) + meraki.result['changed'] = True + else: + net = meraki.get_net(meraki.params['org_name'], meraki.params['net_name'], data=nets) + if meraki.is_update_required(net, payload): + path = meraki.construct_path('update', + net_id=meraki.get_net_id(net_name=meraki.params['net_name'], data=nets) + ) + r = meraki.request(path, + method='PUT', + payload=json.dumps(payload)) + meraki.result['data'] = json.loads(r) + meraki.result['changed'] = True + elif meraki.params['state'] == 'absent': + if is_net_valid(meraki, meraki.params['net_name'], nets) is True: + net_id = meraki.get_net_id(org_name=meraki.params['org_name'], + net_name=meraki.params['net_name'], + data=nets) + path = meraki.construct_path('delete', net_id=net_id) + r = meraki.request(path, method='DELETE') + if meraki.status == 204: + meraki.result['changed'] = True + + # in the event of a successful module execution, you will want to + # simple AnsibleModule.exit_json(), passing the key/value results + meraki.exit_json(**meraki.result) + + +if __name__ == '__main__': + main() diff --git a/test/integration/targets/meraki_network/aliases b/test/integration/targets/meraki_network/aliases new file mode 100644 index 0000000000..ad7ccf7ada --- /dev/null +++ b/test/integration/targets/meraki_network/aliases @@ -0,0 +1 @@ +unsupported diff --git a/test/integration/targets/meraki_network/tasks/main.yml b/test/integration/targets/meraki_network/tasks/main.yml new file mode 100644 index 0000000000..c02ad1ee8d --- /dev/null +++ b/test/integration/targets/meraki_network/tasks/main.yml @@ -0,0 +1,235 @@ +# 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) +--- +- 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 + +- 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: Create network with type wireless + meraki_network: + auth_key: '{{ auth_key }}' + state: present + org_name: '{{test_org_name}}' + net_name: IntTestNetworkWireless + type: wireless + timezone: America/Chicago + delegate_to: localhost + register: create_net_wireless + +- name: Create network with type combined + meraki_network: + auth_key: '{{ auth_key }}' + state: present + org_name: '{{test_org_name}}' + net_name: IntTestNetworkCombined + type: combined + timezone: America/Chicago + delegate_to: localhost + register: create_net_combined + +- 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 + +- debug: + msg: '{{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 + +- debug: + msg: '{{create_net_tags}}' + +- name: Modify network + 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 + +- 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 + +- name: Present assertions + assert: + that: + - create_net_no_type.status == 500 + - '"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' + - '"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 + +- 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 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_all.data | length == 7 + - 'net_query_one.data.name == "IntTestNetworkSwitch"' + +- 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 + +- debug: + msg: '{{delete_all_no_org}}' + +- 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 + +- debug: + msg: '{{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 + loop: + - IntTestNetworkSwitch + - IntTestNetworkWireless + - IntTestNetworkAppliance + - IntTestNetworkCombined + - IntTestNetworkTag + - IntTestNetworkTags + +- name: Query after delete all + meraki_network: + auth_key: '{{ auth_key }}' + state: query + org_name: '{{test_org_name}}' + delegate_to: localhost + register: query_deleted + +- name: Delete assertions + assert: + that: + - '"org_name or org_id parameters are required" in delete_all_no_org.msg' + - query_deleted_org_id.data | length == 6 + - query_deleted.data | length == 0 \ No newline at end of file