]>
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 # spliting 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 fiels
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
70 parts
= line
[0].split(';')
73 part
= part
.replace("'", '')
74 key
, value
= part
.split('=')
75 if 'DM_VG_NAME' in key
:
76 value
= value
.split('/dev/mapper/')[-1]
77 key
= key
.split('DM_')[-1]
83 def sizing(device_size
, parts
=None, size
=None):
85 Calculate proper sizing to fully utilize the volume group in the most
86 efficient way possible. To prevent situations where LVM might accept
87 a percentage that is beyond the vg's capabilities, it will refuse with
88 an error when requesting a larger-than-possible parameter, in addition
89 to rounding down calculations.
91 A dictionary with different sizing parameters is returned, to make it
92 easier for others to choose what they need in order to create logical
95 >>> sizing(100, parts=2)
96 >>> {'parts': 2, 'percentages': 50, 'sizes': 50}
99 if parts
is not None and size
is not None:
101 "Cannot process sizing with both parts (%s) and size (%s)" % (parts
, size
)
104 if size
and size
> device_size
:
105 raise SizeAllocationError(size
, device_size
)
107 def get_percentage(parts
):
108 return int(floor(100 / float(parts
)))
110 if parts
is not None:
111 # Prevent parts being 0, falling back to 1 (100% usage)
113 percentages
= get_percentage(parts
)
116 parts
= int(device_size
/ size
) or 1
117 percentages
= get_percentage(parts
)
119 sizes
= device_size
/ parts
if parts
else int(floor(device_size
))
123 'percentages': percentages
,
128 def parse_tags(lv_tags
):
130 Return a dictionary mapping of all the tags associated with
131 a Volume from the comma-separated tags coming from the LVM API
135 "ceph.osd_fsid=aaa-fff-bbbb,ceph.osd_id=0"
137 For the above example, the expected return value would be::
140 "ceph.osd_fsid": "aaa-fff-bbbb",
147 tags
= lv_tags
.split(',')
148 for tag_assignment
in tags
:
149 if not tag_assignment
.startswith('ceph.'):
151 key
, value
= tag_assignment
.split('=', 1)
152 tag_mapping
[key
] = value
157 def _vdo_parents(devices
):
159 It is possible we didn't get a logical volume, or a mapper path, but
160 a device like /dev/sda2, to resolve this, we must look at all the slaves of
161 every single device in /sys/block and if any of those devices is related to
162 VDO devices, then we can add the parent
165 for parent
in os
.listdir('/sys/block'):
166 for slave
in os
.listdir('/sys/block/%s/slaves' % parent
):
168 parent_devices
.append('/dev/%s' % parent
)
169 parent_devices
.append(parent
)
170 return parent_devices
173 def _vdo_slaves(vdo_names
):
175 find all the slaves associated with each vdo name (from realpath) by going
176 into /sys/block/<realpath>/slaves
179 for vdo_name
in vdo_names
:
180 mapper_path
= '/dev/mapper/%s' % vdo_name
181 if not os
.path
.exists(mapper_path
):
183 # resolve the realpath and realname of the vdo mapper
184 vdo_realpath
= os
.path
.realpath(mapper_path
)
185 vdo_realname
= vdo_realpath
.split('/')[-1]
186 slaves_path
= '/sys/block/%s/slaves' % vdo_realname
187 if not os
.path
.exists(slaves_path
):
189 devices
.append(vdo_realpath
)
190 devices
.append(mapper_path
)
191 devices
.append(vdo_realname
)
192 for slave
in os
.listdir(slaves_path
):
193 devices
.append('/dev/%s' % slave
)
194 devices
.append(slave
)
200 A VDO device can be composed from many different devices, go through each
201 one of those devices and its slaves (if any) and correlate them back to
202 /dev/mapper and their realpaths, and then check if they appear as part of
203 /sys/kvdo/<name>/statistics
205 From the realpath of a logical volume, determine if it is a VDO device or
206 not, by correlating it to the presence of the name in
207 /sys/kvdo/<name>/statistics and all the previously captured devices
209 if not os
.path
.isdir('/sys/kvdo'):
211 realpath
= os
.path
.realpath(path
)
212 realpath_name
= realpath
.split('/')[-1]
215 # get all the vdo names
216 for dirname
in os
.listdir('/sys/kvdo/'):
217 if os
.path
.isdir('/sys/kvdo/%s/statistics' % dirname
):
218 vdo_names
.add(dirname
)
220 # find all the slaves associated with each vdo name (from realpath) by
221 # going into /sys/block/<realpath>/slaves
222 devices
.extend(_vdo_slaves(vdo_names
))
224 # Find all possible parents, looking into slaves that are related to VDO
225 devices
.extend(_vdo_parents(devices
))
230 realpath_name
in devices
])
235 Detect if a path is backed by VDO, proxying the actual call to _is_vdo so
236 that we can prevent an exception breaking OSD creation. If an exception is
237 raised, it will get captured and logged to file, while returning
245 logger
.exception('Unable to properly detect device as VDO: %s', path
)
249 def dmsetup_splitname(dev
):
251 Run ``dmsetup splitname`` and parse the results.
253 .. warning:: This call does not ensure that the device is correct or that
254 it exists. ``dmsetup`` will happily take a non existing path and still
255 return a 0 exit status.
258 'dmsetup', 'splitname', '--noheadings',
259 "--separator=';'", '--nameprefixes', dev
261 out
, err
, rc
= process
.call(command
)
262 return _splitname_parser(out
)
265 def is_lv(dev
, lvs
=None):
267 Boolean to detect if a device is an LV or not.
269 splitname
= dmsetup_splitname(dev
)
270 # Allowing to optionally pass `lvs` can help reduce repetitive checks for
271 # multiple devices at once.
272 lvs
= lvs
if lvs
is not None else Volumes()
273 if splitname
.get('LV_NAME'):
274 lvs
.filter(lv_name
=splitname
['LV_NAME'], vg_name
=splitname
['VG_NAME'])
281 Return the list of group volumes available in the system using flags to
282 include common metadata associated with them
284 Command and sample delimited output should look like::
286 $ vgs --noheadings --units=g --readonly --separator=';' \
287 -o vg_name,pv_count,lv_count,snap_count,vg_attr,vg_size,vg_free
288 ubuntubox-vg;1;2;0;wz--n-;299.52g;12.00m
289 osd_vg;3;1;0;wz--n-;29.21g;9.21g
291 To normalize sizing, the units are forced in 'g' which is equivalent to
292 gigabytes, which uses multiples of 1024 (as opposed to 1000)
294 fields
= 'vg_name,pv_count,lv_count,snap_count,vg_attr,vg_size,vg_free,vg_free_count'
295 stdout
, stderr
, returncode
= process
.call(
296 ['vgs', '--noheadings', '--readonly', '--units=g', '--separator=";"', '-o', fields
]
298 return _output_parser(stdout
, fields
)
303 Return the list of logical volumes available in the system using flags to include common
304 metadata associated with them
306 Command and delimited output should look like::
308 $ lvs --noheadings --readonly --separator=';' -o lv_tags,lv_path,lv_name,vg_name
309 ;/dev/ubuntubox-vg/root;root;ubuntubox-vg
310 ;/dev/ubuntubox-vg/swap_1;swap_1;ubuntubox-vg
313 fields
= 'lv_tags,lv_path,lv_name,vg_name,lv_uuid,lv_size'
314 stdout
, stderr
, returncode
= process
.call(
315 ['lvs', '--noheadings', '--readonly', '--separator=";"', '-o', fields
]
317 return _output_parser(stdout
, fields
)
322 Return the list of physical volumes configured for lvm and available in the
323 system using flags to include common metadata associated with them like the uuid
325 This will only return physical volumes set up to work with LVM.
327 Command and delimited output should look like::
329 $ pvs --noheadings --readonly --separator=';' -o pv_name,pv_tags,pv_uuid
331 /dev/sdv;;07A4F654-4162-4600-8EB3-88D1E42F368D
334 fields
= 'pv_name,pv_tags,pv_uuid,vg_name,lv_uuid'
336 stdout
, stderr
, returncode
= process
.call(
337 ['pvs', '--no-heading', '--readonly', '--separator=";"', '-o', fields
]
340 return _output_parser(stdout
, fields
)
343 def get_lv_from_argument(argument
):
345 Helper proxy function that consumes a possible logical volume passed in from the CLI
346 in the form of `vg/lv`, but with some validation so that an argument that is a full
347 path to a device can be ignored
349 if argument
.startswith('/'):
350 lv
= get_lv(lv_path
=argument
)
353 vg_name
, lv_name
= argument
.split('/')
354 except (ValueError, AttributeError):
356 return get_lv(lv_name
=lv_name
, vg_name
=vg_name
)
359 def get_lv(lv_name
=None, vg_name
=None, lv_path
=None, lv_uuid
=None, lv_tags
=None):
361 Return a matching lv for the current system, requiring ``lv_name``,
362 ``vg_name``, ``lv_path`` or ``tags``. Raises an error if more than one lv
365 It is useful to use ``tags`` when trying to find a specific logical volume,
366 but it can also lead to multiple lvs being found, since a lot of metadata
367 is shared between lvs of a distinct OSD.
369 if not any([lv_name
, vg_name
, lv_path
, lv_uuid
, lv_tags
]):
373 lv_name
=lv_name
, vg_name
=vg_name
, lv_path
=lv_path
, lv_uuid
=lv_uuid
,
378 def get_pv(pv_name
=None, pv_uuid
=None, pv_tags
=None):
380 Return a matching pv (physical volume) for the current system, requiring
381 ``pv_name``, ``pv_uuid``, or ``pv_tags``. Raises an error if more than one
384 if not any([pv_name
, pv_uuid
, pv_tags
]):
387 return pvs
.get(pv_name
=pv_name
, pv_uuid
=pv_uuid
, pv_tags
=pv_tags
)
390 def create_pv(device
):
392 Create a physical volume from a device, useful when devices need to be later mapped
399 '--yes', # answer yes to any prompts
404 def create_vg(devices
, name
=None, name_prefix
=None):
406 Create a Volume Group. Command looks like::
408 vgcreate --force --yes group_name device
410 Once created the volume group is returned as a ``VolumeGroup`` object
412 :param devices: A list of devices to create a VG. Optionally, a single
413 device (as a string) can be used.
414 :param name: Optionally set the name of the VG, defaults to 'ceph-{uuid}'
415 :param name_prefix: Optionally prefix the name of the VG, which will get combined
418 if isinstance(devices
, set):
419 devices
= list(devices
)
420 if not isinstance(devices
, list):
423 name
= "%s-%s" % (name_prefix
, str(uuid
.uuid4()))
425 name
= "ceph-%s" % str(uuid
.uuid4())
433 vg
= get_vg(vg_name
=name
)
437 def extend_vg(vg
, devices
):
439 Extend a Volume Group. Command looks like::
441 vgextend --force --yes group_name [device, ...]
443 Once created the volume group is extended and returned as a ``VolumeGroup`` object
445 :param vg: A VolumeGroup object
446 :param devices: A list of devices to extend the VG. Optionally, a single
447 device (as a string) can be used.
449 if not isinstance(devices
, list):
458 vg
= get_vg(vg_name
=vg
.name
)
462 def remove_vg(vg_name
):
464 Removes a volume group.
466 fail_msg
= "Unable to remove vg %s" % vg_name
478 def remove_pv(pv_name
):
480 Removes a physical volume.
482 fail_msg
= "Unable to remove vg %s" % pv_name
496 Removes a logical volume given it's absolute path.
498 Will return True if the lv is successfully removed or
499 raises a RuntimeError if the removal fails.
501 stdout
, stderr
, returncode
= process
.call(
509 terminal_verbose
=True,
512 raise RuntimeError("Unable to remove %s" % path
)
516 def create_lv(name
, group
, extents
=None, size
=None, tags
=None, uuid_name
=False):
518 Create a Logical Volume in a Volume Group. Command looks like::
520 lvcreate -L 50G -n gfslv vg0
522 ``name``, ``group``, are required. If ``size`` is provided it must follow
523 lvm's size notation (like 1G, or 20M). Tags are an optional dictionary and is expected to
524 conform to the convention of prefixing them with "ceph." like::
526 {"ceph.block_device": "/dev/ceph/osd-1"}
528 :param uuid_name: Optionally combine the ``name`` with UUID to ensure uniqueness
531 name
= '%s-%s' % (name
, uuid
.uuid4())
534 "ceph.osd_id": "null",
536 "ceph.cluster_fsid": "null",
537 "ceph.osd_fsid": "null",
540 # XXX add CEPH_VOLUME_LVM_DEBUG to enable -vvvv on lv operations
542 'journal': 'ceph.journal_device',
543 'data': 'ceph.data_device',
544 'block': 'ceph.block_device',
545 'wal': 'ceph.wal_device',
546 'db': 'ceph.db_device',
547 'lockbox': 'ceph.lockbox_device', # XXX might not ever need this lockbox sorcery
565 # create the lv with all the space available, this is needed because the
566 # system call is different for LVM
576 lv
= get_lv(lv_name
=name
, vg_name
=group
)
579 # when creating a distinct type, the caller doesn't know what the path will
580 # be so this function will set it after creation using the mapping
581 path_tag
= type_path_tag
.get(tags
.get('ceph.type'))
584 {path_tag
: lv
.lv_path
}
589 def create_lvs(volume_group
, parts
=None, size
=None, name_prefix
='ceph-lv'):
591 Create multiple Logical Volumes from a Volume Group by calculating the
592 proper extents from ``parts`` or ``size``. A custom prefix can be used
593 (defaults to ``ceph-lv``), these names are always suffixed with a uuid.
595 LV creation in ceph-volume will require tags, this is expected to be
596 pre-computed by callers who know Ceph metadata like OSD IDs and FSIDs. It
597 will probably not be the case when mass-creating LVs, so common/default
598 tags will be set to ``"null"``.
600 .. note:: LVs that are not in use can be detected by querying LVM for tags that are
603 :param volume_group: The volume group (vg) to use for LV creation
604 :type group: ``VolumeGroup()`` object
605 :param parts: Number of LVs to create *instead of* ``size``.
607 :param size: Size (in gigabytes) of LVs to create, e.g. "as many 10gb LVs as possible"
609 :param extents: The number of LVM extents to use to create the LV. Useful if looking to have
610 accurate LV sizes (LVM rounds sizes otherwise)
612 if parts
is None and size
is None:
613 # fallback to just one part (using 100% of the vg)
617 "ceph.osd_id": "null",
619 "ceph.cluster_fsid": "null",
620 "ceph.osd_fsid": "null",
622 sizing
= volume_group
.sizing(parts
=parts
, size
=size
)
623 for part
in range(0, sizing
['parts']):
624 size
= sizing
['sizes']
625 extents
= sizing
['extents']
626 lv_name
= '%s-%s' % (name_prefix
, uuid
.uuid4())
628 create_lv(lv_name
, volume_group
.name
, extents
=extents
, tags
=tags
)
633 def get_vg(vg_name
=None, vg_tags
=None):
635 Return a matching vg for the current system, requires ``vg_name`` or
636 ``tags``. Raises an error if more than one vg is found.
638 It is useful to use ``tags`` when trying to find a specific volume group,
639 but it can also lead to multiple vgs being found.
641 if not any([vg_name
, vg_tags
]):
644 return vgs
.get(vg_name
=vg_name
, vg_tags
=vg_tags
)
647 class VolumeGroups(list):
649 A list of all known volume groups for the current system, with the ability
650 to filter them via keyword arguments.
657 # get all the vgs in the current system
658 for vg_item
in get_api_vgs():
659 self
.append(VolumeGroup(**vg_item
))
663 Deplete all the items in the list, used internally only so that we can
664 dynamically allocate the items when filtering without the concern of
665 messing up the contents
669 def _filter(self
, vg_name
=None, vg_tags
=None):
671 The actual method that filters using a new list. Useful so that other
672 methods that do not want to alter the contents of the list (e.g.
673 ``self.find``) can operate safely.
675 .. note:: ``vg_tags`` is not yet implemented
677 filtered
= [i
for i
in self
]
679 filtered
= [i
for i
in filtered
if i
.vg_name
== vg_name
]
681 # at this point, `filtered` has either all the volumes in self or is an
682 # actual filtered list if any filters were applied
685 for volume
in filtered
:
686 matches
= all(volume
.tags
.get(k
) == str(v
) for k
, v
in vg_tags
.items())
688 tag_filtered
.append(volume
)
693 def filter(self
, vg_name
=None, vg_tags
=None):
695 Filter out groups on top level attributes like ``vg_name`` or by
696 ``vg_tags`` where a dict is required. For example, to find a Ceph group
697 with dmcache as the type, the filter would look like::
699 vg_tags={'ceph.type': 'dmcache'}
701 .. warning:: These tags are not documented because they are currently
702 unused, but are here to maintain API consistency
704 if not any([vg_name
, vg_tags
]):
705 raise TypeError('.filter() requires vg_name or vg_tags (none given)')
706 # first find the filtered volumes with the values in self
707 filtered_groups
= self
._filter
(
711 # then purge everything
713 # and add the filtered items
714 self
.extend(filtered_groups
)
716 def get(self
, vg_name
=None, vg_tags
=None):
718 This is a bit expensive, since it will try to filter out all the
719 matching items in the list, filter them out applying anything that was
720 added and return the matching item.
722 This method does *not* alter the list, and it will raise an error if
723 multiple VGs are matched
725 It is useful to use ``tags`` when trying to find a specific volume group,
726 but it can also lead to multiple vgs being found (although unlikely)
728 if not any([vg_name
, vg_tags
]):
737 # this is probably never going to happen, but it is here to keep
738 # the API code consistent
739 raise MultipleVGsError(vg_name
)
745 A list of all known (logical) volumes for the current system, with the ability
746 to filter them via keyword arguments.
753 # get all the lvs in the current system
754 for lv_item
in get_api_lvs():
755 self
.append(Volume(**lv_item
))
759 Delete all the items in the list, used internally only so that we can
760 dynamically allocate the items when filtering without the concern of
761 messing up the contents
765 def _filter(self
, lv_name
=None, vg_name
=None, lv_path
=None, lv_uuid
=None, lv_tags
=None):
767 The actual method that filters using a new list. Useful so that other
768 methods that do not want to alter the contents of the list (e.g.
769 ``self.find``) can operate safely.
771 filtered
= [i
for i
in self
]
773 filtered
= [i
for i
in filtered
if i
.lv_name
== lv_name
]
776 filtered
= [i
for i
in filtered
if i
.vg_name
== vg_name
]
779 filtered
= [i
for i
in filtered
if i
.lv_uuid
== lv_uuid
]
782 filtered
= [i
for i
in filtered
if i
.lv_path
== lv_path
]
784 # at this point, `filtered` has either all the volumes in self or is an
785 # actual filtered list if any filters were applied
788 for volume
in filtered
:
789 # all the tags we got need to match on the volume
790 matches
= all(volume
.tags
.get(k
) == str(v
) for k
, v
in lv_tags
.items())
792 tag_filtered
.append(volume
)
797 def filter(self
, lv_name
=None, vg_name
=None, lv_path
=None, lv_uuid
=None, lv_tags
=None):
799 Filter out volumes on top level attributes like ``lv_name`` or by
800 ``lv_tags`` where a dict is required. For example, to find a volume
801 that has an OSD ID of 0, the filter would look like::
803 lv_tags={'ceph.osd_id': '0'}
806 if not any([lv_name
, vg_name
, lv_path
, lv_uuid
, lv_tags
]):
807 raise TypeError('.filter() requires lv_name, vg_name, lv_path, lv_uuid, or tags (none given)')
808 # first find the filtered volumes with the values in self
809 filtered_volumes
= self
._filter
(
816 # then purge everything
818 # and add the filtered items
819 self
.extend(filtered_volumes
)
821 def get(self
, lv_name
=None, vg_name
=None, lv_path
=None, lv_uuid
=None, lv_tags
=None):
823 This is a bit expensive, since it will try to filter out all the
824 matching items in the list, filter them out applying anything that was
825 added and return the matching item.
827 This method does *not* alter the list, and it will raise an error if
828 multiple LVs are matched
830 It is useful to use ``tags`` when trying to find a specific logical volume,
831 but it can also lead to multiple lvs being found, since a lot of metadata
832 is shared between lvs of a distinct OSD.
834 if not any([lv_name
, vg_name
, lv_path
, lv_uuid
, lv_tags
]):
846 raise MultipleLVsError(lv_name
, lv_path
)
850 class PVolumes(list):
852 A list of all known (physical) volumes for the current system, with the ability
853 to filter them via keyword arguments.
860 # get all the pvs in the current system
861 for pv_item
in get_api_pvs():
862 self
.append(PVolume(**pv_item
))
866 Deplete all the items in the list, used internally only so that we can
867 dynamically allocate the items when filtering without the concern of
868 messing up the contents
872 def _filter(self
, pv_name
=None, pv_uuid
=None, pv_tags
=None):
874 The actual method that filters using a new list. Useful so that other
875 methods that do not want to alter the contents of the list (e.g.
876 ``self.find``) can operate safely.
878 filtered
= [i
for i
in self
]
880 filtered
= [i
for i
in filtered
if i
.pv_name
== pv_name
]
883 filtered
= [i
for i
in filtered
if i
.pv_uuid
== pv_uuid
]
885 # at this point, `filtered` has either all the physical volumes in self
886 # or is an actual filtered list if any filters were applied
889 for pvolume
in filtered
:
890 matches
= all(pvolume
.tags
.get(k
) == str(v
) for k
, v
in pv_tags
.items())
892 tag_filtered
.append(pvolume
)
893 # return the tag_filtered pvolumes here, the `filtered` list is no
899 def filter(self
, pv_name
=None, pv_uuid
=None, pv_tags
=None):
901 Filter out volumes on top level attributes like ``pv_name`` or by
902 ``pv_tags`` where a dict is required. For example, to find a physical volume
903 that has an OSD ID of 0, the filter would look like::
905 pv_tags={'ceph.osd_id': '0'}
908 if not any([pv_name
, pv_uuid
, pv_tags
]):
909 raise TypeError('.filter() requires pv_name, pv_uuid, or pv_tags (none given)')
910 # first find the filtered volumes with the values in self
911 filtered_volumes
= self
._filter
(
916 # then purge everything
918 # and add the filtered items
919 self
.extend(filtered_volumes
)
921 def get(self
, pv_name
=None, pv_uuid
=None, pv_tags
=None):
923 This is a bit expensive, since it will try to filter out all the
924 matching items in the list, filter them out applying anything that was
925 added and return the matching item.
927 This method does *not* alter the list, and it will raise an error if
928 multiple pvs are matched
930 It is useful to use ``tags`` when trying to find a specific logical volume,
931 but it can also lead to multiple pvs being found, since a lot of metadata
932 is shared between pvs of a distinct OSD.
934 if not any([pv_name
, pv_uuid
, pv_tags
]):
943 if len(pvs
) > 1 and pv_tags
:
944 raise MultiplePVsError(pv_name
)
948 class VolumeGroup(object):
950 Represents an LVM group, with some top-level attributes like ``vg_name``
953 def __init__(self
, **kw
):
954 for k
, v
in kw
.items():
956 self
.name
= kw
['vg_name']
957 self
.tags
= parse_tags(kw
.get('vg_tags', ''))
960 return '<%s>' % self
.name
963 return self
.__str
__()
965 def _parse_size(self
, size
):
966 error_msg
= "Unable to convert vg size to integer: '%s'" % str(size
)
968 integer
, _
= size
.split('g')
970 logger
.exception(error_msg
)
971 raise RuntimeError(error_msg
)
973 return util
.str_to_int(integer
)
978 Parse the available size in gigabytes from the ``vg_free`` attribute, that
979 will be a string with a character ('g') to indicate gigabytes in size.
980 Returns a rounded down integer to ease internal operations::
987 return self
._parse
_size
(self
.vg_free
)
992 Parse the size in gigabytes from the ``vg_size`` attribute, that
993 will be a string with a character ('g') to indicate gigabytes in size.
994 Returns a rounded down integer to ease internal operations::
1001 return self
._parse
_size
(self
.vg_size
)
1003 def sizing(self
, parts
=None, size
=None):
1005 Calculate proper sizing to fully utilize the volume group in the most
1006 efficient way possible. To prevent situations where LVM might accept
1007 a percentage that is beyond the vg's capabilities, it will refuse with
1008 an error when requesting a larger-than-possible parameter, in addition
1009 to rounding down calculations.
1011 A dictionary with different sizing parameters is returned, to make it
1012 easier for others to choose what they need in order to create logical
1017 >>> data_vg.sizing(parts=4)
1018 {'parts': 4, 'sizes': 256, 'percentages': 25}
1019 >>> data_vg.sizing(size=512)
1020 {'parts': 2, 'sizes': 512, 'percentages': 50}
1023 :param parts: Number of parts to create LVs from
1024 :param size: Size in gigabytes to divide the VG into
1026 :raises SizeAllocationError: When requested size cannot be allocated with
1027 :raises ValueError: If both ``parts`` and ``size`` are given
1029 if parts
is not None and size
is not None:
1031 "Cannot process sizing with both parts (%s) and size (%s)" % (parts
, size
)
1034 # if size is given we need to map that to extents so that we avoid
1035 # issues when trying to get this right with a size in gigabytes find
1036 # the percentage first, cheating, because these values are thrown out
1037 vg_free_count
= util
.str_to_int(self
.vg_free_count
)
1040 extents
= int(size
* vg_free_count
/ self
.free
)
1041 disk_sizing
= sizing(self
.free
, size
=size
, parts
=parts
)
1043 if parts
is not None:
1044 # Prevent parts being 0, falling back to 1 (100% usage)
1046 size
= int(self
.free
/ parts
)
1047 extents
= size
* vg_free_count
/ self
.free
1048 disk_sizing
= sizing(self
.free
, parts
=parts
)
1050 extent_sizing
= sizing(vg_free_count
, size
=extents
)
1052 disk_sizing
['extents'] = int(extents
)
1053 disk_sizing
['percentages'] = extent_sizing
['percentages']
1057 class Volume(object):
1059 Represents a Logical Volume from LVM, with some top-level attributes like
1060 ``lv_name`` and parsed tags as a dictionary of key/value pairs.
1063 def __init__(self
, **kw
):
1064 for k
, v
in kw
.items():
1067 self
.name
= kw
['lv_name']
1068 self
.tags
= parse_tags(kw
['lv_tags'])
1069 self
.encrypted
= self
.tags
.get('ceph.encrypted', '0') == '1'
1072 return '<%s>' % self
.lv_api
['lv_path']
1075 return self
.__str
__()
1079 obj
.update(self
.lv_api
)
1080 obj
['tags'] = self
.tags
1081 obj
['name'] = self
.name
1082 obj
['type'] = self
.tags
['ceph.type']
1083 obj
['path'] = self
.lv_path
1086 def clear_tags(self
):
1088 Removes all tags from the Logical Volume.
1090 for k
, v
in self
.tags
.items():
1091 tag
= "%s=%s" % (k
, v
)
1092 process
.run(['lvchange', '--deltag', tag
, self
.lv_path
])
1094 def set_tags(self
, tags
):
1096 :param tags: A dictionary of tag names and values, like::
1099 "ceph.osd_fsid": "aaa-fff-bbbb",
1103 At the end of all modifications, the tags are refreshed to reflect
1104 LVM's most current view.
1106 for k
, v
in tags
.items():
1108 # after setting all the tags, refresh them for the current object, use the
1109 # lv_* identifiers to filter because those shouldn't change
1110 lv_object
= get_lv(lv_name
=self
.lv_name
, lv_path
=self
.lv_path
)
1111 self
.tags
= lv_object
.tags
1113 def set_tag(self
, key
, value
):
1115 Set the key/value pair as an LVM tag. Does not "refresh" the values of
1116 the current object for its tags. Meant to be a "fire and forget" type
1119 # remove it first if it exists
1120 if self
.tags
.get(key
):
1121 current_value
= self
.tags
[key
]
1122 tag
= "%s=%s" % (key
, current_value
)
1123 process
.call(['lvchange', '--deltag', tag
, self
.lv_api
['lv_path']])
1128 '--addtag', '%s=%s' % (key
, value
), self
.lv_path
1133 class PVolume(object):
1135 Represents a Physical Volume from LVM, with some top-level attributes like
1136 ``pv_name`` and parsed tags as a dictionary of key/value pairs.
1139 def __init__(self
, **kw
):
1140 for k
, v
in kw
.items():
1143 self
.name
= kw
['pv_name']
1144 self
.tags
= parse_tags(kw
['pv_tags'])
1147 return '<%s>' % self
.pv_api
['pv_name']
1150 return self
.__str
__()
1152 def set_tags(self
, tags
):
1154 :param tags: A dictionary of tag names and values, like::
1157 "ceph.osd_fsid": "aaa-fff-bbbb",
1161 At the end of all modifications, the tags are refreshed to reflect
1162 LVM's most current view.
1164 for k
, v
in tags
.items():
1166 # after setting all the tags, refresh them for the current object, use the
1167 # pv_* identifiers to filter because those shouldn't change
1168 pv_object
= get_pv(pv_name
=self
.pv_name
, pv_uuid
=self
.pv_uuid
)
1169 self
.tags
= pv_object
.tags
1171 def set_tag(self
, key
, value
):
1173 Set the key/value pair as an LVM tag. Does not "refresh" the values of
1174 the current object for its tags. Meant to be a "fire and forget" type
1177 **warning**: Altering tags on a PV has to be done ensuring that the
1178 device is actually the one intended. ``pv_name`` is *not* a persistent
1179 value, only ``pv_uuid`` is. Using ``pv_uuid`` is the best way to make
1180 sure the device getting changed is the one needed.
1182 # remove it first if it exists
1183 if self
.tags
.get(key
):
1184 current_value
= self
.tags
[key
]
1185 tag
= "%s=%s" % (key
, current_value
)
1186 process
.call(['pvchange', '--deltag', tag
, self
.pv_name
])
1191 '--addtag', '%s=%s' % (key
, value
), self
.pv_name