import logging
import os
import uuid
+import re
+from itertools import repeat
from math import floor
-from ceph_volume import process, util
-from ceph_volume.exceptions import (
- MultipleLVsError, MultipleVGsError,
- MultiplePVsError, SizeAllocationError
-)
+from ceph_volume import process, util, conf
+from ceph_volume.exceptions import SizeAllocationError
logger = logging.getLogger(__name__)
+def convert_filters_to_str(filters):
+ """
+ Convert filter args from dictionary to following format -
+ filters={filter_name=filter_val,...}
+ """
+ if not filters:
+ return filters
+
+ filter_arg = ''
+ for k, v in filters.items():
+ filter_arg += k + '=' + v + ','
+ # get rid of extra comma at the end
+ filter_arg = filter_arg[:len(filter_arg) - 1]
+
+ return filter_arg
+
+
+def convert_tags_to_str(tags):
+ """
+ Convert tags from dictionary to following format -
+ tags={tag_name=tag_val,...}
+ """
+ if not tags:
+ return tags
+
+ tag_arg = 'tags={'
+ for k, v in tags.items():
+ tag_arg += k + '=' + v + ','
+ # get rid of extra comma at the end
+ tag_arg = tag_arg[:len(tag_arg) - 1] + '}'
+
+ return tag_arg
+
+
+def make_filters_lvmcmd_ready(filters, tags):
+ """
+ Convert filters (including tags) from dictionary to following format -
+ filter_name=filter_val...,tags={tag_name=tag_val,...}
+
+ The command will look as follows =
+ lvs -S filter_name=filter_val...,tags={tag_name=tag_val,...}
+ """
+ filters = convert_filters_to_str(filters)
+ tags = convert_tags_to_str(tags)
+
+ if filters and tags:
+ return filters + ',' + tags
+ if filters and not tags:
+ return filters
+ if not filters and tags:
+ return tags
+ else:
+ return ''
+
+
def _output_parser(output, fields):
"""
Newer versions of LVM allow ``--reportformat=json``, but older versions,
like the one included in Xenial do not. LVM has the ability to filter and
format its output so we assume the output will be in a format this parser
- can handle (using ',' as a delimiter)
+ can handle (using ';' as a delimiter)
:param fields: A string, possibly using ',' to group many items, as it
would be used on the CLI
# splitting on ';' because that is what the lvm call uses as
# '--separator'
output_items = [i.strip() for i in line.split(';')]
- # map the output to the fiels
+ # map the output to the fields
report.append(
dict(zip(field_items, output_items))
)
return {
'parts': parts,
'percentages': percentages,
- 'sizes': int(sizes),
+ 'sizes': int(sizes/1024/1024/1024),
}
return _splitname_parser(out)
+def is_ceph_device(lv):
+ try:
+ lv.tags['ceph.osd_id']
+ except (KeyError, AttributeError):
+ logger.warning('device is not part of ceph: %s', lv)
+ return False
+
+ if lv.tags['ceph.osd_id'] == 'null':
+ return False
+ else:
+ return True
+
+
####################################
#
# Code for LVM Physical Volumes
#
################################
-
-def get_api_pvs():
- """
- Return the list of physical volumes configured for lvm and available in the
- system using flags to include common metadata associated with them like the uuid
-
- This will only return physical volumes set up to work with LVM.
-
- Command and delimited output should look like::
-
- $ 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,lv_uuid'
-
- stdout, stderr, returncode = process.call(
- ['pvs', '--no-heading', '--readonly', '--separator=";"', '-o', fields],
- verbose_on_failure=False
- )
-
- return _output_parser(stdout, fields)
-
+PV_FIELDS = 'pv_name,pv_tags,pv_uuid,vg_name,lv_uuid'
class PVolume(object):
"""
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)
+ pv_object = self.get_single_pv(filter={'pv_name': self.pv_name,
+ 'pv_uuid': self.pv_uuid})
+
+ if not pv_object:
+ raise RuntimeError('No PV was found.')
+
self.tags = pv_object.tags
def set_tag(self, key, value):
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', '--deltag', tag, self.pv_name], run_on_host=True)
process.call(
[
'pvchange',
'--addtag', '%s=%s' % (key, value), self.pv_name
- ]
+ ],
+ run_on_host=True
)
-class PVolumes(list):
- """
- A list of all known (physical) volumes for the current system, with the ability
- to filter them via keyword arguments.
- """
-
- 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):
"""
Create a physical volume from a device, useful when devices need to be later mapped
'-f', # force it
'--yes', # answer yes to any prompts
device
- ])
+ ], run_on_host=True)
def remove_pv(pv_name):
'-f', # force it
pv_name
],
+ run_on_host=True,
fail_msg=fail_msg,
)
-def get_pv(pv_name=None, pv_uuid=None, pv_tags=None, pvs=None):
+def get_pvs(fields=PV_FIELDS, filters='', tags=None):
"""
- 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.
+ Return a list of PVs that are available on the system and match the
+ filters and tags passed. Argument filters takes a dictionary containing
+ arguments required by -S option of LVM. Passing a list of LVM tags can be
+ quite tricky to pass as a dictionary within dictionary, therefore pass
+ dictionary of tags via tags argument and tricky part will be taken care of
+ by the helper methods.
+
+ :param fields: string containing list of fields to be displayed by the
+ pvs command
+ :param sep: string containing separator to be used between two fields
+ :param filters: dictionary containing LVM filters
+ :param tags: dictionary containng LVM tags
+ :returns: list of class PVolume object representing pvs on the system
+ """
+ filters = make_filters_lvmcmd_ready(filters, tags)
+ args = ['pvs', '--noheadings', '--readonly', '--separator=";"', '-S',
+ filters, '-o', fields]
+
+ stdout, stderr, returncode = process.call(args, run_on_host=True, verbose_on_failure=False)
+ pvs_report = _output_parser(stdout, fields)
+ return [PVolume(**pv_report) for pv_report in pvs_report]
+
+
+def get_single_pv(fields=PV_FIELDS, filters=None, tags=None):
"""
- if not any([pv_name, pv_uuid, pv_tags]):
+ Wrapper of get_pvs() meant to be a convenience method to avoid the phrase::
+ pvs = get_pvs()
+ if len(pvs) >= 1:
+ pv = pvs[0]
+ """
+ pvs = get_pvs(fields=fields, filters=filters, tags=tags)
+
+ if len(pvs) == 0:
return None
- if pvs is None or len(pvs) == 0:
- pvs = PVolumes()
+ if len(pvs) > 1:
+ raise RuntimeError('Filters {} matched more than 1 PV present on this host.'.format(str(filters)))
- return pvs.get(pv_name=pv_name, pv_uuid=pv_uuid, pv_tags=pv_tags)
+ return pvs[0]
################################
#
#############################
-
-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)
+VG_FIELDS = 'vg_name,pv_count,lv_count,vg_attr,vg_extent_count,vg_free_count,vg_extent_size'
+VG_CMD_OPTIONS = ['--noheadings', '--readonly', '--units=b', '--nosuffix', '--separator=";"']
class VolumeGroup(object):
for k, v in kw.items():
setattr(self, k, v)
self.name = kw['vg_name']
+ if not self.name:
+ raise ValueError('VolumeGroup must have a non-empty name')
self.tags = parse_tags(kw.get('vg_tags', ''))
def __str__(self):
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 free space in VG in bytes
"""
- return self._parse_size(self.vg_free)
+ return int(self.vg_extent_size) * int(self.vg_free_count)
@property
- def size(self):
+ def free_percent(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::
+ Return free space in VG in bytes
+ """
+ return int(self.vg_free_count) / int(self.vg_extent_count)
- >>> data_vg.vg_size
- '1024.9g'
- >>> data_vg.size
- 1024
+ @property
+ def size(self):
+ """
+ Returns VG size in bytes
"""
- return self._parse_size(self.vg_size)
+ return int(self.vg_extent_size) * int(self.vg_extent_count)
def sizing(self, parts=None, size=None):
"""
vg_free_count = util.str_to_int(self.vg_free_count)
if size:
- extents = int(size * vg_free_count / self.free)
+ size = size * 1024 * 1024 * 1024
+ extents = int(size / int(self.vg_extent_size))
disk_sizing = sizing(self.free, size=size, parts=parts)
else:
if parts is not None:
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.
- """
-
- def __init__(self, populate=True):
- if populate:
- self._populate()
-
- def _populate(self):
- # get all the vgs in the current system
- for vg_item in get_api_vgs():
- self.append(VolumeGroup(**vg_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, vg_name=None, vg_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.
-
- .. note:: ``vg_tags`` is not yet implemented
- """
- filtered = [i for i in self]
- if vg_name:
- filtered = [i for i in filtered if i.vg_name == vg_name]
-
- # at this point, `filtered` has either all the volumes in self or is an
- # actual filtered list if any filters were applied
- if vg_tags:
- tag_filtered = []
- for volume in filtered:
- matches = all(volume.tags.get(k) == str(v) for k, v in vg_tags.items())
- if matches:
- tag_filtered.append(volume)
- return tag_filtered
-
- return filtered
-
- def filter(self, vg_name=None, vg_tags=None):
- """
- Filter out groups on top level attributes like ``vg_name`` or by
- ``vg_tags`` where a dict is required. For example, to find a Ceph group
- with dmcache as the type, the filter would look like::
-
- vg_tags={'ceph.type': 'dmcache'}
-
- .. warning:: These tags are not documented because they are currently
- unused, but are here to maintain API consistency
- """
- if not any([vg_name, vg_tags]):
- raise TypeError('.filter() requires vg_name or vg_tags (none given)')
-
- filtered_vgs = VolumeGroups(populate=False)
- filtered_vgs.extend(self._filter(vg_name, vg_tags))
- return filtered_vgs
-
- def get(self, vg_name=None, vg_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 VGs are matched
-
- It is useful to use ``tags`` when trying to find a specific volume group,
- but it can also lead to multiple vgs being found (although unlikely)
- """
- if not any([vg_name, vg_tags]):
- return None
- vgs = self._filter(
- vg_name=vg_name,
- vg_tags=vg_tags
- )
- if not vgs:
- return None
- if len(vgs) > 1:
- # this is probably never going to happen, but it is here to keep
- # the API code consistent
- raise MultipleVGsError(vg_name)
- return vgs[0]
+ def bytes_to_extents(self, size):
+ '''
+ Return a how many free extents we can fit into a size in bytes. This has
+ some uncertainty involved. If size/extent_size is within 1% of the
+ actual free extents we will return the extent count, otherwise we'll
+ throw an error.
+ This accomodates for the size calculation in batch. We need to report
+ the OSD layout but have not yet created any LVM structures. We use the
+ disk size in batch if no VG is present and that will overshoot the
+ actual free_extent count due to LVM overhead.
+
+ '''
+ b_to_ext = int(size / int(self.vg_extent_size))
+ if b_to_ext < int(self.vg_free_count):
+ # return bytes in extents if there is more space
+ return b_to_ext
+ elif b_to_ext / int(self.vg_free_count) - 1 < 0.01:
+ # return vg_fre_count if its less then 1% off
+ logger.info(
+ 'bytes_to_extents results in {} but only {} '
+ 'are available, adjusting the latter'.format(b_to_ext,
+ self.vg_free_count))
+ return int(self.vg_free_count)
+ # else raise an exception
+ raise RuntimeError('Can\'t convert {} to free extents, only {} ({} '
+ 'bytes) are free'.format(size, self.vg_free_count,
+ self.free))
+
+ def slots_to_extents(self, slots):
+ '''
+ Return how many extents fit the VG slot times
+ '''
+ return int(int(self.vg_extent_count) / slots)
def create_vg(devices, name=None, name_prefix=None):
name = "ceph-%s" % str(uuid.uuid4())
process.run([
'vgcreate',
- '-s',
- '1G',
'--force',
'--yes',
- name] + devices
+ name] + devices,
+ run_on_host=True
)
- vg = get_vg(vg_name=name)
- return vg
+ return get_single_vg(filters={'vg_name': name})
def extend_vg(vg, devices):
'vgextend',
'--force',
'--yes',
- vg.name] + devices
+ vg.name] + devices,
+ run_on_host=True
)
- vg = get_vg(vg_name=vg.name)
- return vg
+ return get_single_vg(filters={'vg_name': vg.name})
def reduce_vg(vg, devices):
'vgreduce',
'--force',
'--yes',
- vg.name] + devices
+ vg.name] + devices,
+ run_on_host=True
)
- vg = get_vg(vg_name=vg.name)
- return vg
+ return get_single_vg(filter={'vg_name': vg.name})
def remove_vg(vg_name):
'-f', # force it
vg_name
],
+ run_on_host=True,
fail_msg=fail_msg,
)
-def get_vg(vg_name=None, vg_tags=None, vgs=None):
+def get_vgs(fields=VG_FIELDS, filters='', 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.
+ Return a list of VGs that are available on the system and match the
+ filters and tags passed. Argument filters takes a dictionary containing
+ arguments required by -S option of LVM. Passing a list of LVM tags can be
+ quite tricky to pass as a dictionary within dictionary, therefore pass
+ dictionary of tags via tags argument and tricky part will be taken care of
+ by the helper methods.
- It is useful to use ``tags`` when trying to find a specific volume group,
- but it can also lead to multiple vgs being found.
+ :param fields: string containing list of fields to be displayed by the
+ vgs command
+ :param sep: string containing separator to be used between two fields
+ :param filters: dictionary containing LVM filters
+ :param tags: dictionary containng LVM tags
+ :returns: list of class VolumeGroup object representing vgs on the system
"""
- if not any([vg_name, vg_tags]):
- return None
- if vgs is None or len(vgs) == 0:
- vgs = VolumeGroups()
+ filters = make_filters_lvmcmd_ready(filters, tags)
+ args = ['vgs'] + VG_CMD_OPTIONS + ['-S', filters, '-o', fields]
- return vgs.get(vg_name=vg_name, vg_tags=vg_tags)
+ stdout, stderr, returncode = process.call(args, run_on_host=True, verbose_on_failure=False)
+ vgs_report =_output_parser(stdout, fields)
+ return [VolumeGroup(**vg_report) for vg_report in vgs_report]
-#################################
-#
-# Code for LVM Logical Volumes
-#
-###############################
+def get_single_vg(fields=VG_FIELDS, filters=None, tags=None):
+ """
+ Wrapper of get_vgs() meant to be a convenience method to avoid the phrase::
+ vgs = get_vgs()
+ if len(vgs) >= 1:
+ vg = vgs[0]
+ """
+ vgs = get_vgs(fields=fields, filters=filters, tags=tags)
+ if len(vgs) == 0:
+ return None
+ if len(vgs) > 1:
+ raise RuntimeError('Filters {} matched more than 1 VG present on this host.'.format(str(filters)))
+
+ return vgs[0]
-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::
+def get_device_vgs(device, name_prefix=''):
+ stdout, stderr, returncode = process.call(
+ ['pvs'] + VG_CMD_OPTIONS + ['-o', VG_FIELDS, device],
+ run_on_host=True,
+ verbose_on_failure=False
+ )
+ vgs = _output_parser(stdout, VG_FIELDS)
+ return [VolumeGroup(**vg) for vg in vgs if vg['vg_name'] and vg['vg_name'].startswith(name_prefix)]
- $ 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'
+def get_all_devices_vgs(name_prefix=''):
+ vg_fields = f'pv_name,{VG_FIELDS}'
+ cmd = ['pvs'] + VG_CMD_OPTIONS + ['-o', vg_fields]
stdout, stderr, returncode = process.call(
- ['lvs', '--noheadings', '--readonly', '--separator=";"', '-a', '-o', fields],
+ cmd,
+ run_on_host=True,
verbose_on_failure=False
)
- return _output_parser(stdout, fields)
+ vgs = _output_parser(stdout, vg_fields)
+ return [VolumeGroup(**vg) for vg in vgs if vg['vg_name']]
+
+#################################
+#
+# Code for LVM Logical Volumes
+#
+###############################
+
+LV_FIELDS = 'lv_tags,lv_path,lv_name,vg_name,lv_uuid,lv_size'
+LV_CMD_OPTIONS = ['--noheadings', '--readonly', '--separator=";"', '-a',
+ '--units=b', '--nosuffix']
class Volume(object):
setattr(self, k, v)
self.lv_api = kw
self.name = kw['lv_name']
+ if not self.name:
+ raise ValueError('Volume must have a non-empty name')
self.tags = parse_tags(kw['lv_tags'])
self.encrypted = self.tags.get('ceph.encrypted', '0') == '1'
self.used_by_ceph = 'ceph.osd_id' in self.tags
report = {
'name': self.lv_name,
'osd_id': self.tags['ceph.osd_id'],
- 'cluster_name': self.tags['ceph.cluster_name'],
+ 'cluster_name': self.tags.get('ceph.cluster_name', conf.cluster),
'type': type_,
'osd_fsid': self.tags['ceph.osd_fsid'],
'cluster_fsid': self.tags['ceph.cluster_fsid'],
+ 'osdspec_affinity': self.tags.get('ceph.osdspec_affinity', ''),
}
type_uuid = '{}_uuid'.format(type_)
report[type_uuid] = self.tags['ceph.{}'.format(type_uuid)]
return report
- def clear_tags(self):
+ def _format_tag_args(self, op, tags):
+ tag_args = ['{}={}'.format(k, v) for k, v in tags.items()]
+ # weird but efficient way of ziping two lists and getting a flat list
+ return list(sum(zip(repeat(op), tag_args), ()))
+
+ def clear_tags(self, keys=None):
"""
- Removes all tags from the Logical Volume.
+ Removes all or passed tags from the Logical Volume.
"""
- for k in list(self.tags):
- self.clear_tag(k)
+ if not keys:
+ keys = self.tags.keys()
+
+ del_tags = {k: self.tags[k] for k in keys if k in self.tags}
+ if not del_tags:
+ # nothing to clear
+ return
+ del_tag_args = self._format_tag_args('--deltag', del_tags)
+ # --deltag returns successful even if the to be deleted tag is not set
+ process.call(['lvchange'] + del_tag_args + [self.lv_path], run_on_host=True)
+ for k in del_tags.keys():
+ del self.tags[k]
def set_tags(self, tags):
At the end of all modifications, the tags are refreshed to reflect
LVM's most current view.
"""
+ self.clear_tags(tags.keys())
+ add_tag_args = self._format_tag_args('--addtag', tags)
+ process.call(['lvchange'] + add_tag_args + [self.lv_path], run_on_host=True)
for k, v in tags.items():
- self.set_tag(k, v)
+ self.tags[k] = v
def clear_tag(self, key):
if self.tags.get(key):
current_value = self.tags[key]
tag = "%s=%s" % (key, current_value)
- process.call(['lvchange', '--deltag', tag, self.lv_path])
+ process.call(['lvchange', '--deltag', tag, self.lv_path], run_on_host=True)
del self.tags[key]
[
'lvchange',
'--addtag', '%s=%s' % (key, value), self.lv_path
- ]
+ ],
+ run_on_host=True
)
self.tags[key] = value
-
-class Volumes(list):
- """
- A list of all known (logical) volumes for the current system, with the ability
- to filter them via keyword arguments.
- """
-
- def __init__(self):
- self._populate()
-
- def _populate(self):
- # get all the lvs in the current system
- for lv_item in get_api_lvs():
- self.append(Volume(**lv_item))
-
- 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[:] = []
-
- 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]
-
- 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):
- """
- 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 deactivate(self):
"""
- 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 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.
-
- 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.
+ Deactivate the LV by calling lvchange -an
"""
- 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]
+ process.call(['lvchange', '-an', self.lv_path], run_on_host=True)
-def create_lv(name, group, extents=None, size=None, tags=None, uuid_name=False, pv=None):
+def create_lv(name_prefix,
+ uuid,
+ vg=None,
+ device=None,
+ slots=None,
+ extents=None,
+ size=None,
+ tags=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
+ ``name_prefix`` is required. If ``size`` is provided its expected to be a
+ byte count. 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",
- }
+ :param name_prefix: name prefix for the LV, typically somehting like ceph-osd-block
+ :param uuid: UUID to ensure uniqueness; is combined with name_prefix to
+ form the LV name
+ :param vg: optional, pass an existing VG to create LV
+ :param device: optional, device to use. Either device of vg must be passed
+ :param slots: optional, number of slots to divide vg up, LV will occupy one
+ one slot if enough space is available
+ :param extends: optional, how many lvm extends to use, supersedes slots
+ :param size: optional, target LV size in bytes, supersedes extents,
+ resulting LV might be smaller depending on extent
+ size of the underlying VG
+ :param tags: optional, a dict of lvm tags to set on the LV
+ """
+ name = '{}-{}'.format(name_prefix, uuid)
+ if not vg:
+ if not device:
+ raise RuntimeError("Must either specify vg or device, none given")
+ # check if a vgs starting with ceph already exists
+ vgs = get_device_vgs(device, 'ceph')
+ if vgs:
+ vg = vgs[0]
+ else:
+ # create on if not
+ vg = create_vg(device, name_prefix='ceph')
+ assert(vg)
- # 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:
+ extents = vg.bytes_to_extents(size)
+ logger.debug('size was passed: {} -> {}'.format(size, extents))
+ elif slots and not extents:
+ extents = vg.slots_to_extents(slots)
+ logger.debug('slots was passed: {} -> {}'.format(slots, extents))
+
+ if extents:
command = [
'lvcreate',
'--yes',
'-l',
- '%s' % extents,
- '-n', name, group
+ '{}'.format(extents),
+ '-n', name, vg.vg_name
]
# create the lv with all the space available, this is needed because the
# system call is different for LVM
'--yes',
'-l',
'100%FREE',
- '-n', name, group
+ '-n', name, vg.vg_name
]
- if pv:
- command.append(pv)
- process.run(command)
+ process.run(command, run_on_host=True)
- lv = get_lv(lv_name=name, vg_name=group)
- lv.set_tags(tags)
+ lv = get_single_lv(filters={'lv_name': name, 'vg_name': vg.vg_name})
+ if tags is None:
+ tags = {
+ "ceph.osd_id": "null",
+ "ceph.type": "null",
+ "ceph.cluster_fsid": "null",
+ "ceph.osd_fsid": "null",
+ }
# 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
+ # XXX add CEPH_VOLUME_LVM_DEBUG to enable -vvvv on lv operations
+ type_path_tag = {
+ '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
+ }
path_tag = type_path_tag.get(tags.get('ceph.type'))
if path_tag:
- lv.set_tags(
- {path_tag: lv.lv_path}
- )
+ tags.update({path_tag: lv.lv_path})
+
+ lv.set_tags(tags)
+
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']
+ lvs.append(
+ create_lv(name_prefix, uuid.uuid4(), vg=volume_group, extents=extents, tags=tags)
+ )
+ return lvs
+
+
def remove_lv(lv):
"""
Removes a logical volume given it's absolute path.
'-f', # force it
path
],
+ run_on_host=True,
show_command=True,
terminal_verbose=True,
)
return True
-def is_lv(dev, lvs=None):
+def get_lvs(fields=LV_FIELDS, filters='', tags=None):
"""
- Boolean to detect if a device is an LV or not.
+ Return a list of LVs that are available on the system and match the
+ filters and tags passed. Argument filters takes a dictionary containing
+ arguments required by -S option of LVM. Passing a list of LVM tags can be
+ quite tricky to pass as a dictionary within dictionary, therefore pass
+ dictionary of tags via tags argument and tricky part will be taken care of
+ by the helper methods.
+
+ :param fields: string containing list of fields to be displayed by the
+ lvs command
+ :param sep: string containing separator to be used between two fields
+ :param filters: dictionary containing LVM filters
+ :param tags: dictionary containng LVM tags
+ :returns: list of class Volume object representing LVs on the system
"""
- 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()
+ filters = make_filters_lvmcmd_ready(filters, tags)
+ args = ['lvs'] + LV_CMD_OPTIONS + ['-S', filters, '-o', fields]
- if splitname.get('LV_NAME'):
- lvs.filter(lv_name=splitname['LV_NAME'], vg_name=splitname['VG_NAME'])
- return len(lvs) > 0
- return False
+ stdout, stderr, returncode = process.call(args, run_on_host=True, verbose_on_failure=False)
+ lvs_report = _output_parser(stdout, fields)
+ return [Volume(**lv_report) for lv_report in lvs_report]
-def get_lv(lv_name=None, vg_name=None, lv_path=None, lv_uuid=None, lv_tags=None, lvs=None):
+def get_single_lv(fields=LV_FIELDS, filters=None, tags=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.
+ Wrapper of get_lvs() meant to be a convenience method to avoid the phrase::
+ lvs = get_lvs()
+ if len(lvs) >= 1:
+ lv = lvs[0]
"""
- if not any([lv_name, vg_name, lv_path, lv_uuid, lv_tags]):
+ lvs = get_lvs(fields=fields, filters=filters, tags=tags)
+
+ if len(lvs) == 0:
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
+ if len(lvs) > 1:
+ raise RuntimeError('Filters {} matched more than 1 LV present on this host.'.format(str(filters)))
+
+ return lvs[0]
+
+
+def get_lvs_from_osd_id(osd_id):
+ return get_lvs(tags={'ceph.osd_id': osd_id})
+
+
+def get_single_lv_from_osd_id(osd_id):
+ return get_single_lv(tags={'ceph.osd_id': osd_id})
+
+
+def get_lv_by_name(name):
+ stdout, stderr, returncode = process.call(
+ ['lvs', '--noheadings', '-o', LV_FIELDS, '-S',
+ 'lv_name={}'.format(name)],
+ run_on_host=True,
+ verbose_on_failure=False
+ )
+ lvs = _output_parser(stdout, LV_FIELDS)
+ return [Volume(**lv) for lv in lvs]
+
+
+def get_lvs_by_tag(lv_tag):
+ stdout, stderr, returncode = process.call(
+ ['lvs', '--noheadings', '--separator=";"', '-a', '-o', LV_FIELDS, '-S',
+ 'lv_tags={{{}}}'.format(lv_tag)],
+ run_on_host=True,
+ verbose_on_failure=False
)
+ lvs = _output_parser(stdout, LV_FIELDS)
+ return [Volume(**lv) for lv in lvs]
-def get_lv_from_argument(argument):
+def get_device_lvs(device, name_prefix=''):
+ stdout, stderr, returncode = process.call(
+ ['pvs'] + LV_CMD_OPTIONS + ['-o', LV_FIELDS, device],
+ run_on_host=True,
+ verbose_on_failure=False
+ )
+ lvs = _output_parser(stdout, LV_FIELDS)
+ return [Volume(**lv) for lv in lvs if lv['lv_name'] and
+ lv['lv_name'].startswith(name_prefix)]
+
+def get_lvs_from_path(devpath):
+ lvs = []
+ if os.path.isabs(devpath):
+ # we have a block device
+ lvs = get_device_lvs(devpath)
+ if not lvs:
+ # maybe this was a LV path /dev/vg_name/lv_name or /dev/mapper/
+ lvs = get_lvs(filters={'path': devpath})
+
+ return lvs
+
+def get_lv_by_fullname(full_name):
"""
- 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
+ returns LV by the specified LV's full name (formatted as vg_name/lv_name)
"""
- if argument.startswith('/'):
- lv = get_lv(lv_path=argument)
- return lv
try:
- vg_name, lv_name = argument.split('/')
- except (ValueError, AttributeError):
+ vg_name, lv_name = full_name.split('/')
+ res_lv = get_single_lv(filters={'lv_name': lv_name,
+ 'vg_name': vg_name})
+ except ValueError:
+ res_lv = None
+ return res_lv
+
+def get_lv_path_from_mapper(mapper):
+ """
+ This functions translates a given mapper device under the format:
+ /dev/mapper/LV to the format /dev/VG/LV.
+ eg:
+ from:
+ /dev/mapper/ceph--c1a97e46--234c--46aa--a549--3ca1d1f356a9-osd--block--32e8e896--172e--4a38--a06a--3702598510ec
+ to:
+ /dev/ceph-c1a97e46-234c-46aa-a549-3ca1d1f356a9/osd-block-32e8e896-172e-4a38-a06a-3702598510ec
+ """
+ results = re.split(r'^\/dev\/mapper\/(.+\w)-(\w.+)', mapper)
+ results = list(filter(None, results))
+
+ if len(results) != 2:
return None
- return get_lv(lv_name=lv_name, vg_name=vg_name)
+ return f"/dev/{results[0].replace('--', '-')}/{results[1].replace('--', '-')}"
-def create_lvs(volume_group, parts=None, size=None, name_prefix='ceph-lv'):
+def get_mapper_from_lv_path(lv_path):
"""
- 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"``.
+ This functions translates a given lv path under the format:
+ /dev/VG/LV to the format /dev/mapper/LV.
+ eg:
+ from:
+ /dev/ceph-c1a97e46-234c-46aa-a549-3ca1d1f356a9/osd-block-32e8e896-172e-4a38-a06a-3702598510ec
+ to:
+ /dev/mapper/ceph--c1a97e46--234c--46aa--a549--3ca1d1f356a9-osd--block--32e8e896--172e--4a38--a06a--3702598510ec
+ """
+ results = re.split(r'^\/dev\/(.+\w)-(\w.+)', lv_path)
+ results = list(filter(None, results))
- .. note:: LVs that are not in use can be detected by querying LVM for tags that are
- set to ``"null"``.
+ if len(results) != 2:
+ return None
- :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
+ return f"/dev/mapper/{results[0].replace('-', '--')}/{results[1].replace('-', '--')}"