]> git.proxmox.com Git - ceph.git/blob - ceph/src/ceph-volume/ceph_volume/util/device.py
update ceph source to reef 18.2.1
[ceph.git] / ceph / src / ceph-volume / ceph_volume / util / device.py
1 # -*- coding: utf-8 -*-
2
3 import logging
4 import os
5 from functools import total_ordering
6 from ceph_volume import sys_info
7 from ceph_volume.api import lvm
8 from ceph_volume.util import disk, system
9 from ceph_volume.util.lsmdisk import LSMDisk
10 from ceph_volume.util.constants import ceph_disk_guids
11 from ceph_volume.util.disk import allow_loop_devices
12
13
14 logger = logging.getLogger(__name__)
15
16
17 report_template = """
18 {dev:<25} {size:<12} {device_nodes:<15} {rot!s:<7} {available!s:<9} {model}"""
19
20
21 def encryption_status(abspath):
22 """
23 Helper function to run ``encryption.status()``. It is done here to avoid
24 a circular import issue (encryption module imports from this module) and to
25 ease testing by allowing monkeypatching of this function.
26 """
27 from ceph_volume.util import encryption
28 return encryption.status(abspath)
29
30
31 class Devices(object):
32 """
33 A container for Device instances with reporting
34 """
35
36 def __init__(self,
37 filter_for_batch=False,
38 with_lsm=False,
39 list_all=False):
40 lvs = lvm.get_lvs()
41 lsblk_all = disk.lsblk_all()
42 all_devices_vgs = lvm.get_all_devices_vgs()
43 if not sys_info.devices:
44 sys_info.devices = disk.get_devices()
45 self._devices = [Device(k,
46 with_lsm,
47 lvs=lvs,
48 lsblk_all=lsblk_all,
49 all_devices_vgs=all_devices_vgs) for k in
50 sys_info.devices.keys()]
51 self.devices = []
52 for device in self._devices:
53 if filter_for_batch and not device.available_lvm_batch:
54 continue
55 if device.is_lv and not list_all:
56 continue
57 if device.is_partition and not list_all:
58 continue
59 self.devices.append(device)
60
61 def pretty_report(self):
62 output = [
63 report_template.format(
64 dev='Device Path',
65 size='Size',
66 rot='rotates',
67 model='Model name',
68 available='available',
69 device_nodes='Device nodes',
70
71 )]
72 for device in sorted(self.devices):
73 output.append(device.report())
74 return ''.join(output)
75
76 def json_report(self):
77 output = []
78 for device in sorted(self.devices):
79 output.append(device.json_report())
80 return output
81
82 @total_ordering
83 class Device(object):
84
85 pretty_template = """
86 {attr:<25} {value}"""
87
88 report_fields = [
89 'ceph_device',
90 'rejected_reasons',
91 'available',
92 'path',
93 'sys_api',
94 'device_id',
95 'lsm_data',
96 ]
97 pretty_report_sys_fields = [
98 'actuators',
99 'human_readable_size',
100 'model',
101 'removable',
102 'ro',
103 'rotational',
104 'sas_address',
105 'scheduler_mode',
106 'vendor',
107 ]
108
109 # define some class variables; mostly to enable the use of autospec in
110 # unittests
111 lvs = []
112
113 def __init__(self, path, with_lsm=False, lvs=None, lsblk_all=None, all_devices_vgs=None):
114 self.path = path
115 # LVs can have a vg/lv path, while disks will have /dev/sda
116 self.symlink = None
117 # check if we are a symlink
118 if os.path.islink(self.path):
119 self.symlink = self.path
120 real_path = os.path.realpath(self.path)
121 # check if we are not a device mapper
122 if "dm-" not in real_path:
123 self.path = real_path
124 if not sys_info.devices:
125 if self.path:
126 sys_info.devices = disk.get_devices(device=self.path)
127 else:
128 sys_info.devices = disk.get_devices()
129 if sys_info.devices.get(self.path, {}):
130 self.device_nodes = sys_info.devices[self.path]['device_nodes']
131 self.sys_api = sys_info.devices.get(self.path, {})
132 self.partitions = self._get_partitions()
133 self.lv_api = None
134 self.lvs = [] if not lvs else lvs
135 self.lsblk_all = lsblk_all
136 self.all_devices_vgs = all_devices_vgs
137 self.vgs = []
138 self.vg_name = None
139 self.lv_name = None
140 self.disk_api = {}
141 self.blkid_api = None
142 self._exists = None
143 self._is_lvm_member = None
144 self.ceph_device = False
145 self._parse()
146 self.lsm_data = self.fetch_lsm(with_lsm)
147
148 self.available_lvm, self.rejected_reasons_lvm = self._check_lvm_reject_reasons()
149 self.available_raw, self.rejected_reasons_raw = self._check_raw_reject_reasons()
150 self.available = self.available_lvm and self.available_raw
151 self.rejected_reasons = list(set(self.rejected_reasons_lvm +
152 self.rejected_reasons_raw))
153
154 self.device_id = self._get_device_id()
155
156 def fetch_lsm(self, with_lsm):
157 '''
158 Attempt to fetch libstoragemgmt (LSM) metadata, and return to the caller
159 as a dict. An empty dict is passed back to the caller if the target path
160 is not a block device, or lsm is unavailable on the host. Otherwise the
161 json returned will provide LSM attributes, and any associated errors that
162 lsm encountered when probing the device.
163 '''
164 if not with_lsm or not self.exists or not self.is_device:
165 return {}
166
167 lsm_disk = LSMDisk(self.path)
168
169 return lsm_disk.json_report()
170
171 def __lt__(self, other):
172 '''
173 Implementing this method and __eq__ allows the @total_ordering
174 decorator to turn the Device class into a totally ordered type.
175 This can slower then implementing all comparison operations.
176 This sorting should put available devices before unavailable devices
177 and sort on the path otherwise (str sorting).
178 '''
179 if self.available == other.available:
180 return self.path < other.path
181 return self.available and not other.available
182
183 def __eq__(self, other):
184 return self.path == other.path
185
186 def __hash__(self):
187 return hash(self.path)
188
189 def load_blkid_api(self):
190 if self.blkid_api is None:
191 self.blkid_api = disk.blkid(self.path)
192
193 def _parse(self):
194 lv = None
195 if not self.sys_api:
196 # if no device was found check if we are a partition
197 partname = self.path.split('/')[-1]
198 for device, info in sys_info.devices.items():
199 part = info['partitions'].get(partname, {})
200 if part:
201 self.sys_api = part
202 break
203
204 if self.lvs:
205 for _lv in self.lvs:
206 # if the path is not absolute, we have 'vg/lv', let's use LV name
207 # to get the LV.
208 if self.path[0] == '/':
209 if _lv.lv_path == self.path:
210 lv = _lv
211 break
212 else:
213 vgname, lvname = self.path.split('/')
214 if _lv.lv_name == lvname and _lv.vg_name == vgname:
215 lv = _lv
216 break
217 else:
218 if self.path[0] == '/':
219 lv = lvm.get_single_lv(filters={'lv_path': self.path})
220 else:
221 vgname, lvname = self.path.split('/')
222 lv = lvm.get_single_lv(filters={'lv_name': lvname,
223 'vg_name': vgname})
224
225 if lv:
226 self.lv_api = lv
227 self.lvs = [lv]
228 self.path = lv.lv_path
229 self.vg_name = lv.vg_name
230 self.lv_name = lv.name
231 self.ceph_device = lvm.is_ceph_device(lv)
232 else:
233 self.lvs = []
234 if self.lsblk_all:
235 for dev in self.lsblk_all:
236 if dev['NAME'] == os.path.basename(self.path):
237 break
238 else:
239 dev = disk.lsblk(self.path)
240 self.disk_api = dev
241 device_type = dev.get('TYPE', '')
242 # always check is this is an lvm member
243 valid_types = ['part', 'disk', 'mpath']
244 if allow_loop_devices():
245 valid_types.append('loop')
246 if device_type in valid_types:
247 self._set_lvm_membership()
248
249 self.ceph_disk = CephDiskDevice(self)
250
251 def __repr__(self):
252 prefix = 'Unknown'
253 if self.is_lv:
254 prefix = 'LV'
255 elif self.is_partition:
256 prefix = 'Partition'
257 elif self.is_device:
258 prefix = 'Raw Device'
259 return '<%s: %s>' % (prefix, self.path)
260
261 def pretty_report(self):
262 def format_value(v):
263 if isinstance(v, list):
264 return ', '.join(v)
265 else:
266 return v
267 def format_key(k):
268 return k.strip('_').replace('_', ' ')
269 output = ['\n====== Device report {} ======\n'.format(self.path)]
270 output.extend(
271 [self.pretty_template.format(
272 attr=format_key(k),
273 value=format_value(v)) for k, v in vars(self).items() if k in
274 self.report_fields and k != 'disk_api' and k != 'sys_api'] )
275 output.extend(
276 [self.pretty_template.format(
277 attr=format_key(k),
278 value=format_value(v)) for k, v in self.sys_api.items() if k in
279 self.pretty_report_sys_fields])
280 for lv in self.lvs:
281 output.append("""
282 --- Logical Volume ---""")
283 output.extend(
284 [self.pretty_template.format(
285 attr=format_key(k),
286 value=format_value(v)) for k, v in lv.report().items()])
287 return ''.join(output)
288
289 def report(self):
290 return report_template.format(
291 dev=self.path,
292 size=self.size_human,
293 rot=self.rotational,
294 available=self.available,
295 model=self.model,
296 device_nodes=self.device_nodes
297 )
298
299 def json_report(self):
300 output = {k.strip('_'): v for k, v in vars(self).items() if k in
301 self.report_fields}
302 output['lvs'] = [lv.report() for lv in self.lvs]
303 return output
304
305 def _get_device_id(self):
306 """
307 Please keep this implementation in sync with get_device_id() in
308 src/common/blkdev.cc
309 """
310 props = ['ID_VENDOR', 'ID_MODEL', 'ID_MODEL_ENC', 'ID_SERIAL_SHORT', 'ID_SERIAL',
311 'ID_SCSI_SERIAL']
312 p = disk.udevadm_property(self.path, props)
313 if p.get('ID_MODEL','').startswith('LVM PV '):
314 p['ID_MODEL'] = p.get('ID_MODEL_ENC', '').replace('\\x20', ' ').strip()
315 if 'ID_VENDOR' in p and 'ID_MODEL' in p and 'ID_SCSI_SERIAL' in p:
316 dev_id = '_'.join([p['ID_VENDOR'], p['ID_MODEL'],
317 p['ID_SCSI_SERIAL']])
318 elif 'ID_MODEL' in p and 'ID_SERIAL_SHORT' in p:
319 dev_id = '_'.join([p['ID_MODEL'], p['ID_SERIAL_SHORT']])
320 elif 'ID_SERIAL' in p:
321 dev_id = p['ID_SERIAL']
322 if dev_id.startswith('MTFD'):
323 # Micron NVMes hide the vendor
324 dev_id = 'Micron_' + dev_id
325 else:
326 # the else branch should fallback to using sysfs and ioctl to
327 # retrieve device_id on FreeBSD. Still figuring out if/how the
328 # python ioctl implementation does that on FreeBSD
329 dev_id = ''
330 dev_id = dev_id.replace(' ', '_')
331 while '__' in dev_id:
332 dev_id = dev_id.replace('__', '_')
333 return dev_id
334
335 def _set_lvm_membership(self):
336 if self._is_lvm_member is None:
337 # this is contentious, if a PV is recognized by LVM but has no
338 # VGs, should we consider it as part of LVM? We choose not to
339 # here, because most likely, we need to use VGs from this PV.
340 self._is_lvm_member = False
341 device_to_check = [self.path]
342 device_to_check.extend(self.partitions)
343
344 # a pv can only be in one vg, so this should be safe
345 # FIXME: While the above assumption holds, sda1 and sda2
346 # can each host a PV and VG. I think the vg_name property is
347 # actually unused (not 100% sure) and can simply be removed
348 vgs = None
349 if not self.all_devices_vgs:
350 self.all_devices_vgs = lvm.get_all_devices_vgs()
351 for path in device_to_check:
352 for dev_vg in self.all_devices_vgs:
353 if dev_vg.pv_name == path:
354 vgs = [dev_vg]
355 if vgs:
356 self.vgs.extend(vgs)
357 self.vg_name = vgs[0]
358 self._is_lvm_member = True
359 self.lvs.extend(lvm.get_device_lvs(path))
360 if self.lvs:
361 self.ceph_device = any([True if lv.tags.get('ceph.osd_id') else False for lv in self.lvs])
362
363 def _get_partitions(self):
364 """
365 For block devices LVM can reside on the raw block device or on a
366 partition. Return a list of paths to be checked for a pv.
367 """
368 partitions = []
369 path_dir = os.path.dirname(self.path)
370 for partition in self.sys_api.get('partitions', {}).keys():
371 partitions.append(os.path.join(path_dir, partition))
372 return partitions
373
374 @property
375 def exists(self):
376 return os.path.exists(self.path)
377
378 @property
379 def has_fs(self):
380 self.load_blkid_api()
381 return 'TYPE' in self.blkid_api
382
383 @property
384 def has_gpt_headers(self):
385 self.load_blkid_api()
386 return self.blkid_api.get("PTTYPE") == "gpt"
387
388 @property
389 def rotational(self):
390 rotational = self.sys_api.get('rotational')
391 if rotational is None:
392 # fall back to lsblk if not found in sys_api
393 # default to '1' if no value is found with lsblk either
394 rotational = self.disk_api.get('ROTA', '1')
395 return rotational == '1'
396
397 @property
398 def model(self):
399 return self.sys_api['model']
400
401 @property
402 def size_human(self):
403 return self.sys_api['human_readable_size']
404
405 @property
406 def size(self):
407 return self.sys_api['size']
408
409 @property
410 def parent_device(self):
411 if 'PKNAME' in self.disk_api:
412 return '/dev/%s' % self.disk_api['PKNAME']
413 return None
414
415 @property
416 def lvm_size(self):
417 """
418 If this device was made into a PV it would lose 1GB in total size
419 due to the 1GB physical extent size we set when creating volume groups
420 """
421 size = disk.Size(b=self.size)
422 lvm_size = disk.Size(gb=size.gb.as_int()) - disk.Size(gb=1)
423 return lvm_size
424
425 @property
426 def is_lvm_member(self):
427 if self._is_lvm_member is None:
428 self._set_lvm_membership()
429 return self._is_lvm_member
430
431 @property
432 def is_ceph_disk_member(self):
433 def is_member(device):
434 return 'ceph' in device.get('PARTLABEL', '') or \
435 device.get('PARTTYPE', '') in ceph_disk_guids.keys()
436 # If we come from Devices(), self.lsblk_all is set already.
437 # Otherwise, we have to grab the data.
438 details = self.lsblk_all or disk.lsblk_all()
439 _is_member = False
440 if self.sys_api.get("partitions"):
441 for part in self.sys_api.get("partitions").keys():
442 for dev in details:
443 if part.startswith(dev['NAME']):
444 if is_member(dev):
445 _is_member = True
446 return _is_member
447 else:
448 return is_member(self.disk_api)
449 raise RuntimeError(f"Couln't check if device {self.path} is a ceph-disk member.")
450
451 @property
452 def has_bluestore_label(self):
453 return disk.has_bluestore_label(self.path)
454
455 @property
456 def is_mapper(self):
457 return self.path.startswith(('/dev/mapper', '/dev/dm-'))
458
459 @property
460 def device_type(self):
461 self.load_blkid_api()
462 if 'type' in self.sys_api:
463 return self.sys_api['type']
464 elif self.disk_api:
465 return self.disk_api['TYPE']
466 elif self.blkid_api:
467 return self.blkid_api['TYPE']
468
469 @property
470 def is_mpath(self):
471 return self.device_type == 'mpath'
472
473 @property
474 def is_lv(self):
475 return self.lv_api is not None
476
477 @property
478 def is_partition(self):
479 self.load_blkid_api()
480 if self.disk_api:
481 return self.disk_api['TYPE'] == 'part'
482 elif self.blkid_api:
483 return self.blkid_api['TYPE'] == 'part'
484 return False
485
486 @property
487 def is_device(self):
488 self.load_blkid_api()
489 api = None
490 if self.disk_api:
491 api = self.disk_api
492 elif self.blkid_api:
493 api = self.blkid_api
494 if api:
495 valid_types = ['disk', 'device', 'mpath']
496 if allow_loop_devices():
497 valid_types.append('loop')
498 return self.device_type in valid_types
499 return False
500
501 @property
502 def is_acceptable_device(self):
503 return self.is_device or self.is_partition or self.is_lv
504
505 @property
506 def is_encrypted(self):
507 """
508 Only correct for LVs, device mappers, and partitions. Will report a ``None``
509 for raw devices.
510 """
511 self.load_blkid_api()
512 crypt_reports = [self.blkid_api.get('TYPE', ''), self.disk_api.get('FSTYPE', '')]
513 if self.is_lv:
514 # if disk APIs are reporting this is encrypted use that:
515 if 'crypto_LUKS' in crypt_reports:
516 return True
517 # if ceph-volume created this, then a tag would let us know
518 elif self.lv_api.encrypted:
519 return True
520 return False
521 elif self.is_partition:
522 return 'crypto_LUKS' in crypt_reports
523 elif self.is_mapper:
524 active_mapper = encryption_status(self.path)
525 if active_mapper:
526 # normalize a bit to ensure same values regardless of source
527 encryption_type = active_mapper['type'].lower().strip('12') # turn LUKS1 or LUKS2 into luks
528 return True if encryption_type in ['plain', 'luks'] else False
529 else:
530 return False
531 else:
532 return None
533
534 @property
535 def used_by_ceph(self):
536 # only filter out data devices as journals could potentially be reused
537 osd_ids = [lv.tags.get("ceph.osd_id") is not None for lv in self.lvs
538 if lv.tags.get("ceph.type") in ["data", "block"]]
539 return any(osd_ids)
540
541 @property
542 def journal_used_by_ceph(self):
543 # similar to used_by_ceph() above. This is for 'journal' devices (db/wal/..)
544 # needed by get_lvm_fast_allocs() in devices/lvm/batch.py
545 # see https://tracker.ceph.com/issues/59640
546 osd_ids = [lv.tags.get("ceph.osd_id") is not None for lv in self.lvs
547 if lv.tags.get("ceph.type") in ["db", "wal"]]
548 return any(osd_ids)
549
550 @property
551 def vg_free_percent(self):
552 if self.vgs:
553 return [vg.free_percent for vg in self.vgs]
554 else:
555 return [1]
556
557 @property
558 def vg_size(self):
559 if self.vgs:
560 return [vg.size for vg in self.vgs]
561 else:
562 # TODO fix this...we can probably get rid of vg_free
563 return self.vg_free
564
565 @property
566 def vg_free(self):
567 '''
568 Returns the free space in all VGs on this device. If no VGs are
569 present, returns the disk size.
570 '''
571 if self.vgs:
572 return [vg.free for vg in self.vgs]
573 else:
574 # We could also query 'lvmconfig
575 # --typeconfig full' and use allocations -> physical_extent_size
576 # value to project the space for a vg
577 # assuming 4M extents here
578 extent_size = 4194304
579 vg_free = int(self.size / extent_size) * extent_size
580 if self.size % extent_size == 0:
581 # If the extent size divides size exactly, deduct on extent for
582 # LVM metadata
583 vg_free -= extent_size
584 return [vg_free]
585
586 @property
587 def has_partitions(self):
588 '''
589 Boolean to determine if a given device has partitions.
590 '''
591 if self.sys_api.get('partitions'):
592 return True
593 return False
594
595 def _check_generic_reject_reasons(self):
596 reasons = [
597 ('removable', 1, 'removable'),
598 ('ro', 1, 'read-only'),
599 ]
600 rejected = [reason for (k, v, reason) in reasons if
601 self.sys_api.get(k, '') == v]
602 if self.is_acceptable_device:
603 # reject disks smaller than 5GB
604 if int(self.sys_api.get('size', 0)) < 5368709120:
605 rejected.append('Insufficient space (<5GB)')
606 else:
607 rejected.append("Device type is not acceptable. It should be raw device or partition")
608 if self.is_ceph_disk_member:
609 rejected.append("Used by ceph-disk")
610
611 try:
612 if self.has_bluestore_label:
613 rejected.append('Has BlueStore device label')
614 except OSError as e:
615 # likely failed to open the device. assuming it is BlueStore is the safest option
616 # so that a possibly-already-existing OSD doesn't get overwritten
617 logger.error('failed to determine if device {} is BlueStore. device should not be used to avoid false negatives. err: {}'.format(self.path, e))
618 rejected.append('Failed to determine if device is BlueStore')
619
620 if self.is_partition:
621 try:
622 if disk.has_bluestore_label(self.parent_device):
623 rejected.append('Parent has BlueStore device label')
624 except OSError as e:
625 # likely failed to open the device. assuming the parent is BlueStore is the safest
626 # option so that a possibly-already-existing OSD doesn't get overwritten
627 logger.error('failed to determine if partition {} (parent: {}) has a BlueStore parent. partition should not be used to avoid false negatives. err: {}'.format(self.path, self.parent_device, e))
628 rejected.append('Failed to determine if parent device is BlueStore')
629
630 if self.has_gpt_headers:
631 rejected.append('Has GPT headers')
632 if self.has_partitions:
633 rejected.append('Has partitions')
634 if self.has_fs:
635 rejected.append('Has a FileSystem')
636 return rejected
637
638 def _check_lvm_reject_reasons(self):
639 rejected = []
640 if self.vgs:
641 available_vgs = [vg for vg in self.vgs if int(vg.vg_free_count) > 10]
642 if not available_vgs:
643 rejected.append('Insufficient space (<10 extents) on vgs')
644 else:
645 # only check generic if no vgs are present. Vgs might hold lvs and
646 # that might cause 'locked' to trigger
647 rejected.extend(self._check_generic_reject_reasons())
648
649 return len(rejected) == 0, rejected
650
651 def _check_raw_reject_reasons(self):
652 rejected = self._check_generic_reject_reasons()
653 if len(self.vgs) > 0:
654 rejected.append('LVM detected')
655
656 return len(rejected) == 0, rejected
657
658 @property
659 def available_lvm_batch(self):
660 if self.sys_api.get("partitions"):
661 return False
662 if system.device_is_mounted(self.path):
663 return False
664 return self.is_device or self.is_lv
665
666
667 class CephDiskDevice(object):
668 """
669 Detect devices that have been created by ceph-disk, report their type
670 (journal, data, etc..). Requires a ``Device`` object as input.
671 """
672
673 def __init__(self, device):
674 self.device = device
675 self._is_ceph_disk_member = None
676
677 @property
678 def partlabel(self):
679 """
680 In containers, the 'PARTLABEL' attribute might not be detected
681 correctly via ``lsblk``, so we poke at the value with ``lsblk`` first,
682 falling back to ``blkid`` (which works correclty in containers).
683 """
684 lsblk_partlabel = self.device.disk_api.get('PARTLABEL')
685 if lsblk_partlabel:
686 return lsblk_partlabel
687 return self.device.blkid_api.get('PARTLABEL', '')
688
689 @property
690 def parttype(self):
691 """
692 Seems like older version do not detect PARTTYPE correctly (assuming the
693 info in util/disk.py#lsblk is still valid).
694 SImply resolve to using blkid since lsblk will throw an error if asked
695 for an unknown columns
696 """
697 return self.device.blkid_api.get('PARTTYPE', '')
698
699 @property
700 def is_member(self):
701 if self._is_ceph_disk_member is None:
702 if 'ceph' in self.partlabel:
703 self._is_ceph_disk_member = True
704 return True
705 elif self.parttype in ceph_disk_guids.keys():
706 return True
707 return False
708 return self._is_ceph_disk_member
709
710 @property
711 def type(self):
712 types = [
713 'data', 'wal', 'db', 'lockbox', 'journal',
714 # ceph-disk uses 'ceph block' when placing data in bluestore, but
715 # keeps the regular OSD files in 'ceph data' :( :( :( :(
716 'block',
717 ]
718 for t in types:
719 if t in self.partlabel:
720 return t
721 label = ceph_disk_guids.get(self.parttype, {})
722 return label.get('type', 'unknown').split('.')[-1]