]> git.proxmox.com Git - ceph.git/blobdiff - ceph/src/ceph-volume/ceph_volume/api/lvm.py
update ceph source to reef 18.2.1
[ceph.git] / ceph / src / ceph-volume / ceph_volume / api / lvm.py
index b6661522e342271a8b25cfa4b24c8dd9ab05d099..dcc4f18627218ce98b956752312a6f5189c359e4 100644 (file)
@@ -6,22 +6,76 @@ set of utilities for interacting with LVM.
 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
@@ -43,7 +97,7 @@ def _output_parser(output, fields):
         # 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))
         )
@@ -126,7 +180,7 @@ def sizing(device_size, parts=None, size=None):
     return {
         'parts': parts,
         'percentages': percentages,
-        'sizes': int(sizes),
+        'sizes': int(sizes/1024/1024/1024),
     }
 
 
@@ -267,36 +321,26 @@ def dmsetup_splitname(dev):
     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):
     """
@@ -333,7 +377,12 @@ 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):
@@ -351,110 +400,17 @@ class PVolume(object):
         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
@@ -466,7 +422,7 @@ def create_pv(device):
         '-f',  # force it
         '--yes', # answer yes to any prompts
         device
-    ])
+    ], run_on_host=True)
 
 
 def remove_pv(pv_name):
@@ -492,22 +448,51 @@ 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]
 
 
 ################################
@@ -516,28 +501,8 @@ def get_pv(pv_name=None, pv_uuid=None, pv_tags=None, pvs=None):
 #
 #############################
 
-
-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):
@@ -549,6 +514,8 @@ 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):
@@ -557,43 +524,26 @@ class VolumeGroup(object):
     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):
         """
@@ -632,7 +582,8 @@ class VolumeGroup(object):
         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:
@@ -648,97 +599,39 @@ class VolumeGroup(object):
         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):
@@ -765,15 +658,13 @@ 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):
@@ -794,11 +685,11 @@ 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):
@@ -817,11 +708,11 @@ 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):
@@ -839,51 +730,82 @@ 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):
@@ -897,6 +819,8 @@ 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
@@ -927,21 +851,37 @@ class Volume(object):
             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):
@@ -956,15 +896,18 @@ class Volume(object):
         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]
 
 
@@ -979,166 +922,77 @@ class Volume(object):
             [
                 '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
@@ -1148,25 +1002,81 @@ def create_lv(name, group, extents=None, size=None, tags=None, uuid_name=False,
             '--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.
@@ -1188,6 +1098,7 @@ def remove_lv(lv):
             '-f',  # force it
             path
         ],
+        run_on_host=True,
         show_command=True,
         terminal_verbose=True,
     )
@@ -1196,97 +1107,142 @@ def remove_lv(lv):
     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('-', '--')}"