]> review.fuel-infra Code Review - openstack-build/cinder-build.git/commitdiff
Adds multiple iSCSI port support to 3PAR
authorJim Branen <james.branen@hp.com>
Wed, 10 Jul 2013 19:34:09 +0000 (12:34 -0700)
committerJim Branen <james.branen@hp.com>
Tue, 16 Jul 2013 15:45:33 +0000 (08:45 -0700)
Added support to the 3PAR iSCSI OpenStack driver to provide the
ability to select the best fit target iSCSI port from a list of
candidate ports. The first time a volume is attached to a host,
all iSCSI ports configured for driver selection, are examined for
best fit. The port with the least active volumes attached will
then be selected as the path to the 3PAR array. Any subsequent
volume attach, to the same host, will use the established target
port.

DocImpact

Fixes bug #1197036

Change-Id: Icf8c28ea3f201e5e21c9a6ed00a2fbdda445c8b3

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

index 9587cd553fe8e98f2d5aa1373dbe0614e24283d2..547b4e2c4d1fde1d09d5393afbbd60543922e52e 100644 (file)
@@ -305,7 +305,10 @@ class HP3PARBaseDriver():
     VOLUME_ID_SNAP = '761fc5e5-5191-4ec7-aeba-33e36de44156'
     FAKE_DESC = 'test description name'
     FAKE_FC_PORTS = ['0987654321234', '123456789000987']
-    FAKE_ISCSI_PORTS = ['10.10.10.10', '10.10.10.11']
+    FAKE_ISCSI_PORTS = {'1.1.1.2': {'nsp': '8:1:1',
+                                    'iqn': ('iqn.2000-05.com.3pardata:'
+                                            '21810002ac00383d'),
+                                    'ip_port': '3262'}}
 
     volume = {'name': VOLUME_NAME,
               'id': VOLUME_ID,
@@ -333,6 +336,43 @@ class HP3PARBaseDriver():
                  'wwnns': ["223456789012345", "223456789054321"],
                  'host': 'fakehost'}
 
+    def setup_configuration(self):
+        configuration = mox.MockObject(conf.Configuration)
+        configuration.hp3par_debug = False
+        configuration.hp3par_username = 'testUser'
+        configuration.hp3par_password = 'testPassword'
+        configuration.hp3par_api_url = 'https://1.1.1.1/api/v1'
+        configuration.hp3par_domain = HP3PAR_DOMAIN
+        configuration.hp3par_cpg = HP3PAR_CPG
+        configuration.hp3par_cpg_snap = HP3PAR_CPG_SNAP
+        configuration.iscsi_ip_address = '1.1.1.2'
+        configuration.iscsi_port = '1234'
+        configuration.san_ip = '2.2.2.2'
+        configuration.san_login = 'test'
+        configuration.san_password = 'test'
+        configuration.hp3par_snapshot_expiration = ""
+        configuration.hp3par_snapshot_retention = ""
+        configuration.hp3par_iscsi_ips = []
+        return configuration
+
+    def setup_fakes(self):
+        self.stubs.Set(hpdriver.hpcommon.HP3PARCommon, "_create_client",
+                       self.fake_create_client)
+        self.stubs.Set(hpdriver.hpcommon.HP3PARCommon, "_get_3par_host",
+                       self.fake_get_3par_host)
+        self.stubs.Set(hpdriver.hpcommon.HP3PARCommon, "_delete_3par_host",
+                       self.fake_delete_3par_host)
+        self.stubs.Set(hpdriver.hpcommon.HP3PARCommon, "_create_3par_vlun",
+                       self.fake_create_3par_vlun)
+        self.stubs.Set(hpdriver.hpcommon.HP3PARCommon, "get_ports",
+                       self.fake_get_ports)
+        self.stubs.Set(hpfcdriver.hpcommon.HP3PARCommon, "get_domain",
+                       self.fake_get_domain)
+
+    def clear_mox(self):
+        self.mox.ResetAll()
+        self.stubs.UnsetAll()
+
     def fake_create_client(self):
         return FakeHP3ParClient(self.driver.configuration.hp3par_api_url)
 
@@ -430,47 +470,26 @@ class TestHP3PARFCDriver(HP3PARBaseDriver, test.TestCase):
     def setUp(self):
         self.tempdir = tempfile.mkdtemp()
         super(TestHP3PARFCDriver, self).setUp()
+        self.setup_driver(self.setup_configuration())
+        self.setup_fakes()
 
