Adds multipath support to the 3PAR iSCSI driver.
target portals, iqns and luns will be returned if multipath
is being used.
Also adds a new function 3par common that allows finding
of all existing VLUNs.
Implements: blueprint 3par-iscsi-multipath-support
Change-Id: I6b17cc9057eaf726b6a31921fd8a141c5a430d52
'initiator': 'iqn.1993-08.org.debian:01:222',
'wwpns': [wwn[0], wwn[1]],
'wwnns': ["223456789012345", "223456789054321"],
- 'host': FAKE_HOST}
+ 'host': FAKE_HOST,
+ 'multipath': False}
+
+ connector_multipath_enabled = {'ip': '10.0.0.2',
+ 'initiator': ('iqn.1993-08.org'
+ '.debian:01:222'),
+ 'wwpns': [wwn[0], wwn[1]],
+ 'wwnns': ["223456789012345",
+ "223456789054321"],
+ 'host': FAKE_HOST,
+ 'multipath': True}
volume_type = {'name': 'gold',
'deleted': False,
hpexceptions.HTTPNotFound('fake'),
[{'active': True,
'volumeName': self.VOLUME_3PAR_NAME,
- 'lun': 90, 'type': 0}]]
+ 'lun': 90, 'type': 0,
+ 'portPos': {'cardPort': 1, 'node': 7, 'slot': 1}, }]]
location = ("%(volume_name)s,%(lun_id)s,%(host)s,%(nsp)s" %
{'volume_name': self.VOLUME_3PAR_NAME,
'target_lun': TARGET_LUN,
'target_portal': '1.1.1.2:1234'}}
+ multipath_properties = {
+ 'driver_volume_type': 'iscsi',
+ 'data':
+ {'encrypted': False,
+ 'target_discovered': True,
+ 'target_iqns': [TARGET_IQN],
+ 'target_luns': [TARGET_LUN],
+ 'target_portals': ['1.1.1.2:1234']}}
+
def setup_driver(self, config=None, mock_conf=None, wsapi_version=None):
self.ctxt = context.get_admin_context()
self.assertDictMatch(result, self.properties)
+ def test_initialize_connection_multipath(self):
+ # setup_mock_client drive with default configuration
+ # and return the mock HTTP 3PAR client
+ mock_client = self.setup_driver()
+ mock_client.getVolume.return_value = {'userCPG': HP3PAR_CPG}
+ mock_client.getCPG.return_value = {}
+ mock_client.getHost.side_effect = [
+ hpexceptions.HTTPNotFound('fake'),
+ {'name': self.FAKE_HOST}]
+ mock_client.queryHost.return_value = {
+ 'members': [{
+ 'name': self.FAKE_HOST
+ }]
+ }
+
+ mock_client.getHostVLUNs.side_effect = [
+ hpexceptions.HTTPNotFound('fake'),
+ [{'active': True,
+ 'volumeName': self.VOLUME_3PAR_NAME,
+ 'lun': self.TARGET_LUN, 'type': 0,
+ 'portPos': {'node': 8, 'slot': 1, 'cardPort': 1}}]]
+
+ location = ("%(volume_name)s,%(lun_id)s,%(host)s,%(nsp)s" %
+ {'volume_name': self.VOLUME_3PAR_NAME,
+ 'lun_id': self.TARGET_LUN,
+ 'host': self.FAKE_HOST,
+ 'nsp': 'something'})
+ mock_client.createVLUN.return_value = location
+
+ mock_client.getiSCSIPorts.return_value = [{
+ 'IPAddr': '1.1.1.2',
+ 'iSCSIName': self.TARGET_IQN,
+ }]
+
+ with mock.patch.object(hpcommon.HP3PARCommon,
+ '_create_client') as mock_create_client:
+ mock_create_client.return_value = mock_client
+ result = self.driver.initialize_connection(
+ self.volume,
+ self.connector_multipath_enabled)
+
+ expected = [
+ mock.call.getVolume(self.VOLUME_3PAR_NAME),
+ mock.call.getCPG(HP3PAR_CPG),
+ mock.call.getHost(self.FAKE_HOST),
+ mock.call.queryHost(iqns=['iqn.1993-08.org.debian:01:222']),
+ mock.call.getHost(self.FAKE_HOST),
+ mock.call.getiSCSIPorts(
+ state=self.mock_client_conf['PORT_STATE_READY']),
+ mock.call.getHostVLUNs(self.FAKE_HOST),
+ mock.call.createVLUN(
+ self.VOLUME_3PAR_NAME,
+ auto=True,
+ hostname=self.FAKE_HOST,
+ portPos=self.FAKE_ISCSI_PORT['portPos']),
+ mock.call.getHostVLUNs(self.FAKE_HOST)]
+
+ mock_client.assert_has_calls(
+ self.standard_login +
+ expected +
+ self.standard_logout)
+
+ self.assertDictMatch(self.multipath_properties, result)
+
+ def test_initialize_connection_multipath_existing_nsp(self):
+ # setup_mock_client drive with default configuration
+ # and return the mock HTTP 3PAR client
+ mock_client = self.setup_driver()
+ mock_client.getVolume.return_value = {'userCPG': HP3PAR_CPG}
+ mock_client.getCPG.return_value = {}
+ mock_client.getHost.side_effect = [
+ hpexceptions.HTTPNotFound('fake'),
+ {'name': self.FAKE_HOST}]
+ mock_client.queryHost.return_value = {
+ 'members': [{
+ 'name': self.FAKE_HOST
+ }]
+ }
+
+ mock_client.getHostVLUNs.side_effect = [
+ [{'hostname': self.FAKE_HOST,
+ 'volumeName': self.VOLUME_3PAR_NAME,
+ 'lun': self.TARGET_LUN,
+ 'portPos': {'node': 8, 'slot': 1, 'cardPort': 1}}],
+ [{'active': True,
+ 'volumeName': self.VOLUME_3PAR_NAME,
+ 'lun': self.TARGET_LUN, 'type': 0}]]
+
+ mock_client.getiSCSIPorts.return_value = [{
+ 'IPAddr': '1.1.1.2',
+ 'iSCSIName': self.TARGET_IQN,
+ }]
+
+ with mock.patch.object(hpcommon.HP3PARCommon,
+ '_create_client') as mock_create_client:
+ mock_create_client.return_value = mock_client
+ result = self.driver.initialize_connection(
+ self.volume,
+ self.connector_multipath_enabled)
+
+ expected = [
+ mock.call.getVolume(self.VOLUME_3PAR_NAME),
+ mock.call.getCPG(HP3PAR_CPG),
+ mock.call.getHost(self.FAKE_HOST),
+ mock.call.queryHost(iqns=['iqn.1993-08.org.debian:01:222']),
+ mock.call.getHost(self.FAKE_HOST),
+ mock.call.getiSCSIPorts(
+ state=self.mock_client_conf['PORT_STATE_READY']),
+ mock.call.getHostVLUNs(self.FAKE_HOST)]
+
+ mock_client.assert_has_calls(
+ self.standard_login +
+ expected +
+ self.standard_logout)
+
+ self.assertDictMatch(self.multipath_properties, result)
+
def test_initialize_connection_encrypted(self):
# setup_mock_client drive with default configuration
# and return the mock HTTP 3PAR client
2.0.45 - Python 3 fixes
2.0.46 - Improved VLUN creation and deletion logic. #1469816
2.0.47 - Changed initialize_connection to use getHostVLUNs. #1475064
+ 2.0.48 - Adding changes to support 3PAR iSCSI multipath.
"""
- VERSION = "2.0.47"
+ VERSION = "2.0.48"
stats = {}
'reserved_percentage': 0,
'pools': pools}
- def _get_vlun(self, volume_name, hostname, lun_id=None):
+ def _get_vlun(self, volume_name, hostname, lun_id=None, nsp=None):
"""find a VLUN on a 3PAR host."""
vluns = self.client.getHostVLUNs(hostname)
found_vlun = None
for vlun in vluns:
if volume_name in vlun['volumeName']:
- if lun_id:
+ if lun_id is not None:
if vlun['lun'] == lun_id:
- found_vlun = vlun
- break
+ if nsp:
+ port = self.build_portPos(nsp)
+ if vlun['portPos'] == port:
+ found_vlun = vlun
+ break
+ else:
+ found_vlun = vlun
+ break
else:
found_vlun = vlun
break
"""
volume_name = self._get_3par_vol_name(volume['id'])
vlun_info = self._create_3par_vlun(volume_name, host['name'], nsp)
- return self._get_vlun(volume_name, host['name'], vlun_info['lun_id'])
+ return self._get_vlun(volume_name,
+ host['name'],
+ vlun_info['lun_id'],
+ nsp)
def delete_vlun(self, volume, hostname):
volume_name = self._get_3par_vol_name(volume['id'])
break
except hpexceptions.HTTPNotFound:
# ignore, no existing VLUNs were found
+ LOG.debug("No existing VLUNs were found for host/volume "
+ "combination: %(host)s, %(vol)s",
+ {'host': host['name'],
+ 'vol': vol_name})
pass
return existing_vlun
+ def find_existing_vluns(self, volume, host):
+ existing_vluns = []
+ try:
+ vol_name = self._get_3par_vol_name(volume['id'])
+ host_vluns = self.client.getHostVLUNs(host['name'])
+
+ # The first existing VLUN found will be returned.
+ for vlun in host_vluns:
+ if vlun['volumeName'] == vol_name:
+ existing_vluns.append(vlun)
+ break
+ except hpexceptions.HTTPNotFound:
+ # ignore, no existing VLUNs were found
+ LOG.debug("No existing VLUNs were found for host/volume "
+ "combination: %(host)s, %(vol)s",
+ {'host': host['name'],
+ 'vol': vol_name})
+ pass
+ return existing_vluns
+
class TaskWaiter(object):
"""TaskWaiter waits for task to be not active and returns status."""
2.0.17 - Python 3 fixes
2.0.18 - Improved VLUN creation and deletion logic. #1469816
2.0.19 - Changed initialize_connection to use getHostVLUNs. #1475064
+ 2.0.20 - Adding changes to support 3PAR iSCSI multipath.
"""
- VERSION = "2.0.19"
+ VERSION = "2.0.20"
def __init__(self, *args, **kwargs):
super(HP3PARISCSIDriver, self).__init__(*args, **kwargs)
volume,
connector)
- least_used_nsp = None
-
- # check if a VLUN already exists for this host
- existing_vlun = common.find_existing_vlun(volume, host)
-
- if existing_vlun:
- # We override the nsp here on purpose to force the
- # volume to be exported out the same IP as it already is.
- # This happens during nova live-migration, we want to
- # disable the picking of a different IP that we export
- # the volume to, or nova complains.
- least_used_nsp = common.build_nsp(existing_vlun['portPos'])
-
- if not least_used_nsp:
- least_used_nsp = self._get_least_used_nsp_for_host(
- common,
- host['name'])
-
- vlun = None
- if existing_vlun is None:
- # now that we have a host, create the VLUN
- vlun = common.create_vlun(volume, host, least_used_nsp)
+ if connector['multipath']:
+ ready_ports = common.client.getiSCSIPorts(
+ state=common.client.PORT_STATE_READY)
+
+ target_portals = []
+ target_iqns = []
+ target_luns = []
+
+ # Target portal ips are defined in cinder.conf.
+ target_portal_ips = self.iscsi_ips.keys()
+
+ # Collect all existing VLUNs for this volume/host combination.
+ existing_vluns = common.find_existing_vluns(volume, host)
+
+ # Cycle through each ready iSCSI port and determine if a new
+ # VLUN should be created or an existing one used.
+ for port in ready_ports:
+ iscsi_ip = port['IPAddr']
+ if iscsi_ip in target_portal_ips:
+ vlun = None
+ # check for an already existing VLUN matching the
+ # nsp for this iSCSI IP. If one is found, use it
+ # instead of creating a new VLUN.
+ for v in existing_vluns:
+ portPos = common.build_portPos(
+ self.iscsi_ips[iscsi_ip]['nsp'])
+ if v['portPos'] == portPos:
+ vlun = v
+ break
+ else:
+ vlun = common.create_vlun(
+ volume, host, self.iscsi_ips[iscsi_ip]['nsp'])
+ iscsi_ip_port = "%s:%s" % (
+ iscsi_ip, self.iscsi_ips[iscsi_ip]['ip_port'])
+ target_portals.append(iscsi_ip_port)
+ target_iqns.append(port['iSCSIName'])
+ target_luns.append(vlun['lun'])
+ else:
+ LOG.warning(_LW("iSCSI IP: '%s' was not found in "
+ "hp3par_iscsi_ips list defined in "
+ "cinder.conf."), iscsi_ip)
+
+ info = {'driver_volume_type': 'iscsi',
+ 'data': {'target_portals': target_portals,
+ 'target_iqns': target_iqns,
+ 'target_luns': target_luns,
+ 'target_discovered': True
+ }
+ }
else:
- vlun = existing_vlun
+ least_used_nsp = None
+
+ # check if a VLUN already exists for this host
+ existing_vlun = common.find_existing_vlun(volume, host)
+
+ if existing_vlun:
+ # We override the nsp here on purpose to force the
+ # volume to be exported out the same IP as it already is.
+ # This happens during nova live-migration, we want to
+ # disable the picking of a different IP that we export
+ # the volume to, or nova complains.
+ least_used_nsp = common.build_nsp(existing_vlun['portPos'])
+
+ if not least_used_nsp:
+ least_used_nsp = self._get_least_used_nsp_for_host(
+ common,
+ host['name'])
+
+ vlun = None
+ if existing_vlun is None:
+ # now that we have a host, create the VLUN
+ vlun = common.create_vlun(volume, host, least_used_nsp)
+ else:
+ vlun = existing_vlun
- if least_used_nsp is None:
- LOG.warning(_LW("Least busy iSCSI port not found, "
- "using first iSCSI port in list."))
- iscsi_ip = self.iscsi_ips.keys()[0]
- else:
- iscsi_ip = self._get_ip_using_nsp(least_used_nsp)
-
- iscsi_ip_port = self.iscsi_ips[iscsi_ip]['ip_port']
- iscsi_target_iqn = self.iscsi_ips[iscsi_ip]['iqn']
- info = {'driver_volume_type': 'iscsi',
- 'data': {'target_portal': "%s:%s" %
- (iscsi_ip, iscsi_ip_port),
- 'target_iqn': iscsi_target_iqn,
- 'target_lun': vlun['lun'],
- 'target_discovered': True
- }
- }
+ if least_used_nsp is None:
+ LOG.warning(_LW("Least busy iSCSI port not found, "
+ "using first iSCSI port in list."))
+ iscsi_ip = self.iscsi_ips.keys()[0]
+ else:
+ iscsi_ip = self._get_ip_using_nsp(least_used_nsp)
+
+ iscsi_ip_port = self.iscsi_ips[iscsi_ip]['ip_port']
+ iscsi_target_iqn = self.iscsi_ips[iscsi_ip]['iqn']
+ info = {'driver_volume_type': 'iscsi',
+ 'data': {'target_portal': "%s:%s" %
+ (iscsi_ip, iscsi_ip_port),
+ 'target_iqn': iscsi_target_iqn,
+ 'target_lun': vlun['lun'],
+ 'target_discovered': True
+ }
+ }
if self.configuration.hp3par_iscsi_chap_enabled:
info['data']['auth_method'] = 'CHAP'