From 08f0e12c74e4c2ba25629fe92108283dd5ae3ff3 Mon Sep 17 00:00:00 2001 From: Vojtech Trefny Date: Thu, 30 Dec 2021 16:08:43 +0100 Subject: [PATCH 1/4] Add support for creating LVM cache pools Resolves: rhbz#2055200 --- blivet/blivet.py | 9 +- blivet/devicelibs/lvm.py | 9 ++ blivet/devices/lvm.py | 160 +++++++++++++++++++++++++++++++-- tests/devices_test/lvm_test.py | 26 ++++++ 4 files changed, 196 insertions(+), 8 deletions(-) diff --git a/blivet/blivet.py b/blivet/blivet.py index c6908eb0..d29fadd0 100644 --- a/blivet/blivet.py +++ b/blivet/blivet.py @@ -576,6 +576,8 @@ class Blivet(object): :type vdo_pool: bool :keyword vdo_lv: whether to create a vdo lv :type vdo_lv: bool + :keyword cache_pool: whether to create a cache pool + :type cache_pool: bool :returns: the new device :rtype: :class:`~.devices.LVMLogicalVolumeDevice` @@ -594,6 +596,7 @@ class Blivet(object): thin_pool = kwargs.pop("thin_pool", False) vdo_pool = kwargs.pop("vdo_pool", False) vdo_lv = kwargs.pop("vdo_lv", False) + cache_pool = kwargs.pop("cache_pool", False) parent = kwargs.get("parents", [None])[0] if (thin_volume or vdo_lv) and parent: # kwargs["parents"] will contain the pool device, so... @@ -609,6 +612,8 @@ class Blivet(object): kwargs["seg_type"] = "vdo-pool" if vdo_lv: kwargs["seg_type"] = "vdo" + if cache_pool: + kwargs["seg_type"] = "cache-pool" mountpoint = kwargs.pop("mountpoint", None) if 'fmt_type' in kwargs: @@ -640,7 +645,7 @@ class Blivet(object): swap = False prefix = "" - if thin_pool or vdo_pool: + if thin_pool or vdo_pool or cache_pool: prefix = "pool" name = self.suggest_device_name(parent=vg, @@ -651,7 +656,7 @@ class Blivet(object): if "%s-%s" % (vg.name, name) in self.names: raise ValueError("name '%s' is already in use" % name) - if thin_pool or thin_volume or vdo_pool or vdo_lv: + if thin_pool or thin_volume or vdo_pool or vdo_lv or cache_pool: cache_req = kwargs.pop("cache_request", None) if cache_req: raise ValueError("Creating cached thin and VDO volumes and pools is not supported") diff --git a/blivet/devicelibs/lvm.py b/blivet/devicelibs/lvm.py index bbde6303..23935009 100644 --- a/blivet/devicelibs/lvm.py +++ b/blivet/devicelibs/lvm.py @@ -54,6 +54,11 @@ LVM_THINP_MIN_CHUNK_SIZE = Size("64 KiB") LVM_THINP_MAX_CHUNK_SIZE = Size("1 GiB") LVM_THINP_ADDRESSABLE_CHUNK_SIZE = Size("17455015526400 B") # 15.88 TiB +# cache constants +LVM_CACHE_MIN_METADATA_SIZE = Size("8 MiB") +LVM_CACHE_MAX_METADATA_SIZE = Size("16 GiB") +LVM_CACHE_DEFAULT_MODE = blockdev.LVMCacheMode.WRITETHROUGH + raid_levels = raid.RAIDLevels(["linear", "striped", "raid1", "raid4", "raid5", "raid6", "raid10"]) raid_seg_types = list(itertools.chain.from_iterable([level.names for level in raid_levels if level.name != "linear"])) @@ -248,3 +253,7 @@ def recommend_thpool_chunk_size(thpool_size): # for every ~15.88 TiB of thinpool data size return min(math.ceil(thpool_size / LVM_THINP_ADDRESSABLE_CHUNK_SIZE) * LVM_THINP_MIN_CHUNK_SIZE, LVM_THINP_MAX_CHUNK_SIZE) + + +def is_valid_cache_md_size(md_size): + return md_size >= LVM_CACHE_MIN_METADATA_SIZE and md_size <= LVM_CACHE_MAX_METADATA_SIZE diff --git a/blivet/devices/lvm.py b/blivet/devices/lvm.py index a971da8e..7cb482ab 100644 --- a/blivet/devices/lvm.py +++ b/blivet/devices/lvm.py @@ -43,6 +43,7 @@ from .. import util from ..storage_log import log_method_call from .. import udev from ..size import Size, KiB, MiB, ROUND_UP, ROUND_DOWN +from ..static_data.lvm_info import lvs_info from ..tasks import availability import logging @@ -646,7 +647,7 @@ class LVMLogicalVolumeBase(DMDevice, RaidDevice): percent=None, cache_request=None, pvs=None, from_lvs=None): if not exists: - if seg_type not in [None, "linear", "thin", "thin-pool", "cache", "vdo-pool", "vdo"] + lvm.raid_seg_types: + if seg_type not in [None, "linear", "thin", "thin-pool", "cache", "vdo-pool", "vdo", "cache-pool"] + lvm.raid_seg_types: raise ValueError("Invalid or unsupported segment type: %s" % seg_type) if seg_type and seg_type in lvm.raid_seg_types and not pvs: raise ValueError("List of PVs has to be given for every non-linear LV") @@ -690,8 +691,8 @@ class LVMLogicalVolumeBase(DMDevice, RaidDevice): # we reserve space for it self._metadata_size = self.vg.pe_size self._size -= self._metadata_size - elif self.seg_type == "thin-pool": - # LVMThinPoolMixin sets self._metadata_size on its own + elif self.seg_type in ("thin-pool", "cache_pool"): + # LVMThinPoolMixin and LVMCachePoolMixin set self._metadata_size on their own if not self.exists and not from_lvs and not grow: # a thin pool we are not going to grow -> lets calculate metadata # size now if not given explicitly @@ -1619,7 +1620,6 @@ class LVMThinPoolMixin(object): """ A list of this pool's LVs """ return self._lvs[:] # we don't want folks changing our list - @util.requires_property("is_thin_pool") def autoset_md_size(self, enforced=False): """ If self._metadata_size not set already, it calculates the recommended value and sets it while subtracting the size from self.size. @@ -2032,9 +2032,142 @@ class LVMVDOLogicalVolumeMixin(object): self.pool._add_log_vol(self) +class LVMCachePoolMixin(object): + def __init__(self, metadata_size, cache_mode=None): + self._metadata_size = metadata_size or Size(0) + self._cache_mode = cache_mode + + def _init_check(self): + if not self.is_cache_pool: + return + + if self._metadata_size and not lvm.is_valid_cache_md_size(self._metadata_size): + raise ValueError("invalid metadatasize value") + + if not self.exists and not self._pv_specs: + raise ValueError("at least one fast PV must be specified to create a cache pool") + + def _check_from_lvs(self): + if self._from_lvs: + if len(self._from_lvs) != 2: + raise errors.DeviceError("two LVs required to create a cache pool") + + def _convert_from_lvs(self): + data_lv, metadata_lv = self._from_lvs + + data_lv.parent_lv = self # also adds the LV to self._internal_lvs + data_lv.int_lv_type = LVMInternalLVtype.data + metadata_lv.parent_lv = self + metadata_lv.int_lv_type = LVMInternalLVtype.meta + + self.size = data_lv.size + + @property + def is_cache_pool(self): + return self.seg_type == "cache-pool" + + @property + def profile(self): + return self._profile + + @property + def type(self): + return "lvmcachepool" + + @property + def resizable(self): + return False + + def read_current_size(self): + log_method_call(self, exists=self.exists, path=self.path, + sysfs_path=self.sysfs_path) + if self.size != Size(0): + return self.size + + if self.exists: + # cache pools are not active and don't have th device mapper mapping + # so we can't get this from sysfs + lv_info = lvs_info.cache.get(self.name) + if lv_info is None: + log.error("Failed to get size for existing cache pool '%s'", self.name) + return Size(0) + else: + return Size(lv_info.size) + + return Size(0) + + def autoset_md_size(self, enforced=False): + """ If self._metadata_size not set already, it calculates the recommended value + and sets it while subtracting the size from self.size. + + """ + + log.debug("Auto-setting cache pool metadata size") + + if self._size <= Size(0): + log.debug("Cache pool size not bigger than 0, just setting metadata size to 0") + self._metadata_size = 0 + return + + old_md_size = self._metadata_size + if self._metadata_size == 0 or enforced: + self._metadata_size = blockdev.lvm.cache_get_default_md_size(self._size) + log.debug("Using recommended metadata size: %s", self._metadata_size) + + self._metadata_size = self.vg.align(self._metadata_size, roundup=True) + log.debug("Rounded metadata size to extents: %s MiB", self._metadata_size.convert_to("MiB")) + + if self._metadata_size == old_md_size: + log.debug("Rounded metadata size unchanged") + else: + new_size = self.size - (self._metadata_size - old_md_size) + log.debug("Adjusting size from %s MiB to %s MiB", + self.size.convert_to("MiB"), new_size.convert_to("MiB")) + self.size = new_size + + def _pre_create(self): + # make sure all the LVs this LV should be created from exist (if any) + if self._from_lvs and any(not lv.exists for lv in self._from_lvs): + raise errors.DeviceError("Component LVs need to be created first") + + def _create(self): + """ Create the device. """ + log_method_call(self, self.name, status=self.status) + if self._cache_mode: + try: + cache_mode = blockdev.lvm.cache_get_mode_from_str(self._cache_mode) + except blockdev.LVMError as e: + raise errors.DeviceError from e + else: + cache_mode = lvm.LVM_CACHE_DEFAULT_MODE + + if self._from_lvs: + extra = dict() + if self.mode: + # we need the string here, it will be passed directly to he lvm command + extra["cachemode"] = self._cache_mode + data_lv = six.next(lv for lv in self._internal_lvs if lv.int_lv_type == LVMInternalLVtype.data) + meta_lv = six.next(lv for lv in self._internal_lvs if lv.int_lv_type == LVMInternalLVtype.meta) + blockdev.lvm.cache_pool_convert(self.vg.name, data_lv.lvname, meta_lv.lvname, self.lvname, **extra) + else: + blockdev.lvm.cache_create_pool(self.vg.name, self.lvname, self.size, + self.metadata_size, + cache_mode, + 0, + [spec.pv.path for spec in self._pv_specs]) + + def dracut_setup_args(self): + return set() + + @property + def direct(self): + """ Is this device directly accessible? """ + return False + + class LVMLogicalVolumeDevice(LVMLogicalVolumeBase, LVMInternalLogicalVolumeMixin, LVMSnapshotMixin, LVMThinPoolMixin, LVMThinLogicalVolumeMixin, LVMVDOPoolMixin, - LVMVDOLogicalVolumeMixin): + LVMVDOLogicalVolumeMixin, LVMCachePoolMixin): """ An LVM Logical Volume """ # generally resizable, see :property:`resizable` for details @@ -2046,7 +2179,7 @@ class LVMLogicalVolumeDevice(LVMLogicalVolumeBase, LVMInternalLogicalVolumeMixin parent_lv=None, int_type=None, origin=None, vorigin=False, metadata_size=None, chunk_size=None, profile=None, from_lvs=None, compression=False, deduplication=False, index_memory=0, - write_policy=None): + write_policy=None, cache_mode=None): """ :param name: the device name (generally a device node's basename) :type name: str @@ -2116,6 +2249,13 @@ class LVMLogicalVolumeDevice(LVMLogicalVolumeBase, LVMInternalLogicalVolumeMixin :keyword write_policy: write policy for the volume or None for default :type write_policy: str + For cache pools only: + + :keyword metadata_size: the size of the metadata LV + :type metadata_size: :class:`~.size.Size` + :keyword cache_mode: mode for the cache or None for default (writethrough) + :type cache_mode: str + """ if isinstance(parents, (list, ParentList)): @@ -2133,6 +2273,7 @@ class LVMLogicalVolumeDevice(LVMLogicalVolumeBase, LVMInternalLogicalVolumeMixin LVMSnapshotMixin.__init__(self, origin, vorigin) LVMThinPoolMixin.__init__(self, metadata_size, chunk_size, profile) LVMThinLogicalVolumeMixin.__init__(self) + LVMCachePoolMixin.__init__(self, metadata_size, cache_mode) LVMLogicalVolumeBase.__init__(self, name, parents, size, uuid, seg_type, fmt, exists, sysfs_path, grow, maxsize, percent, cache_request, pvs, from_lvs) @@ -2144,6 +2285,7 @@ class LVMLogicalVolumeDevice(LVMLogicalVolumeBase, LVMInternalLogicalVolumeMixin LVMSnapshotMixin._init_check(self) LVMThinPoolMixin._init_check(self) LVMThinLogicalVolumeMixin._init_check(self) + LVMCachePoolMixin._init_check(self) if self._from_lvs: self._check_from_lvs() @@ -2169,6 +2311,8 @@ class LVMLogicalVolumeDevice(LVMLogicalVolumeBase, LVMInternalLogicalVolumeMixin ret.append(LVMVDOPoolMixin) if self.is_vdo_lv: ret.append(LVMVDOLogicalVolumeMixin) + if self.is_cache_pool: + ret.append(LVMCachePoolMixin) return ret def _try_specific_call(self, name, *args, **kwargs): @@ -2552,6 +2696,10 @@ class LVMLogicalVolumeDevice(LVMLogicalVolumeBase, LVMInternalLogicalVolumeMixin return True + @type_specific + def autoset_md_size(self, enforced=False): + pass + def attach_cache(self, cache_pool_lv): if self.is_thin_lv or self.is_snapshot_lv or self.is_internal_lv: raise errors.DeviceError("Cannot attach a cache pool to the '%s' LV" % self.name) diff --git a/tests/devices_test/lvm_test.py b/tests/devices_test/lvm_test.py index 59c027da..0105bcae 100644 --- a/tests/devices_test/lvm_test.py +++ b/tests/devices_test/lvm_test.py @@ -868,3 +868,29 @@ class BlivetLVMVDODependenciesTest(unittest.TestCase): vdo_supported = devicefactory.is_supported_device_type(devicefactory.DEVICE_TYPE_LVM_VDO) self.assertFalse(vdo_supported) + + +@unittest.skipUnless(not any(x.unavailable_type_dependencies() for x in DEVICE_CLASSES), "some unsupported device classes required for this test") +class BlivetNewLVMCachePoolDeviceTest(unittest.TestCase): + + def test_new_cache_pool(self): + b = blivet.Blivet() + pv = StorageDevice("pv1", fmt=blivet.formats.get_format("lvmpv"), + size=Size("10 GiB"), exists=True) + vg = LVMVolumeGroupDevice("testvg", parents=[pv], exists=True) + + for dev in (pv, vg): + b.devicetree._add_device(dev) + + # check that all the above devices are in the expected places + self.assertEqual(set(b.devices), {pv, vg}) + self.assertEqual(set(b.vgs), {vg}) + + self.assertEqual(vg.size, Size("10236 MiB")) + + cachepool = b.new_lv(name="cachepool", cache_pool=True, + parents=[vg], pvs=[pv]) + + b.create_device(cachepool) + + self.assertEqual(cachepool.type, "lvmcachepool") -- 2.34.3 From bfb0e71a92f46baae098370207640962c97d8e77 Mon Sep 17 00:00:00 2001 From: Vojtech Trefny Date: Thu, 30 Dec 2021 16:09:04 +0100 Subject: [PATCH 2/4] examples: Add LVM cache pool example Related: rhbz#2055200 --- examples/lvm_cachepool.py | 59 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 59 insertions(+) create mode 100644 examples/lvm_cachepool.py diff --git a/examples/lvm_cachepool.py b/examples/lvm_cachepool.py new file mode 100644 index 00000000..ab2e8a72 --- /dev/null +++ b/examples/lvm_cachepool.py @@ -0,0 +1,59 @@ +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) + + pv = b.new_partition(size=Size("50GiB"), fmt_type="lvmpv", parents=[disk1]) + b.create_device(pv) + pv2 = b.new_partition(size=Size("50GiB"), fmt_type="lvmpv", parents=[disk2]) + b.create_device(pv2) + + # allocate the partitions (decide where and on which disks they'll reside) + blivet.partitioning.do_partitioning(b) + + vg = b.new_vg(parents=[pv, pv2]) + b.create_device(vg) + + # new lv with base size 5GiB and growth up to 15GiB and an ext4 filesystem + lv = b.new_lv(fmt_type="ext4", size=Size("5GiB"), parents=[vg], name="cached") + b.create_device(lv) + + # new cache pool + cpool = b.new_lv(size=Size("1 GiB"), parents=[vg], pvs=[pv2], cache_pool=True, name="fastlv") + b.create_device(cpool) + + # write the new partitions to disk and format them as specified + b.do_it() + print(b.devicetree) + + # attach the newly created cache pool to the "slow" LV + lv.attach_cache(cpool) + + b.reset() + print(b.devicetree) + + input("Check the state and hit ENTER to trigger cleanup") +finally: + b.devicetree.teardown_disk_images() + os.unlink(disk1_file) + os.unlink(disk2_file) -- 2.34.3 From 1fece0e7f15f7b0d457d3db876d23c3272df09bd Mon Sep 17 00:00:00 2001 From: Vojtech Trefny Date: Thu, 30 Dec 2021 16:13:33 +0100 Subject: [PATCH 3/4] lvm: Use blivet static data when checking if the VG is active Instead of calling 'lvs' again in LVMVolumeGroupDevice.status Related: rhbz#2055200 --- blivet/devices/lvm.py | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/blivet/devices/lvm.py b/blivet/devices/lvm.py index 7cb482ab..12d3d073 100644 --- a/blivet/devices/lvm.py +++ b/blivet/devices/lvm.py @@ -220,13 +220,8 @@ class LVMVolumeGroupDevice(ContainerDevice): # special handling for incomplete VGs if not self.complete: - try: - lvs_info = blockdev.lvm.lvs(vg_name=self.name) - except blockdev.LVMError: - lvs_info = [] - - for lv_info in lvs_info: - if lv_info.attr and lv_info.attr[4] == 'a': + for lv_info in lvs_info.cache.values(): + if lv_info.vg_name == self.name and lv_info.attr and lv_info.attr[4] == 'a': return True return False -- 2.34.3 From 8d957f04c2d5f56386b978d1bf890450f38ad108 Mon Sep 17 00:00:00 2001 From: Vojtech Trefny Date: Mon, 30 May 2022 17:02:43 +0200 Subject: [PATCH 4/4] Add option to attach a newly created cache pool to existing LV Because we do not have action for attaching the cache pool, we cannot schedule both adding the fast PV to the VG and attaching the cache pool to existing LV. This hack allows to schedule the attach to happen after the cache pool is created. Related: rhbz#2055200 --- blivet/devices/lvm.py | 38 +++++++++++++++++++++++++++++++++++--- 1 file changed, 35 insertions(+), 3 deletions(-) diff --git a/blivet/devices/lvm.py b/blivet/devices/lvm.py index 12d3d073..feb92f2e 100644 --- a/blivet/devices/lvm.py +++ b/blivet/devices/lvm.py @@ -2028,9 +2028,10 @@ class LVMVDOLogicalVolumeMixin(object): class LVMCachePoolMixin(object): - def __init__(self, metadata_size, cache_mode=None): + def __init__(self, metadata_size, cache_mode=None, attach_to=None): self._metadata_size = metadata_size or Size(0) self._cache_mode = cache_mode + self._attach_to = attach_to def _init_check(self): if not self.is_cache_pool: @@ -2042,6 +2043,9 @@ class LVMCachePoolMixin(object): if not self.exists and not self._pv_specs: raise ValueError("at least one fast PV must be specified to create a cache pool") + if self._attach_to and not self._attach_to.exists: + raise ValueError("cache pool can be attached only to an existing LV") + def _check_from_lvs(self): if self._from_lvs: if len(self._from_lvs) != 2: @@ -2150,6 +2154,31 @@ class LVMCachePoolMixin(object): cache_mode, 0, [spec.pv.path for spec in self._pv_specs]) + if self._attach_to: + self._attach_to.attach_cache(self) + + def _post_create(self): + if self._attach_to: + # post_create tries to activate the LV and after attaching it no longer exists + return + + # pylint: disable=bad-super-call + super(LVMLogicalVolumeBase, self)._post_create() + + def add_hook(self, new=True): + if self._attach_to: + self._attach_to._cache = LVMCache(self._attach_to, size=self.size, exists=False, + pvs=self._pv_specs, mode=self._cache_mode) + + # pylint: disable=bad-super-call + super(LVMLogicalVolumeBase, self).add_hook(new=new) + + def remove_hook(self, modparent=True): + if self._attach_to: + self._attach_to._cache = None + + # pylint: disable=bad-super-call + super(LVMLogicalVolumeBase, self).remove_hook(modparent=modparent) def dracut_setup_args(self): return set() @@ -2174,7 +2203,7 @@ class LVMLogicalVolumeDevice(LVMLogicalVolumeBase, LVMInternalLogicalVolumeMixin parent_lv=None, int_type=None, origin=None, vorigin=False, metadata_size=None, chunk_size=None, profile=None, from_lvs=None, compression=False, deduplication=False, index_memory=0, - write_policy=None, cache_mode=None): + write_policy=None, cache_mode=None, attach_to=None): """ :param name: the device name (generally a device node's basename) :type name: str @@ -2250,6 +2279,9 @@ class LVMLogicalVolumeDevice(LVMLogicalVolumeBase, LVMInternalLogicalVolumeMixin :type metadata_size: :class:`~.size.Size` :keyword cache_mode: mode for the cache or None for default (writethrough) :type cache_mode: str + :keyword attach_to: for non-existing cache pools a logical volume the pool should + be attached to when created + :type attach_to: :class:`LVMLogicalVolumeDevice` """ @@ -2268,7 +2300,7 @@ class LVMLogicalVolumeDevice(LVMLogicalVolumeBase, LVMInternalLogicalVolumeMixin LVMSnapshotMixin.__init__(self, origin, vorigin) LVMThinPoolMixin.__init__(self, metadata_size, chunk_size, profile) LVMThinLogicalVolumeMixin.__init__(self) - LVMCachePoolMixin.__init__(self, metadata_size, cache_mode) + LVMCachePoolMixin.__init__(self, metadata_size, cache_mode, attach_to) LVMLogicalVolumeBase.__init__(self, name, parents, size, uuid, seg_type, fmt, exists, sysfs_path, grow, maxsize, percent, cache_request, pvs, from_lvs) -- 2.34.3