-        configuration = mox.MockObject(conf.Configuration)
-        configuration.hp3par_debug = False
-        configuration.hp3par_username = 'testUser'
-        configuration.hp3par_password = 'testPassword'
-        configuration.hp3par_api_url = 'https://1.1.1.1/api/v1'
-        configuration.hp3par_cpg = HP3PAR_CPG
-        configuration.hp3par_cpg_snap = HP3PAR_CPG_SNAP
-        configuration.iscsi_ip_address = '1.1.1.2'
-        configuration.iscsi_port = '1234'
-        configuration.san_ip = '2.2.2.2'
-        configuration.san_login = 'test'
-        configuration.san_password = 'test'
-        configuration.hp3par_snapshot_expiration = ""
-        configuration.hp3par_snapshot_retention = ""
-        self.stubs.Set(hpfcdriver.hpcommon.HP3PARCommon, "_create_client",
-                       self.fake_create_client)
+    def setup_fakes(self):
+        super(TestHP3PARFCDriver, self).setup_fakes()
         self.stubs.Set(hpfcdriver.HP3PARFCDriver,
                        "_create_3par_fibrechan_host",
                        self.fake_create_3par_fibrechan_host)
 
-        self.stubs.Set(hpfcdriver.hpcommon.HP3PARCommon, "_get_3par_host",
-                       self.fake_get_3par_host)
-        self.stubs.Set(hpfcdriver.hpcommon.HP3PARCommon, "_delete_3par_host",
-                       self.fake_delete_3par_host)
-        self.stubs.Set(hpdriver.hpcommon.HP3PARCommon, "_create_3par_vlun",
-                       self.fake_create_3par_vlun)
-        self.stubs.Set(hpdriver.hpcommon.HP3PARCommon, "get_ports",
-                       self.fake_get_ports)
-        self.stubs.Set(hpfcdriver.hpcommon.HP3PARCommon, "get_domain",
-                       self.fake_get_domain)
-
-        self.configuration = configuration
-
-        self.driver = hpfcdriver.HP3PARFCDriver(configuration=configuration)
-        self.driver.do_setup(None)
-
     def tearDown(self):
         shutil.rmtree(self.tempdir)
         super(TestHP3PARFCDriver, self).tearDown()
 
