diff --git a/changelogs/fragments/mx_static_route.yaml b/changelogs/fragments/mx_static_route.yaml new file mode 100644 index 00000000..99718a68 --- /dev/null +++ b/changelogs/fragments/mx_static_route.yaml @@ -0,0 +1,3 @@ +--- +bugfixes: +- meraki_mx_static_route - Add support for gateway_vlan_id otherwise requests could error \ No newline at end of file diff --git a/galaxy.yml b/galaxy.yml index 72d2a6a9..397904c0 100644 --- a/galaxy.yml +++ b/galaxy.yml @@ -22,7 +22,7 @@ build_ignore: - '*tar.gz' - '*.DS_Store' - '*.json' -- 'venv' +- 'venv*' - '.vscode' - '.gitignore' - '.env' diff --git a/plugins/modules/meraki_mx_static_route.py b/plugins/modules/meraki_mx_static_route.py index 927d4230..51aef265 100644 --- a/plugins/modules/meraki_mx_static_route.py +++ b/plugins/modules/meraki_mx_static_route.py @@ -5,15 +5,16 @@ # 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' + "metadata_version": "1.1", + "status": ["preview"], + "supported_by": "community", } -DOCUMENTATION = r''' +DOCUMENTATION = r""" --- module: meraki_mx_static_route short_description: Manage static routes in the Meraki cloud @@ -51,6 +52,10 @@ description: - Unique ID of static route. type: str + gateway_vlan_id: + description: + - The gateway IP (next hop) VLAN ID of the static route. + type: int fixed_ip_assignments: description: - List of fixed MAC to IP bindings for DHCP. @@ -96,9 +101,9 @@ author: - Kevin Breit (@kbreit) extends_documentation_fragment: cisco.meraki.meraki -''' +""" -EXAMPLES = r''' +EXAMPLES = r""" - name: Create static_route meraki_static_route: auth_key: abc123 @@ -139,9 +144,9 @@ net_name: YourNet route_id: '{{item}}' delegate_to: localhost -''' +""" -RETURN = r''' +RETURN = r""" data: description: Information about the created or manipulated object. returned: info @@ -217,34 +222,39 @@ returned: query or update type: str sample: JimLaptop -''' +""" from ansible.module_utils.basic import AnsibleModule, json -from ansible_collections.cisco.meraki.plugins.module_utils.network.meraki.meraki import MerakiModule, meraki_argument_spec +from ansible_collections.cisco.meraki.plugins.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']} + 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') + 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': route_id}) - r = meraki.request(path, method='GET') + path = meraki.construct_path( + "get_one", net_id=net_id, custom={"route_id": route_id} + ) + r = meraki.request(path, method="GET") return r def does_route_exist(name, routes): for route in routes: - if name == route['name']: + if name == route["name"]: return route return None @@ -261,132 +271,168 @@ 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'), - ) + 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'), - ) + 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'), + 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"), + gateway_vlan_id=dict(type="int"), ) # 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, - ) + module = AnsibleModule( + argument_spec=argument_spec, + supports_check_mode=True, + ) - meraki = MerakiModule(module, function='static_route') - module.params['follow_redirects'] = 'all' + meraki = MerakiModule(module, function="static_route") + module.params["follow_redirects"] = "all" payload = None - query_urls = {'static_route': '/networks/{net_id}/appliance/staticRoutes'} - query_one_urls = {'static_route': '/networks/{net_id}/appliance/staticRoutes/{route_id}'} - create_urls = {'static_route': '/networks/{net_id}/appliance/staticRoutes/'} - update_urls = {'static_route': '/networks/{net_id}/appliance/staticRoutes/{route_id}'} - delete_urls = {'static_route': '/networks/{net_id}/appliance/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']: + query_urls = {"static_route": "/networks/{net_id}/appliance/staticRoutes"} + query_one_urls = { + "static_route": "/networks/{net_id}/appliance/staticRoutes/{route_id}" + } + create_urls = {"static_route": "/networks/{net_id}/appliance/staticRoutes/"} + update_urls = { + "static_route": "/networks/{net_id}/appliance/staticRoutes/{route_id}" + } + delete_urls = { + "static_route": "/networks/{net_id}/appliance/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': + if meraki.params["state"] == "present": payload = dict() - if meraki.params['net_name']: - payload['name'] = meraki.params['net_name'] + 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'] + 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'] + 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) + 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']) + 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'] - if meraki.params['enabled'] is not None: - payload['enabled'] = meraki.params['enabled'] - - route_id = meraki.params['route_id'] - if meraki.params['name'] is not None and route_id is None: - route_status = does_route_exist(meraki.params['name'], get_static_routes(meraki, net_id)) + 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"] + if meraki.params["enabled"] is not None: + payload["enabled"] = meraki.params["enabled"] + if meraki.params["gateway_vlan_id"] is not None: + payload["gatewayVlanId"] = meraki.params["gateway_vlan_id"] + + route_id = meraki.params["route_id"] + if meraki.params["name"] is not None and route_id is None: + route_status = does_route_exist( + meraki.params["name"], get_static_routes(meraki, net_id) + ) if route_status is not None: # Route exists, assign route_id - route_id = route_status['id'] + route_id = route_status["id"] if route_id is not None: existing_route = get_static_route(meraki, net_id, route_id) original = existing_route.copy() payload = update_dict(existing_route, payload) if module.check_mode: - meraki.result['data'] = payload + meraki.result["data"] = payload meraki.exit_json(**meraki.result) - if meraki.is_update_required(original, payload, optional_ignore=['id']): - path = meraki.construct_path('update', net_id=net_id, custom={'route_id': route_id}) - meraki.result['data'] = meraki.request(path, method="PUT", payload=json.dumps(payload)) - meraki.result['changed'] = True + if meraki.is_update_required(original, payload, optional_ignore=["id"]): + path = meraki.construct_path( + "update", net_id=net_id, custom={"route_id": route_id} + ) + meraki.result["data"] = meraki.request( + path, method="PUT", payload=json.dumps(payload) + ) + meraki.result["changed"] = True else: - meraki.result['data'] = original + meraki.result["data"] = original else: if module.check_mode: - meraki.result['data'] = payload + 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': + 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 + 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__': +if __name__ == "__main__": main() diff --git a/tests/integration/targets/meraki_static_route/tasks/main.yml b/tests/integration/targets/meraki_static_route/tasks/main.yml index 6313c23c..ed902a3d 100644 --- a/tests/integration/targets/meraki_static_route/tasks/main.yml +++ b/tests/integration/targets/meraki_static_route/tasks/main.yml @@ -15,12 +15,33 @@ delegate_to: localhost register: net + - name: Enable VLANs on network + meraki_mx_network_vlan_settings: + auth_key: '{{ auth_key }}' + state: present + org_name: '{{test_org_name}}' + net_name: IntTestNetwork + vlans_enabled: yes + delegate_to: localhost + - set_fact: net_id: '{{net.data.id}}' - name: Initialize static route id list set_fact: route_ids: [] + + - name: Create VLAN + meraki_mx_vlan: + auth_key: '{{ auth_key }}' + state: present + org_name: '{{test_org_name}}' + net_name: IntTestNetwork + name: "Test VLAN" + vlan_id: 2 + subnet: "192.168.129.0/24" + appliance_ip: "192.168.129.2" + delegate_to: localhost - name: Create static_route meraki_static_route: @@ -82,6 +103,7 @@ route_id: '{{create_route.data.id}}' subnet: 192.0.3.0/24 enabled: yes + gateway_vlan_id: 1 delegate_to: localhost register: update @@ -89,6 +111,7 @@ that: - update is changed - update.data.subnet == "192.0.3.0/24" + - update.data.gateway_vlan_id == 1 - name: Query static routes meraki_static_route: