4cd9fd
From 3f6bbf52442609b8e6e3919a3fdd8c5af64923e6 Mon Sep 17 00:00:00 2001
4cd9fd
From: Vojtech Trefny <vtrefny@redhat.com>
4cd9fd
Date: Tue, 12 May 2020 12:48:41 +0200
4cd9fd
Subject: [PATCH 1/3] Add basic support for LVM VDO devices
4cd9fd
4cd9fd
This adds support for LVM VDO devices detection during populate
4cd9fd
and allows removing both VDO LVs and VDO pools using actions.
4cd9fd
---
4cd9fd
 blivet/devices/lvm.py           | 150 +++++++++++++++++++++++++++++++-
4cd9fd
 blivet/populator/helpers/lvm.py |  16 +++-
4cd9fd
 tests/action_test.py            |  39 +++++++++
4cd9fd
 tests/devices_test/lvm_test.py  |  34 ++++++++
4cd9fd
 tests/storagetestcase.py        |  11 ++-
4cd9fd
 5 files changed, 245 insertions(+), 5 deletions(-)
4cd9fd
4cd9fd
diff --git a/blivet/devices/lvm.py b/blivet/devices/lvm.py
4cd9fd
index 97de6acd..d9e24a33 100644
4cd9fd
--- a/blivet/devices/lvm.py
4cd9fd
+++ b/blivet/devices/lvm.py
4cd9fd
@@ -1789,8 +1789,132 @@ class LVMThinLogicalVolumeMixin(object):
4cd9fd
         data.pool_name = self.pool.lvname
4cd9fd
 
4cd9fd
 
