that prefixes tags with ``ceph.`` and uses ``=`` for assignment, and provides
set of utilities for interacting with LVM.
"""
-from ceph_volume import process
-from ceph_volume.exceptions import MultipleLVsError, MultipleVGsError, MultiplePVsError
+import logging
+import os
+import uuid
+from math import floor
+from ceph_volume import process, util
+from ceph_volume.exceptions import (
+ MultipleLVsError, MultipleVGsError,
+ MultiplePVsError, SizeAllocationError
+)
+
+logger = logging.getLogger(__name__)
def _output_parser(output, fields):
return report
+def _splitname_parser(line):
+ """
+ Parses the output from ``dmsetup splitname``, that should contain prefixes
+ (--nameprefixes) and set the separator to ";"
+
+ Output for /dev/mapper/vg-lv will usually look like::
+
+ DM_VG_NAME='/dev/mapper/vg';DM_LV_NAME='lv';DM_LV_LAYER=''
+
+
+ The ``VG_NAME`` will usually not be what other callers need (e.g. just 'vg'
+ in the example), so this utility will split ``/dev/mapper/`` out, so that
+ the actual volume group name is kept
+
+ :returns: dictionary with stripped prefixes
+ """
+ parts = line[0].split(';')
+ parsed = {}
+ for part in parts:
+ part = part.replace("'", '')
+ key, value = part.split('=')
+ if 'DM_VG_NAME' in key:
+ value = value.split('/dev/mapper/')[-1]
+ key = key.split('DM_')[-1]
+ parsed[key] = value
+
+ return parsed
+
+
+def sizing(device_size, 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::
+
+ >>> sizing(100, parts=2)
+ >>> {'parts': 2, 'percentages': 50, 'sizes': 50}
+
+ """
+ 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 and size > device_size:
+ raise SizeAllocationError(size, device_size)
+
+ def get_percentage(parts):
+ return int(floor(100 / float(parts)))
+
+ if parts is not None:
+ # Prevent parts being 0, falling back to 1 (100% usage)
+ parts = parts or 1
+ percentages = get_percentage(parts)
+
+ if size:
+ parts = int(device_size / size) or 1
+ percentages = get_percentage(parts)
+
+ sizes = device_size / parts if parts else int(floor(device_size))
+
+ return {
+ 'parts': parts,
+ 'percentages': percentages,
+ 'sizes': int(sizes),
+ }
+
+
def parse_tags(lv_tags):
"""
Return a dictionary mapping of all the tags associated with
return tag_mapping
+def _vdo_parents(devices):
+ """
+ It is possible we didn't get a logical volume, or a mapper path, but
+ a device like /dev/sda2, to resolve this, we must look at all the slaves of
+ every single device in /sys/block and if any of those devices is related to
+ VDO devices, then we can add the parent
+ """
+ parent_devices = []
+ for parent in os.listdir('/sys/block'):
+ for slave in os.listdir('/sys/block/%s/slaves' % parent):
+ if slave in devices:
+ parent_devices.append('/dev/%s' % parent)
+ parent_devices.append(parent)
+ return parent_devices
+
+
+def _vdo_slaves(vdo_names):
+ """
+ find all the slaves associated with each vdo name (from realpath) by going
+ into /sys/block/<realpath>/slaves
+ """
+ devices = []
+ for vdo_name in vdo_names:
+ mapper_path = '/dev/mapper/%s' % vdo_name
+ if not os.path.exists(mapper_path):
+ continue
+ # resolve the realpath and realname of the vdo mapper
+ vdo_realpath = os.path.realpath(mapper_path)
+ vdo_realname = vdo_realpath.split('/')[-1]
+ slaves_path = '/sys/block/%s/slaves' % vdo_realname
+ if not os.path.exists(slaves_path):
+ continue
+ devices.append(vdo_realpath)
+ devices.append(mapper_path)
+ devices.append(vdo_realname)
+ for slave in os.listdir(slaves_path):
+ devices.append('/dev/%s' % slave)
+ devices.append(slave)
+ return devices
+
+
+def _is_vdo(path):
+ """
+ A VDO device can be composed from many different devices, go through each
+ one of those devices and its slaves (if any) and correlate them back to
+ /dev/mapper and their realpaths, and then check if they appear as part of
+ /sys/kvdo/<name>/statistics
+
+ From the realpath of a logical volume, determine if it is a VDO device or
+ not, by correlating it to the presence of the name in
+ /sys/kvdo/<name>/statistics and all the previously captured devices
+ """
+ if not os.path.isdir('/sys/kvdo'):
+ return False
+ realpath = os.path.realpath(path)
+ realpath_name = realpath.split('/')[-1]
+ devices = []
+ vdo_names = set()
+ # get all the vdo names
+ for dirname in os.listdir('/sys/kvdo/'):
+ if os.path.isdir('/sys/kvdo/%s/statistics' % dirname):
+ vdo_names.add(dirname)
+
+ # find all the slaves associated with each vdo name (from realpath) by
+ # going into /sys/block/<realpath>/slaves
+ devices.extend(_vdo_slaves(vdo_names))
+
+ # Find all possible parents, looking into slaves that are related to VDO
+ devices.extend(_vdo_parents(devices))
+
+ return any([
+ path in devices,
+ realpath in devices,
+ realpath_name in devices])
+
+
+def is_vdo(path):
+ """
+ Detect if a path is backed by VDO, proxying the actual call to _is_vdo so
+ that we can prevent an exception breaking OSD creation. If an exception is
+ raised, it will get captured and logged to file, while returning
+ a ``False``.
+ """
+ try:
+ if _is_vdo(path):
+ return '1'
+ return '0'
+ except Exception:
+ logger.exception('Unable to properly detect device as VDO: %s', path)
+ return '0'
+
+
+def dmsetup_splitname(dev):
+ """
+ Run ``dmsetup splitname`` and parse the results.
+
+ .. warning:: This call does not ensure that the device is correct or that
+ it exists. ``dmsetup`` will happily take a non existing path and still
+ return a 0 exit status.
+ """
+ command = [
+ 'dmsetup', 'splitname', '--noheadings',
+ "--separator=';'", '--nameprefixes', dev
+ ]
+ out, err, rc = process.call(command)
+ 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.
+ lvs = lvs if lvs is not None else 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 delimeted output, should look like::
+ Command and sample delimited output should look like::
- $ vgs --noheadings --separator=';' \
+ $ 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'
+ 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', '--separator=";"', '-o', fields]
+ ['vgs', '--noheadings', '--readonly', '--units=g', '--separator=";"', '-o', fields]
)
return _output_parser(stdout, fields)
Return the list of logical volumes available in the system using flags to include common
metadata associated with them
- Command and delimeted output, should look like::
+ Command and delimited output should look like::
- $ lvs --noheadings --separator=';' -o lv_tags,lv_path,lv_name,vg_name
+ $ lvs --noheadings --readonly --separator=';' -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'
+ fields = 'lv_tags,lv_path,lv_name,vg_name,lv_uuid,lv_size'
stdout, stderr, returncode = process.call(
- ['lvs', '--noheadings', '--separator=";"', '-o', fields]
+ ['lvs', '--noheadings', '--readonly', '--separator=";"', '-o', fields]
)
return _output_parser(stdout, fields)
This will only return physical volumes set up to work with LVM.
- Command and delimeted output, should look like::
+ Command and delimited output should look like::
- $ pvs --noheadings --separator=';' -o pv_name,pv_tags,pv_uuid
+ $ pvs --noheadings --readonly --separator=';' -o pv_name,pv_tags,pv_uuid
/dev/sda1;;
/dev/sdv;;07A4F654-4162-4600-8EB3-88D1E42F368D
"""
- fields = 'pv_name,pv_tags,pv_uuid,vg_name'
+ fields = 'pv_name,pv_tags,pv_uuid,vg_name,lv_uuid'
stdout, stderr, returncode = process.call(
- ['pvs', '--no-heading', '--separator=";"', '-o', fields]
+ ['pvs', '--no-heading', '--readonly', '--separator=";"', '-o', fields]
)
return _output_parser(stdout, fields)
])
-def create_vg(name, *devices):
+def create_vg(devices, name=None, name_prefix=None):
"""
Create a Volume Group. Command looks like::
vgcreate --force --yes group_name device
Once created the volume group is returned as a ``VolumeGroup`` object
+
+ :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',
'--force',
'--yes',
- name] + list(devices)
+ name] + devices
)
vg = get_vg(vg_name=name)
return vg
+def extend_vg(vg, devices):
+ """
+ Extend a Volume Group. Command looks like::
+
+ vgextend --force --yes group_name [device, ...]
+
+ Once created the volume group is extended and returned as a ``VolumeGroup`` object
+
+ :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
+ )
+
+ vg = get_vg(vg_name=vg.name)
+ return vg
+
+
def remove_vg(vg_name):
"""
Removes a volume group.
"""
- fail_msg = "Unable to remove vg %s".format(vg_name)
+ fail_msg = "Unable to remove vg %s" % vg_name
process.run(
[
'vgremove',
"""
Removes a physical volume.
"""
- fail_msg = "Unable to remove vg %s".format(pv_name)
+ fail_msg = "Unable to remove vg %s" % pv_name
process.run(
[
'pvremove',
terminal_verbose=True,
)
if returncode != 0:
- raise RuntimeError("Unable to remove %s".format(path))
+ raise RuntimeError("Unable to remove %s" % path)
return True
-def create_lv(name, group, size=None, tags=None):
+def create_lv(name, group, extents=None, size=None, tags=None, uuid_name=False):
"""
Create a Logical Volume in a Volume Group. Command looks like::
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',
'%s' % size,
'-n', name, group
])
+ elif extents:
+ process.run([
+ '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:
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
def _purge(self):
"""
- Deplete all the items in the list, used internally only so that we can
+ 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
"""
)
if not pvs:
return None
- if len(pvs) > 1:
+ if len(pvs) > 1 and pv_tags:
raise MultiplePVsError(pv_name)
return pvs[0]
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}
+
+
+ :param parts: Number of parts to create LVs from
+ :param size: Size in gigabytes to divide the VG into
+
+ :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)
+
+ 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)
+
+ extent_sizing = sizing(vg_free_count, size=extents)
+
+ disk_sizing['extents'] = int(extents)
+ disk_sizing['percentages'] = extent_sizing['percentages']
+ return disk_sizing
+
class Volume(object):
"""