]> review.fuel-infra Code Review - openstack-build/cinder-build.git/commitdiff
3PAR iSCSI volumes attach to single nsp
authorRamy Asselin <ramy.asselin@hp.com>
Wed, 15 Jan 2014 21:29:34 +0000 (13:29 -0800)
committerRamy Asselin <ramy.asselin@hp.com>
Mon, 27 Jan 2014 19:40:32 +0000 (11:40 -0800)
Update the 3PAR iSCSI Driver to correctly identify
the least used iSCSI Node:Slot:Port (NSP) and only attach
the volume using that single NSP.

DocImpact: Driver now requires 3PAR firmware version 3.1.2 MU3
Ref: https://review.openstack.org/#/c/68513/

Change-Id: I1627e44a5168a7dbd19ea62312d65a00044753e9
Closes-Bug: #1269515

cinder/tests/test_hp3par.py
cinder/volume/drivers/san/hp/hp_3par_common.py
cinder/volume/drivers/san/hp/hp_3par_iscsi.py

index af9cfd6d0fb7a14aa5f34f4bfa4f8082aa72b1b9..ee325c4e5a23a44f02f3fad8ba4a18cf26103f88 100644 (file)
@@ -1,5 +1,5 @@
 #
-#    (c) Copyright 2013 Hewlett-Packard Development Company, L.P.
+#    (c) Copyright 2013-2014 Hewlett-Packard Development Company, L.P.
 #    All Rights Reserved.
 #
 #    Licensed under the Apache License, Version 2.0 (the "License"); you may
@@ -481,8 +481,8 @@ class HP3PARBaseDriver():
         else:
             del self._hosts[hostname]
 
-    def fake_create_3par_vlun(self, volume, hostname):
-        self.driver.common.client.createVLUN(volume, 19, hostname)
+    def fake_create_3par_vlun(self, volume, hostname, nsp):
+        self.driver.common.client.createVLUN(volume, 19, hostname, nsp)
 
     def fake_get_ports(self):
         ports = self.FAKE_FC_PORTS
@@ -1233,52 +1233,90 @@ class TestHP3PARISCSIDriver(HP3PARBaseDriver, test.TestCase):
         ports = self.driver.common.get_ports()['members']
         self.assertEqual(len(ports), 3)
 
-    def test_get_iscsi_ip_active(self):
+    def test_get_least_used_nsp_for_host_single(self):
         self.flags(lock_path=self.tempdir)
+        self.clear_mox()
+
+        self.driver.common.client.getPorts = mock.Mock(
+            return_value=PORTS_RET)
+
+        self.driver.common.client.getVLUNs = mock.Mock(
+            return_value=VLUNS1_RET)
+
+        #Setup a single ISCSI IP
+        iscsi_ips = ["10.10.220.253"]
+        self.driver.configuration.hp3par_iscsi_ips = iscsi_ips
+
+        self.driver.initialize_iscsi_ports()
 
-        #record set up
+        nsp = self.driver._get_least_used_nsp_for_host('newhost')
+        self.assertEqual(nsp, "1:8:1")
+
+    def test_get_least_used_nsp_for_host_new(self):
+        self.flags(lock_path=self.tempdir)
         self.clear_mox()
 
-        getPorts = self.mox.CreateMock(FakeHP3ParClient.getPorts)
-        self.stubs.Set(FakeHP3ParClient, "getPorts", getPorts)
+        self.driver.common.client.getPorts = mock.Mock(
+            return_value=PORTS_RET)
 
-        getVLUNs = self.mox.CreateMock(FakeHP3ParClient.getVLUNs)
-        self.stubs.Set(FakeHP3ParClient, "getVLUNs", getVLUNs)
+        self.driver.common.client.getVLUNs = mock.Mock(
+            return_value=VLUNS1_RET)
 
-        getPorts().AndReturn(PORTS_RET)
-        getVLUNs().AndReturn(VLUNS2_RET)
-        self.mox.ReplayAll()
+        #Setup two ISCSI IPs
+        iscsi_ips = ["10.10.220.252", "10.10.220.253"]
+        self.driver.configuration.hp3par_iscsi_ips = iscsi_ips
 
-        config = self.setup_configuration()
-        config.hp3par_iscsi_ips = ['10.10.220.253', '10.10.220.252']
-        self.setup_driver(config, set_up_fakes=False)
-        self.mox.ReplayAll()
+        self.driver.initialize_iscsi_ports()
 
