Skip to content

Commit

Permalink
Add Stratis device factory
Browse files Browse the repository at this point in the history
  • Loading branch information
vojtechtrefny committed Feb 12, 2021
1 parent c30560d commit 929d221
Show file tree
Hide file tree
Showing 3 changed files with 264 additions and 3 deletions.
11 changes: 11 additions & 0 deletions blivet/blivet.py
Original file line number Diff line number Diff line change
Expand Up @@ -315,6 +315,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.
Expand Down
150 changes: 148 additions & 2 deletions blivet/devicefactory.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -84,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)

Expand Down Expand Up @@ -124,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:
Expand All @@ -143,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,
Expand Down Expand Up @@ -599,6 +604,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:
Expand Down Expand Up @@ -2015,3 +2022,142 @@ 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._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 _get_device_space(self):
""" The total disk space required for the factory device. """
return Size(0) # FIXME

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):
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()
106 changes: 105 additions & 1 deletion tests/devicefactory_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,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
Expand Down Expand Up @@ -881,3 +882,106 @@ 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())

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)

@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())

0 comments on commit 929d221

Please sign in to comment.