]> git.proxmox.com Git - ceph.git/blobdiff - ceph/src/ceph-volume/ceph_volume/util/disk.py
update sources to 12.2.8
[ceph.git] / ceph / src / ceph-volume / ceph_volume / util / disk.py
index 434723340258d405b6a87eedf1925e4dba27165e..053338972beea6d3bca415ee0652dd08833af16d 100644 (file)
@@ -1,6 +1,13 @@
+import logging
 import os
+import re
 import stat
 from ceph_volume import process
+from ceph_volume.api import lvm
+from ceph_volume.util.system import get_file_contents
+
+
+logger = logging.getLogger(__name__)
 
 
 # The blkid CLI tool has some oddities which prevents having one common call
@@ -221,3 +228,431 @@ def is_partition(dev):
     if os.path.exists('/sys/dev/block/%d:%d/partition' % (major, minor)):
         return True
     return False
+
+
+def _map_dev_paths(_path, include_abspath=False, include_realpath=False):
+    """
+    Go through all the items in ``_path`` and map them to their absolute path::
+
+        {'sda': '/dev/sda'}
+
+    If ``include_abspath`` is set, then a reverse mapping is set as well::
+
+        {'sda': '/dev/sda', '/dev/sda': 'sda'}
+
+    If ``include_realpath`` is set then the same operation is done for any
+    links found when listing, these are *not* reversed to avoid clashing on
+    existing keys, but both abspath and basename can be included. For example::
+
+        {
+            'ceph-data': '/dev/mapper/ceph-data',
+            '/dev/mapper/ceph-data': 'ceph-data',
+            '/dev/dm-0': '/dev/mapper/ceph-data',
+            'dm-0': '/dev/mapper/ceph-data'
+        }
+
+
+    In case of possible exceptions the mapping is returned empty, and the
+    exception is logged.
+    """
+    mapping = {}
+    try:
+        dev_names = os.listdir(_path)
+    except (OSError, IOError):
+        logger.exception('unable to list block devices from: %s' % _path)
+        return {}
+
+    for dev_name in dev_names:
+        mapping[dev_name] = os.path.join(_path, dev_name)
+
+    if include_abspath:
+        for k, v in list(mapping.items()):
+            mapping[v] = k
+
+    if include_realpath:
+        for abspath in list(mapping.values()):
+            if not os.path.islink(abspath):
+                continue
+
+            realpath = os.path.realpath(abspath)
+            basename = os.path.basename(realpath)
+            mapping[basename] = abspath
+            if include_abspath:
+                mapping[realpath] = abspath
+
+    return mapping
+
+
+def get_block_devs(sys_block_path="/sys/block", skip_loop=True):
+    """
+    Go through all the items in /sys/block and return them as a list.
+
+    The ``sys_block_path`` argument is set for easier testing and is not
+    required for proper operation.
+    """
+    devices = _map_dev_paths(sys_block_path).keys()
+    if skip_loop:
+        return [d for d in devices if not d.startswith('loop')]
+    return list(devices)
+
+
+def get_dev_devs(dev_path="/dev"):
+    """
+    Go through all the items in /dev and return them as a list.
+
+    The ``dev_path`` argument is set for easier testing and is not
+    required for proper operation.
+    """
+    return _map_dev_paths(dev_path, include_abspath=True)
+
+
+def get_mapper_devs(mapper_path="/dev/mapper"):
+    """
+    Go through all the items in /dev and return them as a list.
+
+    The ``dev_path`` argument is set for easier testing and is not
+    required for proper operation.
+    """
+    return _map_dev_paths(mapper_path, include_abspath=True, include_realpath=True)
+
+
+class BaseFloatUnit(float):
+    """
+    Base class to support float representations of size values. Suffix is
+    computed on child classes by inspecting the class name
+    """
+
+    def __repr__(self):
+        return "<%s(%s)>" % (self.__class__.__name__, self.__float__())
+
+    def __str__(self):
+        return "{size:.2f} {suffix}".format(
+            size=self.__float__(),
+            suffix=self.__class__.__name__.split('Float')[-1]
+        )
+
+    def as_int(self):
+        return int(self.real)
+
+    def as_float(self):
+        return self.real
+
+
+class FloatB(BaseFloatUnit):
+    pass
+
+
+class FloatMB(BaseFloatUnit):
+    pass
+
+
+class FloatGB(BaseFloatUnit):
+    pass
+
+
+class FloatKB(BaseFloatUnit):
+    pass
+
+
+class FloatTB(BaseFloatUnit):
+    pass
+
+
+class Size(object):
+    """
+    Helper to provide an interface for different sizes given a single initial
+    input. Allows for comparison between different size objects, which avoids
+    the need to convert sizes before comparison (e.g. comparing megabytes
+    against gigabytes).
+
+    Common comparison operators are supported::
+
+        >>> hd1 = Size(gb=400)
+        >>> hd2 = Size(gb=500)
+        >>> hd1 > hd2
+        False
+        >>> hd1 < hd2
+        True
+        >>> hd1 == hd2
+        False
+        >>> hd1 == Size(gb=400)
+        True
+
+    The Size object can also be multiplied or divided::
+
+        >>> hd1
+        <Size(400.00 GB)>
+        >>> hd1 * 2
+        <Size(800.00 GB)>
+        >>> hd1
+        <Size(800.00 GB)>
+
+    Additions and subtractions are only supported between Size objects::
+
+        >>> Size(gb=224) - Size(gb=100)
+        <Size(124.00 GB)>
+        >>> Size(gb=1) + Size(mb=300)
+        <Size(1.29 GB)>
+
+    Can also display a human-readable representation, with automatic detection
+    on best suited unit, or alternatively, specific unit representation::
+
+        >>> s = Size(mb=2211)
+        >>> s
+        <Size(2.16 GB)>
+        >>> s.mb
+        <FloatMB(2211.0)>
+        >>> print "Total size: %s" % s.mb
+        Total size: 2211.00 MB
+        >>> print "Total size: %s" % s
+        Total size: 2.16 GB
+    """
+
+    def __init__(self, multiplier=1024, **kw):
+        self._multiplier = multiplier
+        # create a mapping of units-to-multiplier, skip bytes as that is
+        # calculated initially always and does not need to convert
+        aliases = [
+            [('kb', 'kilobytes'), self._multiplier],
+            [('mb', 'megabytes'), self._multiplier ** 2],
+            [('gb', 'gigabytes'), self._multiplier ** 3],
+            [('tb', 'terabytes'), self._multiplier ** 4],
+        ]
+        # and mappings for units-to-formatters, including bytes and aliases for
+        # each
+        format_aliases = [
+            [('b', 'bytes'), FloatB],
+            [('kb', 'kilobytes'), FloatKB],
+            [('mb', 'megabytes'), FloatMB],
+            [('gb', 'gigabytes'), FloatGB],
+            [('tb', 'terabytes'), FloatTB],
+        ]
+        self._formatters = {}
+        for key, value in format_aliases:
+            for alias in key:
+                self._formatters[alias] = value
+        self._factors = {}
+        for key, value in aliases:
+            for alias in key:
+                self._factors[alias] = value
+
+        for k, v in kw.items():
+            self._convert(v, k)
+            # only pursue the first occurence
+            break
+
+    def _convert(self, size, unit):
+        """
+        Convert any size down to bytes so that other methods can rely on bytes
+        being available always, regardless of what they pass in, avoiding the
+        need for a mapping of every permutation.
+        """
+        if unit in ['b', 'bytes']:
+            self._b = size
+            return
+        factor = self._factors[unit]
+        self._b = float(size * factor)
+
+    def _get_best_format(self):
+        """
+        Go through all the supported units, and use the first one that is less
+        than 1024. This allows to represent size in the most readable format
+        available
+        """
+        for unit in ['b', 'kb', 'mb', 'gb', 'tb']:
+            if getattr(self, unit) > 1024:
+                continue
+            return getattr(self, unit)
+
+    def __repr__(self):
+        return "<Size(%s)>" % self._get_best_format()
+
+    def __str__(self):
+        return "%s" % self._get_best_format()
+
+    def __lt__(self, other):
+        return self._b < other._b
+
+    def __le__(self, other):
+        return self._b <= other._b
+
+    def __eq__(self, other):
+        return self._b == other._b
+
+    def __ne__(self, other):
+        return self._b != other._b
+
+    def __ge__(self, other):
+        return self._b >= other._b
+
+    def __gt__(self, other):
+        return self._b > other._b
+
+    def __add__(self, other):
+        if isinstance(other, Size):
+            _b = self._b + other._b
+            return Size(b=_b)
+        raise TypeError('Cannot add "Size" object with int')
+
+    def __sub__(self, other):
+        if isinstance(other, Size):
+            _b = self._b - other._b
+            return Size(b=_b)
+        raise TypeError('Cannot subtract "Size" object from int')
+
+    def __mul__(self, other):
+        if isinstance(other, Size):
+            raise TypeError('Cannot multiply with "Size" object')
+        _b = self._b * other
+        return Size(b=_b)
+
+    def __truediv__(self, other):
+        if isinstance(other, Size):
+            return self._b / other._b
+        self._b = self._b / other
+        return self
+
+    def __div__(self, other):
+        if isinstance(other, Size):
+            return self._b / other._b
+        self._b = self._b / other
+        return self
+
+    def __getattr__(self, unit):
+        """
+        Calculate units on the fly, relies on the fact that ``bytes`` has been
+        converted at instantiation. Units that don't exist will trigger an
+        ``AttributeError``
+        """
+        try:
+            formatter = self._formatters[unit]
+        except KeyError:
+            raise AttributeError('Size object has not attribute "%s"' % unit)
+        if unit in ['b', 'bytes']:
+            return formatter(self._b)
+        try:
+            factor = self._factors[unit]
+        except KeyError:
+            raise AttributeError('Size object has not attribute "%s"' % unit)
+        return formatter(float(self._b) / factor)
+
+
+def human_readable_size(size):
+    """
+    Take a size in bytes, and transform it into a human readable size with up
+    to two decimals of precision.
+    """
+    suffixes = ['B', 'KB', 'MB', 'GB', 'TB']
+    suffix_index = 0
+    while size > 1024:
+        suffix_index += 1
+        size = size / 1024.0
+    return "{size:.2f} {suffix}".format(
+        size=size,
+        suffix=suffixes[suffix_index])
+
+
+def get_partitions_facts(sys_block_path):
+    partition_metadata = {}
+    for folder in os.listdir(sys_block_path):
+        folder_path = os.path.join(sys_block_path, folder)
+        if os.path.exists(os.path.join(folder_path, 'partition')):
+            contents = get_file_contents(os.path.join(folder_path, 'partition'))
+            if '1' in contents:
+                part = {}
+                partname = folder
+                part_sys_block_path = os.path.join(sys_block_path, partname)
+
+                part['start'] = get_file_contents(part_sys_block_path + "/start", 0)
+                part['sectors'] = get_file_contents(part_sys_block_path + "/size", 0)
+
+                part['sectorsize'] = get_file_contents(
+                    part_sys_block_path + "/queue/logical_block_size")
+                if not part['sectorsize']:
+                    part['sectorsize'] = get_file_contents(
+                        part_sys_block_path + "/queue/hw_sector_size", 512)
+                part['size'] = human_readable_size(float(part['sectors']) * 512)
+
+                partition_metadata[partname] = part
+    return partition_metadata
+
+
+def is_mapper_device(device_name):
+    return device_name.startswith(('/dev/mapper', '/dev/dm-'))
+
+
+def get_devices(_sys_block_path='/sys/block', _dev_path='/dev', _mapper_path='/dev/mapper'):
+    """
+    Captures all available devices from /sys/block/, including its partitions,
+    along with interesting metadata like sectors, size, vendor,
+    solid/rotational, etc...
+
+    Returns a dictionary, where keys are the full paths to devices.
+
+    ..note:: dmapper devices get their path updated to what they link from, if
+            /dev/dm-0 is linked by /dev/mapper/ceph-data, then the latter gets
+            used as the key.
+
+    ..note:: loop devices, removable media, and logical volumes are never included.
+    """
+    # Portions of this detection process are inspired by some of the fact
+    # gathering done by Ansible in module_utils/facts/hardware/linux.py. The
+    # processing of metadata and final outcome *is very different* and fully
+    # imcompatible. There are ignored devices, and paths get resolved depending
+    # on dm devices, loop, and removable media
+
+    device_facts = {}
+
+    block_devs = get_block_devs(_sys_block_path)
+    dev_devs = get_dev_devs(_dev_path)
+    mapper_devs = get_mapper_devs(_mapper_path)
+
+    for block in block_devs:
+        sysdir = os.path.join(_sys_block_path, block)
+        metadata = {}
+
+        # Ensure that the diskname is an absolute path and that it never points
+        # to a /dev/dm-* device
+        diskname = mapper_devs.get(block) or dev_devs.get(block)
+
+        # If the mapper device is a logical volume it gets excluded
+        if is_mapper_device(diskname):
+            if lvm.is_lv(diskname):
+                continue
+
+        # If the device reports itself as 'removable', get it excluded
+        metadata['removable'] = get_file_contents(os.path.join(sysdir, 'removable'))
+        if metadata['removable'] == '1':
+            continue
+
+        for key in ['vendor', 'model', 'sas_address', 'sas_device_handle']:
+            metadata[key] = get_file_contents(sysdir + "/device/" + key)
+
+        for key in ['sectors', 'size']:
+            metadata[key] = get_file_contents(os.path.join(sysdir, key), 0)
+
+        for key, _file in [('support_discard', '/queue/discard_granularity')]:
+            metadata[key] = get_file_contents(os.path.join(sysdir, _file))
+
+        metadata['partitions'] = get_partitions_facts(sysdir)
+
+        metadata['rotational'] = get_file_contents(sysdir + "/queue/rotational")
+        metadata['scheduler_mode'] = ""
+        scheduler = get_file_contents(sysdir + "/queue/scheduler")
+        if scheduler is not None:
+            m = re.match(r".*?(\[(.*)\])", scheduler)
+            if m:
+                metadata['scheduler_mode'] = m.group(2)
+
+        if not metadata['sectors']:
+            metadata['sectors'] = 0
+        size = metadata['sectors'] or metadata['size']
+        metadata['sectorsize'] = get_file_contents(sysdir + "/queue/logical_block_size")
+        if not metadata['sectorsize']:
+            metadata['sectorsize'] = get_file_contents(sysdir + "/queue/hw_sector_size", 512)
+        metadata['human_readable_size'] = human_readable_size(float(size) * 512)
+        metadata['size'] = float(size) * 512
+        metadata['path'] = diskname
+
+        device_facts[diskname] = metadata
+    return device_facts