diff --git a/blivet/blivet.py b/blivet/blivet.py index f4ce6a48a..48b7f5ecc 100644 --- a/blivet/blivet.py +++ b/blivet/blivet.py @@ -31,6 +31,7 @@ from .devices import BTRFSSubVolumeDevice, BTRFSVolumeDevice from .devices import LVMLogicalVolumeDevice, LVMVolumeGroupDevice from .devices import MDRaidArrayDevice, PartitionDevice, TmpFSDevice, device_path_to_name +from .devices import StratisPoolDevice, StratisFilesystemDevice from .deviceaction import ActionCreateDevice, ActionCreateFormat, ActionDestroyDevice from .deviceaction import ActionDestroyFormat, ActionResizeDevice, ActionResizeFormat from .devicelibs.edd import get_edd_dict @@ -313,6 +314,17 @@ def btrfs_volumes(self): return sorted((d for d in self.devices if d.type == "btrfs volume"), key=lambda d: d.name) + @property + def stratis_pools(self): + """ A list of the Stratis pools in the device tree. + + This is based on the current state of the device tree and + does not necessarily reflect the actual on-disk state of the + system's disks. + """ + return sorted((d for d in self.devices if d.type == "stratis pool"), + key=lambda d: d.name) + @property def swaps(self): """ A list of the swap devices in the device tree. @@ -770,6 +782,77 @@ def new_btrfs_sub_volume(self, *args, **kwargs): kwargs["subvol"] = True return self.new_btrfs(*args, **kwargs) + def new_stratis_pool(self, *args, **kwargs): + """ Return a new StratisPoolDevice instance. + + :returns: the new Stratis pool device + :rtype: :class:`~.devices.StratisPoolDevice` + + All arguments are passed on to the + :class:`~.devices.StratisPoolDevice` constructor. + + If a name is not specified, one will be generated based on the + hostname, and/or product name. + """ + blockdevs = kwargs.pop("parents", []) + + name = kwargs.pop("name", None) + if name: + safe_name = self.safe_device_name(name, devicefactory.DEVICE_TYPE_STRATIS) + if safe_name != name: + log.warning("using '%s' instead of specified name '%s'", + safe_name, name) + name = safe_name + else: + name = self.suggest_container_name(container_type=devicefactory.DEVICE_TYPE_STRATIS) + + if name in self.names: + raise ValueError("name '%s' is already in use" % name) + + return StratisPoolDevice(name, parents=blockdevs, *args, **kwargs) + + def new_stratis_filesystem(self, *args, **kwargs): + """ Return a new StratisFilesystemDevice instance. + + :keyword mountpoint: mountpoint for filesystem + :type mountpoint: str + :returns: the new device + :rtype: :class:`~.devices.StratisFilesystemDevice` + + All other arguments are passed on to the appropriate + :class:`~.devices.StratisFilesystemDevice` constructor. + + If a name is not specified, one will be generated based on the + format type and/or mountpoint. + """ + pool = kwargs.get("parents", [None])[0] + + mountpoint = kwargs.pop("mountpoint", None) + name = kwargs.pop("name", None) + if name: + # make sure the specified name is sensible + full_name = "%s/%s" % (pool.name, name) + safe_name = self.safe_device_name(full_name, devicefactory.DEVICE_TYPE_STRATIS) + if safe_name != full_name: + new_name = safe_name[len(pool.name) + 1:] + log.warning("using '%s' instead of specified name '%s'", + new_name, name) + name = new_name + else: + name = self.suggest_device_name(parent=pool, + mountpoint=mountpoint, + device_type=devicefactory.DEVICE_TYPE_STRATIS) + + if "%s/%s" % (pool.name, name) in self.names: + raise ValueError("name '%s' is already in use" % name) + + device = StratisFilesystemDevice(name, *args, **kwargs) + + # XFS will be created automatically on the device so lets just add it here + device.format = get_format("stratis xfs", mountpoint=mountpoint) + + return device + def new_tmp_fs(self, *args, **kwargs): """ Return a new TmpFSDevice. """ return TmpFSDevice(*args, **kwargs) @@ -917,6 +1000,8 @@ def safe_device_name(self, name, device_type=None): allowed = devicelibs.mdraid.safe_name_characters elif device_type == devicefactory.DEVICE_TYPE_BTRFS: allowed = devicelibs.btrfs.safe_name_characters + elif device_type == devicefactory.DEVICE_TYPE_STRATIS: + allowed = devicelibs.stratis.safe_name_characters else: allowed = "0-9a-zA-Z._-" @@ -940,17 +1025,23 @@ def safe_device_name(self, name, device_type=None): return tmp - def unique_device_name(self, name, parent=None, name_set=True): + def unique_device_name(self, name, parent=None, name_set=True, device_type=None): """ Turn given name into a unique one by adding numeric suffix to it """ + + if device_type == devicefactory.DEVICE_TYPE_STRATIS: + parent_separator = "/" + else: + parent_separator = "-" + if name_set: - if parent and "%s-%s" % (parent.name, name) not in self.names: + if parent and "%s%s%s" % (parent.name, parent_separator, name) not in self.names: return name elif not parent and name not in self.names: return name for suffix in range(100): if parent: - if "%s-%s%02d" % (parent.name, name, suffix) not in self.names: + if "%s%s%s%02d" % (parent.name, parent_separator, name, suffix) not in self.names: return "%s%02d" % (name, suffix) else: if "%s%02d" % (name, suffix) not in self.names: @@ -974,7 +1065,7 @@ def suggest_container_name(self, prefix="", container_type=None): name = self._get_container_name_template(prefix=prefix) if name in self.names: try: - name = self.unique_device_name(name) + name = self.unique_device_name(name, device_type=container_type) except RuntimeError: log.error("failed to create device name based on template '%s'", name) raise @@ -982,7 +1073,8 @@ def suggest_container_name(self, prefix="", container_type=None): return name def suggest_device_name(self, parent=None, swap=None, - mountpoint=None, prefix=""): + mountpoint=None, prefix="", + device_type=None): """ Return a suitable, unused name for a new device. :keyword parent: the parent device @@ -1008,12 +1100,18 @@ def suggest_device_name(self, parent=None, swap=None, if prefix and body: body = "_" + body - name = self.safe_device_name(prefix + body) - full_name = "%s-%s" % (parent.name, name) if parent else name + name = self.safe_device_name(prefix + body, device_type) + + if device_type == devicefactory.DEVICE_TYPE_STRATIS: + parent_separator = "/" + else: + parent_separator = "-" + + full_name = "%s%s%s" % (parent.name, parent_separator, name) if parent else name if full_name in self.names or not body: try: - name = self.unique_device_name(name, parent, bool(body)) + name = self.unique_device_name(name, parent, bool(body), device_type) except RuntimeError: log.error("failed to create device name based on parent '%s', " "prefix '%s', mountpoint '%s', swap '%s'", diff --git a/blivet/devicefactory.py b/blivet/devicefactory.py index efadd2559..e6b8c1fad 100644 --- a/blivet/devicefactory.py +++ b/blivet/devicefactory.py @@ -28,6 +28,7 @@ from .devices import LUKSDevice, LVMLogicalVolumeDevice from .devices import PartitionDevice, MDRaidArrayDevice from .devices.lvm import LVMVDOPoolMixin, LVMVDOLogicalVolumeMixin, DEFAULT_THPOOL_RESERVE +from .devices import StratisFilesystemDevice, StratisPoolDevice from .formats import get_format from .devicelibs import btrfs from .devicelibs import mdraid @@ -59,6 +60,7 @@ DEVICE_TYPE_DISK = 4 DEVICE_TYPE_LVM_THINP = 5 DEVICE_TYPE_LVM_VDO = 6 +DEVICE_TYPE_STRATIS = 7 def is_supported_device_type(device_type): @@ -83,6 +85,8 @@ def is_supported_device_type(device_type): devices = [MDRaidArrayDevice] elif device_type == DEVICE_TYPE_LVM_VDO: devices = [LVMLogicalVolumeDevice, LVMVDOPoolMixin, LVMVDOLogicalVolumeMixin] + elif device_type == DEVICE_TYPE_STRATIS: + devices = [StratisFilesystemDevice, StratisPoolDevice] return not any(c.unavailable_type_dependencies() for c in devices) @@ -123,7 +127,8 @@ def get_device_type(device): "lvmvdopool": DEVICE_TYPE_LVM, "btrfs subvolume": DEVICE_TYPE_BTRFS, "btrfs volume": DEVICE_TYPE_BTRFS, - "mdarray": DEVICE_TYPE_MD} + "mdarray": DEVICE_TYPE_MD, + "stratis filesystem": DEVICE_TYPE_STRATIS} use_dev = device.raw_device if use_dev.is_disk: @@ -142,7 +147,8 @@ def get_device_factory(blivet, device_type=DEVICE_TYPE_LVM, **kwargs): DEVICE_TYPE_MD: MDFactory, DEVICE_TYPE_LVM_THINP: LVMThinPFactory, DEVICE_TYPE_LVM_VDO: LVMVDOFactory, - DEVICE_TYPE_DISK: DeviceFactory} + DEVICE_TYPE_DISK: DeviceFactory, + DEVICE_TYPE_STRATIS: StratisFactory} factory_class = class_table[device_type] log.debug("instantiating %s: %s, %s, %s", factory_class, @@ -378,6 +384,9 @@ def __init__(self, storage, **kwargs): self.__actions = [] self.__roots = [] + def _is_container_encrypted(self): + return all(isinstance(p, LUKSDevice) for p in self.device.container.parents) + def _update_defaults_from_device(self): """ Update default settings based on passed in device, if provided. """ if self.device is None: @@ -408,8 +417,7 @@ def _update_defaults_from_device(self): len(self.device.container.pvs) == 1 and hasattr(self.device.container.pvs[0].raw_device, "level")): self.container_raid_level = self.device.container.pvs[0].raw_device.level - self.container_encrypted = all(isinstance(p, LUKSDevice) - for p in self.device.container.parents) + self.container_encrypted = self._is_container_encrypted() @property def encrypted(self): @@ -597,6 +605,8 @@ def get_container(self, device=None, name=None, allow_existing=False): container = device.volume elif hasattr(device, "subvolumes"): container = device + elif device.type == "stratis filesystem": + container = device.pool elif name: for c in self.storage.devices: if c.name == name and c in self.container_list: @@ -2013,3 +2023,146 @@ def _reconfigure_device(self): return super(BTRFSFactory, self)._reconfigure_device() + + +class StratisFactory(DeviceFactory): + + """ Factory for creating Stratis filesystems with partition block devices. """ + child_factory_class = PartitionSetFactory + child_factory_fstype = "stratis" + size_set_class = TotalSizeSet + + def __init__(self, storage, **kwargs): + # override default size policy, AUTO doesn't make sense for Stratis + self._default_settings["container_size"] = SIZE_POLICY_MAX + super(StratisFactory, self).__init__(storage, **kwargs) + + @property + def pool(self): + return self.container + + @property + def container_list(self): + return self.storage.stratis_pools[:] + + def _reconfigure_container(self): + """ Reconfigure a defined container required by this factory device. """ + if getattr(self.container, "exists", False): + return + + self.container.encrypted = self.container_encrypted or False + self._set_container_members() + + # + # methods related to device size and disk space requirements + # + def _get_total_space(self): + """ Return the total space need for this factory's device/container. + + This is used for the size argument to the child factory constructor + and also to construct the size set in PartitionSetFactory.configure. + """ + + if self.container_size == SIZE_POLICY_AUTO: + raise DeviceFactoryError("Automatic size is not support for Stratis pools") + elif self.container_size == SIZE_POLICY_MAX: + space = Size(0) + # grow the container as large as possible + if self.pool: + space += sum(p.size for p in self.pool.parents) + log.debug("size bumped to %s to include Stratis pool parents", space) + + space += self._get_free_disk_space() + log.debug("size bumped to %s to include free disk space", space) + + return space + else: + return self.container_size + + def _normalize_size(self): + pass + + def _handle_no_size(self): + """ Set device size so that it grows to the largest size possible. """ + + def _get_new_container(self, *args, **kwargs): + if self.container_encrypted: + kwargs["encrypted"] = True + else: + kwargs["encrypted"] = False + return self.storage.new_stratis_pool(*args, **kwargs) + + # + # methods to configure the factory's device + # + def _create_device(self): + """ Create the factory device. """ + if self.device_name: + kwa = {"name": self.device_name} + else: + kwa = {} + + parents = self._get_parent_devices() + try: + # pylint: disable=assignment-from-no-return + device = self._get_new_device(parents=parents, + mountpoint=self.mountpoint, + **kwa) + except (StorageError, ValueError) as e: + log.error("device instance creation failed: %s", e) + raise + + self.storage.create_device(device) + try: + self._post_create() + except (StorageError, blockdev.BlockDevError) as e: + log.error("device post-create method failed: %s", e) + self.storage.destroy_device(device) + raise_from(StorageError(e), e) + else: + if not device.size: + self.storage.destroy_device(device) + raise StorageError("failed to create device") + + self.device = device + + def _get_new_device(self, *args, **kwargs): + """ Create and return the factory device as a StorageDevice. """ + + name = kwargs.pop("name", "") + kwargs["name"] = self.storage.unique_device_name(name, parent=self.pool, name_set=True) + + return self.storage.new_stratis_filesystem(*args, **kwargs) + + def _set_name(self): + if not self.device_name: + # pylint: disable=attribute-defined-outside-init + self.device_name = self.storage.suggest_device_name( + parent=self.pool, + swap=False, + mountpoint=self.mountpoint) + + fsname = "%s/%s" % (self.pool.name, self.device_name) + safe_new_name = self.storage.safe_device_name(fsname, DEVICE_TYPE_STRATIS) + if self.device.name != safe_new_name: + if safe_new_name in self.storage.names: + log.error("not renaming '%s' to in-use name '%s'", + self.device.name, safe_new_name) + return + + if not safe_new_name.startswith(self.pool.name): + log.error("device rename failure (%s)", safe_new_name) + return + + # strip off the vg name before setting + safe_new_name = safe_new_name[len(self.pool.name) + 1:] + log.debug("renaming device '%s' to '%s'", + self.device.name, safe_new_name) + self.raw_device.name = safe_new_name + + def _configure(self): + self._set_container() # just sets self.container based on the specs + super(StratisFactory, self)._configure() + + def _is_container_encrypted(self): + return self.container.encrypted diff --git a/blivet/devicelibs/stratis.py b/blivet/devicelibs/stratis.py new file mode 100644 index 000000000..c494020fe --- /dev/null +++ b/blivet/devicelibs/stratis.py @@ -0,0 +1,240 @@ +# +# stratis.py +# stratis functions +# +# Copyright (C) 2020 Red Hat, Inc. All rights reserved. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# +# Author(s): Vojtech Trefny +# + +import gi +gi.require_version("GLib", "2.0") +gi.require_version("Gio", "2.0") + +from gi.repository import GLib, Gio + +import json +import os +import shutil + +from ..errors import StratisError +from ..size import Size +from ..static_data import stratis_info +from .. import safe_dbus +from .. import util + + +STRATIS_SERVICE = "org.storage.stratis2" +STRATIS_PATH = "/org/storage/stratis2" +STRATIS_POOL_INTF = STRATIS_SERVICE + ".pool" +STRATIS_FILESYSTEM_INTF = STRATIS_SERVICE + ".filesystem" +STRATIS_BLOCKDEV_INTF = STRATIS_SERVICE + ".blockdev" +STRATIS_PROPS_INTF = STRATIS_SERVICE + ".FetchProperties" +STRATIS_MANAGER_INTF = STRATIS_SERVICE + ".Manager.r2" + + +STRATIS_FS_SIZE = Size("1 TiB") +STRATIS_FS_MD_SIZE = Size("600 MiB") + +STRATIS_BD_MD_SIZE = Size("4 MiB") +STRATIS_BD_ENC_MD_SIZE = Size("16 MiB") + + +STRATIS_PREDICT_USAGE = "stratis-predict-usage" + + +safe_name_characters = "0-9a-zA-Z._-" + + +def pool_used(pool_name, blockdevs, encrypted=False): + if not shutil.which(STRATIS_PREDICT_USAGE): + raise StratisError("Utility for predicting stratis pool usage '%s' not available" % STRATIS_PREDICT_USAGE) + + cmd = [STRATIS_PREDICT_USAGE] + cmd.extend(blockdevs) + if encrypted: + cmd.append("--encrypted") + + rc, out = util.run_program_and_capture_output(cmd) + if rc: + raise StratisError("Failed to predict usage for stratis pool %s" % pool_name) + + try: + pred = json.loads(out) + except json.JSONDecodeError as e: + raise StratisError("Failed to get stratis pool usage") from e + + return Size(pred["used"]) + + +def remove_pool(pool_uuid): + if not safe_dbus.check_object_available(STRATIS_SERVICE, STRATIS_PATH): + raise StratisError("Stratis DBus service not available") + + # repopulate the stratis info cache just to be sure all values are still valid + stratis_info.drop_cache() + + if pool_uuid not in stratis_info.pools.keys(): + raise StratisError("Stratis pool with UUID %s not found" % pool_uuid) + + pool_info = stratis_info.pools[pool_uuid] + + try: + (succ, _uuid), rc, err = safe_dbus.call_sync(STRATIS_SERVICE, + STRATIS_PATH, + STRATIS_MANAGER_INTF, + "DestroyPool", + GLib.Variant("(o)", (pool_info.object_path,))) + except safe_dbus.DBusCallError as e: + raise StratisError("Failed to remove stratis pool: %s" % str(e)) + else: + if not succ: + raise StratisError("Failed to remove stratis pool: %s (%d)" % (err, rc)) + + +def remove_filesystem(pool_uuid, fs_uuid): + if not safe_dbus.check_object_available(STRATIS_SERVICE, STRATIS_PATH): + raise StratisError("Stratis DBus service not available") + + # repopulate the stratis info cache just to be sure all values are still valid + stratis_info.drop_cache() + + if pool_uuid not in stratis_info.pools.keys(): + raise StratisError("Stratis pool with UUID %s not found" % pool_uuid) + if fs_uuid not in stratis_info.filesystems.keys(): + raise StratisError("Stratis filesystem with UUID %s not found" % fs_uuid) + + pool_info = stratis_info.pools[pool_uuid] + fs_info = stratis_info.filesystems[fs_uuid] + + try: + (succ, _uuid), rc, err = safe_dbus.call_sync(STRATIS_SERVICE, + pool_info.object_path, + STRATIS_POOL_INTF, + "DestroyFilesystems", + GLib.Variant("(ao)", ([fs_info.object_path],))) + except safe_dbus.DBusCallError as e: + raise StratisError("Failed to remove stratis filesystem: %s" % str(e)) + else: + if not succ: + raise StratisError("Failed to remove stratis filesystem: %s (%d)" % (err, rc)) + + +def set_key(key_desc, passphrase, key_file): + if passphrase: + (read, write) = os.pipe() + os.write(write, passphrase.encode("utf-8")) + fd = read + elif key_file: + fd = os.open(key_file, os.O_RDONLY) + + fd_list = Gio.UnixFDList() + fd_list.append(fd) + + try: + ((_changed, _set), rc, err) = safe_dbus.call_sync(STRATIS_SERVICE, + STRATIS_PATH, + STRATIS_MANAGER_INTF, + "SetKey", + GLib.Variant("(shb)", (key_desc, 0, False)), fds=fd_list) + except safe_dbus.DBusCallError as e: + raise StratisError("Failed to set key for new pool: %s" % str(e)) + else: + if rc != 0: + raise StratisError("Failed to set key for new pool: %s (%d)" % (err, rc)) + finally: + if key_file: + os.close(fd) + if passphrase: + os.close(write) + + +def unlock_pool(pool_uuid): + if not safe_dbus.check_object_available(STRATIS_SERVICE, STRATIS_PATH): + raise StratisError("Stratis DBus service not available") + + try: + (succ, err, _blockdevs) = safe_dbus.call_sync(STRATIS_SERVICE, + STRATIS_PATH, + STRATIS_MANAGER_INTF, + "UnlockPool", + GLib.Variant("(ss)", (pool_uuid, "keyring"))) + except safe_dbus.DBusCallError as e: + raise StratisError("Failed to unlock pool: %s" % str(e)) + else: + if not succ: + raise StratisError("Failed to unlock pool: %s" % err) + + +def create_pool(name, devices, encrypted, passphrase, key_file): + if not safe_dbus.check_object_available(STRATIS_SERVICE, STRATIS_PATH): + raise StratisError("Stratis DBus service not available") + + if encrypted and not (passphrase or key_file): + raise StratisError("Passphrase or key file must be specified for encrypted pool") + + raid_opt = GLib.Variant("(bq)", (False, 0)) + + if encrypted: + key_desc = "blivet-%s" % name # XXX what would be a good key description? + set_key(key_desc, passphrase, key_file) + key_opt = GLib.Variant("(bs)", (True, key_desc)) + else: + key_opt = GLib.Variant("(bs)", (False, "")) + + try: + ((succ, _paths), rc, err) = safe_dbus.call_sync(STRATIS_SERVICE, + STRATIS_PATH, + STRATIS_MANAGER_INTF, + "CreatePool", + GLib.Variant("(s(bq)as(bs))", (name, raid_opt, + devices, key_opt))) + except safe_dbus.DBusCallError as e: + raise StratisError("Failed to create stratis pool: %s" % str(e)) + else: + if not succ: + raise StratisError("Failed to create stratis pool: %s (%d)" % (err, rc)) + + # repopulate the stratis info cache so the new pool will be added + stratis_info.drop_cache() + + +def create_filesystem(name, pool_uuid): + if not safe_dbus.check_object_available(STRATIS_SERVICE, STRATIS_PATH): + raise StratisError("Stratis DBus service not available") + + # repopulate the stratis info cache just to be sure all values are still valid + stratis_info.drop_cache() + + if pool_uuid not in stratis_info.pools.keys(): + raise StratisError("Stratis pool with UUID %s not found" % pool_uuid) + + pool_info = stratis_info.pools[pool_uuid] + + try: + ((succ, _paths), rc, err) = safe_dbus.call_sync(STRATIS_SERVICE, + pool_info.object_path, + STRATIS_POOL_INTF, + "CreateFilesystems", + GLib.Variant("(as)", ([name],))) + except safe_dbus.DBusCallError as e: + raise StratisError("Failed to create stratis filesystem on '%s': %s" % (pool_info.name, str(e))) + else: + if not succ: + raise StratisError("Failed to create stratis filesystem on '%s': %s (%d)" % (pool_info.name, err, rc)) + + # repopulate the stratis info cache so the new filesystem will be added + stratis_info.drop_cache() diff --git a/blivet/devices/__init__.py b/blivet/devices/__init__.py index 984e5af66..8bb0a979e 100644 --- a/blivet/devices/__init__.py +++ b/blivet/devices/__init__.py @@ -35,3 +35,4 @@ from .nodev import NoDevice, TmpFSDevice from .network import NetworkStorageDevice from .nfs import NFSDevice +from .stratis import StratisPoolDevice, StratisFilesystemDevice diff --git a/blivet/devices/stratis.py b/blivet/devices/stratis.py new file mode 100644 index 000000000..baa41f257 --- /dev/null +++ b/blivet/devices/stratis.py @@ -0,0 +1,267 @@ +# devices/stratis.py +# +# Copyright (C) 2020 Red Hat, Inc. +# +# This copyrighted material is made available to anyone wishing to use, +# modify, copy, or redistribute it subject to the terms and conditions of +# the GNU General Public License v.2, or (at your option) any later version. +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY expressed or implied, including the implied warranties of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General +# Public License for more details. You should have received a copy of the +# GNU General Public License along with this program; if not, write to the +# Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA +# 02110-1301, USA. Any Red Hat trademarks that are incorporated in the +# source code or documentation are not subject to the GNU General Public +# License and may only be used or replicated with the express permission of +# Red Hat, Inc. +# +# Red Hat Author(s): Vojtech Trefny +# + +import os + +import logging +log = logging.getLogger("blivet") + +from .storage import StorageDevice +from ..static_data import stratis_info +from ..storage_log import log_method_call +from ..errors import DeviceError, StratisError +from ..size import Size +from .. import devicelibs + + +class StratisPoolDevice(StorageDevice): + """ A stratis pool device """ + + _type = "stratis pool" + _resizable = False + _packages = ["stratisd", "stratis-cli"] + _dev_dir = "/dev/stratis" + _format_immutable = True + + def __init__(self, *args, **kwargs): + """ + :encrypted: whether this pool is encrypted or not + :type encrypted: bool + :keyword passphrase: device passphrase + :type passphrase: str + :keyword key_file: path to a file containing a key + :type key_file: str + """ + self._encrypted = kwargs.pop("encrypted", False) + self.__passphrase = kwargs.pop("passphrase", None) + self._key_file = kwargs.pop("key_file", None) + + super(StratisPoolDevice, self).__init__(*args, **kwargs) + + @property + def blockdevs(self): + """ A list of this pool block devices """ + return self.parents[:] + + @property + def filesystems(self): + """ A list of this pool block filesystems """ + return self.children[:] + + @property + def size(self): + """ The size of this pool """ + # sum up the sizes of the block devices + return sum(parent.size for parent in self.parents) + + @property + def _physical_size(self): + if self.exists: + pool_info = stratis_info.get_pool_info(self.name) + if not pool_info: + raise DeviceError("Failed to get information about pool %s" % self.name) + return pool_info.physical_size + else: + return self.size + + @property + def _physical_used(self): + physical_used = Size(0) + + # filesystems + for filesystem in self.filesystems: + physical_used += filesystem.used_size + + if self.exists: + # pool metadata using stratis-predict-usage which will give us used size + # for an empty pool with blockdevs we are using, i.e. metadata size for this pool + physical_used += devicelibs.stratis.pool_used(self.name, + [bd.path for bd in self.blockdevs], + self.encrypted) + else: + for bd in self.blockdevs: + if bd.exists: + # for existing blockdevs we can also use the stratis-predict-usage tool + physical_used += devicelibs.stratis.pool_used(self.name, + [bd.path], + self.encrypted) + else: + physical_used += devicelibs.stratis.STRATIS_BD_MD_SIZE + if self.encrypted: + physical_used += devicelibs.stratis.STRATIS_BD_ENC_MD_SIZE + + return physical_used + + @property + def free_space(self): + """ Free space in the pool usable for new filesystems """ + return self._physical_size - self._physical_used + + @property + def encrypted(self): + """ True if this device is encrypted. """ + return self._encrypted + + @encrypted.setter + def encrypted(self, encrypted): + self._encrypted = encrypted + + @property + def key_file(self): + """ Path to key file to be used in /etc/crypttab """ + return self._key_file + + def _set_passphrase(self, passphrase): + """ Set the passphrase used to access this device. """ + self.__passphrase = passphrase + + passphrase = property(fset=_set_passphrase) + + @property + def has_key(self): + return ((self.__passphrase not in ["", None]) or + (self._key_file and os.access(self._key_file, os.R_OK))) + + def _pre_create(self): + super(StratisPoolDevice, self)._pre_create() + + if self.encrypted and not self.has_key: + raise StratisError("cannot create encrypted stratis pool without key") + + def _create(self): + """ Create the device. """ + log_method_call(self, self.name, status=self.status) + bd_list = [bd.path for bd in self.parents] + devicelibs.stratis.create_pool(name=self.name, + devices=bd_list, + encrypted=self.encrypted, + passphrase=self.__passphrase, + key_file=self._key_file) + + def _post_create(self): + super(StratisPoolDevice, self)._post_create() + self.format.exists = True + + pool_info = stratis_info.get_pool_info(self.name) + if not pool_info: + raise DeviceError("Failed to get information about newly created pool %s" % self.name) + self.uuid = pool_info.uuid + + for parent in self.parents: + parent.format.pool_name = self.name + parent.format.pool_uuid = self.uuid + + def _destroy(self): + """ Destroy the device. """ + log_method_call(self, self.name, status=self.status) + devicelibs.stratis.remove_pool(self.uuid) + + def add_hook(self, new=True): + super(StratisPoolDevice, self).add_hook(new=new) + if new: + return + + for parent in self.parents: + parent.format.pool_name = self.name + parent.format.pool_uuid = self.uuid + + def remove_hook(self, modparent=True): + if modparent: + for parent in self.parents: + parent.format.pool_name = None + parent.format.pool_uuid = None + + super(StratisPoolDevice, self).remove_hook(modparent=modparent) + + def dracut_setup_args(self): + return set(["stratis.rootfs.pool_uuid=%s" % self.uuid]) + + +class StratisFilesystemDevice(StorageDevice): + """ A stratis pool device """ + + _type = "stratis filesystem" + _resizable = False + _packages = ["stratisd", "stratis-cli"] + _dev_dir = "/dev/stratis" + + def __init__(self, *args, **kwargs): + if kwargs.get("size") is None and not kwargs.get("exists"): + kwargs["size"] = devicelibs.stratis.STRATIS_FS_SIZE + + super(StratisFilesystemDevice, self).__init__(*args, **kwargs) + + if not self.exists and self.pool.free_space <= Size(0): + raise StratisError("cannot create new stratis filesystem, not enough free space in the pool") + + def _get_name(self): + """ This device's name. """ + if self.pool is not None: + return "%s/%s" % (self.pool.name, self._name) + else: + return super(StratisFilesystemDevice, self)._get_name() + + @property + def fsname(self): + """ The Stratis filesystem name (not including pool name). """ + return self._name + + @property + def pool(self): + if not self.parents: + # this should never happen but just to be sure + return None + + return self.parents[0] + + @property + def used_size(self): + """ Size used by this filesystem in the pool """ + if not self.exists: + return devicelibs.stratis.STRATIS_FS_MD_SIZE + else: + fs_info = stratis_info.get_filesystem_info(self.pool.name, self.fsname) + if not fs_info: + raise DeviceError("Failed to get information about filesystem %s" % self.name) + return fs_info.used_size + + def _create(self): + """ Create the device. """ + log_method_call(self, self.name, status=self.status) + devicelibs.stratis.create_filesystem(self.fsname, self.pool.uuid) + + def _post_create(self): + super(StratisFilesystemDevice, self)._post_create() + + fs_info = stratis_info.get_filesystem_info(self.pool.name, self.fsname) + if not fs_info: + raise DeviceError("Failed to get information about newly created filesystem %s" % self.name) + self.uuid = fs_info.uuid + + self.format.pool_uuid = fs_info.pool_uuid + + def _destroy(self): + """ Destroy the device. """ + log_method_call(self, self.name, status=self.status) + devicelibs.stratis.remove_filesystem(self.pool.uuid, self.uuid) + + def dracut_setup_args(self): + return set(["root=%s" % self.path]) diff --git a/blivet/errors.py b/blivet/errors.py index fd51283f7..8a26eadda 100644 --- a/blivet/errors.py +++ b/blivet/errors.py @@ -185,6 +185,11 @@ class BTRFSError(StorageError): class BTRFSValueError(BTRFSError, ValueError): pass + +class StratisError(StorageError): + pass + + # DeviceTree diff --git a/blivet/formats/__init__.py b/blivet/formats/__init__.py index 0dbbae936..eb62e5887 100644 --- a/blivet/formats/__init__.py +++ b/blivet/formats/__init__.py @@ -742,4 +742,4 @@ def populate_ksdata(self, data): register_device_format(DeviceFormat) # import the format modules (which register their device formats) -from . import biosboot, disklabel, dmraid, fslib, fs, luks, lvmpv, mdraid, multipath, prepboot, swap +from . import biosboot, disklabel, dmraid, fslib, fs, luks, lvmpv, mdraid, multipath, prepboot, swap, stratis diff --git a/blivet/formats/fs.py b/blivet/formats/fs.py index 23c6f968a..14d7c858f 100644 --- a/blivet/formats/fs.py +++ b/blivet/formats/fs.py @@ -1132,6 +1132,45 @@ def write_uuid(self): register_device_format(XFS) +class StratisXFS(XFS): + """ XFS on top of Stratis filesystem device """ + + _type = "stratis xfs" + + _mount_class = fsmount.StratisXFSMount + _resize_class = fsresize.UnimplementedFSResize + _size_info_class = fssize.UnimplementedFSSize + _info_class = fsinfo.UnimplementedFSInfo + _minsize_class = fsminsize.UnimplementedFSMinSize + _writelabel_class = fswritelabel.UnimplementedFSWriteLabel + _writeuuid_class = fswriteuuid.UnimplementedFSWriteUUID + + def __init__(self, **kwargs): + super(StratisXFS, self).__init__(**kwargs) + self.pool_uuid = kwargs.pop("pool_uuid", None) + + def _get_options(self): + opts = super(StratisXFS, self)._get_options() + if self.mountpoint != "/": + stratis_opts = "x-systemd.requires=stratis-fstab-setup@%s," \ + "x-systemd.after=stratis-fstab-setup@%s" % (self.pool_uuid, + self.pool_uuid) + else: + stratis_opts = None + return ",".join(o for o in (opts, stratis_opts) if o) + + def _create(self, **kwargs): # pylint: disable=unused-argument + # format is created together with the stratis filesystem device + pass + + def _destroy(self, **kwargs): # pylint: disable=unused-argument + # format is destroyed together with the stratis filesystem device + pass + + +register_device_format(StratisXFS) + + class HFS(FS): _type = "hfs" _modules = ["hfs"] diff --git a/blivet/formats/stratis.py b/blivet/formats/stratis.py new file mode 100644 index 000000000..648c4cac4 --- /dev/null +++ b/blivet/formats/stratis.py @@ -0,0 +1,129 @@ +# stratis.py +# Device format classes for anaconda's storage configuration module. +# +# Copyright (C) 2020 Red Hat, Inc. +# +# This copyrighted material is made available to anyone wishing to use, +# modify, copy, or redistribute it subject to the terms and conditions of +# the GNU General Public License v.2, or (at your option) any later version. +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY expressed or implied, including the implied warranties of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General +# Public License for more details. You should have received a copy of the +# GNU General Public License along with this program; if not, write to the +# Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA +# 02110-1301, USA. Any Red Hat trademarks that are incorporated in the +# source code or documentation are not subject to the GNU General Public +# License and may only be used or replicated with the express permission of +# Red Hat, Inc. +# +# Red Hat Author(s): Vojtech Trefny +# + +import os + +from ..storage_log import log_method_call +from ..i18n import N_ +from ..size import Size +from ..errors import StratisError +from ..devicelibs import stratis +from . import DeviceFormat, register_device_format + +import logging +log = logging.getLogger("blivet") + + +class StratisBlockdev(DeviceFormat): + """ A Stratis block device """ + + _type = "stratis" + _name = N_("Stratis block device") + _udev_types = ["stratis"] + _formattable = True # can be formatted + _supported = True # is supported + _linux_native = True # for clearpart + _min_size = Size("1 GiB") + _packages = ["stratisd"] # required packages + _resizable = False + + def __init__(self, **kwargs): + """ + :keyword device: path to the block device node + :keyword uuid: this Stratis block device UUID (not the pool UUID) + :keyword exists: indicates whether this is an existing format + :type exists: bool + :keyword pool_name: the name of the pool this block device belongs to + :keyword pool_uuid: the UUID of the pool this block device belongs to + :keyword locked_pool: whether this block device belongs to a locked pool or not + :type locked_pool: bool + :keyword locked_pool_key_desc: kernel keyring description for locked pool + :type locked_pool_key_desc: str + :keyword passphrase: passphrase for the locked pool + :type passphrase: str + :keyword key_file: path to a file containing a key + :type key_file: str + + .. note:: + + The 'device' kwarg is required for existing formats. For non- + existent formats, it is only necessary that the :attr:`device` + attribute be set before the :meth:`create` method runs. Note + that you can specify the device at the last moment by specifying + it via the 'device' kwarg to the :meth:`create` method. + """ + log_method_call(self, **kwargs) + DeviceFormat.__init__(self, **kwargs) + + self.pool_name = kwargs.get("pool_name") + self.pool_uuid = kwargs.get("pool_uuid") + self.locked_pool = kwargs.get("locked_pool") + self.locked_pool_key_desc = kwargs.get("locked_pool_key_desc") + + self.__passphrase = kwargs.get("passphrase") + self._key_file = kwargs.get("key_file") + + def __repr__(self): + s = DeviceFormat.__repr__(self) + s += (" pool_name = %(pool_name)s pool_uuid = %(pool_uuid)s locked_pool = %(locked_pool)s" % + {"pool_name": self.pool_name, "pool_uuid": self.pool_uuid, + "locked_pool": self.locked_pool}) + return s + + @property + def status(self): + return self.exists and (self.pool_name or self.pool_uuid) + + @property + def dict(self): + d = super(StratisBlockdev, self).dict + d.update({"pool_name": self.pool_name, "pool_uuid": self.pool_uuid}) + return d + + @property + def key_file(self): + """ Path to key file to be used in /etc/crypttab """ + return self._key_file + + def _set_passphrase(self, passphrase): + """ Set the passphrase used to access this device. """ + self.__passphrase = passphrase + + passphrase = property(fset=_set_passphrase) + + @property + def has_key(self): + return ((self.__passphrase not in ["", None]) or + (self._key_file and os.access(self._key_file, os.R_OK))) + + def unlock_pool(self): + if not self.locked_pool: + raise StratisError("This device doesn't contain a locked Stratis pool") + + if not self.has_key: + raise StratisError("No passphrase/key file for the locked Stratis pool") + + stratis.set_key(self.locked_pool_key_desc, self.__passphrase, self.key_file) + stratis.unlock_pool(self.pool_uuid) + + +register_device_format(StratisBlockdev) diff --git a/blivet/populator/helpers/__init__.py b/blivet/populator/helpers/__init__.py index aaf28c5cd..c5ac412fa 100644 --- a/blivet/populator/helpers/__init__.py +++ b/blivet/populator/helpers/__init__.py @@ -17,6 +17,7 @@ from .multipath import MultipathDevicePopulator, MultipathFormatPopulator from .optical import OpticalDevicePopulator from .partition import PartitionDevicePopulator +from .stratis import StratisFormatPopulator, StratisXFSFormatPopulator __all__ = ["get_device_helper", "get_format_helper"] diff --git a/blivet/populator/helpers/dm.py b/blivet/populator/helpers/dm.py index 30e99aa19..4721390ec 100644 --- a/blivet/populator/helpers/dm.py +++ b/blivet/populator/helpers/dm.py @@ -40,7 +40,8 @@ def match(cls, data): not udev.device_is_dm_integrity(data) and not udev.device_is_dm_lvm(data) and not udev.device_is_dm_mpath(data) and - not udev.device_is_dm_raid(data)) + not udev.device_is_dm_raid(data) and + not udev.device_is_dm_stratis(data)) def run(self): name = udev.device_get_name(self.data) diff --git a/blivet/populator/helpers/formatpopulator.py b/blivet/populator/helpers/formatpopulator.py index e9f71650f..8d78bf0ca 100644 --- a/blivet/populator/helpers/formatpopulator.py +++ b/blivet/populator/helpers/formatpopulator.py @@ -45,7 +45,10 @@ def match(cls, data, device): # pylint: disable=arguments-differ,unused-argumen :returns: whether this class is appropriate for the specified device :rtype: bool """ - ret = False + if device.type == "stratis filesystem": + # XFS on stratis filesystem device handled in StratisXFSFormatPopulator + return False + if cls is FormatPopulator: ret = True else: diff --git a/blivet/populator/helpers/luks.py b/blivet/populator/helpers/luks.py index 52795a98e..e6d7467b6 100644 --- a/blivet/populator/helpers/luks.py +++ b/blivet/populator/helpers/luks.py @@ -31,7 +31,7 @@ from ...flags import flags from .devicepopulator import DevicePopulator from .formatpopulator import FormatPopulator -from ...static_data import luks_data +from ...static_data import luks_data, stratis_info import logging log = logging.getLogger("blivet") @@ -71,6 +71,27 @@ class LUKSFormatPopulator(FormatPopulator): priority = 100 _type_specifier = "luks" + @classmethod + def match(cls, data, device): # pylint: disable=arguments-differ,unused-argument + if not super(LUKSFormatPopulator, cls).match(data, device): + return False + + # locked stratis pools are managed in the StratisFormatPopulator + for pool in stratis_info.locked_pools: + if device.path in pool.devices: + return False + + # unlocked stratis pools are also managed in the StratisFormatPopulator + holders = udev.device_get_holders(data) + if holders: + fs = udev.device_get_format(holders[0]) + if fs == "stratis": + log.debug("ignoring LUKS format on %s, it appears to be an encrypted Stratis pool", + device.name) + return False + + return True + def _get_kwargs(self): kwargs = super(LUKSFormatPopulator, self)._get_kwargs() kwargs["name"] = "luks-%s" % udev.device_get_uuid(self.data) diff --git a/blivet/populator/helpers/stratis.py b/blivet/populator/helpers/stratis.py new file mode 100644 index 000000000..ddcd8ec53 --- /dev/null +++ b/blivet/populator/helpers/stratis.py @@ -0,0 +1,191 @@ +# populator/helpers/stratis.py +# Stratis backend code for populating a DeviceTree. +# +# Copyright (C) 2020 Red Hat, Inc. +# +# This copyrighted material is made available to anyone wishing to use, +# modify, copy, or redistribute it subject to the terms and conditions of +# the GNU Lesser General Public License v.2, or (at your option) any later +# version. This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY expressed or implied, including the implied +# warranties of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See +# the GNU Lesser General Public License for more details. You should have +# received a copy of the GNU Lesser General Public License along with this +# program; if not, write to the Free Software Foundation, Inc., 51 Franklin +# Street, Fifth Floor, Boston, MA 02110-1301, USA. Any Red Hat trademarks +# that are incorporated in the source code or documentation are not subject +# to the GNU Lesser General Public License and may only be used or +# replicated with the express permission of Red Hat, Inc. +# +# Red Hat Author(s): Vojtech Trefny +# + +import copy + +from ...callbacks import callbacks +from ... import udev +from ...formats import get_format +from ...devices.stratis import StratisPoolDevice, StratisFilesystemDevice +from ...devicelibs.stratis import STRATIS_FS_SIZE +from ...storage_log import log_method_call +from .formatpopulator import FormatPopulator + +from ...static_data import stratis_info + +import logging +log = logging.getLogger("blivet") + + +class StratisFormatPopulator(FormatPopulator): + priority = 100 + _type_specifier = "stratis" + + @classmethod + def match(cls, data, device): # pylint: disable=arguments-differ,unused-argument + if super(StratisFormatPopulator, cls).match(data, device): + return True + + # locked stratis pools are managed here + for pool in stratis_info.locked_pools: + if device.path in pool.devices: + return True + + # unlocked encrypted pools are also managed here + if udev.device_get_format(data) == "crypto_LUKS": + holders = udev.device_get_holders(data) + if holders: + fs = udev.device_get_format(holders[0]) + if fs == cls._type_specifier: + return True + + return False + + def _get_blockdev_uuid(self): + if udev.device_get_format(self.data) == "crypto_LUKS": + holders = udev.device_get_holders(self.data) + if holders: + return udev.device_get_uuid(holders[0]) + + return udev.device_get_uuid(self.data) + + def _get_kwargs(self): + kwargs = super(StratisFormatPopulator, self)._get_kwargs() + + name = udev.device_get_name(self.data) + uuid = self._get_blockdev_uuid() + kwargs["uuid"] = uuid + + # stratis block device hosting an encrypted pool + kwargs["locked_pool"] = False + for pool in stratis_info.locked_pools: + if self.device.path in pool.devices: + kwargs["locked_pool"] = True + kwargs["pool_uuid"] = pool.uuid + kwargs["locked_pool_key_desc"] = pool.key_desc + return kwargs + + bd_info = stratis_info.blockdevs.get(uuid) + if bd_info: + if bd_info.pool_name: + kwargs["pool_name"] = bd_info.pool_name + else: + log.warning("Stratis block device %s has no pool_name", name) + if bd_info.pool_uuid: + kwargs["pool_uuid"] = bd_info.pool_uuid + else: + log.warning("Stratis block device %s has no pool_uuid", name) + + return kwargs + + def _add_pool_device(self): + bd_info = stratis_info.blockdevs.get(self.device.format.uuid) + if not bd_info: + # no info about the stratis block device -> we're done + return + + if not bd_info.pool_name: + log.info("stratis block device %s has no pool", self.device.name) + return + + pool_info = stratis_info.pools.get(bd_info.pool_uuid) + if pool_info is None: + log.warning("Failed to get information about Stratis pool %s (%s)", + bd_info.pool_name, bd_info.pool_uuid) + return + + pool_device = self._devicetree.get_device_by_uuid(bd_info.pool_uuid) + if pool_device and self.device not in pool_device.parents: + pool_device.parents.append(self.device) + callbacks.parent_added(device=pool_device, parent=self.device) + elif pool_device is None: + # TODO: stratis duplicate pool name + + pool_device = StratisPoolDevice(pool_info.name, + parents=[self.device], + uuid=pool_info.uuid, + size=pool_info.physical_size, + exists=True, + encrypted=pool_info.encrypted) + self._devicetree._add_device(pool_device) + + # now add filesystems on this pool + for fs_info in stratis_info.filesystems.values(): + if fs_info.pool_uuid != pool_info.uuid: + continue + + fs_device = self._devicetree.get_device_by_uuid(fs_info.uuid) + if fs_device is not None: + log.debug("stratis filesystem already added %s", fs_info.name) + continue + + pool_device = self._devicetree.get_device_by_uuid(fs_info.pool_uuid) + if not pool_device: + log.info("stratis pool %s has not been added yet", fs_info.pool_name) + return + + fs_device = StratisFilesystemDevice(fs_info.name, parents=[pool_device], + uuid=fs_info.uuid, size=STRATIS_FS_SIZE, + exists=True) + self._devicetree._add_device(fs_device) + + # do format handling now + udev_info = udev.get_device(fs_device.sysfs_path) + if not udev_info: + return + + self._devicetree.handle_format(udev_info, fs_device) + fs_device.original_format = copy.deepcopy(fs_device.format) + + def run(self): + log_method_call(self, pv=self.device.name) + super(StratisFormatPopulator, self).run() + + if not self.device.format.locked_pool: + self._add_pool_device() + + +class StratisXFSFormatPopulator(FormatPopulator): + priority = 100 + _type_specifier = "stratis xfs" + + @classmethod + def match(cls, data, device): # pylint: disable=arguments-differ,unused-argument + """ Return True if this helper is appropriate for the given device. + + :param :class:`pyudev.Device` data: udev data describing a device + :param device: device instance corresponding to the udev data + :type device: :class:`~.devices.StorageDevice` + :returns: whether this class is appropriate for the specified device + :rtype: bool + """ + if device.type == "stratis filesystem" and udev.device_get_format(data) == "xfs": + return True + + return False + + def run(self): + """ Create a format instance and associate it with the device instance. """ + kwargs = self._get_kwargs() + kwargs["pool_uuid"] = self.device.pool.uuid + log.info("type detected on '%s' is '%s'", self.device.name, self.type_spec) + self.device.format = get_format(self.type_spec, **kwargs) diff --git a/blivet/populator/populator.py b/blivet/populator/populator.py index 75bb1741d..1173e45af 100644 --- a/blivet/populator/populator.py +++ b/blivet/populator/populator.py @@ -47,7 +47,7 @@ from ..tasks import availability from ..threads import SynchronizedMeta from .helpers import get_device_helper, get_format_helper -from ..static_data import lvs_info, pvs_info, vgs_info, luks_data, mpath_members +from ..static_data import lvs_info, pvs_info, vgs_info, luks_data, mpath_members, stratis_info from ..callbacks import callbacks import logging @@ -88,7 +88,7 @@ def reset(self, disk_images=None): self.set_disk_images(disk_images) # initialize attributes that may later hold cached lvm info - self.drop_lvm_cache() + self.drop_device_info_cache() self._cleanup = False @@ -146,6 +146,10 @@ def _add_name(self, name): if name not in self.names: self.names.append(name) + def _remove_name(self, name): + if name in self.names: + self.names.remove(name) + def _reason_to_skip_device(self, info): sysfs_path = udev.device_get_sysfs_path(info) uuid = udev.device_get_uuid(info) @@ -253,6 +257,29 @@ def handle_device(self, info, update_orig_fmt=False): log.info("device %s is marked as hidden in sysfs, ignoring", name) return + if udev.device_is_private(info): + log.info("device %s is private, ignoring", name) + return + + if udev.device_is_stratis_filesystem(info): + log.debug("skipping %s stratis filesystem, will be handled with corresponding " + "stratis block device", name) + return + + if udev.device_is_dm_luks(info) and udev.device_is_stratis_private(info): + log.debug("skipping %s stratis private crypto device, will be handled with " + "corresponding stratis block device", name) + + # XXX for newly unlocked stratis pools we need to force run handle_format + # on one of the parent block devices to add the pool to the tree + parents = udev.device_get_parents(info) + if parents: + bname = udev.device_get_name(parents[0]) + bdev = self.get_device_by_name(bname) + if bdev: + self.handle_format(info, bdev, force=True) + return + # make sure we note the name of every device we see self._add_name(name) device = self.get_device_by_name(name) @@ -285,7 +312,7 @@ def handle_device(self, info, update_orig_fmt=False): device.device_links = udev.device_get_symlinks(info) callbacks.device_scanned(device_name=name) - def handle_format(self, info, device): + def handle_format(self, info, device, force=False): log_method_call(self, name=getattr(device, "name", None)) if not info: @@ -300,7 +327,7 @@ def handle_format(self, info, device): name = udev.device_get_name(info) if (not device or (not udev.device_get_format(info) and not udev.device_get_disklabel_type(info)) or - device.format.type): + (device.format.type and not force)): # this device has no formatting or it has already been set up log.debug("no type or existing type for %s, bailing", name) return @@ -426,8 +453,7 @@ def _populate(self): self.ignored_disks, self.exclusive_disks) disklib.update_volume_info() - self.drop_lvm_cache() - mpath_members.drop_cache() + self.drop_device_info_cache() if flags.auto_dev_updates and availability.BLOCKDEV_MPATH_PLUGIN.available: blockdev.mpath.set_friendly_names(flags.multipath_friendly_names) @@ -467,11 +493,13 @@ def _populate(self): # inconsistencies are ignored or resolved. self._handle_inconsistencies() - def drop_lvm_cache(self): - """ Drop cached lvm information. """ + def drop_device_info_cache(self): + """ Drop cached device information. """ lvs_info.drop_cache() pvs_info.drop_cache() vgs_info.drop_cache() + mpath_members.drop_cache() + stratis_info.drop_cache() def handle_nodev_filesystems(self): for line in open("/proc/mounts").readlines(): diff --git a/blivet/safe_dbus.py b/blivet/safe_dbus.py index d99143e52..76416f901 100644 --- a/blivet/safe_dbus.py +++ b/blivet/safe_dbus.py @@ -100,7 +100,7 @@ def get_new_session_connection(): def call_sync(service, obj_path, iface, method, args, - connection=None): + connection=None, fds=None): """ Safely call a given method on a given object of a given service over DBus passing given arguments. If a connection is given, it is used, otherwise a @@ -120,6 +120,8 @@ def call_sync(service, obj_path, iface, method, args, :param connection: connection to use (if None, a new connection is established) :type connection: Gio.DBusConnection + :param fds: list of file descriptors for the call + :type: Gio.UnixFDList :return: unpacked value returned by the method :rtype: tuple with elements that depend on the method :raise DBusCallError: if some DBus related error appears @@ -136,9 +138,9 @@ def call_sync(service, obj_path, iface, method, args, raise DBusCallError("Connection is closed") try: - ret = connection.call_sync(service, obj_path, iface, method, args, - None, Gio.DBusCallFlags.NONE, - DEFAULT_DBUS_TIMEOUT, None) + ret = connection.call_with_unix_fd_list_sync(service, obj_path, iface, method, args, + None, Gio.DBusCallFlags.NONE, + DEFAULT_DBUS_TIMEOUT, fds, None) except GLib.GError as gerr: msg = "Failed to call %s method on %s with %s arguments: %s" % \ (method, obj_path, args, gerr.message) # pylint: disable=no-member @@ -148,7 +150,7 @@ def call_sync(service, obj_path, iface, method, args, msg = "No return from %s method on %s with %s arguments" % (method, obj_path, args) raise DBusCallError(msg) - return ret.unpack() + return ret[0].unpack() def get_property_sync(service, obj_path, iface, prop_name, @@ -186,6 +188,33 @@ def get_property_sync(service, obj_path, iface, prop_name, return ret +def get_properties_sync(service, obj_path, iface, connection=None): + """ + Get all properties of a given object provided by a given service. + + :param service: DBus service to use + :type service: str + :param obj_path: object path + :type obj_path: str + :param iface: interface to use + :type iface: str + :param connection: connection to use (if None, a new connection is + established) + :type connection: Gio.DBusConnection + :return: unpacked value of the property + :rtype: tuple with elements that depend on the type of the property + :raise DBusCallError: when the internal dbus_call_safe_sync invocation + raises an exception + + """ + + args = GLib.Variant('(s)', (iface,)) + ret = call_sync(service, obj_path, DBUS_PROPS_IFACE, "GetAll", args, + connection) + + return ret + + def check_object_available(service, obj_path, iface=None): intro_data = call_sync(service, obj_path, DBUS_INTRO_IFACE, "Introspect", None) node_info = Gio.DBusNodeInfo.new_for_xml(intro_data[0]) diff --git a/blivet/static_data/__init__.py b/blivet/static_data/__init__.py index 321dae0fd..b5dd925e4 100644 --- a/blivet/static_data/__init__.py +++ b/blivet/static_data/__init__.py @@ -2,3 +2,4 @@ from .luks_data import luks_data from .mpath_info import mpath_members from .nvdimm import nvdimm +from .stratis_info import stratis_info diff --git a/blivet/static_data/stratis_info.py b/blivet/static_data/stratis_info.py new file mode 100644 index 000000000..981e4afd2 --- /dev/null +++ b/blivet/static_data/stratis_info.py @@ -0,0 +1,280 @@ +# stratis_info.py +# Backend code for populating a DeviceTree. +# +# Copyright (C) 2020 Red Hat, Inc. +# +# This copyrighted material is made available to anyone wishing to use, +# modify, copy, or redistribute it subject to the terms and conditions of +# the GNU Lesser General Public License v.2, or (at your option) any later +# version. This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY expressed or implied, including the implied +# warranties of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See +# the GNU Lesser General Public License for more details. You should have +# received a copy of the GNU Lesser General Public License along with this +# program; if not, write to the Free Software Foundation, Inc., 51 Franklin +# Street, Fifth Floor, Boston, MA 02110-1301, USA. Any Red Hat trademarks +# that are incorporated in the source code or documentation are not subject +# to the GNU Lesser General Public License and may only be used or +# replicated with the express permission of Red Hat, Inc. +# +# Red Hat Author(s): Vojtech Trefny +# + +import gi +gi.require_version("GLib", "2.0") + +from gi.repository import GLib + +import os + +from collections import namedtuple + +from .. import safe_dbus +from ..size import Size + +import logging +log = logging.getLogger("blivet") + + +# XXX we can't import these from devicelibs.stratis, circular imports make python mad +STRATIS_SERVICE = "org.storage.stratis2" +STRATIS_PATH = "/org/storage/stratis2" +STRATIS_POOL_INTF = STRATIS_SERVICE + ".pool.r1" +STRATIS_FILESYSTEM_INTF = STRATIS_SERVICE + ".filesystem" +STRATIS_BLOCKDEV_INTF = STRATIS_SERVICE + ".blockdev" +STRATIS_PROPS_INTF = STRATIS_SERVICE + ".FetchProperties.r4" +STRATIS_MANAGER_INTF = STRATIS_SERVICE + ".Manager.r2" + + +StratisPoolInfo = namedtuple("StratisPoolInfo", ["name", "uuid", "physical_size", "physical_used", "object_path", "encrypted"]) +StratisFilesystemInfo = namedtuple("StratisFilesystemInfo", ["name", "uuid", "used_size", "pool_name", + "pool_uuid", "object_path"]) +StratisBlockdevInfo = namedtuple("StratisBlockdevInfo", ["path", "uuid", "pool_name", "pool_uuid", "object_path"]) +StratisLockedPoolInfo = namedtuple("StratisLockedPoolInfo", ["uuid", "key_desc", "devices"]) + + +class StratisInfo(object): + """ Class to be used as a singleton. + Maintains the Stratis devices info cache. + """ + + def __init__(self): + self._info_cache = None + + def _get_pool_info(self, pool_path): + try: + properties = safe_dbus.get_properties_sync(STRATIS_SERVICE, + pool_path, + STRATIS_POOL_INTF)[0] + except safe_dbus.DBusPropertyError as e: + log.error("Error when getting DBus properties of '%s': %s", + pool_path, str(e)) + + if not properties: + log.error("Failed to get DBus properties of '%s'", pool_path) + return None + + all_props = safe_dbus.call_sync(STRATIS_SERVICE, + pool_path, + STRATIS_PROPS_INTF, + "GetAllProperties", + None)[0] + if all_props: + valid, pool_size = all_props.get("TotalPhysicalSize", + (False, "TotalPhysicalSize not available")) + if not valid: + log.warning("Failed to get Stratis pool physical size for %s: %s", + properties["Name"], pool_size) + pool_size = 0 + + valid, pool_used = all_props.get("TotalPhysicalUsed", + (False, "TotalPhysicalUsed not available")) + if not valid: + log.warning("Failed to get Stratis pool physical used for %s: %s", + properties["Name"], pool_used) + pool_used = 0 + else: + log.error("Failed to get Stratis pool properties for %s.", properties["Name"]) + pool_size = 0 + pool_used = 0 + + return StratisPoolInfo(name=properties["Name"], uuid=properties["Uuid"], + physical_size=Size(pool_size), physical_used=Size(pool_used), + object_path=pool_path, encrypted=properties["Encrypted"]) + + def _get_filesystem_info(self, filesystem_path): + try: + properties = safe_dbus.get_properties_sync(STRATIS_SERVICE, + filesystem_path, + STRATIS_FILESYSTEM_INTF)[0] + except safe_dbus.DBusPropertyError as e: + log.error("Error when getting DBus properties of '%s': %s", + filesystem_path, str(e)) + + if not properties: + log.error("Failed to get DBus properties of '%s'", filesystem_path) + return None + + pool_info = self._get_pool_info(properties["Pool"]) + if not pool_info: + return None + + all_props = safe_dbus.call_sync(STRATIS_SERVICE, + filesystem_path, + STRATIS_PROPS_INTF, + "GetAllProperties", + None)[0] + if all_props: + valid, used_size = all_props.get("Used", + (False, "Used not available")) + if not valid: + log.warning("Failed to get Stratis filesystem used size for %s: %s", + properties["Name"], used_size) + used_size = 0 + else: + log.error("Failed to get Stratis filesystem properties for %s.", properties["Name"]) + used_size = 0 + + return StratisFilesystemInfo(name=properties["Name"], uuid=properties["Uuid"], + used_size=Size(used_size), + pool_name=pool_info.name, pool_uuid=pool_info.uuid, + object_path=filesystem_path) + + def _get_blockdev_info(self, blockdev_path): + try: + properties = safe_dbus.get_properties_sync(STRATIS_SERVICE, + blockdev_path, + STRATIS_BLOCKDEV_INTF)[0] + except safe_dbus.DBusPropertyError as e: + log.error("Error when getting DBus properties of '%s': %s", + blockdev_path, str(e)) + + if not properties: + log.error("Failed to get DBus properties of '%s'", blockdev_path) + return None + + pool_path = properties["Pool"] + if pool_path == "/": + pool_name = "" + else: + pool_info = self._get_pool_info(properties["Pool"]) + if not pool_info: + return None + pool_name = pool_info.name + + return StratisBlockdevInfo(path=properties["Devnode"], uuid=properties["Uuid"], + pool_name=pool_name, pool_uuid=pool_info.uuid, + object_path=blockdev_path) + + def _get_locked_pools_info(self): + locked_pools = [] + + try: + props = safe_dbus.call_sync(STRATIS_SERVICE, + STRATIS_PATH, + STRATIS_PROPS_INTF, + "GetProperties", + GLib.Variant("(as)", (["LockedPoolsWithDevs"],)))[0] + except safe_dbus.DBusCallError as e: + log.error("Failed to get list of locked Stratis pools: %s", str(e)) + return locked_pools + + if props and "LockedPoolsWithDevs" in props.keys(): + valid, pools_info = props["LockedPoolsWithDevs"] + if valid: + for pool_uuid in pools_info.keys(): + info = StratisLockedPoolInfo(uuid=pool_uuid, + key_desc=pools_info[pool_uuid]["key_description"], + devices=[d["devnode"] for d in pools_info[pool_uuid]["devs"]]) + locked_pools.append(info) + + return locked_pools + + def _get_stratis_info(self): + self._info_cache = dict() + self._info_cache["pools"] = dict() + self._info_cache["blockdevs"] = dict() + self._info_cache["filesystems"] = dict() + self._info_cache["locked_pools"] = [] + + try: + ret = safe_dbus.check_object_available(STRATIS_SERVICE, STRATIS_PATH) + except safe_dbus.DBusCallError: + log.warning("Stratis DBus service is not running") + return + else: + if not ret: + log.warning("Stratis DBus service is not available") + + objects = safe_dbus.call_sync(STRATIS_SERVICE, + STRATIS_PATH, + "org.freedesktop.DBus.ObjectManager", + "GetManagedObjects", + None)[0] + + for path, interfaces in objects.items(): + if STRATIS_POOL_INTF in interfaces.keys(): + pool_info = self._get_pool_info(path) + if pool_info: + self._info_cache["pools"][pool_info.uuid] = pool_info + + if STRATIS_FILESYSTEM_INTF in interfaces.keys(): + fs_info = self._get_filesystem_info(path) + if fs_info: + self._info_cache["filesystems"][fs_info.uuid] = fs_info + + if STRATIS_BLOCKDEV_INTF in interfaces.keys(): + bd_info = self._get_blockdev_info(path) + if bd_info: + self._info_cache["blockdevs"][bd_info.uuid] = bd_info + + self._info_cache["locked_pools"] = self._get_locked_pools_info() + + @property + def pools(self): + if self._info_cache is None: + self._get_stratis_info() + + return self._info_cache["pools"] + + @property + def filesystems(self): + if self._info_cache is None: + self._get_stratis_info() + + return self._info_cache["filesystems"] + + @property + def blockdevs(self): + if self._info_cache is None: + self._get_stratis_info() + + return self._info_cache["blockdevs"] + + @property + def locked_pools(self): + if self._info_cache is None: + self._get_stratis_info() + + return self._info_cache["locked_pools"] + + def drop_cache(self): + self._info_cache = None + + def get_pool_info(self, pool_name): + for pool in self.pools.values(): + if pool.name == pool_name: + return pool + + def get_filesystem_info(self, pool_name, fs_name): + for fs in self.filesystems.values(): + if fs.pool_name == pool_name and fs.name == fs_name: + return fs + + def get_blockdev_info(self, bd_path): + for bd in self.blockdevs.values(): + if bd.path == bd_path or bd.path == os.path.realpath(bd_path): + return bd + + +stratis_info = StratisInfo() diff --git a/blivet/tasks/fsmount.py b/blivet/tasks/fsmount.py index 15f79a3e5..65b2470ac 100644 --- a/blivet/tasks/fsmount.py +++ b/blivet/tasks/fsmount.py @@ -191,6 +191,10 @@ def _availability_errors(self): return errors +class StratisXFSMount(FSMount): + fstype = "xfs" + + class TmpFSMount(NoDevFSMount): def _modify_options(self, options): diff --git a/blivet/udev.py b/blivet/udev.py index a8297f3f6..efbc53d6f 100644 --- a/blivet/udev.py +++ b/blivet/udev.py @@ -373,6 +373,7 @@ def device_is_disk(info): device_is_dm_partition(info) or device_is_dm_lvm(info) or device_is_dm_crypt(info) or + device_is_dm_stratis(info) or (device_is_md(info) and (not device_get_md_container(info) and not all(device_is_disk(d) for d in device_get_parents(info)))))) @@ -710,6 +711,11 @@ def device_is_dm_livecd(info): device_get_name(info).startswith("live")) +def device_is_dm_stratis(info): + """ Return True if the device is a Stratis pool or filesystem. """ + return device_dm_subsystem_match(info, "stratis") + + def device_is_biosraid_member(info): # Note that this function does *not* identify raid sets. # Tests to see if device is part of a dmraid set. @@ -1024,3 +1030,27 @@ def device_is_hidden(info): return False return bool(int(hidden)) + + +def device_is_stratis_filesystem(info): + if not device_is_dm_stratis(info): + return False + try: + return info.get("DM_UUID", "").split("-")[4] == "fs" + except IndexError: + return False + + +def device_is_stratis_private(info): + if not (device_is_dm_stratis(info) or device_is_dm_luks(info)): + return False + try: + return info.get("DM_NAME", "").split("-")[2] == "private" + except IndexError: + return False + + +def device_is_private(info): + if device_is_dm_stratis(info): + return device_is_stratis_private(info) + return False diff --git a/doc/api/blivet.rst b/doc/api/blivet.rst index 2b9dbfc1a..c403369a8 100644 --- a/doc/api/blivet.rst +++ b/doc/api/blivet.rst @@ -48,6 +48,8 @@ blivet * :meth:`~blivet.blivet.Blivet.new_lv_from_lvs` * :meth:`~blivet.blivet.Blivet.new_mdarray` * :meth:`~blivet.blivet.Blivet.new_partition` + * :meth:`~blivet.blivet.Blivet.new_stratis_filesystem` + * :meth:`~blivet.blivet.Blivet.new_stratis_pool` * :meth:`~blivet.blivet.Blivet.new_tmp_fs` * :meth:`~blivet.blivet.Blivet.new_vg` * :attr:`~blivet.blivet.Blivet.partitioned` @@ -61,6 +63,7 @@ blivet * :meth:`~blivet.blivet.Blivet.save_passphrase` * :meth:`~blivet.blivet.Blivet.set_default_fstype` * :meth:`~blivet.blivet.Blivet.shutdown` + * :attr:`~blivet.blivet.Blivet.stratis_pools` * :meth:`~blivet.blivet.Blivet.suggest_container_name` * :meth:`~blivet.blivet.Blivet.suggest_device_name` * :attr:`~blivet.blivet.Blivet.swaps` @@ -86,6 +89,7 @@ blivet * :const:`~blivet.devicefactory.DEVICE_TYPE_DISK` * :const:`~blivet.devicefactory.DEVICE_TYPE_LVM_THINP` * :const:`~blivet.devicefactory.DEVICE_TYPE_LVM_VDO` + * :const:`~blivet.devicefactory.DEVICE_TYPE_STRATIS` * :func:`~blivet.devicefactory.is_supported_device_type` * :func:`~blivet.devicefactory.get_device_factory` * :func:`~blivet.devicefactory.get_device_type` @@ -179,6 +183,7 @@ blivet * :class:`~blivet.errors.SinglePhysicalVolumeError` * :class:`~blivet.errors.SizePlacesError` * :class:`~blivet.errors.StorageError` + * :class:`~blivet.errors.StratisError` * :class:`~blivet.errors.SwapSpaceError` * :class:`~blivet.errors.ThreadError` * :class:`~blivet.errors.UdevError` diff --git a/doc/api/devices.rst b/doc/api/devices.rst index 19fbdefc4..bd250cae9 100644 --- a/doc/api/devices.rst +++ b/doc/api/devices.rst @@ -137,3 +137,8 @@ devices * :attr:`~blivet.devices.storage.StorageDevice.target_size` * :meth:`~blivet.devices.storage.StorageDevice.teardown` * :attr:`~blivet.devices.storage.StorageDevice.uuid` + +* :mod:`~blivet.devices.stratis` + * :class:`~blivet.devices.stratis.StratisPoolDevice` (see :ref:`inherited public API `) + * :attr:`~blivet.devices.stratis.StratisPoolDevice.encrypted` + * :class:`~blivet.devices.stratis.StratisFilesystemDevice` (see :ref:`inherited public API `) diff --git a/doc/api/formats.rst b/doc/api/formats.rst index aa409e4a9..61437a495 100644 --- a/doc/api/formats.rst +++ b/doc/api/formats.rst @@ -110,5 +110,10 @@ formats * :mod:`~blivet.formats.prepboot` * :class:`~blivet.formats.prepboot.PPCPRePBoot` (see :ref:`inherited public API `) +* :mod:`~blivet.formats.stratis` + * :class:`~blivet.formats.stratis.StratisBlockdev` (see :ref:`inherited public API `) + * :attr:`~blivet.formats.stratis.StratisBlockdev.has_key` + * :meth:`~blivet.formats.stratis.StratisBlockdev.unlock_pool` + * :mod:`~blivet.formats.swap` * :class:`~blivet.formats.swap.SwapSpace` (see :ref:`inherited public API `) diff --git a/examples/stratis.py b/examples/stratis.py new file mode 100644 index 000000000..0b7219230 --- /dev/null +++ b/examples/stratis.py @@ -0,0 +1,55 @@ +import os + +import blivet +from blivet.size import Size +from blivet.util import set_up_logging, create_sparse_tempfile + +set_up_logging() +b = blivet.Blivet() # create an instance of Blivet (don't add system devices) + +# create a disk image file on which to create new devices +disk1_file = create_sparse_tempfile("disk1", Size("100GiB")) +b.disk_images["disk1"] = disk1_file +disk2_file = create_sparse_tempfile("disk2", Size("100GiB")) +b.disk_images["disk2"] = disk2_file + +b.reset() + +try: + disk1 = b.devicetree.get_device_by_name("disk1") + disk2 = b.devicetree.get_device_by_name("disk2") + + b.initialize_disk(disk1) + b.initialize_disk(disk2) + + bd = b.new_partition(size=Size("50GiB"), fmt_type="stratis", parents=[disk1]) + b.create_device(bd) + bd2 = b.new_partition(size=Size("50GiB"), fmt_type="stratis", parents=[disk2]) + b.create_device(bd2) + + # allocate the partitions (decide where and on which disks they'll reside) + blivet.partitioning.do_partitioning(b) + + pool = b.new_stratis_pool(name="stratis_pool", parents=[bd, bd2]) + b.create_device(pool) + + # # encrypted stratis pool can be created by adding "encrypted" and "passphrase" + # # keywords, only the entire pool can be encrypted: + # pool = b.new_stratis_pool(name="stratis_pool", parents=[bd, bd2], encrypted=True, passphrase="secret") + # b.create_device(pool) + + fs = b.new_stratis_filesystem(name="stratis_filesystem", parents=[pool]) + b.create_device(fs) + + print(b.devicetree) + + # write the new partitions to disk and format them as specified + b.do_it() + print(b.devicetree) + input("Check the state and hit ENTER to trigger cleanup") +finally: + b.devicetree.recursive_remove(pool) + b.do_it() + b.devicetree.teardown_disk_images() + os.unlink(disk1_file) + os.unlink(disk2_file) diff --git a/tests/devicefactory_test.py b/tests/devicefactory_test.py index 93c8bdb71..2c2e6168b 100644 --- a/tests/devicefactory_test.py +++ b/tests/devicefactory_test.py @@ -12,13 +12,14 @@ import blivet from blivet import devicefactory -from blivet.devicelibs import raid, crypto +from blivet.devicelibs import raid, crypto, stratis from blivet.devices import DiskDevice from blivet.devices import DiskFile from blivet.devices import LUKSDevice from blivet.devices import LVMLogicalVolumeDevice from blivet.devices import MDRaidArrayDevice from blivet.devices import PartitionDevice +from blivet.devices import StratisFilesystemDevice from blivet.devices.lvm import DEFAULT_THPOOL_RESERVE from blivet.errors import RaidError from blivet.formats import get_format @@ -893,3 +894,118 @@ def test_mdfactory(self, *args): # pylint: disable=unused-argument self.assertEqual(factory2.container_list, []) self.assertIsNone(factory2.get_container()) + + +class StratisFactoryTestCase(DeviceFactoryTestCase): + device_class = StratisFilesystemDevice + device_type = devicefactory.DEVICE_TYPE_STRATIS + encryption_supported = False + factory_class = devicefactory.StratisFactory + + # pylint: disable=unused-argument + def _get_size_delta(self, devices=None): + """ Return size delta for a specific factory type. + + :keyword devices: list of factory-managed devices or None + :type devices: list(:class:`blivet.devices.StorageDevice`) or NoneType + """ + return Size("2 MiB") + + def _validate_factory_device(self, *args, **kwargs): + device = args[0] + + self.assertEqual(device.type, "stratis filesystem") + self.assertEqual(device.size, stratis.STRATIS_FS_SIZE) + self.assertTrue(hasattr(device, "pool")) + self.assertIsNotNone(device.pool) + self.assertEqual(device.pool.type, "stratis pool") + self.assertIsNotNone(device.format) + self.assertEqual(device.format.type, "stratis xfs") + self.assertEqual(device.format.mountpoint, kwargs.get("mountpoint")) + + if kwargs.get("name"): + self.assertEqual(device.fsname, kwargs.get("name")) + + self.assertTrue(set(device.disks).issubset(kwargs["disks"])) + + if kwargs.get("container_size"): + self.assertAlmostEqual(device.pool.size, + kwargs.get("container_size"), + delta=self._get_size_delta()) + else: + # if container size is not specified, we'll use all available space (not counting size taken by partitions) + self.assertAlmostEqual(device.pool.size, + sum(d.size - Size("2 MiB") for d in self.b.disks), + delta=self._get_size_delta()) + + self.assertEqual(device.pool.encrypted, kwargs.get("container_encrypted", False)) + + return device + + @patch("blivet.devices.stratis.StratisFilesystemDevice.type_external_dependencies", return_value=set()) + @patch("blivet.devices.stratis.StratisPoolDevice.type_external_dependencies", return_value=set()) + def test_device_factory(self, *args): # pylint: disable=unused-argument,arguments-differ + device_type = self.device_type + kwargs = {"disks": self.b.disks, + "mountpoint": "/factorytest"} + device = self._factory_device(device_type, **kwargs) + self._validate_factory_device(device, device_type, **kwargs) + + # rename the device + kwargs["name"] = "stratisfs" + kwargs["device"] = device + device = self._factory_device(device_type, **kwargs) + self._validate_factory_device(device, device_type, **kwargs) + + # new mountpoint + kwargs["mountpoint"] = "/a/different/dir" + device = self._factory_device(device_type, **kwargs) + self._validate_factory_device(device, device_type, **kwargs) + + # change container size + kwargs = {"disks": self.b.disks, + "mountpoint": "/factorytest", + "container_size": Size("2.5 GiB")} + device = self._factory_device(device_type, **kwargs) + self._validate_factory_device(device, device_type, **kwargs) + + # enable encryption on the container + kwargs["container_encrypted"] = True + device = self._factory_device(device_type, **kwargs) + self._validate_factory_device(device, device_type, **kwargs) + + # disable encryption on the container + kwargs["container_encrypted"] = False + device = self._factory_device(device_type, **kwargs) + self._validate_factory_device(device, device_type, **kwargs) + + @patch("blivet.devices.stratis.StratisFilesystemDevice.type_external_dependencies", return_value=set()) + @patch("blivet.devices.stratis.StratisPoolDevice.type_external_dependencies", return_value=set()) + def test_normalize_size(self, *args): # pylint: disable=unused-argument + # size normalization doesn't make sense for stratis -- filesystems are always 1 TiB + pass + + @patch("blivet.devices.stratis.StratisFilesystemDevice.type_external_dependencies", return_value=set()) + @patch("blivet.devices.stratis.StratisPoolDevice.type_external_dependencies", return_value=set()) + def test_get_free_disk_space(self, *args): # pylint: disable=unused-argument + # get_free_disk_space should return the total free space on disks + kwargs = self._get_test_factory_args() + factory = devicefactory.get_device_factory(self.b, + self.device_type, + disks=self.b.disks, + **kwargs) + # disks contain empty disklabels, so free space is sum of disk sizes + self.assertAlmostEqual(factory._get_free_disk_space(), + sum(d.size for d in self.b.disks), + delta=self._get_size_delta()) + + factory.configure() + factory = devicefactory.get_device_factory(self.b, + self.device_type, + disks=self.b.disks, + **kwargs) + # default container size policy for Stratis factory is SIZE_POLICY_MAX so there should + # be (almost) no free space on the disks + self.assertAlmostEqual(factory._get_free_disk_space(), + Size("2 MiB"), + delta=self._get_size_delta()) diff --git a/tests/devices_test/stratis_test.py b/tests/devices_test/stratis_test.py new file mode 100644 index 000000000..fe814e5bc --- /dev/null +++ b/tests/devices_test/stratis_test.py @@ -0,0 +1,96 @@ +import six +import unittest + +try: + from unittest.mock import patch +except ImportError: + from mock import patch + +import blivet + +from blivet.devices import StorageDevice +from blivet.devices import StratisPoolDevice +from blivet.devices import StratisFilesystemDevice +from blivet.errors import StratisError +from blivet.size import Size + + +DEVICE_CLASSES = [ + StratisPoolDevice, + StratisFilesystemDevice, + StorageDevice +] + + +@unittest.skipUnless(not any(x.unavailable_type_dependencies() for x in DEVICE_CLASSES), "some unsupported device classes required for this test") +class BlivetNewStratisDeviceTest(unittest.TestCase): + def test_new_stratis(self): + b = blivet.Blivet() + bd = StorageDevice("bd1", fmt=blivet.formats.get_format("stratis"), + size=Size("1 GiB"), exists=False) + + b.devicetree._add_device(bd) + + pool = b.new_stratis_pool(name="testpool", parents=[bd]) + self.assertEqual(pool.name, "testpool") + self.assertEqual(pool.size, bd.size) + + fs = b.new_stratis_filesystem(name="testfs", parents=[pool]) + + self.assertEqual(fs.name, "testpool/testfs") + self.assertEqual(fs.path, "/dev/stratis/%s" % fs.name) + self.assertEqual(fs.size, Size("1 TiB")) + self.assertEqual(fs.pool, pool) + self.assertEqual(fs.format.type, "stratis xfs") + + with six.assertRaisesRegex(self, StratisError, "not enough free space in the pool"): + # only 1 GiB pool, not enough space for second fs + b.new_stratis_filesystem(name="testfs2", parents=[pool]) + + b.create_device(pool) + b.create_device(fs) + + with patch("blivet.devicelibs.stratis") as stratis_dbus: + with patch.object(pool, "_pre_create"): + with patch.object(pool, "_post_create"): + pool.create() + stratis_dbus.create_pool.assert_called_with(name='testpool', + devices=['/dev/bd1'], + encrypted=False, + passphrase=None, + key_file=None) + + # we would get this from pool._post_create + pool.uuid = "c4fc9ebe-e173-4cab-8d81-cc6abddbe02d" + + with patch("blivet.devicelibs.stratis") as stratis_dbus: + with patch.object(fs, "_pre_create"): + with patch.object(fs, "_post_create"): + fs.create() + stratis_dbus.create_filesystem.assert_called_with("testfs", + "c4fc9ebe-e173-4cab-8d81-cc6abddbe02d") + + def test_new_encryted_stratis(self): + b = blivet.Blivet() + bd = StorageDevice("bd1", fmt=blivet.formats.get_format("stratis"), + size=Size("1 GiB"), exists=True) + + b.devicetree._add_device(bd) + + pool = b.new_stratis_pool(name="testpool", parents=[bd], encrypted=True, passphrase="secret") + self.assertEqual(pool.name, "testpool") + self.assertEqual(pool.size, bd.size) + self.assertTrue(pool.encrypted) + self.assertTrue(pool.has_key) + + b.create_device(pool) + + with patch("blivet.devicelibs.stratis") as stratis_dbus: + with patch.object(pool, "_pre_create"): + with patch.object(pool, "_post_create"): + pool.create() + stratis_dbus.create_pool.assert_called_with(name='testpool', + devices=['/dev/bd1'], + encrypted=True, + passphrase="secret", + key_file=None) diff --git a/tests/populator_test.py b/tests/populator_test.py index 2a8532f0b..b63780f95 100644 --- a/tests/populator_test.py +++ b/tests/populator_test.py @@ -40,6 +40,7 @@ class DMDevicePopulatorTestCase(PopulatorHelperTestCase): @patch("blivet.udev.device_is_dm_mpath", return_value=False) @patch("blivet.udev.device_is_dm_partition", return_value=False) @patch("blivet.udev.device_is_dm_raid", return_value=False) + @patch("blivet.udev.device_is_dm_stratis", return_value=False) @patch("blivet.udev.device_is_dm", return_value=True) def test_match(self, *args): """Test matching of dm device populator.""" @@ -60,6 +61,7 @@ def test_match(self, *args): @patch("blivet.udev.device_is_dm_mpath", return_value=False) @patch("blivet.udev.device_is_dm_partition", return_value=False) @patch("blivet.udev.device_is_dm_raid", return_value=False) + @patch("blivet.udev.device_is_dm_stratis", return_value=False) @patch("blivet.udev.device_is_md", return_value=False) @patch("blivet.udev.device_is_loop", return_value=False) @patch("blivet.udev.device_is_dm", return_value=True)