]> git.proxmox.com Git - ceph.git/blame - ceph/src/ceph-volume/ceph_volume/util/device.py
import ceph 14.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
1adf2230
AA
5from ceph_volume import sys_info
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
91327a77
AA
83 self.lvs = []
84 self.vg_name = None
85 self.lv_name = None
1adf2230
AA
86 self.pvs_api = []
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()
91327a77 93 self.available, self.rejected_reasons = self._check_reject_reasons()
f64942e4 94 self.device_id = self._get_device_id()
91327a77
AA
95
96 def __lt__(self, other):
97 '''
98 Implementing this method and __eq__ allows the @total_ordering
99 decorator to turn the Device class into a totally ordered type.
100 This can slower then implementing all comparison operations.
101 This sorting should put available devices before unavailable devices
102 and sort on the path otherwise (str sorting).
103 '''
104 if self.available == other.available:
105 return self.path < other.path
106 return self.available and not other.available
107
108 def __eq__(self, other):
109 return self.path == other.path
1adf2230 110
11fdf7f2
TL
111 def __hash__(self):
112 return hash(self.path)
113
1adf2230 114 def _parse(self):
91327a77
AA
115 if not sys_info.devices:
116 sys_info.devices = disk.get_devices()
117 self.sys_api = sys_info.devices.get(self.abspath, {})
a8e16298
TL
118 if not self.sys_api:
119 # if no device was found check if we are a partition
120 partname = self.abspath.split('/')[-1]
121 for device, info in sys_info.devices.items():
122 part = info['partitions'].get(partname, {})
123 if part:
124 self.sys_api = part
125 break
91327a77 126
1adf2230
AA
127 # start with lvm since it can use an absolute or relative path
128 lv = lvm.get_lv_from_argument(self.path)
129 if lv:
130 self.lv_api = lv
91327a77 131 self.lvs = [lv]
1adf2230 132 self.abspath = lv.lv_path
91327a77
AA
133 self.vg_name = lv.vg_name
134 self.lv_name = lv.name
1adf2230
AA
135 else:
136 dev = disk.lsblk(self.path)
91327a77 137 self.blkid_api = disk.blkid(self.path)
1adf2230
AA
138 self.disk_api = dev
139 device_type = dev.get('TYPE', '')
140 # always check is this is an lvm member
141 if device_type in ['part', 'disk']:
142 self._set_lvm_membership()
143
91327a77 144 self.ceph_disk = CephDiskDevice(self)
1adf2230
AA
145
146 def __repr__(self):
147 prefix = 'Unknown'
148 if self.is_lv:
149 prefix = 'LV'
150 elif self.is_partition:
151 prefix = 'Partition'
152 elif self.is_device:
153 prefix = 'Raw Device'
154 return '<%s: %s>' % (prefix, self.abspath)
155
91327a77
AA
156 def pretty_report(self):
157 def format_value(v):
158 if isinstance(v, list):
159 return ', '.join(v)
1adf2230 160 else:
91327a77
AA
161 return v
162 def format_key(k):
163 return k.strip('_').replace('_', ' ')
164 output = ['\n====== Device report {} ======\n'.format(self.path)]
165 output.extend(
166 [self.pretty_template.format(
167 attr=format_key(k),
168 value=format_value(v)) for k, v in vars(self).items() if k in
169 self.report_fields and k != 'disk_api' and k != 'sys_api'] )
170 output.extend(
171 [self.pretty_template.format(
172 attr=format_key(k),
173 value=format_value(v)) for k, v in self.sys_api.items() if k in
174 self.pretty_report_sys_fields])
175 for lv in self.lvs:
176 output.append("""
177 --- Logical Volume ---""")
178 output.extend(
179 [self.pretty_template.format(
180 attr=format_key(k),
181 value=format_value(v)) for k, v in lv.report().items()])
182 return ''.join(output)
183
184 def report(self):
185 return report_template.format(
186 dev=self.abspath,
187 size=self.size_human,
188 rot=self.rotational,
189 available=self.available,
190 model=self.model,
191 )
1adf2230 192
91327a77
AA
193 def json_report(self):
194 output = {k.strip('_'): v for k, v in vars(self).items() if k in
195 self.report_fields}
196 output['lvs'] = [lv.report() for lv in self.lvs]
197 return output
198
f64942e4
AA
199 def _get_device_id(self):
200 """
201 Please keep this implementation in sync with get_device_id() in
202 src/common/blkdev.cc
203 """
11fdf7f2 204 props = ['ID_VENDOR', 'ID_MODEL', 'ID_MODEL_ENC', 'ID_SERIAL_SHORT', 'ID_SERIAL',
f64942e4
AA
205 'ID_SCSI_SERIAL']
206 p = disk.udevadm_property(self.abspath, props)
11fdf7f2
TL
207 if p.get('ID_MODEL','').startswith('LVM PV '):
208 p['ID_MODEL'] = p.get('ID_MODEL_ENC', '').replace('\\x20', ' ').strip()
f64942e4
AA
209 if 'ID_VENDOR' in p and 'ID_MODEL' in p and 'ID_SCSI_SERIAL' in p:
210 dev_id = '_'.join([p['ID_VENDOR'], p['ID_MODEL'],
211 p['ID_SCSI_SERIAL']])
212 elif 'ID_MODEL' in p and 'ID_SERIAL_SHORT' in p:
213 dev_id = '_'.join([p['ID_MODEL'], p['ID_SERIAL_SHORT']])
214 elif 'ID_SERIAL' in p:
215 dev_id = p['ID_SERIAL']
216 if dev_id.startswith('MTFD'):
217 # Micron NVMes hide the vendor
218 dev_id = 'Micron_' + dev_id
219 else:
220 # the else branch should fallback to using sysfs and ioctl to
221 # retrieve device_id on FreeBSD. Still figuring out if/how the
222 # python ioctl implementation does that on FreeBSD
223 dev_id = ''
224 dev_id.replace(' ', '_')
225 return dev_id
226
91327a77
AA
227 def _set_lvm_membership(self):
228 if self._is_lvm_member is None:
229 # this is contentious, if a PV is recognized by LVM but has no
230 # VGs, should we consider it as part of LVM? We choose not to
231 # here, because most likely, we need to use VGs from this PV.
232 self._is_lvm_member = False
233 for path in self._get_pv_paths():
234 # check if there was a pv created with the
235 # name of device
eafe8130 236 pvs = lvm.PVolumes().filter(pv_name=path)
91327a77
AA
237 has_vgs = [pv.vg_name for pv in pvs if pv.vg_name]
238 if has_vgs:
f64942e4 239 self.vgs = list(set(has_vgs))
91327a77
AA
240 # a pv can only be in one vg, so this should be safe
241 self.vg_name = has_vgs[0]
242 self._is_lvm_member = True
243 self.pvs_api = pvs
244 for pv in pvs:
245 if pv.vg_name and pv.lv_uuid:
246 lv = lvm.get_lv(vg_name=pv.vg_name, lv_uuid=pv.lv_uuid)
247 if lv:
248 self.lvs.append(lv)
f64942e4
AA
249 else:
250 self.vgs = []
1adf2230
AA
251 return self._is_lvm_member
252
91327a77
AA
253 def _get_pv_paths(self):
254 """
255 For block devices LVM can reside on the raw block device or on a
256 partition. Return a list of paths to be checked for a pv.
257 """
258 paths = [self.abspath]
259 path_dir = os.path.dirname(self.abspath)
260 for part in self.sys_api.get('partitions', {}).keys():
261 paths.append(os.path.join(path_dir, part))
262 return paths
263
1adf2230
AA
264 @property
265 def exists(self):
266 return os.path.exists(self.abspath)
267
91327a77
AA
268 @property
269 def has_gpt_headers(self):
270 return self.blkid_api.get("PTTYPE") == "gpt"
271
272 @property
273 def rotational(self):
81eedcae
TL
274 rotational = self.sys_api.get('rotational')
275 if rotational is None:
276 # fall back to lsblk if not found in sys_api
277 # default to '1' if no value is found with lsblk either
278 rotational = self.disk_api.get('ROTA', '1')
279 return rotational == '1'
91327a77
AA
280
281 @property
282 def model(self):
283 return self.sys_api['model']
284
285 @property
286 def size_human(self):
287 return self.sys_api['human_readable_size']
288
289 @property
290 def size(self):
11fdf7f2
TL
291 return self.sys_api['size']
292
293 @property
294 def lvm_size(self):
295 """
296 If this device was made into a PV it would lose 1GB in total size
297 due to the 1GB physical extent size we set when creating volume groups
298 """
299 size = disk.Size(b=self.size)
300 lvm_size = disk.Size(gb=size.gb.as_int()) - disk.Size(gb=1)
301 return lvm_size
91327a77 302
1adf2230
AA
303 @property
304 def is_lvm_member(self):
305 if self._is_lvm_member is None:
306 self._set_lvm_membership()
307 return self._is_lvm_member
308
91327a77
AA
309 @property
310 def is_ceph_disk_member(self):
f64942e4
AA
311 is_member = self.ceph_disk.is_member
312 if self.sys_api.get("partitions"):
313 for part in self.sys_api.get("partitions").keys():
314 part = Device("/dev/%s" % part)
315 if part.is_ceph_disk_member:
316 is_member = True
317 break
318 return is_member
91327a77 319
1adf2230
AA
320 @property
321 def is_mapper(self):
f64942e4 322 return self.path.startswith(('/dev/mapper', '/dev/dm-'))
1adf2230
AA
323
324 @property
325 def is_lv(self):
326 return self.lv_api is not None
327
328 @property
329 def is_partition(self):
330 if self.disk_api:
331 return self.disk_api['TYPE'] == 'part'
332 return False
333
334 @property
335 def is_device(self):
336 if self.disk_api:
f64942e4
AA
337 is_device = self.disk_api['TYPE'] == 'device'
338 is_disk = self.disk_api['TYPE'] == 'disk'
339 if is_device or is_disk:
340 return True
1adf2230 341 return False
91327a77 342
f64942e4
AA
343 @property
344 def is_encrypted(self):
345 """
346 Only correct for LVs, device mappers, and partitions. Will report a ``None``
347 for raw devices.
348 """
349 crypt_reports = [self.blkid_api.get('TYPE', ''), self.disk_api.get('FSTYPE', '')]
350 if self.is_lv:
351 # if disk APIs are reporting this is encrypted use that:
352 if 'crypto_LUKS' in crypt_reports:
353 return True
354 # if ceph-volume created this, then a tag would let us know
355 elif self.lv_api.encrypted:
356 return True
357 return False
358 elif self.is_partition:
359 return 'crypto_LUKS' in crypt_reports
360 elif self.is_mapper:
361 active_mapper = encryption_status(self.abspath)
362 if active_mapper:
363 # normalize a bit to ensure same values regardless of source
364 encryption_type = active_mapper['type'].lower().strip('12') # turn LUKS1 or LUKS2 into luks
365 return True if encryption_type in ['plain', 'luks'] else False
366 else:
367 return False
368 else:
369 return None
370
91327a77
AA
371 @property
372 def used_by_ceph(self):
373 # only filter out data devices as journals could potentially be reused
374 osd_ids = [lv.tags.get("ceph.osd_id") is not None for lv in self.lvs
375 if lv.tags.get("ceph.type") in ["data", "block"]]
376 return any(osd_ids)
377
378 def _check_reject_reasons(self):
379 """
380 This checks a number of potential reject reasons for a drive and
381 returns a tuple (boolean, list). The first element denotes whether a
382 drive is available or not, the second element lists reasons in case a
383 drive is not available.
384 """
385 reasons = [
386 ('removable', 1, 'removable'),
387 ('ro', 1, 'read-only'),
388 ('locked', 1, 'locked'),
389 ]
390 rejected = [reason for (k, v, reason) in reasons if
391 self.sys_api.get(k, '') == v]
f64942e4
AA
392 if self.is_ceph_disk_member:
393 rejected.append("Used by ceph-disk")
394
91327a77
AA
395 return len(rejected) == 0, rejected
396
397
398class CephDiskDevice(object):
399 """
400 Detect devices that have been created by ceph-disk, report their type
401 (journal, data, etc..). Requires a ``Device`` object as input.
402 """
403
404 def __init__(self, device):
405 self.device = device
406 self._is_ceph_disk_member = None
407
408 @property
409 def partlabel(self):
410 """
411 In containers, the 'PARTLABEL' attribute might not be detected
412 correctly via ``lsblk``, so we poke at the value with ``lsblk`` first,
413 falling back to ``blkid`` (which works correclty in containers).
414 """
415 lsblk_partlabel = self.device.disk_api.get('PARTLABEL')
416 if lsblk_partlabel:
417 return lsblk_partlabel
418 return self.device.blkid_api.get('PARTLABEL', '')
419
494da23a
TL
420 @property
421 def parttype(self):
422 """
423 Seems like older version do not detect PARTTYPE correctly (assuming the
424 info in util/disk.py#lsblk is still valid).
425 SImply resolve to using blkid since lsblk will throw an error if asked
426 for an unknown columns
427 """
428 return self.device.blkid_api.get('PARTTYPE', '')
429
91327a77
AA
430 @property
431 def is_member(self):
432 if self._is_ceph_disk_member is None:
433 if 'ceph' in self.partlabel:
434 self._is_ceph_disk_member = True
435 return True
494da23a
TL
436 elif self.parttype in ceph_disk_guids.keys():
437 return True
91327a77
AA
438 return False
439 return self._is_ceph_disk_member
440
441 @property
442 def type(self):
443 types = [
444 'data', 'wal', 'db', 'lockbox', 'journal',
445 # ceph-disk uses 'ceph block' when placing data in bluestore, but
446 # keeps the regular OSD files in 'ceph data' :( :( :( :(
447 'block',
448 ]
449 for t in types:
450 if t in self.partlabel:
451 return t
494da23a
TL
452 label = ceph_disk_guids.get(self.parttype, {})
453 return label.get('type', 'unknown').split('.')[-1]