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