diff --git a/cinder/tests/test_hp3par.py b/cinder/tests/test_hp3par.py index af9cfd6d0fb..ee325c4e5a2 100644 --- a/cinder/tests/test_hp3par.py +++ b/cinder/tests/test_hp3par.py @@ -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) - - #record set up 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 a single ISCSI IP + iscsi_ips = ["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') + nsp = self.driver._get_least_used_nsp_for_host('newhost') + self.assertEqual(nsp, "1:8:1") - def test_get_iscsi_ip(self): + def test_get_least_used_nsp_for_host_new(self): self.flags(lock_path=self.tempdir) - - #record driver set up 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=PORTS_RET) - getPorts().AndReturn(PORTS_RET) - getVLUNs().AndReturn(VLUNS1_RET) - self.mox.ReplayAll() + self.driver.common.client.getVLUNs = mock.Mock( + return_value=VLUNS1_RET) - 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) + #Setup two ISCSI IPs + iscsi_ips = ["10.10.220.252", "10.10.220.253"] + self.driver.configuration.hp3par_iscsi_ips = iscsi_ips - ip = self.driver._get_iscsi_ip('fakehost') - self.assertEqual(ip, '10.10.220.252') + self.driver.initialize_iscsi_ports() + + # 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_least_used_nsp_for_host_reuse(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 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() + + self.driver.common.client.getPorts = mock.Mock( + return_value=PORTS1_RET) + self.driver.common.client.getVLUNs = mock.Mock( + return_value=VLUNS5_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() + + 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}]}) diff --git a/cinder/volume/drivers/san/hp/hp_3par_common.py b/cinder/volume/drivers/san/hp/hp_3par_common.py index 2dacfb01c77..8f256253d06 100644 --- a/cinder/volume/drivers/san/hp/hp_3par_common.py +++ b/cinder/volume/drivers/san/hp/hp_3par_common.py @@ -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 diff --git a/cinder/volume/drivers/san/hp/hp_3par_iscsi.py b/cinder/volume/drivers/san/hp/hp_3par_iscsi.py index 6d2ae9c9e63..b78b6601f2c 100644 --- a/cinder/volume/drivers/san/hp/hp_3par_iscsi.py +++ b/cinder/volume/drivers/san/hp/hp_3par_iscsi.py @@ -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