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)