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:
parent
f33fc3b69b
commit
f184b5f332
@ -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'])
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -0,0 +1,3 @@
|
||||
---
|
||||
features:
|
||||
- VMAX driver iSCSI Multipathing.
|
Loading…
x
Reference in New Issue
Block a user