FC Stop calling multipath command line
This patch changes how we discover Multipath devices for FibreChannel volume attaches. Running multipath -l <device> can become slower and slower as more and more volumes are attached to a host. To overcome this, there are ways of discovering multipath device paths without using the multipath -l command at all. When multipath daemon is running, and it discovers new volumes, it will create new device paths for the multipath device associated with that new volume. Those multipath device paths are predictable and show up after the multipath device is created. This avoids the repeated looping calls to multipath -l to discover the same paths. SCSI volumes have a WWN that's supposed to be in page 0x83 on the volume itself according to the SCSI SPC-3 spec. That WWN is where the multipath daemon gets it's multipath ID from and what is used to create the predictable multipath device paths on the system. When multipath friendly names are disabled, you get paths of /dev/disk/by-id/dm-uuid-mpath-<WWN> /dev/disk/by-id/scsi-<WWN> /dev/mapper/<WWN> When multipath friendly names are enabled, you get paths of /dev/disk/by-id/dm-uuid-mpath-<WWN> /dev/disk/by-id/dm-name-mpath<N> /dev/disk/by-id/scsi-mpath<N> /dev/mapper/mpath<N> This patch does 3 different attempts to find a multipath device path to use. First it looks in the common location of: /dev/disk/by-id/dm-uuid-mpath-<WWN> Then in the non friendly name path of: /dev/mapper/<WWN> And lastly using the fallback of calling multipath -l <device> to get: /dev/mapper/mpath<N> Partial-Bug: 1487169 Change-Id: I9a9fffcb6882b1c2750b1e7927475093bde36d04
This commit is contained in:
parent
1627b2145b
commit
3ea86f7d60
|
@ -61,3 +61,6 @@ sds_cli: CommandFilter, /usr/local/bin/sds/sds_cli, root
|
||||||
# initiator/connector.py: 'vgs-cluster', 'domain-list', '-l'
|
# initiator/connector.py: 'vgs-cluster', 'domain-list', '-l'
|
||||||
# initiator/connector.py: 'vgs-cluster', 'space-set-apphosts', '-n'...
|
# initiator/connector.py: 'vgs-cluster', 'space-set-apphosts', '-n'...
|
||||||
vgs-cluster: CommandFilter, vgs-cluster, root
|
vgs-cluster: CommandFilter, vgs-cluster, root
|
||||||
|
|
||||||
|
# initiator/linuxscsi.py
|
||||||
|
scsi_id: CommandFilter, /lib/udev/scsi_id, root
|
||||||
|
|
|
@ -908,19 +908,32 @@ class FibreChannelConnector(InitiatorConnector):
|
||||||
"(after %(tries)s rescans)",
|
"(after %(tries)s rescans)",
|
||||||
{'name': self.device_name, 'tries': tries})
|
{'name': self.device_name, 'tries': tries})
|
||||||
|
|
||||||
|
# find out the WWN of the device
|
||||||
|
device_wwn = self._linuxscsi.get_scsi_wwn(self.host_device)
|
||||||
|
LOG.debug("Device WWN = '%(wwn)s'", {'wwn': device_wwn})
|
||||||
|
|
||||||
# see if the new drive is part of a multipath
|
# see if the new drive is part of a multipath
|
||||||
# device. If so, we'll use the multipath device.
|
# device. If so, we'll use the multipath device.
|
||||||
if self.use_multipath:
|
if self.use_multipath:
|
||||||
mdev_info = self._linuxscsi.find_multipath_device(self.device_name)
|
|
||||||
if mdev_info is not None:
|
path = self._linuxscsi.find_multipath_device_path(device_wwn)
|
||||||
LOG.debug("Multipath device discovered %(device)s",
|
if path is not None:
|
||||||
{'device': mdev_info['device']})
|
LOG.debug("Multipath device path discovered %(device)s",
|
||||||
device_path = mdev_info['device']
|
{'device': path})
|
||||||
device_info['multipath_id'] = mdev_info['id']
|
device_path = path
|
||||||
|
# for temporary backwards compatibility
|
||||||
|
device_info['multipath_id'] = device_wwn
|
||||||
else:
|
else:
|
||||||
# we didn't find a multipath device.
|
mpath_info = self._linuxscsi.find_multipath_device(
|
||||||
# so we assume the kernel only sees 1 device
|
self.device_name)
|
||||||
device_path = self.host_device
|
if mpath_info:
|
||||||
|
device_path = mpath_info['device']
|
||||||
|
# for temporary backwards compatibility
|
||||||
|
device_info['multipath_id'] = device_wwn
|
||||||
|
else:
|
||||||
|
# we didn't find a multipath device.
|
||||||
|
# so we assume the kernel only sees 1 device
|
||||||
|
device_path = self.host_device
|
||||||
else:
|
else:
|
||||||
device_path = self.host_device
|
device_path = self.host_device
|
||||||
|
|
||||||
|
@ -980,25 +993,25 @@ class FibreChannelConnector(InitiatorConnector):
|
||||||
target_lun - LUN id of the volume
|
target_lun - LUN id of the volume
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# If this is a multipath device, we need to search again
|
devices = []
|
||||||
# and make sure we remove all the devices. Some of them
|
volume_paths = self._get_volume_paths(connection_properties)
|
||||||
# might not have shown up at attach time.
|
wwn = None
|
||||||
if self.use_multipath and 'multipath_id' in device_info:
|
for path in volume_paths:
|
||||||
multipath_id = device_info['multipath_id']
|
real_path = self._linuxscsi.get_name_from_path(path)
|
||||||
mdev_info = self._linuxscsi.find_multipath_device(multipath_id)
|
if not wwn:
|
||||||
devices = mdev_info['devices']
|
wwn = self._linuxscsi.get_scsi_wwn(path)
|
||||||
self._linuxscsi.flush_multipath_device(multipath_id)
|
device_info = self._linuxscsi.get_device_info(real_path)
|
||||||
else:
|
devices.append(device_info)
|
||||||
devices = []
|
|
||||||
volume_paths = self._get_volume_paths(connection_properties)
|
|
||||||
for path in volume_paths:
|
|
||||||
real_path = self._linuxscsi.get_name_from_path(path)
|
|
||||||
device_info = self._linuxscsi.get_device_info(real_path)
|
|
||||||
devices.append(device_info)
|
|
||||||
|
|
||||||
LOG.debug("devices to remove = %s", devices)
|
LOG.debug("devices to remove = %s", devices)
|
||||||
self._remove_devices(connection_properties, devices)
|
self._remove_devices(connection_properties, devices)
|
||||||
|
|
||||||
|
if self.use_multipath:
|
||||||
|
# There is a bug in multipath where the flushing
|
||||||
|
# doesn't remove the entry if friendly names are on
|
||||||
|
# we'll try anyway.
|
||||||
|
self._linuxscsi.flush_multipath_device(wwn)
|
||||||
|
|
||||||
def _remove_devices(self, connection_properties, devices):
|
def _remove_devices(self, connection_properties, devices):
|
||||||
# There may have been more than 1 device mounted
|
# There may have been more than 1 device mounted
|
||||||
# by the kernel for this volume. We have to remove
|
# by the kernel for this volume. We have to remove
|
||||||
|
|
|
@ -24,6 +24,7 @@ from oslo_log import log as logging
|
||||||
|
|
||||||
from os_brick import exception
|
from os_brick import exception
|
||||||
from os_brick import executor
|
from os_brick import executor
|
||||||
|
from os_brick.i18n import _LI
|
||||||
from os_brick.i18n import _LW
|
from os_brick.i18n import _LW
|
||||||
from os_brick import utils
|
from os_brick import utils
|
||||||
|
|
||||||
|
@ -101,13 +102,22 @@ class LinuxSCSI(executor.Executor):
|
||||||
|
|
||||||
return dev_info
|
return dev_info
|
||||||
|
|
||||||
def remove_multipath_device(self, multipath_name):
|
def get_scsi_wwn(self, path):
|
||||||
|
"""Read the WWN from page 0x83 value for a SCSI device."""
|
||||||
|
|
||||||
|
(out, _err) = self._execute('scsi_id', '--page', '0x83',
|
||||||
|
'--whitelisted', path,
|
||||||
|
run_as_root=True,
|
||||||
|
root_helper=self._root_helper)
|
||||||
|
return out.strip()
|
||||||
|
|
||||||
|
def remove_multipath_device(self, device):
|
||||||
"""This removes LUNs associated with a multipath device
|
"""This removes LUNs associated with a multipath device
|
||||||
and the multipath device itself.
|
and the multipath device itself.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
LOG.debug("remove multipath device %s", multipath_name)
|
LOG.debug("remove multipath device %s", device)
|
||||||
mpath_dev = self.find_multipath_device(multipath_name)
|
mpath_dev = self.find_multipath_device(device)
|
||||||
if mpath_dev:
|
if mpath_dev:
|
||||||
devices = mpath_dev['devices']
|
devices = mpath_dev['devices']
|
||||||
LOG.debug("multipath LUNs to remove %s", devices)
|
LOG.debug("multipath LUNs to remove %s", devices)
|
||||||
|
@ -142,10 +152,70 @@ class LinuxSCSI(executor.Executor):
|
||||||
LOG.warning(_LW("multipath call failed exit %(code)s"),
|
LOG.warning(_LW("multipath call failed exit %(code)s"),
|
||||||
{'code': exc.exit_code})
|
{'code': exc.exit_code})
|
||||||
|
|
||||||
def find_multipath_device(self, device):
|
@utils.retry(exceptions=exception.VolumeDeviceNotFound)
|
||||||
"""Find a multipath device associated with a LUN device name.
|
def wait_for_path(self, volume_path):
|
||||||
|
"""Wait for a path to show up."""
|
||||||
|
LOG.debug("Checking to see if %s exists yet.",
|
||||||
|
volume_path)
|
||||||
|
if not os.path.exists(volume_path):
|
||||||
|
LOG.debug("%(path)s doesn't exists yet.", {'path': volume_path})
|
||||||
|
raise exception.VolumeDeviceNotFound(
|
||||||
|
volume_path=volume_path)
|
||||||
|
else:
|
||||||
|
LOG.debug("%s has shown up.", volume_path)
|
||||||
|
|
||||||
|
def find_multipath_device_path(self, wwn):
|
||||||
|
"""Look for the multipath device file for a volume WWN.
|
||||||
|
|
||||||
|
Multipath devices can show up in several places on
|
||||||
|
a linux system.
|
||||||
|
|
||||||
|
1) When multipath friendly names are ON:
|
||||||
|
a device file will show up in
|
||||||
|
/dev/disk/by-id/dm-uuid-mpath-<WWN>
|
||||||
|
/dev/disk/by-id/dm-name-mpath<N>
|
||||||
|
/dev/disk/by-id/scsi-mpath<N>
|
||||||
|
/dev/mapper/mpath<N>
|
||||||
|
|
||||||
|
2) When multipath friendly names are OFF:
|
||||||
|
/dev/disk/by-id/dm-uuid-mpath-<WWN>
|
||||||
|
/dev/disk/by-id/scsi-<WWN>
|
||||||
|
/dev/mapper/<WWN>
|
||||||
|
|
||||||
|
"""
|
||||||
|
LOG.info(_LI("Find Multipath device file for volume WWN %(wwn)s"),
|
||||||
|
{'wwn': wwn})
|
||||||
|
# First look for the common path
|
||||||
|
wwn_dict = {'wwn': wwn}
|
||||||
|
path = "/dev/disk/by-id/dm-uuid-mpath-%(wwn)s" % wwn_dict
|
||||||
|
try:
|
||||||
|
self.wait_for_path(path)
|
||||||
|
return path
|
||||||
|
except exception.VolumeDeviceNotFound:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# for some reason the common path wasn't found
|
||||||
|
# lets try the dev mapper path
|
||||||
|
path = "/dev/mapper/%(wwn)s" % wwn_dict
|
||||||
|
try:
|
||||||
|
self.wait_for_path(path)
|
||||||
|
return path
|
||||||
|
except exception.VolumeDeviceNotFound:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# couldn't find a path
|
||||||
|
LOG.warning(_LW("couldn't find a valid multipath device path for "
|
||||||
|
"%(wwn)s"), wwn_dict)
|
||||||
|
return None
|
||||||
|
|
||||||
|
def find_multipath_device(self, device):
|
||||||
|
"""Discover multipath devices for a mpath device.
|
||||||
|
|
||||||
|
This uses the slow multipath -l command to find a
|
||||||
|
multipath device description, then screen scrapes
|
||||||
|
the output to discover the multipath device name
|
||||||
|
and it's devices.
|
||||||
|
|
||||||
device can be either a /dev/sdX entry or a multipath id.
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
mdev = None
|
mdev = None
|
||||||
|
@ -167,20 +237,9 @@ class LinuxSCSI(executor.Executor):
|
||||||
if not re.match(MULTIPATH_ERROR_REGEX, line)]
|
if not re.match(MULTIPATH_ERROR_REGEX, line)]
|
||||||
if lines:
|
if lines:
|
||||||
|
|
||||||
# Use the device name, be it the WWID, mpathN or custom alias
|
|
||||||
# of a device to build the device path. This should be the
|
|
||||||
# first item on the first line of output from `multipath -l
|
|
||||||
# ${path}` or `multipath -l ${wwid}`..
|
|
||||||
mdev_name = lines[0].split(" ")[0]
|
mdev_name = lines[0].split(" ")[0]
|
||||||
mdev = '/dev/mapper/%s' % mdev_name
|
mdev = '/dev/mapper/%s' % mdev_name
|
||||||
|
|
||||||
# Find the WWID for the LUN if we are using mpathN or aliases.
|
|
||||||
wwid_search = MULTIPATH_WWID_REGEX.search(lines[0])
|
|
||||||
if wwid_search is not None:
|
|
||||||
mdev_id = wwid_search.group('wwid')
|
|
||||||
else:
|
|
||||||
mdev_id = mdev_name
|
|
||||||
|
|
||||||
# Confirm that the device is present.
|
# Confirm that the device is present.
|
||||||
try:
|
try:
|
||||||
os.stat(mdev)
|
os.stat(mdev)
|
||||||
|
@ -188,6 +247,12 @@ class LinuxSCSI(executor.Executor):
|
||||||
LOG.warn(_LW("Couldn't find multipath device %s"), mdev)
|
LOG.warn(_LW("Couldn't find multipath device %s"), mdev)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
wwid_search = MULTIPATH_WWID_REGEX.search(lines[0])
|
||||||
|
if wwid_search is not None:
|
||||||
|
mdev_id = wwid_search.group('wwid')
|
||||||
|
else:
|
||||||
|
mdev_id = mdev_name
|
||||||
|
|
||||||
LOG.debug("Found multipath device = %(mdev)s",
|
LOG.debug("Found multipath device = %(mdev)s",
|
||||||
{'mdev': mdev})
|
{'mdev': mdev})
|
||||||
device_lines = lines[3:]
|
device_lines = lines[3:]
|
||||||
|
|
|
@ -933,21 +933,26 @@ class FibreChannelConnectorTestCase(ConnectorTestCase):
|
||||||
@mock.patch.object(linuxfc.LinuxFibreChannel, 'get_fc_hbas')
|
@mock.patch.object(linuxfc.LinuxFibreChannel, 'get_fc_hbas')
|
||||||
@mock.patch.object(linuxfc.LinuxFibreChannel, 'get_fc_hbas_info')
|
@mock.patch.object(linuxfc.LinuxFibreChannel, 'get_fc_hbas_info')
|
||||||
@mock.patch.object(linuxscsi.LinuxSCSI, 'remove_scsi_device')
|
@mock.patch.object(linuxscsi.LinuxSCSI, 'remove_scsi_device')
|
||||||
|
@mock.patch.object(linuxscsi.LinuxSCSI, 'get_scsi_wwn')
|
||||||
@mock.patch.object(linuxscsi.LinuxSCSI, 'get_device_info')
|
@mock.patch.object(linuxscsi.LinuxSCSI, 'get_device_info')
|
||||||
def test_connect_volume(self, get_device_info_mock, remove_device_mock,
|
def test_connect_volume(self, get_device_info_mock,
|
||||||
|
get_scsi_wwn_mock,
|
||||||
|
remove_device_mock,
|
||||||
get_fc_hbas_info_mock,
|
get_fc_hbas_info_mock,
|
||||||
get_fc_hbas_mock, realpath_mock, exists_mock):
|
get_fc_hbas_mock, realpath_mock, exists_mock):
|
||||||
get_fc_hbas_mock.side_effect = self.fake_get_fc_hbas
|
get_fc_hbas_mock.side_effect = self.fake_get_fc_hbas
|
||||||
get_fc_hbas_info_mock.side_effect = self.fake_get_fc_hbas_info
|
get_fc_hbas_info_mock.side_effect = self.fake_get_fc_hbas_info
|
||||||
|
|
||||||
|
wwn = '1234567890'
|
||||||
multipath_devname = '/dev/md-1'
|
multipath_devname = '/dev/md-1'
|
||||||
devices = {"device": multipath_devname,
|
devices = {"device": multipath_devname,
|
||||||
"id": "1234567890",
|
"id": wwn,
|
||||||
"devices": [{'device': '/dev/sdb',
|
"devices": [{'device': '/dev/sdb',
|
||||||
'address': '1:0:0:1',
|
'address': '1:0:0:1',
|
||||||
'host': 1, 'channel': 0,
|
'host': 1, 'channel': 0,
|
||||||
'id': 0, 'lun': 1}]}
|
'id': 0, 'lun': 1}]}
|
||||||
get_device_info_mock.return_value = devices['devices'][0]
|
get_device_info_mock.return_value = devices['devices'][0]
|
||||||
|
get_scsi_wwn_mock.return_value = wwn
|
||||||
|
|
||||||
location = '10.0.2.15:3260'
|
location = '10.0.2.15:3260'
|
||||||
name = 'volume-00000001'
|
name = 'volume-00000001'
|
||||||
|
|
|
@ -93,6 +93,44 @@ class LinuxSCSITestCase(base.TestCase):
|
||||||
expected_commands = [('multipath -F')]
|
expected_commands = [('multipath -F')]
|
||||||
self.assertEqual(expected_commands, self.cmds)
|
self.assertEqual(expected_commands, self.cmds)
|
||||||
|
|
||||||
|
def test_get_scsi_wwn(self):
|
||||||
|
fake_path = '/dev/disk/by-id/somepath'
|
||||||
|
fake_wwn = '1234567890'
|
||||||
|
|
||||||
|
def fake_execute(*cmd, **kwargs):
|
||||||
|
return fake_wwn, None
|
||||||
|
|
||||||
|
self.linuxscsi._execute = fake_execute
|
||||||
|
wwn = self.linuxscsi.get_scsi_wwn(fake_path)
|
||||||
|
self.assertEqual(fake_wwn, wwn)
|
||||||
|
|
||||||
|
@mock.patch.object(os.path, 'exists', return_value=True)
|
||||||
|
def test_find_multipath_device_path(self, exists_mock):
|
||||||
|
fake_wwn = '1234567890'
|
||||||
|
found_path = self.linuxscsi.find_multipath_device_path(fake_wwn)
|
||||||
|
expected_path = '/dev/disk/by-id/dm-uuid-mpath-%s' % fake_wwn
|
||||||
|
self.assertEqual(expected_path, found_path)
|
||||||
|
|
||||||
|
@mock.patch.object(os.path, 'exists')
|
||||||
|
def test_find_multipath_device_path_mapper(self, exists_mock):
|
||||||
|
# the wait loop tries 3 times before it gives up
|
||||||
|
# we want to test failing to find the
|
||||||
|
# /dev/disk/by-id/dm-uuid-mpath-<WWN> path
|
||||||
|
# but finding the
|
||||||
|
# /dev/mapper/<WWN> path
|
||||||
|
exists_mock.side_effect = [False, False, False, True]
|
||||||
|
fake_wwn = '1234567890'
|
||||||
|
found_path = self.linuxscsi.find_multipath_device_path(fake_wwn)
|
||||||
|
expected_path = '/dev/mapper/%s' % fake_wwn
|
||||||
|
self.assertEqual(expected_path, found_path)
|
||||||
|
|
||||||
|
@mock.patch.object(os.path, 'exists', return_value=False)
|
||||||
|
def test_find_multipath_device_path_fail(self, exists_mock):
|
||||||
|
fake_wwn = '1234567890'
|
||||||
|
found_path = self.linuxscsi.find_multipath_device_path(fake_wwn)
|
||||||
|
expected_path = None
|
||||||
|
self.assertEqual(expected_path, found_path)
|
||||||
|
|
||||||
@mock.patch.object(linuxscsi.LinuxSCSI, 'find_multipath_device')
|
@mock.patch.object(linuxscsi.LinuxSCSI, 'find_multipath_device')
|
||||||
@mock.patch.object(os.path, 'exists', return_value=True)
|
@mock.patch.object(os.path, 'exists', return_value=True)
|
||||||
def test_remove_multipath_device(self, exists_mock, mock_multipath):
|
def test_remove_multipath_device(self, exists_mock, mock_multipath):
|
||||||
|
|
Loading…
Reference in New Issue