From d5b3ffc51e6332383680ae3bdf24b5f426165da8 Mon Sep 17 00:00:00 2001 From: Kevin Breit Date: Fri, 25 May 2018 19:22:38 -0500 Subject: [PATCH] New module - meraki_snmp (#39882) * Initial commit for meraki_admin module * Initial commit for meraki_snmp module * Update code to be operational for SNMP settings - Add optional_ignore value to is_update_required for one-time fields - Write documentation - Perform checks and execute changes * Minor fixes and test improvements - Fix some documentation errors - Implement and test for idempotency * Removed meraki_admin which shouldn't be there, ansibot changes * Rename params to be lower case - Updated integration tests - Changed CamelCase to lowercase and underscore * Code cleanup changes based on comments from Dag. --- .../module_utils/network/meraki/meraki.py | 8 +- .../modules/network/meraki/meraki_snmp.py | 224 ++++++++++++++++++ test/integration/targets/meraki_snmp/aliases | 1 + .../targets/meraki_snmp/tasks/main.yml | 149 ++++++++++++ 4 files changed, 379 insertions(+), 3 deletions(-) create mode 100644 lib/ansible/modules/network/meraki/meraki_snmp.py create mode 100644 test/integration/targets/meraki_snmp/aliases create mode 100644 test/integration/targets/meraki_snmp/tasks/main.yml diff --git a/lib/ansible/module_utils/network/meraki/meraki.py b/lib/ansible/module_utils/network/meraki/meraki.py index df767735115..8c88ba1cc28 100644 --- a/lib/ansible/module_utils/network/meraki/meraki.py +++ b/lib/ansible/module_utils/network/meraki/meraki.py @@ -122,16 +122,18 @@ class MerakiModule(object): else: self.params['protocol'] = 'http' - def is_update_required(self, original, proposed): + def is_update_required(self, original, proposed, optional_ignore=None): ''' Compare original and proposed data to see if an update is needed ''' is_changed = False ignored_keys = ('id', 'organizationId') + if not optional_ignore: + optional_ignore = ('') # self.fail_json(msg="Update required check", original=original, proposed=proposed) for k, v in original.items(): try: - if k not in ignored_keys: + if k not in ignored_keys and k not in optional_ignore: if v != proposed[k]: is_changed = True except KeyError: @@ -139,7 +141,7 @@ class MerakiModule(object): is_changed = True for k, v in proposed.items(): try: - if k not in ignored_keys: + if k not in ignored_keys and k not in optional_ignore: if v != original[k]: is_changed = True except KeyError: diff --git a/lib/ansible/modules/network/meraki/meraki_snmp.py b/lib/ansible/modules/network/meraki/meraki_snmp.py new file mode 100644 index 00000000000..bd9e49da334 --- /dev/null +++ b/lib/ansible/modules/network/meraki/meraki_snmp.py @@ -0,0 +1,224 @@ +#!/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_snmp +short_description: Manage organizations in the Meraki cloud +version_added: "2.6" +description: +- Allows for management of SNMP settings for 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: + state: + description: + - Specifies whether SNMP information should be queried or modified. + choices: ['query', 'present'] + default: present + v2c_enabled: + description: + - Specifies whether SNMPv2c is enabled. + type: bool + v3_enabled: + description: + - Specifies whether SNMPv3 is enabled. + type: bool + v3_auth_mode: + description: + - Sets authentication mode for SNMPv3. + choices: ['MD5', 'SHA'] + v3_auth_pass: + description: + - Authentication password for SNMPv3. + - Must be at least 8 characters long. + v3_priv_mode: + description: + - Specifies privacy mode for SNMPv3. + choices: ['DES', 'AES128'] + v3_priv_pass: + description: + - Privacy password for SNMPv3. + - Must be at least 8 characters long. + peer_ips: + description: + - Semi-colon delimited IP addresses which can perform SNMP queries. +author: +- Kevin Breit (@kbreit) +extends_documentation_fragment: meraki +''' + +EXAMPLES = r''' +- name: Query SNMP values + meraki_snmp: + auth_key: abc12345 + org_name: YourOrg + state: query + delegate_to: localhost +''' + +RETURN = r''' +data: + description: Information about queried or updated object. + type: list + returned: info + sample: + "data": { + "hostname": "n110.meraki.com", + "peer_ips": null, + "port": 16100, + "v2c_enabled": false, + "v3_auth_mode": null, + "v3_enabled": false, + "v3_priv_mode": null + } +''' + +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_snmp(meraki, org_id): + path = meraki.construct_path('get_all', org_id=org_id) + r = meraki.request(path, + method='GET', + ) + return json.loads(r) + + +def set_snmp(meraki, org_id): + payload = dict() + if meraki.params['peer_ips']: + if len(meraki.params['peer_ips']) > 7: + if ';' not in meraki.params['peer_ips']: + meraki.fail_json(msg='Peer IP addresses are semi-colon delimited.') + if meraki.params['v2c_enabled'] is not None: + payload = {'v2cEnabled': meraki.params['v2c_enabled'], + } + if meraki.params['v3_enabled'] is not None: + if len(meraki.params['v3_auth_pass']) < 8 or len(meraki.params['v3_priv_pass']) < 8: + meraki.fail_json(msg='v3_auth_pass and v3_priv_pass must both be at least 8 characters long.') + if (meraki.params['v3_auth_mode'] is None or + meraki.params['v3_auth_pass'] is None or + meraki.params['v3_priv_mode'] is None or + meraki.params['v3_priv_pass'] is None): + meraki.fail_json(msg='v3_auth_mode, v3_auth_pass, v3_priv_mode, and v3_auth_pass are required') + payload = {'v3Enabled': meraki.params['v3_enabled'], + 'v3AuthMode': meraki.params['v3_auth_mode'].upper(), + 'v3AuthPass': meraki.params['v3_auth_pass'], + 'v3PrivMode': meraki.params['v3_priv_mode'].upper(), + 'v3PrivPass': meraki.params['v3_priv_pass'], + 'peerIps': meraki.params['peer_ips'], + } + full_compare = {'v2cEnabled': meraki.params['v2c_enabled'], + 'v3Enabled': meraki.params['v3_enabled'], + 'v3AuthMode': meraki.params['v3_auth_mode'], + 'v3PrivMode': meraki.params['v3_priv_mode'], + 'peerIps': meraki.params['peer_ips'], + } + if meraki.params['v3_enabled'] is None: + full_compare['v3Enabled'] = False + if meraki.params['v2c_enabled'] is None: + full_compare['v2cEnabled'] = False + path = meraki.construct_path('create', org_id=org_id) + snmp = get_snmp(meraki, org_id) + ignored_parameters = ('v3AuthPass', 'v3PrivPass', 'hostname', 'port', 'v2CommunityString', 'v3User') + if meraki.is_update_required(snmp, full_compare, optional_ignore=ignored_parameters): + r = meraki.request(path, + method='PUT', + payload=json.dumps(payload)) + meraki.result['changed'] = True + return json.loads(r) + return -1 + + +def main(): + + # define the available arguments/parameters that a user can pass to + # the module + argument_spec = meraki_argument_spec() + argument_spec.update(state=dict(type='str', choices=['present', 'query'], default='present'), + org_name=dict(type='str', aliases=['organization']), + org_id=dict(type='int'), + v2c_enabled=dict(type='bool'), + v3_enabled=dict(type='bool'), + v3_auth_mode=dict(type='str', choices=['SHA', 'MD5']), + v3_auth_pass=dict(type='str', no_log=True), + v3_priv_mode=dict(type='str', choices=['DES', 'AES128']), + v3_priv_pass=dict(type='str', no_log=True), + peer_ips=dict(type='str'), + ) + + # seed the result dict in the object + # we primarily care about changed and state + # change is if this module effectively modified the target + # state will include any data that you want your module to pass back + # for consumption, for example, in a subsequent task + result = dict( + changed=False, + ) + # 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='snmp') + meraki.params['follow_redirects'] = 'all' + + query_urls = {'snmp': '/organizations/{org_id}/snmp', + } + + update_urls = {'snmp': '/organizations/{org_id}/snmp', + } + + meraki.url_catalog['get_all'] = query_urls + meraki.url_catalog['create'] = update_urls + + payload = None + + # 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 + # FIXME: Work with Meraki so they can implement a check mode + if module.check_mode: + meraki.exit_json(**meraki.result) + + # execute checks for argument completeness + + # 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 org_id: + org_id = meraki.get_org_id(meraki.params['org_name']) + + if meraki.params['state'] == 'query': + meraki.result['data'] = get_snmp(meraki, org_id) + elif meraki.params['state'] == 'present': + meraki.result['data'] = set_snmp(meraki, org_id) + + # 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_snmp/aliases b/test/integration/targets/meraki_snmp/aliases new file mode 100644 index 00000000000..ad7ccf7ada2 --- /dev/null +++ b/test/integration/targets/meraki_snmp/aliases @@ -0,0 +1 @@ +unsupported diff --git a/test/integration/targets/meraki_snmp/tasks/main.yml b/test/integration/targets/meraki_snmp/tasks/main.yml new file mode 100644 index 00000000000..8e300a256d4 --- /dev/null +++ b/test/integration/targets/meraki_snmp/tasks/main.yml @@ -0,0 +1,149 @@ +# 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) +--- +- name: Test an API key is provided + fail: + msg: Please define an API key + when: auth_key is not defined + +- name: Query all SNMP settings + meraki_snmp: + auth_key: '{{auth_key}}' + org_name: '{{test_org_name}}' + state: query + delegate_to: localhost + register: snmp_query + +- debug: + msg: '{{snmp_query}}' + +- name: Enable SNMPv2c + meraki_snmp: + auth_key: '{{auth_key}}' + org_name: '{{test_org_name}}' + state: present + v2c_enabled: true + delegate_to: localhost + register: snmp_v2_enable + +- debug: + msg: '{{snmp_v2_enable}}' + +- assert: + that: + - snmp_v2_enable.data.v2CommunityString is defined + - snmp_v2_enable.data.v2cEnabled == true + +- name: Disable SNMPv2c + meraki_snmp: + auth_key: '{{auth_key}}' + org_name: '{{test_org_name}}' + state: present + v2c_enabled: False + delegate_to: localhost + register: snmp_v2_disable + +# - debug: +# msg: '{{snmp_v2_disable}}' + +- assert: + that: + - snmp_v2_disable.data.v2CommunityString is not defined + - snmp_v2_disable.data.v2cEnabled == False + +- name: Enable SNMPv3 + meraki_snmp: + auth_key: '{{auth_key}}' + org_name: '{{test_org_name}}' + state: present + v3_enabled: true + v3_auth_mode: SHA + v3_auth_pass: ansiblepass + v3_priv_mode: AES128 + v3_priv_pass: ansiblepass + delegate_to: localhost + register: snmp_v3_enable + +- assert: + that: + - snmp_v3_enable.data.v3Enabled == True + - snmp_v3_enable.changed == True + +- name: Check for idempotency + meraki_snmp: + auth_key: '{{auth_key}}' + org_name: '{{test_org_name}}' + state: present + v3_enabled: true + v3_auth_mode: SHA + v3_auth_pass: ansiblepass + v3_priv_mode: AES128 + v3_priv_pass: ansiblepass + delegate_to: localhost + register: snmp_idempotent + +- debug: + msg: '{{snmp_idempotent}}' + +- assert: + that: + - snmp_idempotent.changed == False + +- name: Add peer IPs + meraki_snmp: + auth_key: '{{auth_key}}' + org_name: '{{test_org_name}}' + state: present + v3_enabled: true + v3_auth_mode: SHA + v3_auth_pass: ansiblepass + v3_priv_mode: AES128 + v3_priv_pass: ansiblepass + peer_ips: 1.1.1.1;2.2.2.2 + delegate_to: localhost + register: peers + +- debug: + msg: '{{peers}}' + +- assert: + that: + - peers.data.peerIps is defined + +- name: Add invalid peer IPs + meraki_snmp: + auth_key: '{{auth_key}}' + org_name: '{{test_org_name}}' + state: present + peer_ips: 1.1.1.1 2.2.2.2 + delegate_to: localhost + register: invalid_peers + ignore_errors: yes + +- assert: + that: + '"Peer IP addresses are semi-colon delimited." in invalid_peers.msg' + +- name: Set short password + meraki_snmp: + auth_key: '{{auth_key}}' + org_name: '{{test_org_name}}' + state: present + v3_enabled: true + v3_auth_mode: SHA + v3_auth_pass: ansible + v3_priv_mode: AES128 + v3_priv_pass: ansible + peer_ips: 1.1.1.1;2.2.2.2 + delegate_to: localhost + register: short_password + ignore_errors: yes + +- debug: + msg: '{{short_password}}' + +- assert: + that: + - '"at least 8" in short_password.msg'