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