+    def setup_driver(self, configuration):
+        self.driver = hpfcdriver.HP3PARFCDriver(configuration=configuration)
+
+        self.stubs.Set(hpdriver.hpcommon.HP3PARCommon, "_create_client",
+                       self.fake_create_client)
+        self.driver.do_setup(None)
+
     def fake_create_3par_fibrechan_host(self, hostname, wwn,
                                         domain, persona_id):
         host = {'FCPaths': [{'driverVersion': None,
@@ -577,7 +596,7 @@ class TestHP3PARFCDriver(HP3PARBaseDriver, test.TestCase):
         self.flags(lock_path=self.tempdir)
 
         #record
-        self.stubs.UnsetAll()
+        self.clear_mox()
         self.stubs.Set(hpfcdriver.hpcommon.HP3PARCommon, "get_domain",
                        self.fake_get_domain)
         _run_ssh = self.mox.CreateMock(hpdriver.hpcommon.HP3PARCommon._run_ssh)
@@ -600,7 +619,7 @@ class TestHP3PARFCDriver(HP3PARBaseDriver, test.TestCase):
         self.flags(lock_path=self.tempdir)
 
         #record
-        self.stubs.UnsetAll()
+        self.clear_mox()
         self.stubs.Set(hpdriver.hpcommon.HP3PARCommon, "get_domain",
                        self.fake_get_domain)
         _run_ssh = self.mox.CreateMock(hpdriver.hpcommon.HP3PARCommon._run_ssh)
@@ -627,7 +646,7 @@ class TestHP3PARFCDriver(HP3PARBaseDriver, test.TestCase):
         self.flags(lock_path=self.tempdir)
 
         #record
-        self.stubs.UnsetAll()
+        self.clear_mox()
         self.stubs.Set(hpdriver.hpcommon.HP3PARCommon, "get_domain",
                        self.fake_get_domain)
         _run_ssh = self.mox.CreateMock(hpdriver.hpcommon.HP3PARCommon._run_ssh)
@@ -657,49 +676,19 @@ class TestHP3PARISCSIDriver(HP3PARBaseDriver, test.TestCase):
     def setUp(self):
         self.tempdir = tempfile.mkdtemp()
         super(TestHP3PARISCSIDriver, self).setUp()
+        self.setup_driver(self.setup_configuration())
+        self.setup_fakes()
 
-        configuration = mox.MockObject(conf.Configuration)
-        configuration.hp3par_debug = False
-        configuration.hp3par_username = 'testUser'
-        configuration.hp3par_password = 'testPassword'
-        configuration.hp3par_api_url = 'https://1.1.1.1/api/v1'
-        configuration.hp3par_cpg = HP3PAR_CPG
-        configuration.hp3par_cpg_snap = HP3PAR_CPG_SNAP
-        configuration.iscsi_ip_address = '1.1.1.2'
-        configuration.iscsi_port = '1234'
-        configuration.san_ip = '2.2.2.2'
-        configuration.san_login = 'test'
-        configuration.san_password = 'test'
-        configuration.hp3par_snapshot_expiration = ""
-        configuration.hp3par_snapshot_retention = ""
+    def setup_fakes(self):
+        super(TestHP3PARISCSIDriver, self).setup_fakes()
 
-        self.stubs.Set(hpdriver.hpcommon.HP3PARCommon, "_create_client",
-                       self.fake_create_client)
-        self.stubs.Set(hpdriver.HP3PARISCSIDriver,
-                       "_iscsi_discover_target_iqn",
-                       self.fake_iscsi_discover_target_iqn)
         self.stubs.Set(hpdriver.HP3PARISCSIDriver, "_create_3par_iscsi_host",
                        self.fake_create_3par_iscsi_host)
-        self.stubs.Set(hpdriver.HP3PARISCSIDriver,
-                       "_iscsi_discover_target_iqn",
-                       self.fake_iscsi_discover_target_iqn)
-
-        self.stubs.Set(hpdriver.hpcommon.HP3PARCommon, "_get_3par_host",
-                       self.fake_get_3par_host)
-        self.stubs.Set(hpdriver.hpcommon.HP3PARCommon, "_delete_3par_host",
-                       self.fake_delete_3par_host)
-        self.stubs.Set(hpdriver.hpcommon.HP3PARCommon, "_create_3par_vlun",
-                       self.fake_create_3par_vlun)
-        self.stubs.Set(hpdriver.hpcommon.HP3PARCommon, "get_domain",
-                       self.fake_get_domain)
-
-        self.driver = hpdriver.HP3PARISCSIDriver(configuration=configuration)
-        self.driver.do_setup(None)
 
-        target_iqn = 'iqn.2000-05.com.3pardata:21810002ac00383d'
+        #target_iqn = 'iqn.2000-05.com.3pardata:21810002ac00383d'
         self.properties = {'data':
                           {'target_discovered': True,
-                           'target_iqn': target_iqn,
+                           'target_iqn': self.TARGET_IQN,
                            'target_lun': 186,
                            'target_portal': '1.1.1.2:1234'},
                            'driver_volume_type': 'iscsi'}
@@ -709,8 +698,17 @@ class TestHP3PARISCSIDriver(HP3PARBaseDriver, test.TestCase):
         self._hosts = {}
         super(TestHP3PARISCSIDriver, self).tearDown()
 
-    def fake_iscsi_discover_target_iqn(self, ip_address):
-        return self.TARGET_IQN
+    def setup_driver(self, configuration, set_up_fakes=True):
+        self.driver = hpdriver.HP3PARISCSIDriver(configuration=configuration)
+
+        self.stubs.Set(hpdriver.hpcommon.HP3PARCommon, "_create_client",
+                       self.fake_create_client)
+
+        if set_up_fakes:
+            self.stubs.Set(hpdriver.hpcommon.HP3PARCommon, "get_ports",
+                           self.fake_get_ports)
+
+        self.driver.do_setup(None)
 
     def fake_create_3par_iscsi_host(self, hostname, iscsi_iqn,
                                     domain, persona_id):
@@ -812,7 +810,7 @@ class TestHP3PARISCSIDriver(HP3PARBaseDriver, test.TestCase):
         self.flags(lock_path=self.tempdir)
 
         #record
-        self.stubs.UnsetAll()
+        self.clear_mox()
         self.stubs.Set(hpdriver.hpcommon.HP3PARCommon, "get_domain",
                        self.fake_get_domain)
         _run_ssh = self.mox.CreateMock(hpdriver.hpcommon.HP3PARCommon._run_ssh)
@@ -836,7 +834,7 @@ class TestHP3PARISCSIDriver(HP3PARBaseDriver, test.TestCase):
         self.flags(lock_path=self.tempdir)
 
         #record
-        self.stubs.UnsetAll()
+        self.clear_mox()
         self.stubs.Set(hpdriver.hpcommon.HP3PARCommon, "get_domain",
                        self.fake_get_domain)
         _run_ssh = self.mox.CreateMock(hpdriver.hpcommon.HP3PARCommon._run_ssh)
@@ -863,7 +861,7 @@ class TestHP3PARISCSIDriver(HP3PARBaseDriver, test.TestCase):
         self.flags(lock_path=self.tempdir)
 
         #record
-        self.stubs.UnsetAll()
+        self.clear_mox()
         self.stubs.Set(hpdriver.hpcommon.HP3PARCommon, "get_domain",
                        self.fake_get_domain)
         _run_ssh = self.mox.CreateMock(hpdriver.hpcommon.HP3PARCommon._run_ssh)
@@ -881,27 +879,11 @@ class TestHP3PARISCSIDriver(HP3PARBaseDriver, test.TestCase):
         host = self.driver._create_host(self.volume, self.connector)
         self.assertEqual(host['name'], self.FAKE_HOST)
 
-    def test_iscsi_discover_target_iqn(self):
-        self.flags(lock_path=self.tempdir)
-
-        #record
-        self.stubs.UnsetAll()
-        _run_ssh = self.mox.CreateMock(hpdriver.hpcommon.HP3PARCommon._run_ssh)
-        self.stubs.Set(hpdriver.hpcommon.HP3PARCommon, "_run_ssh", _run_ssh)
-
-        show_port_cmd = 'showport -ids'
-        _run_ssh(show_port_cmd, False).AndReturn([pack(ISCSI_PORT_IDS_RET),
-                                                 ''])
-        self.mox.ReplayAll()
-
-        iqn = self.driver._iscsi_discover_target_iqn('10.10.120.253')
-        self.assertEqual(iqn, self.TARGET_IQN)
-
     def test_get_volume_state(self):
         self.flags(lock_path=self.tempdir)
 
         #record
-        self.stubs.UnsetAll()
+        self.clear_mox()
         _run_ssh = self.mox.CreateMock(hpdriver.hpcommon.HP3PARCommon._run_ssh)
         self.stubs.Set(hpdriver.hpcommon.HP3PARCommon, "_run_ssh", _run_ssh)
 
@@ -917,7 +899,7 @@ class TestHP3PARISCSIDriver(HP3PARBaseDriver, test.TestCase):
         self.flags(lock_path=self.tempdir)
 
         #record
-        self.stubs.UnsetAll()
+        self.clear_mox()
         _run_ssh = self.mox.CreateMock(hpdriver.hpcommon.HP3PARCommon._run_ssh)
         self.stubs.Set(hpdriver.hpcommon.HP3PARCommon, "_run_ssh", _run_ssh)
 
@@ -925,11 +907,144 @@ class TestHP3PARISCSIDriver(HP3PARBaseDriver, test.TestCase):
         _run_ssh(show_port_cmd, False).AndReturn([pack(PORT_RET), ''])
 
         show_port_i_cmd = 'showport -iscsi'
-        _run_ssh(show_port_i_cmd, False).AndReturn([pack(ISCSI_PORT_RET), ''])
+        _run_ssh(show_port_i_cmd, False).AndReturn([pack(READY_ISCSI_PORT_RET),
+                                                    ''])
+
+        show_port_i_cmd = 'showport -iscsiname'
+        _run_ssh(show_port_i_cmd, False).AndReturn([pack(SHOW_PORT_ISCSI),
+                                                    ''])
         self.mox.ReplayAll()
 
         ports = self.driver.common.get_ports()
         self.assertEqual(ports['FC'][0], '20210002AC00383D')
+        self.assertEqual(ports['iSCSI']['10.10.120.252']['nsp'], '0:8:2')
+
+    def test_get_iscsi_ip_active(self):
+        self.flags(lock_path=self.tempdir)
+
+        #record set up
+        self.clear_mox()
+        _run_ssh = self.mox.CreateMock(hpdriver.hpcommon.HP3PARCommon._run_ssh)
+        self.stubs.Set(hpdriver.hpcommon.HP3PARCommon, "_run_ssh", _run_ssh)
+
+        show_port_cmd = 'showport'
+        _run_ssh(show_port_cmd, False).AndReturn([pack(PORT_RET), ''])
+
+        show_port_i_cmd = 'showport -iscsi'
+        _run_ssh(show_port_i_cmd, False).AndReturn([pack(READY_ISCSI_PORT_RET),
+                                                    ''])
+
+        show_port_i_cmd = 'showport -iscsiname'
+        _run_ssh(show_port_i_cmd, False).AndReturn([pack(SHOW_PORT_ISCSI), ''])
+
+        self.mox.ReplayAll()
+
+        config = self.setup_configuration()
+        config.hp3par_iscsi_ips = ['10.10.220.253', '10.10.220.252']
+        self.setup_driver(config, set_up_fakes=False)
+
+        #record
+        self.clear_mox()
+        _run_ssh = self.mox.CreateMock(hpdriver.hpcommon.HP3PARCommon._run_ssh)
+        self.stubs.Set(hpdriver.hpcommon.HP3PARCommon, "_run_ssh", _run_ssh)
+
+        show_vlun_cmd = 'showvlun -a -host fakehost'
+        _run_ssh(show_vlun_cmd, False).AndReturn([pack(SHOW_VLUN), ''])
+
+        self.mox.ReplayAll()
+
+        ip = self.driver._get_iscsi_ip('fakehost')
+        self.assertEqual(ip, '10.10.220.253')
+
+    def test_get_iscsi_ip(self):
+        self.flags(lock_path=self.tempdir)
+
+        #record driver set up
+        self.clear_mox()
+        _run_ssh = self.mox.CreateMock(hpdriver.hpcommon.HP3PARCommon._run_ssh)
+        self.stubs.Set(hpdriver.hpcommon.HP3PARCommon, "_run_ssh", _run_ssh)
+
+        show_port_cmd = 'showport'
+        _run_ssh(show_port_cmd, False).AndReturn([pack(PORT_RET), ''])
+
+        show_port_i_cmd = 'showport -iscsi'
+        _run_ssh(show_port_i_cmd, False).AndReturn([pack(READY_ISCSI_PORT_RET),
+                                                    ''])
+
+        show_port_i_cmd = 'showport -iscsiname'
+        _run_ssh(show_port_i_cmd, False).AndReturn([pack(SHOW_PORT_ISCSI), ''])
+
+        #record
+        show_vlun_cmd = 'showvlun -a -host fakehost'
+        show_vlun_ret = 'no vluns listed\r\n'
+        _run_ssh(show_vlun_cmd, False).AndReturn([pack(show_vlun_ret), ''])
+        show_vlun_cmd = 'showvlun -a -showcols Port'
+        _run_ssh(show_vlun_cmd, False).AndReturn([pack(SHOW_VLUN_NONE), ''])
+
+        self.mox.ReplayAll()
+
+        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)
+
+        ip = self.driver._get_iscsi_ip('fakehost')
+        self.assertEqual(ip, '10.10.220.252')
+
+    def test_invalid_iscsi_ip(self):
+        self.flags(lock_path=self.tempdir)
+
+        #record driver set up
+        self.clear_mox()
+        _run_ssh = self.mox.CreateMock(hpdriver.hpcommon.HP3PARCommon._run_ssh)
+        self.stubs.Set(hpdriver.hpcommon.HP3PARCommon, "_run_ssh", _run_ssh)
+
+        show_port_cmd = 'showport'
+        _run_ssh(show_port_cmd, False).AndReturn([pack(PORT_RET), ''])
+
+        show_port_i_cmd = 'showport -iscsi'
+        _run_ssh(show_port_i_cmd, False).AndReturn([pack(READY_ISCSI_PORT_RET),
+                                                    ''])
+
+        show_port_i_cmd = 'showport -iscsiname'
+        _run_ssh(show_port_i_cmd, False).AndReturn([pack(SHOW_PORT_ISCSI), ''])
+
+        config = self.setup_configuration()
+        config.hp3par_iscsi_ips = ['10.10.220.250', '10.10.220.251']
+        config.iscsi_ip_address = '10.10.10.10'
+        self.mox.ReplayAll()
+
+        # no valid ip addr should be configured.
+        self.assertRaises(exception.InvalidInput,
+                          self.setup_driver,
+                          config,
+                          set_up_fakes=False)
+
+    def test_get_least_used_nsp(self):
+        self.flags(lock_path=self.tempdir)
+
+        #record
+        self.clear_mox()
+        _run_ssh = self.mox.CreateMock(hpdriver.hpcommon.HP3PARCommon._run_ssh)
+        self.stubs.Set(hpdriver.hpcommon.HP3PARCommon, "_run_ssh", _run_ssh)
+
+        show_vlun_cmd = 'showvlun -a -showcols Port'
+        _run_ssh(show_vlun_cmd, False).AndReturn([pack(SHOW_VLUN_NONE), ''])
+        _run_ssh(show_vlun_cmd, False).AndReturn([pack(SHOW_VLUN_NONE), ''])
+        _run_ssh(show_vlun_cmd, False).AndReturn([pack(SHOW_VLUN_NONE), ''])
+
+        self.mox.ReplayAll()
+        # in use count                           11       12
+        nsp = self.driver._get_least_used_nsp(['0:2:1', '1:8:1'])
+        self.assertEqual(nsp, '0:2:1')
+
+        # in use count                            11       10
+        nsp = self.driver._get_least_used_nsp(['0:2:1', '1:2:1'])
+        self.assertEqual(nsp, '1:2:1')
+
+        # in use count                            0       10
+        nsp = self.driver._get_least_used_nsp(['1:1:1', '1:2:1'])
+        self.assertEqual(nsp, '1:1:1')
 
 
 def pack(arg):
@@ -1106,3 +1221,40 @@ ISCSI_3PAR_RET = (
     'Model      : --\r\n'
     'Contact    : --\r\n'
     'Comment    : --  \r\n\r\n\r\n')
+
+SHOW_PORT_ISCSI = (
+    'N:S:P,IPAddr,---------------iSCSI_Name----------------\r\n'
+    '0:8:1,1.1.1.2,iqn.2000-05.com.3pardata:21810002ac00383d\r\n'
+    '0:8:2,10.10.120.252,iqn.2000-05.com.3pardata:20820002ac00383d\r\n'
+    '1:8:1,10.10.220.253,iqn.2000-05.com.3pardata:21810002ac00383d\r\n'
+    '1:8:2,10.10.220.252,iqn.2000-05.com.3pardata:21820002ac00383d\r\n'
+    '-------------------------------------------------------------\r\n')
+
+SHOW_VLUN = (
+    'Lun,VVName,HostName,---------Host_WWN/iSCSI_Name----------,Port,Type,'
+    'Status,ID\r\n'
+    '0,a,fakehost,iqn.1993-08.org.debian:01:3a779e4abc22,1:8:1,matched set,'
+    'active,0\r\n'
+    '------------------------------------------------------------------------'
+    '--------------\r\n')
+
+SHOW_VLUN_NONE = (
+    'Port\r\n0:2:1\r\n0:2:1\r\n1:8:1\r\n1:8:1\r\n1:8:1\r\n1:2:1\r\n'
+    '1:2:1\r\n1:2:1\r\n1:2:1\r\n1:2:1\r\n1:2:1\r\n1:8:1\r\n1:8:1\r\n1:8:1\r\n'
+    '1:8:1\r\n1:8:1\r\n1:8:1\r\n0:2:1\r\n0:2:1\r\n0:2:1\r\n0:2:1\r\n0:2:1\r\n'
+    '0:2:1\r\n0:2:1\r\n1:8:1\r\n1:8:1\r\n0:2:1\r\n0:2:1\r\n1:2:1\r\n1:2:1\r\n'
+    '1:2:1\r\n1:2:1\r\n1:8:1\r\n-----')
+
+READY_ISCSI_PORT_RET = (
+    'N:S:P,State,IPAddr,Netmask,Gateway,TPGT,MTU,Rate,DHCP,iSNS_Addr,'
+    'iSNS_Port\r\n'
+    '0:8:1,ready,10.10.120.253,255.255.224.0,0.0.0.0,81,1500,10Gbps,'
+    '0,0.0.0.0,3205\r\n'
+    '0:8:2,ready,10.10.120.252,255.255.224.0,0.0.0.0,82,1500,10Gbps,0,'
+    '0.0.0.0,3205\r\n'
+    '1:8:1,ready,10.10.220.253,255.255.224.0,0.0.0.0,181,1500,10Gbps,'
+    '0,0.0.0.0,3205\r\n'
+    '1:8:2,ready,10.10.220.252,255.255.224.0,0.0.0.0,182,1500,10Gbps,0,'
+    '0.0.0.0,3205\r\n'
+    '-------------------------------------------------------------------'
+    '----------------------\r\n')
index 0e46253aa4731983e7949cb1897fd4a390b236ea..f21feefac620b62a34326b8d78cad3ee9ec856a1 100644 (file)
@@ -96,7 +96,10 @@ hp3par_opts = [
                     " and is deleted.  This must be larger than expiration"),
     cfg.BoolOpt('hp3par_debug',
                 default=False,
-                help="Enable HTTP debugging to 3PAR")
+                help="Enable HTTP debugging to 3PAR"),
+    cfg.ListOpt('hp3par_iscsi_ips',
+                default=[],
+                help="List of target iSCSI addresses to use.")
 ]
 
 
