From: Erlon R. Cruz Date: Tue, 4 Aug 2015 13:18:48 +0000 (-0300) Subject: Fix HNAS iSCSI 32 targets limitation error X-Git-Url: https://review.fuel-infra.org/gitweb?a=commitdiff_plain;h=68f752376241e3937d5f726fbfef3e40d8efbf35;p=openstack-build%2Fcinder-build.git Fix HNAS iSCSI 32 targets limitation error When attaching more than 32 targets to an HNAS iSCSI backend, the storage returns an error as there's a limitation on the number of volumes that can be attached to a target. This patch fixes the problem and creates new targets as needed. Closes-Bug: #1479072 Change-Id: Id29259024b003a65cc80dfe63cac1c2d26f36059 --- diff --git a/cinder/tests/unit/test_hitachi_hnas_backend.py b/cinder/tests/unit/test_hitachi_hnas_backend.py index c4b0f7700..4d3f47425 100644 --- a/cinder/tests/unit/test_hitachi_hnas_backend.py +++ b/cinder/tests/unit/test_hitachi_hnas_backend.py @@ -197,6 +197,38 @@ HNAS_RESULT21 = "Target created successfully." HNAS_RESULT22 = "Failed to establish SSC connection" +HNAS_RESULT23 = "\n\ +Alias : cinder-Gold\n\ +Globally unique name: iqn.2015-06.10.10.10.10:evstest1.cinder-gold\n\ +Comment :\n\ +Secret : None\n\ +Authentication : Enabled\n\ +Logical units : No logical units.\n\ +Access configuration :\n\ +\n\ +Alias : cinder-GoldIsh\n\ +Globally unique name: iqn.2015-06.10.10.10.10:evstest1.cinder-goldish\n\ +Comment :\n\ +Secret : None\n\ +Authentication : Enabled\n\ +Logical units : No logical units.\n\ +Access configuration :\n\ +\n\ +Alias : cinder-default\n\ +Globally unique name: iqn.2014-12.10.10.10.10:evstest1.cinder-default\n\ +Comment :\n\ +Secret : pxr6U37LZZJBoMc\n\ +Authentication : Disabled\n\ +Logical units : Logical units :\n\ +\n\ + LUN Logical Unit\n\ + ---- --------------------------------\n\ + 0 volume-8ddd1a54-9daf-4fa5-842...\n\ + 1 volume-99da7ae7-1e7f-4d57-8bf...\n\ +\n\ +Access configuration :\n\ +" + HNAS_CMDS = { ('ssh', '0.0.0.0', 'supervisor', 'supervisor', 'evsfs', 'list'): ["%s" % HNAS_RESULT1, ""], @@ -257,7 +289,14 @@ HNAS_CMDS = { '/.cinder/test_clone.iscsi'): ["%s" % HNAS_RESULT16, ""], ('ssh', '0.0.0.0', 'supervisor', 'supervisor', 'evsipaddr', '-e', '1'): - ["%s" % HNAS_RESULT17, ""] + ["%s" % HNAS_RESULT17, ""], + ('ssh', '0.0.0.0', 'supervisor', 'supervisor', + 'console-context', '--evs', '1', 'iscsi-target', 'list'): + ["%s" % HNAS_RESULT23, ""], + ('ssh', '0.0.0.0', 'supervisor', 'supervisor', 'console-context', '--evs', + '1', 'iscsi-target', 'addlu', 'cinder-default', + 'volume-8ddd1a54-0000-0000-0000', '2'): + ["%s" % HNAS_RESULT13, ""] } DRV_CONF = {'ssh_enabled': 'True', @@ -431,9 +470,10 @@ class HDSHNASBendTest(test.TestCase): side_effect=m_run_cmd) def test_add_iscsi_conn(self, m_cmd): out = self.hnas_bend.add_iscsi_conn("ssh", "0.0.0.0", "supervisor", - "supervisor", "test_lun", + "supervisor", + "volume-8ddd1a54-0000-0000-0000", "test_hdp", "test_port", - "test_iqn", "test_init") + "cinder-default", "test_init") self.assertIn('successfully paired', out) @@ -445,7 +485,7 @@ class HDSHNASBendTest(test.TestCase): self.assertIn('already deleted', out) - @mock.patch.object(hnas_backend.HnasBackend, '_get_evs', return_value=0) + @mock.patch.object(hnas_backend.HnasBackend, 'get_evs', return_value=0) @mock.patch.object(hnas_backend.HnasBackend, 'run_cmd') def test_get_targetiqn(self, m_cmd, m_get_evs): @@ -493,3 +533,60 @@ class HDSHNASBendTest(test.TestCase): "supervisor", "test_iqn", "test_hdp") self.assertEqual('', out) + + @mock.patch.object(hnas_backend.HnasBackend, 'run_cmd') + def test_get_targets(self, m_run_cmd): + # Test normal behaviour + m_run_cmd.return_value = (HNAS_RESULT23, "") + tgt_list = self.hnas_bend._get_targets("ssh", "0.0.0.0", "supervisor", + "supervisor", 1) + self.assertEqual(3, len(tgt_list)) + self.assertEqual(2, len(tgt_list[2]['luns'])) + + # Test calling with parameter + tgt_list = self.hnas_bend._get_targets("ssh", "0.0.0.0", "supervisor", + "supervisor", 1, + 'cinder-default') + self.assertEqual(1, len(tgt_list)) + self.assertEqual(2, len(tgt_list[0]['luns'])) + + # Test error in BE command + m_run_cmd.side_effect = putils.ProcessExecutionError + tgt_list = self.hnas_bend._get_targets("ssh", "0.0.0.0", "supervisor", + "supervisor", 1) + self.assertEqual(0, len(tgt_list)) + + @mock.patch.object(hnas_backend.HnasBackend, + 'run_cmd', side_effect=m_run_cmd) + def test_check_targets(self, m_run_cmd): + result, tgt = self.hnas_bend.check_target("ssh", "0.0.0.0", + "supervisor", + "supervisor", "test_hdp", + "cinder-default") + self.assertTrue(result) + self.assertEqual('cinder-default', tgt['alias']) + + result, tgt = self.hnas_bend.check_target("ssh", "0.0.0.0", + "supervisor", + "supervisor", "test_hdp", + "cinder-no-target") + self.assertFalse(result) + self.assertIsNone(tgt) + + @mock.patch.object(hnas_backend.HnasBackend, + 'run_cmd', side_effect=m_run_cmd) + def test_check_lu(self, m_run_cmd): + ret = self.hnas_bend.check_lu("ssh", "0.0.0.0", "supervisor", + "supervisor", + "volume-8ddd1a54-9daf-4fa5-842", + "test_hdp") + result, lunid, tgt = ret + self.assertTrue(result) + self.assertEqual('0', lunid) + + ret = self.hnas_bend.check_lu("ssh", "0.0.0.0", "supervisor", + "supervisor", + "volume-8ddd1a54-0000-0000-000", + "test_hdp") + result, lunid, tgt = ret + self.assertFalse(result) diff --git a/cinder/tests/unit/test_hitachi_hnas_iscsi.py b/cinder/tests/unit/test_hitachi_hnas_iscsi.py index 46c2af3d7..f5bb826d6 100644 --- a/cinder/tests/unit/test_hitachi_hnas_iscsi.py +++ b/cinder/tests/unit/test_hitachi_hnas_iscsi.py @@ -82,7 +82,8 @@ HNAS_WRONG_CONF2 = """ # The following information is passed on to tests, when creating a volume _VOLUME = {'name': 'testvol', 'volume_id': '1234567890', 'size': 128, 'volume_type': 'silver', 'volume_type_id': '1', - 'provider_location': None, 'id': 'abcdefg', + 'provider_location': '83-68-96-AA-DA-5D.volume-2dfe280e-470a-4182' + '-afb8-1755025c35b8', 'id': 'abcdefg', 'host': 'host1@hnas-iscsi-backend#silver'} @@ -257,6 +258,16 @@ class SimulatedHnasBackend(object): self.out = """wGkJhTpXaaYJ5Rv""" return self.out + def get_evs(self, cmd, ip0, user, pw, fsid): + return '1' + + def check_lu(self, cmd, ip0, user, pw, volume_name, hdp): + return True, 1, {'alias': 'cinder-default', 'secret': 'mysecret', + 'iqn': 'iqn.1993-08.org.debian:01:11f90746eb2'} + + def check_target(self, cmd, ip0, user, pw, hdp, target_alias): + return False, None + class HNASiSCSIDriverTest(test.TestCase): """Test HNAS iSCSI volume driver.""" @@ -426,3 +437,31 @@ class HNASiSCSIDriverTest(test.TestCase): def test_get_pool(self, m_ext_spec): label = self.driver.get_pool(_VOLUME) self.assertEqual('silver', label) + + @mock.patch.object(time, 'sleep') + @mock.patch.object(iscsi.HDSISCSIDriver, '_update_vol_location') + def test_get_service_target(self, m_update_vol_location, m_sleep): + + vol = _VOLUME.copy() + self.backend.check_lu = mock.MagicMock() + self.backend.check_target = mock.MagicMock() + + # Test the case where volume is not already mapped - CHAP enabled + self.backend.check_lu.return_value = (False, 0, None) + self.backend.check_target.return_value = (False, None) + ret = self.driver._get_service_target(vol) + iscsi_ip, iscsi_port, ctl, svc_port, hdp, alias, secret = ret + self.assertEqual('evs1-tgt0', alias) + + # Test the case where volume is not already mapped - CHAP disabled + self.driver.config['chap_enabled'] = 'False' + ret = self.driver._get_service_target(vol) + iscsi_ip, iscsi_port, ctl, svc_port, hdp, alias, secret = ret + self.assertEqual('evs1-tgt0', alias) + + # Test the case where all targets are full + fake_tgt = {'alias': 'fake', 'luns': range(0, 32)} + self.backend.check_lu.return_value = (False, 0, None) + self.backend.check_target.return_value = (True, fake_tgt) + self.assertRaises(exception.NoMoreTargets, + self.driver._get_service_target, vol) diff --git a/cinder/volume/drivers/hitachi/hnas_backend.py b/cinder/volume/drivers/hitachi/hnas_backend.py index f0aa2a4f5..6b2e434af 100644 --- a/cinder/volume/drivers/hitachi/hnas_backend.py +++ b/cinder/volume/drivers/hitachi/hnas_backend.py @@ -25,7 +25,7 @@ from oslo_log import log as logging from oslo_utils import units import six -from cinder.i18n import _, _LW, _LI +from cinder.i18n import _, _LW, _LI, _LE from cinder import exception from cinder import ssh_utils from cinder import utils @@ -44,11 +44,11 @@ class HnasBackend(object): def run_cmd(self, cmd, ip0, user, pw, *args, **kwargs): """Run a command on SMU or using SSH - :param cmd: the command that will be run on SMU + :param cmd: ssc command name :param ip0: string IP address of controller :param user: string user authentication for array :param pw: string password authentication for array - :returns: formated string with version information + :return: formated string with version information """ LOG.debug('Enable ssh: %s', six.text_type(self.drv_configs['ssh_enabled'])) @@ -116,11 +116,12 @@ class HnasBackend(object): def get_version(self, cmd, ver, ip0, user, pw): """Gets version information from the storage unit + :param cmd: ssc command name :param ver: string driver version :param ip0: string IP address of controller :param user: string user authentication for array :param pw: string password authentication for array - :returns: formated string with version information + :return: formated string with version information """ if (self.drv_configs['ssh_enabled'] == 'True' and self.drv_configs['cluster_admin_ip0'] is not None): @@ -154,10 +155,11 @@ class HnasBackend(object): def get_iscsi_info(self, cmd, ip0, user, pw): """Gets IP addresses for EVSs, use EVSID as controller. + :param cmd: ssc command name :param ip0: string IP address of controller :param user: string user authentication for array :param pw: string password authentication for array - :returns: formated string with iSCSI information + :return: formated string with iSCSI information """ out, err = self.run_cmd(cmd, ip0, user, pw, @@ -180,11 +182,12 @@ class HnasBackend(object): def get_hdp_info(self, cmd, ip0, user, pw, fslabel=None): """Gets the list of filesystems and fsids. + :param cmd: ssc command name :param ip0: string IP address of controller :param user: string user authentication for array :param pw: string password authentication for array :param fslabel: filesystem label we want to get info - :returns: formated string with filesystems and fsids + :return: formated string with filesystems and fsids """ if fslabel is None: @@ -240,12 +243,19 @@ class HnasBackend(object): {'out': newout, 'err': err}) return newout - def _get_evs(self, cmd, ip0, user, pw, fsid): - """Gets the EVSID for the named filesystem.""" + def get_evs(self, cmd, ip0, user, pw, fsid): + """Gets the EVSID for the named filesystem. + + :param cmd: ssc command name + :param ip0: string IP address of controller + :param user: string user authentication for array + :param pw: string password authentication for array + :return: EVS id of the file system + """ out, err = self.run_cmd(cmd, ip0, user, pw, "evsfs", "list", check_exit_code=True) - LOG.debug('get_evs: out %s', out) + LOG.debug('get_evs: out %s.', out) lines = out.split('\n') for line in lines: @@ -292,9 +302,89 @@ class HnasBackend(object): {'out': out, 'fslabel': fslabel}) return 0 + def _get_targets(self, cmd, ip0, user, pw, evsid, tgtalias=None): + """Get the target list of an EVS. + + Get the target list of an EVS. Optionally can return the target + list of a specific target. + """ + + LOG.debug("Getting target list for evs %s, tgtalias: %s.", + evsid, tgtalias) + + try: + out, err = self.run_cmd(cmd, ip0, user, pw, "console-context", + "--evs", evsid, 'iscsi-target', 'list', + check_exit_code=True) + except putils.ProcessExecutionError as e: + LOG.error(_LE('Error getting iSCSI target info ' + 'from EVS %(evs)s.'), {'evs': evsid}) + LOG.debug("_get_targets out: %(out)s, err: %(err)s.", + {'out': e.stdout, 'err': e.stderr}) + return [] + + tgt_list = [] + if 'No targets' in out: + LOG.debug("No targets found in EVS %(evsid)s.", {'evsid': evsid}) + return tgt_list + + tgt_raw_list = out.split('Alias')[1:] + for tgt_raw_info in tgt_raw_list: + tgt = {} + tgt['alias'] = tgt_raw_info.split('\n')[0].split(' ').pop() + tgt['iqn'] = tgt_raw_info.split('\n')[1].split(' ').pop() + tgt['secret'] = tgt_raw_info.split('\n')[3].split(' ').pop() + tgt['auth'] = tgt_raw_info.split('\n')[4].split(' ').pop() + luns = [] + tgt_raw_info = tgt_raw_info.split('\n\n')[1] + tgt_raw_list = tgt_raw_info.split('\n')[2:] + + for lun_raw_line in tgt_raw_list: + lun_raw_line = lun_raw_line.strip() + lun_raw_line = lun_raw_line.split(' ') + lun = {} + lun['id'] = lun_raw_line[0] + lun['name'] = lun_raw_line.pop() + luns.append(lun) + + tgt['luns'] = luns + + if tgtalias == tgt['alias']: + return [tgt] + + tgt_list.append(tgt) + + if tgtalias is not None: + # We tried to find 'tgtalias' but didn't find. Return a empty + # list. + LOG.debug("There's no target %(alias)s in EVS %(evsid)s.", + {'alias': tgtalias, 'evsid': evsid}) + return [] + + LOG.debug("Targets in EVS %(evs)s: %(tgtl)s.", + {'evs': evsid, 'tgtl': tgt_list}) + return tgt_list + + def _get_unused_lunid(self, cmd, ip0, user, pw, tgt_info): + + if len(tgt_info['luns']) == 0: + return 0 + + free_lun = 0 + for lun in tgt_info['luns']: + if int(lun['id']) == free_lun: + free_lun += 1 + + if int(lun['id']) > free_lun: + # Found a free LUN number + break + + return free_lun + def get_nfs_info(self, cmd, ip0, user, pw): """Gets information on each NFS export. + :param cmd: ssc command name :param ip0: string IP address of controller :param user: string user authentication for array :param pw: string password authentication for array @@ -322,7 +412,7 @@ class HnasBackend(object): fs = inf[3] if 'Transfer setting' in line and fs != "": fsid = self._get_fsid(cmd, ip0, user, pw, fs) - evsid = self._get_evs(cmd, ip0, user, pw, fsid) + evsid = self.get_evs(cmd, ip0, user, pw, fsid) ips = self._get_evsips(cmd, ip0, user, pw, evsid) newout += "Export: %s Path: %s HDP: %s FSID: %s \ EVS: %s IPS: %s\n" \ @@ -339,6 +429,7 @@ class HnasBackend(object): If the operation can not be performed for some reason, utils.execute() throws an error and aborts the operation. Used for iSCSI only + :param cmd: ssc command name :param ip0: string IP address of controller :param user: string user authentication for array :param pw: string password authentication for array @@ -349,7 +440,7 @@ class HnasBackend(object): successfully created' """ - _evsid = self._get_evs(cmd, ip0, user, pw, hdp) + _evsid = self.get_evs(cmd, ip0, user, pw, hdp) out, err = self.run_cmd(cmd, ip0, user, pw, "console-context", "--evs", _evsid, 'iscsi-lu', 'add', "-e", @@ -361,12 +452,13 @@ class HnasBackend(object): out = "LUN %s HDP: %s size: %s MB, is successfully created" \ % (name, hdp, size) - LOG.debug('create_lu: %s', out) + LOG.debug('create_lu: %s.', out) return out def delete_lu(self, cmd, ip0, user, pw, hdp, lun): """Delete an logical unit. Used for iSCSI only + :param cmd: ssc command name :param ip0: string IP address of controller :param user: string user authentication for array :param pw: string password authentication for array @@ -375,14 +467,14 @@ class HnasBackend(object): :returns: formated string 'Logical unit deleted successfully.' """ - _evsid = self._get_evs(cmd, ip0, user, pw, hdp) + _evsid = self.get_evs(cmd, ip0, user, pw, hdp) out, err = self.run_cmd(cmd, ip0, user, pw, "console-context", "--evs", _evsid, 'iscsi-lu', 'del', '-d', '-f', lun, check_exit_code=True) - LOG.debug('delete_lu: %(out)s -- %(err)s', {'out': out, 'err': err}) + LOG.debug('delete_lu: %(out)s -- %(err)s.', {'out': out, 'err': err}) return out def create_dup(self, cmd, ip0, user, pw, src_lun, hdp, size, name): @@ -391,6 +483,7 @@ class HnasBackend(object): Clone primitive used to support all iSCSI snapshot/cloning functions. Used for iSCSI only. + :param cmd: ssc command name :param ip0: string IP address of controller :param user: string user authentication for array :param pw: string password authentication for array @@ -400,7 +493,7 @@ class HnasBackend(object): :returns: formated string """ - _evsid = self._get_evs(cmd, ip0, user, pw, hdp) + _evsid = self.get_evs(cmd, ip0, user, pw, hdp) out, err = self.run_cmd(cmd, ip0, user, pw, "console-context", "--evs", _evsid, 'iscsi-lu', 'clone', '-e', @@ -411,7 +504,7 @@ class HnasBackend(object): out = "LUN %s HDP: %s size: %s MB, is successfully created" \ % (name, hdp, size) - LOG.debug('create_dup: %(out)s -- %(err)s', {'out': out, 'err': err}) + LOG.debug('create_dup: %(out)s -- %(err)s.', {'out': out, 'err': err}) return out def file_clone(self, cmd, ip0, user, pw, fslabel, src, name): @@ -419,6 +512,7 @@ class HnasBackend(object): Clone primitive used to support all NFS snapshot/cloning functions. + :param cmd: ssc command name :param ip0: string IP address of controller :param user: string user authentication for array :param pw: string password authentication for array @@ -429,7 +523,7 @@ class HnasBackend(object): """ _fsid = self._get_fsid(cmd, ip0, user, pw, fslabel) - _evsid = self._get_evs(cmd, ip0, user, pw, _fsid) + _evsid = self.get_evs(cmd, ip0, user, pw, _fsid) out, err = self.run_cmd(cmd, ip0, user, pw, "console-context", "--evs", _evsid, 'file-clone-create', '-f', fslabel, @@ -438,12 +532,13 @@ class HnasBackend(object): out = "LUN %s HDP: %s Clone: %s -> %s" % (name, _fsid, src, name) - LOG.debug('file_clone: %(out)s -- %(err)s', {'out': out, 'err': err}) + LOG.debug('file_clone: %(out)s -- %(err)s.', {'out': out, 'err': err}) return out def extend_vol(self, cmd, ip0, user, pw, hdp, lun, new_size, name): """Extend a iSCSI volume. + :param cmd: ssc command name :param ip0: string IP address of controller :param user: string user authentication for array :param pw: string password authentication for array @@ -453,7 +548,7 @@ class HnasBackend(object): :param name: formated string """ - _evsid = self._get_evs(cmd, ip0, user, pw, hdp) + _evsid = self.get_evs(cmd, ip0, user, pw, hdp) out, err = self.run_cmd(cmd, ip0, user, pw, "console-context", "--evs", _evsid, 'iscsi-lu', 'expand', @@ -462,81 +557,59 @@ class HnasBackend(object): out = ("LUN: %s successfully extended to %s MB" % (name, new_size)) - LOG.debug('extend_vol: %s', out) + LOG.debug('extend_vol: %s.', out) return out @utils.retry(putils.ProcessExecutionError, retries=HNAS_SSC_RETRIES) - def add_iscsi_conn(self, cmd, ip0, user, pw, lun, hdp, - port, iqn, initiator): + def add_iscsi_conn(self, cmd, ip0, user, pw, lun_name, hdp, + port, tgtalias, initiator): """Setup the lun on on the specified target port + :param cmd: ssc command name :param ip0: string IP address of controller :param user: string user authentication for array :param pw: string password authentication for array - :param lun: id of the logical unit being extended + :param lun_name: id of the logical unit being extended :param hdp: data pool of the logical unit :param port: iSCSI port - :param iqn: iSCSI qualified name + :param tgtalias: iSCSI qualified name :param initiator: initiator address """ - _evsid = self._get_evs(cmd, ip0, user, pw, hdp) - out, err = self.run_cmd(cmd, ip0, user, pw, "console-context", - "--evs", _evsid, - 'iscsi-target', 'list', iqn, - check_exit_code=True) - - # even though ssc uses the target alias, need to return the full iqn - fulliqn = "" - lines = out.split('\n') - for line in lines: - if 'Globally unique name' in line: - fulliqn = line.split()[3] - - # find first free hlun - hlun = 0 - for line in lines: - if line.startswith(' '): - lunline = line.split()[0] - vol = line.split()[1] - if lunline[0].isdigit(): - # see if already mounted - if vol[:29] == lun[:29]: - LOG.info(_LI('lun: %(lun)s already mounted %(lline)s'), - {'lun': lun, 'lline': lunline}) - conn = (int(lunline), lun, initiator, hlun, fulliqn, - hlun, hdp, port) - out = "H-LUN: %d alreadymapped LUN: %s, iSCSI \ - Initiator: %s @ index: %d, and Target: %s \ - @ index %d is successfully paired @ CTL: \ - %s, Port: %s" % conn - LOG.debug('add_iscsi_conn: returns %s', out) - return out - - if int(lunline) == hlun: - hlun += 1 - if int(lunline) > hlun: - # found a hole - break + LOG.debug('Adding %(lun)s to %(tgt)s returns %(tgt)s.', + {'lun': lun_name, 'tgt': tgtalias}) + found, lunid, tgt = self.check_lu(cmd, ip0, user, pw, lun_name, hdp) + evsid = self.get_evs(cmd, ip0, user, pw, hdp) + + if found: + conn = (int(lunid), lun_name, initiator, int(lunid), tgt['iqn'], + int(lunid), hdp, port) + out = ("H-LUN: %d mapped LUN: %s, iSCSI Initiator: %s " + "@ index: %d, and Target: %s @ index %d is " + "successfully paired @ CTL: %s, Port: %s.") % conn + else: + tgt = self._get_targets(cmd, ip0, user, pw, evsid, tgtalias) + lunid = self._get_unused_lunid(cmd, ip0, user, pw, tgt[0]) - out, err = self.run_cmd(cmd, ip0, user, pw, "console-context", - "--evs", _evsid, - 'iscsi-target', 'addlu', - iqn, lun, six.text_type(hlun), - check_exit_code=True) + out, err = self.run_cmd(cmd, ip0, user, pw, "console-context", + "--evs", evsid, + 'iscsi-target', 'addlu', + tgtalias, lun_name, six.text_type(lunid), + check_exit_code=True) - conn = (int(hlun), lun, initiator, int(hlun), fulliqn, int(hlun), - hdp, port) - out = "H-LUN: %d mapped LUN: %s, iSCSI Initiator: %s \ - @ index: %d, and Target: %s @ index %d is \ - successfully paired @ CTL: %s, Port: %s" % conn + conn = (int(lunid), lun_name, initiator, int(lunid), tgt[0]['iqn'], + int(lunid), hdp, port) + out = ("H-LUN: %d mapped LUN: %s, iSCSI Initiator: %s " + "@ index: %d, and Target: %s @ index %d is " + "successfully paired @ CTL: %s, Port: %s.") % conn - LOG.debug('add_iscsi_conn: returns %s', out) + LOG.debug('add_iscsi_conn: returns %s.', out) return out def del_iscsi_conn(self, cmd, ip0, user, pw, evsid, iqn, hlun): """Remove the lun on on the specified target port + :param cmd: ssc command name :param ip0: string IP address of controller :param user: string user authentication for array :param pw: string password authentication for array @@ -563,7 +636,7 @@ class HnasBackend(object): if out != "": # hlun wasn't found - LOG.info(_LI('del_iscsi_conn: hlun not found %s'), out) + LOG.info(_LI('del_iscsi_conn: hlun not found %s.'), out) return out # remove the LU from the target @@ -576,14 +649,14 @@ class HnasBackend(object): out = "H-LUN: %d successfully deleted from target %s" \ % (int(hlun), iqn) - LOG.debug('del_iscsi_conn: %s', out) + LOG.debug('del_iscsi_conn: %s.', out) return out def get_targetiqn(self, cmd, ip0, user, pw, targetalias, hdp, secret): """Obtain the targets full iqn - Return the target's full iqn rather than its alias. - + Returns the target's full iqn rather than its alias. + :param cmd: ssc command name :param ip0: string IP address of controller :param user: string user authentication for array :param pw: string password authentication for array @@ -593,7 +666,7 @@ class HnasBackend(object): :return: string with full IQN """ - _evsid = self._get_evs(cmd, ip0, user, pw, hdp) + _evsid = self.get_evs(cmd, ip0, user, pw, hdp) out, err = self.run_cmd(cmd, ip0, user, pw, "console-context", "--evs", _evsid, 'iscsi-target', 'list', targetalias, @@ -626,6 +699,7 @@ class HnasBackend(object): def set_targetsecret(self, cmd, ip0, user, pw, targetalias, hdp, secret): """Sets the chap secret for the specified target. + :param cmd: ssc command name :param ip0: string IP address of controller :param user: string user authentication for array :param pw: string password authentication for array @@ -634,7 +708,7 @@ class HnasBackend(object): :param secret: CHAP secret of the target """ - _evsid = self._get_evs(cmd, ip0, user, pw, hdp) + _evsid = self.get_evs(cmd, ip0, user, pw, hdp) out, err = self.run_cmd(cmd, ip0, user, pw, "console-context", "--evs", _evsid, 'iscsi-target', 'list', @@ -659,6 +733,7 @@ class HnasBackend(object): def get_targetsecret(self, cmd, ip0, user, pw, targetalias, hdp): """Returns the chap secret for the specified target. + :param cmd: ssc command name :param ip0: string IP address of controller :param user: string user authentication for array :param pw: string password authentication for array @@ -667,7 +742,7 @@ class HnasBackend(object): :return secret: CHAP secret of the target """ - _evsid = self._get_evs(cmd, ip0, user, pw, hdp) + _evsid = self.get_evs(cmd, ip0, user, pw, hdp) out, err = self.run_cmd(cmd, ip0, user, pw, "console-context", "--evs", _evsid, 'iscsi-target', 'list', targetalias, @@ -687,3 +762,65 @@ class HnasBackend(object): return secret else: return "" + + def check_target(self, cmd, ip0, user, pw, hdp, target_alias): + """Checks if a given target exists and gets its info + + :param cmd: ssc command name + :param ip0: string IP address of controller + :param user: string user authentication for array + :param pw: string password authentication for array + :param hdp: pool name used + :param target_alias: alias of the target + :return True if target exists + :return list with the target info + """ + + LOG.debug("Checking if target %(tgt)s exists.", {'tgt': target_alias}) + evsid = self.get_evs(cmd, ip0, user, pw, hdp) + tgt_list = self._get_targets(cmd, ip0, user, pw, evsid) + + for tgt in tgt_list: + if tgt['alias'] == target_alias: + attached_luns = len(tgt['luns']) + LOG.debug("Target %(tgt)s has %(lun)s volumes.", + {'tgt': target_alias, 'lun': attached_luns}) + return True, tgt + + LOG.debug("Target %(tgt)s does not exist.", {'tgt': target_alias}) + return False, None + + def check_lu(self, cmd, ip0, user, pw, volume_name, hdp): + """Checks if a given LUN is already mapped + + :param cmd: ssc command name + :param ip0: string IP address of controller + :param user: string user authentication for array + :param pw: string password authentication for array + :param volume_name: number of the LUN + :param hdp: storage pool of the LUN + :return True if the lun is attached + :return the LUN id + :return Info related to the target + """ + + LOG.debug("Checking if vol %s (hdp: %s) is attached.", + volume_name, hdp) + evsid = self.get_evs(cmd, ip0, user, pw, hdp) + tgt_list = self._get_targets(cmd, ip0, user, pw, evsid) + + for tgt in tgt_list: + if len(tgt['luns']) == 0: + continue + + for lun in tgt['luns']: + lunid = lun['id'] + lunname = lun['name'] + if lunname[:29] == volume_name[:29]: + LOG.debug("LUN %(lun)s attached on %(lunid)s, " + "target: %(tgt)s.", + {'lun': volume_name, 'lunid': lunid, 'tgt': tgt}) + return True, lunid, tgt + + LOG.debug("LUN %(lun)s not attached.", {'lun': volume_name}) + return False, 0, None diff --git a/cinder/volume/drivers/hitachi/hnas_iscsi.py b/cinder/volume/drivers/hitachi/hnas_iscsi.py index 925d72583..d48d7900a 100644 --- a/cinder/volume/drivers/hitachi/hnas_iscsi.py +++ b/cinder/volume/drivers/hitachi/hnas_iscsi.py @@ -18,6 +18,7 @@ iSCSI Cinder Volume driver for Hitachi Unified Storage (HUS-HNAS) platform. """ import os +import six from xml.etree import ElementTree as ETree from oslo_concurrency import processutils @@ -26,6 +27,7 @@ from oslo_log import log as logging from oslo_utils import excutils from oslo_utils import units + from cinder import exception from cinder.i18n import _, _LE, _LI, _LW from cinder import utils as cinder_utils @@ -35,7 +37,7 @@ from cinder.volume import utils from cinder.volume import volume_types -HDS_HNAS_ISCSI_VERSION = '3.1.0' +HDS_HNAS_ISCSI_VERSION = '3.3.0' LOG = logging.getLogger(__name__) @@ -50,6 +52,7 @@ CONF.register_opts(iSCSI_OPTS) HNAS_DEFAULT_CONFIG = {'hnas_cmd': 'ssc', 'chap_enabled': 'True', 'ssh_port': '22'} +MAX_HNAS_ISCSI_TARGETS = 32 def factory_bend(drv_configs): @@ -157,7 +160,10 @@ class HDSISCSIDriver(driver.ISCSIDriver): """HDS HNAS volume driver. Version 1.0.0: Initial driver version - Version 2.2.0: Added support to SSH authentication + Version 2.2.0: Added support to SSH authentication + Version 3.2.0: Added pool aware scheduling + Fixed concurrency errors + Version 3.3.0: Fixed iSCSI target limitation error """ def __init__(self, *args, **kwargs): @@ -213,10 +219,12 @@ class HDSISCSIDriver(driver.ISCSIDriver): return conf def _get_service(self, volume): - """Get available service parameters. + """Get the available service parameters - Get the available service parameters for a given volume using its type. - :param volume: dictionary volume reference + Get the available service parametersfor a given volume using its + type. + :param volume: dictionary volume reference + :return HDP related to the service """ label = utils.extract_host(volume['host'], level='pool') @@ -224,68 +232,145 @@ class HDSISCSIDriver(driver.ISCSIDriver): if label in self.config['services'].keys(): svc = self.config['services'][label] - # HNAS - one time lookup - # see if the client supports CHAP authentication and if - # iscsi_secret has already been set, retrieve the secret if - # available, otherwise generate and store - if self.config['chap_enabled'] == 'True': - # it may not exist, create and set secret - if 'iscsi_secret' not in svc: - LOG.info(_LI("Retrieving secret for service: %s"), label) - - out = self.bend.get_targetsecret(self.config['hnas_cmd'], - self.config['mgmt_ip0'], - self.config['username'], - self.config['password'], - 'cinder-' + label, - svc['hdp']) - svc['iscsi_secret'] = out - if svc['iscsi_secret'] == "": - svc['iscsi_secret'] = utils.generate_password()[0:15] - self.bend.set_targetsecret(self.config['hnas_cmd'], - self.config['mgmt_ip0'], - self.config['username'], - self.config['password'], - 'cinder-' + label, - svc['hdp'], - svc['iscsi_secret']) - - LOG.info(_LI("Set tgt CHAP secret for service: %s"), - label) - else: - # We set blank password when the client does not - # support CHAP. Later on, if the client tries to create a new - # target that does not exists in the backend, we check for this - # value and use a temporary dummy password. - if 'iscsi_secret' not in svc: - # Warns in the first time - LOG.info(_LI("CHAP authentication disabled")) - - svc['iscsi_secret'] = "" - - if 'iscsi_target' not in svc: - LOG.info(_LI("Retrieving target for service: %s"), label) - - out = self.bend.get_targetiqn(self.config['hnas_cmd'], - self.config['mgmt_ip0'], - self.config['username'], - self.config['password'], - 'cinder-' + label, - svc['hdp'], - svc['iscsi_secret']) - svc['iscsi_target'] = out - - self.config['services'][label] = svc - - service = (svc['iscsi_ip'], svc['iscsi_port'], svc['ctl'], - svc['port'], svc['hdp'], svc['iscsi_target'], - svc['iscsi_secret']) + return svc['hdp'] else: - LOG.info(_LI("Available services: %s"), + LOG.info(_LI("Available services: %s."), self.config['services'].keys()) - LOG.error(_LE("No configuration found for service: %s"), label) + LOG.error(_LE("No configuration found for service: %s."), label) raise exception.ParameterNotFound(param=label) + def _get_service_target(self, volume): + """Get the available service parameters + + Get the available service parameters for a given volume using + its type. + :param volume: dictionary volume reference + """ + + hdp = self._get_service(volume) + info = _loc_info(volume['provider_location']) + (arid, lun_name) = info['id_lu'] + + evsid = self.bend.get_evs(self.config['hnas_cmd'], + self.config['mgmt_ip0'], + self.config['username'], + self.config['password'], + hdp) + svc_label = utils.extract_host(volume['host'], level='pool') + svc = self.config['services'][svc_label] + + LOG.info(_LI("_get_service_target hdp: %s."), hdp) + LOG.info(_LI("config[services]: %s."), self.config['services']) + + mapped, lunid, tgt = self.bend.check_lu(self.config['hnas_cmd'], + self.config['mgmt_ip0'], + self.config['username'], + self.config['password'], + lun_name, hdp) + + LOG.info(_LI("Target is %(map)s! Targetlist = %(tgtl)s."), + {'map': "mapped" if mapped else "not mapped", 'tgtl': tgt}) + + # The volume is already mapped to a LUN, so no need to create any + # targets + if mapped: + service = (svc['iscsi_ip'], svc['iscsi_port'], svc['ctl'], + svc['port'], hdp, tgt['alias'], tgt['secret']) + return service + + # Each EVS can have up to 32 targets. Each target can have up to 32 + # LUNs attached and have the name format 'evs-tgt<0-N>'. We run + # from the first 'evs1-tgt0' until we find a target that is not already + # created in the BE or is created but have slots to place new targets. + found_tgt = False + for i in range(0, MAX_HNAS_ISCSI_TARGETS): + tgt_alias = 'evs' + evsid + '-tgt' + six.text_type(i) + # TODO(erlon): we need to go to the BE 32 times here + tgt_exist, tgt = self.bend.check_target(self.config['hnas_cmd'], + self.config['mgmt_ip0'], + self.config['username'], + self.config['password'], + hdp, tgt_alias) + if tgt_exist and len(tgt['luns']) < 32 or not tgt_exist: + # Target exists and has free space or, target does not exist + # yet. Proceed and use the target or create a target using this + # name. + found_tgt = True + break + + # If we've got here and found_tgt is not True, we run out of targets, + # raise and go away. + if not found_tgt: + LOG.error(_LE("No more targets avaliable.")) + raise exception.NoMoreTargets(param=tgt_alias) + + LOG.info(_LI("Using target label: %s."), tgt_alias) + + # Check if we have a secret stored for this target so we don't have to + # go to BE on every query + if 'targets' not in self.config.keys(): + self.config['targets'] = {} + + if tgt_alias not in self.config['targets'].keys(): + self.config['targets'][tgt_alias] = {} + + tgt_info = self.config['targets'][tgt_alias] + + # HNAS - one time lookup + # see if the client supports CHAP authentication and if + # iscsi_secret has already been set, retrieve the secret if + # available, otherwise generate and store + if self.config['chap_enabled'] == 'True': + # It may not exist, create and set secret. + if 'iscsi_secret' not in tgt_info.keys(): + LOG.info(_LI("Retrieving secret for service: %s."), + tgt_alias) + + out = self.bend.get_targetsecret(self.config['hnas_cmd'], + self.config['mgmt_ip0'], + self.config['username'], + self.config['password'], + tgt_alias, hdp) + tgt_info['iscsi_secret'] = out + if tgt_info['iscsi_secret'] == "": + randon_secret = utils.generate_password()[0:15] + tgt_info['iscsi_secret'] = randon_secret + self.bend.set_targetsecret(self.config['hnas_cmd'], + self.config['mgmt_ip0'], + self.config['username'], + self.config['password'], + tgt_alias, hdp, + tgt_info['iscsi_secret']) + + LOG.info(_LI("Set tgt CHAP secret for service: %s."), + tgt_alias) + else: + # We set blank password when the client does not + # support CHAP. Later on, if the client tries to create a new + # target that does not exists in the backend, we check for this + # value and use a temporary dummy password. + if 'iscsi_secret' not in tgt_info.keys(): + # Warns in the first time + LOG.info(_LI("CHAP authentication disabled.")) + + tgt_info['iscsi_secret'] = "" + + if 'tgt_iqn' not in tgt_info: + LOG.info(_LI("Retrieving target for service: %s."), tgt_alias) + + out = self.bend.get_targetiqn(self.config['hnas_cmd'], + self.config['mgmt_ip0'], + self.config['username'], + self.config['password'], + tgt_alias, hdp, + tgt_info['iscsi_secret']) + tgt_info['tgt_iqn'] = out + + self.config['targets'][tgt_alias] = tgt_info + + service = (svc['iscsi_ip'], svc['iscsi_port'], svc['ctl'], + svc['port'], hdp, tgt_alias, tgt_info['iscsi_secret']) + return service def _get_stats(self): @@ -306,7 +391,7 @@ class HDSISCSIDriver(driver.ISCSIDriver): self.config['password'], pool['hdp']) - LOG.debug('Query for pool %(pool)s: %(out)s', + LOG.debug('Query for pool %(pool)s: %(out)s.', {'pool': pool['pool_name'], 'out': out}) (hdp, size, _ign, used) = out.split()[1:5] # in MB @@ -318,7 +403,7 @@ class HDSISCSIDriver(driver.ISCSIDriver): hnas_stat['pools'] = self.pools - LOG.info(_LI("stats: stats: %s"), hnas_stat) + LOG.info(_LI("stats: stats: %s."), hnas_stat) return hnas_stat def _get_hdp_list(self): @@ -456,8 +541,7 @@ class HDSISCSIDriver(driver.ISCSIDriver): :param volume: dictionary volume reference """ - service = self._get_service(volume) - (_ip, _ipp, _ctl, _port, hdp, target, secret) = service + hdp = self._get_service(volume) out = self.bend.create_lu(self.config['hnas_cmd'], self.config['mgmt_ip0'], self.config['username'], @@ -486,8 +570,7 @@ class HDSISCSIDriver(driver.ISCSIDriver): if src['size'] != dst['size']: msg = 'clone volume size mismatch' raise exception.VolumeBackendAPIException(data=msg) - service = self._get_service(dst) - (_ip, _ipp, _ctl, _port, hdp, target, secret) = service + hdp = self._get_service(dst) size = int(src['size']) * units.Ki source_vol = self._id_to_vol(src['id']) (arid, slun) = _loc_info(source_vol['provider_location'])['id_lu'] @@ -512,8 +595,7 @@ class HDSISCSIDriver(driver.ISCSIDriver): :param new_size: int size in GB to extend """ - service = self._get_service(volume) - (_ip, _ipp, _ctl, _port, hdp, target, secret) = service + hdp = self._get_service(volume) (arid, lun) = _loc_info(volume['provider_location'])['id_lu'] self.bend.extend_vol(self.config['hnas_cmd'], self.config['mgmt_ip0'], @@ -552,8 +634,7 @@ class HDSISCSIDriver(driver.ISCSIDriver): LOG.debug("delete lun %(lun)s on %(name)s", {'lun': lun, 'name': name}) - service = self._get_service(volume) - (_ip, _ipp, _ctl, _port, hdp, target, secret) = service + hdp = self._get_service(volume) self.bend.delete_lu(self.config['hnas_cmd'], self.config['mgmt_ip0'], self.config['username'], @@ -572,24 +653,23 @@ class HDSISCSIDriver(driver.ISCSIDriver): {'vol': volume, 'conn': connector}) # connector[ip, host, wwnns, unititator, wwp/ - service = self._get_service(volume) - (ip, ipp, ctl, port, _hdp, target, secret) = service + + service_info = self._get_service_target(volume) + (ip, ipp, ctl, port, _hdp, tgtalias, secret) = service_info info = _loc_info(volume['provider_location']) if 'tgt' in info.keys(): # spurious repeat connection # print info.keys() LOG.debug("initiate_conn: tgt already set %s", info['tgt']) - (arid, lun) = info['id_lu'] - loc = arid + '.' + lun + (arid, lun_name) = info['id_lu'] + loc = arid + '.' + lun_name # sps, use target if provided - iqn = target - try: out = self.bend.add_iscsi_conn(self.config['hnas_cmd'], self.config['mgmt_ip0'], self.config['username'], self.config['password'], - lun, _hdp, port, iqn, + lun_name, _hdp, port, tgtalias, connector['initiator']) except processutils.ProcessExecutionError: msg = _("Error attaching volume %s. " @@ -600,7 +680,7 @@ class HDSISCSIDriver(driver.ISCSIDriver): # sps need hlun, fulliqn hlun = out.split()[1] fulliqn = out.split()[13] - tgt = hnas_portal + ',' + iqn + ',' + loc + ',' + ctl + ',' + tgt = hnas_portal + ',' + tgtalias + ',' + loc + ',' + ctl + ',' tgt += port + ',' + hlun LOG.info(_LI("initiate: connection %s"), tgt) @@ -619,7 +699,9 @@ class HDSISCSIDriver(driver.ISCSIDriver): properties['auth_method'] = 'CHAP' properties['auth_password'] = secret - return {'driver_volume_type': 'iscsi', 'data': properties} + conn_info = {'driver_volume_type': 'iscsi', 'data': properties} + LOG.debug("initialize_connection: conn_info: %s.", conn_info) + return conn_info @cinder_utils.synchronized('volume_mapping') def terminate_connection(self, volume, connector, **kwargs): @@ -634,13 +716,13 @@ class HDSISCSIDriver(driver.ISCSIDriver): LOG.warning(_LW("terminate_conn: provider location empty.")) return (arid, lun) = info['id_lu'] - (_portal, iqn, loc, ctl, port, hlun) = info['tgt'] + (_portal, tgtalias, loc, ctl, port, hlun) = info['tgt'] LOG.info(_LI("terminate: connection %s"), volume['provider_location']) self.bend.del_iscsi_conn(self.config['hnas_cmd'], self.config['mgmt_ip0'], self.config['username'], self.config['password'], - ctl, iqn, hlun) + ctl, tgtalias, hlun) self._update_vol_location(volume['id'], loc) return {'provider_location': loc} @@ -654,8 +736,7 @@ class HDSISCSIDriver(driver.ISCSIDriver): size = int(snapshot['volume_size']) * units.Ki (arid, slun) = _loc_info(snapshot['provider_location'])['id_lu'] - service = self._get_service(volume) - (_ip, _ipp, _ctl, _port, hdp, target, secret) = service + hdp = self._get_service(volume) out = self.bend.create_dup(self.config['hnas_cmd'], self.config['mgmt_ip0'], self.config['username'], @@ -676,8 +757,7 @@ class HDSISCSIDriver(driver.ISCSIDriver): """ source_vol = self._id_to_vol(snapshot['volume_id']) - service = self._get_service(source_vol) - (_ip, _ipp, _ctl, _port, hdp, target, secret) = service + hdp = self._get_service(source_vol) size = int(snapshot['volume_size']) * units.Ki (arid, slun) = _loc_info(source_vol['provider_location'])['id_lu'] out = self.bend.create_dup(self.config['hnas_cmd'], @@ -709,8 +789,7 @@ class HDSISCSIDriver(driver.ISCSIDriver): (arid, lun) = loc.split('.') source_vol = self._id_to_vol(snapshot['volume_id']) - service = self._get_service(source_vol) - (_ip, _ipp, _ctl, _port, hdp, target, secret) = service + hdp = self._get_service(source_vol) myid = self.arid if arid != myid: