From e4ee4e953f8f6b1575b37ed50fc22d210a04cd82 Mon Sep 17 00:00:00 2001 From: Naoki Saito Date: Thu, 8 Aug 2019 17:54:18 +0900 Subject: [PATCH] NEC Driver: Support multi-attach Adding support for multi-attach in NEC driver. Change-Id: I345165d84ff7c75c5a55e1277111f9f93cc0ca55 --- .../unit/volume/drivers/nec/test_volume.py | 200 ++++++++++++++++-- cinder/volume/drivers/nec/volume.py | 50 ++++- cinder/volume/drivers/nec/volume_helper.py | 30 +++ .../drivers/nec-storage-m-series-driver.rst | 2 +- doc/source/reference/support-matrix.ini | 2 +- ...support-multi-attach-8aae5100f513656c.yaml | 5 + 6 files changed, 268 insertions(+), 21 deletions(-) create mode 100644 releasenotes/notes/nec-support-multi-attach-8aae5100f513656c.yaml diff --git a/cinder/tests/unit/volume/drivers/nec/test_volume.py b/cinder/tests/unit/volume/drivers/nec/test_volume.py index 41db89c2b9c..a322991226a 100644 --- a/cinder/tests/unit/volume/drivers/nec/test_volume.py +++ b/cinder/tests/unit/volume/drivers/nec/test_volume.py @@ -17,9 +17,12 @@ import ddt import mock +from cinder import context from cinder import exception +from cinder.objects import volume_attachment from cinder import test from cinder.tests.unit import fake_constants as constants +from cinder.tests.unit.fake_volume import fake_volume_obj from cinder.volume import configuration as conf from cinder.volume.drivers.nec import cli from cinder.volume.drivers.nec import volume_common @@ -360,6 +363,7 @@ class DummyVolume(object): self.volume_id = None self.volume_type_id = None self.attach_status = None + self.volume_attachment = None self.provider_location = None @@ -781,24 +785,85 @@ class ExportTest(volume_helper.MStorageDSVDriver, test.TestCase): self.assertEqual(88, info['data']['target_luns'][1]) def test_iscsi_terminate_connection(self): + ctx = context.RequestContext('admin', 'fake', True) + vol = fake_volume_obj(ctx, id='46045673-41e7-44a7-9333-02f07feab04b') connector = {'initiator': "iqn.1994-05.com.redhat:d1d8e8f23255", - 'multipath': True} - ret = self._iscsi_terminate_connection(self.vol, connector) - self.assertIsNone(ret) + 'multipath': True, 'host': 'DummyHost'} + attachment = { + 'id': constants.ATTACHMENT_ID, + 'volume_id': vol.id, + 'connector': connector + } + attach_object = volume_attachment.VolumeAttachment(**attachment) + attachment = volume_attachment.VolumeAttachmentList( + objects=[attach_object]) + vol.volume_attachment = attachment + with mock.patch.object(self._cli, 'delldsetld', + return_value=(True, '') + ) as delldsetld_mock: + ret = self._iscsi_terminate_connection(vol, connector) + delldsetld_mock.assert_called_once_with( + 'LX:OpenStack0', 'LX:287RbQoP7VdwR1WsPC2fZT') + self.assertIsNone(ret) + + attachment1 = { + 'id': constants.ATTACHMENT_ID, + 'volume_id': vol.id, + 'connector': connector + } + attachment2 = { + 'id': constants.ATTACHMENT2_ID, + 'volume_id': vol.id, + 'connector': connector + } + attach_object1 = volume_attachment.VolumeAttachment(**attachment1) + attach_object2 = volume_attachment.VolumeAttachment(**attachment2) + attachments = volume_attachment.VolumeAttachmentList( + objects=[attach_object1, attach_object2]) + vol.volume_attachment = attachments + with mock.patch.object(self._cli, 'delldsetld', + return_value=(True, '') + ) as delldsetld_mock: + ret = self._iscsi_terminate_connection(vol, connector) + delldsetld_mock.assert_not_called() + self.assertIsNone(ret) def test_iscsi_terminate_connection_negative(self): + ctx = context.RequestContext('admin', 'fake', True) + vol = fake_volume_obj(ctx, id='46045673-41e7-44a7-9333-02f07feab04b') connector = {'initiator': "iqn.1994-05.com.redhat:d1d8e8f23255", - 'multipath': True} + 'multipath': True, 'host': 'DummyHost'} + attachment = { + 'id': constants.ATTACHMENT_ID, + 'volume_id': vol.id, + 'connector': connector + } + attach_object = volume_attachment.VolumeAttachment(**attachment) + attachment = volume_attachment.VolumeAttachmentList( + objects=[attach_object]) + vol.volume_attachment = attachment with self.assertRaisesRegex(exception.VolumeBackendAPIException, r'Failed to unregister Logical Disk from' r' Logical Disk Set \(iSM31064\)'): self.mock_object(self._cli, 'delldsetld', return_value=(False, 'iSM31064')) - self._iscsi_terminate_connection(self.vol, connector) + self._iscsi_terminate_connection(vol, connector) def test_fc_initialize_connection(self): - connector = {'wwpns': ["10000090FAA0786A", "10000090FAA0786B"]} - info = self._fc_initialize_connection(self.vol, connector) + ctx = context.RequestContext('admin', 'fake', True) + vol = fake_volume_obj(ctx, id='46045673-41e7-44a7-9333-02f07feab04b') + connector = {'wwpns': ["10000090FAA0786A", "10000090FAA0786B"], + 'host': 'DummyHost'} + attachment = { + 'id': constants.ATTACHMENT_ID, + 'volume_id': vol.id, + 'connector': connector + } + attach_object = volume_attachment.VolumeAttachment(**attachment) + attachment = volume_attachment.VolumeAttachmentList( + objects=[attach_object]) + vol.volume_attachment = attachment + info = self._fc_initialize_connection(vol, connector) self.assertEqual('fibre_channel', info['driver_volume_type']) self.assertEqual('2100000991020012', info['data']['target_wwn'][0]) self.assertEqual('2200000991020012', info['data']['target_wwn'][1]) @@ -833,11 +898,61 @@ class ExportTest(volume_helper.MStorageDSVDriver, test.TestCase): r' Logical Disk Set \(iSM31064\)'): self.mock_object(self._cli, 'delldsetld', return_value=(False, 'iSM31064')) - self._fc_terminate_connection(self.vol, connector) + self._fc_terminate_connection(vol, connector) + ctx = context.RequestContext('admin', 'fake', True) + vol = fake_volume_obj(ctx, id='46045673-41e7-44a7-9333-02f07feab04b') + attachment = { + 'id': constants.ATTACHMENT_ID, + 'volume_id': vol.id, + 'connector': connector + } + attach_object = volume_attachment.VolumeAttachment(**attachment) + attachment = volume_attachment.VolumeAttachmentList( + objects=[attach_object]) + vol.volume_attachment = attachment + with mock.patch.object(self._cli, 'delldsetld', + return_value=(True, '') + ) as delldsetld_mock: + self._fc_terminate_connection(vol, connector) + delldsetld_mock.assert_called_once_with( + 'LX:OpenStack1', 'LX:287RbQoP7VdwR1WsPC2fZT') + + attachment1 = { + 'id': constants.ATTACHMENT_ID, + 'volume_id': vol.id, + 'connector': connector + } + attachment2 = { + 'id': constants.ATTACHMENT2_ID, + 'volume_id': vol.id, + 'connector': connector + } + attach_object1 = volume_attachment.VolumeAttachment(**attachment1) + attach_object2 = volume_attachment.VolumeAttachment(**attachment2) + attachments = volume_attachment.VolumeAttachmentList( + objects=[attach_object1, attach_object2]) + vol.volume_attachment = attachments + with mock.patch.object(self._cli, 'delldsetld', + return_value=(True, '') + ) as delldsetld_mock: + self._fc_terminate_connection(vol, connector) + delldsetld_mock.assert_not_called() def test_fc_terminate_connection(self): - connector = {'wwpns': ["10000090FAA0786A", "10000090FAA0786B"]} - info = self._fc_terminate_connection(self.vol, connector) + ctx = context.RequestContext('admin', 'fake', True) + vol = fake_volume_obj(ctx, id='46045673-41e7-44a7-9333-02f07feab04b') + connector = {'wwpns': ["10000090FAA0786A", "10000090FAA0786B"], + 'host': 'DummyHost'} + attachment = { + 'id': constants.ATTACHMENT_ID, + 'volume_id': vol.id, + 'connector': connector + } + attach_object = volume_attachment.VolumeAttachment(**attachment) + attachment = volume_attachment.VolumeAttachmentList( + objects=[attach_object]) + vol.volume_attachment = attachment + info = self._fc_terminate_connection(vol, connector) self.assertEqual('fibre_channel', info['driver_volume_type']) self.assertEqual('2100000991020012', info['data']['target_wwn'][0]) self.assertEqual('2200000991020012', info['data']['target_wwn'][1]) @@ -867,10 +982,43 @@ class ExportTest(volume_helper.MStorageDSVDriver, test.TestCase): self.assertEqual( '2A00000991020012', info['data']['initiator_target_map']['10000090FAA0786B'][3]) - info = self._fc_terminate_connection(self.vol, None) + info = self._fc_terminate_connection(vol, None) self.assertEqual('fibre_channel', info['driver_volume_type']) self.assertEqual({}, info['data']) + def test_is_multi_attachment(self): + ctx = context.RequestContext('admin', 'fake', True) + vol = fake_volume_obj(ctx, id=constants.VOLUME_ID) + connector = {'wwpns': ["10000090FAA0786A", "10000090FAA0786B"], + 'host': 'DummyHost'} + attachment1 = { + 'id': constants.ATTACHMENT_ID, + 'volume_id': vol.id, + 'connector': connector + } + attachment2 = { + 'id': constants.ATTACHMENT2_ID, + 'volume_id': vol.id, + 'connector': connector + } + attach_object1 = volume_attachment.VolumeAttachment(**attachment1) + attach_object2 = volume_attachment.VolumeAttachment(**attachment2) + attachments = volume_attachment.VolumeAttachmentList( + objects=[attach_object1, attach_object2]) + vol.volume_attachment = attachments + ret = self._is_multi_attachment(vol, connector) + self.assertTrue(ret) + + attachments = volume_attachment.VolumeAttachmentList( + objects=[attach_object1]) + vol.volume_attachment = attachments + ret = self._is_multi_attachment(vol, connector) + self.assertFalse(ret) + + vol.volume_attachment = None + ret = self._is_multi_attachment(vol, connector) + self.assertFalse(ret) + def test_iscsi_portal_with_controller_node_name(self): self.vol.status = 'downloading' connector = {'initiator': "iqn.1994-05.com.redhat:d1d8e8f23255"} @@ -1010,11 +1158,33 @@ class NonDisruptiveBackup_test(volume_helper.MStorageDSVDriver, self.assertEqual('fibre_channel', ret['driver_volume_type']) def test_terminate_connection_snapshot(self): - connector = {'initiator': "iqn.1994-05.com.redhat:d1d8e8f23255"} - self.iscsi_terminate_connection_snapshot(self.vol, connector) + ctx = context.RequestContext('admin', 'fake', True) + vol = fake_volume_obj(ctx, id="46045673-41e7-44a7-9333-02f07feab04b") + connector = {'initiator': 'iqn.1994-05.com.redhat:d1d8e8f23255', + 'host': 'DummyHost'} + attachment = { + 'id': constants.ATTACHMENT_ID, + 'volume_id': vol.id, + 'connector': connector + } + attach_object = volume_attachment.VolumeAttachment(**attachment) + attachment = volume_attachment.VolumeAttachmentList( + objects=[attach_object]) + vol.volume_attachment = attachment + self.iscsi_terminate_connection_snapshot(vol, connector) - connector = {'wwpns': ["10000090FAA0786A", "10000090FAA0786B"]} - ret = self.fc_terminate_connection_snapshot(self.vol, connector) + connector = {'wwpns': ["10000090FAA0786A", "10000090FAA0786B"], + 'host': 'DummyHost'} + attachment = { + 'id': constants.ATTACHMENT_ID, + 'volume_id': vol.id, + 'connector': connector + } + attach_object = volume_attachment.VolumeAttachment(**attachment) + attachment = volume_attachment.VolumeAttachmentList( + objects=[attach_object]) + vol.volume_attachment = attachment + ret = self.fc_terminate_connection_snapshot(vol, connector) self.assertEqual('fibre_channel', ret['driver_volume_type']) def test_remove_export_snapshot(self): diff --git a/cinder/volume/drivers/nec/volume.py b/cinder/volume/drivers/nec/volume.py index a9849ed22d0..a077e6b30da 100644 --- a/cinder/volume/drivers/nec/volume.py +++ b/cinder/volume/drivers/nec/volume.py @@ -26,10 +26,31 @@ from cinder.zonemanager import utils as fczm_utils @interface.volumedriver class MStorageISCSIDriver(volume_helper.MStorageDSVDriver, driver.ISCSIDriver): - """M-Series Storage Snapshot iSCSI Driver.""" + """M-Series Storage Snapshot iSCSI Driver. + + .. code-block:: none + + Version history: + + 1.8.1 - First open source driver version. + 1.8.2 - Code refactoring. + 1.9.1 - Support optimal path for non-disruptive backup. + 1.9.2 - Support manage/unmanage and manage/unmanage snapshot. + Delete an unused configuration + parameter (ldset_controller_node_name). + Fixed bug #1705001: driver fails to start. + 1.10.1 - Support automatic configuration of SAN access control. + Fixed bug #1753375: SAN access remains permitted on the + source node. + 1.10.2 - Delete max volumes per pool limit. + 1.10.3 - Add faster clone status check. + Fixed bug #1777385: driver removed access permission from + the destination node after live-migraion. + Fixed bug #1778669: LUNs of detached volumes are never reused. + """ VERSION = '1.10.3' - WIKI_NAME = 'NEC_Cinder_CI' + CI_WIKI_NAME = 'NEC_Cinder_CI' def __init__(self, *args, **kwargs): super(MStorageISCSIDriver, self).__init__(*args, **kwargs) @@ -72,10 +93,31 @@ class MStorageISCSIDriver(volume_helper.MStorageDSVDriver, @interface.volumedriver class MStorageFCDriver(volume_helper.MStorageDSVDriver, driver.FibreChannelDriver): - """M-Series Storage Snapshot FC Driver.""" + """M-Series Storage Snapshot FC Driver. + + .. code-block:: none + + Version history: + + 1.8.1 - First open source driver version. + 1.8.2 - Code refactoring. + 1.9.1 - Support optimal path for non-disruptive backup. + 1.9.2 - Support manage/unmanage and manage/unmanage snapshot. + Delete an unused configuration + parameter (ldset_controller_node_name). + Fixed bug #1705001: driver fails to start. + 1.10.1 - Support automatic configuration of SAN access control. + Fixed bug #1753375: SAN access remains permitted on the + source node. + 1.10.2 - Delete max volumes per pool limit. + 1.10.3 - Add faster clone status check. + Fixed bug #1777385: driver removed access permission from + the destination node after live-migraion. + Fixed bug #1778669: LUNs of detached volumes are never reused. + """ VERSION = '1.10.3' - WIKI_NAME = 'NEC_Cinder_CI' + CI_WIKI_NAME = 'NEC_Cinder_CI' def __init__(self, *args, **kwargs): super(MStorageFCDriver, self).__init__(*args, **kwargs) diff --git a/cinder/volume/drivers/nec/volume_helper.py b/cinder/volume/drivers/nec/volume_helper.py index 8ae2d9a9f2d..ce9e271f0f0 100644 --- a/cinder/volume/drivers/nec/volume_helper.py +++ b/cinder/volume/drivers/nec/volume_helper.py @@ -1143,6 +1143,7 @@ class MStorageDriver(volume_common.MStorageVolumeCommon): {'msgparm': msgparm, 'exception': e}) return ret + @coordination.synchronized('mstorage_iscsi_terminate_{volume.id}') def iscsi_terminate_connection(self, volume, connector): msgparm = ('Volume ID = %(id)s, Connector = %(connector)s' % {'id': volume.id, 'connector': connector}) @@ -1166,6 +1167,9 @@ class MStorageDriver(volume_common.MStorageVolumeCommon): LOG.debug('Connector is not specified. Nothing to do.') return + if self._is_multi_attachment(volume, connector): + return + # delete unused access control setting. xml = self._cli.view_all(self._properties['ismview_path']) pools, lds, ldsets, used_ldns, hostports, max_ld_count = ( @@ -1312,6 +1316,7 @@ class MStorageDriver(volume_common.MStorageVolumeCommon): '(%(msgparm)s) (%(exception)s)', {'msgparm': msgparm, 'exception': e}) + @coordination.synchronized('mstorage_fc_terminate_{volume.id}') def fc_terminate_connection(self, volume, connector): msgparm = ('Volume ID = %(id)s, Connector = %(connector)s' % {'id': volume.id, 'connector': connector}) @@ -1332,6 +1337,10 @@ class MStorageDriver(volume_common.MStorageVolumeCommon): '(Volume ID = %(id)s, connector = %(connector)s) Start.', {'id': volume.id, 'connector': connector}) + if connector is not None and ( + self._is_multi_attachment(volume, connector)): + return + xml = self._cli.view_all(self._properties['ismview_path']) pools, lds, ldsets, used_ldns, hostports, max_ld_count = ( self.configs(xml)) @@ -1393,6 +1402,26 @@ class MStorageDriver(volume_common.MStorageVolumeCommon): '(%(msgparm)s) (%(exception)s)', {'msgparm': msgparm, 'exception': e}) + def _is_multi_attachment(self, volume, connector): + """Check the number of attached instances. + + Returns true if the volume is attached to multiple instances. + Returns false if the volume is attached to a single instance. + """ + host = connector['host'] + attach_list = volume.volume_attachment + + if attach_list is None: + return False + + host_list = [att.connector['host'] for att in attach_list if + att is not None and att.connector is not None] + if host_list.count(host) > 1: + LOG.info("Volume is attached to multiple instances on " + "this host.") + return True + return False + def _build_initiator_target_map(self, connector, fc_ports): target_wwns = [] for port in fc_ports: @@ -1419,6 +1448,7 @@ class MStorageDriver(volume_common.MStorageVolumeCommon): data['driver_version'] = self.VERSION data['reserved_percentage'] = self._properties['reserved_percentage'] data['QoS_support'] = True + data['multiattach'] = True data['location_info'] = (self._properties['cli_fip'] + ":" + (','.join(map(str, self._properties['pool_pools'])))) diff --git a/doc/source/configuration/block-storage/drivers/nec-storage-m-series-driver.rst b/doc/source/configuration/block-storage/drivers/nec-storage-m-series-driver.rst index 434aeaa785a..ed8bbdd2b1b 100644 --- a/doc/source/configuration/block-storage/drivers/nec-storage-m-series-driver.rst +++ b/doc/source/configuration/block-storage/drivers/nec-storage-m-series-driver.rst @@ -52,7 +52,7 @@ Supported operations - Efficient non-disruptive volume backup. - Manage and unmanage a volume. - Manage and unmanage a snapshot. - +- Attach a volume to multiple instances at once (multi-attach). Preparation ~~~~~~~~~~~ diff --git a/doc/source/reference/support-matrix.ini b/doc/source/reference/support-matrix.ini index ecf2ed3896c..da78b449fcd 100644 --- a/doc/source/reference/support-matrix.ini +++ b/doc/source/reference/support-matrix.ini @@ -786,7 +786,7 @@ driver.lenovo=complete driver.linbit_linstor=missing driver.lvm=complete driver.macrosan=missing -driver.nec=missing +driver.nec=complete driver.netapp_ontap=complete driver.netapp_solidfire=complete driver.nexenta=missing diff --git a/releasenotes/notes/nec-support-multi-attach-8aae5100f513656c.yaml b/releasenotes/notes/nec-support-multi-attach-8aae5100f513656c.yaml new file mode 100644 index 00000000000..2b3d86f04b4 --- /dev/null +++ b/releasenotes/notes/nec-support-multi-attach-8aae5100f513656c.yaml @@ -0,0 +1,5 @@ +--- +features: + - | + NEC Driver: Added multiattach support. +