1# (c) Copyright 2013 Hewlett-Packard Development Company, L.P.
2#
3#    Licensed under the Apache License, Version 2.0 (the "License"); you may
4#    not use this file except in compliance with the License. You may obtain
5#    a copy of the License at
6#
7#         http://www.apache.org/licenses/LICENSE-2.0
8#
9#    Unless required by applicable law or agreed to in writing, software
10#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
11#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
12#    License for the specific language governing permissions and limitations
13#    under the License.
14
15"""Generic linux scsi subsystem and Multipath utilities.
16
17   Note, this is not iSCSI.
18"""
19import glob
20import os
21import re
22import time
23from typing import Dict, List, Optional  # noqa: H301
24
25from oslo_concurrency import processutils as putils
26from oslo_log import log as logging
27
28from os_brick import exception
29from os_brick import executor
30from os_brick.privileged import rootwrap as priv_rootwrap
31from os_brick import utils
32
33LOG = logging.getLogger(__name__)
34
35MULTIPATH_ERROR_REGEX = re.compile(r"\w{3} \d+ \d\d:\d\d:\d\d \|.*$")
36MULTIPATH_WWID_REGEX = re.compile(r"\((?P<wwid>.+)\)")
37MULTIPATH_DEVICE_ACTIONS = ['unchanged:', 'reject:', 'reload:',
38                            'switchpg:', 'rename:', 'create:',
39                            'resize:']
40
41
42class LinuxSCSI(executor.Executor):
43    # As found in drivers/scsi/scsi_lib.c
44    WWN_TYPES = {'t10.': '1', 'eui.': '2', 'naa.': '3'}
45
46    def echo_scsi_command(self, path, content) -> None:
47        """Used to echo strings to scsi subsystem."""
48
49        args = ["-a", path]
50        kwargs = dict(process_input=content,
51                      run_as_root=True,
52                      root_helper=self._root_helper)
53        self._execute('tee', *args, **kwargs)
54
55    def get_name_from_path(self, path) -> Optional[str]:
56        """Translates /dev/disk/by-path/ entry to /dev/sdX."""
57
58        name = os.path.realpath(path)
59        if name.startswith("/dev/"):
60            return name
61        else:
62            return None
63
64    def remove_scsi_device(self,
65                           device: str,
66                           force: bool = False,
67                           exc=None,
68                           flush: bool = True) -> None:
69        """Removes a scsi device based upon /dev/sdX name."""
70        path = "/sys/block/%s/device/delete" % device.replace("/dev/", "")
71        if os.path.exists(path):
72            exc = exception.ExceptionChainer() if exc is None else exc
73            if flush:
74                # flush any outstanding IO first
75                with exc.context(force, 'Flushing %s failed', device):
76                    self.flush_device_io(device)
77
78            LOG.debug("Remove SCSI device %(device)s with %(path)s",
79                      {'device': device, 'path': path})
80            with exc.context(force, 'Removing %s failed', device):
81                self.echo_scsi_command(path, "1")
82
83    def wait_for_volumes_removal(self, volumes_names: List[str]) -> None:
84        """Wait for device paths to be removed from the system."""
85        str_names = ', '.join(volumes_names)
86        LOG.debug('Checking to see if SCSI volumes %s have been removed.',
87                  str_names)
88        exist = ['/dev/' + volume_name for volume_name in volumes_names]
89
90        # It can take up to 30 seconds to remove a SCSI device if the path
91        # failed right before we start detaching, which is unlikely, but we
92        # still shouldn't fail in that case.
93        for i in range(61):
94            exist = [path for path in exist if os.path.exists(path)]
95            if not exist:
96                LOG.debug("SCSI volumes %s have been removed.", str_names)
97                return
98            # Don't sleep on the last try since we are quitting
99            if i < 60:
100                time.sleep(0.5)
101                # Log every 5 seconds
102                if i % 10 == 0:
103                    LOG.debug('%s still exist.', ', '.join(exist))
104        raise exception.VolumePathNotRemoved(volume_path=exist)
105
106    def get_device_info(self, device: str) -> Dict[str, Optional[str]]:
107        dev_info = {'device': device, 'host': None,
108                    'channel': None, 'id': None, 'lun': None}
109        # The input argument 'device' can be of 2 types:
110        # (a) /dev/disk/by-path/XXX which is a symlink to /dev/sdX device
111        # (b) /dev/sdX
112        # If it's a symlink, get the /dev/sdX name first
113        if os.path.islink(device):
114            device = '/dev/' + os.readlink(device).split('/')[-1]
115        # Else it's already a /dev/sdX device.
116        # Then get it from lsscsi output
117        (out, _err) = self._execute('lsscsi')
118        if out:
119            for line in out.strip().split('\n'):
120                # The last column of lsscsi is device name
121                if line.split()[-1] == device:
122                    # The first column of lsscsi is [H:C:T:L]
123                    hctl_info = line.split()[0].strip('[]').split(':')
124                    dev_info['host'] = hctl_info[0]
125                    dev_info['channel'] = hctl_info[1]
126                    dev_info['id'] = hctl_info[2]
127                    dev_info['lun'] = hctl_info[3]
128                    break
129
130        LOG.debug('dev_info=%s', str(dev_info))
131        return dev_info
132
133    def get_sysfs_wwn(self, device_names, mpath=None) -> str:
134        """Return the wwid from sysfs in any of devices in udev format."""
135        # If we have a multipath DM we know that it has found the WWN
136        if mpath:
137            # We have the WWN in /uuid even with friendly names, unline /name
138            try:
139                with open('/sys/block/%s/dm/uuid' % mpath) as f:
140                    # Contents are matph-WWN, so get the part we want
141                    wwid = f.read().strip()[6:]
142                    if wwid:  # Check should not be needed, but just in case
143                        return wwid
144            except Exception as exc:
145                LOG.warning('Failed to read the DM uuid: %s', exc)
146
147        wwid = self.get_sysfs_wwid(device_names)
148        glob_str = '/dev/disk/by-id/scsi-'
149        wwn_paths = glob.glob(glob_str + '*')
150        # If we don't have multiple designators on page 0x83
151        if wwid and glob_str + wwid in wwn_paths:
152            return wwid
153
154        # If we have multiple designators use symlinks to find out the wwn
155        device_names = set(device_names)
156        for wwn_path in wwn_paths:
157            try:
158                if os.path.islink(wwn_path) and os.stat(wwn_path):
159                    path = os.path.realpath(wwn_path)
160                    if path.startswith('/dev/'):
161                        name = path[5:]
162                        # Symlink may point to the multipath dm if the attach
163                        # was too fast or we took long to check it. Check
164                        # devices belonging to the multipath DM.
165                        if name.startswith('dm-'):
166                            # Get the devices that belong to the DM
167                            slaves_path = '/sys/class/block/%s/slaves' % name
168                            dm_devs = os.listdir(slaves_path)
169                            # This is the right wwn_path if the devices we have
170                            # attached belong to the dm we followed
171                            if device_names.intersection(dm_devs):
172                                break
173
174                        # This is the right wwn_path if  devices we have
175                        elif name in device_names:
176                            break
177            except OSError:
178                continue
179        else:
180            return ''
181        return wwn_path[len(glob_str):]
182
183    def get_sysfs_wwid(self, device_names):
184        """Return the wwid from sysfs in any of devices in udev format."""
185        for device_name in device_names:
186            try:
187                with open('/sys/block/%s/device/wwid' % device_name) as f:
188                    wwid = f.read().strip()
189            except IOError:
190                continue
191            # The sysfs wwid has the wwn type in string format as a prefix,
192            # but udev uses its numerical representation as returned by
193            # scsi_id's page 0x83, so we need to map it
194            udev_wwid = self.WWN_TYPES.get(wwid[:4], '8') + wwid[4:]
195            return udev_wwid
196        return ''
197
198    def get_scsi_wwn(self, path):
199        """Read the WWN from page 0x83 value for a SCSI device."""
200
201        (out, _err) = self._execute('/lib/udev/scsi_id', '--page', '0x83',
202                                    '--whitelisted', path,
203                                    run_as_root=True,
204                                    root_helper=self._root_helper)
205        return out.strip()
206
207    @staticmethod
208    def is_multipath_running(enforce_multipath,
209                             root_helper,
210                             execute=None) -> bool:
211        try:
212            if execute is None:
213                execute = priv_rootwrap.execute
214            cmd = ('multipathd', 'show', 'status')
215            out, _err = execute(*cmd, run_as_root=True,
216                                root_helper=root_helper)
217            # There was a bug in multipathd where it didn't return an error
218            # code and just printed the error message in stdout.
219            if out and out.startswith('error receiving packet'):
220                raise putils.ProcessExecutionError('', out, 1, cmd, None)
221
222        except putils.ProcessExecutionError as err:
223            if enforce_multipath:
224                LOG.error('multipathd is not running: exit code %(err)s',
225                          {'err': err.exit_code})
226                raise
227            return False
228        return True
229
230    def get_dm_name(self, dm):
231        """Get the Device map name given the device name of the dm on sysfs.
232
233        :param dm: Device map name as seen in sysfs. ie: 'dm-0'
234        :returns: String with the name, or empty string if not available.
235                  ie: '36e843b658476b7ed5bc1d4d10d9b1fde'
236        """
237        try:
238            with open('/sys/block/' + dm + '/dm/name') as f:
239                return f.read().strip()
240        except IOError:
241            return ''
242
243    def find_sysfs_multipath_dm(self, device_names):
244        """Find the dm device name given a list of device names
245
246        :param device_names: Iterable with device names, not paths. ie: ['sda']
247        :returns: String with the dm name or None if not found. ie: 'dm-0'
248        """
249        glob_str = '/sys/block/%s/holders/dm-*'
250        for dev_name in device_names:
251            dms = glob.glob(glob_str % dev_name)
252            if dms:
253                __, device_name, __, dm = dms[0].rsplit('/', 3)
254                return dm
255        return None
256
257    @staticmethod
258    def get_dev_path(connection_properties, device_info):
259        """Determine what path was used by Nova/Cinder to access volume."""
260        if device_info and device_info.get('path'):
261            return device_info.get('path')
262
263        return connection_properties.get('device_path') or ''
264
265    @staticmethod
266    def requires_flush(path, path_used, was_multipath):
267        """Check if a device needs to be flushed when detaching.
268
269        A device representing a single path connection to a volume must only be
270        flushed if it has been used directly by Nova or Cinder to write data.
271
272        If the path has been used via a multipath DM or if the device was part
273        of a multipath but a different single path was used for I/O (instead of
274        the multipath) then we don't need to flush.
275        """
276        # No used path happens on failed attachs, when we don't care about
277        # individual flushes.
278        if not path_used:
279            return False
280
281        path = os.path.realpath(path)
282        path_used = os.path.realpath(path_used)
283
284        # Need to flush this device if we used this specific path.  We check
285        # this before checking if it's multipath in case we don't detect it
286        # being multipath correctly (as in bug #1897787).
287        if path_used == path:
288            return True
289
290        # We flush individual path if Nova didn't use a multipath and we
291        # replaced the symlink to a real device with a link to the decrypted
292        # DM.  We know we replaced it because it doesn't link to /dev/XYZ,
293        # instead it maps to /dev/mapped/crypt-XYZ
294        return not was_multipath and '/dev' != os.path.split(path_used)[0]
295
296    def remove_connection(self, devices_names, force=False, exc=None,
297                          path_used=None, was_multipath=False):
298        """Remove LUNs and multipath associated with devices names.
299
300        :param devices_names: Iterable with real device names ('sda', 'sdb')
301        :param force: Whether to forcefully disconnect even if flush fails.
302        :param exc: ExceptionChainer where to add exceptions if forcing
303        :param path_used: What path was used by Nova/Cinder for I/O
304        :param was_multipath: If the path used for I/O was a multipath
305        :returns: Multipath device map name if found and not flushed
306        """
307        if not devices_names:
308            return
309        exc = exception.ExceptionChainer() if exc is None else exc
310
311        multipath_dm = self.find_sysfs_multipath_dm(devices_names)
312        LOG.debug('Removing %(type)s devices %(devices)s',
313                  {'type': 'multipathed' if multipath_dm else 'single pathed',
314                   'devices': ', '.join(devices_names)})
315        multipath_name = multipath_dm and self.get_dm_name(multipath_dm)
316        if multipath_name:
317            with exc.context(force, 'Flushing %s failed', multipath_name):
318                self.flush_multipath_device(multipath_name)
319                multipath_name = None
320            multipath_running = True
321        else:
322            multipath_running = self.is_multipath_running(
323                enforce_multipath=False, root_helper=self._root_helper)
324
325        for device_name in devices_names:
326            dev_path = '/dev/' + device_name
327            if multipath_running:
328                # Recent multipathd doesn't remove path devices in time when
329                # it receives mutiple udev events in a short span, so here we
330                # tell multipathd to remove the path device immediately.
331                # Even if this step fails, later removing an iscsi device
332                # triggers a udev event and multipathd can remove the path
333                # device based on the udev event
334                self.multipath_del_path(dev_path)
335            flush = self.requires_flush(dev_path, path_used, was_multipath)
336            self.remove_scsi_device(dev_path, force, exc, flush)
337
338        # Wait until the symlinks are removed
339        with exc.context(force, 'Some devices remain from %s', devices_names):
340            try:
341                self.wait_for_volumes_removal(devices_names)
342            finally:
343                # Since we use /dev/disk/by-id/scsi- links to get the wwn we
344                # must ensure they are always removed.
345                self._remove_scsi_symlinks(devices_names)
346        return multipath_name
347
348    def _remove_scsi_symlinks(self, devices_names):
349        devices = ['/dev/' + dev for dev in devices_names]
350        links = glob.glob('/dev/disk/by-id/scsi-*')
351        unlink = []
352        for link in links:
353            try:
354                if os.path.realpath(link) in devices:
355                    unlink.append(link)
356            except OSError:
357                # A race condition in Python's posixpath:realpath just occurred
358                # so we can ignore it because the file was just removed between
359                # a check if file exists and a call to os.readlink
360                continue
361
362        if unlink:
363            priv_rootwrap.unlink_root(no_errors=True, *unlink)
364
365    def flush_device_io(self, device):
366        """This is used to flush any remaining IO in the buffers."""
367        if os.path.exists(device):
368            try:
369                # NOTE(geguileo): With 30% connection error rates flush can get
370                # stuck, set timeout to prevent it from hanging here forever.
371                # Retry twice after 20 and 40 seconds.
372                LOG.debug("Flushing IO for device %s", device)
373                self._execute('blockdev', '--flushbufs', device,
374                              run_as_root=True, attempts=3, timeout=300,
375                              interval=10, root_helper=self._root_helper)
376            except putils.ProcessExecutionError as exc:
377                LOG.warning("Failed to flush IO buffers prior to removing "
378                            "device: %(code)s", {'code': exc.exit_code})
379                raise
380
381    def flush_multipath_device(self, device_map_name):
382        LOG.debug("Flush multipath device %s", device_map_name)
383        # NOTE(geguileo): With 30% connection error rates flush can get stuck,
384        # set timeout to prevent it from hanging here forever.  Retry twice
385        # after 20 and 40 seconds.
386        self._execute('multipath', '-f', device_map_name, run_as_root=True,
387                      attempts=3, timeout=300, interval=10,
388                      root_helper=self._root_helper)
389
390    @utils.retry(exception.VolumeDeviceNotFound)
391    def wait_for_path(self, volume_path):
392        """Wait for a path to show up."""
393        LOG.debug("Checking to see if %s exists yet.",
394                  volume_path)
395        if not os.path.exists(volume_path):
396            LOG.debug("%(path)s doesn't exists yet.", {'path': volume_path})
397            raise exception.VolumeDeviceNotFound(
398                device=volume_path)
399        else:
400            LOG.debug("%s has shown up.", volume_path)
401
402    @utils.retry(exception.BlockDeviceReadOnly, retries=5)
403    def wait_for_rw(self, wwn, device_path):
404        """Wait for block device to be Read-Write."""
405        LOG.debug("Checking to see if %s is read-only.",
406                  device_path)
407        out, info = self._execute('lsblk', '-o', 'NAME,RO', '-l', '-n')
408        LOG.debug("lsblk output: %s", out)
409        blkdevs = out.splitlines()
410        for blkdev in blkdevs:
411            # Entries might look like:
412            #
413            #   "3624a93709a738ed78583fd120013902b (dm-1)  1"
414            #
415            # or
416            #
417            #   "sdd                                       0"
418            #
419            # We are looking for the first and last part of them. For FC
420            # multipath devices the name is in the format of '<WWN> (dm-<ID>)'
421            blkdev_parts = blkdev.split(' ')
422            ro = blkdev_parts[-1]
423            name = blkdev_parts[0]
424
425            # We must validate that all pieces of the dm-# device are rw,
426            # if some are still ro it can cause problems.
427            if wwn in name and int(ro) == 1:
428                LOG.debug("Block device %s is read-only", device_path)
429                self._execute('multipath', '-r', check_exit_code=[0, 1, 21],
430                              run_as_root=True, root_helper=self._root_helper)
431                raise exception.BlockDeviceReadOnly(
432                    device=device_path)
433        else:
434            LOG.debug("Block device %s is not read-only.", device_path)
435
436    def find_multipath_device_path(self, wwn):
437        """Look for the multipath device file for a volume WWN.
438
439        Multipath devices can show up in several places on
440        a linux system.
441
442        1) When multipath friendly names are ON:
443            a device file will show up in
444            /dev/disk/by-id/dm-uuid-mpath-<WWN>
445            /dev/disk/by-id/dm-name-mpath<N>
446            /dev/disk/by-id/scsi-mpath<N>
447            /dev/mapper/mpath<N>
448
449        2) When multipath friendly names are OFF:
450            /dev/disk/by-id/dm-uuid-mpath-<WWN>
451            /dev/disk/by-id/scsi-<WWN>
452            /dev/mapper/<WWN>
453
454        """
455        LOG.info("Find Multipath device file for volume WWN %(wwn)s",
456                 {'wwn': wwn})
457        # First look for the common path
458        wwn_dict = {'wwn': wwn}
459        path = "/dev/disk/by-id/dm-uuid-mpath-%(wwn)s" % wwn_dict
460        try:
461            self.wait_for_path(path)
462            return path
463        except exception.VolumeDeviceNotFound:
464            pass
465
466        # for some reason the common path wasn't found
467        # lets try the dev mapper path
468        path = "/dev/mapper/%(wwn)s" % wwn_dict
469        try:
470            self.wait_for_path(path)
471            return path
472        except exception.VolumeDeviceNotFound:
473            pass
474
475        # couldn't find a path
476        LOG.warning("couldn't find a valid multipath device path for "
477                    "%(wwn)s", wwn_dict)
478        return None
479
480    def find_multipath_device(self, device):
481        """Discover multipath devices for a mpath device.
482
483           This uses the slow multipath -l command to find a
484           multipath device description, then screen scrapes
485           the output to discover the multipath device name
486           and it's devices.
487
488        """
489
490        mdev = None
491        devices = []
492        out = None
493        try:
494            (out, _err) = self._execute('multipath', '-l', device,
495                                        run_as_root=True,
496                                        root_helper=self._root_helper)
497        except putils.ProcessExecutionError as exc:
498            LOG.warning("multipath call failed exit %(code)s",
499                        {'code': exc.exit_code})
500            raise exception.CommandExecutionFailed(
501                cmd='multipath -l %s' % device)
502
503        if out:
504            lines_str = out.strip()
505            lines = lines_str.split("\n")
506            lines = [line for line in lines
507                     if not re.match(MULTIPATH_ERROR_REGEX, line) and
508                     len(line)]
509            if lines:
510
511                mdev_name = lines[0].split(" ")[0]
512
513                if mdev_name in MULTIPATH_DEVICE_ACTIONS:
514                    mdev_name = lines[0].split(" ")[1]
515
516                mdev = '/dev/mapper/%s' % mdev_name
517
518                # Confirm that the device is present.
519                try:
520                    os.stat(mdev)
521                except OSError:
522                    LOG.warning("Couldn't find multipath device %s",
523                                mdev)
524                    return None
525
526                wwid_search = MULTIPATH_WWID_REGEX.search(lines[0])
527                if wwid_search is not None:
528                    mdev_id = wwid_search.group('wwid')
529                else:
530                    mdev_id = mdev_name
531
532                LOG.debug("Found multipath device = %(mdev)s",
533                          {'mdev': mdev})
534                device_lines = lines[3:]
535                for dev_line in device_lines:
536                    if dev_line.find("policy") != -1:
537                        continue
538
539                    dev_line = dev_line.lstrip(' |-`')
540                    dev_info = dev_line.split()
541                    address = dev_info[0].split(":")
542
543                    dev = {'device': '/dev/%s' % dev_info[1],
544                           'host': address[0], 'channel': address[1],
545                           'id': address[2], 'lun': address[3]
546                           }
547
548                    devices.append(dev)
549
550        if mdev is not None:
551            info = {"device": mdev,
552                    "id": mdev_id,
553                    "name": mdev_name,
554                    "devices": devices}
555            return info
556        return None
557
558    def get_device_size(self, device):
559        """Get the size in bytes of a volume."""
560        (out, _err) = self._execute('blockdev', '--getsize64',
561                                    device, run_as_root=True,
562                                    root_helper=self._root_helper)
563        var = str(out.strip())
564        if var.isnumeric():
565            return int(var)
566        else:
567            return None
568
569    def multipath_reconfigure(self):
570        """Issue a multipathd reconfigure.
571
572        When attachments come and go, the multipathd seems
573        to get lost and not see the maps.  This causes
574        resize map to fail 100%.  To overcome this we have
575        to issue a reconfigure prior to resize map.
576        """
577        (out, _err) = self._execute('multipathd', 'reconfigure',
578                                    run_as_root=True,
579                                    root_helper=self._root_helper)
580        return out
581
582    def multipath_resize_map(self, mpath_id):
583        """Issue a multipath resize map on device.
584
585        This forces the multipath daemon to update it's
586        size information a particular multipath device.
587        """
588        (out, _err) = self._execute('multipathd', 'resize', 'map', mpath_id,
589                                    run_as_root=True,
590                                    root_helper=self._root_helper)
591        return out
592
593    def extend_volume(self, volume_paths, use_multipath=False):
594        """Signal the SCSI subsystem to test for volume resize.
595
596        This function tries to signal the local system's kernel
597        that an already attached volume might have been resized.
598        """
599        LOG.debug("extend volume %s", volume_paths)
600
601        for volume_path in volume_paths:
602            device = self.get_device_info(volume_path)
603            LOG.debug("Volume device info = %s", device)
604            device_id = ("%(host)s:%(channel)s:%(id)s:%(lun)s" %
605                         {'host': device['host'],
606                          'channel': device['channel'],
607                          'id': device['id'],
608                          'lun': device['lun']})
609
610            scsi_path = ("/sys/bus/scsi/drivers/sd/%(device_id)s" %
611                         {'device_id': device_id})
612
613            size = self.get_device_size(volume_path)
614            LOG.debug("Starting size: %s", size)
615
616            # now issue the device rescan
617            rescan_path = "%(scsi_path)s/rescan" % {'scsi_path': scsi_path}
618            self.echo_scsi_command(rescan_path, "1")
619            new_size = self.get_device_size(volume_path)
620            LOG.debug("volume size after scsi device rescan %s", new_size)
621
622        scsi_wwn = self.get_scsi_wwn(volume_paths[0])
623        if use_multipath:
624            mpath_device = self.find_multipath_device_path(scsi_wwn)
625            if mpath_device:
626                # Force a reconfigure so that resize works
627                self.multipath_reconfigure()
628
629                size = self.get_device_size(mpath_device)
630                LOG.info("mpath(%(device)s) current size %(size)s",
631                         {'device': mpath_device, 'size': size})
632                result = self.multipath_resize_map(scsi_wwn)
633                if 'fail' in result:
634                    LOG.error("Multipathd failed to update the size mapping "
635                              "of multipath device %(scsi_wwn)s volume "
636                              "%(volume)s",
637                              {'scsi_wwn': scsi_wwn, 'volume': volume_paths})
638                    return None
639
640                new_size = self.get_device_size(mpath_device)
641                LOG.info("mpath(%(device)s) new size %(size)s",
642                         {'device': mpath_device, 'size': new_size})
643
644        return new_size
645
646    def process_lun_id(self, lun_ids):
647        if isinstance(lun_ids, list):
648            processed = []
649            for x in lun_ids:
650                x = self._format_lun_id(x)
651                processed.append(x)
652        else:
653            processed = self._format_lun_id(lun_ids)
654        return processed
655
656    def _format_lun_id(self, lun_id):
657        # make sure lun_id is an int
658        lun_id = int(lun_id)
659        if lun_id < 256:
660            return lun_id
661        else:
662            return ("0x%04x%04x00000000" %
663                    (lun_id & 0xffff, lun_id >> 16 & 0xffff))
664
665    def get_hctl(self, session, lun):
666        """Given an iSCSI session return the host, channel, target, and lun."""
667        glob_str = '/sys/class/iscsi_host/host*/device/session' + session
668        paths = glob.glob(glob_str + '/target*')
669        if paths:
670            __, channel, target = os.path.split(paths[0])[1].split(':')
671        # Check if we can get the host
672        else:
673            target = channel = '-'
674            paths = glob.glob(glob_str)
675
676        if not paths:
677            LOG.debug('No hctl found on session %s with lun %s', session, lun)
678            return None
679
680        # Extract the host number from the path
681        host = paths[0][26:paths[0].index('/', 26)]
682        res = (host, channel, target, lun)
683        LOG.debug('HCTL %s found on session %s with lun %s', res, session, lun)
684        return res
685
686    def device_name_by_hctl(self, session, hctl):
687        """Find the device name given a session and the hctl.
688
689        :param session: A string with the session number
690        :param hctl: An iterable with the host, channel, target, and lun as
691                     passed to scan.  ie: ('5', '-', '-', '0')
692        """
693        if '-' in hctl:
694            hctl = ['*' if x == '-' else x for x in hctl]
695        path = ('/sys/class/scsi_host/host%(h)s/device/session%(s)s/target'
696                '%(h)s:%(c)s:%(t)s/%(h)s:%(c)s:%(t)s:%(l)s/block/*' %
697                {'h': hctl[0], 'c': hctl[1], 't': hctl[2], 'l': hctl[3],
698                 's': session})
699        # Sort devices and return the first so we don't return a partition
700        devices = sorted(glob.glob(path))
701        device = os.path.split(devices[0])[1] if devices else None
702        LOG.debug('Searching for a device in session %s and hctl %s yield: %s',
703                  session, hctl, device)
704        return device
705
706    def scan_iscsi(self, host, channel='-', target='-', lun='-'):
707        """Send an iSCSI scan request given the host and optionally the ctl."""
708        LOG.debug('Scanning host %(host)s c: %(channel)s, '
709                  't: %(target)s, l: %(lun)s)',
710                  {'host': host, 'channel': channel,
711                   'target': target, 'lun': lun})
712        self.echo_scsi_command('/sys/class/scsi_host/host%s/scan' % host,
713                               '%(c)s %(t)s %(l)s' % {'c': channel,
714                                                      't': target,
715                                                      'l': lun})
716
717    def multipath_add_wwid(self, wwid):
718        """Add a wwid to the list of know multipath wwids.
719
720        This has the effect of multipathd being willing to create a dm for a
721        multipath even when there's only 1 device.
722        """
723        out, err = self._execute('multipath', '-a', wwid,
724                                 run_as_root=True,
725                                 check_exit_code=False,
726                                 root_helper=self._root_helper)
727        return out.strip() == "wwid '" + wwid + "' added"
728
729    def multipath_add_path(self, realpath):
730        """Add a path to multipathd for monitoring.
731
732        This has the effect of multipathd checking an already checked device
733        for multipath.
734
735        Together with `multipath_add_wwid` we can create a multipath when
736        there's only 1 path.
737        """
738        stdout, stderr = self._execute('multipathd', 'add', 'path', realpath,
739                                       run_as_root=True, timeout=5,
740                                       check_exit_code=False,
741                                       root_helper=self._root_helper)
742        return stdout.strip() == 'ok'
743
744    def multipath_del_path(self, realpath):
745        """Remove a path from multipathd for monitoring."""
746        stdout, stderr = self._execute('multipathd', 'del', 'path', realpath,
747                                       run_as_root=True, timeout=5,
748                                       check_exit_code=False,
749                                       root_helper=self._root_helper)
750        return stdout.strip() == 'ok'
751