diff --git a/lib/ansible/modules/network/meraki/meraki_static_route.py b/lib/ansible/modules/network/meraki/meraki_static_route.py new file mode 100644 index 00000000000..2a9c5af2213 --- /dev/null +++ b/lib/ansible/modules/network/meraki/meraki_static_route.py @@ -0,0 +1,386 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: (c) 2018, 2019 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_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: + auth_key: + description: + - Authentication key provided by the dashboard. + - Required if environmental variable MERAKI_KEY is not set. + type: str + 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 + org_name: + description: + - Name of organization associated to a network. + type: str + org_id: + description: + - ID of organization associated to 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: string + sample: d6fa4821-1234-4dfa-af6b-ae8b16c20c39 + net_id: + description: Identification string of network. + returned: query or update + type: string + sample: N_12345 + name: + description: Name of static route. + returned: success + type: string + sample: Data Center static route + subnet: + description: CIDR notation subnet for static route. + returned: success + type: string + sample: 192.0.1.0/24 + gatewayIp: + description: Next hop IP address. + returned: success + type: string + 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: string + sample: 192.0.1.2 + end: + description: Last address in reservation range, inclusive. + returned: query or update + type: string + sample: 192.0.1.10 + comment: + description: Human readable description of range. + returned: query or update + type: string + 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: string + sample: 192.0.1.11 + name: + description: Hostname given to the endpoint. + returned: query or update + type: string + 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']) + nets = meraki.get_nets(org_id=org_id) + net_id = meraki.params['net_id'] + if net_id is None: + net_id = meraki.get_net_id(net_name=meraki.params['net_name'], data=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: + if module.check_mode: + meraki.result['data'] = payload + meraki.exit_json(**meraki.result) + path = meraki.construct_path('create', net_id=net_id) + meraki.result['data'] = meraki.request(path, method="POST", payload=json.dumps(payload)) + meraki.result['changed'] = True + elif meraki.params['state'] == 'absent': + if module.check_mode: + meraki.exit_json(**meraki.result) + path = meraki.construct_path('delete', net_id=net_id, custom={'route_id': meraki.params['route_id']}) + meraki.result['data'] = meraki.request(path, method='DELETE') + meraki.result['changed'] = True + + # in the event of a successful module execution, you will want to + # simple AnsibleModule.exit_json(), passing the key/value results + meraki.exit_json(**meraki.result) + + +if __name__ == '__main__': + main() diff --git a/test/integration/targets/meraki_static_route/aliases b/test/integration/targets/meraki_static_route/aliases new file mode 100644 index 00000000000..ad7ccf7ada2 --- /dev/null +++ b/test/integration/targets/meraki_static_route/aliases @@ -0,0 +1 @@ +unsupported diff --git a/test/integration/targets/meraki_static_route/tasks/main.yml b/test/integration/targets/meraki_static_route/tasks/main.yml new file mode 100644 index 00000000000..ea2344fc2df --- /dev/null +++ b/test/integration/targets/meraki_static_route/tasks/main.yml @@ -0,0 +1,169 @@ +# 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 + + - 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 \ No newline at end of file