diff --git a/lib/ansible/modules/storage/netapp/na_elementsw_cluster_snmp.py b/lib/ansible/modules/storage/netapp/na_elementsw_cluster_snmp.py new file mode 100644 index 0000000000..a97e053125 --- /dev/null +++ b/lib/ansible/modules/storage/netapp/na_elementsw_cluster_snmp.py @@ -0,0 +1,354 @@ +#!/usr/bin/python +# (c) 2019, NetApp, Inc +# GNU General Public License v3.0+ (see COPYING or +# https://www.gnu.org/licenses/gpl-3.0.txt) + +''' +Element Software Configure SNMP +''' +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +ANSIBLE_METADATA = {'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'community'} + + +DOCUMENTATION = ''' + +module: na_elementsw_cluster_snmp + +short_description: Configure Element SW Cluster SNMP +extends_documentation_fragment: + - netapp.solidfire +version_added: '2.8' +author: NetApp Ansible Team (@carchi8py) +description: +- Configure Element Software cluster SNMP. + +options: + + state: + description: + - This module enables you to enable SNMP on cluster nodes. When you enable SNMP, \ + the action applies to all nodes in the cluster, and the values that are passed replace, \ + in whole, all values set in any previous call to this module. + choices: ['present', 'absent'] + default: 'present' + + snmp_v3_enabled: + description: + - Which version of SNMP has to be enabled. + type: bool + + networks: + description: + - List of networks and what type of access they have to the SNMP servers running on the cluster nodes. + - This parameter is required if SNMP v3 is disabled. + suboptions: + access: + description: + - ro for read-only access. + - rw for read-write access. + - rosys for read-only access to a restricted set of system information. + choices: ['ro', 'rw', 'rosys'] + cidr: + description: + - A CIDR network mask. This network mask must be an integer greater than or equal to 0, \ + and less than or equal to 32. It must also not be equal to 31. + community: + description: + - SNMP community string. + network: + description: + - This parameter along with the cidr variable is used to control which network the access and \ + community string apply to. + - The special value of 'default' is used to specify an entry that applies to all networks. + - The cidr mask is ignored when network value is either a host name or default. + + usm_users: + description: + - List of users and the type of access they have to the SNMP servers running on the cluster nodes. + - This parameter is required if SNMP v3 is enabled. + suboptions: + access: + description: + - rouser for read-only access. + - rwuser for read-write access. + - rosys for read-only access to a restricted set of system information. + choices: ['rouser', 'rwuser', 'rosys'] + name: + description: + - The name of the user. Must contain at least one character, but no more than 32 characters. + - Blank spaces are not allowed. + password: + description: + - The password of the user. Must be between 8 and 255 characters long (inclusive). + - Blank spaces are not allowed. + - Required if 'secLevel' is 'auth' or 'priv.' + passphrase: + description: + - The passphrase of the user. Must be between 8 and 255 characters long (inclusive). + - Blank spaces are not allowed. + - Required if 'secLevel' is 'priv.' + secLevel: + description: + - To define the security level of a user. + choices: ['noauth', 'auth', 'priv'] + +''' + +EXAMPLES = """ + + - name: configure SnmpNetwork + tags: + - elementsw_cluster_snmp + na_elementsw_cluster_snmp: + hostname: "{{ elementsw_hostname }}" + username: "{{ elementsw_username }}" + password: "{{ elementsw_password }}" + state: present + snmp_v3_enabled: True + usm_users: + access: rouser + name: testuser + password: ChangeMe123 + passphrase: ChangeMe123 + secLevel: auth + networks: + access: ro + cidr: 24 + community: TestNetwork + network: 192.168.0.1 + + - name: Disable SnmpNetwork + tags: + - elementsw_cluster_snmp + na_elementsw_cluster_snmp: + hostname: "{{ elementsw_hostname }}" + username: "{{ elementsw_username }}" + password: "{{ elementsw_password }}" + state: absent + +""" + +RETURN = """ + +msg: + description: Success message + returned: success + type: str + +""" + +import traceback + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils._text import to_native +import ansible.module_utils.netapp as netapp_utils +from ansible.module_utils.netapp_elementsw_module import NaElementSWModule +from ansible.module_utils.netapp_module import NetAppModule + +HAS_SF_SDK = netapp_utils.has_sf_sdk() + + +class ElementSWClusterSnmp(object): + """ + Element Software Configure Element SW Cluster SnmpNetwork + """ + def __init__(self): + self.argument_spec = netapp_utils.ontap_sf_host_argument_spec() + + self.argument_spec.update(dict( + state=dict(type='str', choices=['present', 'absent'], default='present'), + snmp_v3_enabled=dict(type='bool'), + networks=dict( + type='dict', + options=dict( + access=dict(type='str', choices=['ro', 'rw', 'rosys']), + cidr=dict(type='int', default=None), + community=dict(type='str', default=None), + network=dict(type='str', default=None) + ) + ), + usm_users=dict( + type='dict', + options=dict( + access=dict(type='str', choices=['rouser', 'rwuser', 'rosys']), + name=dict(type='str', default=None), + password=dict(type='str', default=None), + passphrase=dict(type='str', default=None), + secLevel=dict(type='str', choices=['auth', 'noauth', 'priv']) + ) + ), + )) + + self.module = AnsibleModule( + argument_spec=self.argument_spec, + required_if=[ + ('state', 'present', ['snmp_v3_enabled']), + ('snmp_v3_enabled', True, ['usm_users']), + ('snmp_v3_enabled', False, ['networks']) + ], + supports_check_mode=True + ) + + self.na_helper = NetAppModule() + self.parameters = self.na_helper.set_parameters(self.module.params) + + if self.parameters.get('state') == "present": + if self.parameters.get('usm_users') is not None: + # Getting the configuration details to configure SNMP Version3 + self.access_usm = self.parameters.get('usm_users')['access'] + self.name = self.parameters.get('usm_users')['name'] + self.password = self.parameters.get('usm_users')['password'] + self.passphrase = self.parameters.get('usm_users')['passphrase'] + self.secLevel = self.parameters.get('usm_users')['secLevel'] + if self.parameters.get('networks') is not None: + # Getting the configuration details to configure SNMP Version2 + self.access_network = self.parameters.get('networks')['access'] + self.cidr = self.parameters.get('networks')['cidr'] + self.community = self.parameters.get('networks')['community'] + self.network = self.parameters.get('networks')['network'] + + if HAS_SF_SDK is False: + self.module.fail_json(msg="Unable to import the SolidFire Python SDK") + else: + self.sfe = netapp_utils.create_sf_connection(module=self.module) + + def enable_snmp(self): + """ + enable snmp feature + """ + try: + self.sfe.enable_snmp(snmp_v3_enabled=self.parameters.get('snmp_v3_enabled')) + except Exception as exception_object: + self.module.fail_json(msg='Error enabling snmp feature %s' % to_native(exception_object), + exception=traceback.format_exc()) + + def disable_snmp(self): + """ + disable snmp feature + """ + try: + self.sfe.disable_snmp() + except Exception as exception_object: + self.module.fail_json(msg='Error disabling snmp feature %s' % to_native(exception_object), + exception=traceback.format_exc()) + + def configure_snmp(self, actual_networks, actual_usm_users): + """ + Configure snmp + """ + try: + self.sfe.set_snmp_acl(networks=[actual_networks], usm_users=[actual_usm_users]) + + except Exception as exception_object: + self.module.fail_json(msg='Error Configuring snmp feature %s' % to_native(exception_object.message), + exception=traceback.format_exc()) + + def apply(self): + """ + Cluster SNMP configuration + """ + changed = False + result_message = None + update_required = False + version_change = False + is_snmp_enabled = self.sfe.get_snmp_state().enabled + + if is_snmp_enabled is True: + # IF SNMP is already enabled + if self.parameters.get('state') == 'absent': + # Checking for state change(s) here, and applying it later in the code allows us to support + # check_mode + changed = True + + elif self.parameters.get('state') == 'present': + # Checking if SNMP configuration needs to be updated, + is_snmp_v3_enabled = self.sfe.get_snmp_state().snmp_v3_enabled + + if is_snmp_v3_enabled != self.parameters.get('snmp_v3_enabled'): + # Checking if there any version changes required + version_change = True + changed = True + + if is_snmp_v3_enabled is True: + # Checking If snmp configuration for usm_users needs modification + if len(self.sfe.get_snmp_info().usm_users) == 0: + # If snmp is getting configured for first time + update_required = True + changed = True + else: + for usm_user in self.sfe.get_snmp_info().usm_users: + if usm_user.access != self.access_usm or usm_user.name != self.name or usm_user.password != self.password or \ + usm_user.passphrase != self.passphrase or usm_user.sec_level != self.secLevel: + update_required = True + changed = True + else: + # Checking If snmp configuration for networks needs modification + for snmp_network in self.sfe.get_snmp_info().networks: + if snmp_network.access != self.access_network or snmp_network.cidr != self.cidr or \ + snmp_network.community != self.community or snmp_network.network != self.network: + update_required = True + changed = True + + else: + if self.parameters.get('state') == 'present': + changed = True + + result_message = "" + + if changed: + if self.module.check_mode is True: + result_message = "Check mode, skipping changes" + + else: + if self.parameters.get('state') == "present": + # IF snmp is not enabled, then enable and configure snmp + if self.parameters.get('snmp_v3_enabled') is True: + # IF SNMP is enabled with version 3 + usm_users = {'access': self.access_usm, + 'name': self.name, + 'password': self.password, + 'passphrase': self.passphrase, + 'secLevel': self.secLevel} + networks = None + else: + # IF SNMP is enabled with version 2 + usm_users = None + networks = {'access': self.access_network, + 'cidr': self.cidr, + 'community': self.community, + 'network': self.network} + + if is_snmp_enabled is False or version_change is True: + # Enable and configure snmp + self.enable_snmp() + self.configure_snmp(networks, usm_users) + result_message = "SNMP is enabled and configured" + + elif update_required is True: + # If snmp is already enabled, update the configuraiton if required + self.configure_snmp(networks, usm_users) + result_message = "SNMP is configured" + + elif is_snmp_enabled is True and self.parameters.get('state') == "absent": + # If snmp is enabled and state is absent, disable snmp + self.disable_snmp() + result_message = "SNMP is disabled" + + self.module.exit_json(changed=changed, msg=result_message) + + +def main(): + """ + Main function + """ + na_elementsw_cluster_snmp = ElementSWClusterSnmp() + na_elementsw_cluster_snmp.apply() + + +if __name__ == '__main__': + main() diff --git a/test/units/modules/storage/netapp/test_na_elementsw_cluster_snmp.py b/test/units/modules/storage/netapp/test_na_elementsw_cluster_snmp.py new file mode 100644 index 0000000000..50f90a6cec --- /dev/null +++ b/test/units/modules/storage/netapp/test_na_elementsw_cluster_snmp.py @@ -0,0 +1,178 @@ +# (c) 2019, NetApp, Inc +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +''' unit test for Ansible module: na_elementsw_cluster_snmp.py ''' + +from __future__ import print_function +import json +import pytest + +from units.compat import unittest +from units.compat.mock import patch +from ansible.module_utils import basic +from ansible.module_utils._text import to_bytes +import ansible.module_utils.netapp as netapp_utils + +if not netapp_utils.has_sf_sdk(): + pytestmark = pytest.mark.skip('skipping as missing required SolidFire Python SDK') + +from ansible.modules.storage.netapp.na_elementsw_cluster_snmp \ + import ElementSWClusterSnmp as my_module # module under test + + +def set_module_args(args): + """prepare arguments so that they will be picked up during module creation""" + args = json.dumps({'ANSIBLE_MODULE_ARGS': args}) + basic._ANSIBLE_ARGS = to_bytes(args) # pylint: disable=protected-access + + +class AnsibleExitJson(Exception): + """Exception class to be raised by module.exit_json and caught by the test case""" + pass + + +class AnsibleFailJson(Exception): + """Exception class to be raised by module.fail_json and caught by the test case""" + pass + + +def exit_json(*args, **kwargs): # pylint: disable=unused-argument + """function to patch over exit_json; package return data into an exception""" + if 'changed' not in kwargs: + kwargs['changed'] = False + raise AnsibleExitJson(kwargs) + + +def fail_json(*args, **kwargs): # pylint: disable=unused-argument + """function to patch over fail_json; package return data into an exception""" + kwargs['failed'] = True + raise AnsibleFailJson(kwargs) + + +GET_ERROR = 'some_error_in_get_snmp_info' + + +class MockSFConnection(object): + ''' mock connection to ElementSW host ''' + + class Bunch(object): # pylint: disable=too-few-public-methods + ''' create object with arbitrary attributes ''' + def __init__(self, **kw): + ''' called with (k1=v1, k2=v2), creates obj.k1, obj.k2 with values v1, v2 ''' + setattr(self, '__dict__', kw) + + def __init__(self, force_error=False, where=None): + ''' save arguments ''' + self.force_error = force_error + self.where = where + + +class TestMyModule(unittest.TestCase): + ''' a group of related Unit Tests ''' + + def setUp(self): + self.mock_module_helper = patch.multiple(basic.AnsibleModule, + exit_json=exit_json, + fail_json=fail_json) + self.mock_module_helper.start() + self.addCleanup(self.mock_module_helper.stop) + + def set_default_args(self): + return dict({ + 'hostname': '10.117.78.131', + 'username': 'admin', + 'password': 'netapp1!', + }) + + @patch('ansible.module_utils.netapp.create_sf_connection') + def test_module_fail_when_required_args_missing(self, mock_create_sf_connection): + ''' required arguments are reported as errors ''' + with pytest.raises(AnsibleFailJson) as exc: + set_module_args({}) + my_module() + print('Info: %s' % exc.value) + + @patch('ansible.module_utils.netapp.create_sf_connection') + def test_ensure_enable_snmp_called(self, mock_create_sf_connection): + ''' test if enable_snmp is called ''' + module_args = {} + module_args.update(self.set_default_args()) + module_args.update({'snmp_v3_enabled': True, + 'state': 'present'}) + module_args.update({'usm_users': {'access': 'rouser', + 'name': 'TestUser', + 'password': 'ChangeMe@123', + 'passphrase': 'ChangeMe@123', + 'secLevel': 'auth', }}) + + module_args.update({'networks': {'access': 'ro', + 'cidr': 24, + 'community': 'TestNetwork', + 'network': '192.168.0.1', }}) + set_module_args(module_args) + my_obj = my_module() + with pytest.raises(AnsibleExitJson) as exc: + my_obj.apply() + print('Info: test_if_enable_snmp_called: %s' % repr(exc.value)) + assert exc.value + + @patch('ansible.module_utils.netapp.create_sf_connection') + def test_ensure_configure_snmp_from_version_3_TO_version_2_called(self, mock_create_sf_connection): + ''' test if configure snmp from version_3 to version_2''' + module_args = {} + module_args.update(self.set_default_args()) + module_args.update({'snmp_v3_enabled': False, + 'state': 'present'}) + module_args.update({'usm_users': {'access': 'rouser', + 'name': 'TestUser', + 'password': 'ChangeMe@123', + 'passphrase': 'ChangeMe@123', + 'secLevel': 'auth', }}) + + module_args.update({'networks': {'access': 'ro', + 'cidr': 24, + 'community': 'TestNetwork', + 'network': '192.168.0.1', }}) + set_module_args(module_args) + my_obj = my_module() + with pytest.raises(AnsibleExitJson) as exc: + my_obj.apply() + print('Info: test_ensure_configure_snmp_from_version_3_TO_version_2_called: %s' % repr(exc.value)) + assert exc.value + + @patch('ansible.module_utils.netapp.create_sf_connection') + def test_ensure_configure_snmp_from_version_2_TO_version_3_called(self, mock_create_sf_connection): + ''' test if configure snmp from version_2 to version_3''' + module_args = {} + module_args.update(self.set_default_args()) + module_args.update({'snmp_v3_enabled': True, + 'state': 'present'}) + module_args.update({'usm_users': {'access': 'rouser', + 'name': 'TestUser_sample', + 'password': 'ChangeMe@123', + 'passphrase': 'ChangeMe@123', + 'secLevel': 'auth', }}) + + module_args.update({'networks': {'access': 'ro', + 'cidr': 24, + 'community': 'TestNetwork', + 'network': '192.168.0.1', }}) + set_module_args(module_args) + my_obj = my_module() + with pytest.raises(AnsibleExitJson) as exc: + my_obj.apply() + print('Info: test_ensure_configure_snmp_from_version_2_TO_version_3_called: %s' % repr(exc.value)) + assert exc.value + + @patch('ansible.module_utils.netapp.create_sf_connection') + def test_ensure_disable_snmp_called(self, mock_create_sf_connection): + ''' test if disable_snmp is called ''' + module_args = {} + module_args.update(self.set_default_args()) + module_args.update({'state': 'absent'}) + set_module_args(module_args) + my_obj = my_module() + with pytest.raises(AnsibleExitJson) as exc: + my_obj.apply() + print('Info: test_if_disable_snmp_called: %s' % repr(exc.value)) + assert exc.value