]> git.proxmox.com Git - ceph.git/blame - ceph/src/ceph-volume/ceph_volume/util/disk.py
update sources to 12.2.8
[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(
b32b8144 27 ['blkid', '-s', 'PARTUUID', '-o', 'value', device]
181888fb
FG
28 )
29 return ' '.join(out).strip()
30
31
b32b8144
FG
32def get_part_entry_type(device):
33 """
34 Parses the ``ID_PART_ENTRY_TYPE`` from the "low level" (bypasses the cache)
35 output that uses the ``udev`` type of output. This output is intended to be
36 used for udev rules, but it is useful in this case as it is the only
37 consistent way to retrieve the GUID used by ceph-disk to identify devices.
38 """
39 out, err, rc = process.call(['blkid', '-p', '-o', 'udev', device])
40 for line in out:
41 if 'ID_PART_ENTRY_TYPE=' in line:
42 return line.split('=')[-1].strip()
43 return ''
44
45
181888fb
FG
46def get_device_from_partuuid(partuuid):
47 """
48 If a device has a partuuid, query blkid so that it can tell us what that
49 device is
50 """
51 out, err, rc = process.call(
b32b8144 52 ['blkid', '-t', 'PARTUUID="%s"' % partuuid, '-o', 'device']
181888fb
FG
53 )
54 return ' '.join(out).strip()
3efd9988
FG
55
56
57def _stat_is_device(stat_obj):
58 """
59 Helper function that will interpret ``os.stat`` output directly, so that other
60 functions can call ``os.stat`` once and interpret that result several times
61 """
62 return stat.S_ISBLK(stat_obj)
63
64
b32b8144
FG
65def _lsblk_parser(line):
66 """
67 Parses lines in lsblk output. Requires output to be in pair mode (``-P`` flag). Lines
68 need to be whole strings, the line gets split when processed.
69
70 :param line: A string, with the full line from lsblk output
71 """
72 # parse the COLUMN="value" output to construct the dictionary
73 pairs = line.split('" ')
74 parsed = {}
75 for pair in pairs:
76 try:
77 column, value = pair.split('=')
78 except ValueError:
79 continue
80 parsed[column] = value.strip().strip().strip('"')
81 return parsed
82
83
84def device_family(device):
85 """
86 Returns a list of associated devices. It assumes that ``device`` is
87 a parent device. It is up to the caller to ensure that the device being
88 used is a parent, not a partition.
89 """
90 labels = ['NAME', 'PARTLABEL', 'TYPE']
91 command = ['lsblk', '-P', '-p', '-o', ','.join(labels), device]
92 out, err, rc = process.call(command)
93 devices = []
94 for line in out:
95 devices.append(_lsblk_parser(line))
96
97 return devices
98
99
100def lsblk(device, columns=None, abspath=False):
3efd9988
FG
101 """
102 Create a dictionary of identifying values for a device using ``lsblk``.
103 Each supported column is a key, in its *raw* format (all uppercase
104 usually). ``lsblk`` has support for certain "columns" (in blkid these
105 would be labels), and these columns vary between distributions and
106 ``lsblk`` versions. The newer versions support a richer set of columns,
107 while older ones were a bit limited.
108
b32b8144 109 These are a subset of lsblk columns which are known to work on both CentOS 7 and Xenial:
3efd9988
FG
110
111 NAME device name
112 KNAME internal kernel device name
113 MAJ:MIN major:minor device number
114 FSTYPE filesystem type
115 MOUNTPOINT where the device is mounted
116 LABEL filesystem LABEL
117 UUID filesystem UUID
118 RO read-only device
119 RM removable device
120 MODEL device identifier
121 SIZE size of the device
122 STATE state of the device
123 OWNER user name
124 GROUP group name
125 MODE device node permissions
126 ALIGNMENT alignment offset
127 MIN-IO minimum I/O size
128 OPT-IO optimal I/O size
129 PHY-SEC physical sector size
130 LOG-SEC logical sector size
131 ROTA rotational device
132 SCHED I/O scheduler name
133 RQ-SIZE request queue size
134 TYPE device type
b32b8144 135 PKNAME internal parent kernel device name
3efd9988
FG
136 DISC-ALN discard alignment offset
137 DISC-GRAN discard granularity
138 DISC-MAX discard max bytes
139 DISC-ZERO discard zeroes data
140
141 There is a bug in ``lsblk`` where using all the available (supported)
142 columns will result in no output (!), in order to workaround this the
143 following columns have been removed from the default reporting columns:
144
145 * RQ-SIZE (request queue size)
146 * MIN-IO minimum I/O size
147 * OPT-IO optimal I/O size
148
149 These should be available however when using `columns`. For example::
150
151 >>> lsblk('/dev/sda1', columns=['OPT-IO'])
152 {'OPT-IO': '0'}
153
154 Normal CLI output, as filtered by the flags in this function will look like ::
155
b32b8144 156 $ lsblk --nodeps -P -o NAME,KNAME,MAJ:MIN,FSTYPE,MOUNTPOINT
3efd9988
FG
157 NAME="sda1" KNAME="sda1" MAJ:MIN="8:1" FSTYPE="ext4" MOUNTPOINT="/"
158
159 :param columns: A list of columns to report as keys in its original form.
b32b8144 160 :param abspath: Set the flag for absolute paths on the report
3efd9988
FG
161 """
162 default_columns = [
163 'NAME', 'KNAME', 'MAJ:MIN', 'FSTYPE', 'MOUNTPOINT', 'LABEL', 'UUID',
164 'RO', 'RM', 'MODEL', 'SIZE', 'STATE', 'OWNER', 'GROUP', 'MODE',
165 'ALIGNMENT', 'PHY-SEC', 'LOG-SEC', 'ROTA', 'SCHED', 'TYPE', 'DISC-ALN',
b32b8144 166 'DISC-GRAN', 'DISC-MAX', 'DISC-ZERO', 'PKNAME', 'PARTLABEL'
3efd9988
FG
167 ]
168 device = device.rstrip('/')
169 columns = columns or default_columns
170 # --nodeps -> Avoid adding children/parents to the device, only give information
171 # on the actual device we are querying for
172 # -P -> Produce pairs of COLUMN="value"
b32b8144 173 # -p -> Return full paths to devices, not just the names, when ``abspath`` is set
3efd9988 174 # -o -> Use the columns specified or default ones provided by this function
b32b8144
FG
175 base_command = ['lsblk', '--nodeps', '-P']
176 if abspath:
177 base_command.append('-p')
178 base_command.append('-o')
179 base_command.append(','.join(columns))
180 base_command.append(device)
181 out, err, rc = process.call(base_command)
3efd9988
FG
182
183 if rc != 0:
184 return {}
185
b32b8144 186 return _lsblk_parser(' '.join(out))
3efd9988
FG
187
188
3efd9988
FG
189def is_device(dev):
190 """
191 Boolean to determine if a given device is a block device (**not**
192 a partition!)
193
194 For example: /dev/sda would return True, but not /dev/sdc1
195 """
196 if not os.path.exists(dev):
197 return False
198 # use lsblk first, fall back to using stat
199 TYPE = lsblk(dev).get('TYPE')
200 if TYPE:
201 return TYPE == 'disk'
202
203 # fallback to stat
204 return _stat_is_device(os.lstat(dev).st_mode)
205 if stat.S_ISBLK(os.lstat(dev)):
206 return True
207 return False
208
209
210def is_partition(dev):
211 """
212 Boolean to determine if a given device is a partition, like /dev/sda1
213 """
214 if not os.path.exists(dev):
215 return False
216 # use lsblk first, fall back to using stat
217 TYPE = lsblk(dev).get('TYPE')
218 if TYPE:
219 return TYPE == 'part'
220
221 # fallback to stat
222 stat_obj = os.stat(dev)
223 if _stat_is_device(stat_obj.st_mode):
224 return False
225
226 major = os.major(stat_obj.st_rdev)
227 minor = os.minor(stat_obj.st_rdev)
228 if os.path.exists('/sys/dev/block/%d:%d/partition' % (major, minor)):
229 return True
230 return False
1adf2230
AA
231
232
233def _map_dev_paths(_path, include_abspath=False, include_realpath=False):
234 """
235 Go through all the items in ``_path`` and map them to their absolute path::
236
237 {'sda': '/dev/sda'}
238
239 If ``include_abspath`` is set, then a reverse mapping is set as well::
240
241 {'sda': '/dev/sda', '/dev/sda': 'sda'}
242
243 If ``include_realpath`` is set then the same operation is done for any
244 links found when listing, these are *not* reversed to avoid clashing on
245 existing keys, but both abspath and basename can be included. For example::
246
247 {
248 'ceph-data': '/dev/mapper/ceph-data',
249 '/dev/mapper/ceph-data': 'ceph-data',
250 '/dev/dm-0': '/dev/mapper/ceph-data',
251 'dm-0': '/dev/mapper/ceph-data'
252 }
253
254
255 In case of possible exceptions the mapping is returned empty, and the
256 exception is logged.
257 """
258 mapping = {}
259 try:
260 dev_names = os.listdir(_path)
261 except (OSError, IOError):
262 logger.exception('unable to list block devices from: %s' % _path)
263 return {}
264
265 for dev_name in dev_names:
266 mapping[dev_name] = os.path.join(_path, dev_name)
267
268 if include_abspath:
269 for k, v in list(mapping.items()):
270 mapping[v] = k
271
272 if include_realpath:
273 for abspath in list(mapping.values()):
274 if not os.path.islink(abspath):
275 continue
276
277 realpath = os.path.realpath(abspath)
278 basename = os.path.basename(realpath)
279 mapping[basename] = abspath
280 if include_abspath:
281 mapping[realpath] = abspath
282
283 return mapping
284
285
286def get_block_devs(sys_block_path="/sys/block", skip_loop=True):
287 """
288 Go through all the items in /sys/block and return them as a list.
289
290 The ``sys_block_path`` argument is set for easier testing and is not
291 required for proper operation.
292 """
293 devices = _map_dev_paths(sys_block_path).keys()
294 if skip_loop:
295 return [d for d in devices if not d.startswith('loop')]
296 return list(devices)
297
298
299def get_dev_devs(dev_path="/dev"):
300 """
301 Go through all the items in /dev and return them as a list.
302
303 The ``dev_path`` argument is set for easier testing and is not
304 required for proper operation.
305 """
306 return _map_dev_paths(dev_path, include_abspath=True)
307
308
309def get_mapper_devs(mapper_path="/dev/mapper"):
310 """
311 Go through all the items in /dev and return them as a list.
312
313 The ``dev_path`` argument is set for easier testing and is not
314 required for proper operation.
315 """
316 return _map_dev_paths(mapper_path, include_abspath=True, include_realpath=True)
317
318
319class BaseFloatUnit(float):
320 """
321 Base class to support float representations of size values. Suffix is
322 computed on child classes by inspecting the class name
323 """
324
325 def __repr__(self):
326 return "<%s(%s)>" % (self.__class__.__name__, self.__float__())
327
328 def __str__(self):
329 return "{size:.2f} {suffix}".format(
330 size=self.__float__(),
331 suffix=self.__class__.__name__.split('Float')[-1]
332 )
333
334 def as_int(self):
335 return int(self.real)
336
337 def as_float(self):
338 return self.real
339
340
341class FloatB(BaseFloatUnit):
342 pass
343
344
345class FloatMB(BaseFloatUnit):
346 pass
347
348
349class FloatGB(BaseFloatUnit):
350 pass
351
352
353class FloatKB(BaseFloatUnit):
354 pass
355
356
357class FloatTB(BaseFloatUnit):
358 pass
359
360
361class Size(object):
362 """
363 Helper to provide an interface for different sizes given a single initial
364 input. Allows for comparison between different size objects, which avoids
365 the need to convert sizes before comparison (e.g. comparing megabytes
366 against gigabytes).
367
368 Common comparison operators are supported::
369
370 >>> hd1 = Size(gb=400)
371 >>> hd2 = Size(gb=500)
372 >>> hd1 > hd2
373 False
374 >>> hd1 < hd2
375 True
376 >>> hd1 == hd2
377 False
378 >>> hd1 == Size(gb=400)
379 True
380
381 The Size object can also be multiplied or divided::
382
383 >>> hd1
384 <Size(400.00 GB)>
385 >>> hd1 * 2
386 <Size(800.00 GB)>
387 >>> hd1
388 <Size(800.00 GB)>
389
390 Additions and subtractions are only supported between Size objects::
391
392 >>> Size(gb=224) - Size(gb=100)
393 <Size(124.00 GB)>
394 >>> Size(gb=1) + Size(mb=300)
395 <Size(1.29 GB)>
396
397 Can also display a human-readable representation, with automatic detection
398 on best suited unit, or alternatively, specific unit representation::
399
400 >>> s = Size(mb=2211)
401 >>> s
402 <Size(2.16 GB)>
403 >>> s.mb
404 <FloatMB(2211.0)>
405 >>> print "Total size: %s" % s.mb
406 Total size: 2211.00 MB
407 >>> print "Total size: %s" % s
408 Total size: 2.16 GB
409 """
410
411 def __init__(self, multiplier=1024, **kw):
412 self._multiplier = multiplier
413 # create a mapping of units-to-multiplier, skip bytes as that is
414 # calculated initially always and does not need to convert
415 aliases = [
416 [('kb', 'kilobytes'), self._multiplier],
417 [('mb', 'megabytes'), self._multiplier ** 2],
418 [('gb', 'gigabytes'), self._multiplier ** 3],
419 [('tb', 'terabytes'), self._multiplier ** 4],
420 ]
421 # and mappings for units-to-formatters, including bytes and aliases for
422 # each
423 format_aliases = [
424 [('b', 'bytes'), FloatB],
425 [('kb', 'kilobytes'), FloatKB],
426 [('mb', 'megabytes'), FloatMB],
427 [('gb', 'gigabytes'), FloatGB],
428 [('tb', 'terabytes'), FloatTB],
429 ]
430 self._formatters = {}
431 for key, value in format_aliases:
432 for alias in key:
433 self._formatters[alias] = value
434 self._factors = {}
435 for key, value in aliases:
436 for alias in key:
437 self._factors[alias] = value
438
439 for k, v in kw.items():
440 self._convert(v, k)
441 # only pursue the first occurence
442 break
443
444 def _convert(self, size, unit):
445 """
446 Convert any size down to bytes so that other methods can rely on bytes
447 being available always, regardless of what they pass in, avoiding the
448 need for a mapping of every permutation.
449 """
450 if unit in ['b', 'bytes']:
451 self._b = size
452 return
453 factor = self._factors[unit]
454 self._b = float(size * factor)
455
456 def _get_best_format(self):
457 """
458 Go through all the supported units, and use the first one that is less
459 than 1024. This allows to represent size in the most readable format
460 available
461 """
462 for unit in ['b', 'kb', 'mb', 'gb', 'tb']:
463 if getattr(self, unit) > 1024:
464 continue
465 return getattr(self, unit)
466
467 def __repr__(self):
468 return "<Size(%s)>" % self._get_best_format()
469
470 def __str__(self):
471 return "%s" % self._get_best_format()
472
473 def __lt__(self, other):
474 return self._b < other._b
475
476 def __le__(self, other):
477 return self._b <= other._b
478
479 def __eq__(self, other):
480 return self._b == other._b
481
482 def __ne__(self, other):
483 return self._b != other._b
484
485 def __ge__(self, other):
486 return self._b >= other._b
487
488 def __gt__(self, other):
489 return self._b > other._b
490
491 def __add__(self, other):
492 if isinstance(other, Size):
493 _b = self._b + other._b
494 return Size(b=_b)
495 raise TypeError('Cannot add "Size" object with int')
496
497 def __sub__(self, other):
498 if isinstance(other, Size):
499 _b = self._b - other._b
500 return Size(b=_b)
501 raise TypeError('Cannot subtract "Size" object from int')
502
503 def __mul__(self, other):
504 if isinstance(other, Size):
505 raise TypeError('Cannot multiply with "Size" object')
506 _b = self._b * other
507 return Size(b=_b)
508
509 def __truediv__(self, other):
510 if isinstance(other, Size):
511 return self._b / other._b
512 self._b = self._b / other
513 return self
514
515 def __div__(self, other):
516 if isinstance(other, Size):
517 return self._b / other._b
518 self._b = self._b / other
519 return self
520
521 def __getattr__(self, unit):
522 """
523 Calculate units on the fly, relies on the fact that ``bytes`` has been
524 converted at instantiation. Units that don't exist will trigger an
525 ``AttributeError``
526 """
527 try:
528 formatter = self._formatters[unit]
529 except KeyError:
530 raise AttributeError('Size object has not attribute "%s"' % unit)
531 if unit in ['b', 'bytes']:
532 return formatter(self._b)
533 try:
534 factor = self._factors[unit]
535 except KeyError:
536 raise AttributeError('Size object has not attribute "%s"' % unit)
537 return formatter(float(self._b) / factor)
538
539
540def human_readable_size(size):
541 """
542 Take a size in bytes, and transform it into a human readable size with up
543 to two decimals of precision.
544 """
545 suffixes = ['B', 'KB', 'MB', 'GB', 'TB']
546 suffix_index = 0
547 while size > 1024:
548 suffix_index += 1
549 size = size / 1024.0
550 return "{size:.2f} {suffix}".format(
551 size=size,
552 suffix=suffixes[suffix_index])
553
554
555def get_partitions_facts(sys_block_path):
556 partition_metadata = {}
557 for folder in os.listdir(sys_block_path):
558 folder_path = os.path.join(sys_block_path, folder)
559 if os.path.exists(os.path.join(folder_path, 'partition')):
560 contents = get_file_contents(os.path.join(folder_path, 'partition'))
561 if '1' in contents:
562 part = {}
563 partname = folder
564 part_sys_block_path = os.path.join(sys_block_path, partname)
565
566 part['start'] = get_file_contents(part_sys_block_path + "/start", 0)
567 part['sectors'] = get_file_contents(part_sys_block_path + "/size", 0)
568
569 part['sectorsize'] = get_file_contents(
570 part_sys_block_path + "/queue/logical_block_size")
571 if not part['sectorsize']:
572 part['sectorsize'] = get_file_contents(
573 part_sys_block_path + "/queue/hw_sector_size", 512)
574 part['size'] = human_readable_size(float(part['sectors']) * 512)
575
576 partition_metadata[partname] = part
577 return partition_metadata
578
579
580def is_mapper_device(device_name):
581 return device_name.startswith(('/dev/mapper', '/dev/dm-'))
582
583
584def get_devices(_sys_block_path='/sys/block', _dev_path='/dev', _mapper_path='/dev/mapper'):
585 """
586 Captures all available devices from /sys/block/, including its partitions,
587 along with interesting metadata like sectors, size, vendor,
588 solid/rotational, etc...
589
590 Returns a dictionary, where keys are the full paths to devices.
591
592 ..note:: dmapper devices get their path updated to what they link from, if
593 /dev/dm-0 is linked by /dev/mapper/ceph-data, then the latter gets
594 used as the key.
595
596 ..note:: loop devices, removable media, and logical volumes are never included.
597 """
598 # Portions of this detection process are inspired by some of the fact
599 # gathering done by Ansible in module_utils/facts/hardware/linux.py. The
600 # processing of metadata and final outcome *is very different* and fully
601 # imcompatible. There are ignored devices, and paths get resolved depending
602 # on dm devices, loop, and removable media
603
604 device_facts = {}
605
606 block_devs = get_block_devs(_sys_block_path)
607 dev_devs = get_dev_devs(_dev_path)
608 mapper_devs = get_mapper_devs(_mapper_path)
609
610 for block in block_devs:
611 sysdir = os.path.join(_sys_block_path, block)
612 metadata = {}
613
614 # Ensure that the diskname is an absolute path and that it never points
615 # to a /dev/dm-* device
616 diskname = mapper_devs.get(block) or dev_devs.get(block)
617
618 # If the mapper device is a logical volume it gets excluded
619 if is_mapper_device(diskname):
620 if lvm.is_lv(diskname):
621 continue
622
623 # If the device reports itself as 'removable', get it excluded
624 metadata['removable'] = get_file_contents(os.path.join(sysdir, 'removable'))
625 if metadata['removable'] == '1':
626 continue
627
628 for key in ['vendor', 'model', 'sas_address', 'sas_device_handle']:
629 metadata[key] = get_file_contents(sysdir + "/device/" + key)
630
631 for key in ['sectors', 'size']:
632 metadata[key] = get_file_contents(os.path.join(sysdir, key), 0)
633
634 for key, _file in [('support_discard', '/queue/discard_granularity')]:
635 metadata[key] = get_file_contents(os.path.join(sysdir, _file))
636
637 metadata['partitions'] = get_partitions_facts(sysdir)
638
639 metadata['rotational'] = get_file_contents(sysdir + "/queue/rotational")
640 metadata['scheduler_mode'] = ""
641 scheduler = get_file_contents(sysdir + "/queue/scheduler")
642 if scheduler is not None:
643 m = re.match(r".*?(\[(.*)\])", scheduler)
644 if m:
645 metadata['scheduler_mode'] = m.group(2)
646
647 if not metadata['sectors']:
648 metadata['sectors'] = 0
649 size = metadata['sectors'] or metadata['size']
650 metadata['sectorsize'] = get_file_contents(sysdir + "/queue/logical_block_size")
651 if not metadata['sectorsize']:
652 metadata['sectorsize'] = get_file_contents(sysdir + "/queue/hw_sector_size", 512)
653 metadata['human_readable_size'] = human_readable_size(float(size) * 512)
654 metadata['size'] = float(size) * 512
655 metadata['path'] = diskname
656
657 device_facts[diskname] = metadata
658 return device_facts