]>
git.proxmox.com Git - ceph.git/blob - ceph/src/ceph-volume/ceph_volume/api/lvm.py
2 API for CRUD lvm tag operations. Follows the Ceph LVM tag naming convention
3 that prefixes tags with ``ceph.`` and uses ``=`` for assignment, and provides
4 set of utilities for interacting with LVM.
10 from ceph_volume
import process
, util
11 from ceph_volume
.exceptions
import (
12 MultipleLVsError
, MultipleVGsError
,
13 MultiplePVsError
, SizeAllocationError
16 logger
= logging
.getLogger(__name__
)
19 def _output_parser(output
, fields
):
21 Newer versions of LVM allow ``--reportformat=json``, but older versions,
22 like the one included in Xenial do not. LVM has the ability to filter and
23 format its output so we assume the output will be in a format this parser
24 can handle (using ';' as a delimiter)
26 :param fields: A string, possibly using ',' to group many items, as it
27 would be used on the CLI
28 :param output: The CLI output from the LVM call
30 field_items
= fields
.split(',')
33 # clear the leading/trailing whitespace
36 # remove the extra '"' in each field
37 line
= line
.replace('"', '')
39 # prevent moving forward with empty contents
43 # splitting on ';' because that is what the lvm call uses as
45 output_items
= [i
.strip() for i
in line
.split(';')]
46 # map the output to the fields
48 dict(zip(field_items
, output_items
))
54 def _splitname_parser(line
):
56 Parses the output from ``dmsetup splitname``, that should contain prefixes
57 (--nameprefixes) and set the separator to ";"
59 Output for /dev/mapper/vg-lv will usually look like::
61 DM_VG_NAME='/dev/mapper/vg';DM_LV_NAME='lv';DM_LV_LAYER=''
64 The ``VG_NAME`` will usually not be what other callers need (e.g. just 'vg'
65 in the example), so this utility will split ``/dev/mapper/`` out, so that
66 the actual volume group name is kept
68 :returns: dictionary with stripped prefixes
72 parts
= line
[0].split(';')
74 logger
.exception('Unable to parse mapper device: %s', line
)
78 part
= part
.replace("'", '')
79 key
, value
= part
.split('=')
80 if 'DM_VG_NAME' in key
:
81 value
= value
.split('/dev/mapper/')[-1]
82 key
= key
.split('DM_')[-1]
88 def sizing(device_size
, parts
=None, size
=None):
90 Calculate proper sizing to fully utilize the volume group in the most
91 efficient way possible. To prevent situations where LVM might accept
92 a percentage that is beyond the vg's capabilities, it will refuse with
93 an error when requesting a larger-than-possible parameter, in addition
94 to rounding down calculations.
96 A dictionary with different sizing parameters is returned, to make it
97 easier for others to choose what they need in order to create logical
100 >>> sizing(100, parts=2)
101 >>> {'parts': 2, 'percentages': 50, 'sizes': 50}
104 if parts
is not None and size
is not None:
106 "Cannot process sizing with both parts (%s) and size (%s)" % (parts
, size
)
109 if size
and size
> device_size
:
110 raise SizeAllocationError(size
, device_size
)
112 def get_percentage(parts
):
113 return int(floor(100 / float(parts
)))
115 if parts
is not None:
116 # Prevent parts being 0, falling back to 1 (100% usage)
118 percentages
= get_percentage(parts
)
121 parts
= int(device_size
/ size
) or 1
122 percentages
= get_percentage(parts
)
124 sizes
= device_size
/ parts
if parts
else int(floor(device_size
))
128 'percentages': percentages
,
129 'sizes': int(sizes
/1024/1024/1024),
133 def parse_tags(lv_tags
):
135 Return a dictionary mapping of all the tags associated with
136 a Volume from the comma-separated tags coming from the LVM API
140 "ceph.osd_fsid=aaa-fff-bbbb,ceph.osd_id=0"
142 For the above example, the expected return value would be::
145 "ceph.osd_fsid": "aaa-fff-bbbb",
152 tags
= lv_tags
.split(',')
153 for tag_assignment
in tags
:
154 if not tag_assignment
.startswith('ceph.'):
156 key
, value
= tag_assignment
.split('=', 1)
157 tag_mapping
[key
] = value
162 def _vdo_parents(devices
):
164 It is possible we didn't get a logical volume, or a mapper path, but
165 a device like /dev/sda2, to resolve this, we must look at all the slaves of
166 every single device in /sys/block and if any of those devices is related to
167 VDO devices, then we can add the parent
170 for parent
in os
.listdir('/sys/block'):
171 for slave
in os
.listdir('/sys/block/%s/slaves' % parent
):
173 parent_devices
.append('/dev/%s' % parent
)
174 parent_devices
.append(parent
)
175 return parent_devices
178 def _vdo_slaves(vdo_names
):
180 find all the slaves associated with each vdo name (from realpath) by going
181 into /sys/block/<realpath>/slaves
184 for vdo_name
in vdo_names
:
185 mapper_path
= '/dev/mapper/%s' % vdo_name
186 if not os
.path
.exists(mapper_path
):
188 # resolve the realpath and realname of the vdo mapper
189 vdo_realpath
= os
.path
.realpath(mapper_path
)
190 vdo_realname
= vdo_realpath
.split('/')[-1]
191 slaves_path
= '/sys/block/%s/slaves' % vdo_realname
192 if not os
.path
.exists(slaves_path
):
194 devices
.append(vdo_realpath
)
195 devices
.append(mapper_path
)
196 devices
.append(vdo_realname
)
197 for slave
in os
.listdir(slaves_path
):
198 devices
.append('/dev/%s' % slave
)
199 devices
.append(slave
)
205 A VDO device can be composed from many different devices, go through each
206 one of those devices and its slaves (if any) and correlate them back to
207 /dev/mapper and their realpaths, and then check if they appear as part of
208 /sys/kvdo/<name>/statistics
210 From the realpath of a logical volume, determine if it is a VDO device or
211 not, by correlating it to the presence of the name in
212 /sys/kvdo/<name>/statistics and all the previously captured devices
214 if not os
.path
.isdir('/sys/kvdo'):
216 realpath
= os
.path
.realpath(path
)
217 realpath_name
= realpath
.split('/')[-1]
220 # get all the vdo names
221 for dirname
in os
.listdir('/sys/kvdo/'):
222 if os
.path
.isdir('/sys/kvdo/%s/statistics' % dirname
):
223 vdo_names
.add(dirname
)
225 # find all the slaves associated with each vdo name (from realpath) by
226 # going into /sys/block/<realpath>/slaves
227 devices
.extend(_vdo_slaves(vdo_names
))
229 # Find all possible parents, looking into slaves that are related to VDO
230 devices
.extend(_vdo_parents(devices
))
235 realpath_name
in devices
])
240 Detect if a path is backed by VDO, proxying the actual call to _is_vdo so
241 that we can prevent an exception breaking OSD creation. If an exception is
242 raised, it will get captured and logged to file, while returning
250 logger
.exception('Unable to properly detect device as VDO: %s', path
)
254 def dmsetup_splitname(dev
):
256 Run ``dmsetup splitname`` and parse the results.
258 .. warning:: This call does not ensure that the device is correct or that
259 it exists. ``dmsetup`` will happily take a non existing path and still
260 return a 0 exit status.
263 'dmsetup', 'splitname', '--noheadings',
264 "--separator=';'", '--nameprefixes', dev
266 out
, err
, rc
= process
.call(command
)
267 return _splitname_parser(out
)
270 def is_ceph_device(lv
):
272 lv
.tags
['ceph.osd_id']
273 except (KeyError, AttributeError):
274 logger
.warning('device is not part of ceph: %s', lv
)
277 if lv
.tags
['ceph.osd_id'] == 'null':
283 ####################################
285 # Code for LVM Physical Volumes
287 ################################
289 PV_FIELDS
= 'pv_name,pv_tags,pv_uuid,vg_name,lv_uuid'
293 Return the list of physical volumes configured for lvm and available in the
294 system using flags to include common metadata associated with them like the uuid
296 This will only return physical volumes set up to work with LVM.
298 Command and delimited output should look like::
300 $ pvs --noheadings --readonly --separator=';' -o pv_name,pv_tags,pv_uuid
302 /dev/sdv;;07A4F654-4162-4600-8EB3-88D1E42F368D
305 stdout
, stderr
, returncode
= process
.call(
306 ['pvs', '--no-heading', '--readonly', '--separator=";"', '-o',
308 verbose_on_failure
=False
311 return _output_parser(stdout
, PV_FIELDS
)
314 class PVolume(object):
316 Represents a Physical Volume from LVM, with some top-level attributes like
317 ``pv_name`` and parsed tags as a dictionary of key/value pairs.
320 def __init__(self
, **kw
):
321 for k
, v
in kw
.items():
324 self
.name
= kw
['pv_name']
325 self
.tags
= parse_tags(kw
['pv_tags'])
328 return '<%s>' % self
.pv_api
['pv_name']
331 return self
.__str
__()
333 def set_tags(self
, tags
):
335 :param tags: A dictionary of tag names and values, like::
338 "ceph.osd_fsid": "aaa-fff-bbbb",
342 At the end of all modifications, the tags are refreshed to reflect
343 LVM's most current view.
345 for k
, v
in tags
.items():
347 # after setting all the tags, refresh them for the current object, use the
348 # pv_* identifiers to filter because those shouldn't change
349 pv_object
= get_pv(pv_name
=self
.pv_name
, pv_uuid
=self
.pv_uuid
)
350 self
.tags
= pv_object
.tags
352 def set_tag(self
, key
, value
):
354 Set the key/value pair as an LVM tag. Does not "refresh" the values of
355 the current object for its tags. Meant to be a "fire and forget" type
358 **warning**: Altering tags on a PV has to be done ensuring that the
359 device is actually the one intended. ``pv_name`` is *not* a persistent
360 value, only ``pv_uuid`` is. Using ``pv_uuid`` is the best way to make
361 sure the device getting changed is the one needed.
363 # remove it first if it exists
364 if self
.tags
.get(key
):
365 current_value
= self
.tags
[key
]
366 tag
= "%s=%s" % (key
, current_value
)
367 process
.call(['pvchange', '--deltag', tag
, self
.pv_name
])
372 '--addtag', '%s=%s' % (key
, value
), self
.pv_name
377 class PVolumes(list):
379 A list of all known (physical) volumes for the current system, with the ability
380 to filter them via keyword arguments.
383 def __init__(self
, populate
=True):
388 # get all the pvs in the current system
389 for pv_item
in get_api_pvs():
390 self
.append(PVolume(**pv_item
))
394 Deplete all the items in the list, used internally only so that we can
395 dynamically allocate the items when filtering without the concern of
396 messing up the contents
400 def _filter(self
, pv_name
=None, pv_uuid
=None, pv_tags
=None):
402 The actual method that filters using a new list. Useful so that other
403 methods that do not want to alter the contents of the list (e.g.
404 ``self.find``) can operate safely.
406 filtered
= [i
for i
in self
]
408 filtered
= [i
for i
in filtered
if i
.pv_name
== pv_name
]
411 filtered
= [i
for i
in filtered
if i
.pv_uuid
== pv_uuid
]
413 # at this point, `filtered` has either all the physical volumes in self
414 # or is an actual filtered list if any filters were applied
417 for pvolume
in filtered
:
418 matches
= all(pvolume
.tags
.get(k
) == str(v
) for k
, v
in pv_tags
.items())
420 tag_filtered
.append(pvolume
)
421 # return the tag_filtered pvolumes here, the `filtered` list is no
427 def filter(self
, pv_name
=None, pv_uuid
=None, pv_tags
=None):
429 Filter out volumes on top level attributes like ``pv_name`` or by
430 ``pv_tags`` where a dict is required. For example, to find a physical
431 volume that has an OSD ID of 0, the filter would look like::
433 pv_tags={'ceph.osd_id': '0'}
436 if not any([pv_name
, pv_uuid
, pv_tags
]):
437 raise TypeError('.filter() requires pv_name, pv_uuid, or pv_tags'
440 filtered_pvs
= PVolumes(populate
=False)
441 filtered_pvs
.extend(self
._filter
(pv_name
, pv_uuid
, pv_tags
))
444 def get(self
, pv_name
=None, pv_uuid
=None, pv_tags
=None):
446 This is a bit expensive, since it will try to filter out all the
447 matching items in the list, filter them out applying anything that was
448 added and return the matching item.
450 This method does *not* alter the list, and it will raise an error if
451 multiple pvs are matched
453 It is useful to use ``tags`` when trying to find a specific logical volume,
454 but it can also lead to multiple pvs being found, since a lot of metadata
455 is shared between pvs of a distinct OSD.
457 if not any([pv_name
, pv_uuid
, pv_tags
]):
466 if len(pvs
) > 1 and pv_tags
:
467 raise MultiplePVsError(pv_name
)
471 def create_pv(device
):
473 Create a physical volume from a device, useful when devices need to be later mapped
480 '--yes', # answer yes to any prompts
485 def remove_pv(pv_name
):
487 Removes a physical volume using a double `-f` to prevent prompts and fully
488 remove anything related to LVM. This is tremendously destructive, but so is all other actions
489 when zapping a device.
491 In the case where multiple PVs are found, it will ignore that fact and
492 continue with the removal, specifically in the case of messages like::
494 WARNING: PV $UUID /dev/DEV-1 was already found on /dev/DEV-2
496 These situations can be avoided with custom filtering rules, which this API
497 cannot handle while accommodating custom user filters.
499 fail_msg
= "Unable to remove vg %s" % pv_name
512 def get_pv(pv_name
=None, pv_uuid
=None, pv_tags
=None, pvs
=None):
514 Return a matching pv (physical volume) for the current system, requiring
515 ``pv_name``, ``pv_uuid``, or ``pv_tags``. Raises an error if more than one
518 if not any([pv_name
, pv_uuid
, pv_tags
]):
520 if pvs
is None or len(pvs
) == 0:
523 return pvs
.get(pv_name
=pv_name
, pv_uuid
=pv_uuid
, pv_tags
=pv_tags
)
526 ################################
528 # Code for LVM Volume Groups
530 #############################
532 VG_FIELDS
= 'vg_name,pv_count,lv_count,vg_attr,vg_extent_count,vg_free_count,vg_extent_size'
533 VG_CMD_OPTIONS
= ['--noheadings', '--readonly', '--units=b', '--nosuffix', '--separator=";"']
538 Return the list of group volumes available in the system using flags to
539 include common metadata associated with them
541 Command and sample delimited output should look like::
543 $ vgs --noheadings --units=b --readonly --separator=';' \
544 -o vg_name,pv_count,lv_count,vg_attr,vg_free_count,vg_extent_size
545 ubuntubox-vg;1;2;wz--n-;12;
547 To normalize sizing, the units are forced in 'g' which is equivalent to
548 gigabytes, which uses multiples of 1024 (as opposed to 1000)
550 stdout
, stderr
, returncode
= process
.call(
551 ['vgs'] + VG_CMD_OPTIONS
+ ['-o', VG_FIELDS
],
552 verbose_on_failure
=False
554 return _output_parser(stdout
, VG_FIELDS
)
557 class VolumeGroup(object):
559 Represents an LVM group, with some top-level attributes like ``vg_name``
562 def __init__(self
, **kw
):
563 for k
, v
in kw
.items():
565 self
.name
= kw
['vg_name']
567 raise ValueError('VolumeGroup must have a non-empty name')
568 self
.tags
= parse_tags(kw
.get('vg_tags', ''))
571 return '<%s>' % self
.name
574 return self
.__str
__()
579 Return free space in VG in bytes
581 return int(self
.vg_extent_size
) * int(self
.vg_free_count
)
586 Returns VG size in bytes
588 return int(self
.vg_extent_size
) * int(self
.vg_extent_count
)
590 def sizing(self
, parts
=None, size
=None):
592 Calculate proper sizing to fully utilize the volume group in the most
593 efficient way possible. To prevent situations where LVM might accept
594 a percentage that is beyond the vg's capabilities, it will refuse with
595 an error when requesting a larger-than-possible parameter, in addition
596 to rounding down calculations.
598 A dictionary with different sizing parameters is returned, to make it
599 easier for others to choose what they need in order to create logical
604 >>> data_vg.sizing(parts=4)
605 {'parts': 4, 'sizes': 256, 'percentages': 25}
606 >>> data_vg.sizing(size=512)
607 {'parts': 2, 'sizes': 512, 'percentages': 50}
610 :param parts: Number of parts to create LVs from
611 :param size: Size in gigabytes to divide the VG into
613 :raises SizeAllocationError: When requested size cannot be allocated with
614 :raises ValueError: If both ``parts`` and ``size`` are given
616 if parts
is not None and size
is not None:
618 "Cannot process sizing with both parts (%s) and size (%s)" % (parts
, size
)
621 # if size is given we need to map that to extents so that we avoid
622 # issues when trying to get this right with a size in gigabytes find
623 # the percentage first, cheating, because these values are thrown out
624 vg_free_count
= util
.str_to_int(self
.vg_free_count
)
627 size
= size
* 1024 * 1024 * 1024
628 extents
= int(size
/ int(self
.vg_extent_size
))
629 disk_sizing
= sizing(self
.free
, size
=size
, parts
=parts
)
631 if parts
is not None:
632 # Prevent parts being 0, falling back to 1 (100% usage)
634 size
= int(self
.free
/ parts
)
635 extents
= size
* vg_free_count
/ self
.free
636 disk_sizing
= sizing(self
.free
, parts
=parts
)
638 extent_sizing
= sizing(vg_free_count
, size
=extents
)
640 disk_sizing
['extents'] = int(extents
)
641 disk_sizing
['percentages'] = extent_sizing
['percentages']
644 def bytes_to_extents(self
, size
):
646 Return a how many extents we can fit into a size in bytes.
648 return int(size
/ int(self
.vg_extent_size
))
650 def slots_to_extents(self
, slots
):
652 Return how many extents fit the VG slot times
654 return int(int(self
.vg_free_count
) / slots
)
657 class VolumeGroups(list):
659 A list of all known volume groups for the current system, with the ability
660 to filter them via keyword arguments.
663 def __init__(self
, populate
=True):
668 # get all the vgs in the current system
669 for vg_item
in get_api_vgs():
670 self
.append(VolumeGroup(**vg_item
))
674 Deplete all the items in the list, used internally only so that we can
675 dynamically allocate the items when filtering without the concern of
676 messing up the contents
680 def _filter(self
, vg_name
=None, vg_tags
=None):
682 The actual method that filters using a new list. Useful so that other
683 methods that do not want to alter the contents of the list (e.g.
684 ``self.find``) can operate safely.
686 .. note:: ``vg_tags`` is not yet implemented
688 filtered
= [i
for i
in self
]
690 filtered
= [i
for i
in filtered
if i
.vg_name
== vg_name
]
692 # at this point, `filtered` has either all the volumes in self or is an
693 # actual filtered list if any filters were applied
696 for volume
in filtered
:
697 matches
= all(volume
.tags
.get(k
) == str(v
) for k
, v
in vg_tags
.items())
699 tag_filtered
.append(volume
)
704 def filter(self
, vg_name
=None, vg_tags
=None):
706 Filter out groups on top level attributes like ``vg_name`` or by
707 ``vg_tags`` where a dict is required. For example, to find a Ceph group
708 with dmcache as the type, the filter would look like::
710 vg_tags={'ceph.type': 'dmcache'}
712 .. warning:: These tags are not documented because they are currently
713 unused, but are here to maintain API consistency
715 if not any([vg_name
, vg_tags
]):
716 raise TypeError('.filter() requires vg_name or vg_tags (none given)')
718 filtered_vgs
= VolumeGroups(populate
=False)
719 filtered_vgs
.extend(self
._filter
(vg_name
, vg_tags
))
722 def get(self
, vg_name
=None, vg_tags
=None):
724 This is a bit expensive, since it will try to filter out all the
725 matching items in the list, filter them out applying anything that was
726 added and return the matching item.
728 This method does *not* alter the list, and it will raise an error if
729 multiple VGs are matched
731 It is useful to use ``tags`` when trying to find a specific volume group,
732 but it can also lead to multiple vgs being found (although unlikely)
734 if not any([vg_name
, vg_tags
]):
743 # this is probably never going to happen, but it is here to keep
744 # the API code consistent
745 raise MultipleVGsError(vg_name
)
749 def create_vg(devices
, name
=None, name_prefix
=None):
751 Create a Volume Group. Command looks like::
753 vgcreate --force --yes group_name device
755 Once created the volume group is returned as a ``VolumeGroup`` object
757 :param devices: A list of devices to create a VG. Optionally, a single
758 device (as a string) can be used.
759 :param name: Optionally set the name of the VG, defaults to 'ceph-{uuid}'
760 :param name_prefix: Optionally prefix the name of the VG, which will get combined
763 if isinstance(devices
, set):
764 devices
= list(devices
)
765 if not isinstance(devices
, list):
768 name
= "%s-%s" % (name_prefix
, str(uuid
.uuid4()))
770 name
= "ceph-%s" % str(uuid
.uuid4())
778 vg
= get_vg(vg_name
=name
)
782 def extend_vg(vg
, devices
):
784 Extend a Volume Group. Command looks like::
786 vgextend --force --yes group_name [device, ...]
788 Once created the volume group is extended and returned as a ``VolumeGroup`` object
790 :param vg: A VolumeGroup object
791 :param devices: A list of devices to extend the VG. Optionally, a single
792 device (as a string) can be used.
794 if not isinstance(devices
, list):
803 vg
= get_vg(vg_name
=vg
.name
)
807 def reduce_vg(vg
, devices
):
809 Reduce a Volume Group. Command looks like::
811 vgreduce --force --yes group_name [device, ...]
813 :param vg: A VolumeGroup object
814 :param devices: A list of devices to remove from the VG. Optionally, a
815 single device (as a string) can be used.
817 if not isinstance(devices
, list):
826 vg
= get_vg(vg_name
=vg
.name
)
830 def remove_vg(vg_name
):
832 Removes a volume group.
835 logger
.warning('Skipping removal of invalid VG name: "%s"', vg_name
)
837 fail_msg
= "Unable to remove vg %s" % vg_name
849 def get_vg(vg_name
=None, vg_tags
=None, vgs
=None):
851 Return a matching vg for the current system, requires ``vg_name`` or
852 ``tags``. Raises an error if more than one vg is found.
854 It is useful to use ``tags`` when trying to find a specific volume group,
855 but it can also lead to multiple vgs being found.
857 if not any([vg_name
, vg_tags
]):
859 if vgs
is None or len(vgs
) == 0:
862 return vgs
.get(vg_name
=vg_name
, vg_tags
=vg_tags
)
865 def get_device_vgs(device
, name_prefix
=''):
866 stdout
, stderr
, returncode
= process
.call(
867 ['pvs'] + VG_CMD_OPTIONS
+ ['-o', VG_FIELDS
, device
],
868 verbose_on_failure
=False
870 vgs
= _output_parser(stdout
, VG_FIELDS
)
871 return [VolumeGroup(**vg
) for vg
in vgs
if vg
['vg_name'] and vg
['vg_name'].startswith(name_prefix
)]
874 #################################
876 # Code for LVM Logical Volumes
878 ###############################
880 LV_FIELDS
= 'lv_tags,lv_path,lv_name,vg_name,lv_uuid,lv_size'
881 LV_CMD_OPTIONS
= ['--noheadings', '--readonly', '--separator=";"', '-a']
885 Return the list of logical volumes available in the system using flags to include common
886 metadata associated with them
888 Command and delimited output should look like::
890 $ lvs --noheadings --readonly --separator=';' -a -o lv_tags,lv_path,lv_name,vg_name
891 ;/dev/ubuntubox-vg/root;root;ubuntubox-vg
892 ;/dev/ubuntubox-vg/swap_1;swap_1;ubuntubox-vg
895 stdout
, stderr
, returncode
= process
.call(
896 ['lvs'] + LV_CMD_OPTIONS
+ ['-o', LV_FIELDS
],
897 verbose_on_failure
=False
899 return _output_parser(stdout
, LV_FIELDS
)
902 class Volume(object):
904 Represents a Logical Volume from LVM, with some top-level attributes like
905 ``lv_name`` and parsed tags as a dictionary of key/value pairs.
908 def __init__(self
, **kw
):
909 for k
, v
in kw
.items():
912 self
.name
= kw
['lv_name']
914 raise ValueError('Volume must have a non-empty name')
915 self
.tags
= parse_tags(kw
['lv_tags'])
916 self
.encrypted
= self
.tags
.get('ceph.encrypted', '0') == '1'
917 self
.used_by_ceph
= 'ceph.osd_id' in self
.tags
920 return '<%s>' % self
.lv_api
['lv_path']
923 return self
.__str
__()
927 obj
.update(self
.lv_api
)
928 obj
['tags'] = self
.tags
929 obj
['name'] = self
.name
930 obj
['type'] = self
.tags
['ceph.type']
931 obj
['path'] = self
.lv_path
935 if not self
.used_by_ceph
:
937 'name': self
.lv_name
,
938 'comment': 'not used by ceph'
941 type_
= self
.tags
['ceph.type']
943 'name': self
.lv_name
,
944 'osd_id': self
.tags
['ceph.osd_id'],
945 'cluster_name': self
.tags
['ceph.cluster_name'],
947 'osd_fsid': self
.tags
['ceph.osd_fsid'],
948 'cluster_fsid': self
.tags
['ceph.cluster_fsid'],
950 type_uuid
= '{}_uuid'.format(type_
)
951 report
[type_uuid
] = self
.tags
['ceph.{}'.format(type_uuid
)]
954 def clear_tags(self
):
956 Removes all tags from the Logical Volume.
958 for k
in list(self
.tags
):
962 def set_tags(self
, tags
):
964 :param tags: A dictionary of tag names and values, like::
967 "ceph.osd_fsid": "aaa-fff-bbbb",
971 At the end of all modifications, the tags are refreshed to reflect
972 LVM's most current view.
974 for k
, v
in tags
.items():
978 def clear_tag(self
, key
):
979 if self
.tags
.get(key
):
980 current_value
= self
.tags
[key
]
981 tag
= "%s=%s" % (key
, current_value
)
982 process
.call(['lvchange', '--deltag', tag
, self
.lv_path
])
986 def set_tag(self
, key
, value
):
988 Set the key/value pair as an LVM tag.
990 # remove it first if it exists
996 '--addtag', '%s=%s' % (key
, value
), self
.lv_path
999 self
.tags
[key
] = value
1001 def deactivate(self
):
1003 Deactivate the LV by calling lvchange -an
1005 process
.call(['lvchange', '-an', self
.lv_path
])
1008 class Volumes(list):
1010 A list of all known (logical) volumes for the current system, with the ability
1011 to filter them via keyword arguments.
1017 def _populate(self
):
1018 # get all the lvs in the current system
1019 for lv_item
in get_api_lvs():
1020 self
.append(Volume(**lv_item
))
1024 Delete all the items in the list, used internally only so that we can
1025 dynamically allocate the items when filtering without the concern of
1026 messing up the contents
1030 def _filter(self
, lv_name
=None, vg_name
=None, lv_path
=None, lv_uuid
=None, lv_tags
=None):
1032 The actual method that filters using a new list. Useful so that other
1033 methods that do not want to alter the contents of the list (e.g.
1034 ``self.find``) can operate safely.
1036 filtered
= [i
for i
in self
]
1038 filtered
= [i
for i
in filtered
if i
.lv_name
== lv_name
]
1041 filtered
= [i
for i
in filtered
if i
.vg_name
== vg_name
]
1044 filtered
= [i
for i
in filtered
if i
.lv_uuid
== lv_uuid
]
1047 filtered
= [i
for i
in filtered
if i
.lv_path
== lv_path
]
1049 # at this point, `filtered` has either all the volumes in self or is an
1050 # actual filtered list if any filters were applied
1053 for volume
in filtered
:
1054 # all the tags we got need to match on the volume
1055 matches
= all(volume
.tags
.get(k
) == str(v
) for k
, v
in lv_tags
.items())
1057 tag_filtered
.append(volume
)
1062 def filter(self
, lv_name
=None, vg_name
=None, lv_path
=None, lv_uuid
=None, lv_tags
=None):
1064 Filter out volumes on top level attributes like ``lv_name`` or by
1065 ``lv_tags`` where a dict is required. For example, to find a volume
1066 that has an OSD ID of 0, the filter would look like::
1068 lv_tags={'ceph.osd_id': '0'}
1071 if not any([lv_name
, vg_name
, lv_path
, lv_uuid
, lv_tags
]):
1072 raise TypeError('.filter() requires lv_name, vg_name, lv_path, lv_uuid, or tags (none given)')
1073 # first find the filtered volumes with the values in self
1074 filtered_volumes
= self
._filter
(
1081 # then purge everything
1083 # and add the filtered items
1084 self
.extend(filtered_volumes
)
1086 def get(self
, lv_name
=None, vg_name
=None, lv_path
=None, lv_uuid
=None, lv_tags
=None):
1088 This is a bit expensive, since it will try to filter out all the
1089 matching items in the list, filter them out applying anything that was
1090 added and return the matching item.
1092 This method does *not* alter the list, and it will raise an error if
1093 multiple LVs are matched
1095 It is useful to use ``tags`` when trying to find a specific logical volume,
1096 but it can also lead to multiple lvs being found, since a lot of metadata
1097 is shared between lvs of a distinct OSD.
1099 if not any([lv_name
, vg_name
, lv_path
, lv_uuid
, lv_tags
]):
1111 raise MultipleLVsError(lv_name
, lv_path
)
1115 def create_lv(name_prefix
,
1124 Create a Logical Volume in a Volume Group. Command looks like::
1126 lvcreate -L 50G -n gfslv vg0
1128 ``name_prefix`` is required. If ``size`` is provided its expected to be a
1129 byte count. Tags are an optional dictionary and is expected to
1130 conform to the convention of prefixing them with "ceph." like::
1132 {"ceph.block_device": "/dev/ceph/osd-1"}
1134 :param name_prefix: name prefix for the LV, typically somehting like ceph-osd-block
1135 :param uuid: UUID to ensure uniqueness; is combined with name_prefix to
1137 :param vg: optional, pass an existing VG to create LV
1138 :param device: optional, device to use. Either device of vg must be passed
1139 :param slots: optional, number of slots to divide vg up, LV will occupy one
1140 one slot if enough space is available
1141 :param extends: optional, how many lvm extends to use, supersedes slots
1142 :param size: optional, target LV size in bytes, supersedes extents,
1143 resulting LV might be smaller depending on extent
1144 size of the underlying VG
1145 :param tags: optional, a dict of lvm tags to set on the LV
1147 name
= '{}-{}'.format(name_prefix
, uuid
)
1150 raise RuntimeError("Must either specify vg or device, none given")
1151 # check if a vgs starting with ceph already exists
1152 vgs
= get_device_vgs(device
, 'ceph')
1157 vg
= create_vg(device
, name_prefix
='ceph')
1161 extents
= vg
.bytes_to_extents(size
)
1162 logger
.debug('size was passed: {} -> {}'.format(size
, extents
))
1163 elif slots
and not extents
:
1164 extents
= vg
.slots_to_extents(slots
)
1165 logger
.debug('slots was passed: {} -> {}'.format(slots
, extents
))
1172 '{}'.format(extents
),
1173 '-n', name
, vg
.vg_name
1175 # create the lv with all the space available, this is needed because the
1176 # system call is different for LVM
1183 '-n', name
, vg
.vg_name
1185 process
.run(command
)
1187 lv
= get_lv(lv_name
=name
, vg_name
=vg
.vg_name
)
1191 "ceph.osd_id": "null",
1192 "ceph.type": "null",
1193 "ceph.cluster_fsid": "null",
1194 "ceph.osd_fsid": "null",
1196 # when creating a distinct type, the caller doesn't know what the path will
1197 # be so this function will set it after creation using the mapping
1198 # XXX add CEPH_VOLUME_LVM_DEBUG to enable -vvvv on lv operations
1200 'journal': 'ceph.journal_device',
1201 'data': 'ceph.data_device',
1202 'block': 'ceph.block_device',
1203 'wal': 'ceph.wal_device',
1204 'db': 'ceph.db_device',
1205 'lockbox': 'ceph.lockbox_device', # XXX might not ever need this lockbox sorcery
1207 path_tag
= type_path_tag
.get(tags
.get('ceph.type'))
1209 tags
.update({path_tag
: lv
.lv_path
})
1218 Removes a logical volume given it's absolute path.
1220 Will return True if the lv is successfully removed or
1221 raises a RuntimeError if the removal fails.
1223 :param lv: A ``Volume`` object or the path for an LV
1225 if isinstance(lv
, Volume
):
1230 stdout
, stderr
, returncode
= process
.call(
1238 terminal_verbose
=True,
1241 raise RuntimeError("Unable to remove %s" % path
)
1245 def is_lv(dev
, lvs
=None):
1247 Boolean to detect if a device is an LV or not.
1249 splitname
= dmsetup_splitname(dev
)
1250 # Allowing to optionally pass `lvs` can help reduce repetitive checks for
1251 # multiple devices at once.
1252 if lvs
is None or len(lvs
) == 0:
1255 if splitname
.get('LV_NAME'):
1256 lvs
.filter(lv_name
=splitname
['LV_NAME'], vg_name
=splitname
['VG_NAME'])
1260 def get_lv_by_name(name
):
1261 stdout
, stderr
, returncode
= process
.call(
1262 ['lvs', '--noheadings', '-o', LV_FIELDS
, '-S',
1263 'lv_name={}'.format(name
)],
1264 verbose_on_failure
=False
1266 lvs
= _output_parser(stdout
, LV_FIELDS
)
1267 return [Volume(**lv
) for lv
in lvs
]
1269 def get_lvs_by_tag(lv_tag
):
1270 stdout
, stderr
, returncode
= process
.call(
1271 ['lvs', '--noheadings', '--separator=";"', '-a', '-o', LV_FIELDS
, '-S',
1272 'lv_tags={{{}}}'.format(lv_tag
)],
1273 verbose_on_failure
=False
1275 lvs
= _output_parser(stdout
, LV_FIELDS
)
1276 return [Volume(**lv
) for lv
in lvs
]
1278 def get_lv(lv_name
=None, vg_name
=None, lv_path
=None, lv_uuid
=None, lv_tags
=None, lvs
=None):
1280 Return a matching lv for the current system, requiring ``lv_name``,
1281 ``vg_name``, ``lv_path`` or ``tags``. Raises an error if more than one lv
1284 It is useful to use ``tags`` when trying to find a specific logical volume,
1285 but it can also lead to multiple lvs being found, since a lot of metadata
1286 is shared between lvs of a distinct OSD.
1288 if not any([lv_name
, vg_name
, lv_path
, lv_uuid
, lv_tags
]):
1293 lv_name
=lv_name
, vg_name
=vg_name
, lv_path
=lv_path
, lv_uuid
=lv_uuid
,
1298 def get_lv_from_argument(argument
):
1300 Helper proxy function that consumes a possible logical volume passed in from the CLI
1301 in the form of `vg/lv`, but with some validation so that an argument that is a full
1302 path to a device can be ignored
1304 if argument
.startswith('/'):
1305 lv
= get_lv(lv_path
=argument
)
1308 vg_name
, lv_name
= argument
.split('/')
1309 except (ValueError, AttributeError):
1311 return get_lv(lv_name
=lv_name
, vg_name
=vg_name
)
1314 def create_lvs(volume_group
, parts
=None, size
=None, name_prefix
='ceph-lv'):
1316 Create multiple Logical Volumes from a Volume Group by calculating the
1317 proper extents from ``parts`` or ``size``. A custom prefix can be used
1318 (defaults to ``ceph-lv``), these names are always suffixed with a uuid.
1320 LV creation in ceph-volume will require tags, this is expected to be
1321 pre-computed by callers who know Ceph metadata like OSD IDs and FSIDs. It
1322 will probably not be the case when mass-creating LVs, so common/default
1323 tags will be set to ``"null"``.
1325 .. note:: LVs that are not in use can be detected by querying LVM for tags that are
1328 :param volume_group: The volume group (vg) to use for LV creation
1329 :type group: ``VolumeGroup()`` object
1330 :param parts: Number of LVs to create *instead of* ``size``.
1332 :param size: Size (in gigabytes) of LVs to create, e.g. "as many 10gb LVs as possible"
1334 :param extents: The number of LVM extents to use to create the LV. Useful if looking to have
1335 accurate LV sizes (LVM rounds sizes otherwise)
1337 if parts
is None and size
is None:
1338 # fallback to just one part (using 100% of the vg)
1342 "ceph.osd_id": "null",
1343 "ceph.type": "null",
1344 "ceph.cluster_fsid": "null",
1345 "ceph.osd_fsid": "null",
1347 sizing
= volume_group
.sizing(parts
=parts
, size
=size
)
1348 for part
in range(0, sizing
['parts']):
1349 size
= sizing
['sizes']
1350 extents
= sizing
['extents']
1352 create_lv(name_prefix
, uuid
.uuid4(), vg
=volume_group
, extents
=extents
, tags
=tags
)
1357 def get_device_lvs(device
, name_prefix
=''):
1358 stdout
, stderr
, returncode
= process
.call(
1359 ['pvs'] + LV_CMD_OPTIONS
+ ['-o', LV_FIELDS
, device
],
1360 verbose_on_failure
=False
1362 lvs
= _output_parser(stdout
, LV_FIELDS
)
1363 return [Volume(**lv
) for lv
in lvs
if lv
['lv_name'] and
1364 lv
['lv_name'].startswith(name_prefix
)]
1367 #############################################################
1369 # New methods to get PVs, LVs, and VGs.
1370 # Later, these can be easily merged with get_api_* methods
1372 ###########################################################
1374 def convert_filters_to_str(filters
):
1376 Convert filter args from dictionary to following format -
1377 filters={filter_name=filter_val,...}
1383 for k
, v
in filters
.items():
1384 filter_arg
+= k
+ '=' + v
+ ','
1385 # get rid of extra comma at the end
1386 filter_arg
= filter_arg
[:len(filter_arg
) - 1]
1390 def convert_tags_to_str(tags
):
1392 Convert tags from dictionary to following format -
1393 tags={tag_name=tag_val,...}
1399 for k
, v
in tags
.items():
1400 tag_arg
+= k
+ '=' + v
+ ','
1401 # get rid of extra comma at the end
1402 tag_arg
= tag_arg
[:len(tag_arg
) - 1] + '}'
1406 def make_filters_lvmcmd_ready(filters
, tags
):
1408 Convert filters (including tags) from dictionary to following format -
1409 filter_name=filter_val...,tags={tag_name=tag_val,...}
1411 The command will look as follows =
1412 lvs -S filter_name=filter_val...,tags={tag_name=tag_val,...}
1414 filters
= convert_filters_to_str(filters
)
1415 tags
= convert_tags_to_str(tags
)
1417 if filters
and tags
:
1418 return filters
+ ',' + tags
1419 if filters
and not tags
:
1421 if not filters
and tags
:
1426 def get_pvs(fields
=PV_FIELDS
, filters
='', tags
=None):
1428 Return a list of PVs that are available on the system and match the
1429 filters and tags passed. Argument filters takes a dictionary containing
1430 arguments required by -S option of LVM. Passing a list of LVM tags can be
1431 quite tricky to pass as a dictionary within dictionary, therefore pass
1432 dictionary of tags via tags argument and tricky part will be taken care of
1433 by the helper methods.
1435 :param fields: string containing list of fields to be displayed by the
1437 :param sep: string containing separator to be used between two fields
1438 :param filters: dictionary containing LVM filters
1439 :param tags: dictionary containng LVM tags
1440 :returns: list of class PVolume object representing pvs on the system
1442 filters
= make_filters_lvmcmd_ready(filters
, tags
)
1443 args
= ['pvs', '--no-heading', '--readonly', '--separator=";"', '-S',
1444 filters
, '-o', fields
]
1446 stdout
, stderr
, returncode
= process
.call(args
, verbose_on_failure
=False)
1447 pvs_report
= _output_parser(stdout
, fields
)
1448 return [PVolume(**pv_report
) for pv_report
in pvs_report
]
1450 def get_first_pv(fields
=PV_FIELDS
, filters
=None, tags
=None):
1452 Wrapper of get_pv meant to be a convenience method to avoid the phrase::
1457 pvs
= get_pvs(fields
=fields
, filters
=filters
, tags
=tags
)
1458 return pvs
[0] if len(pvs
) > 0 else []
1460 def get_vgs(fields
=VG_FIELDS
, filters
='', tags
=None):
1462 Return a list of VGs that are available on the system and match the
1463 filters and tags passed. Argument filters takes a dictionary containing
1464 arguments required by -S option of LVM. Passing a list of LVM tags can be
1465 quite tricky to pass as a dictionary within dictionary, therefore pass
1466 dictionary of tags via tags argument and tricky part will be taken care of
1467 by the helper methods.
1469 :param fields: string containing list of fields to be displayed by the
1471 :param sep: string containing separator to be used between two fields
1472 :param filters: dictionary containing LVM filters
1473 :param tags: dictionary containng LVM tags
1474 :returns: list of class VolumeGroup object representing vgs on the system
1476 filters
= make_filters_lvmcmd_ready(filters
, tags
)
1477 args
= ['vgs'] + VG_CMD_OPTIONS
+ ['-S', filters
, '-o', fields
]
1479 stdout
, stderr
, returncode
= process
.call(args
, verbose_on_failure
=False)
1480 vgs_report
=_output_parser(stdout
, fields
)
1481 return [VolumeGroup(**vg_report
) for vg_report
in vgs_report
]
1483 def get_first_vg(fields
=VG_FIELDS
, filters
=None, tags
=None):
1485 Wrapper of get_vg meant to be a convenience method to avoid the phrase::
1490 vgs
= get_vgs(fields
=fields
, filters
=filters
, tags
=tags
)
1491 return vgs
[0] if len(vgs
) > 0 else []
1493 def get_lvs(fields
=LV_FIELDS
, filters
='', tags
=None):
1495 Return a list of LVs that are available on the system and match the
1496 filters and tags passed. Argument filters takes a dictionary containing
1497 arguments required by -S option of LVM. Passing a list of LVM tags can be
1498 quite tricky to pass as a dictionary within dictionary, therefore pass
1499 dictionary of tags via tags argument and tricky part will be taken care of
1500 by the helper methods.
1502 :param fields: string containing list of fields to be displayed by the
1504 :param sep: string containing separator to be used between two fields
1505 :param filters: dictionary containing LVM filters
1506 :param tags: dictionary containng LVM tags
1507 :returns: list of class Volume object representing LVs on the system
1509 filters
= make_filters_lvmcmd_ready(filters
, tags
)
1510 args
= ['lvs'] + LV_CMD_OPTIONS
+ ['-S', filters
, '-o', fields
]
1512 stdout
, stderr
, returncode
= process
.call(args
, verbose_on_failure
=False)
1513 lvs_report
= _output_parser(stdout
, fields
)
1514 return [Volume(**lv_report
) for lv_report
in lvs_report
]
1516 def get_first_lv(fields
=LV_FIELDS
, filters
=None, tags
=None):
1518 Wrapper of get_lv meant to be a convenience method to avoid the phrase::
1523 lvs
= get_lvs(fields
=fields
, filters
=filters
, tags
=tags
)
1524 return lvs
[0] if len(lvs
) > 0 else []