@@ -458,7 +461,7 @@ exit
         # Protocol,Label,Partner,FailoverState
         out = out[1:len(out) - 2]
 
-        ports = {'FC': [], 'iSCSI': []}
+        ports = {'FC': [], 'iSCSI': {}}
         for line in out:
             tmp = line.split(',')
 
@@ -477,9 +480,26 @@ exit
         for line in out:
             tmp = line.split(',')
 
-            if tmp:
+            if tmp and len(tmp) > 2:
                 if tmp[1] == 'ready':
-                    ports['iSCSI'].append(tmp[2])
+                    ports['iSCSI'][tmp[2]] = {}
+
+        # now get the nsp and iqn
+        result = self._cli_run('showport -iscsiname', None)
+        if result:
+            # first line is header
+            # nsp, ip,iqn
+            result = result[1:]
+            for line in result:
+                info = line.split(",")
+                if info and len(info) > 2:
+                    if info[1] in ports['iSCSI']:
+                        nsp = info[0]
+                        ip_addr = info[1]
+                        iqn = info[2]
+                        ports['iSCSI'][ip_addr] = {'nsp': nsp,
+                                                   'iqn': iqn
+                                                   }
 
         LOG.debug("PORTS = %s" % pprint.pformat(ports))
         return ports
index ce158d16bf604815d2602662c91a485fdb108f87..3fb90849b83d3d34ebd2a6b98beaf9e425cbfa18 100644 (file)
@@ -30,6 +30,8 @@ Set the following in the cinder.conf file to enable the
 volume_driver=cinder.volume.drivers.san.hp.hp_3par_iscsi.HP3PARISCSIDriver
 """
 
+import sys
+
 from hp3parclient import exceptions as hpexceptions
 
 from cinder import exception
@@ -41,6 +43,7 @@ from cinder.volume.drivers.san import san
 
 VERSION = 1.0
 LOG = logging.getLogger(__name__)
+DEFAULT_ISCSI_PORT = 3260
 
 
 class HP3PARISCSIDriver(cinder.volume.driver.ISCSIDriver):
@@ -62,8 +65,7 @@ class HP3PARISCSIDriver(cinder.volume.driver.ISCSIDriver):
     def _check_flags(self):
         """Sanity check to ensure we have required options set."""
         required_flags = ['hp3par_api_url', 'hp3par_username',
-                          'hp3par_password', 'iscsi_ip_address',
-                          'iscsi_port', 'san_ip', 'san_login',
+                          'hp3par_password', 'san_ip', 'san_login',
                           'san_password']
         self.common.check_flags(self.configuration, required_flags)
 
@@ -80,10 +82,66 @@ class HP3PARISCSIDriver(cinder.volume.driver.ISCSIDriver):
     def do_setup(self, context):
         self.common = self._init_common()
         self._check_flags()
-        self.common.do_setup(context)
 
-        # make sure ssh works.
-        self._iscsi_discover_target_iqn(self.configuration.iscsi_ip_address)
+        # map iscsi_ip-> ip_port
+        #             -> iqn
+        #             -> nsp
+        self.iscsi_ips = {}
+        temp_iscsi_ip = {}
+
+        # use the 3PAR ip_addr list for iSCSI configuration
+        if len(self.configuration.hp3par_iscsi_ips) > 0:
+            # add port values to ip_addr, if necessary
+            for ip_addr in self.configuration.hp3par_iscsi_ips:
+                ip = ip_addr.split(':')
+                if len(ip) == 1:
+                    temp_iscsi_ip[ip_addr] = {'ip_port': DEFAULT_ISCSI_PORT}
+                elif len(ip) == 2:
+                    temp_iscsi_ip[ip[0]] = {'ip_port': ip[1]}
+                else:
+                    msg = _("Invalid IP address format '%s'") % ip_addr
+                    LOG.warn(msg)
+
+        # add the single value iscsi_ip_address option to the IP dictionary.
+        # This way we can see if it's a valid iSCSI IP. If it's not valid,
+        # we won't use it and won't bother to report it, see below
+        if (self.configuration.iscsi_ip_address not in temp_iscsi_ip):
+            ip = self.configuration.iscsi_ip_address
+            ip_port = self.configuration.iscsi_port
+            temp_iscsi_ip[ip] = {'ip_port': ip_port}
+
+        # get all the valid iSCSI ports from 3PAR
+        # when found, add the valid iSCSI ip, ip port, iqn and nsp
+        # to the iSCSI IP dictionary
+        # ...this will also make sure ssh works.
+        iscsi_ports = self.common.get_ports()['iSCSI']
+        for (ip, iscsi_info) in iscsi_ports.iteritems():
+            if ip in temp_iscsi_ip:
+                ip_port = temp_iscsi_ip[ip]['ip_port']
+                self.iscsi_ips[ip] = {'ip_port': ip_port,
+                                      'nsp': iscsi_info['nsp'],
+                                      'iqn': iscsi_info['iqn']
+                                      }
+                del temp_iscsi_ip[ip]
+
+        # if the single value iscsi_ip_address option is still in the
+        # temp dictionary it's because it defaults to $my_ip which doesn't
+        # make sense in this context. So, if present, remove it and move on.
+        if (self.configuration.iscsi_ip_address in temp_iscsi_ip):
+            del temp_iscsi_ip[self.configuration.iscsi_ip_address]
+
+        # lets see if there are invalid iSCSI IPs left in the temp dict
+        if len(temp_iscsi_ip) > 0:
+            msg = _("Found invalid iSCSI IP address(s) in configuration "
+                    "option(s) hp3par_iscsi_ips or iscsi_ip_address '%s.'") % \
+                   (", ".join(temp_iscsi_ip))
+            LOG.warn(msg)
+
+        if not len(self.iscsi_ips) > 0:
+            msg = _('At least one valid iSCSI IP address must be set.')
+            raise exception.InvalidInput(reason=(msg))
+
+        self.common.do_setup(context)
 
     def check_for_setup_error(self):
         """Returns an error if prerequisites aren't met."""
