diff --git a/SOURCES/0012-xfs-grow-support.patch b/SOURCES/0012-xfs-grow-support.patch new file mode 100644 index 0000000..1607c51 --- /dev/null +++ b/SOURCES/0012-xfs-grow-support.patch @@ -0,0 +1,459 @@ +From 433d863cd8a57e5fc30948ff905e6a477ed5f17c Mon Sep 17 00:00:00 2001 +From: Vojtech Trefny +Date: Tue, 14 Jul 2020 11:27:08 +0200 +Subject: [PATCH 1/4] Add support for XFS format grow + +--- + blivet/formats/fs.py | 2 ++ + blivet/tasks/availability.py | 1 + + blivet/tasks/fsresize.py | 54 ++++++++++++++++++++++++++++++++++++ + 3 files changed, 57 insertions(+) + +diff --git a/blivet/formats/fs.py b/blivet/formats/fs.py +index eee15aaa..12cb9885 100644 +--- a/blivet/formats/fs.py ++++ b/blivet/formats/fs.py +@@ -1089,11 +1089,13 @@ class XFS(FS): + _formattable = True + _linux_native = True + _supported = True ++ _resizable = True + _packages = ["xfsprogs"] + _info_class = fsinfo.XFSInfo + _mkfs_class = fsmkfs.XFSMkfs + _readlabel_class = fsreadlabel.XFSReadLabel + _size_info_class = fssize.XFSSize ++ _resize_class = fsresize.XFSResize + _sync_class = fssync.XFSSync + _writelabel_class = fswritelabel.XFSWriteLabel + _writeuuid_class = fswriteuuid.XFSWriteUUID +diff --git a/blivet/tasks/availability.py b/blivet/tasks/availability.py +index b6b5955a..df62780c 100644 +--- a/blivet/tasks/availability.py ++++ b/blivet/tasks/availability.py +@@ -455,5 +455,6 @@ TUNE2FS_APP = application_by_version("tune2fs", E2FSPROGS_VERSION) + XFSADMIN_APP = application("xfs_admin") + XFSDB_APP = application("xfs_db") + XFSFREEZE_APP = application("xfs_freeze") ++XFSRESIZE_APP = application("xfs_growfs") + + MOUNT_APP = application("mount") +diff --git a/blivet/tasks/fsresize.py b/blivet/tasks/fsresize.py +index e7e26984..12c0367f 100644 +--- a/blivet/tasks/fsresize.py ++++ b/blivet/tasks/fsresize.py +@@ -20,7 +20,10 @@ + # Red Hat Author(s): Anne Mulhern + + import abc ++import os ++import tempfile + ++from contextlib import contextmanager + from six import add_metaclass + + from ..errors import FSError +@@ -32,6 +35,9 @@ from . import task + from . import fstask + from . import dfresize + ++import logging ++log = logging.getLogger("blivet") ++ + + @add_metaclass(abc.ABCMeta) + class FSResizeTask(fstask.FSTask): +@@ -115,6 +121,54 @@ class NTFSResize(FSResize): + ] + + ++class XFSResize(FSResize): ++ ext = availability.XFSRESIZE_APP ++ unit = B ++ size_fmt = None ++ ++ @contextmanager ++ def _do_temp_mount(self): ++ if self.fs.status: ++ yield ++ else: ++ dev_name = os.path.basename(self.fs.device) ++ tmpdir = tempfile.mkdtemp(prefix="xfs-tempmount-%s" % dev_name) ++ log.debug("mounting XFS on '%s' to '%s' for resize", self.fs.device, tmpdir) ++ try: ++ self.fs.mount(mountpoint=tmpdir) ++ except FSError as e: ++ raise FSError("Failed to mount XFS filesystem for resize: %s" % str(e)) ++ ++ try: ++ yield ++ finally: ++ util.umount(mountpoint=tmpdir) ++ os.rmdir(tmpdir) ++ ++ def _get_block_size(self): ++ if self.fs._current_info: ++ # this should be set by update_size_info() ++ for line in self.fs._current_info.split("\n"): ++ if line.startswith("blocksize ="): ++ return int(line.split("=")[-1]) ++ ++ raise FSError("Failed to get XFS filesystem block size for resize") ++ ++ def size_spec(self): ++ # size for xfs_growfs is in blocks ++ return str(self.fs.target_size.convert_to(self.unit) / self._get_block_size()) ++ ++ @property ++ def args(self): ++ return [self.fs.system_mountpoint, "-D", self.size_spec()] ++ ++ def do_task(self): ++ """ Resizes the XFS format. """ ++ ++ with self._do_temp_mount(): ++ super(XFSResize, self).do_task() ++ ++ + class TmpFSResize(FSResize): + + ext = availability.MOUNT_APP +-- +2.26.2 + + +From 56d05334231c30699a9c77dedbc23fdb021b9dee Mon Sep 17 00:00:00 2001 +From: Vojtech Trefny +Date: Tue, 14 Jul 2020 11:27:51 +0200 +Subject: [PATCH 2/4] Add tests for XFS resize + +XFS supports only grow so we can't reuse most of the fstesting +code and we also need to test the resize on partition because +XFS won't allow grow to size bigger than the underlying block +device. +--- + tests/formats_test/fs_test.py | 91 +++++++++++++++++++++++++++++++++ + tests/formats_test/fstesting.py | 33 ++++++------ + 2 files changed, 107 insertions(+), 17 deletions(-) + +diff --git a/tests/formats_test/fs_test.py b/tests/formats_test/fs_test.py +index 15fc0c35..9bc5d20d 100644 +--- a/tests/formats_test/fs_test.py ++++ b/tests/formats_test/fs_test.py +@@ -2,8 +2,13 @@ import os + import tempfile + import unittest + ++import parted ++ + import blivet.formats.fs as fs + from blivet.size import Size, ROUND_DOWN ++from blivet.errors import DeviceFormatError ++from blivet.formats import get_format ++from blivet.devices import PartitionDevice, DiskDevice + + from tests import loopbackedtestcase + +@@ -50,6 +55,92 @@ class ReiserFSTestCase(fstesting.FSAsRoot): + class XFSTestCase(fstesting.FSAsRoot): + _fs_class = fs.XFS + ++ def can_resize(self, an_fs): ++ resize_tasks = (an_fs._resize, an_fs._size_info) ++ return not any(t.availability_errors for t in resize_tasks) ++ ++ def _create_partition(self, disk, size): ++ disk.format = get_format("disklabel", device=disk.path, label_type="msdos") ++ disk.format.create() ++ pstart = disk.format.alignment.grainSize ++ pend = pstart + int(Size(size) / disk.format.parted_device.sectorSize) ++ disk.format.add_partition(pstart, pend, parted.PARTITION_NORMAL) ++ disk.format.parted_disk.commit() ++ part = disk.format.parted_disk.getPartitionBySector(pstart) ++ ++ device = PartitionDevice(os.path.basename(part.path)) ++ device.disk = disk ++ device.exists = True ++ device.parted_partition = part ++ ++ return device ++ ++ def _remove_partition(self, partition, disk): ++ disk.format.remove_partition(partition.parted_partition) ++ disk.format.parted_disk.commit() ++ ++ def test_resize(self): ++ an_fs = self._fs_class() ++ if not an_fs.formattable: ++ self.skipTest("can not create filesystem %s" % an_fs.name) ++ an_fs.device = self.loop_devices[0] ++ self.assertIsNone(an_fs.create()) ++ an_fs.update_size_info() ++ ++ self._test_sizes(an_fs) ++ # CHECKME: target size is still 0 after updated_size_info is called. ++ self.assertEqual(an_fs.size, Size(0) if an_fs.resizable else an_fs._size) ++ ++ if not self.can_resize(an_fs): ++ self.assertFalse(an_fs.resizable) ++ # Not resizable, so can not do resizing actions. ++ with self.assertRaises(DeviceFormatError): ++ an_fs.target_size = Size("64 MiB") ++ with self.assertRaises(DeviceFormatError): ++ an_fs.do_resize() ++ else: ++ disk = DiskDevice(os.path.basename(self.loop_devices[0])) ++ part = self._create_partition(disk, Size("50 MiB")) ++ an_fs = self._fs_class() ++ an_fs.device = part.path ++ self.assertIsNone(an_fs.create()) ++ an_fs.update_size_info() ++ ++ self.assertTrue(an_fs.resizable) ++ ++ # grow the partition so we can grow the filesystem ++ self._remove_partition(part, disk) ++ part = self._create_partition(disk, size=part.size + Size("40 MiB")) ++ ++ # Try a reasonable target size ++ TARGET_SIZE = Size("64 MiB") ++ an_fs.target_size = TARGET_SIZE ++ self.assertEqual(an_fs.target_size, TARGET_SIZE) ++ self.assertNotEqual(an_fs._size, TARGET_SIZE) ++ self.assertIsNone(an_fs.do_resize()) ++ ACTUAL_SIZE = TARGET_SIZE.round_to_nearest(an_fs._resize.unit, rounding=ROUND_DOWN) ++ self.assertEqual(an_fs.size, ACTUAL_SIZE) ++ self.assertEqual(an_fs._size, ACTUAL_SIZE) ++ self._test_sizes(an_fs) ++ ++ self._remove_partition(part, disk) ++ ++ # and no errors should occur when checking ++ self.assertIsNone(an_fs.do_check()) ++ ++ def test_shrink(self): ++ self.skipTest("Not checking resize for this test category.") ++ ++ def test_too_small(self): ++ self.skipTest("Not checking resize for this test category.") ++ ++ def test_no_explicit_target_size2(self): ++ self.skipTest("Not checking resize for this test category.") ++ ++ def test_too_big2(self): ++ # XXX this tests assumes that resizing to max size - 1 B will fail, but xfs_grow won't ++ self.skipTest("Not checking resize for this test category.") ++ + + class HFSTestCase(fstesting.FSAsRoot): + _fs_class = fs.HFS +diff --git a/tests/formats_test/fstesting.py b/tests/formats_test/fstesting.py +index 62f806f9..86b2a116 100644 +--- a/tests/formats_test/fstesting.py ++++ b/tests/formats_test/fstesting.py +@@ -11,16 +11,6 @@ from blivet.size import Size, ROUND_DOWN + from blivet.formats import fs + + +-def can_resize(an_fs): +- """ Returns True if this filesystem has all necessary resizing tools +- available. +- +- :param an_fs: a filesystem object +- """ +- resize_tasks = (an_fs._resize, an_fs._size_info, an_fs._minsize) +- return not any(t.availability_errors for t in resize_tasks) +- +- + @add_metaclass(abc.ABCMeta) + class FSAsRoot(loopbackedtestcase.LoopBackedTestCase): + +@@ -32,6 +22,15 @@ class FSAsRoot(loopbackedtestcase.LoopBackedTestCase): + def __init__(self, methodName='run_test'): + super(FSAsRoot, self).__init__(methodName=methodName, device_spec=[self._DEVICE_SIZE]) + ++ def can_resize(self, an_fs): ++ """ Returns True if this filesystem has all necessary resizing tools ++ available. ++ ++ :param an_fs: a filesystem object ++ """ ++ resize_tasks = (an_fs._resize, an_fs._size_info, an_fs._minsize) ++ return not any(t.availability_errors for t in resize_tasks) ++ + def _test_sizes(self, an_fs): + """ Test relationships between different size values. + +@@ -190,7 +189,7 @@ class FSAsRoot(loopbackedtestcase.LoopBackedTestCase): + # CHECKME: target size is still 0 after updated_size_info is called. + self.assertEqual(an_fs.size, Size(0) if an_fs.resizable else an_fs._size) + +- if not can_resize(an_fs): ++ if not self.can_resize(an_fs): + self.assertFalse(an_fs.resizable) + # Not resizable, so can not do resizing actions. + with self.assertRaises(DeviceFormatError): +@@ -221,7 +220,7 @@ class FSAsRoot(loopbackedtestcase.LoopBackedTestCase): + # in constructor call behavior would be different. + + an_fs = self._fs_class() +- if not can_resize(an_fs): ++ if not self.can_resize(an_fs): + self.skipTest("Not checking resize for this test category.") + if not an_fs.formattable: + self.skipTest("can not create filesystem %s" % an_fs.name) +@@ -244,7 +243,7 @@ class FSAsRoot(loopbackedtestcase.LoopBackedTestCase): + """ + SIZE = Size("64 MiB") + an_fs = self._fs_class(size=SIZE) +- if not can_resize(an_fs): ++ if not self.can_resize(an_fs): + self.skipTest("Not checking resize for this test category.") + if not an_fs.formattable: + self.skipTest("can not create filesystem %s" % an_fs.name) +@@ -264,7 +263,7 @@ class FSAsRoot(loopbackedtestcase.LoopBackedTestCase): + + def test_shrink(self): + an_fs = self._fs_class() +- if not can_resize(an_fs): ++ if not self.can_resize(an_fs): + self.skipTest("Not checking resize for this test category.") + if not an_fs.formattable: + self.skipTest("can not create filesystem %s" % an_fs.name) +@@ -296,7 +295,7 @@ class FSAsRoot(loopbackedtestcase.LoopBackedTestCase): + + def test_too_small(self): + an_fs = self._fs_class() +- if not can_resize(an_fs): ++ if not self.can_resize(an_fs): + self.skipTest("Not checking resize for this test category.") + if not an_fs.formattable: + self.skipTest("can not create or resize filesystem %s" % an_fs.name) +@@ -315,7 +314,7 @@ class FSAsRoot(loopbackedtestcase.LoopBackedTestCase): + + def test_too_big(self): + an_fs = self._fs_class() +- if not can_resize(an_fs): ++ if not self.can_resize(an_fs): + self.skipTest("Not checking resize for this test category.") + if not an_fs.formattable: + self.skipTest("can not create filesystem %s" % an_fs.name) +@@ -334,7 +333,7 @@ class FSAsRoot(loopbackedtestcase.LoopBackedTestCase): + + def test_too_big2(self): + an_fs = self._fs_class() +- if not can_resize(an_fs): ++ if not self.can_resize(an_fs): + self.skipTest("Not checking resize for this test category.") + if not an_fs.formattable: + self.skipTest("can not create filesystem %s" % an_fs.name) +-- +2.26.2 + + +From 51acc04f4639f143b55789a06a68aae988a91296 Mon Sep 17 00:00:00 2001 +From: Vojtech Trefny +Date: Wed, 15 Jul 2020 12:59:04 +0200 +Subject: [PATCH 3/4] Add support for checking and fixing XFS using xfs_repair + +--- + blivet/formats/fs.py | 1 + + blivet/tasks/availability.py | 1 + + blivet/tasks/fsck.py | 12 ++++++++++++ + tests/formats_test/fs_test.py | 6 +++--- + 4 files changed, 17 insertions(+), 3 deletions(-) + +diff --git a/blivet/formats/fs.py b/blivet/formats/fs.py +index 12cb9885..06fbdf10 100644 +--- a/blivet/formats/fs.py ++++ b/blivet/formats/fs.py +@@ -1091,6 +1091,7 @@ class XFS(FS): + _supported = True + _resizable = True + _packages = ["xfsprogs"] ++ _fsck_class = fsck.XFSCK + _info_class = fsinfo.XFSInfo + _mkfs_class = fsmkfs.XFSMkfs + _readlabel_class = fsreadlabel.XFSReadLabel +diff --git a/blivet/tasks/availability.py b/blivet/tasks/availability.py +index df62780c..f3b76650 100644 +--- a/blivet/tasks/availability.py ++++ b/blivet/tasks/availability.py +@@ -456,5 +456,6 @@ XFSADMIN_APP = application("xfs_admin") + XFSDB_APP = application("xfs_db") + XFSFREEZE_APP = application("xfs_freeze") + XFSRESIZE_APP = application("xfs_growfs") ++XFSREPAIR_APP = application("xfs_repair") + + MOUNT_APP = application("mount") +diff --git a/blivet/tasks/fsck.py b/blivet/tasks/fsck.py +index 5274f13a..8477f5f8 100644 +--- a/blivet/tasks/fsck.py ++++ b/blivet/tasks/fsck.py +@@ -123,6 +123,18 @@ class Ext2FSCK(FSCK): + return "\n".join(msgs) or None + + ++class XFSCK(FSCK): ++ _fsck_errors = {1: "Runtime error encountered during repair operation.", ++ 2: "XFS repair was unable to proceed due to a dirty log."} ++ ++ ext = availability.XFSREPAIR_APP ++ options = [] ++ ++ def _error_message(self, rc): ++ msgs = (self._fsck_errors[c] for c in self._fsck_errors.keys() if rc & c) ++ return "\n".join(msgs) or None ++ ++ + class HFSPlusFSCK(FSCK): + _fsck_errors = {3: "Quick check found a dirty filesystem; no repairs done.", + 4: "Root filesystem was dirty. System should be rebooted.", +diff --git a/tests/formats_test/fs_test.py b/tests/formats_test/fs_test.py +index 9bc5d20d..8fb099fd 100644 +--- a/tests/formats_test/fs_test.py ++++ b/tests/formats_test/fs_test.py +@@ -123,10 +123,10 @@ class XFSTestCase(fstesting.FSAsRoot): + self.assertEqual(an_fs._size, ACTUAL_SIZE) + self._test_sizes(an_fs) + +- self._remove_partition(part, disk) ++ # and no errors should occur when checking ++ self.assertIsNone(an_fs.do_check()) + +- # and no errors should occur when checking +- self.assertIsNone(an_fs.do_check()) ++ self._remove_partition(part, disk) + + def test_shrink(self): + self.skipTest("Not checking resize for this test category.") +-- +2.26.2 + + +From 2a6947098e66f880193f3bac2282a6c7857ca5f7 Mon Sep 17 00:00:00 2001 +From: Vojtech Trefny +Date: Thu, 16 Jul 2020 09:05:35 +0200 +Subject: [PATCH 4/4] Use xfs_db in read-only mode when getting XFS information + +This way it will also work on mounted filesystems. +--- + blivet/tasks/fsinfo.py | 2 +- + 1 file changed, 1 insertion(+), 1 deletion(-) + +diff --git a/blivet/tasks/fsinfo.py b/blivet/tasks/fsinfo.py +index af208f5d..41ff700f 100644 +--- a/blivet/tasks/fsinfo.py ++++ b/blivet/tasks/fsinfo.py +@@ -95,7 +95,7 @@ class ReiserFSInfo(FSInfo): + + class XFSInfo(FSInfo): + ext = availability.XFSDB_APP +- options = ["-c", "sb 0", "-c", "p dblocks", "-c", "p blocksize"] ++ options = ["-c", "sb 0", "-c", "p dblocks", "-c", "p blocksize", "-r"] + + + class UnimplementedFSInfo(fstask.UnimplementedFSTask): +-- +2.26.2 + diff --git a/SOURCES/0013-Do-not-limit-swap-to-128-GiB.patch b/SOURCES/0013-Do-not-limit-swap-to-128-GiB.patch new file mode 100644 index 0000000..5b9f0ed --- /dev/null +++ b/SOURCES/0013-Do-not-limit-swap-to-128-GiB.patch @@ -0,0 +1,76 @@ +From aa4ce218fe9b4ee3571d872ff1575a499596181c Mon Sep 17 00:00:00 2001 +From: Vojtech Trefny +Date: Fri, 29 May 2020 12:14:30 +0200 +Subject: [PATCH 1/2] Do not limit swap to 128 GiB + +The limit was part of change to limit suggested swap size in +kickstart which doesn't use the SwapSpace._max_size so there is no +reason to limit this for manual installations. +16 TiB seems to be max usable swap size based on mkswap code. + +Resolves: rhbz#1656485 +--- + blivet/formats/swap.py | 3 +-- + 1 file changed, 1 insertion(+), 2 deletions(-) + +diff --git a/blivet/formats/swap.py b/blivet/formats/swap.py +index 4b8a7edf..3cc59138 100644 +--- a/blivet/formats/swap.py ++++ b/blivet/formats/swap.py +@@ -52,8 +52,7 @@ class SwapSpace(DeviceFormat): + _linux_native = True # for clearpart + _plugin = availability.BLOCKDEV_SWAP_PLUGIN + +- # see rhbz#744129 for details +- _max_size = Size("128 GiB") ++ _max_size = Size("16 TiB") + + config_actions_map = {"label": "write_label"} + +-- +2.26.2 + + +From 93aa6ad87116f1c86616d73dbe561251c4a0c286 Mon Sep 17 00:00:00 2001 +From: Vojtech Trefny +Date: Thu, 11 Jun 2020 14:27:44 +0200 +Subject: [PATCH 2/2] Add test for SwapSpace max size + +--- + tests/formats_test/swap_test.py | 24 ++++++++++++++++++++++++ + 1 file changed, 24 insertions(+) + create mode 100644 tests/formats_test/swap_test.py + +diff --git a/tests/formats_test/swap_test.py b/tests/formats_test/swap_test.py +new file mode 100644 +index 00000000..56356144 +--- /dev/null ++++ b/tests/formats_test/swap_test.py +@@ -0,0 +1,24 @@ ++import test_compat # pylint: disable=unused-import ++ ++import six ++import unittest ++ ++from blivet.devices.storage import StorageDevice ++from blivet.errors import DeviceError ++from blivet.formats import get_format ++ ++from blivet.size import Size ++ ++ ++class SwapNodevTestCase(unittest.TestCase): ++ ++ def test_swap_max_size(self): ++ StorageDevice("dev", size=Size("129 GiB"), ++ fmt=get_format("swap")) ++ ++ StorageDevice("dev", size=Size("15 TiB"), ++ fmt=get_format("swap")) ++ ++ with six.assertRaisesRegex(self, DeviceError, "device is too large for new format"): ++ StorageDevice("dev", size=Size("17 TiB"), ++ fmt=get_format("swap")) +-- +2.26.2 + diff --git a/SOURCES/0014-Use-UnusableConfigurationError-for-patially-hidden-multipath-devices.patch b/SOURCES/0014-Use-UnusableConfigurationError-for-patially-hidden-multipath-devices.patch new file mode 100644 index 0000000..1e14de6 --- /dev/null +++ b/SOURCES/0014-Use-UnusableConfigurationError-for-patially-hidden-multipath-devices.patch @@ -0,0 +1,78 @@ +From 4e6a322d32d2a12f8a87ab763a6286cf3d7b5c27 Mon Sep 17 00:00:00 2001 +From: Vojtech Trefny +Date: Tue, 8 Sep 2020 13:57:40 +0200 +Subject: [PATCH] Use UnusableConfigurationError for partially hidden multipath + devices + +Follow-up for https://github.com/storaged-project/blivet/pull/883 +to make Anaconda show an error message instead of crashing. + +Resolves: rhbz#1877052 +--- + blivet/devicetree.py | 4 ++-- + blivet/errors.py | 6 ++++++ + tests/devicetree_test.py | 4 ++-- + 3 files changed, 10 insertions(+), 4 deletions(-) + +diff --git a/blivet/devicetree.py b/blivet/devicetree.py +index 2afb0d0e..57a9bbd7 100644 +--- a/blivet/devicetree.py ++++ b/blivet/devicetree.py +@@ -32,7 +32,7 @@ from gi.repository import BlockDev as blockdev + + from .actionlist import ActionList + from .callbacks import callbacks +-from .errors import DeviceError, DeviceTreeError, StorageError, DuplicateUUIDError ++from .errors import DeviceError, DeviceTreeError, StorageError, DuplicateUUIDError, InvalidMultideviceSelection + from .deviceaction import ActionDestroyDevice, ActionDestroyFormat + from .devices import BTRFSDevice, NoDevice, PartitionDevice + from .devices import LVMLogicalVolumeDevice, LVMVolumeGroupDevice +@@ -936,7 +936,7 @@ class DeviceTreeBase(object): + if is_ignored: + if len(disk.children) == 1: + if not all(self._is_ignored_disk(d) for d in disk.children[0].parents): +- raise DeviceTreeError("Including only a subset of raid/multipath member disks is not allowed.") ++ raise InvalidMultideviceSelection("Including only a subset of raid/multipath member disks is not allowed.") + + # and also children like fwraid or mpath + self.hide(disk.children[0]) +diff --git a/blivet/errors.py b/blivet/errors.py +index 811abf81..7a93f1ce 100644 +--- a/blivet/errors.py ++++ b/blivet/errors.py +@@ -233,6 +233,12 @@ class DuplicateVGError(UnusableConfigurationError): + "Hint 2: You can get the VG UUIDs by running " + "'pvs -o +vg_uuid'.") + ++ ++class InvalidMultideviceSelection(UnusableConfigurationError): ++ suggestion = N_("All parent devices must be selected when choosing exclusive " ++ "or ignored disks for a multipath or firmware RAID device.") ++ ++ + # DeviceAction + + +diff --git a/tests/devicetree_test.py b/tests/devicetree_test.py +index 6032e7f6..4e47ffc3 100644 +--- a/tests/devicetree_test.py ++++ b/tests/devicetree_test.py +@@ -5,7 +5,7 @@ import six + import unittest + + from blivet.actionlist import ActionList +-from blivet.errors import DeviceTreeError, DuplicateUUIDError ++from blivet.errors import DeviceTreeError, DuplicateUUIDError, InvalidMultideviceSelection + from blivet.deviceaction import ACTION_TYPE_DESTROY, ACTION_OBJECT_DEVICE + from blivet.devicelibs import lvm + from blivet.devices import DiskDevice +@@ -512,5 +512,5 @@ class DeviceTreeIgnoredExclusiveMultipathTestCase(unittest.TestCase): + self.tree.ignored_disks = ["sda", "sdb"] + self.tree.exclusive_disks = [] + +- with self.assertRaises(DeviceTreeError): ++ with self.assertRaises(InvalidMultideviceSelection): + self.tree._hide_ignored_disks() +-- +2.26.2 + diff --git a/SOURCES/0015-Fix-possible-UnicodeDecodeError-when-reading-model-f.patch b/SOURCES/0015-Fix-possible-UnicodeDecodeError-when-reading-model-f.patch new file mode 100644 index 0000000..24e408e --- /dev/null +++ b/SOURCES/0015-Fix-possible-UnicodeDecodeError-when-reading-model-f.patch @@ -0,0 +1,32 @@ +From 866a48e6c3d8246d2897bb402a191df5f2848aa4 Mon Sep 17 00:00:00 2001 +From: Vojtech Trefny +Date: Tue, 23 Jun 2020 10:33:33 +0200 +Subject: [PATCH] Fix possible UnicodeDecodeError when reading model from sysfs + +Some Innovation IT NVMe devices have an (invalid) unicode in their +model name. + +Resolves: rhbz#1849326 +--- + blivet/udev.py | 5 +++-- + 1 file changed, 3 insertions(+), 2 deletions(-) + +diff --git a/blivet/udev.py b/blivet/udev.py +index 41c99496..2c795225 100644 +--- a/blivet/udev.py ++++ b/blivet/udev.py +@@ -185,8 +185,9 @@ def __is_blacklisted_blockdev(dev_name): + if any(re.search(expr, dev_name) for expr in device_name_blacklist): + return True + +- if os.path.exists("/sys/class/block/%s/device/model" % (dev_name,)): +- model = open("/sys/class/block/%s/device/model" % (dev_name,)).read() ++ model_path = "/sys/class/block/%s/device/model" % dev_name ++ if os.path.exists(model_path): ++ model = open(model_path, encoding="utf-8", errors="replace").read() + for bad in ("IBM *STMF KERNEL", "SCEI Flash-5", "DGC LUNZ"): + if model.find(bad) != -1: + log.info("ignoring %s with model %s", dev_name, model) +-- +2.26.2 + diff --git a/SOURCES/0016-Basic-LVM-VDO-support.patch b/SOURCES/0016-Basic-LVM-VDO-support.patch new file mode 100644 index 0000000..b52342b --- /dev/null +++ b/SOURCES/0016-Basic-LVM-VDO-support.patch @@ -0,0 +1,415 @@ +From 3f6bbf52442609b8e6e3919a3fdd8c5af64923e6 Mon Sep 17 00:00:00 2001 +From: Vojtech Trefny +Date: Tue, 12 May 2020 12:48:41 +0200 +Subject: [PATCH 1/3] Add basic support for LVM VDO devices + +This adds support for LVM VDO devices detection during populate +and allows removing both VDO LVs and VDO pools using actions. +--- + blivet/devices/lvm.py | 150 +++++++++++++++++++++++++++++++- + blivet/populator/helpers/lvm.py | 16 +++- + tests/action_test.py | 39 +++++++++ + tests/devices_test/lvm_test.py | 34 ++++++++ + tests/storagetestcase.py | 11 ++- + 5 files changed, 245 insertions(+), 5 deletions(-) + +diff --git a/blivet/devices/lvm.py b/blivet/devices/lvm.py +index 97de6acd..d9e24a33 100644 +--- a/blivet/devices/lvm.py ++++ b/blivet/devices/lvm.py +@@ -1789,8 +1789,132 @@ class LVMThinLogicalVolumeMixin(object): + data.pool_name = self.pool.lvname + + ++class LVMVDOPoolMixin(object): ++ def __init__(self): ++ self._lvs = [] ++ ++ @property ++ def is_vdo_pool(self): ++ return self.seg_type == "vdo-pool" ++ ++ @property ++ def type(self): ++ return "lvmvdopool" ++ ++ @property ++ def resizable(self): ++ return False ++ ++ @util.requires_property("is_vdo_pool") ++ def _add_log_vol(self, lv): ++ """ Add an LV to this VDO pool. """ ++ if lv in self._lvs: ++ raise ValueError("lv is already part of this VDO pool") ++ ++ self.vg._add_log_vol(lv) ++ log.debug("Adding %s/%s to %s", lv.name, lv.size, self.name) ++ self._lvs.append(lv) ++ ++ @util.requires_property("is_vdo_pool") ++ def _remove_log_vol(self, lv): ++ """ Remove an LV from this VDO pool. """ ++ if lv not in self._lvs: ++ raise ValueError("specified lv is not part of this VDO pool") ++ ++ self._lvs.remove(lv) ++ self.vg._remove_log_vol(lv) ++ ++ @property ++ @util.requires_property("is_vdo_pool") ++ def lvs(self): ++ """ A list of this VDO pool's LVs """ ++ return self._lvs[:] # we don't want folks changing our list ++ ++ @property ++ def direct(self): ++ """ Is this device directly accessible? """ ++ return False ++ ++ def _create(self): ++ """ Create the device. """ ++ raise NotImplementedError ++ ++ ++class LVMVDOLogicalVolumeMixin(object): ++ def __init__(self): ++ pass ++ ++ def _init_check(self): ++ pass ++ ++ def _check_parents(self): ++ """Check that this device has parents as expected""" ++ if isinstance(self.parents, (list, ParentList)): ++ if len(self.parents) != 1: ++ raise ValueError("constructor requires a single vdo-pool LV") ++ ++ container = self.parents[0] ++ else: ++ container = self.parents ++ ++ if not container or not isinstance(container, LVMLogicalVolumeDevice) or not container.is_vdo_pool: ++ raise ValueError("constructor requires a vdo-pool LV") ++ ++ @property ++ def vg_space_used(self): ++ return Size(0) # the pool's size is already accounted for in the vg ++ ++ @property ++ def is_vdo_lv(self): ++ return self.seg_type == "vdo" ++ ++ @property ++ def vg(self): ++ # parents[0] is the pool, not the VG so set the VG here ++ return self.pool.vg ++ ++ @property ++ def type(self): ++ return "vdolv" ++ ++ @property ++ def resizable(self): ++ return False ++ ++ @property ++ @util.requires_property("is_vdo_lv") ++ def pool(self): ++ return self.parents[0] ++ ++ def _create(self): ++ """ Create the device. """ ++ raise NotImplementedError ++ ++ def _destroy(self): ++ # nothing to do here, VDO LV is destroyed automatically together with ++ # the VDO pool ++ pass ++ ++ def remove_hook(self, modparent=True): ++ if modparent: ++ self.pool._remove_log_vol(self) ++ ++ # pylint: disable=bad-super-call ++ super(LVMLogicalVolumeBase, self).remove_hook(modparent=modparent) ++ ++ def add_hook(self, new=True): ++ # pylint: disable=bad-super-call ++ super(LVMLogicalVolumeBase, self).add_hook(new=new) ++ if new: ++ return ++ ++ if self not in self.pool.lvs: ++ self.pool._add_log_vol(self) ++ ++ + class LVMLogicalVolumeDevice(LVMLogicalVolumeBase, LVMInternalLogicalVolumeMixin, LVMSnapshotMixin, +- LVMThinPoolMixin, LVMThinLogicalVolumeMixin): ++ LVMThinPoolMixin, LVMThinLogicalVolumeMixin, LVMVDOPoolMixin, ++ LVMVDOLogicalVolumeMixin): + """ An LVM Logical Volume """ + + # generally resizable, see :property:`resizable` for details +@@ -1879,6 +2003,8 @@ class LVMLogicalVolumeDevice(LVMLogicalVolumeBase, LVMInternalLogicalVolumeMixin + LVMLogicalVolumeBase.__init__(self, name, parents, size, uuid, seg_type, + fmt, exists, sysfs_path, grow, maxsize, + percent, cache_request, pvs, from_lvs) ++ LVMVDOPoolMixin.__init__(self) ++ LVMVDOLogicalVolumeMixin.__init__(self) + + LVMInternalLogicalVolumeMixin._init_check(self) + LVMSnapshotMixin._init_check(self) +@@ -1905,6 +2031,10 @@ class LVMLogicalVolumeDevice(LVMLogicalVolumeBase, LVMInternalLogicalVolumeMixin + ret.append(LVMThinPoolMixin) + if self.is_thin_lv: + ret.append(LVMThinLogicalVolumeMixin) ++ if self.is_vdo_pool: ++ ret.append(LVMVDOPoolMixin) ++ if self.is_vdo_lv: ++ ret.append(LVMVDOLogicalVolumeMixin) + return ret + + def _try_specific_call(self, name, *args, **kwargs): +@@ -2066,6 +2196,11 @@ class LVMLogicalVolumeDevice(LVMLogicalVolumeBase, LVMInternalLogicalVolumeMixin + def display_lv_name(self): + return self.lvname + ++ @property ++ @type_specific ++ def pool(self): ++ return super(LVMLogicalVolumeDevice, self).pool ++ + def _setup(self, orig=False): + """ Open, or set up, a device. """ + log_method_call(self, self.name, orig=orig, status=self.status, +@@ -2167,6 +2302,19 @@ class LVMLogicalVolumeDevice(LVMLogicalVolumeBase, LVMInternalLogicalVolumeMixin + udev.settle() + blockdev.lvm.lvresize(self.vg.name, self._name, self.size) + ++ @type_specific ++ def _add_log_vol(self, lv): ++ pass ++ ++ @type_specific ++ def _remove_log_vol(self, lv): ++ pass ++ ++ @property ++ @type_specific ++ def lvs(self): ++ return [] ++ + @property + @type_specific + def direct(self): +diff --git a/blivet/populator/helpers/lvm.py b/blivet/populator/helpers/lvm.py +index 4b674fac..ff8bf59f 100644 +--- a/blivet/populator/helpers/lvm.py ++++ b/blivet/populator/helpers/lvm.py +@@ -211,9 +211,6 @@ class LVMFormatPopulator(FormatPopulator): + origin = self._devicetree.get_device_by_name(origin_device_name) + + lv_kwargs["origin"] = origin +- elif lv_attr[0] == 'v': +- # skip vorigins +- return + elif lv_attr[0] in 'IrielTCo' and lv_name.endswith(']'): + # an internal LV, add the an instance of the appropriate class + # to internal_lvs for later processing when non-internal LVs are +@@ -237,6 +234,19 @@ class LVMFormatPopulator(FormatPopulator): + origin = self._devicetree.get_device_by_name(origin_device_name) + lv_kwargs["origin"] = origin + ++ lv_parents = [self._devicetree.get_device_by_name(pool_device_name)] ++ elif lv_attr[0] == 'd': ++ # vdo pool ++ # nothing to do here ++ pass ++ elif lv_attr[0] == 'v': ++ if lv_type != "vdo": ++ # skip vorigins ++ return ++ pool_name = blockdev.lvm.vdolvpoolname(vg_name, lv_name) ++ pool_device_name = "%s-%s" % (vg_name, pool_name) ++ add_required_lv(pool_device_name, "failed to look up VDO pool") ++ + lv_parents = [self._devicetree.get_device_by_name(pool_device_name)] + elif lv_name.endswith(']'): + # unrecognized Internal LVM2 device +diff --git a/tests/action_test.py b/tests/action_test.py +index 90c1b312..8f9a7424 100644 +--- a/tests/action_test.py ++++ b/tests/action_test.py +@@ -1252,6 +1252,45 @@ class DeviceActionTestCase(StorageTestCase): + self.assertEqual(set(self.storage.lvs), {pool}) + self.assertEqual(set(pool._internal_lvs), {lv1, lv2}) + ++ def test_lvm_vdo_destroy(self): ++ self.destroy_all_devices() ++ sdc = self.storage.devicetree.get_device_by_name("sdc") ++ sdc1 = self.new_device(device_class=PartitionDevice, name="sdc1", ++ size=Size("50 GiB"), parents=[sdc], ++ fmt=blivet.formats.get_format("lvmpv")) ++ self.schedule_create_device(sdc1) ++ ++ vg = self.new_device(device_class=LVMVolumeGroupDevice, ++ name="vg", parents=[sdc1]) ++ self.schedule_create_device(vg) ++ ++ pool = self.new_device(device_class=LVMLogicalVolumeDevice, ++ name="data", parents=[vg], ++ size=Size("10 GiB"), ++ seg_type="vdo-pool", exists=True) ++ self.storage.devicetree._add_device(pool) ++ lv = self.new_device(device_class=LVMLogicalVolumeDevice, ++ name="meta", parents=[pool], ++ size=Size("50 GiB"), ++ seg_type="vdo", exists=True) ++ self.storage.devicetree._add_device(lv) ++ ++ remove_lv = self.schedule_destroy_device(lv) ++ self.assertListEqual(pool.lvs, []) ++ self.assertNotIn(lv, vg.lvs) ++ ++ # cancelling the action should put lv back to both vg and pool lvs ++ self.storage.devicetree.actions.remove(remove_lv) ++ self.assertListEqual(pool.lvs, [lv]) ++ self.assertIn(lv, vg.lvs) ++ ++ # can't remove non-leaf pool ++ with self.assertRaises(ValueError): ++ self.schedule_destroy_device(pool) ++ ++ self.schedule_destroy_device(lv) ++ self.schedule_destroy_device(pool) ++ + + class ConfigurationActionsTest(unittest.TestCase): + +diff --git a/tests/devices_test/lvm_test.py b/tests/devices_test/lvm_test.py +index 9e701d18..204cb99a 100644 +--- a/tests/devices_test/lvm_test.py ++++ b/tests/devices_test/lvm_test.py +@@ -405,6 +405,40 @@ class LVMDeviceTest(unittest.TestCase): + exists=False) + self.assertFalse(vg.is_empty) + ++ def test_lvm_vdo_pool(self): ++ pv = StorageDevice("pv1", fmt=blivet.formats.get_format("lvmpv"), ++ size=Size("1 GiB"), exists=True) ++ vg = LVMVolumeGroupDevice("testvg", parents=[pv]) ++ pool = LVMLogicalVolumeDevice("testpool", parents=[vg], size=Size("512 MiB"), ++ seg_type="vdo-pool", exists=True) ++ self.assertTrue(pool.is_vdo_pool) ++ ++ free = vg.free_space ++ lv = LVMLogicalVolumeDevice("testlv", parents=[pool], size=Size("2 GiB"), ++ seg_type="vdo", exists=True) ++ self.assertTrue(lv.is_vdo_lv) ++ self.assertEqual(lv.vg, vg) ++ self.assertEqual(lv.pool, pool) ++ ++ # free space in the vg shouldn't be affected by the vdo lv ++ self.assertEqual(lv.vg_space_used, 0) ++ self.assertEqual(free, vg.free_space) ++ ++ self.assertListEqual(pool.lvs, [lv]) ++ ++ # now try to destroy both the pool and the vdo lv ++ # for the lv this should be a no-op, destroying the pool should destroy both ++ with patch("blivet.devices.lvm.blockdev.lvm") as lvm: ++ lv.destroy() ++ lv.remove_hook() ++ self.assertFalse(lv.exists) ++ self.assertFalse(lvm.lvremove.called) ++ self.assertListEqual(pool.lvs, []) ++ ++ pool.destroy() ++ self.assertFalse(pool.exists) ++ self.assertTrue(lvm.lvremove.called) ++ + + class TypeSpecificCallsTest(unittest.TestCase): + def test_type_specific_calls(self): +diff --git a/tests/storagetestcase.py b/tests/storagetestcase.py +index e581bca6..1844dec5 100644 +--- a/tests/storagetestcase.py ++++ b/tests/storagetestcase.py +@@ -96,7 +96,16 @@ class StorageTestCase(unittest.TestCase): + def new_device(self, *args, **kwargs): + """ Return a new Device instance suitable for testing. """ + device_class = kwargs.pop("device_class") +- exists = kwargs.pop("exists", False) ++ ++ # we intentionally don't pass the "exists" kwarg to the constructor ++ # becauses this causes issues with some devices (especially partitions) ++ # but we still need it for some LVs like VDO because we can't create ++ # those so we need to fake their existence even for the constructor ++ if device_class is blivet.devices.LVMLogicalVolumeDevice: ++ exists = kwargs.get("exists", False) ++ else: ++ exists = kwargs.pop("exists", False) ++ + part_type = kwargs.pop("part_type", parted.PARTITION_NORMAL) + device = device_class(*args, **kwargs) + +-- +2.26.2 + + +From f05a66e1bed1ca1f3cd7d7ffecd6693ab4d7f32a Mon Sep 17 00:00:00 2001 +From: Vojtech Trefny +Date: Tue, 12 May 2020 12:52:47 +0200 +Subject: [PATCH 2/3] Fix checking for filesystem support in action_test + +--- + tests/action_test.py | 2 +- + 1 file changed, 1 insertion(+), 1 deletion(-) + +diff --git a/tests/action_test.py b/tests/action_test.py +index 8f9a7424..228eb97a 100644 +--- a/tests/action_test.py ++++ b/tests/action_test.py +@@ -56,7 +56,7 @@ FORMAT_CLASSES = [ + + + @unittest.skipUnless(not any(x.unavailable_type_dependencies() for x in DEVICE_CLASSES), "some unsupported device classes required for this test") +-@unittest.skipUnless(not any(x().utils_available for x in FORMAT_CLASSES), "some unsupported format classes required for this test") ++@unittest.skipUnless(all(x().utils_available for x in FORMAT_CLASSES), "some unsupported format classes required for this test") + class DeviceActionTestCase(StorageTestCase): + + """ DeviceActionTestSuite """ +-- +2.26.2 + + +From 69bd2e69e21c8779377a6f54b3d83cb35138867a Mon Sep 17 00:00:00 2001 +From: Vojtech Trefny +Date: Tue, 12 May 2020 12:54:03 +0200 +Subject: [PATCH 3/3] Fix LV min size for resize in test_action_dependencies + +We've recently changed min size for all filesystems so we can't +resize the LV to the device minimal size. +This was overlooked in the original change because these tests +were skipped. +--- + tests/action_test.py | 2 +- + 1 file changed, 1 insertion(+), 1 deletion(-) + +diff --git a/tests/action_test.py b/tests/action_test.py +index 228eb97a..77176f46 100644 +--- a/tests/action_test.py ++++ b/tests/action_test.py +@@ -870,7 +870,7 @@ class DeviceActionTestCase(StorageTestCase): + name="testlv2", parents=[testvg]) + testlv2.format = self.new_format("ext4", device=testlv2.path, + exists=True, device_instance=testlv2) +- shrink_lv2 = ActionResizeDevice(testlv2, testlv2.size - Size("10 GiB")) ++ shrink_lv2 = ActionResizeDevice(testlv2, testlv2.size - Size("10 GiB") + Ext4FS._min_size) + shrink_lv2.apply() + + self.assertTrue(grow_lv.requires(shrink_lv2)) +-- +2.26.2 + diff --git a/SPECS/python-blivet.spec b/SPECS/python-blivet.spec index da3276d..44bca8a 100644 --- a/SPECS/python-blivet.spec +++ b/SPECS/python-blivet.spec @@ -23,7 +23,7 @@ Version: 3.2.2 #%%global prerelease .b2 # prerelease, if defined, should be something like .a1, .b1, .b2.dev1, or .c2 -Release: 6%{?prerelease}%{?dist} +Release: 7%{?prerelease}%{?dist} Epoch: 1 License: LGPLv2+ Group: System Environment/Libraries @@ -42,6 +42,11 @@ Patch7: 0008-set-allowed-disk-labels-for-s390x-as-standard-ones-plus-dasd.patch Patch8: 0009-Do-not-use-BlockDev-utils_have_kernel_module-to-check-for-modules.patch Patch9: 0010-Fix-name-resolution-for-MD-devices-and-partitions-on.patch Patch10: 0011-Fix-ignoring-disk-devices-with-parents-or-children.patch +Patch11: 0012-xfs-grow-support.patch +Patch12: 0013-Do-not-limit-swap-to-128-GiB.patch +Patch13: 0014-Use-UnusableConfigurationError-for-patially-hidden-multipath-devices.patch +Patch14: 0015-Fix-possible-UnicodeDecodeError-when-reading-model-f.patch +Patch15: 0016-Basic-LVM-VDO-support.patch # Versions of required components (done so we make sure the buildrequires # match the requires versions of things). @@ -203,6 +208,18 @@ configuration. %endif %changelog +* Wed Nov 18 2020 Vojtech Trefny - 3.2.2-7 +- Add support for XFS format grow + Resolves: rhbz#1862349 +- Do not limit swap to 128 GiB + Resolves: rhbz#1656485 +- Use UnusableConfigurationError for partially hidden multipath devices + Resolves: rhbz#1877052 +- Fix possible UnicodeDecodeError when reading model from sysfs + Resolves: rhbz#1849326 +- Add basic support for LVM VDO devices + Resolves: rhbz#1828745 + * Thu Aug 20 2020 Vojtech Trefny - 3.2.2-6 - Fix name resolution for MD devices and partitions on them Resolves: rhbz#1862904