# (c) Copyright 2013 Hewlett-Packard Development Company, L.P. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. """Generic linux scsi subsystem and Multipath utilities. Note, this is not iSCSI. """ import os import re from oslo_concurrency import processutils as putils 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 LOG = logging.getLogger(__name__) MULTIPATH_ERROR_REGEX = re.compile("\w{3} \d+ \d\d:\d\d:\d\d \|.*$") MULTIPATH_WWID_REGEX = re.compile("\((?P.+)\)") class LinuxSCSI(executor.Executor): def __init__(self, root_helper, execute=putils.execute, *args, **kwargs): super(LinuxSCSI, self).__init__(root_helper, execute, *args, **kwargs) def echo_scsi_command(self, path, content): """Used to echo strings to scsi subsystem.""" args = ["-a", path] kwargs = dict(process_input=content, run_as_root=True, root_helper=self._root_helper) self._execute('tee', *args, **kwargs) def get_name_from_path(self, path): """Translates /dev/disk/by-path/ entry to /dev/sdX.""" name = os.path.realpath(path) if name.startswith("/dev/"): return name else: return None def remove_scsi_device(self, device): """Removes a scsi device based upon /dev/sdX name.""" path = "/sys/block/%s/device/delete" % device.replace("/dev/", "") if os.path.exists(path): # flush any outstanding IO first self.flush_device_io(device) LOG.debug("Remove SCSI device %(device)s with %(path)s", {'device': device, 'path': path}) self.echo_scsi_command(path, "1") @utils.retry(exceptions=exception.VolumePathNotRemoved, retries=3, backoff_rate=2) def wait_for_volume_removal(self, volume_path): """This is used to ensure that volumes are gone.""" LOG.debug("Checking to see if SCSI volume %s has been removed.", volume_path) if os.path.exists(volume_path): LOG.debug("%(path)s still exists.", {'path': volume_path}) raise exception.VolumePathNotRemoved( volume_path=volume_path) else: LOG.debug("SCSI volume %s has been removed.", volume_path) def get_device_info(self, device): (out, _err) = self._execute('sg_scan', device, run_as_root=True, root_helper=self._root_helper) dev_info = {'device': device, 'host': None, 'channel': None, 'id': None, 'lun': None} if out: line = out.strip() line = line.replace(device + ": ", "") info = line.split(" ") for item in info: if '=' in item: pair = item.split('=') dev_info[pair[0]] = pair[1] elif 'scsi' in item: dev_info['host'] = item.replace('scsi', '') return dev_info 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", device) mpath_dev = self.find_multipath_device(device) if mpath_dev: devices = mpath_dev['devices'] LOG.debug("multipath LUNs to remove %s", devices) for device in devices: self.remove_scsi_device(device['device']) self.flush_multipath_device(mpath_dev['id']) def flush_device_io(self, device): """This is used to flush any remaining IO in the buffers.""" try: LOG.debug("Flushing IO for device %s", device) self._execute('blockdev', '--flushbufs', device, run_as_root=True, root_helper=self._root_helper) except putils.ProcessExecutionError as exc: LOG.warning(_LW("Failed to flush IO buffers prior to removing " "device: %(code)s"), {'code': exc.exit_code}) def flush_multipath_device(self, device): try: LOG.debug("Flush multipath device %s", device) self._execute('multipath', '-f', device, run_as_root=True, root_helper=self._root_helper) except putils.ProcessExecutionError as exc: LOG.warning(_LW("multipath call failed exit %(code)s"), {'code': exc.exit_code}) def flush_multipath_devices(self): try: self._execute('multipath', '-F', run_as_root=True, root_helper=self._root_helper) except putils.ProcessExecutionError as exc: LOG.warning(_LW("multipath call failed exit %(code)s"), {'code': exc.exit_code}) @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. """ mdev = None devices = [] out = None try: (out, _err) = self._execute('multipath', '-l', device, run_as_root=True, root_helper=self._root_helper) except putils.ProcessExecutionError as exc: LOG.warning(_LW("multipath call failed exit %(code)s"), {'code': exc.exit_code}) return None if out: lines = out.strip() lines = lines.split("\n") lines = [line for line in lines if not re.match(MULTIPATH_ERROR_REGEX, line)] if lines: mdev_name = lines[0].split(" ")[0] mdev = '/dev/mapper/%s' % mdev_name # Confirm that the device is present. try: os.stat(mdev) except OSError: 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:] for dev_line in device_lines: if dev_line.find("policy") != -1: continue dev_line = dev_line.lstrip(' |-`') dev_info = dev_line.split() address = dev_info[0].split(":") dev = {'device': '/dev/%s' % dev_info[1], 'host': address[0], 'channel': address[1], 'id': address[2], 'lun': address[3] } devices.append(dev) if mdev is not None: info = {"device": mdev, "id": mdev_id, "name": mdev_name, "devices": devices} return info return None