-        ip = self.driver._get_iscsi_ip('fakehost')
-        self.assertEqual(ip, '10.10.220.252')
+        # Host 'newhost' does not yet have any iscsi paths,
+        # so the 'least used' is returned
+        nsp = self.driver._get_least_used_nsp_for_host('newhost')
+        self.assertEqual(nsp, "1:8:2")
 
-    def test_get_iscsi_ip(self):
+    def test_get_least_used_nsp_for_host_reuse(self):
         self.flags(lock_path=self.tempdir)
+        self.clear_mox()
 
-        #record driver set up
+        self.driver.common.client.getPorts = mock.Mock(
+            return_value=PORTS_RET)
+
+        self.driver.common.client.getVLUNs = mock.Mock(
+            return_value=VLUNS1_RET)
+
+        #Setup two ISCSI IPs
+        iscsi_ips = ["10.10.220.252", "10.10.220.253"]
+        self.driver.configuration.hp3par_iscsi_ips = iscsi_ips
+
+        self.driver.initialize_iscsi_ports()
+
+        # hosts 'foo' and 'bar' already have active iscsi paths
+        # the same one should be used
+        nsp = self.driver._get_least_used_nsp_for_host('foo')
+        self.assertEqual(nsp, "1:8:2")
+
+        nsp = self.driver._get_least_used_nsp_for_host('bar')
+        self.assertEqual(nsp, "1:8:1")
+
+    def test_get_least_used_nps_for_host_fc(self):
+        # Ensure that with an active FC path setup
+        # an ISCSI path is used
+        self.flags(lock_path=self.tempdir)
         self.clear_mox()
-        getPorts = self.mox.CreateMock(FakeHP3ParClient.getPorts)
-        self.stubs.Set(FakeHP3ParClient, "getPorts", getPorts)
 
-        getVLUNs = self.mox.CreateMock(FakeHP3ParClient.getVLUNs)
-        self.stubs.Set(FakeHP3ParClient, "getVLUNs", getVLUNs)
+        self.driver.common.client.getPorts = mock.Mock(
+            return_value=PORTS1_RET)
+        self.driver.common.client.getVLUNs = mock.Mock(
+            return_value=VLUNS5_RET)
 
-        getPorts().AndReturn(PORTS_RET)
-        getVLUNs().AndReturn(VLUNS1_RET)
-        self.mox.ReplayAll()
+        #Setup two ISCSI IPs
+        iscsi_ips = ["10.10.220.252", "10.10.220.253"]
+        self.driver.configuration.hp3par_iscsi_ips = iscsi_ips
 
-        config = self.setup_configuration()
-        config.iscsi_ip_address = '10.10.10.10'
-        config.hp3par_iscsi_ips = ['10.10.220.253', '10.10.220.252']
-        self.setup_driver(config, set_up_fakes=False)
+        self.driver.initialize_iscsi_ports()
 
-        ip = self.driver._get_iscsi_ip('fakehost')
-        self.assertEqual(ip, '10.10.220.252')
+        nsp = self.driver._get_least_used_nsp_for_host('newhost')
+        self.assertNotEqual(nsp, "0:6:3")
+        self.assertEqual(nsp, "1:8:1")
 
     def test_invalid_iscsi_ip(self):
         self.flags(lock_path=self.tempdir)
@@ -1443,3 +1481,8 @@ VLUNS4_RET = ({'members':
                  'active': True},
                 {'portPos': {'node': 0, 'slot': 2, 'cardPort': 1},
                  'active': True}]})
+VLUNS5_RET = ({'members':
+               [{'portPos': {'node': 0, 'slot': 8, 'cardPort': 2},
+                 'active': True},
+                {'portPos': {'node': 1, 'slot': 8, 'cardPort': 1},
+                 'active': True}]})
index 2dacfb01c7733c5823f5cfa928f1da43e27bfbe9..8f256253d06e10f2a2d356e31bb3539f8985f976 100644 (file)
@@ -1,8 +1,6 @@
-#    (c) Copyright 2012-2013 Hewlett-Packard Development Company, L.P.
+#    (c) Copyright 2012-2014 Hewlett-Packard Development Company, L.P.
 #    All Rights Reserved.
 #
-#    Copyright 2012 OpenStack Foundation
-#
 #    Licensed under the Apache License, Version 2.0 (the "License"); you may
 #    not use this file except in compliance with the License. You may obtain
 #    a copy of the License at
