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
This commit is contained in:
Helen Walsh 2016-05-16 17:32:34 +01:00
parent f33fc3b69b
commit f184b5f332
4 changed files with 183 additions and 62 deletions

View File

@ -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'])

View File

@ -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,

View File

@ -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,

View File

@ -0,0 +1,3 @@
---
features:
- VMAX driver iSCSI Multipathing.