From f184b5f33240242e016a739821def2d2059eff1d Mon Sep 17 00:00:00 2001 From: Helen Walsh Date: Mon, 16 May 2016 17:32:34 +0100 Subject: [PATCH] EMC VMAX - iSCSI Multipath support iSCSI Multipathing allows you to configure multiple I/O paths between server nodes and VMAX2 or VMAX3 by supporting multiple iSCSI IP portals. DocImpact Change-Id: Ie9fa678e61aa31ed5cca5c846a2dcecf2e107641 Implements: blueprint vmax-iscsi-multipath --- .../unit/volume/drivers/emc/test_emc_vmax.py | 71 +++++++- cinder/volume/drivers/emc/emc_vmax_common.py | 16 +- cinder/volume/drivers/emc/emc_vmax_iscsi.py | 155 ++++++++++++------ ...vmax-iscsi-multipath-76cc09bacf4fdfbf.yaml | 3 + 4 files changed, 183 insertions(+), 62 deletions(-) create mode 100644 releasenotes/notes/vmax-iscsi-multipath-76cc09bacf4fdfbf.yaml diff --git a/cinder/tests/unit/volume/drivers/emc/test_emc_vmax.py b/cinder/tests/unit/volume/drivers/emc/test_emc_vmax.py index 521ce151459..5ae3896e86e 100644 --- a/cinder/tests/unit/volume/drivers/emc/test_emc_vmax.py +++ b/cinder/tests/unit/volume/drivers/emc/test_emc_vmax.py @@ -1921,8 +1921,10 @@ class EMCVMAXISCSIDriverNoFastTestCase(test.TestCase): def fake_do_iscsi_discovery(self, volume): output = [] - item = '10.10.0.50: 3260,1 iqn.1992-04.com.emc: 50000973f006dd80' - output.append(item) + properties = {} + properties['target_portal'] = '10.10.0.50:3260' + properties['target_iqn'] = 'iqn.1992-04.com.emc:50000973f006dd80' + output.append(properties) return output def fake_sleep(self, seconds): @@ -3958,8 +3960,10 @@ class EMCVMAXISCSIDriverFastTestCase(test.TestCase): def fake_do_iscsi_discovery(self, volume): output = [] - item = '10.10.0.50: 3260,1 iqn.1992-04.com.emc: 50000973f006dd80' - output.append(item) + properties = {} + properties['target_portal'] = '10.10.0.50:3260' + properties['target_iqn'] = 'iqn.1992-04.com.emc:50000973f006dd80' + output.append(properties) return output def fake_sleep(self, seconds): @@ -6672,8 +6676,10 @@ class EMCV2MultiPoolDriverTestCase(test.TestCase): def fake_do_iscsi_discovery(self, volume): output = [] - item = '10.10.0.50: 3260,1 iqn.1992-04.com.emc: 50000973f006dd80' - output.append(item) + properties = {} + properties['target_portal'] = '10.10.0.50:3260' + properties['target_iqn'] = 'iqn.1992-04.com.emc:50000973f006dd80' + output.append(properties) return output def fake_sleep(self, seconds): @@ -8189,3 +8195,56 @@ class EMCVMAXProvisionTest(test.TestCase): masking.provision.add_members_to_masking_group.assert_called_with( conn, controllerConfigService, storageGroupInstanceName, volumeInstanceName, volumeName, extraSpecs) + + +class EMCVMAXISCSITest(test.TestCase): + def setUp(self): + self.data = EMCVMAXCommonData() + + super(EMCVMAXISCSITest, self).setUp() + + configuration = mock.Mock() + configuration.safe_get.return_value = 'iSCSITests' + configuration.config_group = 'iSCSITests' + self.mock_object(emc_vmax_iscsi.EMCVMAXISCSIDriver, + 'smis_do_iscsi_discovery', + self.fake_do_iscsi_discovery) + emc_vmax_common.EMCVMAXCommon._gather_info = mock.Mock() + driver = emc_vmax_iscsi.EMCVMAXISCSIDriver(configuration=configuration) + driver.db = FakeDB() + self.driver = driver + + def fake_do_iscsi_discovery(self, volume): + output = [] + properties = {} + properties['target_portal'] = '10.10.0.50:3260' + properties['target_iqn'] = 'iqn.1992-04.com.emc:50000973f006dd80' + output.append(properties) + properties = {} + properties['target_portal'] = '10.10.0.51:3260' + properties['target_iqn'] = 'iqn.1992-04.com.emc:50000973f006dd81' + output.append(properties) + return output + + def test_parse_target_list(self): + targets = ["10.10.10.31:3260,0 iqn.1f:29.ID2", + "10.10.10.32:3260,0 iqn.2f:29.ID2"] + out_targets = self.driver._parse_target_list(targets) + self.assertEqual('10.10.10.31:3260', out_targets[0]['target_portal']) + self.assertEqual('iqn.1f:29.ID2', out_targets[0]['target_iqn']) + self.assertEqual('10.10.10.32:3260', out_targets[1]['target_portal']) + self.assertEqual('iqn.2f:29.ID2', out_targets[1]['target_iqn']) + + def test_smis_get_iscsi_properties(self): + self.driver.iscsi_ip_addresses = ['10.10.0.50', '10.10.0.51'] + device_info = {'hostlunid': 1} + self.driver.common.find_device_number = ( + mock.Mock(return_value=device_info)) + properties = self.driver.smis_get_iscsi_properties( + self.data.test_volume, self.data.connector, True) + self.assertEqual([1, 1], properties['target_luns']) + self.assertEqual(['iqn.1992-04.com.emc:50000973f006dd80', + 'iqn.1992-04.com.emc:50000973f006dd81'], + properties['target_iqns']) + self.assertEqual(['10.10.0.50:3260', '10.10.0.51:3260'], + properties['target_portals']) diff --git a/cinder/volume/drivers/emc/emc_vmax_common.py b/cinder/volume/drivers/emc/emc_vmax_common.py index c1e68bf8533..a7bd8c8862e 100644 --- a/cinder/volume/drivers/emc/emc_vmax_common.py +++ b/cinder/volume/drivers/emc/emc_vmax_common.py @@ -385,6 +385,7 @@ class EMCVMAXCommon(object): """ portGroupName = None extraSpecs = self._initial_setup(volume) + is_multipath = connector.get('multipath', False) volumeName = volume['name'] LOG.info(_LI("Initialize connection: %(volume)s."), @@ -421,11 +422,13 @@ class EMCVMAXCommon(object): volume, connector, extraSpecs, maskingViewDict)) if self.protocol.lower() == 'iscsi': - return self._find_ip_protocol_endpoints( - self.conn, deviceInfoDict['storagesystem'], - portGroupName) - else: - return deviceInfoDict + deviceInfoDict['iscsi_ip_addresses'] = ( + self._find_ip_protocol_endpoints( + self.conn, deviceInfoDict['storagesystem'], + portGroupName)) + deviceInfoDict['is_multipath'] = is_multipath + + return deviceInfoDict def _attach_volume(self, volume, connector, extraSpecs, maskingViewDict, isLiveMigration=False): @@ -4453,7 +4456,8 @@ class EMCVMAXCommon(object): ipaddress = ( self.utils.get_iscsi_ip_address( conn, ipendpointinstancename)) - foundipaddresses.append(ipaddress) + if ipaddress: + foundipaddresses.append(ipaddress) return foundipaddresses def _extend_v3_volume(self, volumeInstance, volumeName, newSize, diff --git a/cinder/volume/drivers/emc/emc_vmax_iscsi.py b/cinder/volume/drivers/emc/emc_vmax_iscsi.py index c9e89c76a43..a5df907dbfe 100644 --- a/cinder/volume/drivers/emc/emc_vmax_iscsi.py +++ b/cinder/volume/drivers/emc/emc_vmax_iscsi.py @@ -77,6 +77,8 @@ class EMCVMAXISCSIDriver(driver.ISCSIDriver): - SnapVX licensing checks for VMAX3 (bug #1587017) - VMAX oversubscription Support (blueprint vmax-oversubscription) - QoS support (blueprint vmax-qos) + - VMAX2/VMAX3 iscsi multipath support (iscsi only) + https://blueprints.launchpad.net/cinder/+spec/vmax-iscsi-multipath """ @@ -187,13 +189,32 @@ class EMCVMAXISCSIDriver(driver.ISCSIDriver): 'volume_id': '12345678-1234-4321-1234-123456789012', } } - + Example return value (multipath is enabled):: + { + 'driver_volume_type': 'iscsi' + 'data': { + 'target_discovered': True, + 'target_iqns': ['iqn.2010-10.org.openstack:volume-00001', + 'iqn.2010-10.org.openstack:volume-00002'], + 'target_portals': ['127.0.0.1:3260', '127.0.1.1:3260'], + 'target_luns': [1, 1], + } + } """ - self.iscsi_ip_addresses = self.common.initialize_connection( + device_info = self.common.initialize_connection( volume, connector) + try: + self.iscsi_ip_addresses = device_info['iscsi_ip_addresses'] + is_multipath = device_info['is_multipath'] + except KeyError as ex: + exception_message = (_("Cannot get iSCSI ipaddresses or " + "multipath flag. Exception is %(ex)s. ") + % {'ex': ex}) + + raise exception.VolumeBackendAPIException(data=exception_message) iscsi_properties = self.smis_get_iscsi_properties( - volume, connector) + volume, connector, is_multipath) LOG.info(_LI("Leaving initialize_connection: %s"), iscsi_properties) return { @@ -208,9 +229,9 @@ class EMCVMAXISCSIDriver(driver.ISCSIDriver): '-t', 'sendtargets', '-p', iscsi_ip_address, run_as_root=True) - return out, _err, False, None + return out, _err, None except Exception as ex: - return None, None, True, ex + return None, None, ex def smis_do_iscsi_discovery(self, volume): """Calls iscsiadm with each iscsi ip address in the list""" @@ -219,12 +240,13 @@ class EMCVMAXISCSIDriver(driver.ISCSIDriver): if len(self.iscsi_ip_addresses) == 0: LOG.error(_LE("The list of iscsi_ip_addresses is empty")) return targets - + outList = [] for iscsi_ip_address in self.iscsi_ip_addresses: - out, _err, go_again, ex = self._call_iscsiadm(iscsi_ip_address) - if not go_again: - break - if not out: + out, _err, ex = self._call_iscsiadm(iscsi_ip_address) + if out: + outList.append(out) + + if len(outList) == 0: if ex: exception_message = (_("Unsuccessful iscsiadm. " "Exception is %(ex)s. ") @@ -236,74 +258,104 @@ class EMCVMAXISCSIDriver(driver.ISCSIDriver): LOG.info(_LI( "smis_do_iscsi_discovery is: %(out)s."), {'out': out}) + for out in outList: + for target in out.splitlines(): + targets.append(target) - for target in out.splitlines(): - targets.append(target) + outTargets = self._parse_target_list(targets) + return outTargets - return targets + def _parse_target_list(self, targets): + """Parse target list into usable format. - def smis_get_iscsi_properties(self, volume, connector): + :param targets: list of all targets + :return: outTargets + """ + outTargets = [] + for target in targets: + results = target.split(" ") + properties = {} + properties['target_portal'] = results[0].split(",")[0] + properties['target_iqn'] = results[1] + outTargets.append(properties) + return outTargets + + def smis_get_iscsi_properties(self, volume, connector, is_multipath): """Gets iscsi configuration. We ideally get saved information in the volume entity, but fall back to discovery if need be. Discovery may be completely removed in future The properties are: - - - `target_discovered` - boolean indicating whether discovery was - used - - `target_iqn` - the IQN of the iSCSI target - - `target_portal` - the portal of the iSCSI target - - `target_lun` - the lun of the iSCSI target - - `volume_id` - the UUID of the volume - - `auth_method`, `auth_username`, `auth_password` - the - authentication details. Right now, either auth_method is not - present meaning no authentication, or auth_method == `CHAP` - meaning use CHAP with the specified credentials. - + :target_discovered: boolean indicating whether discovery was used + :target_iqn: the IQN of the iSCSI target + :target_portal: the portal of the iSCSI target + :target_lun: the lun of the iSCSI target + :volume_id: the UUID of the volume + :auth_method:, :auth_username:, :auth_password: + the authentication details. Right now, either auth_method is not + present meaning no authentication, or auth_method == `CHAP` + meaning use CHAP with the specified credentials. """ - properties = {} - location = self.smis_do_iscsi_discovery(volume) - if not location: + targets = self.smis_do_iscsi_discovery(volume) + if len(targets) == 0: raise exception.InvalidVolume(_("Could not find iSCSI export " - " for volume %(volumeName)s.") + "for volume %(volumeName)s.") % {'volumeName': volume['name']}) - LOG.debug("ISCSI Discovery: Found %s", location) - properties['target_discovered'] = True + LOG.debug("ISCSI Discovery: Found %s", targets) device_info = self.common.find_device_number( volume, connector['host']) - if device_info is None or device_info['hostlunid'] is None: + isError = False + if device_info: + try: + lun_id = device_info['hostlunid'] + except KeyError: + isError = True + else: + isError = True + + if isError: + LOG.error(_LE("Unable to get the lun id")) exception_message = (_("Cannot find device number for volume " "%(volumeName)s.") % {'volumeName': volume['name']}) raise exception.VolumeBackendAPIException(data=exception_message) - device_number = device_info['hostlunid'] + properties = {'target_discovered': False, + 'target_iqn': 'unknown', + 'target_iqns': None, + 'target_portal': 'unknown', + 'target_portals': None, + 'target_lun': 'unknown', + 'target_luns': None, + 'volume_id': volume['id']} + + if len(self.iscsi_ip_addresses) > 0: + if len(self.iscsi_ip_addresses) > 1 and is_multipath: + properties['target_iqns'] = [t['target_iqn'] for t in targets] + properties['target_portals'] = ( + [t['target_portal'] for t in targets]) + properties['target_luns'] = [lun_id] * len(targets) + properties['target_discovered'] = True + properties['target_iqn'] = [t['target_iqn'] for t in targets][0] + properties['target_portal'] = ( + [t['target_portal'] for t in targets][0]) + properties['target_lun'] = lun_id + else: + LOG.error(_LE('Failed to find available iSCSI targets.')) LOG.info(_LI( - "location is: %(location)s"), {'location': location}) - - for loc in location: - results = loc.split(" ") - properties['target_portal'] = results[0].split(",")[0] - properties['target_iqn'] = results[1] - - properties['target_lun'] = device_number - - properties['volume_id'] = volume['id'] - + "ISCSI properties: %(properties)s."), {'properties': properties}) LOG.info(_LI( - "ISCSI properties: %(properties)s"), {'properties': properties}) - LOG.info(_LI( - "ISCSI volume is: %(volume)s"), {'volume': volume}) + "ISCSI volume is: %(volume)s."), {'volume': volume}) if 'provider_auth' in volume: auth = volume['provider_auth'] LOG.info(_LI( - "AUTH properties: %(authProps)s"), {'authProps': auth}) + "AUTH properties: %(authProps)s."), {'authProps': auth}) if auth is not None: (auth_method, auth_username, auth_secret) = auth.split() @@ -412,7 +464,10 @@ class EMCVMAXISCSIDriver(driver.ISCSIDriver): return self.common.manage_existing_get_size(volume, external_ref) def unmanage(self, volume): - """Export VMAX volume and leave volume intact on the backend array.""" + """Export VMAX volume from Cinder. + + Leave the volume intact on the backend array. + """ return self.common.unmanage(volume) def update_consistencygroup(self, context, group, diff --git a/releasenotes/notes/vmax-iscsi-multipath-76cc09bacf4fdfbf.yaml b/releasenotes/notes/vmax-iscsi-multipath-76cc09bacf4fdfbf.yaml new file mode 100644 index 00000000000..58b99575fd7 --- /dev/null +++ b/releasenotes/notes/vmax-iscsi-multipath-76cc09bacf4fdfbf.yaml @@ -0,0 +1,3 @@ +--- +features: + - VMAX driver iSCSI Multipathing.