]>
Commit | Line | Data |
---|---|---|
1adf2230 | 1 | import logging |
3efd9988 | 2 | import os |
1adf2230 | 3 | import re |
3efd9988 | 4 | import stat |
181888fb | 5 | from ceph_volume import process |
1adf2230 AA |
6 | from ceph_volume.api import lvm |
7 | from ceph_volume.util.system import get_file_contents | |
8 | ||
9 | ||
10 | logger = 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 |
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( | |
f67539c2 | 27 | ['blkid', '-c', '/dev/null', '-s', 'PARTUUID', '-o', 'value', device] |
181888fb FG |
28 | ) |
29 | return ' '.join(out).strip() | |
30 | ||
31 | ||
91327a77 AA |
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', | |
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 | ||
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( | |
f67539c2 | 101 | ['blkid', '-c', '/dev/null', '-p', device] |
91327a77 AA |
102 | ) |
103 | return _blkid_parser(' '.join(out)) | |
104 | ||
105 | ||
b32b8144 FG |
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 | """ | |
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 |
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( | |
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 |
131 | def 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 |
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 | ||
b32b8144 FG |
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 | ||
f64942e4 AA |
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 | ||
b32b8144 | 231 | def 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 |
320 | def 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 | ||
341 | def 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 |
364 | class 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 | ||
386 | class FloatB(BaseFloatUnit): | |
387 | pass | |
388 | ||
389 | ||
390 | class FloatMB(BaseFloatUnit): | |
391 | pass | |
392 | ||
393 | ||
394 | class FloatGB(BaseFloatUnit): | |
395 | pass | |
396 | ||
397 | ||
398 | class FloatKB(BaseFloatUnit): | |
399 | pass | |
400 | ||
401 | ||
402 | class FloatTB(BaseFloatUnit): | |
403 | pass | |
404 | ||
405 | ||
406 | class Size(object): | |
407 | """ | |
408 | Helper to provide an interface for different sizes given a single initial | |
409 | input. Allows for comparison between different size objects, which avoids | |
410 | the need to convert sizes before comparison (e.g. comparing megabytes | |
411 | against gigabytes). | |
412 | ||
413 | Common comparison operators are supported:: | |
414 | ||
415 | >>> hd1 = Size(gb=400) | |
416 | >>> hd2 = Size(gb=500) | |
417 | >>> hd1 > hd2 | |
418 | False | |
419 | >>> hd1 < hd2 | |
420 | True | |
421 | >>> hd1 == hd2 | |
422 | False | |
423 | >>> hd1 == Size(gb=400) | |
424 | True | |
425 | ||
426 | The Size object can also be multiplied or divided:: | |
427 | ||
428 | >>> hd1 | |
429 | <Size(400.00 GB)> | |
430 | >>> hd1 * 2 | |
431 | <Size(800.00 GB)> | |
432 | >>> hd1 | |
433 | <Size(800.00 GB)> | |
434 | ||
435 | Additions and subtractions are only supported between Size objects:: | |
436 | ||
437 | >>> Size(gb=224) - Size(gb=100) | |
438 | <Size(124.00 GB)> | |
439 | >>> Size(gb=1) + Size(mb=300) | |
440 | <Size(1.29 GB)> | |
441 | ||
442 | Can also display a human-readable representation, with automatic detection | |
443 | on best suited unit, or alternatively, specific unit representation:: | |
444 | ||
445 | >>> s = Size(mb=2211) | |
446 | >>> s | |
447 | <Size(2.16 GB)> | |
448 | >>> s.mb | |
449 | <FloatMB(2211.0)> | |
9f95a23c | 450 | >>> print("Total size: %s" % s.mb) |
1adf2230 | 451 | Total size: 2211.00 MB |
9f95a23c | 452 | >>> print("Total size: %s" % s) |
1adf2230 AA |
453 | Total size: 2.16 GB |
454 | """ | |
455 | ||
92f5a8d4 TL |
456 | @classmethod |
457 | def parse(cls, size): | |
458 | if (len(size) > 2 and | |
459 | size[-2].lower() in ['k', 'm', 'g', 't'] and | |
460 | size[-1].lower() == 'b'): | |
461 | return cls(**{size[-2:].lower(): float(size[0:-2])}) | |
462 | elif size[-1].lower() in ['b', 'k', 'm', 'g', 't']: | |
463 | return cls(**{size[-1].lower(): float(size[0:-1])}) | |
464 | else: | |
465 | return cls(b=float(size)) | |
466 | ||
467 | ||
1adf2230 AA |
468 | def __init__(self, multiplier=1024, **kw): |
469 | self._multiplier = multiplier | |
470 | # create a mapping of units-to-multiplier, skip bytes as that is | |
471 | # calculated initially always and does not need to convert | |
472 | aliases = [ | |
92f5a8d4 TL |
473 | [('k', 'kb', 'kilobytes'), self._multiplier], |
474 | [('m', 'mb', 'megabytes'), self._multiplier ** 2], | |
475 | [('g', 'gb', 'gigabytes'), self._multiplier ** 3], | |
476 | [('t', 'tb', 'terabytes'), self._multiplier ** 4], | |
1adf2230 AA |
477 | ] |
478 | # and mappings for units-to-formatters, including bytes and aliases for | |
479 | # each | |
480 | format_aliases = [ | |
481 | [('b', 'bytes'), FloatB], | |
482 | [('kb', 'kilobytes'), FloatKB], | |
483 | [('mb', 'megabytes'), FloatMB], | |
484 | [('gb', 'gigabytes'), FloatGB], | |
485 | [('tb', 'terabytes'), FloatTB], | |
486 | ] | |
487 | self._formatters = {} | |
488 | for key, value in format_aliases: | |
489 | for alias in key: | |
490 | self._formatters[alias] = value | |
491 | self._factors = {} | |
492 | for key, value in aliases: | |
493 | for alias in key: | |
494 | self._factors[alias] = value | |
495 | ||
496 | for k, v in kw.items(): | |
497 | self._convert(v, k) | |
11fdf7f2 | 498 | # only pursue the first occurrence |
1adf2230 AA |
499 | break |
500 | ||
501 | def _convert(self, size, unit): | |
502 | """ | |
503 | Convert any size down to bytes so that other methods can rely on bytes | |
504 | being available always, regardless of what they pass in, avoiding the | |
505 | need for a mapping of every permutation. | |
506 | """ | |
507 | if unit in ['b', 'bytes']: | |
508 | self._b = size | |
509 | return | |
510 | factor = self._factors[unit] | |
511 | self._b = float(size * factor) | |
512 | ||
513 | def _get_best_format(self): | |
514 | """ | |
515 | Go through all the supported units, and use the first one that is less | |
516 | than 1024. This allows to represent size in the most readable format | |
517 | available | |
518 | """ | |
519 | for unit in ['b', 'kb', 'mb', 'gb', 'tb']: | |
520 | if getattr(self, unit) > 1024: | |
521 | continue | |
522 | return getattr(self, unit) | |
523 | ||
524 | def __repr__(self): | |
525 | return "<Size(%s)>" % self._get_best_format() | |
526 | ||
527 | def __str__(self): | |
528 | return "%s" % self._get_best_format() | |
529 | ||
11fdf7f2 TL |
530 | def __format__(self, spec): |
531 | return str(self._get_best_format()).__format__(spec) | |
532 | ||
92f5a8d4 TL |
533 | def __int__(self): |
534 | return int(self._b) | |
535 | ||
536 | def __float__(self): | |
537 | return self._b | |
538 | ||
1adf2230 | 539 | def __lt__(self, other): |
92f5a8d4 TL |
540 | if isinstance(other, Size): |
541 | return self._b < other._b | |
542 | else: | |
543 | return self.b < other | |
1adf2230 AA |
544 | |
545 | def __le__(self, other): | |
92f5a8d4 TL |
546 | if isinstance(other, Size): |
547 | return self._b <= other._b | |
548 | else: | |
549 | return self.b <= other | |
1adf2230 AA |
550 | |
551 | def __eq__(self, other): | |
92f5a8d4 TL |
552 | if isinstance(other, Size): |
553 | return self._b == other._b | |
554 | else: | |
555 | return self.b == other | |
1adf2230 AA |
556 | |
557 | def __ne__(self, other): | |
92f5a8d4 TL |
558 | if isinstance(other, Size): |
559 | return self._b != other._b | |
560 | else: | |
561 | return self.b != other | |
1adf2230 AA |
562 | |
563 | def __ge__(self, other): | |
92f5a8d4 TL |
564 | if isinstance(other, Size): |
565 | return self._b >= other._b | |
566 | else: | |
567 | return self.b >= other | |
1adf2230 AA |
568 | |
569 | def __gt__(self, other): | |
92f5a8d4 TL |
570 | if isinstance(other, Size): |
571 | return self._b > other._b | |
572 | else: | |
573 | return self.b > other | |
1adf2230 AA |
574 | |
575 | def __add__(self, other): | |
576 | if isinstance(other, Size): | |
577 | _b = self._b + other._b | |
578 | return Size(b=_b) | |
579 | raise TypeError('Cannot add "Size" object with int') | |
580 | ||
581 | def __sub__(self, other): | |
582 | if isinstance(other, Size): | |
583 | _b = self._b - other._b | |
584 | return Size(b=_b) | |
585 | raise TypeError('Cannot subtract "Size" object from int') | |
586 | ||
587 | def __mul__(self, other): | |
588 | if isinstance(other, Size): | |
589 | raise TypeError('Cannot multiply with "Size" object') | |
590 | _b = self._b * other | |
591 | return Size(b=_b) | |
592 | ||
593 | def __truediv__(self, other): | |
594 | if isinstance(other, Size): | |
595 | return self._b / other._b | |
91327a77 AA |
596 | _b = self._b / other |
597 | return Size(b=_b) | |
1adf2230 AA |
598 | |
599 | def __div__(self, other): | |
600 | if isinstance(other, Size): | |
601 | return self._b / other._b | |
91327a77 AA |
602 | _b = self._b / other |
603 | return Size(b=_b) | |
1adf2230 | 604 | |
f91f0fd5 TL |
605 | def __bool__(self): |
606 | return self.b != 0 | |
607 | ||
608 | def __nonzero__(self): | |
609 | return self.__bool__() | |
610 | ||
1adf2230 AA |
611 | def __getattr__(self, unit): |
612 | """ | |
613 | Calculate units on the fly, relies on the fact that ``bytes`` has been | |
614 | converted at instantiation. Units that don't exist will trigger an | |
615 | ``AttributeError`` | |
616 | """ | |
617 | try: | |
618 | formatter = self._formatters[unit] | |
619 | except KeyError: | |
620 | raise AttributeError('Size object has not attribute "%s"' % unit) | |
621 | if unit in ['b', 'bytes']: | |
622 | return formatter(self._b) | |
623 | try: | |
624 | factor = self._factors[unit] | |
625 | except KeyError: | |
626 | raise AttributeError('Size object has not attribute "%s"' % unit) | |
627 | return formatter(float(self._b) / factor) | |
628 | ||
629 | ||
630 | def human_readable_size(size): | |
631 | """ | |
632 | Take a size in bytes, and transform it into a human readable size with up | |
633 | to two decimals of precision. | |
634 | """ | |
635 | suffixes = ['B', 'KB', 'MB', 'GB', 'TB'] | |
636 | suffix_index = 0 | |
637 | while size > 1024: | |
638 | suffix_index += 1 | |
639 | size = size / 1024.0 | |
640 | return "{size:.2f} {suffix}".format( | |
641 | size=size, | |
642 | suffix=suffixes[suffix_index]) | |
643 | ||
644 | ||
81eedcae TL |
645 | def size_from_human_readable(s): |
646 | """ | |
647 | Takes a human readable string and converts into a Size. If no unit is | |
648 | passed, bytes is assumed. | |
649 | """ | |
650 | s = s.replace(' ', '') | |
651 | if s[-1].isdigit(): | |
652 | return Size(b=float(s)) | |
653 | n = float(s[:-1]) | |
654 | if s[-1].lower() == 't': | |
655 | return Size(tb=n) | |
656 | if s[-1].lower() == 'g': | |
657 | return Size(gb=n) | |
658 | if s[-1].lower() == 'm': | |
659 | return Size(mb=n) | |
660 | if s[-1].lower() == 'k': | |
661 | return Size(kb=n) | |
662 | return None | |
663 | ||
664 | ||
1adf2230 AA |
665 | def get_partitions_facts(sys_block_path): |
666 | partition_metadata = {} | |
667 | for folder in os.listdir(sys_block_path): | |
668 | folder_path = os.path.join(sys_block_path, folder) | |
669 | if os.path.exists(os.path.join(folder_path, 'partition')): | |
670 | contents = get_file_contents(os.path.join(folder_path, 'partition')) | |
f64942e4 | 671 | if contents: |
1adf2230 AA |
672 | part = {} |
673 | partname = folder | |
674 | part_sys_block_path = os.path.join(sys_block_path, partname) | |
675 | ||
676 | part['start'] = get_file_contents(part_sys_block_path + "/start", 0) | |
677 | part['sectors'] = get_file_contents(part_sys_block_path + "/size", 0) | |
678 | ||
679 | part['sectorsize'] = get_file_contents( | |
680 | part_sys_block_path + "/queue/logical_block_size") | |
681 | if not part['sectorsize']: | |
682 | part['sectorsize'] = get_file_contents( | |
683 | part_sys_block_path + "/queue/hw_sector_size", 512) | |
92f5a8d4 TL |
684 | part['size'] = float(part['sectors']) * 512 |
685 | part['human_readable_size'] = human_readable_size(float(part['sectors']) * 512) | |
f64942e4 AA |
686 | part['holders'] = [] |
687 | for holder in os.listdir(part_sys_block_path + '/holders'): | |
688 | part['holders'].append(holder) | |
1adf2230 AA |
689 | |
690 | partition_metadata[partname] = part | |
691 | return partition_metadata | |
692 | ||
693 | ||
694 | def is_mapper_device(device_name): | |
695 | return device_name.startswith(('/dev/mapper', '/dev/dm-')) | |
696 | ||
697 | ||
91327a77 AA |
698 | def is_locked_raw_device(disk_path): |
699 | """ | |
700 | A device can be locked by a third party software like a database. | |
701 | To detect that case, the device is opened in Read/Write and exclusive mode | |
702 | """ | |
703 | open_flags = (os.O_RDWR | os.O_EXCL) | |
704 | open_mode = 0 | |
705 | fd = None | |
706 | ||
707 | try: | |
708 | fd = os.open(disk_path, open_flags, open_mode) | |
709 | except OSError: | |
710 | return 1 | |
711 | ||
712 | try: | |
713 | os.close(fd) | |
714 | except OSError: | |
715 | return 1 | |
716 | ||
717 | return 0 | |
718 | ||
719 | ||
92f5a8d4 TL |
720 | def get_block_devs_lsblk(): |
721 | ''' | |
722 | This returns a list of lists with 3 items per inner list. | |
723 | KNAME - reflects the kernel device name , for example /dev/sda or /dev/dm-0 | |
724 | NAME - the device name, for example /dev/sda or | |
725 | /dev/mapper/<vg_name>-<lv_name> | |
726 | TYPE - the block device type: disk, partition, lvm and such | |
727 | ||
728 | ''' | |
729 | cmd = ['lsblk', '-plno', 'KNAME,NAME,TYPE'] | |
730 | stdout, stderr, rc = process.call(cmd) | |
731 | # lsblk returns 1 on failure | |
732 | if rc == 1: | |
733 | raise OSError('lsblk returned failure, stderr: {}'.format(stderr)) | |
734 | return [re.split(r'\s+', line) for line in stdout] | |
735 | ||
736 | ||
737 | def get_devices(_sys_block_path='/sys/block'): | |
1adf2230 | 738 | """ |
92f5a8d4 TL |
739 | Captures all available block devices as reported by lsblk. |
740 | Additional interesting metadata like sectors, size, vendor, | |
741 | solid/rotational, etc. is collected from /sys/block/<device> | |
1adf2230 AA |
742 | |
743 | Returns a dictionary, where keys are the full paths to devices. | |
744 | ||
1adf2230 AA |
745 | ..note:: loop devices, removable media, and logical volumes are never included. |
746 | """ | |
1adf2230 AA |
747 | |
748 | device_facts = {} | |
749 | ||
92f5a8d4 | 750 | block_devs = get_block_devs_lsblk() |
1adf2230 AA |
751 | |
752 | for block in block_devs: | |
92f5a8d4 TL |
753 | devname = os.path.basename(block[0]) |
754 | diskname = block[1] | |
f91f0fd5 | 755 | if block[2] not in ['disk', 'mpath']: |
91327a77 | 756 | continue |
92f5a8d4 TL |
757 | sysdir = os.path.join(_sys_block_path, devname) |
758 | metadata = {} | |
1adf2230 AA |
759 | |
760 | # If the mapper device is a logical volume it gets excluded | |
761 | if is_mapper_device(diskname): | |
cd265ab1 | 762 | if lvm.get_device_lvs(diskname): |
1adf2230 AA |
763 | continue |
764 | ||
92f5a8d4 TL |
765 | # all facts that have no defaults |
766 | # (<name>, <path relative to _sys_block_path>) | |
767 | facts = [('removable', 'removable'), | |
768 | ('ro', 'ro'), | |
769 | ('vendor', 'device/vendor'), | |
770 | ('model', 'device/model'), | |
771 | ('rev', 'device/rev'), | |
772 | ('sas_address', 'device/sas_address'), | |
773 | ('sas_device_handle', 'device/sas_device_handle'), | |
774 | ('support_discard', 'queue/discard_granularity'), | |
775 | ('rotational', 'queue/rotational'), | |
776 | ('nr_requests', 'queue/nr_requests'), | |
777 | ] | |
778 | for key, file_ in facts: | |
779 | metadata[key] = get_file_contents(os.path.join(sysdir, file_)) | |
91327a77 | 780 | |
1adf2230 AA |
781 | metadata['scheduler_mode'] = "" |
782 | scheduler = get_file_contents(sysdir + "/queue/scheduler") | |
783 | if scheduler is not None: | |
784 | m = re.match(r".*?(\[(.*)\])", scheduler) | |
785 | if m: | |
786 | metadata['scheduler_mode'] = m.group(2) | |
787 | ||
92f5a8d4 TL |
788 | metadata['partitions'] = get_partitions_facts(sysdir) |
789 | ||
790 | size = get_file_contents(os.path.join(sysdir, 'size'), 0) | |
791 | ||
792 | metadata['sectors'] = get_file_contents(os.path.join(sysdir, 'sectors'), 0) | |
793 | fallback_sectorsize = get_file_contents(sysdir + "/queue/hw_sector_size", 512) | |
794 | metadata['sectorsize'] = get_file_contents(sysdir + | |
795 | "/queue/logical_block_size", | |
796 | fallback_sectorsize) | |
1adf2230 | 797 | metadata['size'] = float(size) * 512 |
92f5a8d4 | 798 | metadata['human_readable_size'] = human_readable_size(metadata['size']) |
1adf2230 | 799 | metadata['path'] = diskname |
91327a77 | 800 | metadata['locked'] = is_locked_raw_device(metadata['path']) |
1adf2230 AA |
801 | |
802 | device_facts[diskname] = metadata | |
803 | return device_facts | |
522d829b TL |
804 | |
805 | def has_bluestore_label(device_path): | |
806 | isBluestore = False | |
807 | bluestoreDiskSignature = 'bluestore block device' # 22 bytes long | |
808 | ||
809 | # throws OSError on failure | |
810 | logger.info("opening device {} to check for BlueStore label".format(device_path)) | |
811 | with open(device_path, "rb") as fd: | |
812 | # read first 22 bytes looking for bluestore disk signature | |
813 | signature = fd.read(22) | |
814 | if signature.decode('ascii', 'replace') == bluestoreDiskSignature: | |
815 | isBluestore = True | |
816 | ||
817 | return isBluestore |