return _splitname_parser(out)
-def is_lv(dev, lvs=None):
- """
- Boolean to detect if a device is an LV or not.
- """
- splitname = dmsetup_splitname(dev)
- # Allowing to optionally pass `lvs` can help reduce repetitive checks for
- # multiple devices at once.
- if lvs is None or len(lvs) == 0:
- lvs = Volumes()
-
- if splitname.get('LV_NAME'):
- lvs.filter(lv_name=splitname['LV_NAME'], vg_name=splitname['VG_NAME'])
- return len(lvs) > 0
- return False
-
-
-def get_api_vgs():
- """
- Return the list of group volumes available in the system using flags to
- include common metadata associated with them
-
- Command and sample delimited output should look like::
-
- $ vgs --noheadings --units=g --readonly --separator=';' \
- -o vg_name,pv_count,lv_count,snap_count,vg_attr,vg_size,vg_free
- ubuntubox-vg;1;2;0;wz--n-;299.52g;12.00m
- osd_vg;3;1;0;wz--n-;29.21g;9.21g
-
- To normalize sizing, the units are forced in 'g' which is equivalent to
- gigabytes, which uses multiples of 1024 (as opposed to 1000)
- """
- fields = 'vg_name,pv_count,lv_count,snap_count,vg_attr,vg_size,vg_free,vg_free_count'
- stdout, stderr, returncode = process.call(
- ['vgs', '--noheadings', '--readonly', '--units=g', '--separator=";"', '-o', fields],
- verbose_on_failure=False
- )
- return _output_parser(stdout, fields)
-
-
-def get_api_lvs():
- """
- Return the list of logical volumes available in the system using flags to include common
- metadata associated with them
-
- Command and delimited output should look like::
-
- $ lvs --noheadings --readonly --separator=';' -a -o lv_tags,lv_path,lv_name,vg_name
- ;/dev/ubuntubox-vg/root;root;ubuntubox-vg
- ;/dev/ubuntubox-vg/swap_1;swap_1;ubuntubox-vg
-
- """
- fields = 'lv_tags,lv_path,lv_name,vg_name,lv_uuid,lv_size'
- stdout, stderr, returncode = process.call(
- ['lvs', '--noheadings', '--readonly', '--separator=";"', '-a', '-o', fields],
- verbose_on_failure=False
- )
- return _output_parser(stdout, fields)
+####################################
+#
+# Code for LVM Physical Volumes
+#
+################################
def get_api_pvs():
return _output_parser(stdout, fields)
-def get_lv_from_argument(argument):
+class PVolume(object):
"""
- Helper proxy function that consumes a possible logical volume passed in from the CLI
- in the form of `vg/lv`, but with some validation so that an argument that is a full
- path to a device can be ignored
+ Represents a Physical Volume from LVM, with some top-level attributes like
+ ``pv_name`` and parsed tags as a dictionary of key/value pairs.
"""
- if argument.startswith('/'):
- lv = get_lv(lv_path=argument)
- return lv
- try:
- vg_name, lv_name = argument.split('/')
- except (ValueError, AttributeError):
- return None
- return get_lv(lv_name=lv_name, vg_name=vg_name)
+ def __init__(self, **kw):
+ for k, v in kw.items():
+ setattr(self, k, v)
+ self.pv_api = kw
+ self.name = kw['pv_name']
+ self.tags = parse_tags(kw['pv_tags'])
-def get_lv(lv_name=None, vg_name=None, lv_path=None, lv_uuid=None, lv_tags=None, lvs=None):
- """
- Return a matching lv for the current system, requiring ``lv_name``,
- ``vg_name``, ``lv_path`` or ``tags``. Raises an error if more than one lv
- is found.
+ def __str__(self):
+ return '<%s>' % self.pv_api['pv_name']
- It is useful to use ``tags`` when trying to find a specific logical volume,
- but it can also lead to multiple lvs being found, since a lot of metadata
- is shared between lvs of a distinct OSD.
- """
- if not any([lv_name, vg_name, lv_path, lv_uuid, lv_tags]):
- return None
- if lvs is None:
- lvs = Volumes()
- return lvs.get(
- lv_name=lv_name, vg_name=vg_name, lv_path=lv_path, lv_uuid=lv_uuid,
- lv_tags=lv_tags
- )
+ def __repr__(self):
+ return self.__str__()
+ def set_tags(self, tags):
+ """
+ :param tags: A dictionary of tag names and values, like::
-def get_pv(pv_name=None, pv_uuid=None, pv_tags=None):
+ {
+ "ceph.osd_fsid": "aaa-fff-bbbb",
+ "ceph.osd_id": "0"
+ }
+
+ At the end of all modifications, the tags are refreshed to reflect
+ LVM's most current view.
+ """
+ for k, v in tags.items():
+ self.set_tag(k, v)
+ # after setting all the tags, refresh them for the current object, use the
+ # pv_* identifiers to filter because those shouldn't change
+ pv_object = get_pv(pv_name=self.pv_name, pv_uuid=self.pv_uuid)
+ self.tags = pv_object.tags
+
+ def set_tag(self, key, value):
+ """
+ Set the key/value pair as an LVM tag. Does not "refresh" the values of
+ the current object for its tags. Meant to be a "fire and forget" type
+ of modification.
+
+ **warning**: Altering tags on a PV has to be done ensuring that the
+ device is actually the one intended. ``pv_name`` is *not* a persistent
+ value, only ``pv_uuid`` is. Using ``pv_uuid`` is the best way to make
+ sure the device getting changed is the one needed.
+ """
+ # remove it first if it exists
+ if self.tags.get(key):
+ current_value = self.tags[key]
+ tag = "%s=%s" % (key, current_value)
+ process.call(['pvchange', '--deltag', tag, self.pv_name])
+
+ process.call(
+ [
+ 'pvchange',
+ '--addtag', '%s=%s' % (key, value), self.pv_name
+ ]
+ )
+
+
+class PVolumes(list):
"""
- Return a matching pv (physical volume) for the current system, requiring
- ``pv_name``, ``pv_uuid``, or ``pv_tags``. Raises an error if more than one
- pv is found.
+ A list of all known (physical) volumes for the current system, with the ability
+ to filter them via keyword arguments.
"""
- if not any([pv_name, pv_uuid, pv_tags]):
- return None
- pvs = PVolumes()
- return pvs.get(pv_name=pv_name, pv_uuid=pv_uuid, pv_tags=pv_tags)
+
+ def __init__(self, populate=True):
+ if populate:
+ self._populate()
+
+ def _populate(self):
+ # get all the pvs in the current system
+ for pv_item in get_api_pvs():
+ self.append(PVolume(**pv_item))
+
+ def _purge(self):
+ """
+ Deplete all the items in the list, used internally only so that we can
+ dynamically allocate the items when filtering without the concern of
+ messing up the contents
+ """
+ self[:] = []
+
+ def _filter(self, pv_name=None, pv_uuid=None, pv_tags=None):
+ """
+ The actual method that filters using a new list. Useful so that other
+ methods that do not want to alter the contents of the list (e.g.
+ ``self.find``) can operate safely.
+ """
+ filtered = [i for i in self]
+ if pv_name:
+ filtered = [i for i in filtered if i.pv_name == pv_name]
+
+ if pv_uuid:
+ filtered = [i for i in filtered if i.pv_uuid == pv_uuid]
+
+ # at this point, `filtered` has either all the physical volumes in self
+ # or is an actual filtered list if any filters were applied
+ if pv_tags:
+ tag_filtered = []
+ for pvolume in filtered:
+ matches = all(pvolume.tags.get(k) == str(v) for k, v in pv_tags.items())
+ if matches:
+ tag_filtered.append(pvolume)
+ # return the tag_filtered pvolumes here, the `filtered` list is no
+ # longer usable
+ return tag_filtered
+
+ return filtered
+
+ def filter(self, pv_name=None, pv_uuid=None, pv_tags=None):
+ """
+ Filter out volumes on top level attributes like ``pv_name`` or by
+ ``pv_tags`` where a dict is required. For example, to find a physical
+ volume that has an OSD ID of 0, the filter would look like::
+
+ pv_tags={'ceph.osd_id': '0'}
+
+ """
+ if not any([pv_name, pv_uuid, pv_tags]):
+ raise TypeError('.filter() requires pv_name, pv_uuid, or pv_tags'
+ '(none given)')
+
+ filtered_pvs = PVolumes(populate=False)
+ filtered_pvs.extend(self._filter(pv_name, pv_uuid, pv_tags))
+ return filtered_pvs
+
+ def get(self, pv_name=None, pv_uuid=None, pv_tags=None):
+ """
+ This is a bit expensive, since it will try to filter out all the
+ matching items in the list, filter them out applying anything that was
+ added and return the matching item.
+
+ This method does *not* alter the list, and it will raise an error if
+ multiple pvs are matched
+
+ It is useful to use ``tags`` when trying to find a specific logical volume,
+ but it can also lead to multiple pvs being found, since a lot of metadata
+ is shared between pvs of a distinct OSD.
+ """
+ if not any([pv_name, pv_uuid, pv_tags]):
+ return None
+ pvs = self._filter(
+ pv_name=pv_name,
+ pv_uuid=pv_uuid,
+ pv_tags=pv_tags
+ )
+ if not pvs:
+ return None
+ if len(pvs) > 1 and pv_tags:
+ raise MultiplePVsError(pv_name)
+ return pvs[0]
def create_pv(device):
])
-def create_vg(devices, name=None, name_prefix=None):
+def remove_pv(pv_name):
"""
- Create a Volume Group. Command looks like::
+ Removes a physical volume using a double `-f` to prevent prompts and fully
+ remove anything related to LVM. This is tremendously destructive, but so is all other actions
+ when zapping a device.
- vgcreate --force --yes group_name device
+ In the case where multiple PVs are found, it will ignore that fact and
+ continue with the removal, specifically in the case of messages like::
- Once created the volume group is returned as a ``VolumeGroup`` object
+ WARNING: PV $UUID /dev/DEV-1 was already found on /dev/DEV-2
- :param devices: A list of devices to create a VG. Optionally, a single
- device (as a string) can be used.
- :param name: Optionally set the name of the VG, defaults to 'ceph-{uuid}'
- :param name_prefix: Optionally prefix the name of the VG, which will get combined
- with a UUID string
+ These situations can be avoided with custom filtering rules, which this API
+ cannot handle while accommodating custom user filters.
"""
- if isinstance(devices, set):
- devices = list(devices)
- if not isinstance(devices, list):
- devices = [devices]
- if name_prefix:
- name = "%s-%s" % (name_prefix, str(uuid.uuid4()))
- elif name is None:
- name = "ceph-%s" % str(uuid.uuid4())
- process.run([
- 'vgcreate',
- '-s',
- '1G',
- '--force',
- '--yes',
- name] + devices
+ fail_msg = "Unable to remove vg %s" % pv_name
+ process.run(
+ [
+ 'pvremove',
+ '-v', # verbose
+ '-f', # force it
+ '-f', # force it
+ pv_name
+ ],
+ fail_msg=fail_msg,
)
- vg = get_vg(vg_name=name)
- return vg
-
-def extend_vg(vg, devices):
+def get_pv(pv_name=None, pv_uuid=None, pv_tags=None):
"""
- Extend a Volume Group. Command looks like::
+ Return a matching pv (physical volume) for the current system, requiring
+ ``pv_name``, ``pv_uuid``, or ``pv_tags``. Raises an error if more than one
+ pv is found.
+ """
+ if not any([pv_name, pv_uuid, pv_tags]):
+ return None
+ pvs = PVolumes()
+ return pvs.get(pv_name=pv_name, pv_uuid=pv_uuid, pv_tags=pv_tags)
- vgextend --force --yes group_name [device, ...]
- Once created the volume group is extended and returned as a ``VolumeGroup`` object
+################################
+#
+# Code for LVM Volume Groups
+#
+#############################
- :param vg: A VolumeGroup object
- :param devices: A list of devices to extend the VG. Optionally, a single
- device (as a string) can be used.
+
+def get_api_vgs():
"""
- if not isinstance(devices, list):
- devices = [devices]
- process.run([
- 'vgextend',
- '--force',
- '--yes',
- vg.name] + devices
- )
+ Return the list of group volumes available in the system using flags to
+ include common metadata associated with them
- vg = get_vg(vg_name=vg.name)
- return vg
+ Command and sample delimited output should look like::
+ $ vgs --noheadings --units=g --readonly --separator=';' \
+ -o vg_name,pv_count,lv_count,snap_count,vg_attr,vg_size,vg_free
+ ubuntubox-vg;1;2;0;wz--n-;299.52g;12.00m
+ osd_vg;3;1;0;wz--n-;29.21g;9.21g
-def reduce_vg(vg, devices):
+ To normalize sizing, the units are forced in 'g' which is equivalent to
+ gigabytes, which uses multiples of 1024 (as opposed to 1000)
"""
- Reduce a Volume Group. Command looks like::
+ fields = 'vg_name,pv_count,lv_count,snap_count,vg_attr,vg_size,vg_free,vg_free_count'
+ stdout, stderr, returncode = process.call(
+ ['vgs', '--noheadings', '--readonly', '--units=g', '--separator=";"', '-o', fields],
+ verbose_on_failure=False
+ )
+ return _output_parser(stdout, fields)
- vgreduce --force --yes group_name [device, ...]
- :param vg: A VolumeGroup object
- :param devices: A list of devices to remove from the VG. Optionally, a
- single device (as a string) can be used.
+class VolumeGroup(object):
+ """
+ Represents an LVM group, with some top-level attributes like ``vg_name``
"""
- if not isinstance(devices, list):
- devices = [devices]
- process.run([
- 'vgreduce',
- '--force',
- '--yes',
- vg.name] + devices
- )
- vg = get_vg(vg_name=vg.name)
- return vg
+ def __init__(self, **kw):
+ for k, v in kw.items():
+ setattr(self, k, v)
+ self.name = kw['vg_name']
+ self.tags = parse_tags(kw.get('vg_tags', ''))
+ def __str__(self):
+ return '<%s>' % self.name
-def remove_vg(vg_name):
- """
- Removes a volume group.
- """
- if not vg_name:
- logger.warning('Skipping removal of invalid VG name: "%s"', vg_name)
- return
- fail_msg = "Unable to remove vg %s" % vg_name
- process.run(
- [
- 'vgremove',
- '-v', # verbose
- '-f', # force it
- vg_name
- ],
- fail_msg=fail_msg,
- )
+ def __repr__(self):
+ return self.__str__()
+ def _parse_size(self, size):
+ error_msg = "Unable to convert vg size to integer: '%s'" % str(size)
+ try:
+ integer, _ = size.split('g')
+ except ValueError:
+ logger.exception(error_msg)
+ raise RuntimeError(error_msg)
-def remove_pv(pv_name):
- """
- Removes a physical volume using a double `-f` to prevent prompts and fully
- remove anything related to LVM. This is tremendously destructive, but so is all other actions
- when zapping a device.
+ return util.str_to_int(integer)
- In the case where multiple PVs are found, it will ignore that fact and
- continue with the removal, specifically in the case of messages like::
+ @property
+ def free(self):
+ """
+ Parse the available size in gigabytes from the ``vg_free`` attribute, that
+ will be a string with a character ('g') to indicate gigabytes in size.
+ Returns a rounded down integer to ease internal operations::
- WARNING: PV $UUID /dev/DEV-1 was already found on /dev/DEV-2
+ >>> data_vg.vg_free
+ '0.01g'
+ >>> data_vg.size
+ 0
+ """
+ return self._parse_size(self.vg_free)
- These situations can be avoided with custom filtering rules, which this API
- cannot handle while accommodating custom user filters.
- """
- fail_msg = "Unable to remove vg %s" % pv_name
- process.run(
- [
- 'pvremove',
- '-v', # verbose
- '-f', # force it
- '-f', # force it
- pv_name
- ],
- fail_msg=fail_msg,
- )
+ @property
+ def size(self):
+ """
+ Parse the size in gigabytes from the ``vg_size`` attribute, that
+ will be a string with a character ('g') to indicate gigabytes in size.
+ Returns a rounded down integer to ease internal operations::
+ >>> data_vg.vg_size
+ '1024.9g'
+ >>> data_vg.size
+ 1024
+ """
+ return self._parse_size(self.vg_size)
-def remove_lv(lv):
- """
- Removes a logical volume given it's absolute path.
+ def sizing(self, parts=None, size=None):
+ """
+ Calculate proper sizing to fully utilize the volume group in the most
+ efficient way possible. To prevent situations where LVM might accept
+ a percentage that is beyond the vg's capabilities, it will refuse with
+ an error when requesting a larger-than-possible parameter, in addition
+ to rounding down calculations.
- Will return True if the lv is successfully removed or
- raises a RuntimeError if the removal fails.
+ A dictionary with different sizing parameters is returned, to make it
+ easier for others to choose what they need in order to create logical
+ volumes::
- :param lv: A ``Volume`` object or the path for an LV
- """
- if isinstance(lv, Volume):
- path = lv.lv_path
- else:
- path = lv
+ >>> data_vg.free
+ 1024
+ >>> data_vg.sizing(parts=4)
+ {'parts': 4, 'sizes': 256, 'percentages': 25}
+ >>> data_vg.sizing(size=512)
+ {'parts': 2, 'sizes': 512, 'percentages': 50}
- stdout, stderr, returncode = process.call(
- [
- 'lvremove',
- '-v', # verbose
- '-f', # force it
- path
- ],
- show_command=True,
- terminal_verbose=True,
- )
- if returncode != 0:
- raise RuntimeError("Unable to remove %s" % path)
- return True
+ :param parts: Number of parts to create LVs from
+ :param size: Size in gigabytes to divide the VG into
-def create_lv(name, group, extents=None, size=None, tags=None, uuid_name=False, pv=None):
- """
- Create a Logical Volume in a Volume Group. Command looks like::
+ :raises SizeAllocationError: When requested size cannot be allocated with
+ :raises ValueError: If both ``parts`` and ``size`` are given
+ """
+ if parts is not None and size is not None:
+ raise ValueError(
+ "Cannot process sizing with both parts (%s) and size (%s)" % (parts, size)
+ )
- lvcreate -L 50G -n gfslv vg0
+ # if size is given we need to map that to extents so that we avoid
+ # issues when trying to get this right with a size in gigabytes find
+ # the percentage first, cheating, because these values are thrown out
+ vg_free_count = util.str_to_int(self.vg_free_count)
- ``name``, ``group``, are required. If ``size`` is provided it must follow
- lvm's size notation (like 1G, or 20M). Tags are an optional dictionary and is expected to
- conform to the convention of prefixing them with "ceph." like::
+ if size:
+ extents = int(size * vg_free_count / self.free)
+ disk_sizing = sizing(self.free, size=size, parts=parts)
+ else:
+ if parts is not None:
+ # Prevent parts being 0, falling back to 1 (100% usage)
+ parts = parts or 1
+ size = int(self.free / parts)
+ extents = size * vg_free_count / self.free
+ disk_sizing = sizing(self.free, parts=parts)
- {"ceph.block_device": "/dev/ceph/osd-1"}
+ extent_sizing = sizing(vg_free_count, size=extents)
- :param uuid_name: Optionally combine the ``name`` with UUID to ensure uniqueness
- """
- if uuid_name:
- name = '%s-%s' % (name, uuid.uuid4())
- if tags is None:
- tags = {
- "ceph.osd_id": "null",
- "ceph.type": "null",
- "ceph.cluster_fsid": "null",
- "ceph.osd_fsid": "null",
- }
-
- # XXX add CEPH_VOLUME_LVM_DEBUG to enable -vvvv on lv operations
- type_path_tag = {
- 'journal': 'ceph.journal_device',
- 'data': 'ceph.data_device',
- 'block': 'ceph.block_device',
- 'wal': 'ceph.wal_device',
- 'db': 'ceph.db_device',
- 'lockbox': 'ceph.lockbox_device', # XXX might not ever need this lockbox sorcery
- }
- if size:
- command = [
- 'lvcreate',
- '--yes',
- '-L',
- '%s' % size,
- '-n', name, group
- ]
- elif extents:
- command = [
- 'lvcreate',
- '--yes',
- '-l',
- '%s' % extents,
- '-n', name, group
- ]
- # create the lv with all the space available, this is needed because the
- # system call is different for LVM
- else:
- command = [
- 'lvcreate',
- '--yes',
- '-l',
- '100%FREE',
- '-n', name, group
- ]
- if pv:
- command.append(pv)
- process.run(command)
-
- lv = get_lv(lv_name=name, vg_name=group)
- lv.set_tags(tags)
-
- # when creating a distinct type, the caller doesn't know what the path will
- # be so this function will set it after creation using the mapping
- path_tag = type_path_tag.get(tags.get('ceph.type'))
- if path_tag:
- lv.set_tags(
- {path_tag: lv.lv_path}
- )
- return lv
-
-
-def create_lvs(volume_group, parts=None, size=None, name_prefix='ceph-lv'):
- """
- Create multiple Logical Volumes from a Volume Group by calculating the
- proper extents from ``parts`` or ``size``. A custom prefix can be used
- (defaults to ``ceph-lv``), these names are always suffixed with a uuid.
-
- LV creation in ceph-volume will require tags, this is expected to be
- pre-computed by callers who know Ceph metadata like OSD IDs and FSIDs. It
- will probably not be the case when mass-creating LVs, so common/default
- tags will be set to ``"null"``.
-
- .. note:: LVs that are not in use can be detected by querying LVM for tags that are
- set to ``"null"``.
-
- :param volume_group: The volume group (vg) to use for LV creation
- :type group: ``VolumeGroup()`` object
- :param parts: Number of LVs to create *instead of* ``size``.
- :type parts: int
- :param size: Size (in gigabytes) of LVs to create, e.g. "as many 10gb LVs as possible"
- :type size: int
- :param extents: The number of LVM extents to use to create the LV. Useful if looking to have
- accurate LV sizes (LVM rounds sizes otherwise)
- """
- if parts is None and size is None:
- # fallback to just one part (using 100% of the vg)
- parts = 1
- lvs = []
- tags = {
- "ceph.osd_id": "null",
- "ceph.type": "null",
- "ceph.cluster_fsid": "null",
- "ceph.osd_fsid": "null",
- }
- sizing = volume_group.sizing(parts=parts, size=size)
- for part in range(0, sizing['parts']):
- size = sizing['sizes']
- extents = sizing['extents']
- lv_name = '%s-%s' % (name_prefix, uuid.uuid4())
- lvs.append(
- create_lv(lv_name, volume_group.name, extents=extents, tags=tags)
- )
- return lvs
-
-
-def get_vg(vg_name=None, vg_tags=None):
- """
- Return a matching vg for the current system, requires ``vg_name`` or
- ``tags``. Raises an error if more than one vg is found.
-
- It is useful to use ``tags`` when trying to find a specific volume group,
- but it can also lead to multiple vgs being found.
- """
- if not any([vg_name, vg_tags]):
- return None
- vgs = VolumeGroups()
- return vgs.get(vg_name=vg_name, vg_tags=vg_tags)
-
-
-class VolumeGroups(list):
+ disk_sizing['extents'] = int(extents)
+ disk_sizing['percentages'] = extent_sizing['percentages']
+ return disk_sizing
+
+
+class VolumeGroups(list):
"""
A list of all known volume groups for the current system, with the ability
to filter them via keyword arguments.
return vgs[0]
-class Volumes(list):
- """
- A list of all known (logical) volumes for the current system, with the ability
- to filter them via keyword arguments.
+def create_vg(devices, name=None, name_prefix=None):
"""
+ Create a Volume Group. Command looks like::
- def __init__(self):
- self._populate()
+ vgcreate --force --yes group_name device
- def _populate(self):
- # get all the lvs in the current system
- for lv_item in get_api_lvs():
- self.append(Volume(**lv_item))
+ Once created the volume group is returned as a ``VolumeGroup`` object
- def _purge(self):
- """
- Delete all the items in the list, used internally only so that we can
- dynamically allocate the items when filtering without the concern of
- messing up the contents
- """
- self[:] = []
+ :param devices: A list of devices to create a VG. Optionally, a single
+ device (as a string) can be used.
+ :param name: Optionally set the name of the VG, defaults to 'ceph-{uuid}'
+ :param name_prefix: Optionally prefix the name of the VG, which will get combined
+ with a UUID string
+ """
+ if isinstance(devices, set):
+ devices = list(devices)
+ if not isinstance(devices, list):
+ devices = [devices]
+ if name_prefix:
+ name = "%s-%s" % (name_prefix, str(uuid.uuid4()))
+ elif name is None:
+ name = "ceph-%s" % str(uuid.uuid4())
+ process.run([
+ 'vgcreate',
+ '-s',
+ '1G',
+ '--force',
+ '--yes',
+ name] + devices
+ )
- def _filter(self, lv_name=None, vg_name=None, lv_path=None, lv_uuid=None, lv_tags=None):
- """
- The actual method that filters using a new list. Useful so that other
- methods that do not want to alter the contents of the list (e.g.
- ``self.find``) can operate safely.
- """
- filtered = [i for i in self]
- if lv_name:
- filtered = [i for i in filtered if i.lv_name == lv_name]
+ vg = get_vg(vg_name=name)
+ return vg
- if vg_name:
- filtered = [i for i in filtered if i.vg_name == vg_name]
- if lv_uuid:
- filtered = [i for i in filtered if i.lv_uuid == lv_uuid]
+def extend_vg(vg, devices):
+ """
+ Extend a Volume Group. Command looks like::
- if lv_path:
- filtered = [i for i in filtered if i.lv_path == lv_path]
+ vgextend --force --yes group_name [device, ...]
- # at this point, `filtered` has either all the volumes in self or is an
- # actual filtered list if any filters were applied
- if lv_tags:
- tag_filtered = []
- for volume in filtered:
- # all the tags we got need to match on the volume
- matches = all(volume.tags.get(k) == str(v) for k, v in lv_tags.items())
- if matches:
- tag_filtered.append(volume)
- return tag_filtered
+ Once created the volume group is extended and returned as a ``VolumeGroup`` object
- return filtered
+ :param vg: A VolumeGroup object
+ :param devices: A list of devices to extend the VG. Optionally, a single
+ device (as a string) can be used.
+ """
+ if not isinstance(devices, list):
+ devices = [devices]
+ process.run([
+ 'vgextend',
+ '--force',
+ '--yes',
+ vg.name] + devices
+ )
- def filter(self, lv_name=None, vg_name=None, lv_path=None, lv_uuid=None, lv_tags=None):
- """
- Filter out volumes on top level attributes like ``lv_name`` or by
- ``lv_tags`` where a dict is required. For example, to find a volume
- that has an OSD ID of 0, the filter would look like::
+ vg = get_vg(vg_name=vg.name)
+ return vg
- lv_tags={'ceph.osd_id': '0'}
- """
- if not any([lv_name, vg_name, lv_path, lv_uuid, lv_tags]):
- raise TypeError('.filter() requires lv_name, vg_name, lv_path, lv_uuid, or tags (none given)')
- # first find the filtered volumes with the values in self
- filtered_volumes = self._filter(
- lv_name=lv_name,
- vg_name=vg_name,
- lv_path=lv_path,
- lv_uuid=lv_uuid,
- lv_tags=lv_tags
- )
- # then purge everything
- self._purge()
- # and add the filtered items
- self.extend(filtered_volumes)
+def reduce_vg(vg, devices):
+ """
+ Reduce a Volume Group. Command looks like::
- def get(self, lv_name=None, vg_name=None, lv_path=None, lv_uuid=None, lv_tags=None):
- """
- This is a bit expensive, since it will try to filter out all the
- matching items in the list, filter them out applying anything that was
- added and return the matching item.
+ vgreduce --force --yes group_name [device, ...]
- This method does *not* alter the list, and it will raise an error if
- multiple LVs are matched
+ :param vg: A VolumeGroup object
+ :param devices: A list of devices to remove from the VG. Optionally, a
+ single device (as a string) can be used.
+ """
+ if not isinstance(devices, list):
+ devices = [devices]
+ process.run([
+ 'vgreduce',
+ '--force',
+ '--yes',
+ vg.name] + devices
+ )
- It is useful to use ``tags`` when trying to find a specific logical volume,
- but it can also lead to multiple lvs being found, since a lot of metadata
- is shared between lvs of a distinct OSD.
- """
- if not any([lv_name, vg_name, lv_path, lv_uuid, lv_tags]):
- return None
- lvs = self._filter(
- lv_name=lv_name,
- vg_name=vg_name,
- lv_path=lv_path,
- lv_uuid=lv_uuid,
- lv_tags=lv_tags
- )
- if not lvs:
- return None
- if len(lvs) > 1:
- raise MultipleLVsError(lv_name, lv_path)
- return lvs[0]
+ vg = get_vg(vg_name=vg.name)
+ return vg
-class PVolumes(list):
+def remove_vg(vg_name):
"""
- A list of all known (physical) volumes for the current system, with the ability
- to filter them via keyword arguments.
+ Removes a volume group.
"""
-
- def __init__(self, populate=True):
- if populate:
- self._populate()
-
- def _populate(self):
- # get all the pvs in the current system
- for pv_item in get_api_pvs():
- self.append(PVolume(**pv_item))
-
- def _purge(self):
- """
- Deplete all the items in the list, used internally only so that we can
- dynamically allocate the items when filtering without the concern of
- messing up the contents
- """
- self[:] = []
-
- def _filter(self, pv_name=None, pv_uuid=None, pv_tags=None):
- """
- The actual method that filters using a new list. Useful so that other
- methods that do not want to alter the contents of the list (e.g.
- ``self.find``) can operate safely.
- """
- filtered = [i for i in self]
- if pv_name:
- filtered = [i for i in filtered if i.pv_name == pv_name]
-
- if pv_uuid:
- filtered = [i for i in filtered if i.pv_uuid == pv_uuid]
-
- # at this point, `filtered` has either all the physical volumes in self
- # or is an actual filtered list if any filters were applied
- if pv_tags:
- tag_filtered = []
- for pvolume in filtered:
- matches = all(pvolume.tags.get(k) == str(v) for k, v in pv_tags.items())
- if matches:
- tag_filtered.append(pvolume)
- # return the tag_filtered pvolumes here, the `filtered` list is no
- # longer usable
- return tag_filtered
-
- return filtered
-
- def filter(self, pv_name=None, pv_uuid=None, pv_tags=None):
- """
- Filter out volumes on top level attributes like ``pv_name`` or by
- ``pv_tags`` where a dict is required. For example, to find a physical
- volume that has an OSD ID of 0, the filter would look like::
-
- pv_tags={'ceph.osd_id': '0'}
-
- """
- if not any([pv_name, pv_uuid, pv_tags]):
- raise TypeError('.filter() requires pv_name, pv_uuid, or pv_tags'
- '(none given)')
-
- filtered_pvs = PVolumes(populate=False)
- filtered_pvs.extend(self._filter(pv_name, pv_uuid, pv_tags))
- return filtered_pvs
-
- def get(self, pv_name=None, pv_uuid=None, pv_tags=None):
- """
- This is a bit expensive, since it will try to filter out all the
- matching items in the list, filter them out applying anything that was
- added and return the matching item.
-
- This method does *not* alter the list, and it will raise an error if
- multiple pvs are matched
-
- It is useful to use ``tags`` when trying to find a specific logical volume,
- but it can also lead to multiple pvs being found, since a lot of metadata
- is shared between pvs of a distinct OSD.
- """
- if not any([pv_name, pv_uuid, pv_tags]):
- return None
- pvs = self._filter(
- pv_name=pv_name,
- pv_uuid=pv_uuid,
- pv_tags=pv_tags
- )
- if not pvs:
- return None
- if len(pvs) > 1 and pv_tags:
- raise MultiplePVsError(pv_name)
- return pvs[0]
+ if not vg_name:
+ logger.warning('Skipping removal of invalid VG name: "%s"', vg_name)
+ return
+ fail_msg = "Unable to remove vg %s" % vg_name
+ process.run(
+ [
+ 'vgremove',
+ '-v', # verbose
+ '-f', # force it
+ vg_name
+ ],
+ fail_msg=fail_msg,
+ )
-class VolumeGroup(object):
- """
- Represents an LVM group, with some top-level attributes like ``vg_name``
+def get_vg(vg_name=None, vg_tags=None):
"""
+ Return a matching vg for the current system, requires ``vg_name`` or
+ ``tags``. Raises an error if more than one vg is found.
- def __init__(self, **kw):
- for k, v in kw.items():
- setattr(self, k, v)
- self.name = kw['vg_name']
- self.tags = parse_tags(kw.get('vg_tags', ''))
-
- def __str__(self):
- return '<%s>' % self.name
-
- def __repr__(self):
- return self.__str__()
-
- def _parse_size(self, size):
- error_msg = "Unable to convert vg size to integer: '%s'" % str(size)
- try:
- integer, _ = size.split('g')
- except ValueError:
- logger.exception(error_msg)
- raise RuntimeError(error_msg)
-
- return util.str_to_int(integer)
-
- @property
- def free(self):
- """
- Parse the available size in gigabytes from the ``vg_free`` attribute, that
- will be a string with a character ('g') to indicate gigabytes in size.
- Returns a rounded down integer to ease internal operations::
-
- >>> data_vg.vg_free
- '0.01g'
- >>> data_vg.size
- 0
- """
- return self._parse_size(self.vg_free)
-
- @property
- def size(self):
- """
- Parse the size in gigabytes from the ``vg_size`` attribute, that
- will be a string with a character ('g') to indicate gigabytes in size.
- Returns a rounded down integer to ease internal operations::
-
- >>> data_vg.vg_size
- '1024.9g'
- >>> data_vg.size
- 1024
- """
- return self._parse_size(self.vg_size)
-
- def sizing(self, parts=None, size=None):
- """
- Calculate proper sizing to fully utilize the volume group in the most
- efficient way possible. To prevent situations where LVM might accept
- a percentage that is beyond the vg's capabilities, it will refuse with
- an error when requesting a larger-than-possible parameter, in addition
- to rounding down calculations.
-
- A dictionary with different sizing parameters is returned, to make it
- easier for others to choose what they need in order to create logical
- volumes::
-
- >>> data_vg.free
- 1024
- >>> data_vg.sizing(parts=4)
- {'parts': 4, 'sizes': 256, 'percentages': 25}
- >>> data_vg.sizing(size=512)
- {'parts': 2, 'sizes': 512, 'percentages': 50}
+ It is useful to use ``tags`` when trying to find a specific volume group,
+ but it can also lead to multiple vgs being found.
+ """
+ if not any([vg_name, vg_tags]):
+ return None
+ vgs = VolumeGroups()
+ return vgs.get(vg_name=vg_name, vg_tags=vg_tags)
- :param parts: Number of parts to create LVs from
- :param size: Size in gigabytes to divide the VG into
+#################################
+#
+# Code for LVM Logical Volumes
+#
+###############################
- :raises SizeAllocationError: When requested size cannot be allocated with
- :raises ValueError: If both ``parts`` and ``size`` are given
- """
- if parts is not None and size is not None:
- raise ValueError(
- "Cannot process sizing with both parts (%s) and size (%s)" % (parts, size)
- )
- # if size is given we need to map that to extents so that we avoid
- # issues when trying to get this right with a size in gigabytes find
- # the percentage first, cheating, because these values are thrown out
- vg_free_count = util.str_to_int(self.vg_free_count)
+def get_api_lvs():
+ """
+ Return the list of logical volumes available in the system using flags to include common
+ metadata associated with them
- if size:
- extents = int(size * vg_free_count / self.free)
- disk_sizing = sizing(self.free, size=size, parts=parts)
- else:
- if parts is not None:
- # Prevent parts being 0, falling back to 1 (100% usage)
- parts = parts or 1
- size = int(self.free / parts)
- extents = size * vg_free_count / self.free
- disk_sizing = sizing(self.free, parts=parts)
+ Command and delimited output should look like::
- extent_sizing = sizing(vg_free_count, size=extents)
+ $ lvs --noheadings --readonly --separator=';' -a -o lv_tags,lv_path,lv_name,vg_name
+ ;/dev/ubuntubox-vg/root;root;ubuntubox-vg
+ ;/dev/ubuntubox-vg/swap_1;swap_1;ubuntubox-vg
- disk_sizing['extents'] = int(extents)
- disk_sizing['percentages'] = extent_sizing['percentages']
- return disk_sizing
+ """
+ fields = 'lv_tags,lv_path,lv_name,vg_name,lv_uuid,lv_size'
+ stdout, stderr, returncode = process.call(
+ ['lvs', '--noheadings', '--readonly', '--separator=";"', '-a', '-o', fields],
+ verbose_on_failure=False
+ )
+ return _output_parser(stdout, fields)
class Volume(object):
self.tags[key] = value
-class PVolume(object):
+class Volumes(list):
"""
- Represents a Physical Volume from LVM, with some top-level attributes like
- ``pv_name`` and parsed tags as a dictionary of key/value pairs.
+ A list of all known (logical) volumes for the current system, with the ability
+ to filter them via keyword arguments.
"""
- def __init__(self, **kw):
- for k, v in kw.items():
- setattr(self, k, v)
- self.pv_api = kw
- self.name = kw['pv_name']
- self.tags = parse_tags(kw['pv_tags'])
-
- def __str__(self):
- return '<%s>' % self.pv_api['pv_name']
+ def __init__(self):
+ self._populate()
- def __repr__(self):
- return self.__str__()
+ def _populate(self):
+ # get all the lvs in the current system
+ for lv_item in get_api_lvs():
+ self.append(Volume(**lv_item))
- def set_tags(self, tags):
+ def _purge(self):
"""
- :param tags: A dictionary of tag names and values, like::
+ Delete all the items in the list, used internally only so that we can
+ dynamically allocate the items when filtering without the concern of
+ messing up the contents
+ """
+ self[:] = []
- {
- "ceph.osd_fsid": "aaa-fff-bbbb",
- "ceph.osd_id": "0"
- }
+ def _filter(self, lv_name=None, vg_name=None, lv_path=None, lv_uuid=None, lv_tags=None):
+ """
+ The actual method that filters using a new list. Useful so that other
+ methods that do not want to alter the contents of the list (e.g.
+ ``self.find``) can operate safely.
+ """
+ filtered = [i for i in self]
+ if lv_name:
+ filtered = [i for i in filtered if i.lv_name == lv_name]
- At the end of all modifications, the tags are refreshed to reflect
- LVM's most current view.
+ if vg_name:
+ filtered = [i for i in filtered if i.vg_name == vg_name]
+
+ if lv_uuid:
+ filtered = [i for i in filtered if i.lv_uuid == lv_uuid]
+
+ if lv_path:
+ filtered = [i for i in filtered if i.lv_path == lv_path]
+
+ # at this point, `filtered` has either all the volumes in self or is an
+ # actual filtered list if any filters were applied
+ if lv_tags:
+ tag_filtered = []
+ for volume in filtered:
+ # all the tags we got need to match on the volume
+ matches = all(volume.tags.get(k) == str(v) for k, v in lv_tags.items())
+ if matches:
+ tag_filtered.append(volume)
+ return tag_filtered
+
+ return filtered
+
+ def filter(self, lv_name=None, vg_name=None, lv_path=None, lv_uuid=None, lv_tags=None):
"""
- for k, v in tags.items():
- self.set_tag(k, v)
- # after setting all the tags, refresh them for the current object, use the
- # pv_* identifiers to filter because those shouldn't change
- pv_object = get_pv(pv_name=self.pv_name, pv_uuid=self.pv_uuid)
- self.tags = pv_object.tags
+ Filter out volumes on top level attributes like ``lv_name`` or by
+ ``lv_tags`` where a dict is required. For example, to find a volume
+ that has an OSD ID of 0, the filter would look like::
+
+ lv_tags={'ceph.osd_id': '0'}
- def set_tag(self, key, value):
"""
- Set the key/value pair as an LVM tag. Does not "refresh" the values of
- the current object for its tags. Meant to be a "fire and forget" type
- of modification.
+ if not any([lv_name, vg_name, lv_path, lv_uuid, lv_tags]):
+ raise TypeError('.filter() requires lv_name, vg_name, lv_path, lv_uuid, or tags (none given)')
+ # first find the filtered volumes with the values in self
+ filtered_volumes = self._filter(
+ lv_name=lv_name,
+ vg_name=vg_name,
+ lv_path=lv_path,
+ lv_uuid=lv_uuid,
+ lv_tags=lv_tags
+ )
+ # then purge everything
+ self._purge()
+ # and add the filtered items
+ self.extend(filtered_volumes)
- **warning**: Altering tags on a PV has to be done ensuring that the
- device is actually the one intended. ``pv_name`` is *not* a persistent
- value, only ``pv_uuid`` is. Using ``pv_uuid`` is the best way to make
- sure the device getting changed is the one needed.
+ def get(self, lv_name=None, vg_name=None, lv_path=None, lv_uuid=None, lv_tags=None):
"""
- # remove it first if it exists
- if self.tags.get(key):
- current_value = self.tags[key]
- tag = "%s=%s" % (key, current_value)
- process.call(['pvchange', '--deltag', tag, self.pv_name])
+ This is a bit expensive, since it will try to filter out all the
+ matching items in the list, filter them out applying anything that was
+ added and return the matching item.
- process.call(
- [
- 'pvchange',
- '--addtag', '%s=%s' % (key, value), self.pv_name
- ]
+ This method does *not* alter the list, and it will raise an error if
+ multiple LVs are matched
+
+ It is useful to use ``tags`` when trying to find a specific logical volume,
+ but it can also lead to multiple lvs being found, since a lot of metadata
+ is shared between lvs of a distinct OSD.
+ """
+ if not any([lv_name, vg_name, lv_path, lv_uuid, lv_tags]):
+ return None
+ lvs = self._filter(
+ lv_name=lv_name,
+ vg_name=vg_name,
+ lv_path=lv_path,
+ lv_uuid=lv_uuid,
+ lv_tags=lv_tags
+ )
+ if not lvs:
+ return None
+ if len(lvs) > 1:
+ raise MultipleLVsError(lv_name, lv_path)
+ return lvs[0]
+
+
+def create_lv(name, group, extents=None, size=None, tags=None, uuid_name=False, pv=None):
+ """
+ Create a Logical Volume in a Volume Group. Command looks like::
+
+ lvcreate -L 50G -n gfslv vg0
+
+ ``name``, ``group``, are required. If ``size`` is provided it must follow
+ lvm's size notation (like 1G, or 20M). Tags are an optional dictionary and is expected to
+ conform to the convention of prefixing them with "ceph." like::
+
+ {"ceph.block_device": "/dev/ceph/osd-1"}
+
+ :param uuid_name: Optionally combine the ``name`` with UUID to ensure uniqueness
+ """
+ if uuid_name:
+ name = '%s-%s' % (name, uuid.uuid4())
+ if tags is None:
+ tags = {
+ "ceph.osd_id": "null",
+ "ceph.type": "null",
+ "ceph.cluster_fsid": "null",
+ "ceph.osd_fsid": "null",
+ }
+
+ # XXX add CEPH_VOLUME_LVM_DEBUG to enable -vvvv on lv operations
+ type_path_tag = {
+ 'journal': 'ceph.journal_device',
+ 'data': 'ceph.data_device',
+ 'block': 'ceph.block_device',
+ 'wal': 'ceph.wal_device',
+ 'db': 'ceph.db_device',
+ 'lockbox': 'ceph.lockbox_device', # XXX might not ever need this lockbox sorcery
+ }
+ if size:
+ command = [
+ 'lvcreate',
+ '--yes',
+ '-L',
+ '%s' % size,
+ '-n', name, group
+ ]
+ elif extents:
+ command = [
+ 'lvcreate',
+ '--yes',
+ '-l',
+ '%s' % extents,
+ '-n', name, group
+ ]
+ # create the lv with all the space available, this is needed because the
+ # system call is different for LVM
+ else:
+ command = [
+ 'lvcreate',
+ '--yes',
+ '-l',
+ '100%FREE',
+ '-n', name, group
+ ]
+ if pv:
+ command.append(pv)
+ process.run(command)
+
+ lv = get_lv(lv_name=name, vg_name=group)
+ lv.set_tags(tags)
+
+ # when creating a distinct type, the caller doesn't know what the path will
+ # be so this function will set it after creation using the mapping
+ path_tag = type_path_tag.get(tags.get('ceph.type'))
+ if path_tag:
+ lv.set_tags(
+ {path_tag: lv.lv_path}
+ )
+ return lv
+
+
+def remove_lv(lv):
+ """
+ Removes a logical volume given it's absolute path.
+
+ Will return True if the lv is successfully removed or
+ raises a RuntimeError if the removal fails.
+
+ :param lv: A ``Volume`` object or the path for an LV
+ """
+ if isinstance(lv, Volume):
+ path = lv.lv_path
+ else:
+ path = lv
+
+ stdout, stderr, returncode = process.call(
+ [
+ 'lvremove',
+ '-v', # verbose
+ '-f', # force it
+ path
+ ],
+ show_command=True,
+ terminal_verbose=True,
+ )
+ if returncode != 0:
+ raise RuntimeError("Unable to remove %s" % path)
+ return True
+
+
+def is_lv(dev, lvs=None):
+ """
+ Boolean to detect if a device is an LV or not.
+ """
+ splitname = dmsetup_splitname(dev)
+ # Allowing to optionally pass `lvs` can help reduce repetitive checks for
+ # multiple devices at once.
+ if lvs is None or len(lvs) == 0:
+ lvs = Volumes()
+
+ if splitname.get('LV_NAME'):
+ lvs.filter(lv_name=splitname['LV_NAME'], vg_name=splitname['VG_NAME'])
+ return len(lvs) > 0
+ return False
+
+
+def get_lv(lv_name=None, vg_name=None, lv_path=None, lv_uuid=None, lv_tags=None, lvs=None):
+ """
+ Return a matching lv for the current system, requiring ``lv_name``,
+ ``vg_name``, ``lv_path`` or ``tags``. Raises an error if more than one lv
+ is found.
+
+ It is useful to use ``tags`` when trying to find a specific logical volume,
+ but it can also lead to multiple lvs being found, since a lot of metadata
+ is shared between lvs of a distinct OSD.
+ """
+ if not any([lv_name, vg_name, lv_path, lv_uuid, lv_tags]):
+ return None
+ if lvs is None:
+ lvs = Volumes()
+ return lvs.get(
+ lv_name=lv_name, vg_name=vg_name, lv_path=lv_path, lv_uuid=lv_uuid,
+ lv_tags=lv_tags
+ )
+
+
+def get_lv_from_argument(argument):
+ """
+ Helper proxy function that consumes a possible logical volume passed in from the CLI
+ in the form of `vg/lv`, but with some validation so that an argument that is a full
+ path to a device can be ignored
+ """
+ if argument.startswith('/'):
+ lv = get_lv(lv_path=argument)
+ return lv
+ try:
+ vg_name, lv_name = argument.split('/')
+ except (ValueError, AttributeError):
+ return None
+ return get_lv(lv_name=lv_name, vg_name=vg_name)
+
+
+def create_lvs(volume_group, parts=None, size=None, name_prefix='ceph-lv'):
+ """
+ Create multiple Logical Volumes from a Volume Group by calculating the
+ proper extents from ``parts`` or ``size``. A custom prefix can be used
+ (defaults to ``ceph-lv``), these names are always suffixed with a uuid.
+
+ LV creation in ceph-volume will require tags, this is expected to be
+ pre-computed by callers who know Ceph metadata like OSD IDs and FSIDs. It
+ will probably not be the case when mass-creating LVs, so common/default
+ tags will be set to ``"null"``.
+
+ .. note:: LVs that are not in use can be detected by querying LVM for tags that are
+ set to ``"null"``.
+
+ :param volume_group: The volume group (vg) to use for LV creation
+ :type group: ``VolumeGroup()`` object
+ :param parts: Number of LVs to create *instead of* ``size``.
+ :type parts: int
+ :param size: Size (in gigabytes) of LVs to create, e.g. "as many 10gb LVs as possible"
+ :type size: int
+ :param extents: The number of LVM extents to use to create the LV. Useful if looking to have
+ accurate LV sizes (LVM rounds sizes otherwise)
+ """
+ if parts is None and size is None:
+ # fallback to just one part (using 100% of the vg)
+ parts = 1
+ lvs = []
+ tags = {
+ "ceph.osd_id": "null",
+ "ceph.type": "null",
+ "ceph.cluster_fsid": "null",
+ "ceph.osd_fsid": "null",
+ }
+ sizing = volume_group.sizing(parts=parts, size=size)
+ for part in range(0, sizing['parts']):
+ size = sizing['sizes']
+ extents = sizing['extents']
+ lv_name = '%s-%s' % (name_prefix, uuid.uuid4())
+ lvs.append(
+ create_lv(lv_name, volume_group.name, extents=extents, tags=tags)
)
+ return lvs