diff --git a/cinder/tests/unit/volume/drivers/dell_emc/vnx/mocked_cinder.yaml b/cinder/tests/unit/volume/drivers/dell_emc/vnx/mocked_cinder.yaml index 639004f8010..d633db12e54 100644 --- a/cinder/tests/unit/volume/drivers/dell_emc/vnx/mocked_cinder.yaml +++ b/cinder/tests/unit/volume/drivers/dell_emc/vnx/mocked_cinder.yaml @@ -82,7 +82,11 @@ group: &group_base status: 'creating' replication_status: 'enabled' - +connector: &connector_base + _properties: + host: host_1 + initiator: ['iqn.2012-07.org.fake:01'] + ip: 192.168.1.111 ########################################################### # TestCommonAdapter, TestISCSIAdapter, TestFCAdapter @@ -331,6 +335,13 @@ test_auto_register_initiator_no_white_list: test_auto_register_initiator_no_port_to_reg: volume: *volume_base +test_terminate_connection: + volume: *volume_base + connector: *connector_base + +test_terminate_connection_force_detach: + volume: *volume_base + test_remove_host_access: volume: *volume_base diff --git a/cinder/tests/unit/volume/drivers/dell_emc/vnx/mocked_vnx.yaml b/cinder/tests/unit/volume/drivers/dell_emc/vnx/mocked_vnx.yaml index d6e4c97bbcb..9f7b78f0f53 100644 --- a/cinder/tests/unit/volume/drivers/dell_emc/vnx/mocked_vnx.yaml +++ b/cinder/tests/unit/volume/drivers/dell_emc/vnx/mocked_vnx.yaml @@ -1853,6 +1853,29 @@ test_build_provider_location: _properties: serial: 'vnx-serial' +test_terminate_connection: + sg: &sg_terminate_connection + _properties: + existed: True + vnx: + _methods: + get_sg: *sg_terminate_connection + +test_terminate_connection_force_detach: + sg: &sg_terminate_connection_force_detach_1 + _properties: + existed: True + sg: &sg_terminate_connection_force_detach_2 + _properties: + existed: True + sgs: &sgs_terminate_connection_force_detach + _methods: + shadow_copy: [*sg_terminate_connection_force_detach_1, + *sg_terminate_connection_force_detach_2] + vnx: + _methods: + get_sg: *sgs_terminate_connection_force_detach + test_remove_host_access: sg: &sg_remove_host_access _properties: diff --git a/cinder/tests/unit/volume/drivers/dell_emc/vnx/test_adapter.py b/cinder/tests/unit/volume/drivers/dell_emc/vnx/test_adapter.py index e475514fbef..b801dfd570e 100644 --- a/cinder/tests/unit/volume/drivers/dell_emc/vnx/test_adapter.py +++ b/cinder/tests/unit/volume/drivers/dell_emc/vnx/test_adapter.py @@ -1517,6 +1517,41 @@ class TestISCSIAdapter(test.TestCase): self.assertRaises(exception.InvalidConfigurationValue, vnx_iscsi._normalize_config) + @res_mock.mock_driver_input + @res_mock.patch_iscsi_adapter + def test_terminate_connection(self, adapter, mocked_res, mocked_input): + cinder_volume = mocked_input['volume'] + connector = mocked_input['connector'] + adapter.remove_host_access = mock.Mock() + adapter.update_storage_group_if_required = mock.Mock() + adapter.build_terminate_connection_return_data = mock.Mock() + adapter.terminate_connection_cleanup = mock.Mock() + + adapter.terminate_connection(cinder_volume, connector) + adapter.remove_host_access.assert_called_once() + adapter.update_storage_group_if_required.assert_called_once() + adapter.build_terminate_connection_return_data \ + .assert_called_once() + adapter.terminate_connection_cleanup.assert_called_once() + + @res_mock.mock_driver_input + @res_mock.patch_iscsi_adapter + def test_terminate_connection_force_detach(self, adapter, mocked_res, + mocked_input): + cinder_volume = mocked_input['volume'] + connector = None + adapter.remove_host_access = mock.Mock() + adapter.update_storage_group_if_required = mock.Mock() + adapter.build_terminate_connection_return_data = mock.Mock() + adapter.terminate_connection_cleanup = mock.Mock() + + adapter.terminate_connection(cinder_volume, connector) + adapter.remove_host_access.assert_called() + adapter.update_storage_group_if_required.assert_called() + adapter.build_terminate_connection_return_data \ + .assert_not_called() + adapter.terminate_connection_cleanup.assert_called() + class TestFCAdapter(test.TestCase): STORAGE_PROTOCOL = common.PROTOCOL_FC @@ -1626,3 +1661,38 @@ class TestFCAdapter(test.TestCase): self.assertEqual({'wwn1': ['5006016636E01CB2']}, tgt_map) get_mapping.assert_called_once_with( ['wwn1', 'wwn2'], ['5006016636E01CB2']) + + @res_mock.mock_driver_input + @res_mock.patch_iscsi_adapter + def test_terminate_connection(self, adapter, mocked_res, mocked_input): + cinder_volume = mocked_input['volume'] + connector = mocked_input['connector'] + adapter.remove_host_access = mock.Mock() + adapter.update_storage_group_if_required = mock.Mock() + adapter.build_terminate_connection_return_data = mock.Mock() + adapter.terminate_connection_cleanup = mock.Mock() + + adapter.terminate_connection(cinder_volume, connector) + adapter.remove_host_access.assert_called_once() + adapter.update_storage_group_if_required.assert_called_once() + adapter.build_terminate_connection_return_data \ + .assert_called_once() + adapter.terminate_connection_cleanup.assert_called_once() + + @res_mock.mock_driver_input + @res_mock.patch_iscsi_adapter + def test_terminate_connection_force_detach(self, adapter, mocked_res, + mocked_input): + cinder_volume = mocked_input['volume'] + connector = None + adapter.remove_host_access = mock.Mock() + adapter.update_storage_group_if_required = mock.Mock() + adapter.build_terminate_connection_return_data = mock.Mock() + adapter.terminate_connection_cleanup = mock.Mock() + + adapter.terminate_connection(cinder_volume, connector) + adapter.remove_host_access.assert_called() + adapter.update_storage_group_if_required.assert_called() + adapter.build_terminate_connection_return_data \ + .assert_not_called() + adapter.terminate_connection_cleanup.assert_called() diff --git a/cinder/volume/drivers/dell_emc/vnx/adapter.py b/cinder/volume/drivers/dell_emc/vnx/adapter.py index d3e56066fbd..8b6ce6f7f46 100644 --- a/cinder/volume/drivers/dell_emc/vnx/adapter.py +++ b/cinder/volume/drivers/dell_emc/vnx/adapter.py @@ -1010,19 +1010,32 @@ class CommonAdapter(replication.ReplicationAdapter): :param volume: `common.Volume` object with volume information. :param connector: connector information from Nova. """ - host = self.build_host(connector) - sg = self.client.get_storage_group(host.name) - self.remove_host_access(volume, host, sg) + # None `connector` means force detach the volume from all hosts. + is_force_detach = False + if connector is None: + LOG.info('Force detaching volume %s from all hosts.', volume.name) + is_force_detach = True - # build_terminate_connection return data should go before - # terminate_connection_cleanup. The storage group may be deleted in - # the terminate_connection_cleanup which is needed during getting - # return data - self.update_storage_group_if_required(sg) - re = self.build_terminate_connection_return_data(host, sg) - self.terminate_connection_cleanup(host, sg) + host = None if is_force_detach else self.build_host(connector) + sg_list = (self.client.filter_sg(volume.vnx_lun_id) if is_force_detach + else [self.client.get_storage_group(host.name)]) - return re + return_data = None + for sg in sg_list: + self.remove_host_access(volume, host, sg) + + # build_terminate_connection return data should go before + # terminate_connection_cleanup. The storage group may be deleted in + # the terminate_connection_cleanup which is needed during getting + # return data + self.update_storage_group_if_required(sg) + if not is_force_detach: + # force detach will return None + return_data = self.build_terminate_connection_return_data( + host, sg) + self.terminate_connection_cleanup(host, sg) + + return return_data def update_storage_group_if_required(self, sg): if sg.existed and self.destroy_empty_sg: @@ -1036,17 +1049,19 @@ class CommonAdapter(replication.ReplicationAdapter): :param sg: object of `storops` storage group. """ lun = self.client.get_lun(lun_id=volume.vnx_lun_id) - hostname = host.name if not sg.existed: - LOG.warning("Storage Group %s is not found. " - "Nothing can be done in terminate_connection().", - hostname) + # `host` is None when force-detach + if host is not None: + # Only print this warning message when normal detach + LOG.warning("Storage Group %s is not found. " + "Nothing can be done in terminate_connection().", + host.name) else: try: sg.detach_alu(lun) except storops_ex.VNXDetachAluNotFoundError: LOG.warning("Volume %(vol)s is not in Storage Group %(sg)s.", - {'vol': volume.name, 'sg': hostname}) + {'vol': volume.name, 'sg': sg.name}) def build_terminate_connection_return_data(self, host, sg): raise NotImplementedError() @@ -1064,7 +1079,8 @@ class CommonAdapter(replication.ReplicationAdapter): LOG.info("Storage Group %s is empty.", sg.name) sg.disconnect_host(sg.name) sg.delete() - if self.itor_auto_dereg: + if host is not None and self.itor_auto_dereg: + # `host` is None when force-detach self._deregister_initiator(host) except storops_ex.StoropsException: LOG.warning("Failed to destroy Storage Group %s.", diff --git a/cinder/volume/drivers/dell_emc/vnx/client.py b/cinder/volume/drivers/dell_emc/vnx/client.py index 3fda8e55a21..072bc9ece9f 100644 --- a/cinder/volume/drivers/dell_emc/vnx/client.py +++ b/cinder/volume/drivers/dell_emc/vnx/client.py @@ -726,3 +726,6 @@ class Client(object): def add_lun_to_ioclass(self, ioclass_name, lun_id): ioclass = self.vnx.get_ioclass(name=ioclass_name) ioclass.add_lun(lun_id) + + def filter_sg(self, attached_lun_id): + return self.vnx.get_sg().shadow_copy(attached_lun=attached_lun_id) diff --git a/doc/source/configuration/block-storage/drivers/emc-vnx-driver.rst b/doc/source/configuration/block-storage/drivers/emc-vnx-driver.rst index f64e679df19..afcb6fcbcb3 100644 --- a/doc/source/configuration/block-storage/drivers/emc-vnx-driver.rst +++ b/doc/source/configuration/block-storage/drivers/emc-vnx-driver.rst @@ -16,7 +16,7 @@ System requirements - VNX Operational Environment for Block version 5.32 or higher. - VNX Snapshot and Thin Provisioning license should be activated for VNX. -- Python library ``storops`` to interact with VNX. +- Python library ``storops`` version 0.5.7 or higher to interact with VNX. - Navisphere CLI v7.32 or higher is installed along with the driver. Supported operations @@ -599,6 +599,13 @@ Obsolete extra specs - ``storagetype:provisioning`` - ``storagetype:pool`` +Force detach +------------ + +The user could use `os-force_detach` action to detach a volume from all its attached hosts. +For more detail, please refer to +https://developer.openstack.org/api-ref/block-storage/v2/?expanded=force-detach-volume-detail#force-detach-volume + Advanced features ~~~~~~~~~~~~~~~~~ diff --git a/releasenotes/notes/vnx-add-force-detach-support-26f215e6f70cc03b.yaml b/releasenotes/notes/vnx-add-force-detach-support-26f215e6f70cc03b.yaml new file mode 100644 index 00000000000..0ce5bb387c0 --- /dev/null +++ b/releasenotes/notes/vnx-add-force-detach-support-26f215e6f70cc03b.yaml @@ -0,0 +1,4 @@ +--- +features: + - | + Add support to force detach a volume from all hosts on VNX.