From 36add6e86f65366ae5efc602f59fb8af1b0980bb Mon Sep 17 00:00:00 2001 From: Chris Archibald Date: Tue, 27 Aug 2019 06:06:59 -0700 Subject: [PATCH] New Module: na_ontap_ndmp (#59815) * new module * fixes * fixes * fix unit tests * update tests * fixes --- .../modules/storage/netapp/na_ontap_ndmp.py | 342 ++++++++++++++++++ .../storage/netapp/test_na_ontap_ndmp.py | 166 +++++++++ 2 files changed, 508 insertions(+) create mode 100644 lib/ansible/modules/storage/netapp/na_ontap_ndmp.py create mode 100644 test/units/modules/storage/netapp/test_na_ontap_ndmp.py diff --git a/lib/ansible/modules/storage/netapp/na_ontap_ndmp.py b/lib/ansible/modules/storage/netapp/na_ontap_ndmp.py new file mode 100644 index 00000000000..53e03ee861a --- /dev/null +++ b/lib/ansible/modules/storage/netapp/na_ontap_ndmp.py @@ -0,0 +1,342 @@ +#!/usr/bin/python +""" this is ndmp module + + (c) 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 +__metaclass__ = type + +ANSIBLE_METADATA = { + 'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'community' +} + + +DOCUMENTATION = ''' +--- +module: na_ontap_ndmp +short_description: NetApp ONTAP NDMP services configuration +extends_documentation_fragment: + - netapp.na_ontap +version_added: '2.9' +author: NetApp Ansible Team (@carchi8py) + +description: + - Modify NDMP Services. + +options: + + vserver: + description: + - Name of the vserver. + required: true + type: str + + abort_on_disk_error: + description: + - Enable abort on disk error. + type: bool + + authtype: + description: + - Authentication type. + type: list + + backup_log_enable: + description: + - Enable backup log. + type: bool + + data_port_range: + description: + - Data port range. + type: str + + debug_enable: + description: + - Enable debug. + type: bool + + debug_filter: + description: + - Debug filter. + type: str + + dump_detailed_stats: + description: + - Enable logging of VM stats for dump. + type: bool + + dump_logical_find: + description: + - Enable logical find for dump. + type: str + + enable: + description: + - Enable NDMP on vserver. + type: bool + + fh_dir_retry_interval: + description: + - FH throttle value for dir. + type: int + + fh_node_retry_interval: + description: + - FH throttle value for node. + type: int + + ignore_ctime_enabled: + description: + - Ignore ctime. + type: bool + + is_secure_control_connection_enabled: + description: + - Is secure control connection enabled. + type: bool + + offset_map_enable: + description: + - Enable offset map. + type: bool + + per_qtree_exclude_enable: + description: + - Enable per qtree exclusion. + type: bool + + preferred_interface_role: + description: + - Preferred interface role. + type: list + + restore_vm_cache_size: + description: + - Restore VM file cache size. + type: int + + secondary_debug_filter: + description: + - Secondary debug filter. + type: str + + tcpnodelay: + description: + - Enable TCP nodelay. + type: bool + + tcpwinsize: + description: + - TCP window size. + type: int +''' + +EXAMPLES = ''' + - name: modify ndmp + na_ontap_ndmp: + vserver: ansible + hostname: "{{ hostname }}" + abort_on_disk_error: true + authtype: plaintext,challenge + backup_log_enable: true + data_port_range: 8000-9000 + debug_enable: true + debug_filter: filter + dump_detailed_stats: true + dump_logical_find: default + enable: true + fh_dir_retry_interval: 100 + fh_node_retry_interval: 100 + ignore_ctime_enabled: true + is_secure_control_connection_enabled: true + offset_map_enable: true + per_qtree_exclude_enable: true + preferred_interface_role: node_mgmt,intercluster + restore_vm_cache_size: 1000 + secondary_debug_filter: filter + tcpnodelay: true + tcpwinsize: 10000 + username: user + password: pass + https: False +''' + +RETURN = ''' +''' + +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_module import NetAppModule + +HAS_NETAPP_LIB = netapp_utils.has_netapp_lib() + + +class NetAppONTAPNdmp(object): + ''' + modify vserver cifs security + ''' + def __init__(self): + + self.argument_spec = netapp_utils.na_ontap_host_argument_spec() + self.modifiable_options = dict( + abort_on_disk_error=dict(required=False, type='bool'), + authtype=dict(required=False, type='list'), + backup_log_enable=dict(required=False, type='bool'), + data_port_range=dict(required=False, type='str'), + debug_enable=dict(required=False, type='bool'), + debug_filter=dict(required=False, type='str'), + dump_detailed_stats=dict(required=False, type='bool'), + dump_logical_find=dict(required=False, type='str'), + enable=dict(required=False, type='bool'), + fh_dir_retry_interval=dict(required=False, type='int'), + fh_node_retry_interval=dict(required=False, type='int'), + ignore_ctime_enabled=dict(required=False, type='bool'), + is_secure_control_connection_enabled=dict(required=False, type='bool'), + offset_map_enable=dict(required=False, type='bool'), + per_qtree_exclude_enable=dict(required=False, type='bool'), + preferred_interface_role=dict(required=False, type='list'), + restore_vm_cache_size=dict(required=False, type='int'), + secondary_debug_filter=dict(required=False, type='str'), + tcpnodelay=dict(required=False, type='bool'), + tcpwinsize=dict(required=False, type='int') + ) + self.argument_spec.update(dict( + vserver=dict(required=True, type='str') + )) + + self.argument_spec.update(self.modifiable_options) + + 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, vserver=self.parameters['vserver']) + + def ndmp_get_iter(self): + """ + get current vserver ndmp attributes. + :return: a dict of ndmp attributes. + """ + ndmp_get = netapp_utils.zapi.NaElement('ndmp-vserver-attributes-get-iter') + query = netapp_utils.zapi.NaElement('query') + ndmp_info = netapp_utils.zapi.NaElement('ndmp-vserver-attributes-info') + ndmp_info.add_new_child('vserver', self.parameters['vserver']) + query.add_child_elem(ndmp_info) + ndmp_get.add_child_elem(query) + ndmp_details = dict() + try: + result = self.server.invoke_successfully(ndmp_get, enable_tunneling=True) + except netapp_utils.zapi.NaApiError as error: + self.module.fail_json(msg='Error fetching ndmp from %s: %s' + % (self.parameters['vserver'], to_native(error)), + exception=traceback.format_exc()) + + if result.get_child_by_name('num-records') and int(result.get_child_content('num-records')) > 0: + ndmp_attributes = result.get_child_by_name('attributes-list').get_child_by_name('ndmp-vserver-attributes-info') + self.get_ndmp_details(ndmp_details, ndmp_attributes) + return ndmp_details + + def get_ndmp_details(self, ndmp_details, ndmp_attributes): + """ + :param ndmp_details: a dict of current ndmp. + :param ndmp_attributes: ndmp returned from api call in xml format. + :return: None + """ + for option in self.modifiable_options.keys(): + option_type = self.modifiable_options[option]['type'] + if option_type == 'bool': + ndmp_details[option] = self.str_to_bool(ndmp_attributes.get_child_content(self.attribute_to_name(option))) + elif option_type == 'int': + ndmp_details[option] = int(ndmp_attributes.get_child_content(self.attribute_to_name(option))) + elif option_type == 'list': + child_list = ndmp_attributes.get_child_by_name(self.attribute_to_name(option)) + values = [child.get_content() for child in child_list.get_children()] + ndmp_details[option] = values + else: + ndmp_details[option] = ndmp_attributes.get_child_content(self.attribute_to_name(option)) + + def modify_ndmp(self, modify): + """ + :param modify: A list of attributes to modify + :return: None + """ + ndmp_modify = netapp_utils.zapi.NaElement('ndmp-vserver-attributes-modify') + for attribute in modify: + if attribute == 'authtype': + authtypes = netapp_utils.zapi.NaElement('authtype') + types = self.parameters['authtype'] + for authtype in types: + authtypes.add_new_child('ndmpd-authtypes', authtype) + ndmp_modify.add_child_elem(authtypes) + elif attribute == 'preferred_interface_role': + preferred_interface_roles = netapp_utils.zapi.NaElement('preferred-interface-role') + roles = self.parameters['preferred_interface_role'] + for role in roles: + preferred_interface_roles.add_new_child('netport-role', role) + ndmp_modify.add_child_elem(preferred_interface_roles) + else: + ndmp_modify.add_new_child(self.attribute_to_name(attribute), str(self.parameters[attribute])) + try: + self.server.invoke_successfully(ndmp_modify, enable_tunneling=True) + except netapp_utils.zapi.NaApiError as e: + self.module.fail_json(msg='Error modifying ndmp on %s: %s' + % (self.parameters['vserver'], to_native(e)), + exception=traceback.format_exc()) + + @staticmethod + def attribute_to_name(attribute): + return str.replace(attribute, '_', '-') + + @staticmethod + def str_to_bool(s): + if s == 'true': + return True + else: + return False + + def apply(self): + """Call modify operations.""" + self.asup_log_for_cserver("na_ontap_ndmp") + current = self.ndmp_get_iter() + modify = self.na_helper.get_modified_attributes(current, self.parameters) + if self.na_helper.changed: + if self.module.check_mode: + pass + else: + if modify: + self.modify_ndmp(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 + """ + results = netapp_utils.get_cserver(self.server) + cserver = netapp_utils.setup_na_ontap_zapi(module=self.module, vserver=results) + netapp_utils.ems_log_event(event_name, cserver) + + +def main(): + obj = NetAppONTAPNdmp() + obj.apply() + + +if __name__ == '__main__': + main() diff --git a/test/units/modules/storage/netapp/test_na_ontap_ndmp.py b/test/units/modules/storage/netapp/test_na_ontap_ndmp.py new file mode 100644 index 00000000000..69cd58049f9 --- /dev/null +++ b/test/units/modules/storage/netapp/test_na_ontap_ndmp.py @@ -0,0 +1,166 @@ +# (c) 2019, 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 (absolute_import, division, print_function) +__metaclass__ = type +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_ndmp \ + import NetAppONTAPNdmp as ndmp_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.type = kind + self.data = 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.type == 'ndmp': + xml = self.build_ndmp_info(self.data) + if self.type == 'error': + error = netapp_utils.zapi.NaApiError('test', 'error') + raise error + self.xml_out = xml + return xml + + @staticmethod + def build_ndmp_info(ndmp_details): + ''' build xml data for ndmp ''' + xml = netapp_utils.zapi.NaElement('xml') + attributes = { + 'num-records': 1, + 'attributes-list': { + 'ndmp-vserver-attributes-info': { + 'ignore_ctime_enabled': ndmp_details['ignore_ctime_enabled'], + 'backup_log_enable': ndmp_details['backup_log_enable'], + 'authtype': [ + {'ndmpd-authtypes': 'plaintext'}, + {'ndmpd-authtypes': 'challenge'} + ] + } + } + } + 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_ndmp = { + 'ignore_ctime_enabled': 'true', + 'backup_log_enable': 'false' + } + + def mock_args(self): + return { + 'ignore_ctime_enabled': self.mock_ndmp['ignore_ctime_enabled'], + 'backup_log_enable': self.mock_ndmp['backup_log_enable'], + 'vserver': 'ansible', + 'hostname': 'test', + 'username': 'test_user', + 'password': 'test_pass!', + 'https': 'False' + } + + def get_ndmp_mock_object(self, kind=None): + """ + Helper method to return an na_ontap_ndmp object + :param kind: passes this param to MockONTAPConnection() + :return: na_ontap_ndmp object + """ + obj = ndmp_module() + obj.asup_log_for_cserver = Mock(return_value=None) + obj.server = Mock() + obj.server.invoke_successfully = Mock() + if kind is None: + obj.server = MockONTAPConnection() + else: + obj.server = MockONTAPConnection(kind=kind, data=self.mock_ndmp) + return obj + + @patch('ansible.modules.storage.netapp.na_ontap_ndmp.NetAppONTAPNdmp.ndmp_get_iter') + def test_successful_modify(self, ger_ndmp): + ''' Test successful modify ndmp''' + data = self.mock_args() + set_module_args(data) + current = { + 'ignore_ctime_enabled': False, + 'backup_log_enable': True + } + ger_ndmp.side_effect = [ + current + ] + with pytest.raises(AnsibleExitJson) as exc: + self.get_ndmp_mock_object('ndmp').apply() + assert exc.value.args[0]['changed'] + + @patch('ansible.modules.storage.netapp.na_ontap_ndmp.NetAppONTAPNdmp.ndmp_get_iter') + def test_modify_error(self, ger_ndmp): + ''' Test modify error ''' + data = self.mock_args() + set_module_args(data) + current = { + 'ignore_ctime_enabled': False, + 'backup_log_enable': True + } + ger_ndmp.side_effect = [ + current + ] + with pytest.raises(AnsibleFailJson) as exc: + self.get_ndmp_mock_object('error').apply() + assert exc.value.args[0]['msg'] == 'Error modifying ndmp on ansible: NetApp API failed. Reason - test:error'