From a499244a53ae3ebb99b468c1a7dc075eca2c8224 Mon Sep 17 00:00:00 2001 From: Kevin Breit Date: Mon, 18 Feb 2019 18:27:21 -0600 Subject: [PATCH] New module - meraki/meraki_content_filtering (#51223) * Initial commit - Module mostly works but doesn't work for URL categories - Additional tests should be added - Documentation is needed - Module is close to being complete * Improved documentation - Added parameter documentation - Added examples * Lint changes and bug fixes - Fixed content filtering syntax bug per Meraki documentation update * Change test cases so they work * Remove duplicate key * Improve documentation and check mode --- .../meraki/meraki_content_filtering.py | 222 ++++++++++++++++++ .../targets/meraki_content_filtering/aliases | 1 + .../meraki_config_template/aliases | 1 + .../meraki_config_template/tasks/main.yml | 117 +++++++++ .../meraki_content_filtering/tasks/main.yml | 131 +++++++++++ 5 files changed, 472 insertions(+) create mode 100644 lib/ansible/modules/network/meraki/meraki_content_filtering.py create mode 100644 test/integration/targets/meraki_content_filtering/aliases create mode 100644 test/integration/targets/meraki_content_filtering/meraki_config_template/aliases create mode 100644 test/integration/targets/meraki_content_filtering/meraki_config_template/tasks/main.yml create mode 100644 test/integration/targets/meraki_content_filtering/tasks/main.yml diff --git a/lib/ansible/modules/network/meraki/meraki_content_filtering.py b/lib/ansible/modules/network/meraki/meraki_content_filtering.py new file mode 100644 index 0000000000..7676d654a3 --- /dev/null +++ b/lib/ansible/modules/network/meraki/meraki_content_filtering.py @@ -0,0 +1,222 @@ +#!/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_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 + 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] + 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 + +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.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']), + 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']), + ) + + # 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']) + nets = meraki.get_nets(org_id=org_id) + + 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 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 module.check_mode: + meraki.result['data'] = payload + meraki.exit_json(**meraki.result) + if meraki.is_update_required(current, payload): + response = meraki.request(path, method='PUT', payload=json.dumps(payload)) + 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() diff --git a/test/integration/targets/meraki_content_filtering/aliases b/test/integration/targets/meraki_content_filtering/aliases new file mode 100644 index 0000000000..ad7ccf7ada --- /dev/null +++ b/test/integration/targets/meraki_content_filtering/aliases @@ -0,0 +1 @@ +unsupported diff --git a/test/integration/targets/meraki_content_filtering/meraki_config_template/aliases b/test/integration/targets/meraki_content_filtering/meraki_config_template/aliases new file mode 100644 index 0000000000..ad7ccf7ada --- /dev/null +++ b/test/integration/targets/meraki_content_filtering/meraki_config_template/aliases @@ -0,0 +1 @@ +unsupported diff --git a/test/integration/targets/meraki_content_filtering/meraki_config_template/tasks/main.yml b/test/integration/targets/meraki_content_filtering/meraki_config_template/tasks/main.yml new file mode 100644 index 0000000000..4bec7d21de --- /dev/null +++ b/test/integration/targets/meraki_content_filtering/meraki_config_template/tasks/main.yml @@ -0,0 +1,117 @@ +# 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 \ No newline at end of file diff --git a/test/integration/targets/meraki_content_filtering/tasks/main.yml b/test/integration/targets/meraki_content_filtering/tasks/main.yml new file mode 100644 index 0000000000..43d40c04a2 --- /dev/null +++ b/test/integration/targets/meraki_content_filtering/tasks/main.yml @@ -0,0 +1,131 @@ +# 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: 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: 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 + + - debug: + var: single_allowed.data.allowedUrlPatterns + + - assert: + that: + - single_allowed.data.allowedUrlPatterns | 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 + + - 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.blockedUrlPatterns | 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.allowedUrlPatterns | 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_cateogry + + - debug: + var: blocked_cateogry + + - assert: + that: + - blocked_cateogry.changed == True + - blocked_cateogry.data.blockedUrlCategories | length == 1 + - blocked_cateogry.data.urlCategoryListSize == "fullList" + + 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: + - + # blocked_categories: + # -