4cd9fd
+class LVMVDOPoolMixin(object):
4cd9fd
+    def __init__(self):
4cd9fd
+        self._lvs = []
4cd9fd
+
4cd9fd
+    @property
4cd9fd
+    def is_vdo_pool(self):
4cd9fd
+        return self.seg_type == "vdo-pool"
4cd9fd
+
4cd9fd
+    @property
4cd9fd
+    def type(self):
4cd9fd
+        return "lvmvdopool"
4cd9fd
+
4cd9fd
+    @property
4cd9fd
+    def resizable(self):
4cd9fd
+        return False
4cd9fd
+
4cd9fd
+    @util.requires_property("is_vdo_pool")
4cd9fd
+    def _add_log_vol(self, lv):
4cd9fd
+        """ Add an LV to this VDO pool. """
4cd9fd
+        if lv in self._lvs:
4cd9fd
+            raise ValueError("lv is already part of this VDO pool")
4cd9fd
+
4cd9fd
+        self.vg._add_log_vol(lv)
4cd9fd
+        log.debug("Adding %s/%s to %s", lv.name, lv.size, self.name)
4cd9fd
+        self._lvs.append(lv)
4cd9fd
+
4cd9fd
+    @util.requires_property("is_vdo_pool")
4cd9fd
+    def _remove_log_vol(self, lv):
4cd9fd
+        """ Remove an LV from this VDO pool. """
4cd9fd
+        if lv not in self._lvs:
4cd9fd
+            raise ValueError("specified lv is not part of this VDO pool")
4cd9fd
+
4cd9fd
+        self._lvs.remove(lv)
4cd9fd
+        self.vg._remove_log_vol(lv)
4cd9fd
+
4cd9fd
+    @property
4cd9fd
+    @util.requires_property("is_vdo_pool")
4cd9fd
+    def lvs(self):
4cd9fd
+        """ A list of this VDO pool's LVs """
4cd9fd
+        return self._lvs[:]     # we don't want folks changing our list
4cd9fd
+
4cd9fd
+    @property
4cd9fd
+    def direct(self):
4cd9fd
+        """ Is this device directly accessible? """
4cd9fd
+        return False
4cd9fd
+
4cd9fd
+    def _create(self):
4cd9fd
+        """ Create the device. """
4cd9fd
+        raise NotImplementedError
4cd9fd
+
4cd9fd
+
4cd9fd
+class LVMVDOLogicalVolumeMixin(object):
4cd9fd
+    def __init__(self):
4cd9fd
+        pass
4cd9fd
+
4cd9fd
+    def _init_check(self):
4cd9fd
+        pass
4cd9fd
+
4cd9fd
+    def _check_parents(self):
4cd9fd
+        """Check that this device has parents as expected"""
4cd9fd
+        if isinstance(self.parents, (list, ParentList)):
4cd9fd
+            if len(self.parents) != 1:
4cd9fd
+                raise ValueError("constructor requires a single vdo-pool LV")
4cd9fd
+
4cd9fd
+            container = self.parents[0]
4cd9fd
+        else:
4cd9fd
+            container = self.parents
4cd9fd
+
4cd9fd
+        if not container or not isinstance(container, LVMLogicalVolumeDevice) or not container.is_vdo_pool:
4cd9fd
+            raise ValueError("constructor requires a vdo-pool LV")
4cd9fd
+
4cd9fd
+    @property
4cd9fd
+    def vg_space_used(self):
4cd9fd
+        return Size(0)    # the pool's size is already accounted for in the vg
4cd9fd
+
4cd9fd
+    @property
4cd9fd
+    def is_vdo_lv(self):
4cd9fd
+        return self.seg_type == "vdo"
4cd9fd
+
4cd9fd
+    @property
4cd9fd
+    def vg(self):
4cd9fd
+        # parents[0] is the pool, not the VG so set the VG here
4cd9fd
+        return self.pool.vg
4cd9fd
+
4cd9fd
+    @property
4cd9fd
+    def type(self):
4cd9fd
+        return "vdolv"
4cd9fd
+
4cd9fd
+    @property
4cd9fd
+    def resizable(self):
4cd9fd
+        return False
4cd9fd
+
4cd9fd
+    @property
4cd9fd
+    @util.requires_property("is_vdo_lv")
4cd9fd
+    def pool(self):
4cd9fd
+        return self.parents[0]
4cd9fd
+
4cd9fd
+    def _create(self):
4cd9fd
+        """ Create the device. """
4cd9fd
+        raise NotImplementedError
4cd9fd
+
4cd9fd
+    def _destroy(self):
4cd9fd
+        # nothing to do here, VDO LV is destroyed automatically together with
4cd9fd
+        # the VDO pool
4cd9fd
+        pass
4cd9fd
+
4cd9fd
+    def remove_hook(self, modparent=True):
4cd9fd
+        if modparent:
4cd9fd
+            self.pool._remove_log_vol(self)
4cd9fd
+
4cd9fd
+        # pylint: disable=bad-super-call
4cd9fd
+        super(LVMLogicalVolumeBase, self).remove_hook(modparent=modparent)
4cd9fd
+
4cd9fd
+    def add_hook(self, new=True):
4cd9fd
+        # pylint: disable=bad-super-call
4cd9fd
+        super(LVMLogicalVolumeBase, self).add_hook(new=new)
4cd9fd
+        if new:
4cd9fd
+            return
4cd9fd
+
4cd9fd
+        if self not in self.pool.lvs:
4cd9fd
+            self.pool._add_log_vol(self)
4cd9fd
+
4cd9fd
+
4cd9fd
 class LVMLogicalVolumeDevice(LVMLogicalVolumeBase, LVMInternalLogicalVolumeMixin, LVMSnapshotMixin,
4cd9fd
-                             LVMThinPoolMixin, LVMThinLogicalVolumeMixin):
4cd9fd
+                             LVMThinPoolMixin, LVMThinLogicalVolumeMixin, LVMVDOPoolMixin,
4cd9fd
+                             LVMVDOLogicalVolumeMixin):
4cd9fd
     """ An LVM Logical Volume """
4cd9fd
 
4cd9fd
     # generally resizable, see :property:`resizable` for details