@@ -18,7 +16,7 @@
 """
 Volume driver common utilities for HP 3PAR Storage array
 
-The 3PAR drivers requires 3.1.2 MU2 firmware on the 3PAR array.
+The 3PAR drivers requires 3.1.2 MU3 firmware on the 3PAR array.
 
 You will need to install the python hp3parclient.
 sudo pip install hp3parclient
@@ -113,13 +111,22 @@ class HP3PARCommon(object):
         1.2.3 - Methods to update key/value pair bug #1258033
         1.2.4 - Remove deprecated config option hp3par_domain
         1.2.5 - Raise Ex when deleting snapshot with dependencies bug #1250249
+        1.2.6 - Allow optional specifying n:s:p for vlun creation bug #1269515
+                This update now requires 3.1.2 MU3 firmware
 
     """
 
-    VERSION = "1.2.5"
+    VERSION = "1.2.6"
 
     stats = {}
 
+    # TODO(Ramy): move these to the 3PAR Client
+    VLUN_TYPE_EMPTY = 1
+    VLUN_TYPE_PORT = 2
+    VLUN_TYPE_HOST = 3
+    VLUN_TYPE_MATCHED_SET = 4
+    VLUN_TYPE_HOST_SET = 5
+
     # Valid values for volume type extra specs
     # The first value in the list is the default value
     valid_prov_values = ['thin', 'full']
@@ -385,9 +392,15 @@ exit
     def _delete_3par_host(self, hostname):
         self.client.deleteHost(hostname)
 
-    def _create_3par_vlun(self, volume, hostname):
+    def _create_3par_vlun(self, volume, hostname, nsp):
         try:
-            self.client.createVLUN(volume, hostname=hostname, auto=True)
+            if nsp is None:
+                self.client.createVLUN(volume, hostname=hostname, auto=True)
+            else:
+                port = self.build_portPos(nsp)
+                self.client.createVLUN(volume, hostname=hostname, auto=True,
+                                       portPos=port)
+
         except hpexceptions.HTTPBadRequest as e:
             if 'must be in the same domain' in e.get_description():
                 LOG.error(e.get_description())
@@ -485,19 +498,25 @@ exit
 
         self.stats = stats
 
-    def create_vlun(self, volume, host):
+    def create_vlun(self, volume, host, nsp=None):
         """Create a VLUN.
 
         In order to export a volume on a 3PAR box, we have to create a VLUN.
         """
         volume_name = self._get_3par_vol_name(volume['id'])
-        self._create_3par_vlun(volume_name, host['name'])
+        self._create_3par_vlun(volume_name, host['name'], nsp)
         return self.client.getVLUN(volume_name)
 
     def delete_vlun(self, volume, hostname):
         volume_name = self._get_3par_vol_name(volume['id'])
         vlun = self.client.getVLUN(volume_name)
-        self.client.deleteVLUN(volume_name, vlun['lun'], hostname)
+        # VLUN Type of MATCHED_SET 4 requires the port to be provided
+        if self.VLUN_TYPE_MATCHED_SET == vlun['type']:
+            self.client.deleteVLUN(volume_name, vlun['lun'], hostname,
+                                   vlun['portPos'])
+        else:
+            self.client.deleteVLUN(volume_name, vlun['lun'], hostname)
+
         try:
             self._delete_3par_host(hostname)
         except hpexceptions.HTTPConflict as ex:
@@ -1095,3 +1114,11 @@ exit
         return '%s:%s:%s' % (portPos['node'],
                              portPos['slot'],
                              portPos['cardPort'])
+
+    def build_portPos(self, nsp):
+        split = nsp.split(":")
+        portPos = {}
+        portPos['node'] = int(split[0])
+        portPos['slot'] = int(split[1])
+        portPos['cardPort'] = int(split[2])
+        return portPos
index 6d2ae9c9e63a673c5433a0a5efec389d262fce30..b78b6601f2c13b9754b1355e58e3d237b383bf6e 100644 (file)
@@ -1,8 +1,6 @@
-#    (c) Copyright 2012-2013 Hewlett-Packard Development Company, L.P.
+#    (c) Copyright 2012-2014 Hewlett-Packard Development Company, L.P.
 #    All Rights Reserved.
 #
-#    Copyright 2012 OpenStack Foundation
-#
 #    Licensed under the Apache License, Version 2.0 (the "License"); you may
 #    not use this file except in compliance with the License. You may obtain
 #    a copy of the License at