@@ -95,10 +153,7 @@ class HP3PARISCSIDriver(cinder.volume.driver.ISCSIDriver):
         metadata = self.common.create_volume(volume)
         self.common.client_logout()
 
-        return {'provider_location': "%s:%s" %
-                (self.configuration.iscsi_ip_address,
-                 self.configuration.iscsi_port),
-                'metadata': metadata}
+        return {'metadata': metadata}
 
     @utils.synchronized('3par', external=True)
     def create_cloned_volume(self, volume, src_vref):
@@ -107,10 +162,7 @@ class HP3PARISCSIDriver(cinder.volume.driver.ISCSIDriver):
         new_vol = self.common.create_cloned_volume(volume, src_vref)
         self.common.client_logout()
 
-        return {'provider_location': "%s:%s" %
-                (self.configuration.iscsi_ip_address,
-                 self.configuration.iscsi_port),
-                'metadata': new_vol}
+        return {'metadata': new_vol}
 
     @utils.synchronized('3par', external=True)
     def delete_volume(self, volume):
@@ -168,9 +220,6 @@ class HP3PARISCSIDriver(cinder.volume.driver.ISCSIDriver):
           * create vlun on the 3par
         """
         self.common.client_login()
-        # get the target_iqn on the 3par interface.
-        target_iqn = self._iscsi_discover_target_iqn(
-            self.configuration.iscsi_ip_address)
 
         # we have to make sure we have a host
         host = self._create_host(volume, connector)
@@ -179,11 +228,14 @@ class HP3PARISCSIDriver(cinder.volume.driver.ISCSIDriver):
         vlun = self.common.create_vlun(volume, host)
 
         self.common.client_logout()
+
+        iscsi_ip = self._get_iscsi_ip(host['name'])
+        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" %
-                         (self.configuration.iscsi_ip_address,
-                          self.configuration.iscsi_port),
-                         'target_iqn': target_iqn,
+                         (iscsi_ip, iscsi_ip_port),
+                         'target_iqn': iscsi_target_iqn,
                          'target_lun': vlun['lun'],
                          'target_discovered': True
                          }
@@ -199,21 +251,6 @@ class HP3PARISCSIDriver(cinder.volume.driver.ISCSIDriver):
                                          connector['initiator'])
         self.common.client_logout()
 
-    def _iscsi_discover_target_iqn(self, remote_ip):
-        result = self.common._cli_run('showport -ids', None)
-
-        iqn = None
-        if result:
-            # first line is header
-            result = result[1:]
-            for line in result:
-                info = line.split(",")
-                if info and len(info) > 2:
-                    if info[1] == remote_ip:
-                        iqn = info[2]
-
-        return iqn
-
     def _create_3par_iscsi_host(self, hostname, iscsi_iqn, domain, persona_id):
         """Create a 3PAR host.
 
