diff --git a/etc/os-brick/rootwrap.d/os-brick.filters b/etc/os-brick/rootwrap.d/os-brick.filters index 9f672fc32..d4d15c020 100644 --- a/etc/os-brick/rootwrap.d/os-brick.filters +++ b/etc/os-brick/rootwrap.d/os-brick.filters @@ -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 diff --git a/os_brick/initiator/connector.py b/os_brick/initiator/connector.py index d1fb35ab9..5cae74008 100644 --- a/os_brick/initiator/connector.py +++ b/os_brick/initiator/connector.py @@ -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 diff --git a/os_brick/initiator/linuxscsi.py b/os_brick/initiator/linuxscsi.py index e86c8eca1..4d427b553 100644 --- a/os_brick/initiator/linuxscsi.py +++ b/os_brick/initiator/linuxscsi.py @@ -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- + /dev/disk/by-id/dm-name-mpath + /dev/disk/by-id/scsi-mpath + /dev/mapper/mpath + + 2) When multipath friendly names are OFF: + /dev/disk/by-id/dm-uuid-mpath- + /dev/disk/by-id/scsi- + /dev/mapper/ + + """ + 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:] diff --git a/os_brick/tests/initiator/test_connector.py b/os_brick/tests/initiator/test_connector.py index f08f95a87..1834703a4 100644 --- a/os_brick/tests/initiator/test_connector.py +++ b/os_brick/tests/initiator/test_connector.py @@ -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' diff --git a/os_brick/tests/initiator/test_linuxscsi.py b/os_brick/tests/initiator/test_linuxscsi.py index 6b24166fa..d26192193 100644 --- a/os_brick/tests/initiator/test_linuxscsi.py +++ b/os_brick/tests/initiator/test_linuxscsi.py @@ -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- path + # but finding the + # /dev/mapper/ 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):