diff --git a/lib/ansible/modules/storage/netapp/na_ontap_net_port.py b/lib/ansible/modules/storage/netapp/na_ontap_net_port.py index 4e2aedecea..a1631d4bdb 100644 --- a/lib/ansible/modules/storage/netapp/na_ontap_net_port.py +++ b/lib/ansible/modules/storage/netapp/na_ontap_net_port.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 @@ -30,9 +30,11 @@ options: description: - Specifies the name of node. required: true - port: + ports: + aliases: + - port description: - - Specifies the name of port. + - Specifies the name of port(s). required: true mtu: description: @@ -60,21 +62,24 @@ options: EXAMPLES = """ - name: Modify Net Port na_ontap_net_port: - state=present - username={{ netapp_username }} - password={{ netapp_password }} - hostname={{ netapp_hostname }} - node={{ Vsim server name }} - port=e0d - autonegotiate_admin=true + state: present + username: "{{ netapp_username }}" + password: "{{ netapp_password }}" + hostname: "{{ netapp_hostname }}" + node: "{{ node_name }}" + ports: e0d,e0c + autonegotiate_admin: true """ 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() @@ -92,7 +97,7 @@ class NetAppOntapNetPort(object): self.argument_spec.update(dict( state=dict(required=False, choices=['present'], default='present'), node=dict(required=True, type="str"), - port=dict(required=True, type="str"), + ports=dict(required=True, type="list", aliases=['port']), mtu=dict(required=False, type="str", default=None), autonegotiate_admin=dict(required=False, type="str", default=None), duplex_admin=dict(required=False, type="str", default=None), @@ -106,119 +111,107 @@ class NetAppOntapNetPort(object): supports_check_mode=True ) - p = self.module.params - - # set up state variables - self.state = p['state'] - self.node = p['node'] - self.port = p['port'] - # the following option are optional, but at least one need to be set - self.mtu = p['mtu'] - self.autonegotiate_admin = p["autonegotiate_admin"] - self.duplex_admin = p["duplex_admin"] - self.speed_admin = p["speed_admin"] - self.flowcontrol_admin = p["flowcontrol_admin"] + self.na_helper = NetAppModule() + self.parameters = self.na_helper.set_parameters(self.module.params) + self.set_playbook_zapi_key_map() if HAS_NETAPP_LIB is False: - self.module.fail_json( - msg="the python NetApp-Lib module is required") + 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 get_net_port(self): + def set_playbook_zapi_key_map(self): + self.na_helper.zapi_string_keys = { + 'mtu': 'mtu', + 'autonegotiate_admin': 'is-administrative-auto-negotiate', + 'duplex_admin': 'administrative-duplex', + 'speed_admin': 'administrative-speed', + 'flowcontrol_admin': 'administrative-flowcontrol', + 'ipspace': 'ipspace' + } + + def get_net_port(self, port): """ Return details about the net port - - :return: Details about the net port. None if not found. + :param: port: Name of the port + :return: Dictionary with current state of the port. None if not found. :rtype: dict """ - net_port_info = netapp_utils.zapi.NaElement('net-port-get-iter') - net_port_attributes = netapp_utils.zapi.NaElement('net-port-info') - net_port_attributes.add_new_child('node', self.node) - net_port_attributes.add_new_child('port', self.port) - query = netapp_utils.zapi.NaElement('query') - query.add_child_elem(net_port_attributes) - net_port_info.add_child_elem(query) - result = self.server.invoke_successfully(net_port_info, True) - return_value = None - - if result.get_child_by_name('num-records') and \ - int(result.get_child_content('num-records')) >= 1: - - net_port_attributes = result.get_child_by_name('attributes-list').\ - get_child_by_name('net-port-info') - return_value = { - 'node': net_port_attributes.get_child_content('node'), - 'port': net_port_attributes.get_child_content('port'), - 'mtu': net_port_attributes.get_child_content('mtu'), - 'autonegotiate_admin': net_port_attributes.get_child_content( - 'is-administrative-auto-negotiate'), - 'duplex_admin': net_port_attributes.get_child_content( - 'administrative-duplex'), - 'speed_admin': net_port_attributes.get_child_content( - 'administrative-speed'), - 'flowcontrol_admin': net_port_attributes.get_child_content( - 'administrative-flowcontrol'), + net_port_get = netapp_utils.zapi.NaElement('net-port-get-iter') + attributes = { + 'query': { + 'net-port-info': { + 'node': self.parameters['node'], + 'port': port + } } - return return_value + } + net_port_get.translate_struct(attributes) - def modify_net_port(self): + try: + result = self.server.invoke_successfully(net_port_get, True) + if result.get_child_by_name('num-records') and int(result.get_child_content('num-records')) >= 1: + port_info = result['attributes-list']['net-port-info'] + port_details = dict() + else: + return None + except netapp_utils.zapi.NaApiError as error: + self.module.fail_json(msg='Error getting net ports for %s: %s' % (self.parameters['node'], to_native(error)), + exception=traceback.format_exc()) + + for item_key, zapi_key in self.na_helper.zapi_string_keys.items(): + port_details[item_key] = port_info.get_child_content(zapi_key) + return port_details + + def modify_net_port(self, port, modify): """ Modify a port + + :param port: Name of the port + :param modify: dict with attributes to be modified + :return: None """ - port_obj = netapp_utils.zapi.NaElement('net-port-modify') - port_obj.add_new_child("node", self.node) - port_obj.add_new_child("port", self.port) - # The following options are optional. - # We will only call them if they are not set to None - if self.mtu: - port_obj.add_new_child("mtu", self.mtu) - if self.autonegotiate_admin: - port_obj.add_new_child( - "is-administrative-auto-negotiate", self.autonegotiate_admin) - if self.duplex_admin: - port_obj.add_new_child("administrative-duplex", self.duplex_admin) - if self.speed_admin: - port_obj.add_new_child("administrative-speed", self.speed_admin) - if self.flowcontrol_admin: - port_obj.add_new_child( - "administrative-flowcontrol", self.flowcontrol_admin) - self.server.invoke_successfully(port_obj, True) + port_modify = netapp_utils.zapi.NaElement('net-port-modify') + port_attributes = {'node': self.parameters['node'], + 'port': port} + for key in modify: + if key in self.na_helper.zapi_string_keys: + zapi_key = self.na_helper.zapi_string_keys.get(key) + port_attributes[zapi_key] = modify[key] + port_modify.translate_struct(port_attributes) + try: + self.server.invoke_successfully(port_modify, enable_tunneling=True) + except netapp_utils.zapi.NaApiError as error: + self.module.fail_json(msg='Error modifying net ports for %s: %s' % (self.parameters['node'], to_native(error)), + exception=traceback.format_exc()) + + def autosupport_log(self): + """ + AutoSupport log for na_ontap_net_port + :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("na_ontap_net_port", cserver) def apply(self): """ Run Module based on play book """ - changed = False - net_port_exists = False - 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_net_port", cserver) - net_port_details = self.get_net_port() - if net_port_details: - net_port_exists = True - if self.state == 'present': - if (self.mtu and self.mtu != net_port_details['mtu']) or \ - (self.autonegotiate_admin and - self.autonegotiate_admin != net_port_details['autonegotiate_admin']) or \ - (self.duplex_admin and - self.duplex_admin != net_port_details['duplex_admin']) or \ - (self.speed_admin and - self.speed_admin != net_port_details['speed_admin']) or \ - (self.flowcontrol_admin and - self.flowcontrol_admin != net_port_details['flowcontrol_admin']): - changed = True - if changed: - if self.module.check_mode: - pass - else: - if self.state == 'present': - if net_port_exists: - self.modify_net_port() - self.module.exit_json(changed=changed) + self.autosupport_log() + # Run the task for all ports in the list of 'ports' + for port in self.parameters['ports']: + current = self.get_net_port(port) + 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_net_port(port, modify) + self.module.exit_json(changed=self.na_helper.changed) def main(): diff --git a/test/units/modules/storage/netapp/test_na_ontap_net_port.py b/test/units/modules/storage/netapp/test_na_ontap_net_port.py new file mode 100644 index 0000000000..b557a964b6 --- /dev/null +++ b/test/units/modules/storage/netapp/test_na_ontap_net_port.py @@ -0,0 +1,179 @@ +# (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_net_port \ + import NetAppOntapNetPort as port_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 == 'port': + xml = self.build_port_info(self.data) + self.xml_out = xml + return xml + + @staticmethod + def build_port_info(port_details): + ''' build xml data for net-port-info ''' + xml = netapp_utils.zapi.NaElement('xml') + attributes = { + 'num-records': 1, + 'attributes-list': { + 'net-port-info': { + # 'port': port_details['port'], + 'mtu': port_details['mtu'], + 'is-administrative-auto-negotiate': 'true', + 'ipspace': 'default', + 'administrative-flowcontrol': port_details['flowcontrol_admin'], + 'node': port_details['node'] + } + } + } + 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.server = MockONTAPConnection() + self.mock_port = { + 'node': 'test', + 'ports': 'a1', + 'flowcontrol_admin': 'something', + 'mtu': '1000' + } + + def mock_args(self): + return { + 'node': self.mock_port['node'], + 'flowcontrol_admin': self.mock_port['flowcontrol_admin'], + 'ports': [self.mock_port['ports']], + 'mtu': self.mock_port['mtu'], + 'hostname': 'test', + 'username': 'test_user', + 'password': 'test_pass!' + } + + def get_port_mock_object(self, kind=None, data=None): + """ + Helper method to return an na_ontap_net_port object + :param kind: passes this param to MockONTAPConnection() + :return: na_ontap_net_port object + """ + obj = port_module() + obj.autosupport_log = Mock(return_value=None) + if data is None: + data = self.mock_port + obj.server = MockONTAPConnection(kind=kind, data=data) + return 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({}) + port_module() + print('Info: %s' % exc.value.args[0]['msg']) + + def test_get_nonexistent_port(self): + ''' Test if get_net_port returns None for non-existent port ''' + set_module_args(self.mock_args()) + result = self.get_port_mock_object().get_net_port('test') + assert result is None + + def test_get_existing_port(self): + ''' Test if get_net_port returns details for existing port ''' + set_module_args(self.mock_args()) + result = self.get_port_mock_object('port').get_net_port('test') + assert result['mtu'] == self.mock_port['mtu'] + assert result['flowcontrol_admin'] == self.mock_port['flowcontrol_admin'] + + def test_successful_modify(self): + ''' Test modify_net_port ''' + data = self.mock_args() + data['mtu'] = '2000' + set_module_args(data) + with pytest.raises(AnsibleExitJson) as exc: + self.get_port_mock_object('port').apply() + assert exc.value.args[0]['changed'] + + def test_successful_modify_multiple_ports(self): + ''' Test modify_net_port ''' + data = self.mock_args() + data['ports'] = ['a1', 'a2'] + data['mtu'] = '2000' + set_module_args(data) + with pytest.raises(AnsibleExitJson) as exc: + self.get_port_mock_object('port').apply() + assert exc.value.args[0]['changed'] + + @patch('ansible.modules.storage.netapp.na_ontap_net_port.NetAppOntapNetPort.get_net_port') + def test_get_called(self, get_port): + ''' Test get_net_port ''' + data = self.mock_args() + data['ports'] = ['a1', 'a2'] + set_module_args(data) + with pytest.raises(AnsibleExitJson) as exc: + self.get_port_mock_object('port').apply() + assert get_port.call_count == 2