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