Merge "Failover to alternative iSCSI portals on login failure"
This commit is contained in:
@@ -217,18 +217,27 @@ class ISCSIConnector(InitiatorConnector):
|
||||
super(ISCSIConnector, self).set_execute(execute)
|
||||
self._linuxscsi.set_execute(execute)
|
||||
|
||||
def _iterate_multiple_targets(self, connection_properties, ips_iqns_luns):
|
||||
for ip, iqn, lun in ips_iqns_luns:
|
||||
def _iterate_all_targets(self, connection_properties):
|
||||
for ip, iqn, lun in self._get_all_targets(connection_properties):
|
||||
props = copy.deepcopy(connection_properties)
|
||||
props['target_portal'] = ip
|
||||
props['target_iqn'] = iqn
|
||||
props['target_lun'] = lun
|
||||
for key in ('target_portals', 'target_iqns', 'target_luns'):
|
||||
props.pop(key, None)
|
||||
yield props
|
||||
|
||||
def _multipath_targets(self, connection_properties):
|
||||
return zip(connection_properties.get('target_portals', []),
|
||||
connection_properties.get('target_iqns', []),
|
||||
connection_properties.get('target_luns', []))
|
||||
def _get_all_targets(self, connection_properties):
|
||||
if all([key in connection_properties for key in ('target_portals',
|
||||
'target_iqns',
|
||||
'target_luns')]):
|
||||
return zip(connection_properties['target_portals'],
|
||||
connection_properties['target_iqns'],
|
||||
connection_properties['target_luns'])
|
||||
|
||||
return [(connection_properties['target_portal'],
|
||||
connection_properties['target_iqn'],
|
||||
connection_properties.get('target_lun', 0))]
|
||||
|
||||
def _discover_iscsi_portals(self, connection_properties):
|
||||
if all([key in connection_properties for key in ('target_portals',
|
||||
@@ -273,8 +282,16 @@ 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
|
||||
for props in self._iterate_all_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?
|
||||
@@ -293,7 +310,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)):
|
||||
@@ -358,13 +375,7 @@ class ISCSIConnector(InitiatorConnector):
|
||||
|
||||
# When multiple portals/iqns/luns are specified, we need to remove
|
||||
# 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'],
|
||||
connection_properties['target_iqn'],
|
||||
connection_properties.get('target_lun', 0)]]
|
||||
for props in self._iterate_multiple_targets(connection_properties,
|
||||
ips_iqns_luns):
|
||||
for props in self._iterate_all_targets(connection_properties):
|
||||
self._disconnect_volume_iscsi(props)
|
||||
|
||||
def _disconnect_volume_iscsi(self, connection_properties):
|
||||
@@ -388,16 +399,8 @@ class ISCSIConnector(InitiatorConnector):
|
||||
self._disconnect_from_iscsi_portal(connection_properties)
|
||||
|
||||
def _get_device_path(self, connection_properties):
|
||||
multipath_targets = self._multipath_targets(connection_properties)
|
||||
if multipath_targets:
|
||||
return ["/dev/disk/by-path/ip-%s-iscsi-%s-lun-%s" % x for x in
|
||||
multipath_targets]
|
||||
|
||||
path = ("/dev/disk/by-path/ip-%(portal)s-iscsi-%(iqn)s-lun-%(lun)s" %
|
||||
{'portal': connection_properties['target_portal'],
|
||||
'iqn': connection_properties['target_iqn'],
|
||||
'lun': connection_properties.get('target_lun', 0)})
|
||||
return [path]
|
||||
return ["/dev/disk/by-path/ip-%s-iscsi-%s-lun-%s" % x for x in
|
||||
self._get_all_targets(connection_properties)]
|
||||
|
||||
def get_initiator(self):
|
||||
"""Secure helper to read file as root."""
|
||||
@@ -536,17 +539,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",
|
||||
|
||||
@@ -223,15 +223,15 @@ class ISCSIConnectorTestCase(ConnectorTestCase):
|
||||
initiator = self.connector.get_initiator()
|
||||
self.assertEqual(initiator, 'iqn.1234-56.foo.bar:01:23456789abc')
|
||||
|
||||
@test.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):
|
||||
self.stubs.Set(os.path, 'exists', lambda x: True)
|
||||
location = '10.0.2.15:3260'
|
||||
name = 'volume-00000001'
|
||||
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')
|
||||
@@ -255,12 +255,89 @@ 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)
|
||||
|
||||
@test.testtools.skipUnless(os.path.exists('/dev/disk/by-path'),
|
||||
'Test requires /dev/disk/by-path')
|
||||
def test_connect_volume(self):
|
||||
self._test_connect_volume({}, [])
|
||||
|
||||
@test.testtools.skipUnless(os.path.exists('/dev/disk/by-path'),
|
||||
'Test requires /dev/disk/by-path')
|
||||
def test_connect_volume_with_alternative_targets(self):
|
||||
location = '10.0.2.15:3260'
|
||||
location2 = '10.0.3.15:3260'
|
||||
iqn = 'iqn.2010-10.org.openstack:volume-00000001'
|
||||
iqn2 = 'iqn.2010-10.org.openstack:volume-00000001-2'
|
||||
extra_props = {'target_portals': [location, location2],
|
||||
'target_iqns': [iqn, iqn2],
|
||||
'target_luns': [1, 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)
|
||||
|
||||
@test.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_portals'] = [location, location2]
|
||||
connection_info['data']['target_iqns'] = [iqn, iqn2]
|
||||
connection_info['data']['target_luns'] = [1, 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()
|
||||
for key in ('target_portals', 'target_iqns', 'target_luns'):
|
||||
props.pop(key, None)
|
||||
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()
|
||||
for key in ('target_portals', 'target_iqns', 'target_luns'):
|
||||
props.pop(key, None)
|
||||
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'
|
||||
|
||||
@@ -78,6 +78,21 @@ class TestBaseISCSITargetDriver(test.TestCase):
|
||||
self.assertEqual(self.expected_iscsi_properties,
|
||||
self.target._get_iscsi_properties(self.testvol))
|
||||
|
||||
def test_get_iscsi_properties_multiple_targets(self):
|
||||
testvol = self.testvol.copy()
|
||||
expected_iscsi_properties = self.expected_iscsi_properties.copy()
|
||||
iqn = expected_iscsi_properties['target_iqn']
|
||||
testvol.update(
|
||||
{'provider_location': '10.10.7.1:3260;10.10.8.1:3260 '
|
||||
'iqn.2010-10.org.openstack:'
|
||||
'volume-%s 0' % self.fake_volume_id})
|
||||
expected_iscsi_properties.update(
|
||||
{'target_portals': ['10.10.7.1:3260', '10.10.8.1:3260'],
|
||||
'target_iqns': [iqn, iqn],
|
||||
'target_luns': [0, 0]})
|
||||
self.assertEqual(expected_iscsi_properties,
|
||||
self.target._get_iscsi_properties(testvol))
|
||||
|
||||
def test_build_iscsi_auth_string(self):
|
||||
auth_string = 'chap chap-user chap-password'
|
||||
self.assertEqual(auth_string,
|
||||
|
||||
@@ -4534,7 +4534,10 @@ class ISCSITestCase(DriverTestCase):
|
||||
"attached_mode": "rw"}
|
||||
iscsi_driver = \
|
||||
cinder.volume.targets.tgt.TgtAdm(configuration=self.configuration)
|
||||
result = iscsi_driver._get_iscsi_properties(volume, multipath=True)
|
||||
result = iscsi_driver._get_iscsi_properties(volume)
|
||||
self.assertEqual(result["target_portal"], "1.1.1.1:3260")
|
||||
self.assertEqual(result["target_iqn"], "iqn:iqn")
|
||||
self.assertEqual(result["target_lun"], 0)
|
||||
self.assertEqual(["1.1.1.1:3260", "2.2.2.2:3261"],
|
||||
result["target_portals"])
|
||||
self.assertEqual(["iqn:iqn", "iqn:iqn"], result["target_iqns"])
|
||||
|
||||
@@ -1235,10 +1235,15 @@ class ISCSIDriver(VolumeDriver):
|
||||
:access_mode: the volume access mode allow client used
|
||||
('rw' or 'ro' currently supported)
|
||||
|
||||
In some of drivers, When multipath=True is specified, :target_iqn,
|
||||
:target_portal, :target_lun may be replaced with :target_iqns,
|
||||
:target_portals, :target_luns, which contain lists of multiple values.
|
||||
In this case, the initiator should establish sessions to all the path.
|
||||
In some of drivers that support multiple connections (for multipath
|
||||
and for single path with failover on connection failure), it returns
|
||||
:target_iqns, :target_portals, :target_luns, which contain lists of
|
||||
multiple values. The main portal information is also returned in
|
||||
:target_iqn, :target_portal, :target_lun for backward compatibility.
|
||||
|
||||
Note that some of drivers don't return :target_portals even if they
|
||||
support multipath. Then the connector should use sendtargets discovery
|
||||
to find the other portals if it supports multipath.
|
||||
"""
|
||||
|
||||
properties = {}
|
||||
@@ -1276,14 +1281,13 @@ class ISCSIDriver(VolumeDriver):
|
||||
else:
|
||||
lun = 0
|
||||
|
||||
if multipath:
|
||||
if nr_portals > 1:
|
||||
properties['target_portals'] = portals
|
||||
properties['target_iqns'] = [iqn] * nr_portals
|
||||
properties['target_luns'] = [lun] * nr_portals
|
||||
else:
|
||||
properties['target_portal'] = portals[0]
|
||||
properties['target_iqn'] = iqn
|
||||
properties['target_lun'] = lun
|
||||
properties['target_portal'] = portals[0]
|
||||
properties['target_iqn'] = iqn
|
||||
properties['target_lun'] = lun
|
||||
|
||||
properties['volume_id'] = volume['id']
|
||||
|
||||
@@ -1351,14 +1355,31 @@ class ISCSIDriver(VolumeDriver):
|
||||
}
|
||||
}
|
||||
|
||||
If the backend driver supports multiple connections for multipath and
|
||||
for single path with failover, "target_portals", "target_iqns",
|
||||
"target_luns" are also populated::
|
||||
|
||||
{
|
||||
'driver_volume_type': 'iscsi'
|
||||
'data': {
|
||||
'target_discovered': False,
|
||||
'target_iqn': 'iqn.2010-10.org.openstack:volume1',
|
||||
'target_iqns': ['iqn.2010-10.org.openstack:volume1',
|
||||
'iqn.2010-10.org.openstack:volume1-2'],
|
||||
'target_portal': '10.0.0.1:3260',
|
||||
'target_portals': ['10.0.0.1:3260', '10.0.1.1:3260']
|
||||
'target_lun': 1,
|
||||
'target_luns': [1, 1],
|
||||
'volume_id': 1,
|
||||
'access_mode': 'rw'
|
||||
}
|
||||
}
|
||||
"""
|
||||
# NOTE(jdg): Yes, this is duplicated in the volume/target
|
||||
# drivers, for now leaving it as there are 3'rd party
|
||||
# drivers that don't use target drivers, but inherit from
|
||||
# this base class and use this init data
|
||||
iscsi_properties = self._get_iscsi_properties(volume,
|
||||
connector.get(
|
||||
'multipath'))
|
||||
iscsi_properties = self._get_iscsi_properties(volume)
|
||||
return {
|
||||
'driver_volume_type': 'iscsi',
|
||||
'data': iscsi_properties
|
||||
|
||||
@@ -70,10 +70,15 @@ class ISCSITarget(driver.Target):
|
||||
:access_mode: the volume access mode allow client used
|
||||
('rw' or 'ro' currently supported)
|
||||
|
||||
When multipath=True is specified, :target_iqn, :target_portal,
|
||||
:target_lun may be replaced with :target_iqns, :target_portals,
|
||||
:target_luns, which contain lists of multiple values.
|
||||
In this case, the initiator should establish sessions to all the path.
|
||||
In some of drivers that support multiple connections (for multipath
|
||||
and for single path with failover on connection failure), it returns
|
||||
:target_iqns, :target_portals, :target_luns, which contain lists of
|
||||
multiple values. The main portal information is also returned in
|
||||
:target_iqn, :target_portal, :target_lun for backward compatibility.
|
||||
|
||||
Note that some of drivers don't return :target_portals even if they
|
||||
support multipath. Then the connector should use sendtargets discovery
|
||||
to find the other portals if it supports multipath.
|
||||
"""
|
||||
|
||||
properties = {}
|
||||
@@ -113,14 +118,13 @@ class ISCSITarget(driver.Target):
|
||||
else:
|
||||
lun = 0
|
||||
|
||||
if multipath:
|
||||
if nr_portals > 1 or multipath:
|
||||
properties['target_portals'] = portals
|
||||
properties['target_iqns'] = [iqn] * nr_portals
|
||||
properties['target_luns'] = [lun] * nr_portals
|
||||
else:
|
||||
properties['target_portal'] = portals[0]
|
||||
properties['target_iqn'] = iqn
|
||||
properties['target_lun'] = lun
|
||||
properties['target_portal'] = portals[0]
|
||||
properties['target_iqn'] = iqn
|
||||
properties['target_lun'] = lun
|
||||
|
||||
properties['volume_id'] = volume['id']
|
||||
|
||||
|
||||
Reference in New Issue
Block a user