]> git.proxmox.com Git - ceph.git/blobdiff - ceph/src/ceph-volume/ceph_volume/util/device.py
update sources to 12.2.10
[ceph.git] / ceph / src / ceph-volume / ceph_volume / util / device.py
index 7dca60a49ae4f23ab707cf80ebd2ac7eb5400378..1810448869b1b01f2d676ecb6cea36f99f8b3579 100644 (file)
+# -*- coding: utf-8 -*-
+
 import os
+from functools import total_ordering
 from ceph_volume import sys_info
 from ceph_volume.api import lvm
 from ceph_volume.util import disk
 
+report_template = """
+{dev:<25} {size:<12} {rot!s:<7} {available!s:<9} {model}"""
+
+
+class Devices(object):
+    """
+    A container for Device instances with reporting
+    """
+
+    def __init__(self, devices=None):
+        if not sys_info.devices:
+            sys_info.devices = disk.get_devices()
+        self.devices = [Device(k) for k in
+                            sys_info.devices.keys()]
+
+    def pretty_report(self, all=True):
+        output = [
+            report_template.format(
+                dev='Device Path',
+                size='Size',
+                rot='rotates',
+                model='Model name',
+                available='available',
+            )]
+        for device in sorted(self.devices):
+            output.append(device.report())
+        return ''.join(output)
 
+    def json_report(self):
+        output = []
+        for device in sorted(self.devices):
+            output.append(device.json_report())
+        return output
+
+@total_ordering
 class Device(object):
 
+    pretty_template = """
+     {attr:<25} {value}"""
+
+    report_fields = [
+        'rejected_reasons',
+        'available',
+        'path',
+        'sys_api',
+    ]
+    pretty_report_sys_fields = [
+        'human_readable_size',
+        'model',
+        'removable',
+        'ro',
+        'rotational',
+        'sas_address',
+        'scheduler_mode',
+        'vendor',
+    ]
+
     def __init__(self, path):
         self.path = path
         # LVs can have a vg/lv path, while disks will have /dev/sda
         self.abspath = path
         self.lv_api = None
+        self.lvs = []
+        self.vg_name = None
+        self.lv_name = None
         self.pvs_api = []
         self.disk_api = {}
+        self.blkid_api = {}
         self.sys_api = {}
         self._exists = None
         self._is_lvm_member = None
         self._parse()
+        self.available, self.rejected_reasons = self._check_reject_reasons()
+
+    def __lt__(self, other):
+        '''
+        Implementing this method and __eq__ allows the @total_ordering
+        decorator to turn the Device class into a totally ordered type.
+        This can slower then implementing all comparison operations.
+        This sorting should put available devices before unavailable devices
+        and sort on the path otherwise (str sorting).
+        '''
+        if self.available == other.available:
+            return self.path < other.path
+        return self.available and not other.available
+
+    def __eq__(self, other):
+        return self.path == other.path
 
     def _parse(self):
+        if not sys_info.devices:
+            sys_info.devices = disk.get_devices()
+        self.sys_api = sys_info.devices.get(self.abspath, {})
+
         # start with lvm since it can use an absolute or relative path
         lv = lvm.get_lv_from_argument(self.path)
         if lv:
             self.lv_api = lv
+            self.lvs = [lv]
             self.abspath = lv.lv_path
+            self.vg_name = lv.vg_name
+            self.lv_name = lv.name
         else:
             dev = disk.lsblk(self.path)
+            self.blkid_api = disk.blkid(self.path)
             self.disk_api = dev
             device_type = dev.get('TYPE', '')
             # always check is this is an lvm member
             if device_type in ['part', 'disk']:
                 self._set_lvm_membership()
 
-        if not sys_info.devices:
-            sys_info.devices = disk.get_devices()
-        self.sys_api = sys_info.devices.get(self.abspath, {})
+        self.ceph_disk = CephDiskDevice(self)
 
     def __repr__(self):
         prefix = 'Unknown'
@@ -46,37 +129,118 @@ class Device(object):
             prefix = 'Raw Device'
         return '<%s: %s>' % (prefix, self.abspath)
 
-    def _set_lvm_membership(self):
-        if self._is_lvm_member is None:
-            # check if there was a pv created with the
-            # name of device
-            pvs = lvm.PVolumes()
-            pvs.filter(pv_name=self.abspath)
-            if not pvs:
-                self._is_lvm_member = False
-                return self._is_lvm_member
-            has_vgs = [pv.vg_name for pv in pvs if pv.vg_name]
-            if has_vgs:
-                self._is_lvm_member = True
-                self.pvs_api = pvs
+    def pretty_report(self):
+        def format_value(v):
+            if isinstance(v, list):
+                return ', '.join(v)
             else:
-                # this is contentious, if a PV is recognized by LVM but has no
-                # VGs, should we consider it as part of LVM? We choose not to
-                # here, because most likely, we need to use VGs from this PV.
-                self._is_lvm_member = False
+                return v
+        def format_key(k):
+            return k.strip('_').replace('_', ' ')
+        output = ['\n====== Device report {} ======\n'.format(self.path)]
+        output.extend(
+            [self.pretty_template.format(
+                attr=format_key(k),
+                value=format_value(v)) for k, v in vars(self).items() if k in
+                self.report_fields and k != 'disk_api' and k != 'sys_api'] )
+        output.extend(
+            [self.pretty_template.format(
+                attr=format_key(k),
+                value=format_value(v)) for k, v in self.sys_api.items() if k in
+                self.pretty_report_sys_fields])
+        for lv in self.lvs:
+            output.append("""
+    --- Logical Volume ---""")
+            output.extend(
+                [self.pretty_template.format(
+                    attr=format_key(k),
+                    value=format_value(v)) for k, v in lv.report().items()])
+        return ''.join(output)
+
+    def report(self):
+        return report_template.format(
+            dev=self.abspath,
+            size=self.size_human,
+            rot=self.rotational,
+            available=self.available,
+            model=self.model,
+        )
 
