]> git.proxmox.com Git - ceph.git/blame - ceph/src/ceph-volume/ceph_volume/util/device.py
import ceph 15.2.10
[ceph.git] / ceph / src / ceph-volume / ceph_volume / util / device.py
CommitLineData
91327a77
AA
1# -*- coding: utf-8 -*-
2
1adf2230 3import os
91327a77 4from functools import total_ordering
92f5a8d4 5from ceph_volume import sys_info, process
1adf2230 6from ceph_volume.api import lvm
f91f0fd5
TL
7from ceph_volume.util import disk, system
8from ceph_volume.util.lsmdisk import LSMDisk
494da23a 9from ceph_volume.util.constants import ceph_disk_guids
1adf2230 10
91327a77
AA
11report_template = """
12{dev:<25} {size:<12} {rot!s:<7} {available!s:<9} {model}"""
13
14
f64942e4
AA
15def 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
91327a77
AA
25class Devices(object):
26 """
27 A container for Device instances with reporting
28 """
29
f91f0fd5 30 def __init__(self, filter_for_batch=False, with_lsm=False):
91327a77
AA
31 if not sys_info.devices:
32 sys_info.devices = disk.get_devices()
f91f0fd5 33 self.devices = [Device(k, with_lsm) for k in
91327a77 34 sys_info.devices.keys()]
f91f0fd5
TL
35 if filter_for_batch:
36 self.devices = [d for d in self.devices if d.available_lvm_batch]
91327a77 37
f91f0fd5 38 def pretty_report(self):
91327a77
AA
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)
1adf2230 50
91327a77
AA
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
1adf2230
AA
58class Device(object):
59
91327a77
AA
60 pretty_template = """
61 {attr:<25} {value}"""
62
63 report_fields = [
64 'rejected_reasons',
65 'available',
66 'path',
67 'sys_api',
eafe8130 68 'device_id',
f91f0fd5 69 'lsm_data',
91327a77
AA
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
f91f0fd5
TL
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):
1adf2230
AA
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
91327a77 91 self.lvs = []
f91f0fd5 92 self.vgs = []
91327a77
AA
93 self.vg_name = None
94 self.lv_name = None
1adf2230 95 self.disk_api = {}
91327a77 96 self.blkid_api = {}
1adf2230
AA
97 self.sys_api = {}
98 self._exists = None
99 self._is_lvm_member = None
100 self._parse()
f91f0fd5 101 self.lsm_data = self.fetch_lsm(with_lsm)
92f5a8d4
TL
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
f64942e4 109 self.device_id = self._get_device_id()
91327a77 110
f91f0fd5
TL
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
91327a77
AA
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
1adf2230 140
11fdf7f2
TL
141 def __hash__(self):
142 return hash(self.path)
143
1adf2230 144 def _parse(self):
91327a77
AA
145 if not sys_info.devices:
146 sys_info.devices = disk.get_devices()
147 self.sys_api = sys_info.devices.get(self.abspath, {})
a8e16298
TL
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
91327a77 156
f6b5b4d7
TL
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})
1adf2230
AA
165 if lv:
166 self.lv_api = lv
91327a77 167 self.lvs = [lv]
1adf2230 168 self.abspath = lv.lv_path
91327a77
AA
169 self.vg_name = lv.vg_name
170 self.lv_name = lv.name
1adf2230
AA
171 else:
172 dev = disk.lsblk(self.path)
91327a77 173 self.blkid_api = disk.blkid(self.path)
1adf2230
AA
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
91327a77 180 self.ceph_disk = CephDiskDevice(self)
1adf2230
AA
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
91327a77
AA
192 def pretty_report(self):
193 def format_value(v):
194 if isinstance(v, list):
195 return ', '.join(v)
1adf2230 196 else:
91327a77
AA
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 )
1adf2230 228
91327a77
AA
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
f64942e4
AA
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 """
11fdf7f2 240 props = ['ID_VENDOR', 'ID_MODEL', 'ID_MODEL_ENC', 'ID_SERIAL_SHORT', 'ID_SERIAL',
f64942e4
AA
241 'ID_SCSI_SERIAL']
242 p = disk.udevadm_property(self.abspath, props)
11fdf7f2
TL
243 if p.get('ID_MODEL','').startswith('LVM PV '):
244 p['ID_MODEL'] = p.get('ID_MODEL_ENC', '').replace('\\x20', ' ').strip()
f64942e4
AA
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
91327a77
AA
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():
92f5a8d4
TL
270 vgs = lvm.get_device_vgs(path)
271 if vgs:
272 self.vgs.extend(vgs)
91327a77 273 # a pv can only be in one vg, so this should be safe
92f5a8d4
TL
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]
91327a77 278 self._is_lvm_member = True
92f5a8d4 279 self.lvs.extend(lvm.get_device_lvs(path))
1adf2230
AA
280 return self._is_lvm_member
281
91327a77
AA
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
1adf2230
AA
293 @property
294 def exists(self):
295 return os.path.exists(self.abspath)
296
91327a77
AA
297 @property
298 def has_gpt_headers(self):
299 return self.blkid_api.get("PTTYPE") == "gpt"
300
301 @property
302 def rotational(self):
81eedcae
TL
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'
91327a77
AA
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):
11fdf7f2
TL
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
91327a77 331
1adf2230
AA
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
91327a77
AA
338 @property
339 def is_ceph_disk_member(self):
f64942e4
AA
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
91327a77 348
92f5a8d4
TL
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
1adf2230
AA
358 @property
359 def is_mapper(self):
f64942e4 360 return self.path.startswith(('/dev/mapper', '/dev/dm-'))
1adf2230
AA
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'
f91f0fd5
TL
370 elif self.blkid_api:
371 return self.blkid_api['TYPE'] == 'part'
1adf2230
AA
372 return False
373
374 @property
375 def is_device(self):
f91f0fd5 376 api = None
1adf2230 377 if self.disk_api:
f91f0fd5
TL
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'
f64942e4
AA
384 if is_device or is_disk:
385 return True
1adf2230 386 return False
91327a77 387
f91f0fd5
TL
388 @property
389 def is_acceptable_device(self):
390 return self.is_device or self.is_partition
391
f64942e4
AA
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
91327a77
AA
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
f91f0fd5
TL
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
cd265ab1 457 if self.size % extent_size == 0:
f91f0fd5
TL
458 # If the extent size divides size exactly, deduct on extent for
459 # LVM metadata
460 vg_free -= extent_size
461 return [vg_free]
92f5a8d4
TL
462
463 def _check_generic_reject_reasons(self):
91327a77
AA
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]
f91f0fd5
TL
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")
f64942e4
AA
477 if self.is_ceph_disk_member:
478 rejected.append("Used by ceph-disk")
92f5a8d4
TL
479 if self.has_bluestore_label:
480 rejected.append('Has BlueStore device label')
481 return rejected
482
483 def _check_lvm_reject_reasons(self):
f6b5b4d7
TL
484 rejected = []
485 if self.vgs:
f91f0fd5 486 available_vgs = [vg for vg in self.vgs if int(vg.vg_free_count) > 10]
f6b5b4d7 487 if not available_vgs:
f91f0fd5 488 rejected.append('Insufficient space (<10 extents) on vgs')
f6b5b4d7
TL
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())
92f5a8d4
TL
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')
f64942e4 499
91327a77
AA
500 return len(rejected) == 0, rejected
501
f91f0fd5
TL
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
91327a77
AA
510
511class 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
494da23a
TL
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
91327a77
AA
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
494da23a
TL
549 elif self.parttype in ceph_disk_guids.keys():
550 return True
91327a77
AA
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
494da23a
TL
565 label = ceph_disk_guids.get(self.parttype, {})
566 return label.get('type', 'unknown').split('.')[-1]