From: Anthony Lee Date: Wed, 29 Jul 2015 23:22:54 +0000 (-0700) Subject: Add multipath support to 3PAR iSCSI driver X-Git-Url: https://review.fuel-infra.org/gitweb?a=commitdiff_plain;h=e5296c1da379c91aaa493b2e675be73c759d259a;p=openstack-build%2Fcinder-build.git 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 --- 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'