+    def json_report(self):
+        output = {k.strip('_'): v for k, v in vars(self).items() if k in
+                  self.report_fields}
+        output['lvs'] = [lv.report() for lv in self.lvs]
+        return output
+
+    def _set_lvm_membership(self):
+        if self._is_lvm_member is None:
+            # this is contentious, if a PV is recognized by LVM but has no
+            # VGs, should we consider it as part of LVM? We choose not to
+            # here, because most likely, we need to use VGs from this PV.
+            self._is_lvm_member = False
+            for path in self._get_pv_paths():
+                # check if there was a pv created with the
+                # name of device
+                pvs = lvm.PVolumes()
+                pvs.filter(pv_name=path)
+                has_vgs = [pv.vg_name for pv in pvs if pv.vg_name]
+                if has_vgs:
+                    # a pv can only be in one vg, so this should be safe
+                    self.vg_name = has_vgs[0]
+                    self._is_lvm_member = True
+                    self.pvs_api = pvs
+                    for pv in pvs:
+                        if pv.vg_name and pv.lv_uuid:
+                            lv = lvm.get_lv(vg_name=pv.vg_name, lv_uuid=pv.lv_uuid)
+                            if lv:
+                                self.lvs.append(lv)
         return self._is_lvm_member
 
+    def _get_pv_paths(self):
+        """
+        For block devices LVM can reside on the raw block device or on a
+        partition. Return a list of paths to be checked for a pv.
+        """
+        paths = [self.abspath]
+        path_dir = os.path.dirname(self.abspath)
+        for part in self.sys_api.get('partitions', {}).keys():
+            paths.append(os.path.join(path_dir, part))
+        return paths
+
     @property
     def exists(self):
         return os.path.exists(self.abspath)
 
+    @property
+    def has_gpt_headers(self):
+        return self.blkid_api.get("PTTYPE") == "gpt"
+
+    @property
+    def rotational(self):
+        return self.sys_api['rotational'] == '1'
+
+    @property
+    def model(self):
+        return self.sys_api['model']
+
+    @property
+    def size_human(self):
+        return self.sys_api['human_readable_size']
+
+    @property
+    def size(self):
+            return self.sys_api['size']
+
     @property
     def is_lvm_member(self):
         if self._is_lvm_member is None:
             self._set_lvm_membership()
         return self._is_lvm_member
 
+    @property
+    def is_ceph_disk_member(self):
+        return self.ceph_disk.is_member
+
     @property
     def is_mapper(self):
         return self.path.startswith('/dev/mapper')
@@ -96,3 +260,71 @@ class Device(object):
         if self.disk_api:
             return self.disk_api['TYPE'] == 'device'
         return False
+
+    @property
+    def used_by_ceph(self):
+        # only filter out data devices as journals could potentially be reused
+        osd_ids = [lv.tags.get("ceph.osd_id") is not None for lv in self.lvs
+                   if lv.tags.get("ceph.type") in ["data", "block"]]
+        return any(osd_ids)
+
+    def _check_reject_reasons(self):
+        """
+        This checks a number of potential reject reasons for a drive and
+        returns a tuple (boolean, list). The first element denotes whether a
+        drive is available or not, the second element lists reasons in case a
+        drive is not available.
+        """
+        reasons = [
+            ('removable', 1, 'removable'),
+            ('ro', 1, 'read-only'),
+            ('locked', 1, 'locked'),
+        ]
+        rejected = [reason for (k, v, reason) in reasons if
+                    self.sys_api.get(k, '') == v]
+        return len(rejected) == 0, rejected
+
+
+class CephDiskDevice(object):
+    """
+    Detect devices that have been created by ceph-disk, report their type
+    (journal, data, etc..). Requires a ``Device`` object as input.
+    """
+
+    def __init__(self, device):
+        self.device = device
+        self._is_ceph_disk_member = None
+
+    @property
+    def partlabel(self):
+        """
+        In containers, the 'PARTLABEL' attribute might not be detected
+        correctly via ``lsblk``, so we poke at the value with ``lsblk`` first,
+        falling back to ``blkid`` (which works correclty in containers).
+        """
+        lsblk_partlabel = self.device.disk_api.get('PARTLABEL')
+        if lsblk_partlabel:
+            return lsblk_partlabel
+        return self.device.blkid_api.get('PARTLABEL', '')
+
+    @property
+    def is_member(self):
+        if self._is_ceph_disk_member is None:
+            if 'ceph' in self.partlabel:
+                self._is_ceph_disk_member = True
+                return True
+            return False
+        return self._is_ceph_disk_member
+
+    @property
+    def type(self):
+        types = [
+            'data', 'wal', 'db', 'lockbox', 'journal',
+            # ceph-disk uses 'ceph block' when placing data in bluestore, but
+            # keeps the regular OSD files in 'ceph data' :( :( :( :(
+            'block',
+        ]
+        for t in types:
+            if t in self.partlabel:
+                return t
+        return 'unknown'