From 1da29c8ebefda1ef2352bd38c7f9113922c92005 Mon Sep 17 00:00:00 2001 From: Chris Archibald Date: Tue, 19 Feb 2019 06:54:49 -0800 Subject: [PATCH] Update user roles to NetAppModule format (#50992) * changes to clusteR * Revert "changes to clusteR" This reverts commit 33ee1b71e4bc8435fb315762a871f8c4cb6c5f80. * Rewrite of user roles * Rewrite of user roles * Fix author tag * update * fix author --- .../storage/netapp/na_ontap_user_role.py | 146 +++++++---- .../storage/netapp/test_na_ontap_user_role.py | 238 ++++++++++++++++++ 2 files changed, 338 insertions(+), 46 deletions(-) create mode 100644 test/units/modules/storage/netapp/test_na_ontap_user_role.py diff --git a/lib/ansible/modules/storage/netapp/na_ontap_user_role.py b/lib/ansible/modules/storage/netapp/na_ontap_user_role.py index 0a48acffc7..167e78529d 100644 --- a/lib/ansible/modules/storage/netapp/na_ontap_user_role.py +++ b/lib/ansible/modules/storage/netapp/na_ontap_user_role.py @@ -1,6 +1,6 @@ #!/usr/bin/python -# (c) 2018, NetApp, Inc +# (c) 2018-2019, NetApp, Inc # 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 @@ -49,6 +49,12 @@ options: choices: ['none', 'readonly', 'all'] default: all + query: + description: + - A query for the role. The query must apply to the specified command or directory name. + - Use double quotes "" for modifying a existing query to none. + version_added: '2.8' + vserver: description: - The name of the vserver to use. @@ -62,8 +68,21 @@ EXAMPLES = """ na_ontap_user_role: state: present name: ansibleRole - command_directory_name: DEFAULT + command_directory_name: volume access_level: none + query: show + vserver: ansibleVServer + hostname: "{{ netapp_hostname }}" + username: "{{ netapp_username }}" + password: "{{ netapp_password }}" + + - name: Modify User Role + na_ontap_user_role: + state: present + name: ansibleRole + command_directory_name: volume + access_level: none + query: "" vserver: ansibleVServer hostname: "{{ netapp_hostname }}" username: "{{ netapp_username }}" @@ -78,6 +97,7 @@ import traceback from ansible.module_utils.basic import AnsibleModule from ansible.module_utils._text import to_native +from ansible.module_utils.netapp_module import NetAppModule import ansible.module_utils.netapp as netapp_utils @@ -95,24 +115,20 @@ class NetAppOntapUserRole(object): access_level=dict(required=False, type='str', default='all', choices=['none', 'readonly', 'all']), vserver=dict(required=True, type='str'), + query=dict(required=False, type='str') )) self.module = AnsibleModule( argument_spec=self.argument_spec, supports_check_mode=True ) - parameters = self.module.params - # set up state variables - self.state = parameters['state'] - self.name = parameters['name'] - self.command_directory_name = parameters['command_directory_name'] - self.access_level = parameters['access_level'] - self.vserver = parameters['vserver'] + self.na_helper = NetAppModule() + self.parameters = self.na_helper.set_parameters(self.module.params) if HAS_NETAPP_LIB is False: self.module.fail_json(msg="the python NetApp-Lib module is required") else: - self.server = netapp_utils.setup_na_ontap_zapi(module=self.module) + self.server = netapp_utils.setup_na_ontap_zapi(module=self.module, vserver=self.parameters['vserver']) def get_role(self): """ @@ -123,14 +139,14 @@ class NetAppOntapUserRole(object): False if role is not found :rtype: bool """ + options = {'vserver': self.parameters['vserver'], + 'role-name': self.parameters['name'], + 'command-directory-name': self.parameters['command_directory_name']} security_login_role_get_iter = netapp_utils.zapi.NaElement( 'security-login-role-get-iter') query_details = netapp_utils.zapi.NaElement.create_node_with_children( - 'security-login-role-info', **{'vserver': self.vserver, - 'role-name': self.name, - 'command-directory-name': - self.command_directory_name}) + 'security-login-role-info', **options) query = netapp_utils.zapi.NaElement('query') query.add_child_elem(query_details) security_login_role_get_iter.add_child_elem(query) @@ -141,68 +157,106 @@ class NetAppOntapUserRole(object): except netapp_utils.zapi.NaApiError as e: # Error 16031 denotes a role not being found. if to_native(e.code) == "16031": - return False + return None # Error 16039 denotes command directory not found. elif to_native(e.code) == "16039": - return False + return None else: self.module.fail_json(msg='Error getting role %s: %s' % (self.name, to_native(e)), exception=traceback.format_exc()) if (result.get_child_by_name('num-records') and int(result.get_child_content('num-records')) >= 1): - return True - return False + role_info = result.get_child_by_name('attributes-list').get_child_by_name('security-login-role-info') + result = { + 'name': role_info['role-name'], + 'access_level': role_info['access-level'], + 'command_directory_name': role_info['command-directory-name'], + 'query': role_info['role-query'] + } + return result + + return None def create_role(self): - role_create = netapp_utils.zapi.NaElement.create_node_with_children( - 'security-login-role-create', **{'vserver': self.vserver, - 'role-name': self.name, - 'command-directory-name': - self.command_directory_name, - 'access-level': - self.access_level}) + options = {'vserver': self.parameters['vserver'], + 'role-name': self.parameters['name'], + 'command-directory-name': self.parameters['command_directory_name'], + 'access-level': self.parameters['access_level']} + if self.parameters.get('query'): + options['role-query'] = self.parameters['query'] + role_create = netapp_utils.zapi.NaElement.create_node_with_children('security-login-role-create', **options) + try: self.server.invoke_successfully(role_create, enable_tunneling=False) except netapp_utils.zapi.NaApiError as error: - self.module.fail_json(msg='Error creating role %s: %s' % (self.name, to_native(error)), + self.module.fail_json(msg='Error creating role %s: %s' % (self.parameters['name'], to_native(error)), exception=traceback.format_exc()) def delete_role(self): role_delete = netapp_utils.zapi.NaElement.create_node_with_children( - 'security-login-role-delete', **{'vserver': self.vserver, - 'role-name': self.name, + 'security-login-role-delete', **{'vserver': self.parameters['vserver'], + 'role-name': self.parameters['name'], 'command-directory-name': - self.command_directory_name}) + self.parameters['command_directory_name']}) try: self.server.invoke_successfully(role_delete, enable_tunneling=False) except netapp_utils.zapi.NaApiError as error: - self.module.fail_json(msg='Error removing role %s: %s' % (self.name, to_native(error)), + self.module.fail_json(msg='Error removing role %s: %s' % (self.parameters['name'], to_native(error)), + exception=traceback.format_exc()) + + def modify_role(self, modify): + options = {'vserver': self.parameters['vserver'], + 'role-name': self.parameters['name'], + 'command-directory-name': self.parameters['command_directory_name']} + if 'access_level' in modify.keys(): + options['access-level'] = self.parameters['access_level'] + if 'query' in modify.keys(): + options['role-query'] = self.parameters['query'] + + role_modify = netapp_utils.zapi.NaElement.create_node_with_children('security-login-role-modify', **options) + + try: + self.server.invoke_successfully(role_modify, + enable_tunneling=False) + except netapp_utils.zapi.NaApiError as error: + self.module.fail_json(msg='Error modifying role %s: %s' % (self.parameters['name'], to_native(error)), exception=traceback.format_exc()) def apply(self): - changed = False - vserver = netapp_utils.setup_na_ontap_zapi(module=self.module, vserver=self.vserver) - netapp_utils.ems_log_event("na_ontap_user_role", vserver) - role_exists = self.get_role() - if role_exists: - if self.state == 'absent': - changed = True - else: - if self.state == 'present': - changed = True - if changed: + self.asup_log_for_cserver('na_ontap_user_role') + current = self.get_role() + cd_action = self.na_helper.get_cd_action(current, self.parameters) + + # if desired state specify empty quote query and current query is None, set desired query to None. + # otherwise na_helper.get_modified_attributes will detect a change. + if self.parameters.get('query') == '' and current is not None: + if current['query'] is None: + self.parameters['query'] = None + + modify = self.na_helper.get_modified_attributes(current, self.parameters) + if self.na_helper.changed: if self.module.check_mode: pass else: - if self.state == 'present': - if not role_exists: - self.create_role() - elif self.state == 'absent': + if cd_action == 'create': + self.create_role() + elif cd_action == 'delete': self.delete_role() - self.module.exit_json(changed=changed) + elif modify: + self.modify_role(modify) + self.module.exit_json(changed=self.na_helper.changed) + + def asup_log_for_cserver(self, event_name): + """ + Fetch admin vserver for the given cluster + Create and Autosupport log event with the given module name + :param event_name: Name of the event log + :return: None + """ + netapp_utils.ems_log_event(event_name, self.server) def main(): diff --git a/test/units/modules/storage/netapp/test_na_ontap_user_role.py b/test/units/modules/storage/netapp/test_na_ontap_user_role.py new file mode 100644 index 0000000000..bb3dfd615e --- /dev/null +++ b/test/units/modules/storage/netapp/test_na_ontap_user_role.py @@ -0,0 +1,238 @@ +# (c) 2018, NetApp, Inc +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +''' unit test template for ONTAP Ansible module ''' + +from __future__ import print_function +import json +import pytest + +from units.compat import unittest +from units.compat.mock import patch, Mock +from ansible.module_utils import basic +from ansible.module_utils._text import to_bytes +import ansible.module_utils.netapp as netapp_utils + +from ansible.modules.storage.netapp.na_ontap_user_role \ + import NetAppOntapUserRole as role_module # module under test + +if not netapp_utils.has_netapp_lib(): + pytestmark = pytest.mark.skip('skipping as missing required netapp_lib') + + +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) + + +class MockONTAPConnection(object): + ''' mock server connection to ONTAP host ''' + + def __init__(self, kind=None, data=None): + ''' save arguments ''' + self.kind = kind + self.params = data + self.xml_in = None + self.xml_out = None + + def invoke_successfully(self, xml, enable_tunneling): # pylint: disable=unused-argument + ''' mock invoke_successfully returning xml data ''' + self.xml_in = xml + if self.kind == 'role': + xml = self.build_role_info(self.params) + if self.kind == 'error': + error = netapp_utils.zapi.NaApiError('test', 'error') + raise error + self.xml_out = xml + return xml + + @staticmethod + def build_role_info(vol_details): + ''' build xml data for role-attributes ''' + xml = netapp_utils.zapi.NaElement('xml') + attributes = { + 'num-records': 1, + 'attributes-list': { + 'security-login-role-info': { + 'access-level': 'all', + 'command-directory-name': 'volume', + 'role-name': 'testrole', + 'role-query': 'show', + 'vserver': 'ansible' + } + } + } + xml.translate_struct(attributes) + return xml + + +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) + self.mock_role = { + 'name': 'testrole', + 'access_level': 'all', + 'command_directory_name': 'volume', + 'vserver': 'ansible' + } + + def mock_args(self): + return { + 'name': self.mock_role['name'], + 'vserver': self.mock_role['vserver'], + 'command_directory_name': self.mock_role['command_directory_name'], + 'hostname': 'test', + 'username': 'test_user', + 'password': 'test_pass!', + 'https': 'False' + } + + def get_role_mock_object(self, kind=None): + """ + Helper method to return an na_ontap_user_role object + :param kind: passes this param to MockONTAPConnection() + :return: na_ontap_user_role object + """ + role_obj = role_module() + role_obj.asup_log_for_cserver = Mock(return_value=None) + role_obj.cluster = Mock() + role_obj.cluster.invoke_successfully = Mock() + if kind is None: + role_obj.server = MockONTAPConnection() + else: + role_obj.server = MockONTAPConnection(kind=kind, data=self.mock_role) + return role_obj + + def test_module_fail_when_required_args_missing(self): + ''' required arguments are reported as errors ''' + with pytest.raises(AnsibleFailJson) as exc: + set_module_args({}) + role_module() + print('Info: %s' % exc.value.args[0]['msg']) + + def test_get_nonexistent_policy(self): + ''' Test if get_role returns None for non-existent role ''' + set_module_args(self.mock_args()) + result = self.get_role_mock_object().get_role() + assert result is None + + def test_get_existing_role(self): + ''' Test if get_role returns details for existing role ''' + set_module_args(self.mock_args()) + result = self.get_role_mock_object('role').get_role() + assert result['name'] == self.mock_role['name'] + + def test_successful_create(self): + ''' Test successful create ''' + data = self.mock_args() + set_module_args(data) + with pytest.raises(AnsibleExitJson) as exc: + self.get_role_mock_object().apply() + assert exc.value.args[0]['changed'] + + def test_create_idempotency(self): + ''' Test create idempotency ''' + data = self.mock_args() + data['query'] = 'show' + set_module_args(data) + obj = self.get_role_mock_object('role') + with pytest.raises(AnsibleExitJson) as exc: + obj.apply() + assert not exc.value.args[0]['changed'] + + @patch('ansible.modules.storage.netapp.na_ontap_user_role.NetAppOntapUserRole.get_role') + def test_create_error(self, get_role): + ''' Test create error ''' + set_module_args(self.mock_args()) + get_role.side_effect = [ + None + ] + with pytest.raises(AnsibleFailJson) as exc: + self.get_role_mock_object('error').apply() + assert exc.value.args[0]['msg'] == 'Error creating role testrole: NetApp API failed. Reason - test:error' + + @patch('ansible.modules.storage.netapp.na_ontap_user_role.NetAppOntapUserRole.get_role') + def test_successful_modify(self, get_role): + ''' Test successful modify ''' + data = self.mock_args() + data['query'] = 'show' + set_module_args(data) + current = self.mock_role + current['query'] = 'show-space' + get_role.side_effect = [ + current + ] + obj = self.get_role_mock_object() + with pytest.raises(AnsibleExitJson) as exc: + obj.apply() + assert exc.value.args[0]['changed'] + + @patch('ansible.modules.storage.netapp.na_ontap_user_role.NetAppOntapUserRole.get_role') + def test_modify_idempotency(self, get_role): + ''' Test modify idempotency ''' + data = self.mock_args() + data['query'] = 'show' + set_module_args(data) + current = self.mock_role + current['query'] = 'show' + get_role.side_effect = [ + current + ] + obj = self.get_role_mock_object() + with pytest.raises(AnsibleExitJson) as exc: + obj.apply() + assert not exc.value.args[0]['changed'] + + @patch('ansible.modules.storage.netapp.na_ontap_user_role.NetAppOntapUserRole.get_role') + def test_modify_error(self, get_role): + ''' Test modify error ''' + data = self.mock_args() + data['query'] = 'show' + set_module_args(data) + current = self.mock_role + current['query'] = 'show-space' + get_role.side_effect = [ + current + ] + with pytest.raises(AnsibleFailJson) as exc: + self.get_role_mock_object('error').apply() + assert exc.value.args[0]['msg'] == 'Error modifying role testrole: NetApp API failed. Reason - test:error' + + def test_successful_delete(self): + ''' Test delete existing role ''' + data = self.mock_args() + data['state'] = 'absent' + set_module_args(data) + with pytest.raises(AnsibleExitJson) as exc: + self.get_role_mock_object('role').apply() + assert exc.value.args[0]['changed']