]> git.proxmox.com Git - ceph.git/blob - ceph/src/ceph-volume/ceph_volume/util/device.py
update source to Ceph Pacific 16.2.2
[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 % extent_size == 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 if self.has_gpt_headers:
482 rejected.append('Has GPT headers')
483 return rejected
484
485 def _check_lvm_reject_reasons(self):
486 rejected = []
487 if self.vgs:
488 available_vgs = [vg for vg in self.vgs if int(vg.vg_free_count) > 10]
489 if not available_vgs:
490 rejected.append('Insufficient space (<10 extents) on vgs')
491 else:
492 # only check generic if no vgs are present. Vgs might hold lvs and
493 # that might cause 'locked' to trigger
494 rejected.extend(self._check_generic_reject_reasons())
495
496 return len(rejected) == 0, rejected
497
498 def _check_raw_reject_reasons(self):
499 rejected = self._check_generic_reject_reasons()
500 if len(self.vgs) > 0:
501 rejected.append('LVM detected')
502
503 return len(rejected) == 0, rejected
504
505 @property
506 def available_lvm_batch(self):
507 if self.sys_api.get("partitions"):
508 return False
509 if system.device_is_mounted(self.path):
510 return False
511 return self.is_device or self.is_lv
512
513
514 class CephDiskDevice(object):
515 """
516 Detect devices that have been created by ceph-disk, report their type
517 (journal, data, etc..). Requires a ``Device`` object as input.
518 """
519
520 def __init__(self, device):
521 self.device = device
522 self._is_ceph_disk_member = None
523
524 @property
525 def partlabel(self):
526 """
527 In containers, the 'PARTLABEL' attribute might not be detected
528 correctly via ``lsblk``, so we poke at the value with ``lsblk`` first,
529 falling back to ``blkid`` (which works correclty in containers).
530 """
531 lsblk_partlabel = self.device.disk_api.get('PARTLABEL')
532 if lsblk_partlabel:
533 return lsblk_partlabel
534 return self.device.blkid_api.get('PARTLABEL', '')
535
536 @property
537 def parttype(self):
538 """
539 Seems like older version do not detect PARTTYPE correctly (assuming the
540 info in util/disk.py#lsblk is still valid).
541 SImply resolve to using blkid since lsblk will throw an error if asked
542 for an unknown columns
543 """
544 return self.device.blkid_api.get('PARTTYPE', '')
545
546 @property
547 def is_member(self):
548 if self._is_ceph_disk_member is None:
549 if 'ceph' in self.partlabel:
550 self._is_ceph_disk_member = True
551 return True
552 elif self.parttype in ceph_disk_guids.keys():
553 return True
554 return False
555 return self._is_ceph_disk_member
556
557 @property
558 def type(self):
559 types = [
560 'data', 'wal', 'db', 'lockbox', 'journal',
561 # ceph-disk uses 'ceph block' when placing data in bluestore, but
562 # keeps the regular OSD files in 'ceph data' :( :( :( :(
563 'block',
564 ]
565 for t in types:
566 if t in self.partlabel:
567 return t
568 label = ceph_disk_guids.get(self.parttype, {})
569 return label.get('type', 'unknown').split('.')[-1]