diff --git a/lib/ansible/modules/storage/netapp/na_ontap_portset.py b/lib/ansible/modules/storage/netapp/na_ontap_portset.py index 497114dbf7..42b35cac27 100644 --- a/lib/ansible/modules/storage/netapp/na_ontap_portset.py +++ b/lib/ansible/modules/storage/netapp/na_ontap_portset.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 @@ -15,7 +15,7 @@ DOCUMENTATION = ''' short_description: NetApp ONTAP Create/Delete portset author: NetApp Ansible Team (@carchi8py) description: - - Create/Delete ONTAP portset. + - Create/Delete ONTAP portset, modify ports in a portset. extends_documentation_fragment: - netapp.na_ontap module: na_ontap_portset @@ -33,8 +33,8 @@ options: description: - Name of the port set to create. type: - required: true description: + - Required for create. - Protocols accepted for this portset. choices: ['fcp', 'iscsi', 'mixed'] force: @@ -43,6 +43,12 @@ options: - If 'true', forcibly destroy the portset, even if there are existing igroup bindings. type: bool default: False + ports: + description: + - Specify the ports associated with this portset. Should be comma separated. + - It represents the expected state of a list of ports at any time, and replaces the current value of ports. + - Adds a port if it is specified in expected state but not in current state. + - Deletes a port if it is in current state but not in expected state. version_added: "2.8" ''' @@ -53,16 +59,27 @@ EXAMPLES = """ state: present vserver: vserver_name name: portset_name + ports: a1 type: "{{ protocol type }}" username: "{{ netapp username }}" password: "{{ netapp password }}" hostname: "{{ netapp hostname }}" + - name: Modify ports in portset + na_ontap_portset: + state: present + vserver: vserver_name + name: portset_name + ports: a1,a2 + username: "{{ netapp username }}" + password: "{{ netapp password }}" + hostname: "{{ netapp hostname }}" + - name: Delete Portset na_ontap_portset: state: absent - vserver: ansible_test - name: test + vserver: vserver_name + name: portset_name force: True type: "{{ protocol type }}" username: "{{ netapp username }}" @@ -94,9 +111,10 @@ class NetAppONTAPPortset(object): state=dict(required=False, default='present'), vserver=dict(required=True, type='str'), name=dict(required=True, type='str'), - type=dict(required=True, type='str', choices=[ + type=dict(required=False, type='str', choices=[ 'fcp', 'iscsi', 'mixed']), - force=dict(required=False, type='bool', default=False) + force=dict(required=False, type='bool', default=False), + ports=dict(required=False, type='list') )) self.module = AnsibleModule( @@ -124,7 +142,8 @@ class NetAppONTAPPortset(object): portset_info = netapp_utils.zapi.NaElement('portset-info') portset_info.add_new_child('vserver', self.parameters['vserver']) portset_info.add_new_child('portset-name', self.parameters['name']) - portset_info.add_new_child('portset-type', self.parameters['type']) + if self.parameters.get('type'): + portset_info.add_new_child('portset-type', self.parameters['type']) query.add_child_elem(portset_info) portset_get.add_child_elem(query) return portset_get @@ -135,7 +154,7 @@ class NetAppONTAPPortset(object): :return: Dictionary of current portset details if query successful, else return None """ portset_get_iter = self.portset_get_iter() - portset_info = dict() + result, portset_info = None, dict() try: result = self.server.invoke_successfully(portset_get_iter, enable_tunneling=True) except netapp_utils.zapi.NaApiError as error: @@ -143,12 +162,13 @@ class NetAppONTAPPortset(object): % (self.parameters['name'], to_native(error)), exception=traceback.format_exc()) # return portset details - if result.get_child_by_name('num-records') and \ - int(result.get_child_content('num-records')) > 0: - porset_get_info = result.get_child_by_name('attributes-list').get_child_by_name('portset-info') - portset_info['vserver'] = porset_get_info.get_child_content('vserver') - portset_info['portset_name'] = porset_get_info.get_child_content('portset-name') - portset_info['portset_type'] = porset_get_info.get_child_content('portset-type') + if result.get_child_by_name('num-records') and int(result.get_child_content('num-records')) > 0: + portset_get_info = result.get_child_by_name('attributes-list').get_child_by_name('portset-info') + if int(portset_get_info.get_child_content('portset-port-total')) > 0: + ports = portset_get_info.get_child_by_name('portset-port-info') + portset_info['ports'] = [port.get_content() for port in ports.get_children()] + else: + portset_info['ports'] = [] return portset_info return None @@ -156,6 +176,8 @@ class NetAppONTAPPortset(object): """ Create a portset """ + if self.parameters.get('type') is None: + self.module.fail_json(msg='Error: Missing required parameter for create (type)') portset_info = netapp_utils.zapi.NaElement("portset-create") portset_info.add_new_child("portset-name", self.parameters['name']) portset_info.add_new_child("portset-type", self.parameters['type']) @@ -183,17 +205,63 @@ class NetAppONTAPPortset(object): (self.parameters['name'], to_native(error)), exception=traceback.format_exc()) + def remove_ports(self, ports): + """ + Removes all existing ports from portset + :return: None + """ + for port in ports: + self.modify_port(port, 'portset-remove') + + def add_ports(self): + """ + Add the list of ports to portset + :return: None + """ + # don't add if ports is empty string + if self.parameters.get('ports') == [''] or self.parameters.get('ports') is None: + return + for port in self.parameters['ports']: + self.modify_port(port, 'portset-add') + + def modify_port(self, port, zapi): + """ + Add or remove an port to/from a portset + """ + port.strip() # remove leading spaces if any (eg: if user types a space after comma in initiators list) + options = {'portset-name': self.parameters['name'], + 'portset-port-name': port} + + portset_modify = netapp_utils.zapi.NaElement.create_node_with_children(zapi, **options) + + try: + self.server.invoke_successfully(portset_modify, enable_tunneling=True) + except netapp_utils.zapi.NaApiError as error: + self.module.fail_json(msg='Error modifying port in portset %s: %s' % (self.parameters['name'], + to_native(error)), + exception=traceback.format_exc()) + def apply(self): """ Applies action from playbook """ netapp_utils.ems_log_event("na_ontap_autosupport", self.server) - current = self.portset_get() + current, modify = self.portset_get(), None cd_action = self.na_helper.get_cd_action(current, self.parameters) - if cd_action == 'create': - self.create_portset() - elif cd_action == 'delete': - self.delete_portset() + if cd_action is None and self.parameters['state'] == 'present': + modify = self.na_helper.get_modified_attributes(current, self.parameters) + + if self.na_helper.changed: + if self.module.check_mode: + pass + else: + if cd_action == 'create': + self.create_portset() + elif cd_action == 'delete': + self.delete_portset() + elif modify: + self.remove_ports(current['ports']) + self.add_ports() self.module.exit_json(changed=self.na_helper.changed) diff --git a/test/units/modules/storage/netapp/test_na_ontap_portset.py b/test/units/modules/storage/netapp/test_na_ontap_portset.py index 205c06ee27..2a80fd3e48 100644 --- a/test/units/modules/storage/netapp/test_na_ontap_portset.py +++ b/test/units/modules/storage/netapp/test_na_ontap_portset.py @@ -75,7 +75,8 @@ class MockONTAPConnection(object): xml = netapp_utils.zapi.NaElement('xml') data = {'num-records': 1, 'attributes-list': {'portset-info': {'portset-name': portset, - 'vserver': vserver, 'portset-type': type}}} + 'vserver': vserver, 'portset-type': type, + 'portset-port-total': '0'}}} xml.translate_struct(data) print(xml.to_string()) return xml @@ -134,7 +135,7 @@ class TestMyModule(unittest.TestCase): assert portset is None def test_ensure_portset_apply_called(self): - ''' a more interesting test ''' + ''' Test successful create ''' module_args = {'name': 'create'} module_args.update(self.set_default_args()) set_module_args(module_args) @@ -153,10 +154,33 @@ class TestMyModule(unittest.TestCase): portset = my_obj.portset_get() print('Info: test_portset_get: %s' % repr(portset)) assert portset is not None - assert 'create' == portset['portset_name'] with pytest.raises(AnsibleExitJson) as exc: my_obj.apply() print('Info: test_portset_apply: %s' % repr(exc.value)) assert exc.value.args[0]['changed'] - portset = my_obj.portset_get() - assert 'create' == portset['portset_name'] + + def test_modify_ports(self): + ''' Test modify_portset method ''' + module_args = {'ports': ['l1', 'l2']} + module_args.update(self.set_default_args()) + set_module_args(module_args) + my_obj = my_module() + if not self.use_vsim: + my_obj.server = MockONTAPConnection('portset') + with pytest.raises(AnsibleExitJson) as exc: + my_obj.apply() + print('Info: test_portset_apply: %s' % repr(exc.value)) + assert exc.value.args[0]['changed'] + + def test_delete_portset(self): + ''' Test successful delete ''' + module_args = {'state': 'absent'} + module_args.update(self.set_default_args()) + set_module_args(module_args) + my_obj = my_module() + if not self.use_vsim: + my_obj.server = MockONTAPConnection('portset') + with pytest.raises(AnsibleExitJson) as exc: + my_obj.apply() + print('Info: test_portset_apply: %s' % repr(exc.value)) + assert exc.value.args[0]['changed']