@@ -268,3 +305,81 @@ class HP3PARISCSIDriver(cinder.volume.driver.ISCSIDriver):
     @utils.synchronized('3par', external=True)
     def remove_export(self, context, volume):
         pass
+
+    def _get_iscsi_ip(self, hostname):
+        """Get an iSCSI IP address to use.
+
+        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
+        """
+        if len(self.iscsi_ips) == 1:
+            return self.iscsi_ips.keys()[0]
+
+        # if we currently have an active port, use it
+        nsp = self._get_active_nsp(hostname)
+
+        if nsp is None:
+            # no active vlun, find least busy port
+            nsp = self._get_least_used_nsp(self._get_iscsi_nsps())
+            if 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(nsp)
+
+    def _get_iscsi_nsps(self):
+        """Return the list of candidate nsps."""
+        nsps = []
+        for value in self.iscsi_ips.values():
+            nsps.append(value['nsp'])
+        return nsps
+
+    def _get_ip_using_nsp(self, nsp):
+        """Return IP assiciated with given nsp."""
+        for (key, value) in self.iscsi_ips.items():
+            if value['nsp'] == nsp:
+                return key
+
+    def _get_active_nsp(self, hostname):
+        """Return the active nsp, if one exists, for the given host."""
+        result = self.common._cli_run('showvlun -a -host %s' % hostname, None)
+        if result:
+            # first line is header
+            result = result[1:]
+            for line in result:
+                info = line.split(",")
+                if info and len(info) > 4:
+                    return info[4]
+
+    def _get_least_used_nsp(self, nspss):
+        """"Return the nsp that has the fewest active vluns."""
+        # return only the nsp (node:server:port)
+        result = self.common._cli_run('showvlun -a -showcols Port', None)
+
+        # count the number of nsps (there is 1 for each active vlun)
+        nsp_counts = {}
+        for nsp in nspss:
+            # initialize counts to zero
+            nsp_counts[nsp] = 0
+
+        current_least_used_nsp = None
+        if result:
+            # first line is header
+            result = result[1:]
+            for line in result:
+                nsp = line.strip()
+                if nsp in nsp_counts:
+                    nsp_counts[nsp] = nsp_counts[nsp] + 1
+
+            # identify key (nsp) of least used nsp
+            current_smallest_count = sys.maxint
+            for (nsp, count) in nsp_counts.iteritems():
+                if count < current_smallest_count:
+                    current_least_used_nsp = nsp
+                    current_smallest_count = count
+
+        return current_least_used_nsp
index 666ebc1af2055103ed7e37c3522f281f09d50d83..59898a44ffdae4851c5ea3a94e2a8b5c1e128e20 100644 (file)
 # Enable HTTP debugging to 3PAR (boolean value)
 #hp3par_debug=false
 
+#List of target iSCSI addresses to use (list value)
+#hp3par_iscsi_ips=
+
 
 #
 # Options defined in cinder.volume.drivers.san.san