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
This commit is contained in:
Gorka Eguileor 2017-02-12 01:58:46 +01:00
parent 32c837dacb
commit a6e789f27e
3 changed files with 259 additions and 48 deletions

View File

@ -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

View File

@ -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})

View File

@ -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)