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