@@ -17,7 +15,7 @@
 #
 """
 Volume driver for HP 3PAR Storage array.
-This driver requires 3.1.2 MU2 firmware on the 3PAR array, using
+This driver requires 3.1.2 MU3 firmware on the 3PAR array, using
 the 2.x version of the hp3parclient.
 
 You will need to install the python hp3parclient.
@@ -58,10 +56,12 @@ class HP3PARISCSIDriver(cinder.volume.driver.ISCSIDriver):
         1.2.3 - log exceptions before raising
         1.2.4 - Fixed iSCSI active path bug #1224594
         1.2.5 - Added metadata during attach/detach bug #1258033
+        1.2.6 - Use least-used iscsi n:s:p for iscsi volume attach bug #1269515
+                This update now requires 3.1.2 MU3 firmware
 
     """
 
-    VERSION = "1.2.5"
+    VERSION = "1.2.6"
 
     def __init__(self, *args, **kwargs):
         super(HP3PARISCSIDriver, self).__init__(*args, **kwargs)
@@ -257,11 +257,18 @@ class HP3PARISCSIDriver(cinder.volume.driver.ISCSIDriver):
 
             # we have to make sure we have a host
             host = self._create_host(volume, connector)
+            least_used_nsp = self._get_least_used_nsp_for_host(host['name'])
 
             # now that we have a host, create the VLUN
-            vlun = self.common.create_vlun(volume, host)
+            vlun = self.common.create_vlun(volume, host, least_used_nsp)
 
-            iscsi_ip = self._get_iscsi_ip(host['name'])
+            if least_used_nsp is None:
+                msg = _("Least busy iSCSI port not found, "
+                        "using first iSCSI port in list.")
+                LOG.warn(msg)
+                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']
@@ -348,38 +355,34 @@ class HP3PARISCSIDriver(cinder.volume.driver.ISCSIDriver):
     def remove_export(self, context, volume):
         pass
 
-    def _get_iscsi_ip(self, hostname):
-        """Get an iSCSI IP address to use.
+    def _get_least_used_nsp_for_host(self, hostname):
+        """Get the least used NSP for the current host.
 
-        Steps to determine which IP address to use.
-          * If only one IP address, return it
-          * If there is an active vlun, return the IP associated with it
-          * Return IP with fewest active vluns
+        Steps to determine which NSP to use.
+            * If only one iSCSI NSP, return it
+            * If there is already an active vlun to this host, return its NSP
+            * Return NSP with fewest active vluns
         """
-        if len(self.iscsi_ips) == 1:
-            return self.iscsi_ips.keys()[0]
 
+        iscsi_nsps = self._get_iscsi_nsps()
+        # If there's only one path, use it
+        if len(iscsi_nsps) == 1:
+            return iscsi_nsps[0]
+
+        # Try to reuse an existing iscsi path to the host
         vluns = self.common.client.getVLUNs()
-        # see if there is already a path to the
-        # host, if so use it
         for vlun in vluns['members']:
             if vlun['active']:
                 if vlun['hostname'] == hostname:
-                    # this host already has a path, so use it
-                    nsp = self.common.build_nsp(vlun['portPos'])
-                    return self._get_ip_using_nsp(nsp)
+                    temp_nsp = self.common.build_nsp(vlun['portPos'])
+                    if temp_nsp in iscsi_nsps:
+                        # this host already has an iscsi path, so use it
+                        return temp_nsp
 
-        # no current path find least used port
+        # Calculate the least used iscsi nsp
         least_used_nsp = self._get_least_used_nsp(vluns['members'],
                                                   self._get_iscsi_nsps())
-
-        if least_used_nsp is None:
-            msg = _("Least busy iSCSI port not found, "
-                    "using first iSCSI port in list.")
-            LOG.warn(msg)
-            return self.iscsi_ips.keys()[0]
-
-        return self._get_ip_using_nsp(least_used_nsp)
+        return least_used_nsp
 
     def _get_iscsi_nsps(self):
         """Return the list of candidate nsps."""
@@ -389,7 +392,7 @@ class HP3PARISCSIDriver(cinder.volume.driver.ISCSIDriver):
         return nsps
 
     def _get_ip_using_nsp(self, nsp):
-        """Return IP assiciated with given nsp."""
+        """Return IP associated with given nsp."""
         for (key, value) in self.iscsi_ips.items():
             if value['nsp'] == nsp:
                 return key