From e5296c1da379c91aaa493b2e675be73c759d259a Mon Sep 17 00:00:00 2001 From: Anthony Lee Date: Wed, 29 Jul 2015 16:22:54 -0700 Subject: [PATCH] Add multipath support to 3PAR iSCSI driver 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 --- cinder/tests/unit/test_hp3par.py | 141 +++++++++++++++++- .../volume/drivers/san/hp/hp_3par_common.py | 46 +++++- cinder/volume/drivers/san/hp/hp_3par_iscsi.py | 133 ++++++++++++----- 3 files changed, 271 insertions(+), 49 deletions(-) diff --git a/cinder/tests/unit/test_hp3par.py b/cinder/tests/unit/test_hp3par.py index 59a664c5b..83876defa 100644 --- a/cinder/tests/unit/test_hp3par.py +++ b/cinder/tests/unit/test_hp3par.py @@ -179,7 +179,17 @@ class HP3PARBaseDriver(object): '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, @@ -3076,7 +3086,8 @@ class TestHP3PARFCDriver(HP3PARBaseDriver, test.TestCase): 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, @@ -3704,6 +3715,15 @@ class TestHP3PARISCSIDriver(HP3PARBaseDriver, test.TestCase): '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() @@ -3788,6 +3808,123 @@ class TestHP3PARISCSIDriver(HP3PARBaseDriver, test.TestCase): 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 diff --git a/cinder/volume/drivers/san/hp/hp_3par_common.py b/cinder/volume/drivers/san/hp/hp_3par_common.py index 111e053e9..8c6aa7302 100644 --- a/cinder/volume/drivers/san/hp/hp_3par_common.py +++ b/cinder/volume/drivers/san/hp/hp_3par_common.py @@ -180,10 +180,11 @@ class HP3PARCommon(object): 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 = {} @@ -764,16 +765,22 @@ class HP3PARCommon(object): '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 @@ -790,7 +797,10 @@ class HP3PARCommon(object): """ 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']) @@ -2111,9 +2121,33 @@ class HP3PARCommon(object): 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.""" diff --git a/cinder/volume/drivers/san/hp/hp_3par_iscsi.py b/cinder/volume/drivers/san/hp/hp_3par_iscsi.py index 0d7868fe2..9c53da42c 100644 --- a/cinder/volume/drivers/san/hp/hp_3par_iscsi.py +++ b/cinder/volume/drivers/san/hp/hp_3par_iscsi.py @@ -89,10 +89,11 @@ class HP3PARISCSIDriver(cinder.volume.driver.ISCSIDriver): 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) @@ -291,48 +292,98 @@ class HP3PARISCSIDriver(cinder.volume.driver.ISCSIDriver): 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' -- 2.45.2