]> git.proxmox.com Git - ceph.git/blobdiff - ceph/src/ceph-volume/ceph_volume/devices/lvm/api.py
update sources to v12.2.1
[ceph.git] / ceph / src / ceph-volume / ceph_volume / devices / lvm / api.py
index 944a4343dad438a7ffd1ffd88cd6d50408fd9972..e5bc26234715675543625d42484a14ef3c907114 100644 (file)
@@ -4,7 +4,7 @@ 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
+from ceph_volume.exceptions import MultipleLVsError, MultipleVGsError, MultiplePVsError
 
 
 def _output_parser(output, fields):
@@ -101,14 +101,37 @@ def get_api_lvs():
           ;/dev/ubuntubox-vg/swap_1;swap_1;ubuntubox-vg
 
     """
-    fields = 'lv_tags,lv_path,lv_name,vg_name'
+    fields = 'lv_tags,lv_path,lv_name,vg_name,lv_uuid'
     stdout, stderr, returncode = process.call(
         ['sudo', 'lvs', '--noheadings', '--separator=";"', '-o', fields]
     )
     return _output_parser(stdout, fields)
 
 
-def get_lv(lv_name=None, vg_name=None, lv_path=None, lv_tags=None):
+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
+
+    Command and delimeted output, should look like::
+
+        $ sudo pvs --noheadings --separator=';' -o pv_name,pv_tags,pv_uuid
+          /dev/sda1;;
+          /dev/sdv;;07A4F654-4162-4600-8EB3-88D1E42F368D
+
+    """
+    fields = 'pv_name,pv_tags,pv_uuid'
+
+    # note the use of `pvs -a` which will return every physical volume including
+    # ones that have not been initialized as "pv" by LVM
+    stdout, stderr, returncode = process.call(
+        ['sudo', 'pvs', '-a', '--no-heading', '--separator=";"', '-o', fields]
+    )
+
+    return _output_parser(stdout, fields)
+
+
+def get_lv(lv_name=None, vg_name=None, lv_path=None, lv_uuid=None, lv_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
@@ -118,10 +141,40 @@ def get_lv(lv_name=None, vg_name=None, lv_path=None, lv_tags=None):
     but it can also lead to multiple lvs being found, since a lot of metadata
     is shared between lvs of a distinct OSD.
     """
-    if not any([lv_name, vg_name, lv_path, lv_tags]):
+    if not any([lv_name, vg_name, lv_path, lv_uuid, lv_tags]):
         return None
     lvs = Volumes()
-    return lvs.get(lv_name=lv_name, vg_name=vg_name, lv_path=lv_path, lv_tags=lv_tags)
+    return lvs.get(
+        lv_name=lv_name, vg_name=vg_name, lv_path=lv_path, lv_uuid=lv_uuid,
+        lv_tags=lv_tags
+    )
+
+
+def get_pv(pv_name=None, pv_uuid=None, pv_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.
+    """
+    if not any([pv_name, pv_uuid, pv_tags]):
+        return None
+    pvs = PVolumes()
+    return pvs.get(pv_name=pv_name, pv_uuid=pv_uuid, pv_tags=pv_tags)
+
+
+def create_pv(device):
+    """
+    Create a physical volume from a device, useful when devices need to be later mapped
+    to journals.
+    """
+    process.run([
+        'sudo',
+        'pvcreate',
+        '-v',  # verbose
+        '-f',  # force it
+        '--yes', # answer yes to any prompts
+        device
+    ])
 
 
 def create_lv(name, group, size=None, **tags):
@@ -231,13 +284,10 @@ class VolumeGroups(list):
         # actual filtered list if any filters were applied
         if vg_tags:
             tag_filtered = []
-            for k, v in vg_tags.items():
-                for volume in filtered:
-                    if volume.tags.get(k) == str(v):
-                        if volume not in tag_filtered:
-                            tag_filtered.append(volume)
-            # return the tag_filtered volumes here, the `filtered` list is no
-            # longer useable
+            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
@@ -314,7 +364,7 @@ class Volumes(list):
         """
         self[:] = []
 
-    def _filter(self, lv_name=None, vg_name=None, lv_path=None, lv_tags=None):
+    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.
@@ -327,6 +377,9 @@ class Volumes(list):
         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]
 
@@ -334,18 +387,16 @@ class Volumes(list):
         # actual filtered list if any filters were applied
         if lv_tags:
             tag_filtered = []
-            for k, v in lv_tags.items():
-                for volume in filtered:
-                    if volume.tags.get(k) == str(v):
-                        if volume not in tag_filtered:
-                            tag_filtered.append(volume)
-            # return the tag_filtered volumes here, the `filtered` list is no
-            # longer useable
+            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_tags=None):
+    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
@@ -354,13 +405,14 @@ class Volumes(list):
             lv_tags={'ceph.osd_id': '0'}
 
         """
-        if not any([lv_name, vg_name, lv_path, lv_tags]):
-            raise TypeError('.filter() requires lv_name, vg_name, lv_path, or tags (none given)')
+        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
@@ -368,7 +420,7 @@ class Volumes(list):
         # and add the filtered items
         self.extend(filtered_volumes)
 
-    def get(self, lv_name=None, vg_name=None, lv_path=None, lv_tags=None):
+    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
@@ -381,12 +433,13 @@ class Volumes(list):
         but it can also lead to multiple lvs being found, since a lot of metadata
         is shared between lvs of a distinct OSD.
         """
-        if not any([lv_name, vg_name, lv_path, lv_tags]):
+        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:
@@ -396,6 +449,104 @@ class Volumes(list):
         return lvs[0]
 
 
+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):
+        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 useable
+            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)')
+        # first find the filtered volumes with the values in self
+        filtered_volumes = self._filter(
+            pv_name=pv_name,
+            pv_uuid=pv_uuid,
+            pv_tags=pv_tags
+        )
+        # then purge everything
+        self._purge()
+        # and add the filtered items
+        self.extend(filtered_volumes)
+
+    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:
+            raise MultiplePVsError(pv_name)
+        return pvs[0]
+
+
 class VolumeGroup(object):
     """
     Represents an LVM group, with some top-level attributes like ``vg_name``
@@ -470,3 +621,66 @@ class Volume(object):
                 '--addtag', '%s=%s' % (key, value), self.lv_path
             ]
         )
