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