From ecdb61695430bfd1031bef8aa31d25b528dfeab5 Mon Sep 17 00:00:00 2001 From: vicmunoz Date: Fri, 10 May 2019 17:24:03 +0200 Subject: [PATCH] NetApp ONTAP module for manage ipspaces (#49821) * NetApp ONTAP module for manage ipspaces * fixes for ci_cd and code layout * indentation fixes * code style fixes * fixing yamllint issue * unit test for module na_ontap_ipspace * fixing sanity tests * change pytest.skip to pytest.mark.skip * adding ansible version to 2.9 --- .../storage/netapp/na_ontap_ipspace.py | 258 ++++++++++++++++++ .../storage/netapp/test_na_ontap_ipspace.py | 138 ++++++++++ 2 files changed, 396 insertions(+) create mode 100644 lib/ansible/modules/storage/netapp/na_ontap_ipspace.py create mode 100644 test/units/modules/storage/netapp/test_na_ontap_ipspace.py diff --git a/lib/ansible/modules/storage/netapp/na_ontap_ipspace.py b/lib/ansible/modules/storage/netapp/na_ontap_ipspace.py new file mode 100644 index 0000000000..56f1e87692 --- /dev/null +++ b/lib/ansible/modules/storage/netapp/na_ontap_ipspace.py @@ -0,0 +1,258 @@ +#!/usr/bin/python +""" +this is ipspace module + +# (c) 2018, NTT Europe Ltd. +# 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 = ''' +--- +module: na_ontap_ipspace + +short_description: NetApp ONTAP Manage an ipspace + +version_added: '2.9' + +author: + - NTTE Storage Engineering (@vicmunoz) + +description: + - Manage an ipspace for an Ontap Cluster + +extends_documentation_fragment: + - netapp.na_ontap + +options: + state: + description: + - Whether the specified ipspace should exist or not + choices: ['present', 'absent'] + default: present + name: + description: + - The name of the ipspace to manage + required: true + from_name: + description: + - Name of the existing ipspace to be renamed to name +''' + +EXAMPLES = """ + - name: Create ipspace + na_ontap_ipspace: + state: present + name: ansibleIpspace + hostname: "{{ netapp_hostname }}" + username: "{{ netapp_username }}" + password: "{{ netapp_password }}" + + - name: Delete ipspace + na_ontap_ipspace: + state: absent + name: ansibleIpspace + hostname: "{{ netapp_hostname }}" + username: "{{ netapp_username }}" + password: "{{ netapp_password }}" + + - name: Rename ipspace + na_ontap_ipspace: + state: present + name: ansibleIpspace_newname + from_name: ansibleIpspace + hostname: "{{ netapp_hostname }}" + username: "{{ netapp_username }}" + password: "{{ netapp_password }}" +""" + +RETURN = """ +""" + +import traceback + +import ansible.module_utils.netapp as netapp_utils +from ansible.module_utils.netapp_module import NetAppModule +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils._text import to_native + +HAS_NETAPP_LIB = netapp_utils.has_netapp_lib() + + +class NetAppOntapIpspace(object): + '''Class with ipspace operations''' + + def __init__(self): + self.argument_spec = netapp_utils.na_ontap_host_argument_spec() + self.argument_spec.update(dict( + state=dict( + required=False, choices=['present', 'absent'], + default='present'), + name=dict(required=True, type='str'), + from_name=dict(required=False, type='str'), + )) + self.module = AnsibleModule( + argument_spec=self.argument_spec, + supports_check_mode=True + ) + + 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) + return + + def ipspace_get_iter(self, name): + """ + Return net-ipspaces-get-iter query results + :param name: Name of the ipspace + :return: NaElement if ipspace found, None otherwise + """ + ipspace_get_iter = netapp_utils.zapi.NaElement('net-ipspaces-get-iter') + query_details = netapp_utils.zapi.NaElement.create_node_with_children( + 'net-ipspaces-info', **{'ipspace': name}) + query = netapp_utils.zapi.NaElement('query') + query.add_child_elem(query_details) + ipspace_get_iter.add_child_elem(query) + try: + result = self.server.invoke_successfully( + ipspace_get_iter, enable_tunneling=False) + except netapp_utils.zapi.NaApiError as error: + # Error 14636 denotes an ipspace does not exist + # Error 13073 denotes an ipspace not found + if to_native(error.code) == "14636" or to_native(error.code) == "13073": + return None + else: + self.module.self.fail_json( + msg=to_native(error), + exception=traceback.format_exc()) + return result + + def get_ipspace(self, name=None): + """ + Fetch details if ipspace exists + :param name: Name of the ipspace to be fetched + :return: + Dictionary of current details if ipspace found + None if ipspace is not found + """ + if name is None: + name = self.parameters['name'] + ipspace_get = self.ipspace_get_iter(name) + if (ipspace_get and ipspace_get.get_child_by_name('num-records') and + int(ipspace_get.get_child_content('num-records')) >= 1): + current_ipspace = dict() + attr_list = ipspace_get.get_child_by_name('attributes-list') + attr = attr_list.get_child_by_name('net-ipspaces-info') + current_ipspace['name'] = attr.get_child_content('ipspace') + return current_ipspace + return None + + def create_ipspace(self): + """ + Create ipspace + :return: None + """ + ipspace_create = netapp_utils.zapi.NaElement.create_node_with_children( + 'net-ipspaces-create', **{'ipspace': self.parameters['name']}) + try: + self.server.invoke_successfully(ipspace_create, + enable_tunneling=False) + except netapp_utils.zapi.NaApiError as error: + self.module.self.fail_json( + msg="Error provisioning ipspace %s: %s" % ( + self.parameters['name'], + to_native(error)), + exception=traceback.format_exc()) + + def delete_ipspace(self): + """ + Destroy ipspace + :return: None + """ + ipspace_destroy = netapp_utils.zapi.NaElement.create_node_with_children( + 'net-ipspaces-destroy', + **{'ipspace': self.parameters['name']}) + try: + self.server.invoke_successfully( + ipspace_destroy, enable_tunneling=False) + except netapp_utils.zapi.NaApiError as error: + self.module.self.fail_json( + msg="Error removing ipspace %s: %s" % ( + self.parameters['name'], + to_native(error)), + exception=traceback.format_exc()) + + def rename_ipspace(self): + """ + Rename an ipspace + :return: Nothing + """ + ipspace_rename = netapp_utils.zapi.NaElement.create_node_with_children( + 'net-ipspaces-rename', + **{'ipspace': self.parameters['from_name'], + 'new-name': self.parameters['name']}) + try: + self.server.invoke_successfully(ipspace_rename, + enable_tunneling=False) + except netapp_utils.zapi.NaApiError as error: + self.module.fail_json( + msg="Error renaming ipspace %s: %s" % ( + self.parameters['from_name'], + to_native(error)), + exception=traceback.format_exc()) + + def apply(self): + """ + Apply action to the ipspace + :return: Nothing + """ + current = self.get_ipspace() + # rename and create are mutually exclusive + rename, cd_action = None, None + if self.parameters.get('from_name'): + rename = self.na_helper.is_rename_action( + self.get_ipspace(self.parameters['from_name']), + current) + if rename is None: + self.module.fail_json( + msg="Error renaming: ipspace %s does not exist" % + self.parameters['from_name']) + else: + cd_action = self.na_helper.get_cd_action(current, self.parameters) + if self.na_helper.changed: + if self.module.check_mode: + pass + else: + if rename: + self.rename_ipspace() + elif cd_action == 'create': + self.create_ipspace() + elif cd_action == 'delete': + self.delete_ipspace() + self.module.exit_json(changed=self.na_helper.changed) + + +def main(): + """ + Execute action + :return: nothing + """ + obj = NetAppOntapIpspace() + obj.apply() + + +if __name__ == '__main__': + main() diff --git a/test/units/modules/storage/netapp/test_na_ontap_ipspace.py b/test/units/modules/storage/netapp/test_na_ontap_ipspace.py new file mode 100644 index 0000000000..abaa11d358 --- /dev/null +++ b/test/units/modules/storage/netapp/test_na_ontap_ipspace.py @@ -0,0 +1,138 @@ +# (c) 2018, NTT Europe Ltd. +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +""" unit test for Ansible module: na_ontap_ipspace """ + +from __future__ import print_function +import json +import pytest + +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_ipspace \ + import NetAppOntapIpspace as my_module +from units.compat import unittest +from units.compat.mock import patch + +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, parm1=None): + ''' save arguments ''' + self.type = kind + self.parm1 = parm1 + 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.type == 'ipspace': + xml = self.build_ipspace_info(self.parm1) + self.xml_out = xml + return xml + + @staticmethod + def build_ipspace_info(ipspace): + ''' build xml data for ipspace ''' + xml = netapp_utils.zapi.NaElement('xml') + data = {'num-records': 1, + 'attributes-list': {'net-ipspaces-info': {'ipspace': ipspace}}} + xml.translate_struct(data) + print(xml.to_string()) + 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.server = MockONTAPConnection() + + def set_default_args(self): + return dict({ + 'name': 'test_ipspace', + 'hostname': 'hostname', + 'username': 'username', + 'password': 'password' + }) + + def test_fail_requiredargs_missing(self): + ''' required arguments are reported as errors ''' + with pytest.raises(AnsibleFailJson) as exc: + set_module_args({}) + my_module() + print('Info: %s' % exc.value.args[0]['msg']) + + def test_get_ipspace_iscalled(self): + ''' test if get_ipspace() is called ''' + set_module_args(self.set_default_args()) + my_obj = my_module() + my_obj.server = self.server + ipspace = my_obj.get_ipspace() + print('Info: test_get_ipspace: %s' % repr(ipspace)) + assert ipspace is None + + def test_ipspace_apply_iscalled(self): + ''' test if apply() is called ''' + module_args = {'name': 'test_apply_ips'} + module_args.update(self.set_default_args()) + set_module_args(module_args) + my_obj = my_module() + my_obj.server = self.server + ipspace = my_obj.get_ipspace() + print('Info: test_get_ipspace: %s' % repr(ipspace)) + assert ipspace is None + with pytest.raises(AnsibleExitJson) as exc: + my_obj.apply() + print('Info: test_ipspace_apply: %s' % repr(exc.value)) + assert exc.value.args[0]['changed'] + my_obj.server = MockONTAPConnection('ipspace', 'test_apply_ips') + ipspace = my_obj.get_ipspace() + print('Info: test_get_ipspace: %s' % repr(ipspace)) + assert ipspace is not None + assert ipspace['name'] == 'test_apply_ips' + with pytest.raises(AnsibleExitJson) as exc: + my_obj.apply() + print('Info: test_ipspace_apply: %s' % repr(exc.value)) + assert exc.value.args[0]['changed'] + ipspace = my_obj.get_ipspace() + assert ipspace['name'] == 'test_apply_ips'