]> git.proxmox.com Git - ceph.git/blame - ceph/src/ceph-volume/ceph_volume/util/disk.py
import quincy beta 17.1.0
[ceph.git] / ceph / src / ceph-volume / ceph_volume / util / disk.py
CommitLineData
1adf2230 1import logging
3efd9988 2import os
1adf2230 3import re
3efd9988 4import stat
181888fb 5from ceph_volume import process
1adf2230
AA
6from ceph_volume.api import lvm
7from ceph_volume.util.system import get_file_contents
8
9
10logger = logging.getLogger(__name__)
181888fb
FG
11
12
b32b8144
FG
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
181888fb
FG
20def 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(
f67539c2 27 ['blkid', '-c', '/dev/null', '-s', 'PARTUUID', '-o', 'value', device]
181888fb
FG
28 )
29 return ' '.join(out).strip()
30
31
91327a77
AA
32def _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',
494da23a 54 'PART_ENTRY_TYPE': 'PARTTYPE',
91327a77
AA
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
74def 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(
f67539c2 101 ['blkid', '-c', '/dev/null', '-p', device]
91327a77
AA
102 )
103 return _blkid_parser(' '.join(out))
104
105
b32b8144
FG
106def 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 """
f67539c2 113 out, err, rc = process.call(['blkid', '-c', '/dev/null', '-p', '-o', 'udev', device])
b32b8144
FG
114 for line in out:
115 if 'ID_PART_ENTRY_TYPE=' in line:
116 return line.split('=')[-1].strip()
117 return ''
118
119
181888fb
FG
120def 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(
f67539c2 126 ['blkid', '-c', '/dev/null', '-t', 'PARTUUID="%s"' % partuuid, '-o', 'device']
181888fb
FG
127 )
128 return ' '.join(out).strip()
3efd9988
FG
129
130
f64942e4
AA
131def remove_partition(device):
132 """
133 Removes a partition using parted
134
135 :param device: A ``Device()`` object
136 """
f64942e4
AA
137 udev_info = udevadm_property(device.abspath)
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.abspath)
141
142 process.run(
522d829b 143 ['parted', device.parent_device, '--script', '--', 'rm', partition_number]
f64942e4
AA
144 )
145
146
3efd9988
FG
147def _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
b32b8144
FG
155def _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
174def 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
f64942e4
AA
190def 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
222def _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
b32b8144 231def lsblk(device, columns=None, abspath=False):
3efd9988
FG
232 """
233 Create a dictionary of identifying values for a device using ``lsblk``.
234 Each supported column is a key, in its *raw* format (all uppercase
235 usually). ``lsblk`` has support for certain "columns" (in blkid these
236 would be labels), and these columns vary between distributions and
237 ``lsblk`` versions. The newer versions support a richer set of columns,
238 while older ones were a bit limited.
239
b32b8144 240 These are a subset of lsblk columns which are known to work on both CentOS 7 and Xenial:
3efd9988
FG
241
242 NAME device name
243 KNAME internal kernel device name
244 MAJ:MIN major:minor device number
245 FSTYPE filesystem type
246 MOUNTPOINT where the device is mounted
247 LABEL filesystem LABEL
248 UUID filesystem UUID
249 RO read-only device
250 RM removable device
251 MODEL device identifier
252 SIZE size of the device
253 STATE state of the device
254 OWNER user name
255 GROUP group name
256 MODE device node permissions
257 ALIGNMENT alignment offset
258 MIN-IO minimum I/O size
259 OPT-IO optimal I/O size
260 PHY-SEC physical sector size
261 LOG-SEC logical sector size
262 ROTA rotational device
263 SCHED I/O scheduler name
264 RQ-SIZE request queue size
265 TYPE device type
b32b8144 266 PKNAME internal parent kernel device name
3efd9988
FG
267 DISC-ALN discard alignment offset
268 DISC-GRAN discard granularity
269 DISC-MAX discard max bytes
270 DISC-ZERO discard zeroes data
271
272 There is a bug in ``lsblk`` where using all the available (supported)
273 columns will result in no output (!), in order to workaround this the
274 following columns have been removed from the default reporting columns:
275
276 * RQ-SIZE (request queue size)
277 * MIN-IO minimum I/O size
278 * OPT-IO optimal I/O size
279
280 These should be available however when using `columns`. For example::
281
282 >>> lsblk('/dev/sda1', columns=['OPT-IO'])
283 {'OPT-IO': '0'}
284
285 Normal CLI output, as filtered by the flags in this function will look like ::
286
b32b8144 287 $ lsblk --nodeps -P -o NAME,KNAME,MAJ:MIN,FSTYPE,MOUNTPOINT
3efd9988
FG
288 NAME="sda1" KNAME="sda1" MAJ:MIN="8:1" FSTYPE="ext4" MOUNTPOINT="/"
289
290 :param columns: A list of columns to report as keys in its original form.
b32b8144 291 :param abspath: Set the flag for absolute paths on the report
3efd9988
FG
292 """
293 default_columns = [
294 'NAME', 'KNAME', 'MAJ:MIN', 'FSTYPE', 'MOUNTPOINT', 'LABEL', 'UUID',
295 'RO', 'RM', 'MODEL', 'SIZE', 'STATE', 'OWNER', 'GROUP', 'MODE',
296 'ALIGNMENT', 'PHY-SEC', 'LOG-SEC', 'ROTA', 'SCHED', 'TYPE', 'DISC-ALN',
b32b8144 297 'DISC-GRAN', 'DISC-MAX', 'DISC-ZERO', 'PKNAME', 'PARTLABEL'
3efd9988
FG
298 ]
299 device = device.rstrip('/')
300 columns = columns or default_columns
301 # --nodeps -> Avoid adding children/parents to the device, only give information
302 # on the actual device we are querying for
303 # -P -> Produce pairs of COLUMN="value"
b32b8144 304 # -p -> Return full paths to devices, not just the names, when ``abspath`` is set
3efd9988 305 # -o -> Use the columns specified or default ones provided by this function
b32b8144
FG
306 base_command = ['lsblk', '--nodeps', '-P']
307 if abspath:
308 base_command.append('-p')
309 base_command.append('-o')
310 base_command.append(','.join(columns))
311 base_command.append(device)
312 out, err, rc = process.call(base_command)
3efd9988
FG
313
314 if rc != 0:
315 return {}
316
b32b8144 317 return _lsblk_parser(' '.join(out))
3efd9988
FG
318
319
3efd9988
FG
320def is_device(dev):
321 """
322 Boolean to determine if a given device is a block device (**not**
323 a partition!)
324
325 For example: /dev/sda would return True, but not /dev/sdc1
326 """
327 if not os.path.exists(dev):
328 return False
329 # use lsblk first, fall back to using stat
330 TYPE = lsblk(dev).get('TYPE')
331 if TYPE:
f91f0fd5 332 return TYPE in ['disk', 'mpath']
3efd9988
FG
333
334 # fallback to stat
335 return _stat_is_device(os.lstat(dev).st_mode)
336 if stat.S_ISBLK(os.lstat(dev)):
337 return True
338 return False
339
340
341def is_partition(dev):
342 """
343 Boolean to determine if a given device is a partition, like /dev/sda1
344 """
345 if not os.path.exists(dev):
346 return False
347 # use lsblk first, fall back to using stat
348 TYPE = lsblk(dev).get('TYPE')
349 if TYPE:
350 return TYPE == 'part'
351
352 # fallback to stat
353 stat_obj = os.stat(dev)
354 if _stat_is_device(stat_obj.st_mode):
355 return False
356
357 major = os.major(stat_obj.st_rdev)
358 minor = os.minor(stat_obj.st_rdev)
359 if os.path.exists('/sys/dev/block/%d:%d/partition' % (major, minor)):
360 return True
361 return False
1adf2230
AA
362
363
1adf2230
AA
364class BaseFloatUnit(float):
365 """
366 Base class to support float representations of size values. Suffix is
367 computed on child classes by inspecting the class name
368 """
369
370 def __repr__(self):
371 return "<%s(%s)>" % (self.__class__.__name__, self.__float__())
372
373 def __str__(self):
374 return "{size:.2f} {suffix}".format(
375 size=self.__float__(),
376 suffix=self.__class__.__name__.split('Float')[-1]
377 )
378
379 def as_int(self):
380 return int(self.real)
381
382 def as_float(self):
383 return self.real
384
385
386class FloatB(BaseFloatUnit):
387 pass
388
389
390class FloatMB(BaseFloatUnit):
391 pass
392
393
394class FloatGB(BaseFloatUnit):
395 pass
396
397
398class FloatKB(BaseFloatUnit):
399 pass
400
401
402class FloatTB(BaseFloatUnit):
403 pass
404
20effc67
TL
405class FloatPB(BaseFloatUnit):
406 pass
1adf2230
AA
407
408class Size(object):
409 """
410 Helper to provide an interface for different sizes given a single initial
411 input. Allows for comparison between different size objects, which avoids
412 the need to convert sizes before comparison (e.g. comparing megabytes
413 against gigabytes).
414
415 Common comparison operators are supported::
416
417 >>> hd1 = Size(gb=400)
418 >>> hd2 = Size(gb=500)
419 >>> hd1 > hd2
420 False
421 >>> hd1 < hd2
422 True
423 >>> hd1 == hd2
424 False
425 >>> hd1 == Size(gb=400)
426 True
427
428 The Size object can also be multiplied or divided::
429
430 >>> hd1
431 <Size(400.00 GB)>
432 >>> hd1 * 2
433 <Size(800.00 GB)>
434 >>> hd1
435 <Size(800.00 GB)>
436
437 Additions and subtractions are only supported between Size objects::
438
439 >>> Size(gb=224) - Size(gb=100)
440 <Size(124.00 GB)>
441 >>> Size(gb=1) + Size(mb=300)
442 <Size(1.29 GB)>
443
444 Can also display a human-readable representation, with automatic detection
445 on best suited unit, or alternatively, specific unit representation::
446
447 >>> s = Size(mb=2211)
448 >>> s
449 <Size(2.16 GB)>
450 >>> s.mb
451 <FloatMB(2211.0)>
9f95a23c 452 >>> print("Total size: %s" % s.mb)
1adf2230 453 Total size: 2211.00 MB
9f95a23c 454 >>> print("Total size: %s" % s)
1adf2230
AA
455 Total size: 2.16 GB
456 """
457
92f5a8d4
TL
458 @classmethod
459 def parse(cls, size):
460 if (len(size) > 2 and
20effc67 461 size[-2].lower() in ['k', 'm', 'g', 't', 'p'] and
92f5a8d4
TL
462 size[-1].lower() == 'b'):
463 return cls(**{size[-2:].lower(): float(size[0:-2])})
20effc67 464 elif size[-1].lower() in ['b', 'k', 'm', 'g', 't', 'p']:
92f5a8d4
TL
465 return cls(**{size[-1].lower(): float(size[0:-1])})
466 else:
467 return cls(b=float(size))
468
469
1adf2230
AA
470 def __init__(self, multiplier=1024, **kw):
471 self._multiplier = multiplier
472 # create a mapping of units-to-multiplier, skip bytes as that is
473 # calculated initially always and does not need to convert
474 aliases = [
92f5a8d4
TL
475 [('k', 'kb', 'kilobytes'), self._multiplier],
476 [('m', 'mb', 'megabytes'), self._multiplier ** 2],
477 [('g', 'gb', 'gigabytes'), self._multiplier ** 3],
478 [('t', 'tb', 'terabytes'), self._multiplier ** 4],
20effc67 479 [('p', 'pb', 'petabytes'), self._multiplier ** 5]
1adf2230
AA
480 ]
481 # and mappings for units-to-formatters, including bytes and aliases for
482 # each
483 format_aliases = [
484 [('b', 'bytes'), FloatB],
485 [('kb', 'kilobytes'), FloatKB],
486 [('mb', 'megabytes'), FloatMB],
487 [('gb', 'gigabytes'), FloatGB],
488 [('tb', 'terabytes'), FloatTB],
20effc67 489 [('pb', 'petabytes'), FloatPB],
1adf2230
AA
490 ]
491 self._formatters = {}
492 for key, value in format_aliases:
493 for alias in key:
494 self._formatters[alias] = value
495 self._factors = {}
496 for key, value in aliases:
497 for alias in key:
498 self._factors[alias] = value
499
500 for k, v in kw.items():
501 self._convert(v, k)
11fdf7f2 502 # only pursue the first occurrence
1adf2230
AA
503 break
504
505 def _convert(self, size, unit):
506 """
507 Convert any size down to bytes so that other methods can rely on bytes
508 being available always, regardless of what they pass in, avoiding the
509 need for a mapping of every permutation.
510 """
511 if unit in ['b', 'bytes']:
512 self._b = size
513 return
514 factor = self._factors[unit]
515 self._b = float(size * factor)
516
517 def _get_best_format(self):
518 """
519 Go through all the supported units, and use the first one that is less
520 than 1024. This allows to represent size in the most readable format
521 available
522 """
20effc67 523 for unit in ['b', 'kb', 'mb', 'gb', 'tb', 'pb']:
1adf2230
AA
524 if getattr(self, unit) > 1024:
525 continue
526 return getattr(self, unit)
527
528 def __repr__(self):
529 return "<Size(%s)>" % self._get_best_format()
530
531 def __str__(self):
532 return "%s" % self._get_best_format()
533
11fdf7f2
TL
534 def __format__(self, spec):
535 return str(self._get_best_format()).__format__(spec)
536
92f5a8d4
TL
537 def __int__(self):
538 return int(self._b)
539
540 def __float__(self):
541 return self._b
542
1adf2230 543 def __lt__(self, other):
92f5a8d4
TL
544 if isinstance(other, Size):
545 return self._b < other._b
546 else:
547 return self.b < other
1adf2230
AA
548
549 def __le__(self, other):
92f5a8d4
TL
550 if isinstance(other, Size):
551 return self._b <= other._b
552 else:
553 return self.b <= other
1adf2230
AA
554
555 def __eq__(self, other):
92f5a8d4
TL
556 if isinstance(other, Size):
557 return self._b == other._b
558 else:
559 return self.b == other
1adf2230
AA
560
561 def __ne__(self, other):
92f5a8d4
TL
562 if isinstance(other, Size):
563 return self._b != other._b
564 else:
565 return self.b != other
1adf2230
AA
566
567 def __ge__(self, other):
92f5a8d4
TL
568 if isinstance(other, Size):
569 return self._b >= other._b
570 else:
571 return self.b >= other
1adf2230
AA
572
573 def __gt__(self, other):
92f5a8d4
TL
574 if isinstance(other, Size):
575 return self._b > other._b
576 else:
577 return self.b > other
1adf2230
AA
578
579 def __add__(self, other):
580 if isinstance(other, Size):
581 _b = self._b + other._b
582 return Size(b=_b)
583 raise TypeError('Cannot add "Size" object with int')
584
585 def __sub__(self, other):
586 if isinstance(other, Size):
587 _b = self._b - other._b
588 return Size(b=_b)
589 raise TypeError('Cannot subtract "Size" object from int')
590
591 def __mul__(self, other):
592 if isinstance(other, Size):
593 raise TypeError('Cannot multiply with "Size" object')
594 _b = self._b * other
595 return Size(b=_b)
596
597 def __truediv__(self, other):
598 if isinstance(other, Size):
599 return self._b / other._b
91327a77
AA
600 _b = self._b / other
601 return Size(b=_b)
1adf2230
AA
602
603 def __div__(self, other):
604 if isinstance(other, Size):
605 return self._b / other._b
91327a77
AA
606 _b = self._b / other
607 return Size(b=_b)
1adf2230 608
f91f0fd5
TL
609 def __bool__(self):
610 return self.b != 0
611
612 def __nonzero__(self):
613 return self.__bool__()
614
1adf2230
AA
615 def __getattr__(self, unit):
616 """
617 Calculate units on the fly, relies on the fact that ``bytes`` has been
618 converted at instantiation. Units that don't exist will trigger an
619 ``AttributeError``
620 """
621 try:
622 formatter = self._formatters[unit]
623 except KeyError:
624 raise AttributeError('Size object has not attribute "%s"' % unit)
625 if unit in ['b', 'bytes']:
626 return formatter(self._b)
627 try:
628 factor = self._factors[unit]
629 except KeyError:
630 raise AttributeError('Size object has not attribute "%s"' % unit)
631 return formatter(float(self._b) / factor)
632
633
634def human_readable_size(size):
635 """
636 Take a size in bytes, and transform it into a human readable size with up
637 to two decimals of precision.
638 """
20effc67
TL
639 suffixes = ['B', 'KB', 'MB', 'GB', 'TB', 'PB']
640 for suffix in suffixes:
641 if size >= 1024:
642 size = size / 1024
643 else:
644 break
1adf2230
AA
645 return "{size:.2f} {suffix}".format(
646 size=size,
20effc67 647 suffix=suffix)
1adf2230
AA
648
649
81eedcae
TL
650def size_from_human_readable(s):
651 """
652 Takes a human readable string and converts into a Size. If no unit is
653 passed, bytes is assumed.
654 """
655 s = s.replace(' ', '')
656 if s[-1].isdigit():
657 return Size(b=float(s))
658 n = float(s[:-1])
20effc67
TL
659 if s[-1].lower() == 'p':
660 return Size(pb=n)
81eedcae
TL
661 if s[-1].lower() == 't':
662 return Size(tb=n)
663 if s[-1].lower() == 'g':
664 return Size(gb=n)
665 if s[-1].lower() == 'm':
666 return Size(mb=n)
667 if s[-1].lower() == 'k':
668 return Size(kb=n)
669 return None
670
671
1adf2230
AA
672def get_partitions_facts(sys_block_path):
673 partition_metadata = {}
674 for folder in os.listdir(sys_block_path):
675 folder_path = os.path.join(sys_block_path, folder)
676 if os.path.exists(os.path.join(folder_path, 'partition')):
677 contents = get_file_contents(os.path.join(folder_path, 'partition'))
f64942e4 678 if contents:
1adf2230
AA
679 part = {}
680 partname = folder
681 part_sys_block_path = os.path.join(sys_block_path, partname)
682
683 part['start'] = get_file_contents(part_sys_block_path + "/start", 0)
684 part['sectors'] = get_file_contents(part_sys_block_path + "/size", 0)
685
686 part['sectorsize'] = get_file_contents(
687 part_sys_block_path + "/queue/logical_block_size")
688 if not part['sectorsize']:
689 part['sectorsize'] = get_file_contents(
690 part_sys_block_path + "/queue/hw_sector_size", 512)
92f5a8d4
TL
691 part['size'] = float(part['sectors']) * 512
692 part['human_readable_size'] = human_readable_size(float(part['sectors']) * 512)
f64942e4
AA
693 part['holders'] = []
694 for holder in os.listdir(part_sys_block_path + '/holders'):
695 part['holders'].append(holder)
1adf2230
AA
696
697 partition_metadata[partname] = part
698 return partition_metadata
699
700
701def is_mapper_device(device_name):
702 return device_name.startswith(('/dev/mapper', '/dev/dm-'))
703
704
91327a77
AA
705def is_locked_raw_device(disk_path):
706 """
707 A device can be locked by a third party software like a database.
708 To detect that case, the device is opened in Read/Write and exclusive mode
709 """
710 open_flags = (os.O_RDWR | os.O_EXCL)
711 open_mode = 0
712 fd = None
713
714 try:
715 fd = os.open(disk_path, open_flags, open_mode)
716 except OSError:
717 return 1
718
719 try:
720 os.close(fd)
721 except OSError:
722 return 1
723
724 return 0
725
726
92f5a8d4
TL
727def get_block_devs_lsblk():
728 '''
729 This returns a list of lists with 3 items per inner list.
730 KNAME - reflects the kernel device name , for example /dev/sda or /dev/dm-0
731 NAME - the device name, for example /dev/sda or
732 /dev/mapper/<vg_name>-<lv_name>
733 TYPE - the block device type: disk, partition, lvm and such
734
735 '''
736 cmd = ['lsblk', '-plno', 'KNAME,NAME,TYPE']
737 stdout, stderr, rc = process.call(cmd)
738 # lsblk returns 1 on failure
739 if rc == 1:
740 raise OSError('lsblk returned failure, stderr: {}'.format(stderr))
741 return [re.split(r'\s+', line) for line in stdout]
742
743
744def get_devices(_sys_block_path='/sys/block'):
1adf2230 745 """
92f5a8d4
TL
746 Captures all available block devices as reported by lsblk.
747 Additional interesting metadata like sectors, size, vendor,
748 solid/rotational, etc. is collected from /sys/block/<device>
1adf2230
AA
749
750 Returns a dictionary, where keys are the full paths to devices.
751
1adf2230
AA
752 ..note:: loop devices, removable media, and logical volumes are never included.
753 """
1adf2230
AA
754
755 device_facts = {}
756
92f5a8d4 757 block_devs = get_block_devs_lsblk()
1adf2230
AA
758
759 for block in block_devs:
92f5a8d4
TL
760 devname = os.path.basename(block[0])
761 diskname = block[1]
f91f0fd5 762 if block[2] not in ['disk', 'mpath']:
91327a77 763 continue
92f5a8d4
TL
764 sysdir = os.path.join(_sys_block_path, devname)
765 metadata = {}
1adf2230
AA
766
767 # If the mapper device is a logical volume it gets excluded
768 if is_mapper_device(diskname):
cd265ab1 769 if lvm.get_device_lvs(diskname):
1adf2230
AA
770 continue
771
92f5a8d4
TL
772 # all facts that have no defaults
773 # (<name>, <path relative to _sys_block_path>)
774 facts = [('removable', 'removable'),
775 ('ro', 'ro'),
776 ('vendor', 'device/vendor'),
777 ('model', 'device/model'),
778 ('rev', 'device/rev'),
779 ('sas_address', 'device/sas_address'),
780 ('sas_device_handle', 'device/sas_device_handle'),
781 ('support_discard', 'queue/discard_granularity'),
782 ('rotational', 'queue/rotational'),
783 ('nr_requests', 'queue/nr_requests'),
784 ]
785 for key, file_ in facts:
786 metadata[key] = get_file_contents(os.path.join(sysdir, file_))
91327a77 787
1adf2230
AA
788 metadata['scheduler_mode'] = ""
789 scheduler = get_file_contents(sysdir + "/queue/scheduler")
790 if scheduler is not None:
791 m = re.match(r".*?(\[(.*)\])", scheduler)
792 if m:
793 metadata['scheduler_mode'] = m.group(2)
794
92f5a8d4
TL
795 metadata['partitions'] = get_partitions_facts(sysdir)
796
797 size = get_file_contents(os.path.join(sysdir, 'size'), 0)
798
799 metadata['sectors'] = get_file_contents(os.path.join(sysdir, 'sectors'), 0)
800 fallback_sectorsize = get_file_contents(sysdir + "/queue/hw_sector_size", 512)
801 metadata['sectorsize'] = get_file_contents(sysdir +
802 "/queue/logical_block_size",
803 fallback_sectorsize)
1adf2230 804 metadata['size'] = float(size) * 512
92f5a8d4 805 metadata['human_readable_size'] = human_readable_size(metadata['size'])
1adf2230 806 metadata['path'] = diskname
91327a77 807 metadata['locked'] = is_locked_raw_device(metadata['path'])
1adf2230
AA
808
809 device_facts[diskname] = metadata
810 return device_facts
522d829b
TL
811
812def has_bluestore_label(device_path):
813 isBluestore = False
814 bluestoreDiskSignature = 'bluestore block device' # 22 bytes long
815
816 # throws OSError on failure
817 logger.info("opening device {} to check for BlueStore label".format(device_path))
818 with open(device_path, "rb") as fd:
819 # read first 22 bytes looking for bluestore disk signature
820 signature = fd.read(22)
821 if signature.decode('ascii', 'replace') == bluestoreDiskSignature:
822 isBluestore = True
823
824 return isBluestore