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:
Walter A. Boring IV 2015-08-14 17:39:15 -07:00 committed by Walter A. Boring IV (hemna)
parent 1627b2145b
commit 3ea86f7d60
5 changed files with 167 additions and 43 deletions

View File

@ -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', 'space-set-apphosts', '-n'...
vgs-cluster: CommandFilter, vgs-cluster, root
# initiator/linuxscsi.py
scsi_id: CommandFilter, /lib/udev/scsi_id, root

View File

@ -908,19 +908,32 @@ class FibreChannelConnector(InitiatorConnector):
"(after %(tries)s rescans)",
{'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
# device. If so, we'll use the multipath device.
if self.use_multipath:
mdev_info = self._linuxscsi.find_multipath_device(self.device_name)
if mdev_info is not None:
LOG.debug("Multipath device discovered %(device)s",
{'device': mdev_info['device']})
device_path = mdev_info['device']
device_info['multipath_id'] = mdev_info['id']
path = self._linuxscsi.find_multipath_device_path(device_wwn)
if path is not None:
LOG.debug("Multipath device path discovered %(device)s",
{'device': path})
device_path = path
# 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
mpath_info = self._linuxscsi.find_multipath_device(
self.device_name)
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:
device_path = self.host_device
@ -980,25 +993,25 @@ class FibreChannelConnector(InitiatorConnector):
target_lun - LUN id of the volume
"""
# If this is a multipath device, we need to search again
# and make sure we remove all the devices. Some of them
# might not have shown up at attach time.
if self.use_multipath and 'multipath_id' in device_info:
multipath_id = device_info['multipath_id']
mdev_info = self._linuxscsi.find_multipath_device(multipath_id)
devices = mdev_info['devices']
self._linuxscsi.flush_multipath_device(multipath_id)
else:
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)
devices = []
volume_paths = self._get_volume_paths(connection_properties)
wwn = None
for path in volume_paths:
real_path = self._linuxscsi.get_name_from_path(path)
if not wwn:
wwn = self._linuxscsi.get_scsi_wwn(path)
device_info = self._linuxscsi.get_device_info(real_path)
devices.append(device_info)
LOG.debug("devices to remove = %s", 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):
# There may have been more than 1 device mounted
# by the kernel for this volume. We have to remove

View File

@ -24,6 +24,7 @@ from oslo_log import log as logging
from os_brick import exception
from os_brick import executor
from os_brick.i18n import _LI
from os_brick.i18n import _LW
from os_brick import utils
@ -101,13 +102,22 @@ class LinuxSCSI(executor.Executor):
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
and the multipath device itself.
"""
LOG.debug("remove multipath device %s", multipath_name)
mpath_dev = self.find_multipath_device(multipath_name)
LOG.debug("remove multipath device %s", device)
mpath_dev = self.find_multipath_device(device)
if mpath_dev:
devices = mpath_dev['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"),
{'code': exc.exit_code})
def find_multipath_device(self, device):
"""Find a multipath device associated with a LUN device name.
@utils.retry(exceptions=exception.VolumeDeviceNotFound)
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
@ -167,20 +237,9 @@ class LinuxSCSI(executor.Executor):
if not re.match(MULTIPATH_ERROR_REGEX, line)]
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 = '/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.
try:
os.stat(mdev)
@ -188,6 +247,12 @@ class LinuxSCSI(executor.Executor):
LOG.warn(_LW("Couldn't find multipath device %s"), mdev)
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",
{'mdev': mdev})
device_lines = lines[3:]

View File

@ -933,21 +933,26 @@ class FibreChannelConnectorTestCase(ConnectorTestCase):
@mock.patch.object(linuxfc.LinuxFibreChannel, 'get_fc_hbas')
@mock.patch.object(linuxfc.LinuxFibreChannel, 'get_fc_hbas_info')
@mock.patch.object(linuxscsi.LinuxSCSI, 'remove_scsi_device')
@mock.patch.object(linuxscsi.LinuxSCSI, 'get_scsi_wwn')
@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_mock, realpath_mock, exists_mock):
get_fc_hbas_mock.side_effect = self.fake_get_fc_hbas
get_fc_hbas_info_mock.side_effect = self.fake_get_fc_hbas_info
wwn = '1234567890'
multipath_devname = '/dev/md-1'
devices = {"device": multipath_devname,
"id": "1234567890",
"id": wwn,
"devices": [{'device': '/dev/sdb',
'address': '1:0:0:1',
'host': 1, 'channel': 0,
'id': 0, 'lun': 1}]}
get_device_info_mock.return_value = devices['devices'][0]
get_scsi_wwn_mock.return_value = wwn
location = '10.0.2.15:3260'
name = 'volume-00000001'

View File

@ -93,6 +93,44 @@ class LinuxSCSITestCase(base.TestCase):
expected_commands = [('multipath -F')]
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(os.path, 'exists', return_value=True)
def test_remove_multipath_device(self, exists_mock, mock_multipath):