+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
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