diff --git a/lib/ansible/modules/storage/netapp/na_ontap_gather_facts.py b/lib/ansible/modules/storage/netapp/na_ontap_gather_facts.py index a3a14fe1bb..b792e38648 100644 --- a/lib/ansible/modules/storage/netapp/na_ontap_gather_facts.py +++ b/lib/ansible/modules/storage/netapp/na_ontap_gather_facts.py @@ -61,7 +61,8 @@ ontap_facts: "storage_failover_info": {...}, "vserver_login_banner_info": {...}, "vserver_motd_info": {...}, - "vserver_info": {...} + "vserver_info": {...}, + "ontap_version": {...} }' ''' @@ -85,7 +86,7 @@ except ImportError: HAS_NETAPP_LIB = netapp_utils.has_netapp_lib() -class NetAppGatherFacts(object): +class NetAppONTAPGatherFacts(object): def __init__(self, module): self.module = module @@ -96,6 +97,16 @@ class NetAppGatherFacts(object): else: self.server = netapp_utils.setup_na_ontap_zapi(module=self.module) + def ontapi(self): + api_call = netapp_utils.zapi.NaElement('system-get-ontapi-version') + try: + results = self.server.invoke_successfully(api_call, enable_tunneling=False) + ontapi_version = results.get_child_content('minor-version') + return ontapi_version + except netapp_utils.zapi.NaApiError as e: + self.module.fail_json(msg="Error calling API %s: %s" % + (api_call.to_string(), to_native(e)), exception=traceback.format_exc()) + def call_api(self, call, query=None): api_call = netapp_utils.zapi.NaElement(call) result = None @@ -168,6 +179,9 @@ class NetAppGatherFacts(object): return out def get_all(self): + results = netapp_utils.get_cserver(self.server) + cserver = netapp_utils.setup_na_ontap_zapi(module=self.module, vserver=results) + netapp_utils.ems_log_event("na_ontap_gather_facts", cserver) self.netapp_info['net_interface_info'] = self.get_generic_get_iter( 'net-interface-get-iter', attribute='net-interface-info', @@ -201,7 +215,7 @@ class NetAppGatherFacts(object): self.netapp_info['volume_info'] = self.get_generic_get_iter( 'volume-get-iter', attribute='volume-attributes', - field=('name', 'owning-vserver-name', 'aggr-name'), + field=('name', 'owning-vserver-name'), query={'max-records': '1024'} ) self.netapp_info['lun_info'] = self.get_generic_get_iter( @@ -247,22 +261,32 @@ class NetAppGatherFacts(object): query={'max-records': '1024'} ) + self.netapp_info['ontap_version'] = self.ontapi() + return self.netapp_info # https://stackoverflow.com/questions/14962485/finding-a-key-recursively-in-a-dictionary -def _finditem(obj, key): +def __finditem(obj, key): if key in obj: return obj[key] for dummy, v in obj.items(): if isinstance(v, dict): - item = _finditem(v, key) + item = __finditem(v, key) if item is not None: return item return None +def _finditem(obj, key): + + value = __finditem(obj, key) + if value is not None: + return value + raise KeyError(key) + + def convert_keys(d): out = {} if isinstance(d, dict): @@ -292,7 +316,7 @@ def main(): module.fail_json(msg="json missing") state = module.params['state'] - v = NetAppGatherFacts(module) + v = NetAppONTAPGatherFacts(module) g = v.get_all() result = {'state': state, 'changed': False} module.exit_json(ansible_facts={'ontap_facts': g}, **result) diff --git a/test/units/modules/storage/netapp/test_na_ontap_gather_facts.py b/test/units/modules/storage/netapp/test_na_ontap_gather_facts.py new file mode 100644 index 0000000000..e99217e497 --- /dev/null +++ b/test/units/modules/storage/netapp/test_na_ontap_gather_facts.py @@ -0,0 +1,125 @@ +# (c) 2018, NetApp, Inc +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +''' unit tests for ONTAP Ansible module na_ontap_gather_facts ''' + +from __future__ import print_function +import json +import pytest +import sys + +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 + +from ansible.modules.storage.netapp.na_ontap_gather_facts \ + import NetAppONTAPGatherFacts as my_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, 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 + print(xml.to_string()) + if self.type == 'vserver': + xml = self.build_vserver_info(self.parm1) + self.xml_out = xml + print(xml.to_string()) + return xml + + @staticmethod + def build_vserver_info(vserver): + ''' build xml data for vserser-info ''' + xml = netapp_utils.zapi.NaElement('xml') + attributes = netapp_utils.zapi.NaElement('attributes-list') + attributes.add_node_with_children('vserver-info', + **{'vserver-name': vserver}) + xml.add_child_elem(attributes) + # 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 test_module_fail_when_required_args_missing(self): + ''' required arguments are reported as errors ''' + with pytest.raises(AnsibleFailJson) as exc: + set_module_args({}) + module = basic.AnsibleModule( + argument_spec=netapp_utils.na_ontap_host_argument_spec(), + supports_check_mode=True + ) + my_module(module) + print('Info: %s' % exc.value.args[0]['msg']) + + @patch('ansible.module_utils.netapp.ems_log_event') + def test_ensure_command_called(self, mock_ems_log): + ''' calling get_all will raise a KeyError exception ''' + set_module_args({ + 'hostname': 'hostname', + 'username': 'username', + 'password': 'password', + }) + module = basic.AnsibleModule( + argument_spec=netapp_utils.na_ontap_host_argument_spec(), + supports_check_mode=True + ) + my_obj = my_module(module) + my_obj.server = MockONTAPConnection('vserver', 'SVMadmin') + with pytest.raises(KeyError) as exc: + my_obj.get_all() + if sys.version_info >= (2, 7): + msg = 'net-interface-info' + assert exc.value.args[0] == msg