From a6e789f27edc5cae84a786f3d3604cc6546820d7 Mon Sep 17 00:00:00 2001 From: Gorka Eguileor Date: Sun, 12 Feb 2017 01:58:46 +0100 Subject: [PATCH] Fix iSCSI multipath rescan iSCSI multipath rescan uses iscsiadm --rescan option for nodes and sessions, which can end up recreating devices that had just been removed if there's a race condition between the removal of a SCSI device and the connection of a volume. The race condition happens if a rescan done when attaching happens right between us removing the path and removing the exported lun, because the rescan will add not only the new path we are attaching, but the old path we are removing, since the lun still hasn't been removed. This would leave orphaned devices that unnecessarily pollute our environment, This patch narrows the rescan to only rescan for the specific target id, channel, and lun number if we can find this information. When we cannot find this information we do the scan as we were doing it before. Closes-Bug: #1664032 Change-Id: I1b3bd34db260165a6ea9ca061f946d6dfcf8553f --- os_brick/exception.py | 8 + os_brick/initiator/connectors/iscsi.py | 120 ++++++++++-- .../tests/initiator/connectors/test_iscsi.py | 179 +++++++++++++++--- 3 files changed, 259 insertions(+), 48 deletions(-) diff --git a/os_brick/exception.py b/os_brick/exception.py index 98381ea60..3fa11bbd3 100644 --- a/os_brick/exception.py +++ b/os_brick/exception.py @@ -159,3 +159,11 @@ class VolumeEncryptionNotSupported(Invalid): # NOTE(mriedem): This extends ValueError to maintain backward compatibility. class InvalidConnectorProtocol(ValueError): pass + + +class HostChannelsTargetsNotFound(BrickException): + message = _('Unable to find host, channel, and target for %(iqns)s.') + + def __init__(self, message=None, iqns=None, found=None): + super(HostChannelsTargetsNotFound, self).__init__(message, iqns=iqns) + self.found = found diff --git a/os_brick/initiator/connectors/iscsi.py b/os_brick/initiator/connectors/iscsi.py index 9f2c0d606..b923b7b46 100644 --- a/os_brick/initiator/connectors/iscsi.py +++ b/os_brick/initiator/connectors/iscsi.py @@ -13,6 +13,7 @@ # under the License. +import collections import copy import glob import os @@ -165,7 +166,8 @@ class ISCSIConnector(base.BaseLinuxConnector, base_iscsi.BaseISCSIConnector): LOG.info(_LI("Multipath discovery for iSCSI enabled")) # Multipath installed, discovering other targets if available try: - ips_iqns = self._discover_iscsi_portals(connection_properties) + ips_iqns_luns = self._discover_iscsi_portals( + connection_properties) except Exception: if 'target_portals' in connection_properties: raise exception.TargetPortalsNotFound( @@ -186,13 +188,14 @@ class ISCSIConnector(base.BaseLinuxConnector, base_iscsi.BaseISCSIConnector): # latter, so try the ip,iqn combinations to find the targets # which constitutes the multipath device. main_iqn = connection_properties['target_iqn'] - all_portals = set([ip for ip, iqn in ips_iqns]) - match_portals = set([ip for ip, iqn in ips_iqns - if iqn == main_iqn]) + all_portals = {(ip, lun) for ip, iqn, lun in ips_iqns_luns} + match_portals = {(ip, lun) for ip, iqn, lun in ips_iqns_luns + if iqn == main_iqn} if len(all_portals) == len(match_portals): - ips_iqns = zip(all_portals, [main_iqn] * len(all_portals)) + ips_iqns_luns = [(p[0], main_iqn, p[1]) + for p in all_portals] - for ip, iqn in ips_iqns: + for ip, iqn, lun in ips_iqns_luns: props = copy.deepcopy(connection_properties) props['target_portal'] = ip props['target_iqn'] = iqn @@ -201,7 +204,8 @@ class ISCSIConnector(base.BaseLinuxConnector, base_iscsi.BaseISCSIConnector): connected_to_portal = True if use_rescan: - self._rescan_iscsi() + self._rescan_iscsi(ips_iqns_luns) + host_devices = self._get_device_path(connection_properties) else: LOG.info(_LI("Multipath discovery for iSCSI not enabled.")) @@ -283,12 +287,19 @@ class ISCSIConnector(base.BaseLinuxConnector, base_iscsi.BaseISCSIConnector): def _get_transport(self): return self.transport + @staticmethod + def _get_luns(con_props, iqns=None): + luns = con_props.get('target_luns') + num_luns = len(con_props['target_iqns']) if iqns is None else len(iqns) + return luns or [con_props.get('target_lun')] * num_luns + def _discover_iscsi_portals(self, connection_properties): if all([key in connection_properties for key in ('target_portals', 'target_iqns')]): # Use targets specified by connection_properties - return zip(connection_properties['target_portals'], - connection_properties['target_iqns']) + return list(zip(connection_properties['target_portals'], + connection_properties['target_iqns'], + self._get_luns(connection_properties))) out = None iscsi_transport = ('iser' if self._get_transport() == 'iser' @@ -332,7 +343,9 @@ class ISCSIConnector(base.BaseLinuxConnector, base_iscsi.BaseISCSIConnector): '-p', connection_properties['target_portal']], check_exit_code=[0, 255])[0] or "" - return self._get_target_portals_from_iscsiadm_output(out) + ips, iqns = self._get_target_portals_from_iscsiadm_output(out) + luns = self._get_luns(connection_properties, iqns) + return list(zip(ips, iqns, luns)) def _run_iscsiadm_update_discoverydb(self, connection_properties, iscsi_transport='default'): @@ -607,16 +620,18 @@ class ISCSIConnector(base.BaseLinuxConnector, base_iscsi.BaseISCSIConnector): **kwargs) def _get_target_portals_from_iscsiadm_output(self, output): - # return both portals and iqns + # return both portals and iqns as 2 lists # # as we are parsing a command line utility, allow for the # possibility that additional debug data is spewed in the # stream, and only grab actual ip / iqn lines. - targets = [] + ips = [] + iqns = [] for data in [line.split() for line in output.splitlines()]: if len(data) == 2 and data[1].startswith('iqn.'): - targets.append(data) - return targets + ips.append(data[0]) + iqns.append(data[1]) + return ips, iqns def _disconnect_volume_multipath_iscsi(self, connection_properties, multipath_name): @@ -637,14 +652,14 @@ class ISCSIConnector(base.BaseLinuxConnector, base_iscsi.BaseISCSIConnector): # Do a discovery to find all targets. # Targets for multiple paths for the same multipath device # may not be the same. - all_ips_iqns = self._discover_iscsi_portals(connection_properties) + all_ips_iqns_luns = self._discover_iscsi_portals(connection_properties) # As discovery result may contain other targets' iqns, extract targets # to be disconnected whose block devices are already deleted here. ips_iqns = [] entries = [device.lstrip('ip-').split('-lun-')[0] for device in self._get_iscsi_devices()] - for ip, iqn in all_ips_iqns: + for ip, iqn, lun in all_ips_iqns_luns: ip_iqn = "%s-iscsi-%s" % (ip.split(",")[0], iqn) if ip_iqn not in entries: ips_iqns.append([ip, iqn]) @@ -837,8 +852,71 @@ class ISCSIConnector(base.BaseLinuxConnector, base_iscsi.BaseISCSIConnector): 'out': out, 'err': err}) return (out, err) - def _rescan_iscsi(self): - self._run_iscsiadm_bare(('-m', 'node', '--rescan'), - check_exit_code=[0, 1, 21, 255]) - self._run_iscsiadm_bare(('-m', 'session', '--rescan'), - check_exit_code=[0, 1, 21, 255]) + @utils.retry(exception.HostChannelsTargetsNotFound, backoff_rate=1.5) + def _get_hosts_channels_targets_luns(self, ips_iqns_luns): + iqns = {iqn: lun for ip, iqn, lun in ips_iqns_luns} + LOG.debug('Getting hosts, channels, and targets for iqns: %s', + iqns.keys()) + + # Get all targets indexed by scsi host path + targets_paths = glob.glob('/sys/class/scsi_host/host*/device/session*/' + 'target*') + targets = collections.defaultdict(list) + for path in targets_paths: + target = path.split('/target')[1] + host = path.split('/device/')[0] + targets[host].append(target.split(':')) + + # Get all scsi targets + sessions = glob.glob('/sys/class/scsi_host/host*/device/session*/' + 'iscsi_session/session*/targetname') + + result = [] + for session in sessions: + # Read iSCSI target name + try: + with open(session, 'r') as f: + targetname = f.read().strip('\n') + except Exception: + continue + + # If we are interested in it we store its target information + if targetname in iqns: + host = session.split('/device/')[0] + for __, channel, target_id in targets[host]: + result.append((host, channel, target_id, iqns[targetname])) + # Stop as soon as we have the info of all our iqns, even if + # there are more sessions to check + del iqns[targetname] + if not iqns: + break + + # In some cases the login and udev triggers may not have been fast + # enough to create all sysfs entries, so we want to retry. + else: + raise exception.HostChannelsTargetsNotFound(iqns=iqns.keys(), + found=result) + return result + + def _rescan_iscsi(self, ips_iqns_luns): + try: + hctls = self._get_hosts_channels_targets_luns(ips_iqns_luns) + except exception.HostChannelsTargetsNotFound as e: + if not e.found: + LOG.error(_LE('iSCSI scan failed: %s'), e) + return + + hctls = e.found + LOG.warning(_LW('iSCSI scan: %(error)s\nScanning %(hosts)s'), + {'error': e, 'hosts': [h for h, c, t, l in hctls]}) + + for host_path, channel, target_id, target_lun in hctls: + LOG.debug('Scanning host %(host)s c: %(channel)s, ' + 't: %(target)s, l: %(lun)s)', + {'host': host_path, 'channel': channel, + 'target': target_id, 'lun': target_lun}) + self._linuxscsi.echo_scsi_command( + "%s/scan" % host_path, + "%(c)s %(t)s %(l)s" % {'c': channel, + 't': target_id, + 'l': target_lun}) diff --git a/os_brick/tests/initiator/connectors/test_iscsi.py b/os_brick/tests/initiator/connectors/test_iscsi.py index 2ca448faf..09a3a71c2 100644 --- a/os_brick/tests/initiator/connectors/test_iscsi.py +++ b/os_brick/tests/initiator/connectors/test_iscsi.py @@ -426,7 +426,7 @@ class ISCSIConnectorTestCase(test_connector.ConnectorTestCase): self.connector_with_multipath = \ iscsi.ISCSIConnector(None, use_multipath=True) iscsiadm_mock.return_value = "%s %s" % (location, iqn) - portals_mock.return_value = [[location, iqn]] + portals_mock.return_value = ([location], [iqn]) result = self.connector_with_multipath.connect_volume( connection_properties['data']) @@ -525,6 +525,8 @@ class ISCSIConnectorTestCase(test_connector.ConnectorTestCase): self.connector_with_multipath.connect_volume, connection_properties['data']) + @mock.patch.object(iscsi.ISCSIConnector, + '_get_hosts_channels_targets_luns', return_value=[]) @mock.patch.object(linuxscsi.LinuxSCSI, 'get_scsi_wwn') @mock.patch.object(os.path, 'exists', return_value=True) @mock.patch.object(host_driver.HostDriver, 'get_all_block_devices') @@ -538,7 +540,8 @@ class ISCSIConnectorTestCase(test_connector.ConnectorTestCase): def test_connect_volume_with_multiple_portals( self, mock_process_lun_id, mock_discover_mpath_device, mock_get_iqn, mock_run_multipath, mock_iscsi_devices, - mock_get_device_map, mock_devices, mock_exists, mock_scsi_wwn): + mock_get_device_map, mock_devices, mock_exists, mock_scsi_wwn, + mock_get_htcls): mock_scsi_wwn.return_value = test_connector.FAKE_SCSI_WWN location1 = '10.0.2.15:3260' location2 = '[2001:db8::1]:3260' @@ -547,10 +550,12 @@ class ISCSIConnectorTestCase(test_connector.ConnectorTestCase): name2 = 'volume-00000001-2' iqn1 = 'iqn.2010-10.org.openstack:%s' % name1 iqn2 = 'iqn.2010-10.org.openstack:%s' % name2 + lun1 = 1 + lun2 = 2 fake_multipath_dev = '/dev/mapper/fake-multipath-dev' vol = {'id': 1, 'name': name1} connection_properties = self.iscsi_connection_multipath( - vol, [location1, location2], [iqn1, iqn2], [1, 2]) + vol, [location1, location2], [iqn1, iqn2], [lun1, lun2]) devs = ['/dev/disk/by-path/ip-%s-iscsi-%s-lun-1' % (location1, iqn1), '/dev/disk/by-path/ip-%s-iscsi-%s-lun-2' % (dev_loc2, iqn2)] mock_devices.return_value = devs @@ -558,7 +563,7 @@ class ISCSIConnectorTestCase(test_connector.ConnectorTestCase): mock_get_iqn.return_value = [iqn1, iqn2] mock_discover_mpath_device.return_value = ( fake_multipath_dev, test_connector.FAKE_SCSI_WWN) - mock_process_lun_id.return_value = [1, 2] + mock_process_lun_id.return_value = [lun1, lun2] result = self.connector_with_multipath.connect_volume( connection_properties['data']) @@ -580,6 +585,11 @@ class ISCSIConnectorTestCase(test_connector.ConnectorTestCase): for command in expected_commands: self.assertIn(command, self.cmds) + mock_get_htcls.assert_called_once_with([(location1, iqn1, lun1), + (location2, iqn2, lun2)]) + + @mock.patch.object(iscsi.ISCSIConnector, + '_get_hosts_channels_targets_luns', return_value=[]) @mock.patch.object(linuxscsi.LinuxSCSI, 'get_scsi_wwn') @mock.patch.object(os.path, 'exists') @mock.patch.object(host_driver.HostDriver, 'get_all_block_devices') @@ -595,8 +605,7 @@ class ISCSIConnectorTestCase(test_connector.ConnectorTestCase): self, mock_process_lun_id, mock_discover_mpath_device, mock_iscsiadm, mock_get_iqn, mock_run_multipath, mock_iscsi_devices, mock_get_multipath_device_map, - mock_devices, mock_exists, - mock_scsi_wwn): + mock_devices, mock_exists, mock_scsi_wwn, mock_get_htcls): mock_scsi_wwn.return_value = test_connector.FAKE_SCSI_WWN location1 = '10.0.2.15:3260' location2 = '[2001:db8::1]:3260' @@ -659,6 +668,12 @@ class ISCSIConnectorTestCase(test_connector.ConnectorTestCase): mock_iscsiadm.assert_any_call(props, ('--logout',), check_exit_code=[0, 21, 255]) + lun1, lun2 = connection_properties['data']['target_luns'] + mock_get_htcls.assert_called_once_with([(location1, iqn1, lun1), + (location2, iqn2, lun2)]) + + @mock.patch.object(iscsi.ISCSIConnector, + '_get_hosts_channels_targets_luns', return_value=[]) @mock.patch.object(linuxscsi.LinuxSCSI, 'get_scsi_wwn') @mock.patch.object(os.path, 'exists', return_value=True) @mock.patch.object(iscsi.ISCSIConnector, @@ -671,7 +686,8 @@ class ISCSIConnectorTestCase(test_connector.ConnectorTestCase): def test_connect_volume_with_multipath_connecting( self, mock_discover_mpath_device, mock_run_multipath, mock_iscsi_devices, mock_devices, - mock_connect, mock_portals, mock_exists, mock_scsi_wwn): + mock_connect, mock_portals, mock_exists, mock_scsi_wwn, + mock_get_htcls): mock_scsi_wwn.return_value = test_connector.FAKE_SCSI_WWN location1 = '10.0.2.15:3260' location2 = '[2001:db8::1]:3260' @@ -687,8 +703,8 @@ class ISCSIConnectorTestCase(test_connector.ConnectorTestCase): '/dev/disk/by-path/ip-%s-iscsi-%s-lun-2' % (dev_loc2, iqn2)] mock_devices.return_value = devs mock_iscsi_devices.return_value = devs - mock_portals.return_value = [[location1, iqn1], [location2, iqn1], - [location2, iqn2]] + mock_portals.return_value = ([location1, location2, location2], + [iqn1, iqn1, iqn2]) mock_discover_mpath_device.return_value = ( fake_multipath_dev, test_connector.FAKE_SCSI_WWN) @@ -704,8 +720,16 @@ class ISCSIConnectorTestCase(test_connector.ConnectorTestCase): props2['target_portal'] = locations[1] expected_calls = [mock.call(props1), mock.call(props2)] self.assertEqual(expected_result, result) + mock_connect.assert_has_calls(expected_calls, any_order=True) self.assertEqual(expected_calls, mock_connect.call_args_list) + lun = connection_properties['data']['target_lun'] + self.assertEqual(1, mock_get_htcls.call_count) + # Order of elements in the list is randomized because it comes from + # a set. + self.assertSetEqual({(location1, iqn1, lun), (location2, iqn1, lun)}, + set(mock_get_htcls.call_args[0][0])) + @mock.patch('retrying.time.sleep', mock.Mock()) @mock.patch.object(os.path, 'exists', return_value=True) @mock.patch.object(iscsi.ISCSIConnector, '_get_target_portals_from_iscsiadm_output') @@ -729,8 +753,8 @@ class ISCSIConnectorTestCase(test_connector.ConnectorTestCase): '/dev/disk/by-path/ip-%s-iscsi-%s-lun-2' % (location2, iqn2)] mock_devices.return_value = devs mock_iscsi_devices.return_value = devs - mock_portals.return_value = [[location1, iqn1], [location2, iqn1], - [location2, iqn2]] + mock_portals.return_value = ([location1, location2, location2], + [iqn1, iqn1, iqn2]) mock_connect.return_value = False self.assertRaises(exception.FailedISCSITargetPortalLogin, @@ -768,9 +792,10 @@ class ISCSIConnectorTestCase(test_connector.ConnectorTestCase): test_output = '''10.15.84.19:3260 iqn.1992-08.com.netapp:sn.33615311 10.15.85.19:3260 iqn.1992-08.com.netapp:sn.33615311''' res = connector._get_target_portals_from_iscsiadm_output(test_output) - ip_iqn1 = ['10.15.84.19:3260', 'iqn.1992-08.com.netapp:sn.33615311'] - ip_iqn2 = ['10.15.85.19:3260', 'iqn.1992-08.com.netapp:sn.33615311'] - expected = [ip_iqn1, ip_iqn2] + ips = ['10.15.84.19:3260', '10.15.85.19:3260'] + iqns = ['iqn.1992-08.com.netapp:sn.33615311', + 'iqn.1992-08.com.netapp:sn.33615311'] + expected = (ips, iqns) self.assertEqual(expected, res) @mock.patch.object(os, 'walk') @@ -832,7 +857,7 @@ class ISCSIConnectorTestCase(test_connector.ConnectorTestCase): portal = '10.0.0.1:3260' dev = ('ip-%s-iscsi-%s-lun-0' % (portal, iqn1)) - get_portals_mock.return_value = [[portal, iqn1]] + get_portals_mock.return_value = ([portal], [iqn1]) multipath_iqn_mock.return_value = iqns get_all_devices_mock.return_value = [dev, '/dev/mapper/md-1'] get_multipath_device_map_mock.return_value = {dev: '/dev/mapper/md-3'} @@ -865,7 +890,7 @@ class ISCSIConnectorTestCase(test_connector.ConnectorTestCase): # Multiple targets are discovered, but only block devices for target-1 # is deleted and target-2 is in use. - get_portals_mock.return_value = [[portal, iqn1], [portal, iqn2]] + get_portals_mock.return_value = ([portal, portal], [iqn1, iqn2]) multipath_iqn_mock.return_value = [iqn2, iqn2] get_all_devices_mock.return_value = [dev2, '/dev/mapper/md-1'] get_multipath_map_mock.return_value = {dev2: '/dev/mapper/md-3'} @@ -897,7 +922,7 @@ class ISCSIConnectorTestCase(test_connector.ConnectorTestCase): name = 'volume-00000001' iqn = 'iqn.2010-10.org.openstack:%s' % name - get_portals_mock.return_value = [[portal, iqn]] + get_portals_mock.return_value = ([portal], [iqn]) fake_property = {'target_portal': portal, 'target_iqn': iqn} self.connector._disconnect_volume_multipath_iscsi(fake_property, @@ -925,7 +950,7 @@ class ISCSIConnectorTestCase(test_connector.ConnectorTestCase): iqn = 'iqn.2010-10.org.openstack:%s' % name dev = ('ip-%s-iscsi-%s-lun-0' % (portal, iqn)) - get_portals_mock.return_value = [[portal, iqn]] + get_portals_mock.return_value = ([portal], [iqn]) get_all_devices_mock.return_value = [dev, '/dev/mapper/md-1'] get_iscsi_devices_mock.return_value = [] @@ -939,13 +964,11 @@ class ISCSIConnectorTestCase(test_connector.ConnectorTestCase): def test_iscsiadm_discover_parsing(self): # Ensure that parsing iscsiadm discover ignores cruft. - targets = [ - ["192.168.204.82:3260,1", - ("iqn.2010-10.org.openstack:volume-" - "f9b12623-6ce3-4dac-a71f-09ad4249bdd3")], - ["192.168.204.82:3261,1", - ("iqn.2010-10.org.openstack:volume-" - "f9b12623-6ce3-4dac-a71f-09ad4249bdd4")]] + ips = ["192.168.204.82:3260,1", "192.168.204.82:3261,1"] + iqns = ["iqn.2010-10.org.openstack:volume-" + "f9b12623-6ce3-4dac-a71f-09ad4249bdd3", + "iqn.2010-10.org.openstack:volume-" + "f9b12623-6ce3-4dac-a71f-09ad4249bdd4"] # This slight wonkiness brought to you by pep8, as the actual # example output runs about 97 chars wide. @@ -954,10 +977,10 @@ Starting iSCSI initiator service: done Setting up iSCSI targets: unused %s %s %s %s -""" % (targets[0][0], targets[0][1], targets[1][0], targets[1][1]) +""" % (ips[0], iqns[0], ips[1], iqns[1]) out = self.connector.\ _get_target_portals_from_iscsiadm_output(sample_input) - self.assertEqual(out, targets) + self.assertEqual((ips, iqns), out) def test_sanitize_log_run_iscsiadm(self): # Tests that the parameters to the _run_iscsiadm function @@ -1037,3 +1060,105 @@ Setting up iSCSI targets: unused self.assertRaises(exception.TargetPortalsNotFound, self.connector._get_potential_volume_paths, connection_properties) + + @mock.patch.object(iscsi.ISCSIConnector, + '_get_hosts_channels_targets_luns') + def test_rescan_iscsi_no_hctls(self, mock_get_htcls): + mock_get_htcls.side_effect = exception.HostChannelsTargetsNotFound( + iqns=['iqn1', 'iqn2'], found=[]) + with mock.patch.object(self.connector, '_linuxscsi') as mock_linuxscsi: + self.connector._rescan_iscsi(mock.sentinel.input) + mock_linuxscsi.echo_scsi_command.assert_not_called() + mock_get_htcls.assert_called_once_with(mock.sentinel.input) + + @mock.patch.object(iscsi.ISCSIConnector, + '_get_hosts_channels_targets_luns') + def test_rescan_iscsi_partial_hctls(self, mock_get_htcls): + mock_get_htcls.side_effect = exception.HostChannelsTargetsNotFound( + iqns=['iqn1'], found=[('h', 'c', 't', 'l')]) + with mock.patch.object(self.connector, '_linuxscsi') as mock_linuxscsi: + self.connector._rescan_iscsi(mock.sentinel.input) + mock_linuxscsi.echo_scsi_command.assert_called_once_with( + 'h/scan', 'c t l') + mock_get_htcls.assert_called_once_with(mock.sentinel.input) + + @mock.patch.object(iscsi.ISCSIConnector, + '_get_hosts_channels_targets_luns') + @mock.patch.object(iscsi.ISCSIConnector, '_run_iscsiadm_bare') + def test_rescan_iscsi_hctls(self, mock_iscsiadm, mock_get_htcls): + mock_get_htcls.return_value = [ + ('/sys/class/iscsi_host/host4', '0', '0', '1'), + ('/sys/class/iscsi_host/host5', '0', '0', '2'), + ] + + with mock.patch.object(self.connector, '_linuxscsi') as mock_linuxscsi: + self.connector._rescan_iscsi(mock.sentinel.input) + mock_linuxscsi.echo_scsi_command.assert_has_calls(( + mock.call('/sys/class/iscsi_host/host4/scan', '0 0 1'), + mock.call('/sys/class/iscsi_host/host5/scan', '0 0 2'), + )) + mock_get_htcls.assert_called_once_with(mock.sentinel.input) + mock_iscsiadm.assert_not_called() + + @mock.patch('six.moves.builtins.open', create=True) + @mock.patch('glob.glob') + def test_get_hctls(self, mock_glob, mock_open): + host4 = '/sys/class/scsi_host/host4' + host5 = '/sys/class/scsi_host/host5' + host6 = '/sys/class/scsi_host/host6' + host7 = '/sys/class/scsi_host/host7' + + mock_glob.side_effect = ( + (host4 + '/device/session5/target0:1:2', + host5 + '/device/session6/target3:4:5', + host6 + '/device/session7/target6:7:8', + host7 + '/device/session8/target9:10:11'), + (host4 + '/device/session5/iscsi_session/session5/targetname', + host5 + '/device/session6/iscsi_session/session6/targetname', + host6 + '/device/session7/iscsi_session/session7/targetname', + host7 + '/device/session8/iscsi_session/session8/targetname'), + ) + + mock_open.side_effect = ( + mock.mock_open(read_data='iqn0\n').return_value, + mock.mock_open(read_data='iqn1\n').return_value, + mock.mock_open(read_data='iqn2\n').return_value, + mock.mock_open(read_data='iqn3\n').return_value, + ) + + ips_iqns_luns = [('ip1', 'iqn1', 'lun1'), ('ip2', 'iqn2', 'lun2')] + result = self.connector._get_hosts_channels_targets_luns(ips_iqns_luns) + self.assertEqual( + [(host5, '4', '5', 'lun1'), (host6, '7', '8', 'lun2')], + result) + mock_glob.assert_has_calls(( + mock.call('/sys/class/scsi_host/host*/device/session*/target*'), + mock.call('/sys/class/scsi_host/host*/device/session*/' + 'iscsi_session/session*/targetname'), + )) + self.assertEqual(3, mock_open.call_count) + + @mock.patch('retrying.time.sleep', mock.Mock()) + @mock.patch('six.moves.builtins.open', create=True) + @mock.patch('glob.glob', return_value=[]) + def test_get_hctls_not_found(self, mock_glob, mock_open): + host4 = '/sys/class/scsi_host/host4' + mock_glob.side_effect = [ + [(host4 + '/device/session5/target0:1:2')], + [(host4 + '/device/session5/iscsi_session/session5/targetname')], + ] * 3 + # Test exception on open as well as having only half of the htcls + mock_open.side_effect = [ + mock.Mock(side_effect=Exception()), + mock.mock_open(read_data='iqn1\n').return_value, + mock.mock_open(read_data='iqn1\n').return_value, + ] + + ips_iqns_luns = [('ip1', 'iqn1', 'lun1'), ('ip2', 'iqn2', 'lun2')] + + exc = self.assertRaises( + exception.HostChannelsTargetsNotFound, + self.connector._get_hosts_channels_targets_luns, ips_iqns_luns) + + # Verify exception contains found results + self.assertEqual([(host4, '1', '2', 'lun1')], exc.found)