From d9e2c642e229f6a7e9448e81b64a94493a89c280 Mon Sep 17 00:00:00 2001 From: Jim Branen Date: Wed, 10 Jul 2013 12:34:09 -0700 Subject: [PATCH] Adds multiple iSCSI port support to 3PAR 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 | 344 +++++++++++++----- .../volume/drivers/san/hp/hp_3par_common.py | 28 +- cinder/volume/drivers/san/hp/hp_3par_iscsi.py | 183 ++++++++-- etc/cinder/cinder.conf.sample | 3 + 4 files changed, 424 insertions(+), 134 deletions(-) diff --git a/cinder/tests/test_hp3par.py b/cinder/tests/test_hp3par.py index 9587cd553..547b4e2c4 100644 --- a/cinder/tests/test_hp3par.py +++ b/cinder/tests/test_hp3par.py @@ -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') diff --git a/cinder/volume/drivers/san/hp/hp_3par_common.py b/cinder/volume/drivers/san/hp/hp_3par_common.py index 0e46253aa..f21feefac 100644 --- a/cinder/volume/drivers/san/hp/hp_3par_common.py +++ b/cinder/volume/drivers/san/hp/hp_3par_common.py @@ -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 diff --git a/cinder/volume/drivers/san/hp/hp_3par_iscsi.py b/cinder/volume/drivers/san/hp/hp_3par_iscsi.py index ce158d16b..3fb90849b 100644 --- a/cinder/volume/drivers/san/hp/hp_3par_iscsi.py +++ b/cinder/volume/drivers/san/hp/hp_3par_iscsi.py @@ -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 diff --git a/etc/cinder/cinder.conf.sample b/etc/cinder/cinder.conf.sample index 666ebc1af..59898a44f 100644 --- a/etc/cinder/cinder.conf.sample +++ b/etc/cinder/cinder.conf.sample @@ -1160,6 +1160,9 @@ # 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 -- 2.45.2