#
-# (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
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
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)
'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}]})
-# (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
"""
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
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']
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())
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:
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
-# (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
#
"""
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.
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)
# 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']
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."""
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