]>
git.proxmox.com Git - ceph.git/blob - ceph/src/ceph-volume/ceph_volume/util/device.py
1 # -*- coding: utf-8 -*-
5 from functools
import total_ordering
6 from ceph_volume
import sys_info
7 from ceph_volume
. api
import lvm
8 from ceph_volume
. util
import disk
, system
9 from ceph_volume
. util
. lsmdisk
import LSMDisk
10 from ceph_volume
. util
. constants
import ceph_disk_guids
11 from ceph_volume
. util
. disk
import allow_loop_devices
14 logger
= logging
. getLogger ( __name__
)
18 {dev:<25} {size:<12} {device_nodes:<15} {rot!s:<7} {available!s:<9} {model} """
21 def encryption_status ( abspath
):
23 Helper function to run ``encryption.status()``. It is done here to avoid
24 a circular import issue (encryption module imports from this module) and to
25 ease testing by allowing monkeypatching of this function.
27 from ceph_volume
. util
import encryption
28 return encryption
. status ( abspath
)
31 class Devices ( object ):
33 A container for Device instances with reporting
37 filter_for_batch
= False ,
41 lsblk_all
= disk
. lsblk_all ()
42 all_devices_vgs
= lvm
. get_all_devices_vgs ()
43 if not sys_info
. devices
:
44 sys_info
. devices
= disk
. get_devices ()
45 self
._ devices
= [ Device ( k
,
49 all_devices_vgs
= all_devices_vgs
) for k
in
50 sys_info
. devices
. keys ()]
52 for device
in self
._ devices
:
53 if filter_for_batch
and not device
. available_lvm_batch
:
55 if device
. is_lv
and not list_all
:
57 if device
. is_partition
and not list_all
:
59 self
. devices
. append ( device
)
61 def pretty_report ( self
):
63 report_template
. format (
68 available
= 'available' ,
69 device_nodes
= 'Device nodes' ,
72 for device
in sorted ( self
. devices
):
73 output
. append ( device
. report ())
74 return '' . join ( output
)
76 def json_report ( self
):
78 for device
in sorted ( self
. devices
):
79 output
. append ( device
. json_report ())
97 pretty_report_sys_fields
= [
99 'human_readable_size' ,
109 # define some class variables; mostly to enable the use of autospec in
113 def __init__ ( self
, path
, with_lsm
= False , lvs
= None , lsblk_all
= None , all_devices_vgs
= None ):
115 # LVs can have a vg/lv path, while disks will have /dev/sda
117 # check if we are a symlink
118 if os
. path
. islink ( self
. path
):
119 self
. symlink
= self
. path
120 real_path
= os
. path
. realpath ( self
. path
)
121 # check if we are not a device mapper
122 if "dm-" not in real_path
:
123 self
. path
= real_path
124 if not sys_info
. devices
:
126 sys_info
. devices
= disk
. get_devices ( device
= self
. path
)
128 sys_info
. devices
= disk
. get_devices ()
129 if sys_info
. devices
. get ( self
. path
, {}):
130 self
. device_nodes
= sys_info
. devices
[ self
. path
][ 'device_nodes' ]
131 self
. sys_api
= sys_info
. devices
. get ( self
. path
, {})
132 self
. partitions
= self
._ get
_ partitions
()
134 self
. lvs
= [] if not lvs
else lvs
135 self
. lsblk_all
= lsblk_all
136 self
. all_devices_vgs
= all_devices_vgs
141 self
. blkid_api
= None
143 self
._ is
_l vm
_ member
= None
144 self
. ceph_device
= False
146 self
. lsm_data
= self
. fetch_lsm ( with_lsm
)
148 self
. available_lvm
, self
. rejected_reasons_lvm
= self
._ check
_l vm
_ reject
_ reasons
()
149 self
. available_raw
, self
. rejected_reasons_raw
= self
._ check
_ raw
_ reject
_ reasons
()
150 self
. available
= self
. available_lvm
and self
. available_raw
151 self
. rejected_reasons
= list ( set ( self
. rejected_reasons_lvm
+
152 self
. rejected_reasons_raw
))
154 self
. device_id
= self
._ get
_ device
_ id
()
156 def fetch_lsm ( self
, with_lsm
):
158 Attempt to fetch libstoragemgmt (LSM) metadata, and return to the caller
159 as a dict. An empty dict is passed back to the caller if the target path
160 is not a block device, or lsm is unavailable on the host. Otherwise the
161 json returned will provide LSM attributes, and any associated errors that
162 lsm encountered when probing the device.
164 if not with_lsm
or not self
. exists
or not self
. is_device
:
167 lsm_disk
= LSMDisk ( self
. path
)
169 return lsm_disk
. json_report ()
171 def __lt__ ( self
, other
):
173 Implementing this method and __eq__ allows the @total_ordering
174 decorator to turn the Device class into a totally ordered type.
175 This can slower then implementing all comparison operations.
176 This sorting should put available devices before unavailable devices
177 and sort on the path otherwise (str sorting).
179 if self
. available
== other
. available
:
180 return self
. path
< other
. path
181 return self
. available
and not other
. available
183 def __eq__ ( self
, other
):
184 return self
. path
== other
. path
187 return hash ( self
. path
)
189 def load_blkid_api ( self
):
190 if self
. blkid_api
is None :
191 self
. blkid_api
= disk
. blkid ( self
. path
)
196 # if no device was found check if we are a partition
197 partname
= self
. path
. split ( '/' )[- 1 ]
198 for device
, info
in sys_info
. devices
. items ():
199 part
= info
[ 'partitions' ]. get ( partname
, {})
206 # if the path is not absolute, we have 'vg/lv', let's use LV name
208 if self
. path
[ 0 ] == '/' :
209 if _lv
. lv_path
== self
. path
:
213 vgname
, lvname
= self
. path
. split ( '/' )
214 if _lv
. lv_name
== lvname
and _lv
. vg_name
== vgname
:
218 if self
. path
[ 0 ] == '/' :
219 lv
= lvm
. get_single_lv ( filters
={ 'lv_path' : self
. path
})
221 vgname
, lvname
= self
. path
. split ( '/' )
222 lv
= lvm
. get_single_lv ( filters
={ 'lv_name' : lvname
,
228 self
. path
= lv
. lv_path
229 self
. vg_name
= lv
. vg_name
230 self
. lv_name
= lv
. name
231 self
. ceph_device
= lvm
. is_ceph_device ( lv
)
235 for dev
in self
. lsblk_all
:
236 if dev
[ 'NAME' ] == os
. path
. basename ( self
. path
):
239 dev
= disk
. lsblk ( self
. path
)
241 device_type
= dev
. get ( 'TYPE' , '' )
242 # always check is this is an lvm member
243 valid_types
= [ 'part' , 'disk' , 'mpath' ]
244 if allow_loop_devices ():
245 valid_types
. append ( 'loop' )
246 if device_type
in valid_types
:
247 self
._ set
_l vm
_ membership
()
249 self
. ceph_disk
= CephDiskDevice ( self
)
255 elif self
. is_partition
:
258 prefix
= 'Raw Device'
259 return '< %s : %s >' % ( prefix
, self
. path
)
261 def pretty_report ( self
):
263 if isinstance ( v
, list ):
268 return k
. strip ( '_' ). replace ( '_' , ' ' )
269 output
= [ ' \n ====== Device report {} ====== \n ' . format ( self
. path
)]
271 [ self
. pretty_template
. format (
273 value
= format_value ( v
)) for k
, v
in vars ( self
). items () if k
in
274 self
. report_fields
and k
!= 'disk_api' and k
!= 'sys_api' ] )
276 [ self
. pretty_template
. format (
278 value
= format_value ( v
)) for k
, v
in self
. sys_api
. items () if k
in
279 self
. pretty_report_sys_fields
])
282 --- Logical Volume ---""" )
284 [ self
. pretty_template
. format (
286 value
= format_value ( v
)) for k
, v
in lv
. report (). items ()])
287 return '' . join ( output
)
290 return report_template
. format (
292 size
= self
. size_human
,
294 available
= self
. available
,
296 device_nodes
= self
. device_nodes
299 def json_report ( self
):
300 output
= { k
. strip ( '_' ): v
for k
, v
in vars ( self
). items () if k
in
302 output
[ 'lvs' ] = [ lv
. report () for lv
in self
. lvs
]
305 def _get_device_id ( self
):
307 Please keep this implementation in sync with get_device_id() in
310 props
= [ 'ID_VENDOR' , 'ID_MODEL' , 'ID_MODEL_ENC' , 'ID_SERIAL_SHORT' , 'ID_SERIAL' ,
312 p
= disk
. udevadm_property ( self
. path
, props
)
313 if p
. get ( 'ID_MODEL' , '' ). startswith ( 'LVM PV ' ):
314 p
[ 'ID_MODEL' ] = p
. get ( 'ID_MODEL_ENC' , '' ). replace ( ' \\ x20' , ' ' ). strip ()
315 if 'ID_VENDOR' in p
and 'ID_MODEL' in p
and 'ID_SCSI_SERIAL' in p
:
316 dev_id
= '_' . join ([ p
[ 'ID_VENDOR' ], p
[ 'ID_MODEL' ],
317 p
[ 'ID_SCSI_SERIAL' ]])
318 elif 'ID_MODEL' in p
and 'ID_SERIAL_SHORT' in p
:
319 dev_id
= '_' . join ([ p
[ 'ID_MODEL' ], p
[ 'ID_SERIAL_SHORT' ]])
320 elif 'ID_SERIAL' in p
:
321 dev_id
= p
[ 'ID_SERIAL' ]
322 if dev_id
. startswith ( 'MTFD' ):
323 # Micron NVMes hide the vendor
324 dev_id
= 'Micron_' + dev_id
326 # the else branch should fallback to using sysfs and ioctl to
327 # retrieve device_id on FreeBSD. Still figuring out if/how the
328 # python ioctl implementation does that on FreeBSD
330 dev_id
= dev_id
. replace ( ' ' , '_' )
331 while '__' in dev_id
:
332 dev_id
= dev_id
. replace ( '__' , '_' )
335 def _set_lvm_membership ( self
):
336 if self
._ is
_l vm
_ member
is None :
337 # this is contentious, if a PV is recognized by LVM but has no
338 # VGs, should we consider it as part of LVM? We choose not to
339 # here, because most likely, we need to use VGs from this PV.
340 self
._ is
_l vm
_ member
= False
341 device_to_check
= [ self
. path
]
342 device_to_check
. extend ( self
. partitions
)
344 # a pv can only be in one vg, so this should be safe
345 # FIXME: While the above assumption holds, sda1 and sda2
346 # can each host a PV and VG. I think the vg_name property is
347 # actually unused (not 100% sure) and can simply be removed
349 if not self
. all_devices_vgs
:
350 self
. all_devices_vgs
= lvm
. get_all_devices_vgs ()
351 for path
in device_to_check
:
352 for dev_vg
in self
. all_devices_vgs
:
353 if dev_vg
. pv_name
== path
:
357 self
. vg_name
= vgs
[ 0 ]
358 self
._ is
_l vm
_ member
= True
359 self
. lvs
. extend ( lvm
. get_device_lvs ( path
))
361 self
. ceph_device
= any ([ True if lv
. tags
. get ( 'ceph.osd_id' ) else False for lv
in self
. lvs
])
363 def _get_partitions ( self
):
365 For block devices LVM can reside on the raw block device or on a
366 partition. Return a list of paths to be checked for a pv.
369 path_dir
= os
. path
. dirname ( self
. path
)
370 for partition
in self
. sys_api
. get ( 'partitions' , {}). keys ():
371 partitions
. append ( os
. path
. join ( path_dir
, partition
))
376 return os
. path
. exists ( self
. path
)
380 self
. load_blkid_api ()
381 return 'TYPE' in self
. blkid_api
384 def has_gpt_headers ( self
):
385 self
. load_blkid_api ()
386 return self
. blkid_api
. get ( "PTTYPE" ) == "gpt"
389 def rotational ( self
):
390 rotational
= self
. sys_api
. get ( 'rotational' )
391 if rotational
is None :
392 # fall back to lsblk if not found in sys_api
393 # default to '1' if no value is found with lsblk either
394 rotational
= self
. disk_api
. get ( 'ROTA' , '1' )
395 return rotational
== '1'
399 return self
. sys_api
[ 'model' ]
402 def size_human ( self
):
403 return self
. sys_api
[ 'human_readable_size' ]
407 return self
. sys_api
[ 'size' ]
410 def parent_device ( self
):
411 if 'PKNAME' in self
. disk_api
:
412 return '/dev/ %s ' % self
. disk_api
[ 'PKNAME' ]
418 If this device was made into a PV it would lose 1GB in total size
419 due to the 1GB physical extent size we set when creating volume groups
421 size
= disk
. Size ( b
= self
. size
)
422 lvm_size
= disk
. Size ( gb
= size
. gb
. as_int ()) - disk
. Size ( gb
= 1 )
426 def is_lvm_member ( self
):
427 if self
._ is
_l vm
_ member
is None :
428 self
._ set
_l vm
_ membership
()
429 return self
._ is
_l vm
_ member
432 def is_ceph_disk_member ( self
):
433 def is_member ( device
):
434 return 'ceph' in device
. get ( 'PARTLABEL' , '' ) or \
435 device
. get ( 'PARTTYPE' , '' ) in ceph_disk_guids
. keys ()
436 # If we come from Devices(), self.lsblk_all is set already.
437 # Otherwise, we have to grab the data.
438 details
= self
. lsblk_all
or disk
. lsblk_all ()
440 if self
. sys_api
. get ( "partitions" ):
441 for part
in self
. sys_api
. get ( "partitions" ). keys ():
443 if part
. startswith ( dev
[ 'NAME' ]):
448 return is_member ( self
. disk_api
)
449 raise RuntimeError ( f
"Couln't check if device {self.path} is a ceph-disk member." )
452 def has_bluestore_label ( self
):
453 return disk
. has_bluestore_label ( self
. path
)
457 return self
. path
. startswith (( '/dev/mapper' , '/dev/dm-' ))
460 def device_type ( self
):
461 self
. load_blkid_api ()
462 if 'type' in self
. sys_api
:
463 return self
. sys_api
[ 'type' ]
465 return self
. disk_api
[ 'TYPE' ]
467 return self
. blkid_api
[ 'TYPE' ]
471 return self
. device_type
== 'mpath'
475 return self
. lv_api
is not None
478 def is_partition ( self
):
479 self
. load_blkid_api ()
481 return self
. disk_api
[ 'TYPE' ] == 'part'
483 return self
. blkid_api
[ 'TYPE' ] == 'part'
488 self
. load_blkid_api ()
495 valid_types
= [ 'disk' , 'device' , 'mpath' ]
496 if allow_loop_devices ():
497 valid_types
. append ( 'loop' )
498 return self
. device_type
in valid_types
502 def is_acceptable_device ( self
):
503 return self
. is_device
or self
. is_partition
or self
. is_lv
506 def is_encrypted ( self
):
508 Only correct for LVs, device mappers, and partitions. Will report a ``None``
511 self
. load_blkid_api ()
512 crypt_reports
= [ self
. blkid_api
. get ( 'TYPE' , '' ), self
. disk_api
. get ( 'FSTYPE' , '' )]
514 # if disk APIs are reporting this is encrypted use that:
515 if 'crypto_LUKS' in crypt_reports
:
517 # if ceph-volume created this, then a tag would let us know
518 elif self
. lv_api
. encrypted
:
521 elif self
. is_partition
:
522 return 'crypto_LUKS' in crypt_reports
524 active_mapper
= encryption_status ( self
. path
)
526 # normalize a bit to ensure same values regardless of source
527 encryption_type
= active_mapper
[ 'type' ]. lower (). strip ( '12' ) # turn LUKS1 or LUKS2 into luks
528 return True if encryption_type
in [ 'plain' , 'luks' ] else False
535 def used_by_ceph ( self
):
536 # only filter out data devices as journals could potentially be reused
537 osd_ids
= [ lv
. tags
. get ( "ceph.osd_id" ) is not None for lv
in self
. lvs
538 if lv
. tags
. get ( "ceph.type" ) in [ "data" , "block" ]]
542 def journal_used_by_ceph ( self
):
543 # similar to used_by_ceph() above. This is for 'journal' devices (db/wal/..)
544 # needed by get_lvm_fast_allocs() in devices/lvm/batch.py
545 # see https://tracker.ceph.com/issues/59640
546 osd_ids
= [ lv
. tags
. get ( "ceph.osd_id" ) is not None for lv
in self
. lvs
547 if lv
. tags
. get ( "ceph.type" ) in [ "db" , "wal" ]]
551 def vg_free_percent ( self
):
553 return [ vg
. free_percent
for vg
in self
. vgs
]
560 return [ vg
. size
for vg
in self
. vgs
]
562 # TODO fix this...we can probably get rid of vg_free
568 Returns the free space in all VGs on this device. If no VGs are
569 present, returns the disk size.
572 return [ vg
. free
for vg
in self
. vgs
]
574 # We could also query 'lvmconfig
575 # --typeconfig full' and use allocations -> physical_extent_size
576 # value to project the space for a vg
577 # assuming 4M extents here
578 extent_size
= 4194304
579 vg_free
= int ( self
. size
/ extent_size
) * extent_size
580 if self
. size
% extent_size
== 0 :
581 # If the extent size divides size exactly, deduct on extent for
583 vg_free
-= extent_size
587 def has_partitions ( self
):
589 Boolean to determine if a given device has partitions.
591 if self
. sys_api
. get ( 'partitions' ):
595 def _check_generic_reject_reasons ( self
):
597 ( 'removable' , 1 , 'removable' ),
598 ( 'ro' , 1 , 'read-only' ),
600 rejected
= [ reason
for ( k
, v
, reason
) in reasons
if
601 self
. sys_api
. get ( k
, '' ) == v
]
602 if self
. is_acceptable_device
:
603 # reject disks smaller than 5GB
604 if int ( self
. sys_api
. get ( 'size' , 0 )) < 5368709120 :
605 rejected
. append ( 'Insufficient space (<5GB)' )
607 rejected
. append ( "Device type is not acceptable. It should be raw device or partition" )
608 if self
. is_ceph_disk_member
:
609 rejected
. append ( "Used by ceph-disk" )
612 if self
. has_bluestore_label
:
613 rejected
. append ( 'Has BlueStore device label' )
615 # likely failed to open the device. assuming it is BlueStore is the safest option
616 # so that a possibly-already-existing OSD doesn't get overwritten
617 logger
. error ( 'failed to determine if device {} is BlueStore. device should not be used to avoid false negatives. err: {}' . format ( self
. path
, e
))
618 rejected
. append ( 'Failed to determine if device is BlueStore' )
620 if self
. is_partition
:
622 if disk
. has_bluestore_label ( self
. parent_device
):
623 rejected
. append ( 'Parent has BlueStore device label' )
625 # likely failed to open the device. assuming the parent is BlueStore is the safest
626 # option so that a possibly-already-existing OSD doesn't get overwritten
627 logger
. error ( 'failed to determine if partition {} (parent: {}) has a BlueStore parent. partition should not be used to avoid false negatives. err: {}' . format ( self
. path
, self
. parent_device
, e
))
628 rejected
. append ( 'Failed to determine if parent device is BlueStore' )
630 if self
. has_gpt_headers
:
631 rejected
. append ( 'Has GPT headers' )
632 if self
. has_partitions
:
633 rejected
. append ( 'Has partitions' )
635 rejected
. append ( 'Has a FileSystem' )
638 def _check_lvm_reject_reasons ( self
):
641 available_vgs
= [ vg
for vg
in self
. vgs
if int ( vg
. vg_free_count
) > 10 ]
642 if not available_vgs
:
643 rejected
. append ( 'Insufficient space (<10 extents) on vgs' )
645 # only check generic if no vgs are present. Vgs might hold lvs and
646 # that might cause 'locked' to trigger
647 rejected
. extend ( self
._ check
_ generic
_ reject
_ reasons
())
649 return len ( rejected
) == 0 , rejected
651 def _check_raw_reject_reasons ( self
):
652 rejected
= self
._ check
_ generic
_ reject
_ reasons
()
653 if len ( self
. vgs
) > 0 :
654 rejected
. append ( 'LVM detected' )
656 return len ( rejected
) == 0 , rejected
659 def available_lvm_batch ( self
):
660 if self
. sys_api
. get ( "partitions" ):
662 if system
. device_is_mounted ( self
. path
):
664 return self
. is_device
or self
. is_lv
667 class CephDiskDevice ( object ):
669 Detect devices that have been created by ceph-disk, report their type
670 (journal, data, etc..). Requires a ``Device`` object as input.
673 def __init__ ( self
, device
):
675 self
._ is
_ ceph
_ disk
_ member
= None
680 In containers, the 'PARTLABEL' attribute might not be detected
681 correctly via ``lsblk``, so we poke at the value with ``lsblk`` first,
682 falling back to ``blkid`` (which works correclty in containers).
684 lsblk_partlabel
= self
. device
. disk_api
. get ( 'PARTLABEL' )
686 return lsblk_partlabel
687 return self
. device
. blkid_api
. get ( 'PARTLABEL' , '' )
692 Seems like older version do not detect PARTTYPE correctly (assuming the
693 info in util/disk.py#lsblk is still valid).
694 SImply resolve to using blkid since lsblk will throw an error if asked
695 for an unknown columns
697 return self
. device
. blkid_api
. get ( 'PARTTYPE' , '' )
701 if self
._ is
_ ceph
_ disk
_ member
is None :
702 if 'ceph' in self
. partlabel
:
703 self
._ is
_ ceph
_ disk
_ member
= True
705 elif self
. parttype
in ceph_disk_guids
. keys ():
708 return self
._ is
_ ceph
_ disk
_ member
713 'data' , 'wal' , 'db' , 'lockbox' , 'journal' ,
714 # ceph-disk uses 'ceph block' when placing data in bluestore, but
715 # keeps the regular OSD files in 'ceph data' :( :( :( :(
719 if t
in self
. partlabel
:
721 label
= ceph_disk_guids
. get ( self
. parttype
, {})
722 return label
. get ( 'type' , 'unknown' ). split ( '.' )[- 1 ]