]> git.proxmox.com Git - ceph.git/blob - ceph/src/ceph-volume/ceph_volume/util/disk.py
d2459e1208674d6b49e63348b29138f4350d315e
[ceph.git] / ceph / src / ceph-volume / ceph_volume / util / disk.py
1 import logging
2 import os
3 import re
4 import stat
5 from ceph_volume import process
6 from ceph_volume.api import lvm
7 from ceph_volume.util.system import get_file_contents
8
9
10 logger = logging.getLogger(__name__)
11
12
13 # The blkid CLI tool has some oddities which prevents having one common call
14 # to extract the information instead of having separate utilities. The `udev`
15 # type of output is needed in older versions of blkid (v 2.23) that will not
16 # work correctly with just the ``-p`` flag to bypass the cache for example.
17 # Xenial doesn't have this problem as it uses a newer blkid version.
18
19
20 def get_partuuid(device):
21 """
22 If a device is a partition, it will probably have a PARTUUID on it that
23 will persist and can be queried against `blkid` later to detect the actual
24 device
25 """
26 out, err, rc = process.call(
27 ['blkid', '-c', '/dev/null', '-s', 'PARTUUID', '-o', 'value', device]
28 )
29 return ' '.join(out).strip()
30
31
32 def _blkid_parser(output):
33 """
34 Parses the output from a system ``blkid`` call, requires output to be
35 produced using the ``-p`` flag which bypasses the cache, mangling the
36 names. These names are corrected to what it would look like without the
37 ``-p`` flag.
38
39 Normal output::
40
41 /dev/sdb1: UUID="62416664-cbaf-40bd-9689-10bd337379c3" TYPE="xfs" [...]
42 """
43 # first spaced separated item is garbage, gets tossed:
44 output = ' '.join(output.split()[1:])
45 # split again, respecting possible whitespace in quoted values
46 pairs = output.split('" ')
47 raw = {}
48 processed = {}
49 mapping = {
50 'UUID': 'UUID',
51 'TYPE': 'TYPE',
52 'PART_ENTRY_NAME': 'PARTLABEL',
53 'PART_ENTRY_UUID': 'PARTUUID',
54 'PART_ENTRY_TYPE': 'PARTTYPE',
55 'PTTYPE': 'PTTYPE',
56 }
57
58 for pair in pairs:
59 try:
60 column, value = pair.split('=')
61 except ValueError:
62 continue
63 raw[column] = value.strip().strip().strip('"')
64
65 for key, value in raw.items():
66 new_key = mapping.get(key)
67 if not new_key:
68 continue
69 processed[new_key] = value
70
71 return processed
72
73
74 def blkid(device):
75 """
76 The blkid interface to its CLI, creating an output similar to what is
77 expected from ``lsblk``. In most cases, ``lsblk()`` should be the preferred
78 method for extracting information about a device. There are some corner
79 cases where it might provide information that is otherwise unavailable.
80
81 The system call uses the ``-p`` flag which bypasses the cache, the caveat
82 being that the keys produced are named completely different to expected
83 names.
84
85 For example, instead of ``PARTLABEL`` it provides a ``PART_ENTRY_NAME``.
86 A bit of translation between these known keys is done, which is why
87 ``lsblk`` should always be preferred: the output provided here is not as
88 rich, given that a translation of keys is required for a uniform interface
89 with the ``-p`` flag.
90
91 Label name to expected output chart:
92
93 cache bypass name expected name
94
95 UUID UUID
96 TYPE TYPE
97 PART_ENTRY_NAME PARTLABEL
98 PART_ENTRY_UUID PARTUUID
99 """
100 out, err, rc = process.call(
101 ['blkid', '-c', '/dev/null', '-p', device]
102 )
103 return _blkid_parser(' '.join(out))
104
105
106 def get_part_entry_type(device):
107 """
108 Parses the ``ID_PART_ENTRY_TYPE`` from the "low level" (bypasses the cache)
109 output that uses the ``udev`` type of output. This output is intended to be
110 used for udev rules, but it is useful in this case as it is the only
111 consistent way to retrieve the GUID used by ceph-disk to identify devices.
112 """
113 out, err, rc = process.call(['blkid', '-c', '/dev/null', '-p', '-o', 'udev', device])
114 for line in out:
115 if 'ID_PART_ENTRY_TYPE=' in line:
116 return line.split('=')[-1].strip()
117 return ''
118
119
120 def get_device_from_partuuid(partuuid):
121 """
122 If a device has a partuuid, query blkid so that it can tell us what that
123 device is
124 """
125 out, err, rc = process.call(
126 ['blkid', '-c', '/dev/null', '-t', 'PARTUUID="%s"' % partuuid, '-o', 'device']
127 )
128 return ' '.join(out).strip()
129
130
131 def remove_partition(device):
132 """
133 Removes a partition using parted
134
135 :param device: A ``Device()`` object
136 """
137 udev_info = udevadm_property(device.path)
138 partition_number = udev_info.get('ID_PART_ENTRY_NUMBER')
139 if not partition_number:
140 raise RuntimeError('Unable to detect the partition number for device: %s' % device.path)
141
142 process.run(
143 ['parted', device.parent_device, '--script', '--', 'rm', partition_number]
144 )
145
146
147 def _stat_is_device(stat_obj):
148 """
149 Helper function that will interpret ``os.stat`` output directly, so that other
150 functions can call ``os.stat`` once and interpret that result several times
151 """
152 return stat.S_ISBLK(stat_obj)
153
154
155 def _lsblk_parser(line):
156 """
157 Parses lines in lsblk output. Requires output to be in pair mode (``-P`` flag). Lines
158 need to be whole strings, the line gets split when processed.
159
160 :param line: A string, with the full line from lsblk output
161 """
162 # parse the COLUMN="value" output to construct the dictionary
163 pairs = line.split('" ')
164 parsed = {}
165 for pair in pairs:
166 try:
167 column, value = pair.split('=')
168 except ValueError:
169 continue
170 parsed[column] = value.strip().strip().strip('"')
171 return parsed
172
173
174 def device_family(device):
175 """
176 Returns a list of associated devices. It assumes that ``device`` is
177 a parent device. It is up to the caller to ensure that the device being
178 used is a parent, not a partition.
179 """
180 labels = ['NAME', 'PARTLABEL', 'TYPE']
181 command = ['lsblk', '-P', '-p', '-o', ','.join(labels), device]
182 out, err, rc = process.call(command)
183 devices = []
184 for line in out:
185 devices.append(_lsblk_parser(line))
186
187 return devices
188
189
190 def udevadm_property(device, properties=[]):
191 """
192 Query udevadm for information about device properties.
193 Optionally pass a list of properties to return. A requested property might
194 not be returned if not present.
195
196 Expected output format::
197 # udevadm info --query=property --name=/dev/sda :(
198 DEVNAME=/dev/sda
199 DEVTYPE=disk
200 ID_ATA=1
201 ID_BUS=ata
202 ID_MODEL=SK_hynix_SC311_SATA_512GB
203 ID_PART_TABLE_TYPE=gpt
204 ID_PART_TABLE_UUID=c8f91d57-b26c-4de1-8884-0c9541da288c
205 ID_PATH=pci-0000:00:17.0-ata-3
206 ID_PATH_TAG=pci-0000_00_17_0-ata-3
207 ID_REVISION=70000P10
208 ID_SERIAL=SK_hynix_SC311_SATA_512GB_MS83N71801150416A
209 TAGS=:systemd:
210 USEC_INITIALIZED=16117769
211 ...
212 """
213 out = _udevadm_info(device)
214 ret = {}
215 for line in out:
216 p, v = line.split('=', 1)
217 if not properties or p in properties:
218 ret[p] = v
219 return ret
220
221
222 def _udevadm_info(device):
223 """
224 Call udevadm and return the output
225 """
226 cmd = ['udevadm', 'info', '--query=property', device]
227 out, _err, _rc = process.call(cmd)
228 return out
229
230
231 def lsblk(device, columns=None, abspath=False):
232 return lsblk_all(device=device,
233 columns=columns,
234 abspath=abspath)
235
236 def lsblk_all(device='', columns=None, abspath=False):
237 """
238 Create a dictionary of identifying values for a device using ``lsblk``.
239 Each supported column is a key, in its *raw* format (all uppercase
240 usually). ``lsblk`` has support for certain "columns" (in blkid these
241 would be labels), and these columns vary between distributions and
242 ``lsblk`` versions. The newer versions support a richer set of columns,
243 while older ones were a bit limited.
244
245 These are a subset of lsblk columns which are known to work on both CentOS 7 and Xenial:
246
247 NAME device name
248 KNAME internal kernel device name
249 PKNAME internal kernel parent device name
250 MAJ:MIN major:minor device number
251 FSTYPE filesystem type
252 MOUNTPOINT where the device is mounted
253 LABEL filesystem LABEL
254 UUID filesystem UUID
255 RO read-only device
256 RM removable device
257 MODEL device identifier
258 SIZE size of the device
259 STATE state of the device
260 OWNER user name
261 GROUP group name
262 MODE device node permissions
263 ALIGNMENT alignment offset
264 MIN-IO minimum I/O size
265 OPT-IO optimal I/O size
266 PHY-SEC physical sector size
267 LOG-SEC logical sector size
268 ROTA rotational device
269 SCHED I/O scheduler name
270 RQ-SIZE request queue size
271 TYPE device type
272 PKNAME internal parent kernel device name
273 DISC-ALN discard alignment offset
274 DISC-GRAN discard granularity
275 DISC-MAX discard max bytes
276 DISC-ZERO discard zeroes data
277
278 There is a bug in ``lsblk`` where using all the available (supported)
279 columns will result in no output (!), in order to workaround this the
280 following columns have been removed from the default reporting columns:
281
282 * RQ-SIZE (request queue size)
283 * MIN-IO minimum I/O size
284 * OPT-IO optimal I/O size
285
286 These should be available however when using `columns`. For example::
287
288 >>> lsblk('/dev/sda1', columns=['OPT-IO'])
289 {'OPT-IO': '0'}
290
291 Normal CLI output, as filtered by the flags in this function will look like ::
292
293 $ lsblk -P -o NAME,KNAME,PKNAME,MAJ:MIN,FSTYPE,MOUNTPOINT
294 NAME="sda1" KNAME="sda1" MAJ:MIN="8:1" FSTYPE="ext4" MOUNTPOINT="/"
295
296 :param columns: A list of columns to report as keys in its original form.
297 :param abspath: Set the flag for absolute paths on the report
298 """
299 default_columns = [
300 'NAME', 'KNAME', 'PKNAME', 'MAJ:MIN', 'FSTYPE', 'MOUNTPOINT', 'LABEL',
301 'UUID', 'RO', 'RM', 'MODEL', 'SIZE', 'STATE', 'OWNER', 'GROUP', 'MODE',
302 'ALIGNMENT', 'PHY-SEC', 'LOG-SEC', 'ROTA', 'SCHED', 'TYPE', 'DISC-ALN',
303 'DISC-GRAN', 'DISC-MAX', 'DISC-ZERO', 'PKNAME', 'PARTLABEL'
304 ]
305 columns = columns or default_columns
306 # -P -> Produce pairs of COLUMN="value"
307 # -p -> Return full paths to devices, not just the names, when ``abspath`` is set
308 # -o -> Use the columns specified or default ones provided by this function
309 base_command = ['lsblk', '-P']
310 if abspath:
311 base_command.append('-p')
312 base_command.append('-o')
313 base_command.append(','.join(columns))
314
315 out, err, rc = process.call(base_command)
316
317 if rc != 0:
318 raise RuntimeError(f"Error: {err}")
319
320 result = []
321
322 for line in out:
323 result.append(_lsblk_parser(line))
324
325 if not device:
326 return result
327
328 for dev in result:
329 if dev['NAME'] == os.path.basename(device):
330 return dev
331
332 return {}
333
334 def is_device(dev):
335 """
336 Boolean to determine if a given device is a block device (**not**
337 a partition!)
338
339 For example: /dev/sda would return True, but not /dev/sdc1
340 """
341 if not os.path.exists(dev):
342 return False
343 if not dev.startswith('/dev/'):
344 return False
345 if dev[len('/dev/'):].startswith('loop'):
346 if not allow_loop_devices():
347 return False
348
349 # fallback to stat
350 return _stat_is_device(os.lstat(dev).st_mode)
351
352
353 def is_partition(dev):
354 """
355 Boolean to determine if a given device is a partition, like /dev/sda1
356 """
357 if not os.path.exists(dev):
358 return False
359 # use lsblk first, fall back to using stat
360 TYPE = lsblk(dev).get('TYPE')
361 if TYPE:
362 return TYPE == 'part'
363
364 # fallback to stat
365 stat_obj = os.stat(dev)
366 if _stat_is_device(stat_obj.st_mode):
367 return False
368
369 major = os.major(stat_obj.st_rdev)
370 minor = os.minor(stat_obj.st_rdev)
371 if os.path.exists('/sys/dev/block/%d:%d/partition' % (major, minor)):
372 return True
373 return False
374
375
376 def is_ceph_rbd(dev):
377 """
378 Boolean to determine if a given device is a ceph RBD device, like /dev/rbd0
379 """
380 return dev.startswith(('/dev/rbd'))
381
382
383 class BaseFloatUnit(float):
384 """
385 Base class to support float representations of size values. Suffix is
386 computed on child classes by inspecting the class name
387 """
388
389 def __repr__(self):
390 return "<%s(%s)>" % (self.__class__.__name__, self.__float__())
391
392 def __str__(self):
393 return "{size:.2f} {suffix}".format(
394 size=self.__float__(),
395 suffix=self.__class__.__name__.split('Float')[-1]
396 )
397
398 def as_int(self):
399 return int(self.real)
400
401 def as_float(self):
402 return self.real
403
404
405 class FloatB(BaseFloatUnit):
406 pass
407
408
409 class FloatMB(BaseFloatUnit):
410 pass
411
412
413 class FloatGB(BaseFloatUnit):
414 pass
415
416
417 class FloatKB(BaseFloatUnit):
418 pass
419
420
421 class FloatTB(BaseFloatUnit):
422 pass
423
424 class FloatPB(BaseFloatUnit):
425 pass
426
427 class Size(object):
428 """
429 Helper to provide an interface for different sizes given a single initial
430 input. Allows for comparison between different size objects, which avoids
431 the need to convert sizes before comparison (e.g. comparing megabytes
432 against gigabytes).
433
434 Common comparison operators are supported::
435
436 >>> hd1 = Size(gb=400)
437 >>> hd2 = Size(gb=500)
438 >>> hd1 > hd2
439 False
440 >>> hd1 < hd2
441 True
442 >>> hd1 == hd2
443 False
444 >>> hd1 == Size(gb=400)
445 True
446
447 The Size object can also be multiplied or divided::
448
449 >>> hd1
450 <Size(400.00 GB)>
451 >>> hd1 * 2
452 <Size(800.00 GB)>
453 >>> hd1
454 <Size(800.00 GB)>
455
456 Additions and subtractions are only supported between Size objects::
457
458 >>> Size(gb=224) - Size(gb=100)
459 <Size(124.00 GB)>
460 >>> Size(gb=1) + Size(mb=300)
461 <Size(1.29 GB)>
462
463 Can also display a human-readable representation, with automatic detection
464 on best suited unit, or alternatively, specific unit representation::
465
466 >>> s = Size(mb=2211)
467 >>> s
468 <Size(2.16 GB)>
469 >>> s.mb
470 <FloatMB(2211.0)>
471 >>> print("Total size: %s" % s.mb)
472 Total size: 2211.00 MB
473 >>> print("Total size: %s" % s)
474 Total size: 2.16 GB
475 """
476
477 @classmethod
478 def parse(cls, size):
479 if (len(size) > 2 and
480 size[-2].lower() in ['k', 'm', 'g', 't', 'p'] and
481 size[-1].lower() == 'b'):
482 return cls(**{size[-2:].lower(): float(size[0:-2])})
483 elif size[-1].lower() in ['b', 'k', 'm', 'g', 't', 'p']:
484 return cls(**{size[-1].lower(): float(size[0:-1])})
485 else:
486 return cls(b=float(size))
487
488
489 def __init__(self, multiplier=1024, **kw):
490 self._multiplier = multiplier
491 # create a mapping of units-to-multiplier, skip bytes as that is
492 # calculated initially always and does not need to convert
493 aliases = [
494 [('k', 'kb', 'kilobytes'), self._multiplier],
495 [('m', 'mb', 'megabytes'), self._multiplier ** 2],
496 [('g', 'gb', 'gigabytes'), self._multiplier ** 3],
497 [('t', 'tb', 'terabytes'), self._multiplier ** 4],
498 [('p', 'pb', 'petabytes'), self._multiplier ** 5]
499 ]
500 # and mappings for units-to-formatters, including bytes and aliases for
501 # each
502 format_aliases = [
503 [('b', 'bytes'), FloatB],
504 [('kb', 'kilobytes'), FloatKB],
505 [('mb', 'megabytes'), FloatMB],
506 [('gb', 'gigabytes'), FloatGB],
507 [('tb', 'terabytes'), FloatTB],
508 [('pb', 'petabytes'), FloatPB],
509 ]
510 self._formatters = {}
511 for key, value in format_aliases:
512 for alias in key:
513 self._formatters[alias] = value
514 self._factors = {}
515 for key, value in aliases:
516 for alias in key:
517 self._factors[alias] = value
518
519 for k, v in kw.items():
520 self._convert(v, k)
521 # only pursue the first occurrence
522 break
523
524 def _convert(self, size, unit):
525 """
526 Convert any size down to bytes so that other methods can rely on bytes
527 being available always, regardless of what they pass in, avoiding the
528 need for a mapping of every permutation.
529 """
530 if unit in ['b', 'bytes']:
531 self._b = size
532 return
533 factor = self._factors[unit]
534 self._b = float(size * factor)
535
536 def _get_best_format(self):
537 """
538 Go through all the supported units, and use the first one that is less
539 than 1024. This allows to represent size in the most readable format
540 available
541 """
542 for unit in ['b', 'kb', 'mb', 'gb', 'tb', 'pb']:
543 if getattr(self, unit) > 1024:
544 continue
545 return getattr(self, unit)
546
547 def __repr__(self):
548 return "<Size(%s)>" % self._get_best_format()
549
550 def __str__(self):
551 return "%s" % self._get_best_format()
552
553 def __format__(self, spec):
554 return str(self._get_best_format()).__format__(spec)
555
556 def __int__(self):
557 return int(self._b)
558
559 def __float__(self):
560 return self._b
561
562 def __lt__(self, other):
563 if isinstance(other, Size):
564 return self._b < other._b
565 else:
566 return self.b < other
567
568 def __le__(self, other):
569 if isinstance(other, Size):
570 return self._b <= other._b
571 else:
572 return self.b <= other
573
574 def __eq__(self, other):
575 if isinstance(other, Size):
576 return self._b == other._b
577 else:
578 return self.b == other
579
580 def __ne__(self, other):
581 if isinstance(other, Size):
582 return self._b != other._b
583 else:
584 return self.b != other
585
586 def __ge__(self, other):
587 if isinstance(other, Size):
588 return self._b >= other._b
589 else:
590 return self.b >= other
591
592 def __gt__(self, other):
593 if isinstance(other, Size):
594 return self._b > other._b
595 else:
596 return self.b > other
597
598 def __add__(self, other):
599 if isinstance(other, Size):
600 _b = self._b + other._b
601 return Size(b=_b)
602 raise TypeError('Cannot add "Size" object with int')
603
604 def __sub__(self, other):
605 if isinstance(other, Size):
606 _b = self._b - other._b
607 return Size(b=_b)
608 raise TypeError('Cannot subtract "Size" object from int')
609
610 def __mul__(self, other):
611 if isinstance(other, Size):
612 raise TypeError('Cannot multiply with "Size" object')
613 _b = self._b * other
614 return Size(b=_b)
615
616 def __truediv__(self, other):
617 if isinstance(other, Size):
618 return self._b / other._b
619 _b = self._b / other
620 return Size(b=_b)
621
622 def __div__(self, other):
623 if isinstance(other, Size):
624 return self._b / other._b
625 _b = self._b / other
626 return Size(b=_b)
627
628 def __bool__(self):
629 return self.b != 0
630
631 def __nonzero__(self):
632 return self.__bool__()
633
634 def __getattr__(self, unit):
635 """
636 Calculate units on the fly, relies on the fact that ``bytes`` has been
637 converted at instantiation. Units that don't exist will trigger an
638 ``AttributeError``
639 """
640 try:
641 formatter = self._formatters[unit]
642 except KeyError:
643 raise AttributeError('Size object has not attribute "%s"' % unit)
644 if unit in ['b', 'bytes']:
645 return formatter(self._b)
646 try:
647 factor = self._factors[unit]
648 except KeyError:
649 raise AttributeError('Size object has not attribute "%s"' % unit)
650 return formatter(float(self._b) / factor)
651
652
653 def human_readable_size(size):
654 """
655 Take a size in bytes, and transform it into a human readable size with up
656 to two decimals of precision.
657 """
658 suffixes = ['B', 'KB', 'MB', 'GB', 'TB', 'PB']
659 for suffix in suffixes:
660 if size >= 1024:
661 size = size / 1024
662 else:
663 break
664 return "{size:.2f} {suffix}".format(
665 size=size,
666 suffix=suffix)
667
668
669 def size_from_human_readable(s):
670 """
671 Takes a human readable string and converts into a Size. If no unit is
672 passed, bytes is assumed.
673 """
674 s = s.replace(' ', '')
675 if s[-1].isdigit():
676 return Size(b=float(s))
677 n = float(s[:-1])
678 if s[-1].lower() == 'p':
679 return Size(pb=n)
680 if s[-1].lower() == 't':
681 return Size(tb=n)
682 if s[-1].lower() == 'g':
683 return Size(gb=n)
684 if s[-1].lower() == 'm':
685 return Size(mb=n)
686 if s[-1].lower() == 'k':
687 return Size(kb=n)
688 return None
689
690
691 def get_partitions_facts(sys_block_path):
692 partition_metadata = {}
693 for folder in os.listdir(sys_block_path):
694 folder_path = os.path.join(sys_block_path, folder)
695 if os.path.exists(os.path.join(folder_path, 'partition')):
696 contents = get_file_contents(os.path.join(folder_path, 'partition'))
697 if contents:
698 part = {}
699 partname = folder
700 part_sys_block_path = os.path.join(sys_block_path, partname)
701
702 part['start'] = get_file_contents(part_sys_block_path + "/start", 0)
703 part['sectors'] = get_file_contents(part_sys_block_path + "/size", 0)
704
705 part['sectorsize'] = get_file_contents(
706 part_sys_block_path + "/queue/logical_block_size")
707 if not part['sectorsize']:
708 part['sectorsize'] = get_file_contents(
709 part_sys_block_path + "/queue/hw_sector_size", 512)
710 part['size'] = float(part['sectors']) * 512
711 part['human_readable_size'] = human_readable_size(float(part['sectors']) * 512)
712 part['holders'] = []
713 for holder in os.listdir(part_sys_block_path + '/holders'):
714 part['holders'].append(holder)
715
716 partition_metadata[partname] = part
717 return partition_metadata
718
719
720 def is_mapper_device(device_name):
721 return device_name.startswith(('/dev/mapper', '/dev/dm-'))
722
723
724 def is_locked_raw_device(disk_path):
725 """
726 A device can be locked by a third party software like a database.
727 To detect that case, the device is opened in Read/Write and exclusive mode
728 """
729 open_flags = (os.O_RDWR | os.O_EXCL)
730 open_mode = 0
731 fd = None
732
733 try:
734 fd = os.open(disk_path, open_flags, open_mode)
735 except OSError:
736 return 1
737
738 try:
739 os.close(fd)
740 except OSError:
741 return 1
742
743 return 0
744
745
746 class AllowLoopDevices(object):
747 allow = False
748 warned = False
749
750 @classmethod
751 def __call__(cls):
752 val = os.environ.get("CEPH_VOLUME_ALLOW_LOOP_DEVICES", "false").lower()
753 if val not in ("false", 'no', '0'):
754 cls.allow = True
755 if not cls.warned:
756 logger.warning(
757 "CEPH_VOLUME_ALLOW_LOOP_DEVICES is set in your "
758 "environment, so we will allow the use of unattached loop"
759 " devices as disks. This feature is intended for "
760 "development purposes only and will never be supported in"
761 " production. Issues filed based on this behavior will "
762 "likely be ignored."
763 )
764 cls.warned = True
765 return cls.allow
766
767
768 allow_loop_devices = AllowLoopDevices()
769
770
771 def get_block_devs_sysfs(_sys_block_path='/sys/block', _sys_dev_block_path='/sys/dev/block'):
772 def holder_inner_loop():
773 for holder in holders:
774 # /sys/block/sdy/holders/dm-8/dm/uuid
775 holder_dm_type = get_file_contents(os.path.join(_sys_block_path, dev, f'holders/{holder}/dm/uuid')).split('-')[0].lower()
776 if holder_dm_type == 'mpath':
777 return True
778
779 # First, get devices that are _not_ partitions
780 result = list()
781 dev_names = os.listdir(_sys_block_path)
782 for dev in dev_names:
783 name = kname = os.path.join("/dev", dev)
784 if not os.path.exists(name):
785 continue
786 type_ = 'disk'
787 holders = os.listdir(os.path.join(_sys_block_path, dev, 'holders'))
788 if get_file_contents(os.path.join(_sys_block_path, dev, 'removable')) == "1":
789 continue
790 if holder_inner_loop():
791 continue
792 dm_dir_path = os.path.join(_sys_block_path, dev, 'dm')
793 if os.path.isdir(dm_dir_path):
794 dm_type = get_file_contents(os.path.join(dm_dir_path, 'uuid'))
795 type_ = dm_type.split('-')[0].lower()
796 basename = get_file_contents(os.path.join(dm_dir_path, 'name'))
797 name = os.path.join("/dev/mapper", basename)
798 if dev.startswith('loop'):
799 if not allow_loop_devices():
800 continue
801 # Skip loop devices that are not attached
802 if not os.path.exists(os.path.join(_sys_block_path, dev, 'loop')):
803 continue
804 type_ = 'loop'
805 result.append([kname, name, type_])
806 # Next, look for devices that _are_ partitions
807 for item in os.listdir(_sys_dev_block_path):
808 is_part = get_file_contents(os.path.join(_sys_dev_block_path, item, 'partition')) == "1"
809 dev = os.path.basename(os.readlink(os.path.join(_sys_dev_block_path, item)))
810 if not is_part:
811 continue
812 name = kname = os.path.join("/dev", dev)
813 result.append([name, kname, "part"])
814 return sorted(result, key=lambda x: x[0])
815
816
817 def get_devices(_sys_block_path='/sys/block', device=''):
818 """
819 Captures all available block devices as reported by lsblk.
820 Additional interesting metadata like sectors, size, vendor,
821 solid/rotational, etc. is collected from /sys/block/<device>
822
823 Returns a dictionary, where keys are the full paths to devices.
824
825 ..note:: loop devices, removable media, and logical volumes are never included.
826 """
827
828 device_facts = {}
829
830 block_devs = get_block_devs_sysfs(_sys_block_path)
831
832 block_types = ['disk', 'mpath']
833 if allow_loop_devices():
834 block_types.append('loop')
835
836 for block in block_devs:
837 devname = os.path.basename(block[0])
838 diskname = block[1]
839 if block[2] not in block_types:
840 continue
841 sysdir = os.path.join(_sys_block_path, devname)
842 metadata = {}
843
844 # If the device is ceph rbd it gets excluded
845 if is_ceph_rbd(diskname):
846 continue
847
848 # If the mapper device is a logical volume it gets excluded
849 if is_mapper_device(diskname):
850 if lvm.get_device_lvs(diskname):
851 continue
852
853 # all facts that have no defaults
854 # (<name>, <path relative to _sys_block_path>)
855 facts = [('removable', 'removable'),
856 ('ro', 'ro'),
857 ('vendor', 'device/vendor'),
858 ('model', 'device/model'),
859 ('rev', 'device/rev'),
860 ('sas_address', 'device/sas_address'),
861 ('sas_device_handle', 'device/sas_device_handle'),
862 ('support_discard', 'queue/discard_granularity'),
863 ('rotational', 'queue/rotational'),
864 ('nr_requests', 'queue/nr_requests'),
865 ]
866 for key, file_ in facts:
867 metadata[key] = get_file_contents(os.path.join(sysdir, file_))
868
869 device_slaves = os.listdir(os.path.join(sysdir, 'slaves'))
870 if device_slaves:
871 metadata['device_nodes'] = ','.join(device_slaves)
872 else:
873 metadata['device_nodes'] = devname
874
875 metadata['scheduler_mode'] = ""
876 scheduler = get_file_contents(sysdir + "/queue/scheduler")
877 if scheduler is not None:
878 m = re.match(r".*?(\[(.*)\])", scheduler)
879 if m:
880 metadata['scheduler_mode'] = m.group(2)
881
882 metadata['partitions'] = get_partitions_facts(sysdir)
883
884 size = get_file_contents(os.path.join(sysdir, 'size'), 0)
885
886 metadata['sectors'] = get_file_contents(os.path.join(sysdir, 'sectors'), 0)
887 fallback_sectorsize = get_file_contents(sysdir + "/queue/hw_sector_size", 512)
888 metadata['sectorsize'] = get_file_contents(sysdir +
889 "/queue/logical_block_size",
890 fallback_sectorsize)
891 metadata['size'] = float(size) * 512
892 metadata['human_readable_size'] = human_readable_size(metadata['size'])
893 metadata['path'] = diskname
894 metadata['locked'] = is_locked_raw_device(metadata['path'])
895 metadata['type'] = block[2]
896
897 device_facts[diskname] = metadata
898 return device_facts
899
900 def has_bluestore_label(device_path):
901 isBluestore = False
902 bluestoreDiskSignature = 'bluestore block device' # 22 bytes long
903
904 # throws OSError on failure
905 logger.info("opening device {} to check for BlueStore label".format(device_path))
906 try:
907 with open(device_path, "rb") as fd:
908 # read first 22 bytes looking for bluestore disk signature
909 signature = fd.read(22)
910 if signature.decode('ascii', 'replace') == bluestoreDiskSignature:
911 isBluestore = True
912 except IsADirectoryError:
913 logger.info(f'{device_path} is a directory, skipping.')
914
915 return isBluestore