+
+
+class PVolume(object):
+    """
+    Represents a Physical Volume from LVM, with some top-level attributes like
+    ``pv_name`` and parsed tags as a dictionary of key/value pairs.
+    """
+
+    def __init__(self, **kw):
+        for k, v in kw.items():
+            setattr(self, k, v)
+        self.pv_api = kw
+        self.name = kw['pv_name']
+        self.tags = parse_tags(kw['pv_tags'])
+
+    def __str__(self):
+        return '<%s>' % self.pv_api['pv_name']
+
+    def __repr__(self):
+        return self.__str__()
+
+    def set_tags(self, tags):
+        """
+        :param tags: A dictionary of tag names and values, like::
+
+            {
+                "ceph.osd_fsid": "aaa-fff-bbbb",
+                "ceph.osd_id": "0"
+            }
+
+        At the end of all modifications, the tags are refreshed to reflect
+        LVM's most current view.
+        """
+        for k, v in tags.items():
+            self.set_tag(k, v)
+        # after setting all the tags, refresh them for the current object, use the
+        # pv_* identifiers to filter because those shouldn't change
+        pv_object = get_pv(pv_name=self.pv_name, pv_uuid=self.pv_uuid)
+        self.tags = pv_object.tags
+
+    def set_tag(self, key, value):
+        """
+        Set the key/value pair as an LVM tag. Does not "refresh" the values of
+        the current object for its tags. Meant to be a "fire and forget" type
+        of modification.
+
+        **warning**: Altering tags on a PV has to be done ensuring that the
+        device is actually the one intended. ``pv_name`` is *not* a persistent
+        value, only ``pv_uuid`` is. Using ``pv_uuid`` is the best way to make
+        sure the device getting changed is the one needed.
+        """
+        # remove it first if it exists
+        if self.tags.get(key):
+            current_value = self.tags[key]
+            tag = "%s=%s" % (key, current_value)
+            process.call(['sudo', 'pvchange', '--deltag', tag, self.pv_name])
+
+        process.call(
+            [
+                'sudo', 'pvchange',
+                '--addtag', '%s=%s' % (key, value), self.pv_name
+            ]
+        )