Merge "Failover to alternative iSCSI portals on login failure"

This commit is contained in:
Jenkins 2015-02-23 16:44:46 +00:00 committed by Gerrit Code Review
commit 312386616c
2 changed files with 107 additions and 16 deletions

View File

@ -232,6 +232,11 @@ class ISCSIConnector(InitiatorConnector):
props['target_lun'] = lun
yield props
def _alternative_targets(self, connection_properties):
return zip(connection_properties.get('target_alternative_portals', []),
connection_properties.get('target_alternative_iqns', []),
connection_properties.get('target_alternative_luns', []))
def _multipath_targets(self, connection_properties):
return zip(connection_properties.get('target_portals', []),
connection_properties.get('target_iqns', []),
@ -280,8 +285,19 @@ class ISCSIConnector(InitiatorConnector):
self._rescan_iscsi()
host_devices = self._get_device_path(connection_properties)
else:
self._connect_to_iscsi_portal(connection_properties)
host_devices = self._get_device_path(connection_properties)
target_props = connection_properties
if not self._connect_to_iscsi_portal(target_props):
for props in self._iterate_multiple_targets(
connection_properties,
self._alternative_targets(connection_properties)):
if self._connect_to_iscsi_portal(props):
target_props = props
break
else:
LOG.warn(_LW(
'Failed to login to any of the iSCSI targets.'))
host_devices = self._get_device_path(target_props)
# The /dev/disk/by-path/... node is not always present immediately
# TODO(justinsb): This retry-with-delay is a pattern, move to utils?
@ -300,7 +316,7 @@ class ISCSIConnector(InitiatorConnector):
if self.use_multipath:
self._rescan_iscsi()
else:
self._run_iscsiadm(connection_properties, ("--rescan",))
self._run_iscsiadm(target_props, ("--rescan",))
tries = tries + 1
if all(map(lambda x: not os.path.exists(x), host_devices)):
@ -367,9 +383,10 @@ class ISCSIConnector(InitiatorConnector):
# unused devices created by logging into other LUNs' session.
ips_iqns_luns = self._multipath_targets(connection_properties)
if not ips_iqns_luns:
ips_iqns_luns = [[connection_properties['target_portal'],
ips_iqns_luns = ([[connection_properties['target_portal'],
connection_properties['target_iqn'],
connection_properties.get('target_lun', 0)]]
connection_properties.get('target_lun', 0)]] +
self._alternative_targets(connection_properties))
for props in self._iterate_multiple_targets(connection_properties,
ips_iqns_luns):
self._disconnect_volume_iscsi(props)
@ -543,17 +560,20 @@ class ISCSIConnector(InitiatorConnector):
("--login",),
check_exit_code=[0, 255])
except putils.ProcessExecutionError as err:
#as this might be one of many paths,
#only set successful logins to startup automatically
if err.exit_code in [15]:
self._iscsiadm_update(connection_properties,
"node.startup",
"automatic")
return
# exit_code=15 means the session already exists, so it should
# be regarded as successful login.
if err.exit_code not in [15]:
LOG.warn(_LW('Failed to login iSCSI target %(iqn)s '
'on portal %(portal)s (exit code %(err)s).'),
{'iqn': connection_properties['target_iqn'],
'portal': connection_properties['target_portal'],
'err': err.exit_code})
return False
self._iscsiadm_update(connection_properties,
"node.startup",
"automatic")
return True
def _disconnect_from_iscsi_portal(self, connection_properties):
self._iscsiadm_update(connection_properties, "node.startup", "manual",

View File

@ -230,9 +230,7 @@ class ISCSIConnectorTestCase(ConnectorTestCase):
initiator = self.connector.get_initiator()
self.assertEqual(initiator, 'iqn.1234-56.foo.bar:01:23456789abc')
@testtools.skipUnless(os.path.exists('/dev/disk/by-path'),
'Test requires /dev/disk/by-path')
def test_connect_volume(self):
def _test_connect_volume(self, extra_props, additional_commands):
exists_mock = mock.Mock()
exists_mock.return_value = True
os.path.exists = exists_mock
@ -241,6 +239,8 @@ class ISCSIConnectorTestCase(ConnectorTestCase):
iqn = 'iqn.2010-10.org.openstack:%s' % name
vol = {'id': 1, 'name': name}
connection_info = self.iscsi_connection(vol, location, iqn)
for key, value in extra_props.iteritems():
connection_info['data'][key] = value
device = self.connector.connect_volume(connection_info['data'])
dev_str = '/dev/disk/by-path/ip-%s-iscsi-%s-lun-1' % (location, iqn)
self.assertEqual(device['type'], 'block')
@ -264,12 +264,83 @@ class ISCSIConnectorTestCase(ConnectorTestCase):
('iscsiadm -m node -T %s -p %s --logout' %
(iqn, location)),
('iscsiadm -m node -T %s -p %s --op delete' %
(iqn, location)), ]
(iqn, location)), ] + additional_commands
LOG.debug("self.cmds = %s" % self.cmds)
LOG.debug("expected = %s" % expected_commands)
self.assertEqual(expected_commands, self.cmds)
@testtools.skipUnless(os.path.exists('/dev/disk/by-path'),
'Test requires /dev/disk/by-path')
def test_connect_volume(self):
self._test_connect_volume({}, [])
@testtools.skipUnless(os.path.exists('/dev/disk/by-path'),
'Test requires /dev/disk/by-path')
def test_connect_volume_with_alternative_targets(self):
location2 = '10.0.3.15:3260'
iqn2 = 'iqn.2010-10.org.openstack:volume-00000001-2'
extra_props = {'target_alternative_portals': [location2],
'target_alternative_iqns': [iqn2],
'target_alternative_luns': [2]}
additional_commands = [('blockdev --flushbufs /dev/sdb'),
('tee -a /sys/block/sdb/device/delete'),
('iscsiadm -m node -T %s -p %s --op update'
' -n node.startup -v manual' %
(iqn2, location2)),
('iscsiadm -m node -T %s -p %s --logout' %
(iqn2, location2)),
('iscsiadm -m node -T %s -p %s --op delete' %
(iqn2, location2))]
self._test_connect_volume(extra_props, additional_commands)
@testtools.skipUnless(os.path.exists('/dev/disk/by-path'),
'Test requires /dev/disk/by-path')
@mock.patch.object(os.path, 'exists')
@mock.patch.object(connector.ISCSIConnector, '_run_iscsiadm')
def test_connect_volume_with_alternative_targets_primary_error(
self, mock_iscsiadm, mock_exists):
location = '10.0.2.15:3260'
location2 = '10.0.3.15:3260'
name = 'volume-00000001'
iqn = 'iqn.2010-10.org.openstack:%s' % name
iqn2 = 'iqn.2010-10.org.openstack:%s-2' % name
vol = {'id': 1, 'name': name}
connection_info = self.iscsi_connection(vol, location, iqn)
connection_info['data']['target_alternative_portals'] = [location2]
connection_info['data']['target_alternative_iqns'] = [iqn2]
connection_info['data']['target_alternative_luns'] = [2]
dev_str2 = '/dev/disk/by-path/ip-%s-iscsi-%s-lun-2' % (location2, iqn2)
def fake_run_iscsiadm(iscsi_properties, iscsi_command, **kwargs):
if iscsi_properties['target_portal'] == location:
if iscsi_command == ('--login',):
raise putils.ProcessExecutionError(None, None, 21)
return mock.DEFAULT
mock_iscsiadm.side_effect = fake_run_iscsiadm
mock_exists.side_effect = lambda x: x == dev_str2
device = self.connector.connect_volume(connection_info['data'])
self.assertEqual('block', device['type'])
self.assertEqual(dev_str2, device['path'])
props = connection_info['data'].copy()
props['target_portal'] = location2
props['target_iqn'] = iqn2
props['target_lun'] = 2
mock_iscsiadm.assert_any_call(props, ('--login',),
check_exit_code=[0, 255])
mock_iscsiadm.reset_mock()
self.connector.disconnect_volume(connection_info['data'], device)
props = connection_info['data'].copy()
mock_iscsiadm.assert_any_call(props, ('--logout',),
check_exit_code=[0, 21, 255])
props['target_portal'] = location2
props['target_iqn'] = iqn2
props['target_lun'] = 2
mock_iscsiadm.assert_any_call(props, ('--logout',),
check_exit_code=[0, 21, 255])
def test_connect_volume_with_multipath(self):
location = '10.0.2.15:3260'
name = 'volume-00000001'