From db783eb2b9f38b54df30db3a24adcd46d5a55a4a Mon Sep 17 00:00:00 2001 From: Kevin Breit Date: Fri, 26 Jul 2019 10:39:20 -0500 Subject: [PATCH] New module - meraki_firewalled_services (#57289) * Initial commit for module - Module can query or modify network services on Meraki gear - Check mode supported - Integration tests included * Small change to examples * Move mutual exclusive to build in approach * Sanity err * Split integration tests into two files to avoid delegate_to * Add the integration tests, woops --- .../meraki/meraki_firewalled_services.py | 241 ++++++++++++++++++ .../meraki_firewalled_services/aliases | 1 + .../meraki_firewalled_services/tasks/main.yml | 7 + .../tasks/tests.yml | 196 ++++++++++++++ 4 files changed, 445 insertions(+) create mode 100644 lib/ansible/modules/network/meraki/meraki_firewalled_services.py create mode 100644 test/integration/targets/meraki_firewalled_services/aliases create mode 100644 test/integration/targets/meraki_firewalled_services/tasks/main.yml create mode 100644 test/integration/targets/meraki_firewalled_services/tasks/tests.yml diff --git a/lib/ansible/modules/network/meraki/meraki_firewalled_services.py b/lib/ansible/modules/network/meraki/meraki_firewalled_services.py new file mode 100644 index 00000000000..57305bdf674 --- /dev/null +++ b/lib/ansible/modules/network/meraki/meraki_firewalled_services.py @@ -0,0 +1,241 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: (c) 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_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 +''' + +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 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', element='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 = None + 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() diff --git a/test/integration/targets/meraki_firewalled_services/aliases b/test/integration/targets/meraki_firewalled_services/aliases new file mode 100644 index 00000000000..ad7ccf7ada2 --- /dev/null +++ b/test/integration/targets/meraki_firewalled_services/aliases @@ -0,0 +1 @@ +unsupported diff --git a/test/integration/targets/meraki_firewalled_services/tasks/main.yml b/test/integration/targets/meraki_firewalled_services/tasks/main.yml new file mode 100644 index 00000000000..60aa04a9635 --- /dev/null +++ b/test/integration/targets/meraki_firewalled_services/tasks/main.yml @@ -0,0 +1,7 @@ +# 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 diff --git a/test/integration/targets/meraki_firewalled_services/tasks/tests.yml b/test/integration/targets/meraki_firewalled_services/tasks/tests.yml new file mode 100644 index 00000000000..ec23edf2210 --- /dev/null +++ b/test/integration/targets/meraki_firewalled_services/tasks/tests.yml @@ -0,0 +1,196 @@ +# 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