4cd9fd
@@ -1879,6 +2003,8 @@ class LVMLogicalVolumeDevice(LVMLogicalVolumeBase, LVMInternalLogicalVolumeMixin
4cd9fd
         LVMLogicalVolumeBase.__init__(self, name, parents, size, uuid, seg_type,
4cd9fd
                                       fmt, exists, sysfs_path, grow, maxsize,
4cd9fd
                                       percent, cache_request, pvs, from_lvs)
4cd9fd
+        LVMVDOPoolMixin.__init__(self)
4cd9fd
+        LVMVDOLogicalVolumeMixin.__init__(self)
4cd9fd
 
4cd9fd
         LVMInternalLogicalVolumeMixin._init_check(self)
4cd9fd
         LVMSnapshotMixin._init_check(self)
4cd9fd
@@ -1905,6 +2031,10 @@ class LVMLogicalVolumeDevice(LVMLogicalVolumeBase, LVMInternalLogicalVolumeMixin
4cd9fd
             ret.append(LVMThinPoolMixin)
4cd9fd
         if self.is_thin_lv:
4cd9fd
             ret.append(LVMThinLogicalVolumeMixin)
4cd9fd
+        if self.is_vdo_pool:
4cd9fd
+            ret.append(LVMVDOPoolMixin)
4cd9fd
+        if self.is_vdo_lv:
4cd9fd
+            ret.append(LVMVDOLogicalVolumeMixin)
4cd9fd
         return ret
4cd9fd
 
4cd9fd
     def _try_specific_call(self, name, *args, **kwargs):
4cd9fd
@@ -2066,6 +2196,11 @@ class LVMLogicalVolumeDevice(LVMLogicalVolumeBase, LVMInternalLogicalVolumeMixin
4cd9fd
     def display_lv_name(self):
4cd9fd
         return self.lvname
4cd9fd
 
4cd9fd
+    @property
4cd9fd
+    @type_specific
4cd9fd
+    def pool(self):
4cd9fd
+        return super(LVMLogicalVolumeDevice, self).pool
4cd9fd
+
4cd9fd
     def _setup(self, orig=False):
4cd9fd
         """ Open, or set up, a device. """
4cd9fd
         log_method_call(self, self.name, orig=orig, status=self.status,
4cd9fd
@@ -2167,6 +2302,19 @@ class LVMLogicalVolumeDevice(LVMLogicalVolumeBase, LVMInternalLogicalVolumeMixin
4cd9fd
         udev.settle()
4cd9fd
         blockdev.lvm.lvresize(self.vg.name, self._name, self.size)
4cd9fd
 
4cd9fd
+    @type_specific
4cd9fd
+    def _add_log_vol(self, lv):
4cd9fd
+        pass
4cd9fd
+
4cd9fd
+    @type_specific
4cd9fd
+    def _remove_log_vol(self, lv):
4cd9fd
+        pass
4cd9fd
+
4cd9fd
+    @property
4cd9fd
+    @type_specific
4cd9fd
+    def lvs(self):
4cd9fd
+        return []
4cd9fd
+
4cd9fd
     @property
4cd9fd
     @type_specific
4cd9fd
     def direct(self):
4cd9fd
diff --git a/blivet/populator/helpers/lvm.py b/blivet/populator/helpers/lvm.py
4cd9fd
index 4b674fac..ff8bf59f 100644
4cd9fd
--- a/blivet/populator/helpers/lvm.py
4cd9fd
+++ b/blivet/populator/helpers/lvm.py
4cd9fd
@@ -211,9 +211,6 @@ class LVMFormatPopulator(FormatPopulator):
4cd9fd
                     origin = self._devicetree.get_device_by_name(origin_device_name)
4cd9fd
 
4cd9fd
                 lv_kwargs["origin"] = origin
4cd9fd
-            elif lv_attr[0] == 'v':
4cd9fd
-                # skip vorigins
4cd9fd
-                return
4cd9fd
             elif lv_attr[0] in 'IrielTCo' and lv_name.endswith(']'):
4cd9fd
                 # an internal LV, add the an instance of the appropriate class
4cd9fd
                 # to internal_lvs for later processing when non-internal LVs are
4cd9fd
@@ -237,6 +234,19 @@ class LVMFormatPopulator(FormatPopulator):
4cd9fd
                     origin = self._devicetree.get_device_by_name(origin_device_name)
4cd9fd
                     lv_kwargs["origin"] = origin
4cd9fd
 
4cd9fd
+                lv_parents = [self._devicetree.get_device_by_name(pool_device_name)]
4cd9fd
+            elif lv_attr[0] == 'd':
4cd9fd
+                # vdo pool
4cd9fd
+                # nothing to do here
4cd9fd
+                pass
4cd9fd
+            elif lv_attr[0] == 'v':
4cd9fd
+                if lv_type != "vdo":
4cd9fd
+                    # skip vorigins
4cd9fd
+                    return
4cd9fd
+                pool_name = blockdev.lvm.vdolvpoolname(vg_name, lv_name)
4cd9fd
+                pool_device_name = "%s-%s" % (vg_name, pool_name)
4cd9fd
+                add_required_lv(pool_device_name, "failed to look up VDO pool")
4cd9fd
+
4cd9fd
                 lv_parents = [self._devicetree.get_device_by_name(pool_device_name)]
4cd9fd
             elif lv_name.endswith(']'):
4cd9fd
                 # unrecognized Internal LVM2 device
4cd9fd
diff --git a/tests/action_test.py b/tests/action_test.py
4cd9fd
index 90c1b312..8f9a7424 100644
4cd9fd
--- a/tests/action_test.py
4cd9fd
+++ b/tests/action_test.py
4cd9fd
@@ -1252,6 +1252,45 @@ class DeviceActionTestCase(StorageTestCase):
4cd9fd
         self.assertEqual(set(self.storage.lvs), {pool})
4cd9fd
         self.assertEqual(set(pool._internal_lvs), {lv1, lv2})
4cd9fd
 
4cd9fd
+    def test_lvm_vdo_destroy(self):
4cd9fd
+        self.destroy_all_devices()
4cd9fd
+        sdc = self.storage.devicetree.get_device_by_name("sdc")
4cd9fd
+        sdc1 = self.new_device(device_class=PartitionDevice, name="sdc1",
4cd9fd
+                               size=Size("50 GiB"), parents=[sdc],
4cd9fd
+                               fmt=blivet.formats.get_format("lvmpv"))
4cd9fd
+        self.schedule_create_device(sdc1)
4cd9fd
+
4cd9fd
+        vg = self.new_device(device_class=LVMVolumeGroupDevice,
4cd9fd
+                             name="vg", parents=[sdc1])
4cd9fd
+        self.schedule_create_device(vg)
4cd9fd
+
4cd9fd
+        pool = self.new_device(device_class=LVMLogicalVolumeDevice,
4cd9fd
+                               name="data", parents=[vg],
4cd9fd
+                               size=Size("10 GiB"),
4cd9fd
+                               seg_type="vdo-pool", exists=True)
4cd9fd
+        self.storage.devicetree._add_device(pool)
4cd9fd
+        lv = self.new_device(device_class=LVMLogicalVolumeDevice,
4cd9fd
+                             name="meta", parents=[pool],
4cd9fd
+                             size=Size("50 GiB"),
4cd9fd
+                             seg_type="vdo", exists=True)
4cd9fd
+        self.storage.devicetree._add_device(lv)
4cd9fd
+
4cd9fd
+        remove_lv = self.schedule_destroy_device(lv)
4cd9fd
+        self.assertListEqual(pool.lvs, [])
4cd9fd
+        self.assertNotIn(lv, vg.lvs)
4cd9fd
+
4cd9fd
+        # cancelling the action should put lv back to both vg and pool lvs
4cd9fd
+        self.storage.devicetree.actions.remove(remove_lv)
4cd9fd
+        self.assertListEqual(pool.lvs, [lv])
4cd9fd
+        self.assertIn(lv, vg.lvs)
4cd9fd
+
4cd9fd
+        # can't remove non-leaf pool
4cd9fd
+        with self.assertRaises(ValueError):
4cd9fd
+            self.schedule_destroy_device(pool)
4cd9fd
+
4cd9fd
+        self.schedule_destroy_device(lv)
4cd9fd
+        self.schedule_destroy_device(pool)
4cd9fd
+
4cd9fd
 
4cd9fd
 class ConfigurationActionsTest(unittest.TestCase):
4cd9fd
 
4cd9fd
diff --git a/tests/devices_test/lvm_test.py b/tests/devices_test/lvm_test.py
4cd9fd
index 9e701d18..204cb99a 100644
4cd9fd
--- a/tests/devices_test/lvm_test.py
4cd9fd
+++ b/tests/devices_test/lvm_test.py
4cd9fd
@@ -405,6 +405,40 @@ class LVMDeviceTest(unittest.TestCase):
4cd9fd
                                exists=False)
4cd9fd
         self.assertFalse(vg.is_empty)
4cd9fd
 
4cd9fd
+    def test_lvm_vdo_pool(self):
4cd9fd
+        pv = StorageDevice("pv1", fmt=blivet.formats.get_format("lvmpv"),
4cd9fd
+                           size=Size("1 GiB"), exists=True)
4cd9fd
+        vg = LVMVolumeGroupDevice("testvg", parents=[pv])
4cd9fd
+        pool = LVMLogicalVolumeDevice("testpool", parents=[vg], size=Size("512 MiB"),
4cd9fd
+                                      seg_type="vdo-pool", exists=True)
4cd9fd
+        self.assertTrue(pool.is_vdo_pool)
4cd9fd
+
4cd9fd
+        free = vg.free_space
4cd9fd
+        lv = LVMLogicalVolumeDevice("testlv", parents=[pool], size=Size("2 GiB"),
4cd9fd
+                                    seg_type="vdo", exists=True)
4cd9fd
+        self.assertTrue(lv.is_vdo_lv)
4cd9fd
+        self.assertEqual(lv.vg, vg)
4cd9fd
+        self.assertEqual(lv.pool, pool)
4cd9fd
+
4cd9fd
+        # free space in the vg shouldn't be affected by the vdo lv
4cd9fd
+        self.assertEqual(lv.vg_space_used, 0)
4cd9fd
+        self.assertEqual(free, vg.free_space)
4cd9fd
+
4cd9fd
+        self.assertListEqual(pool.lvs, [lv])
4cd9fd
+
4cd9fd
+        # now try to destroy both the pool and the vdo lv
4cd9fd
+        # for the lv this should be a no-op, destroying the pool should destroy both
4cd9fd
+        with patch("blivet.devices.lvm.blockdev.lvm") as lvm:
4cd9fd
+            lv.destroy()
4cd9fd
+            lv.remove_hook()
4cd9fd
+            self.assertFalse(lv.exists)
4cd9fd
+            self.assertFalse(lvm.lvremove.called)
4cd9fd
+            self.assertListEqual(pool.lvs, [])
4cd9fd
+
4cd9fd
+            pool.destroy()
4cd9fd
+            self.assertFalse(pool.exists)
4cd9fd
+            self.assertTrue(lvm.lvremove.called)
4cd9fd
+
4cd9fd
 
4cd9fd
 class TypeSpecificCallsTest(unittest.TestCase):
4cd9fd
     def test_type_specific_calls(self):
4cd9fd
diff --git a/tests/storagetestcase.py b/tests/storagetestcase.py
4cd9fd
index e581bca6..1844dec5 100644
4cd9fd
--- a/tests/storagetestcase.py
4cd9fd
+++ b/tests/storagetestcase.py
4cd9fd
@@ -96,7 +96,16 @@ class StorageTestCase(unittest.TestCase):
4cd9fd
     def new_device(self, *args, **kwargs):
4cd9fd
         """ Return a new Device instance suitable for testing. """
4cd9fd
         device_class = kwargs.pop("device_class")
4cd9fd
-        exists = kwargs.pop("exists", False)
4cd9fd
+
4cd9fd
+        # we intentionally don't pass the "exists" kwarg to the constructor
4cd9fd
+        # becauses this causes issues with some devices (especially partitions)
4cd9fd
+        # but we still need it for some LVs like VDO because we can't create
4cd9fd
+        # those so we need to fake their existence even for the constructor
4cd9fd
+        if device_class is blivet.devices.LVMLogicalVolumeDevice:
4cd9fd
+            exists = kwargs.get("exists", False)
4cd9fd
+        else:
4cd9fd
+            exists = kwargs.pop("exists", False)
4cd9fd
+
4cd9fd
         part_type = kwargs.pop("part_type", parted.PARTITION_NORMAL)
4cd9fd
         device = device_class(*args, **kwargs)
4cd9fd
 
4cd9fd
-- 
4cd9fd
2.26.2
4cd9fd
4cd9fd
4cd9fd
From f05a66e1bed1ca1f3cd7d7ffecd6693ab4d7f32a Mon Sep 17 00:00:00 2001
4cd9fd
From: Vojtech Trefny <vtrefny@redhat.com>
4cd9fd
Date: Tue, 12 May 2020 12:52:47 +0200
4cd9fd
Subject: [PATCH 2/3] Fix checking for filesystem support in action_test
4cd9fd
4cd9fd
---
4cd9fd
 tests/action_test.py | 2 +-
4cd9fd
 1 file changed, 1 insertion(+), 1 deletion(-)
4cd9fd
4cd9fd
diff --git a/tests/action_test.py b/tests/action_test.py
4cd9fd
index 8f9a7424..228eb97a 100644
4cd9fd
--- a/tests/action_test.py
4cd9fd
+++ b/tests/action_test.py
4cd9fd
@@ -56,7 +56,7 @@ FORMAT_CLASSES = [
4cd9fd
 
4cd9fd
 
4cd9fd
 @unittest.skipUnless(not any(x.unavailable_type_dependencies() for x in DEVICE_CLASSES), "some unsupported device classes required for this test")
4cd9fd
-@unittest.skipUnless(not any(x().utils_available for x in FORMAT_CLASSES), "some unsupported format classes required for this test")
4cd9fd
+@unittest.skipUnless(all(x().utils_available for x in FORMAT_CLASSES), "some unsupported format classes required for this test")
4cd9fd
 class DeviceActionTestCase(StorageTestCase):
4cd9fd
 
4cd9fd
     """ DeviceActionTestSuite """
4cd9fd
-- 
4cd9fd
2.26.2
4cd9fd
4cd9fd
4cd9fd
From 69bd2e69e21c8779377a6f54b3d83cb35138867a Mon Sep 17 00:00:00 2001
4cd9fd
From: Vojtech Trefny <vtrefny@redhat.com>
4cd9fd
Date: Tue, 12 May 2020 12:54:03 +0200
4cd9fd
Subject: [PATCH 3/3] Fix LV min size for resize in test_action_dependencies
4cd9fd
4cd9fd
We've recently changed min size for all filesystems so we can't
4cd9fd
resize the LV to the device minimal size.
4cd9fd
This was overlooked in the original change because these tests
4cd9fd
were skipped.
4cd9fd
---
4cd9fd
 tests/action_test.py | 2 +-
4cd9fd
 1 file changed, 1 insertion(+), 1 deletion(-)
4cd9fd
4cd9fd
diff --git a/tests/action_test.py b/tests/action_test.py
4cd9fd
index 228eb97a..77176f46 100644
4cd9fd
--- a/tests/action_test.py
4cd9fd
+++ b/tests/action_test.py
4cd9fd
@@ -870,7 +870,7 @@ class DeviceActionTestCase(StorageTestCase):
4cd9fd
                                   name="testlv2", parents=[testvg])
4cd9fd
         testlv2.format = self.new_format("ext4", device=testlv2.path,
4cd9fd
                                          exists=True, device_instance=testlv2)
4cd9fd
-        shrink_lv2 = ActionResizeDevice(testlv2, testlv2.size - Size("10 GiB"))
4cd9fd
+        shrink_lv2 = ActionResizeDevice(testlv2, testlv2.size - Size("10 GiB") + Ext4FS._min_size)
4cd9fd
         shrink_lv2.apply()
4cd9fd
 
4cd9fd
         self.assertTrue(grow_lv.requires(shrink_lv2))
4cd9fd
-- 
4cd9fd
2.26.2
4cd9fd