]> review.fuel-infra Code Review - openstack-build/cinder-build.git/commitdiff
Add multipath support to 3PAR iSCSI driver
authorAnthony Lee <anthony.mic.lee@hp.com>
Wed, 29 Jul 2015 23:22:54 +0000 (16:22 -0700)
committerAnthony Lee <anthony.mic.lee@hp.com>
Wed, 12 Aug 2015 18:05:26 +0000 (11:05 -0700)
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
cinder/volume/drivers/san/hp/hp_3par_common.py
cinder/volume/drivers/san/hp/hp_3par_iscsi.py

index 59a664c5b8758328271c993b2b932199b60d628c..83876defa39edcd2c7cacc626f782eba23f21125 100644 (file)
@@ -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
index 111e053e90bd4cfffa7665322b47096cbbc5ecc4..8c6aa730278f59663f0d57247fc4a85b2fd5e88c 100644 (file)
@@ -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."""
 
index 0d7868fe21cfdc32e0cf536cb4579448564a0411..9c53da42cad03a7439833abc6efc213a75475ee8 100644 (file)
@@ -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'