diff --git a/SOURCES/0012-Add-support-for-specifying-stripe-size-for-RAID-LVs.patch b/SOURCES/0012-Add-support-for-specifying-stripe-size-for-RAID-LVs.patch new file mode 100644 index 0000000..0493b57 --- /dev/null +++ b/SOURCES/0012-Add-support-for-specifying-stripe-size-for-RAID-LVs.patch @@ -0,0 +1,205 @@ +From 7a86d4306e3022b73035e21f66d515174264700e Mon Sep 17 00:00:00 2001 +From: Vojtech Trefny +Date: Thu, 9 Mar 2023 13:18:42 +0100 +Subject: [PATCH 1/2] Add support for specifying stripe size for RAID LVs + +--- + blivet/devices/lvm.py | 28 +++++++++++++++++--- + tests/storage_tests/devices_test/lvm_test.py | 12 +++++++-- + tests/unit_tests/devices_test/lvm_test.py | 27 +++++++++++++++++++ + 3 files changed, 61 insertions(+), 6 deletions(-) + +diff --git a/blivet/devices/lvm.py b/blivet/devices/lvm.py +index b8595d63..41358e9b 100644 +--- a/blivet/devices/lvm.py ++++ b/blivet/devices/lvm.py +@@ -659,7 +659,8 @@ class LVMLogicalVolumeBase(DMDevice, RaidDevice): + + def __init__(self, name, parents=None, size=None, uuid=None, seg_type=None, + fmt=None, exists=False, sysfs_path='', grow=None, maxsize=None, +- percent=None, cache_request=None, pvs=None, from_lvs=None): ++ percent=None, cache_request=None, pvs=None, from_lvs=None, ++ stripe_size=0): + + if not exists: + if seg_type not in [None, "linear", "thin", "thin-pool", "cache", "vdo-pool", "vdo", "cache-pool"] + lvm.raid_seg_types: +@@ -756,6 +757,15 @@ class LVMLogicalVolumeBase(DMDevice, RaidDevice): + if self._pv_specs: + self._assign_pv_space() + ++ self._stripe_size = stripe_size ++ if not self.exists and self._stripe_size: ++ if self.seg_type not in lvm.raid_seg_types: ++ raise errors.DeviceError("Stripe size can be specified only for RAID volumes") ++ if self.seg_type in ("raid1", "RAID1", "1", 1, "mirror"): ++ raise errors.DeviceError("Specifying stripe size is not allowed for RAID1 or mirror") ++ if self.cache: ++ raise errors.DeviceError("Creating cached LVs with custom stripe size is not supported") ++ + def _assign_pv_space(self): + if not self.is_raid_lv: + # nothing to do for non-RAID (and thus non-striped) LVs here +@@ -2295,7 +2305,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, attach_to=None): ++ write_policy=None, cache_mode=None, attach_to=None, stripe_size=0): + """ + :param name: the device name (generally a device node's basename) + :type name: str +@@ -2375,6 +2385,11 @@ class LVMLogicalVolumeDevice(LVMLogicalVolumeBase, LVMInternalLogicalVolumeMixin + be attached to when created + :type attach_to: :class:`LVMLogicalVolumeDevice` + ++ For RAID LVs only: ++ ++ :keyword stripe_size: size of the RAID stripe ++ :type stripe_size: :class:`~.size.Size` ++ + """ + + if isinstance(parents, (list, ParentList)): +@@ -2395,7 +2410,8 @@ class LVMLogicalVolumeDevice(LVMLogicalVolumeBase, LVMInternalLogicalVolumeMixin + 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) ++ percent, cache_request, pvs, from_lvs, ++ stripe_size) + LVMVDOPoolMixin.__init__(self, compression, deduplication, index_memory, + write_policy) + LVMVDOLogicalVolumeMixin.__init__(self) +@@ -2651,8 +2667,12 @@ class LVMLogicalVolumeDevice(LVMLogicalVolumeBase, LVMInternalLogicalVolumeMixin + pvs = [spec.pv.path for spec in self._pv_specs] + pvs = pvs or None + ++ extra = dict() ++ if self._stripe_size: ++ extra["stripesize"] = str(int(self._stripe_size.convert_to("KiB"))) ++ + blockdev.lvm.lvcreate(self.vg.name, self._name, self.size, +- type=self.seg_type, pv_list=pvs) ++ type=self.seg_type, pv_list=pvs, **extra) + else: + fast_pvs = [pv.path for pv in self.cache.fast_pvs] + +diff --git a/tests/storage_tests/devices_test/lvm_test.py b/tests/storage_tests/devices_test/lvm_test.py +index a055fc27..97ef1c4b 100644 +--- a/tests/storage_tests/devices_test/lvm_test.py ++++ b/tests/storage_tests/devices_test/lvm_test.py +@@ -1,4 +1,5 @@ + import os ++import subprocess + + from ..storagetestcase import StorageTestCase + +@@ -127,7 +128,7 @@ class LVMTestCase(StorageTestCase): + self.assertTrue(snap.is_snapshot_lv) + self.assertEqual(snap.origin, thinlv) + +- def _test_lvm_raid(self, seg_type, raid_level): ++ def _test_lvm_raid(self, seg_type, raid_level, stripe_size=0): + disk1 = self.storage.devicetree.get_device_by_path(self.vdevs[0]) + self.assertIsNotNone(disk1) + self.storage.initialize_disk(disk1) +@@ -151,7 +152,7 @@ class LVMTestCase(StorageTestCase): + + raidlv = self.storage.new_lv(fmt_type="ext4", size=blivet.size.Size("50 MiB"), + parents=[vg], name="blivetTestRAIDLV", +- seg_type=seg_type, pvs=[pv1, pv2]) ++ seg_type=seg_type, pvs=[pv1, pv2], stripe_size=stripe_size) + self.storage.create_device(raidlv) + + self.storage.do_it() +@@ -163,9 +164,16 @@ class LVMTestCase(StorageTestCase): + self.assertEqual(raidlv.raid_level, raid_level) + self.assertEqual(raidlv.seg_type, seg_type) + ++ if stripe_size: ++ out = subprocess.check_output(["lvs", "-o", "stripe_size", "--noheadings", "--nosuffix", "--units=b", raidlv.vg.name + "/" + raidlv.lvname]) ++ self.assertEqual(out.decode().strip(), str(int(stripe_size.convert_to()))) ++ + def test_lvm_raid_raid0(self): + self._test_lvm_raid("raid0", blivet.devicelibs.raid.RAID0) + ++ def test_lvm_raid_raid0_stripe_size(self): ++ self._test_lvm_raid("raid0", blivet.devicelibs.raid.RAID0, stripe_size=blivet.size.Size("1 MiB")) ++ + def test_lvm_raid_striped(self): + self._test_lvm_raid("striped", blivet.devicelibs.raid.Striped) + +diff --git a/tests/unit_tests/devices_test/lvm_test.py b/tests/unit_tests/devices_test/lvm_test.py +index 995c2da4..d7b55224 100644 +--- a/tests/unit_tests/devices_test/lvm_test.py ++++ b/tests/unit_tests/devices_test/lvm_test.py +@@ -363,6 +363,33 @@ class LVMDeviceTest(unittest.TestCase): + self.assertEqual(pv.format.free, Size("264 MiB")) + self.assertEqual(pv2.format.free, Size("256 MiB")) + ++ def test_lvm_logical_volume_raid_stripe_size(self): ++ pv = StorageDevice("pv1", fmt=blivet.formats.get_format("lvmpv"), ++ size=Size("1025 MiB")) ++ pv2 = StorageDevice("pv2", fmt=blivet.formats.get_format("lvmpv"), ++ size=Size("513 MiB")) ++ vg = LVMVolumeGroupDevice("testvg", parents=[pv, pv2]) ++ ++ with self.assertRaises(blivet.errors.DeviceError): ++ # non-raid LV ++ lv = LVMLogicalVolumeDevice("testlv", parents=[vg], size=Size("1 GiB"), ++ fmt=blivet.formats.get_format("xfs"), ++ exists=False, stripe_size=Size("1 MiB")) ++ ++ with self.assertRaises(blivet.errors.DeviceError): ++ # raid1 LV ++ lv = LVMLogicalVolumeDevice("testlv", parents=[vg], size=Size("1 GiB"), ++ fmt=blivet.formats.get_format("xfs"), ++ exists=False, seg_type="raid1", pvs=[pv, pv2], ++ stripe_size=Size("1 MiB")) ++ ++ lv = LVMLogicalVolumeDevice("testlv", parents=[vg], size=Size("1 GiB"), ++ fmt=blivet.formats.get_format("xfs"), ++ exists=False, seg_type="raid0", pvs=[pv, pv2], ++ stripe_size=Size("1 MiB")) ++ ++ self.assertEqual(lv._stripe_size, Size("1 MiB")) ++ + def test_target_size(self): + pv = StorageDevice("pv1", fmt=blivet.formats.get_format("lvmpv"), + size=Size("1 GiB")) +-- +2.40.1 + + +From bbfd1a70abe8271f5fe3d29fe2be3bb8a1c6ecbc Mon Sep 17 00:00:00 2001 +From: Vojtech Trefny +Date: Wed, 3 May 2023 08:55:31 +0200 +Subject: [PATCH 2/2] Revert "tests: Skip test_lvcreate_type on CentOS/RHEL 9" + +This reverts commit 16b90071145d2d0f19a38f3003561a0cc9d6e281. + +The kernel issue was resolved, we no longer need to skip the test. +--- + tests/skip.yml | 6 ------ + 1 file changed, 6 deletions(-) + +diff --git a/tests/skip.yml b/tests/skip.yml +index 66b34493..c0ca0eaf 100644 +--- a/tests/skip.yml ++++ b/tests/skip.yml +@@ -24,12 +24,6 @@ + + --- + +-- test: storage_tests.devices_test.lvm_test.LVMTestCase.test_lvm_raid +- skip_on: +- - distro: "centos" +- version: "9" +- reason: "Creating RAID 1 LV on CentOS/RHEL 9 causes a system deadlock" +- + - test: storage_tests.formats_test.fs_test.XFSTestCase.test_resize + skip_on: + - distro: ["centos", "enterprise_linux"] +-- +2.40.1 + diff --git a/SOURCES/0013-Fix-setting-kickstart-data.patch b/SOURCES/0013-Fix-setting-kickstart-data.patch new file mode 100644 index 0000000..040b5c6 --- /dev/null +++ b/SOURCES/0013-Fix-setting-kickstart-data.patch @@ -0,0 +1,68 @@ +From 1af0d3c37a93e431790e641a329a7f34dabf291a Mon Sep 17 00:00:00 2001 +From: Vojtech Trefny +Date: Thu, 2 Mar 2023 12:34:42 +0100 +Subject: [PATCH] Fix setting kickstart data + +When changing our code to PEP8 compliant we also changed some +pykickstart properties like onPart by accident. This PR fixes this. + +Resolves: rhbz#2174296 +--- + blivet/devices/btrfs.py | 4 ++-- + blivet/devices/lvm.py | 2 +- + blivet/devices/partition.py | 6 +++--- + 3 files changed, 6 insertions(+), 6 deletions(-) + +diff --git a/blivet/devices/btrfs.py b/blivet/devices/btrfs.py +index 1ae6a04d..3f56624e 100644 +--- a/blivet/devices/btrfs.py ++++ b/blivet/devices/btrfs.py +@@ -498,8 +498,8 @@ class BTRFSVolumeDevice(BTRFSDevice, ContainerDevice, RaidDevice): + + def populate_ksdata(self, data): + super(BTRFSVolumeDevice, self).populate_ksdata(data) +- data.data_level = self.data_level.name if self.data_level else None +- data.metadata_level = self.metadata_level.name if self.metadata_level else None ++ data.dataLevel = self.data_level.name if self.data_level else None ++ data.metaDataLevel = self.metadata_level.name if self.metadata_level else None + data.devices = ["btrfs.%d" % p.id for p in self.parents] + data.preexist = self.exists + +diff --git a/blivet/devices/lvm.py b/blivet/devices/lvm.py +index 41358e9b..c3132457 100644 +--- a/blivet/devices/lvm.py ++++ b/blivet/devices/lvm.py +@@ -1161,7 +1161,7 @@ class LVMLogicalVolumeBase(DMDevice, RaidDevice): + + if self.req_grow: + # base size could be literal or percentage +- data.max_size_mb = self.req_max_size.convert_to(MiB) ++ data.maxSizeMB = self.req_max_size.convert_to(MiB) + elif data.resize: + data.size = self.target_size.convert_to(MiB) + +diff --git a/blivet/devices/partition.py b/blivet/devices/partition.py +index 89d907c2..0e9250ce 100644 +--- a/blivet/devices/partition.py ++++ b/blivet/devices/partition.py +@@ -982,14 +982,14 @@ class PartitionDevice(StorageDevice): + data.size = self.req_base_size.round_to_nearest(MiB, rounding=ROUND_DOWN).convert_to(spec=MiB) + data.grow = self.req_grow + if self.req_grow: +- data.max_size_mb = self.req_max_size.convert_to(MiB) ++ data.maxSizeMB = self.req_max_size.convert_to(MiB) + + # data.disk = self.disk.name # by-id + if self.req_disks and len(self.req_disks) == 1: + data.disk = self.disk.name +- data.prim_only = self.req_primary ++ data.primOnly = self.req_primary + else: +- data.on_part = self.name # by-id ++ data.onPart = self.name # by-id + + if data.resize: + # on s390x in particular, fractional sizes are reported, which +-- +2.40.1 + diff --git a/SOURCES/0014-Do-not-set-memory-limit-for-LUKS2-when-running-in-FI.patch b/SOURCES/0014-Do-not-set-memory-limit-for-LUKS2-when-running-in-FI.patch new file mode 100644 index 0000000..8cc0bd5 --- /dev/null +++ b/SOURCES/0014-Do-not-set-memory-limit-for-LUKS2-when-running-in-FI.patch @@ -0,0 +1,133 @@ +From c2b06150df0b876c7d442097b6c9ca90c9ca2ecc Mon Sep 17 00:00:00 2001 +From: Vojtech Trefny +Date: Thu, 4 May 2023 11:35:44 +0200 +Subject: [PATCH] Do not set memory limit for LUKS2 when running in FIPS mode + +With FIPS enabled LUKS uses pbkdf and not argon so the memory +limit is not a valid parameter. + +Resolves: rhbz#2193096 +--- + blivet/devicelibs/crypto.py | 11 +++++++ + blivet/formats/luks.py | 12 ++++---- + tests/unit_tests/formats_tests/luks_test.py | 30 +++++++++++++++++++ + .../unit_tests/formats_tests/methods_test.py | 3 +- + 4 files changed, 50 insertions(+), 6 deletions(-) + +diff --git a/blivet/devicelibs/crypto.py b/blivet/devicelibs/crypto.py +index f0caf0f7..68e68db1 100644 +--- a/blivet/devicelibs/crypto.py ++++ b/blivet/devicelibs/crypto.py +@@ -21,6 +21,7 @@ + # + + import hashlib ++import os + + import gi + gi.require_version("BlockDev", "2.0") +@@ -100,3 +101,13 @@ def calculate_integrity_metadata_size(device_size, algorithm=DEFAULT_INTEGRITY_A + jsize = (jsize / SECTOR_SIZE + 1) * SECTOR_SIZE # round up to sector + + return msize + jsize ++ ++ ++def is_fips_enabled(): ++ if not os.path.exists("/proc/sys/crypto/fips_enabled"): ++ # if the file doesn't exist, we are definitely not in FIPS mode ++ return False ++ ++ with open("/proc/sys/crypto/fips_enabled", "r") as f: ++ enabled = f.read() ++ return enabled.strip() == "1" +diff --git a/blivet/formats/luks.py b/blivet/formats/luks.py +index 2637e0c5..adf3c711 100644 +--- a/blivet/formats/luks.py ++++ b/blivet/formats/luks.py +@@ -303,11 +303,13 @@ class LUKS(DeviceFormat): + if luks_data.pbkdf_args: + self.pbkdf_args = luks_data.pbkdf_args + else: +- mem_limit = crypto.calculate_luks2_max_memory() +- if mem_limit: +- self.pbkdf_args = LUKS2PBKDFArgs(max_memory_kb=int(mem_limit.convert_to(KiB))) +- luks_data.pbkdf_args = self.pbkdf_args +- log.info("PBKDF arguments for LUKS2 not specified, using defaults with memory limit %s", mem_limit) ++ # argon is not used with FIPS so we don't need to adjust the memory when in FIPS mode ++ if not crypto.is_fips_enabled(): ++ mem_limit = crypto.calculate_luks2_max_memory() ++ if mem_limit: ++ self.pbkdf_args = LUKS2PBKDFArgs(max_memory_kb=int(mem_limit.convert_to(KiB))) ++ luks_data.pbkdf_args = self.pbkdf_args ++ log.info("PBKDF arguments for LUKS2 not specified, using defaults with memory limit %s", mem_limit) + + if self.pbkdf_args: + pbkdf = blockdev.CryptoLUKSPBKDF(type=self.pbkdf_args.type, +diff --git a/tests/unit_tests/formats_tests/luks_test.py b/tests/unit_tests/formats_tests/luks_test.py +index ec7b7592..1127e968 100644 +--- a/tests/unit_tests/formats_tests/luks_test.py ++++ b/tests/unit_tests/formats_tests/luks_test.py +@@ -6,9 +6,14 @@ except ImportError: + import unittest + + from blivet.formats.luks import LUKS ++from blivet.size import Size ++from blivet.static_data import luks_data + + + class LUKSNodevTestCase(unittest.TestCase): ++ def setUp(self): ++ luks_data.pbkdf_args = None ++ + def test_create_discard_option(self): + # flags.discard_new=False --> no discard + fmt = LUKS(exists=False) +@@ -51,6 +56,31 @@ class LUKSNodevTestCase(unittest.TestCase): + fmt = LUKS(cipher="aes-cbc-plain64") + self.assertEqual(fmt.key_size, 0) + ++ def test_luks2_pbkdf_memory_fips(self): ++ fmt = LUKS() ++ with patch("blivet.formats.luks.blockdev.crypto") as bd: ++ # fips enabled, pbkdf memory should not be set ++ with patch("blivet.formats.luks.crypto") as crypto: ++ attrs = {"is_fips_enabled.return_value": True, ++ "get_optimal_luks_sector_size.return_value": 0, ++ "calculate_luks2_max_memory.return_value": Size("256 MiB")} ++ crypto.configure_mock(**attrs) ++ ++ fmt._create() ++ crypto.calculate_luks2_max_memory.assert_not_called() ++ self.assertEqual(bd.luks_format.call_args[1]["extra"].pbkdf.max_memory_kb, 0) ++ ++ # fips disabled, pbkdf memory should be set ++ with patch("blivet.formats.luks.crypto") as crypto: ++ attrs = {"is_fips_enabled.return_value": False, ++ "get_optimal_luks_sector_size.return_value": 0, ++ "calculate_luks2_max_memory.return_value": Size("256 MiB")} ++ crypto.configure_mock(**attrs) ++ ++ fmt._create() ++ crypto.calculate_luks2_max_memory.assert_called() ++ self.assertEqual(bd.luks_format.call_args[1]["extra"].pbkdf.max_memory_kb, 256 * 1024) ++ + def test_sector_size(self): + fmt = LUKS() + self.assertEqual(fmt.luks_sector_size, 512) +diff --git a/tests/unit_tests/formats_tests/methods_test.py b/tests/unit_tests/formats_tests/methods_test.py +index 2743b7db..5d30c260 100644 +--- a/tests/unit_tests/formats_tests/methods_test.py ++++ b/tests/unit_tests/formats_tests/methods_test.py +@@ -366,7 +366,8 @@ class LUKSMethodsTestCase(FormatMethodsTestCase): + + def _test_create_backend(self): + self.format.exists = False +- self.format.create() ++ with patch("blivet.devicelibs.crypto.is_fips_enabled", return_value=False): ++ self.format.create() + self.assertTrue(self.patches["blockdev"].crypto.luks_format.called) # pylint: disable=no-member + + def _test_setup_backend(self): +-- +2.40.1 + diff --git a/SOURCES/0015-Add-support-for-filesystem-online-resize.patch b/SOURCES/0015-Add-support-for-filesystem-online-resize.patch new file mode 100644 index 0000000..39f30e8 --- /dev/null +++ b/SOURCES/0015-Add-support-for-filesystem-online-resize.patch @@ -0,0 +1,265 @@ +From eb16230427fc1081f8515e6ad69ccf99ca521e5d Mon Sep 17 00:00:00 2001 +From: Vojtech Trefny +Date: Tue, 4 Apr 2023 13:31:40 +0200 +Subject: [PATCH 1/2] Add support for filesystem online resize + +Resolves: rhbz#2168680 +--- + blivet/devices/lvm.py | 13 ++++++++----- + blivet/devices/partition.py | 11 ++++++----- + blivet/flags.py | 3 +++ + blivet/formats/fs.py | 32 ++++++++++++++++++++++++++++---- + blivet/formats/fslib.py | 7 +++++++ + 5 files changed, 52 insertions(+), 14 deletions(-) + +diff --git a/blivet/devices/lvm.py b/blivet/devices/lvm.py +index c3132457..ca45c4b5 100644 +--- a/blivet/devices/lvm.py ++++ b/blivet/devices/lvm.py +@@ -42,6 +42,7 @@ from .. import errors + from .. import util + from ..storage_log import log_method_call + from .. import udev ++from ..flags import flags + from ..size import Size, KiB, MiB, ROUND_UP, ROUND_DOWN + from ..static_data.lvm_info import lvs_info + from ..tasks import availability +@@ -2729,12 +2730,14 @@ class LVMLogicalVolumeDevice(LVMLogicalVolumeBase, LVMInternalLogicalVolumeMixin + # Setup VG parents (in case they are dmraid partitions for example) + self.vg.setup_parents(orig=True) + +- if self.original_format.exists: +- self.original_format.teardown() +- if self.format.exists: +- self.format.teardown() ++ if not flags.allow_online_fs_resize: ++ if self.original_format.exists: ++ self.original_format.teardown() ++ if self.format.exists: ++ self.format.teardown() ++ ++ udev.settle() + +- udev.settle() + blockdev.lvm.lvresize(self.vg.name, self._name, self.size) + + @type_specific +diff --git a/blivet/devices/partition.py b/blivet/devices/partition.py +index 0e9250ce..6ae4b8d3 100644 +--- a/blivet/devices/partition.py ++++ b/blivet/devices/partition.py +@@ -745,11 +745,12 @@ class PartitionDevice(StorageDevice): + if not self.exists: + raise errors.DeviceError("device has not been created") + +- # don't teardown when resizing luks +- if self.format.type == "luks" and self.children: +- self.children[0].format.teardown() +- else: +- self.teardown() ++ if not flags.allow_online_fs_resize: ++ # don't teardown when resizing luks ++ if self.format.type == "luks" and self.children: ++ self.children[0].format.teardown() ++ else: ++ self.teardown() + + if not self.sysfs_path: + return +diff --git a/blivet/flags.py b/blivet/flags.py +index 6364164d..ecfa7ad7 100644 +--- a/blivet/flags.py ++++ b/blivet/flags.py +@@ -91,6 +91,9 @@ class Flags(object): + + self.debug_threads = False + ++ # Allow online filesystem resizes ++ self.allow_online_fs_resize = False ++ + def get_boot_cmdline(self): + with open("/proc/cmdline") as f: + buf = f.read().strip() +diff --git a/blivet/formats/fs.py b/blivet/formats/fs.py +index 33922f3a..3f553eb0 100644 +--- a/blivet/formats/fs.py ++++ b/blivet/formats/fs.py +@@ -56,7 +56,7 @@ from ..i18n import N_ + from .. import udev + from ..mounts import mounts_cache + +-from .fslib import kernel_filesystems ++from .fslib import kernel_filesystems, FSResize + + import logging + log = logging.getLogger("blivet") +@@ -88,6 +88,9 @@ class FS(DeviceFormat): + # value is already unpredictable and can change in the future... + _metadata_size_factor = 1.0 + ++ # support for resize: grow/shrink, online/offline ++ _resize_support = 0 ++ + config_actions_map = {"label": "write_label"} + + def __init__(self, **kwargs): +@@ -436,12 +439,27 @@ class FS(DeviceFormat): + self.write_uuid() + + def _pre_resize(self): +- # file systems need a check before being resized +- self.do_check() ++ if self.status: ++ if flags.allow_online_fs_resize: ++ if self.target_size > self.size and not self._resize_support & FSResize.ONLINE_GROW: ++ raise FSError("This filesystem doesn't support online growing") ++ if self.target_size < self.size and not self._resize_support & FSResize.ONLINE_SHRINK: ++ raise FSError("This filesystem doesn't support online shrinking") ++ else: ++ raise FSError("Resizing of mounted filesystems is disabled") ++ ++ if self.status: ++ # fsck tools in general don't allow checks on mounted filesystems ++ log.debug("Filesystem on %s is mounted, not checking", self.device) ++ else: ++ # file systems need a check before being resized ++ self.do_check() ++ + super(FS, self)._pre_resize() + + def _post_resize(self): +- self.do_check() ++ if not self.status: ++ self.do_check() + super(FS, self)._post_resize() + + def do_check(self): +@@ -838,6 +856,7 @@ class Ext2FS(FS): + _formattable = True + _supported = True + _resizable = True ++ _resize_support = FSResize.ONLINE_GROW | FSResize.OFFLINE_GROW | FSResize.OFFLINE_SHRINK + _linux_native = True + _max_size = Size("8 TiB") + _dump = True +@@ -1097,6 +1116,7 @@ class XFS(FS): + _linux_native = True + _supported = True + _resizable = True ++ _resize_support = FSResize.ONLINE_GROW | FSResize.OFFLINE_GROW + _packages = ["xfsprogs"] + _fsck_class = fsck.XFSCK + _info_class = fsinfo.XFSInfo +@@ -1247,6 +1267,7 @@ class NTFS(FS): + _labelfs = fslabeling.NTFSLabeling() + _uuidfs = fsuuid.NTFSUUID() + _resizable = True ++ _resize_support = FSResize.OFFLINE_GROW | FSResize.OFFLINE_SHRINK + _formattable = True + _supported = True + _min_size = Size("1 MiB") +@@ -1490,6 +1511,9 @@ class TmpFS(NoDevFS): + # same, nothing actually needs to be set + pass + ++ def _pre_resize(self): ++ self.do_check() ++ + def do_resize(self): + # Override superclass method to record whether mount options + # should include an explicit size specification. +diff --git a/blivet/formats/fslib.py b/blivet/formats/fslib.py +index ea93b1fd..8722e942 100644 +--- a/blivet/formats/fslib.py ++++ b/blivet/formats/fslib.py +@@ -36,3 +36,10 @@ def update_kernel_filesystems(): + + + update_kernel_filesystems() ++ ++ ++class FSResize(): ++ OFFLINE_SHRINK = 1 << 1 ++ OFFLINE_GROW = 1 << 2 ++ ONLINE_SHRINK = 1 << 3 ++ ONLINE_GROW = 1 << 4 +-- +2.40.1 + + +From 3fce5d0bfd7b09a976ff49feed15077477c6a425 Mon Sep 17 00:00:00 2001 +From: Vojtech Trefny +Date: Thu, 6 Apr 2023 14:02:11 +0200 +Subject: [PATCH 2/2] Add a test case for filesystem online resize + +Related: rhbz#2168680 +--- + tests/storage_tests/formats_test/fs_test.py | 43 ++++++++++++++++++++- + 1 file changed, 42 insertions(+), 1 deletion(-) + +diff --git a/tests/storage_tests/formats_test/fs_test.py b/tests/storage_tests/formats_test/fs_test.py +index 97f4cbbe..1d42dc21 100644 +--- a/tests/storage_tests/formats_test/fs_test.py ++++ b/tests/storage_tests/formats_test/fs_test.py +@@ -6,9 +6,10 @@ import parted + + import blivet.formats.fs as fs + from blivet.size import Size, ROUND_DOWN +-from blivet.errors import DeviceFormatError ++from blivet.errors import DeviceFormatError, FSError + from blivet.formats import get_format + from blivet.devices import PartitionDevice, DiskDevice ++from blivet.flags import flags + + from .loopbackedtestcase import LoopBackedTestCase + +@@ -26,6 +27,46 @@ class Ext3FSTestCase(Ext2FSTestCase): + class Ext4FSTestCase(Ext3FSTestCase): + _fs_class = fs.Ext4FS + ++ def test_online_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() ++ ++ if not self.can_resize(an_fs): ++ self.skipTest("filesystem is not resizable") ++ ++ # shrink offline first (ext doesn't support online shrinking) ++ 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()) ++ ++ with tempfile.TemporaryDirectory() as mountpoint: ++ an_fs.mount(mountpoint=mountpoint) ++ ++ # grow back when mounted ++ TARGET_SIZE = Size("100 MiB") ++ an_fs.target_size = TARGET_SIZE ++ self.assertEqual(an_fs.target_size, TARGET_SIZE) ++ self.assertNotEqual(an_fs._size, TARGET_SIZE) ++ ++ # should fail, online resize disabled by default ++ with self.assertRaisesRegex(FSError, "Resizing of mounted filesystems is disabled"): ++ an_fs.do_resize() ++ ++ # enable online resize ++ flags.allow_online_fs_resize = True ++ an_fs.do_resize() ++ flags.allow_online_fs_resize = False ++ self._test_sizes(an_fs) ++ self.assertEqual(an_fs.system_mountpoint, mountpoint) ++ ++ an_fs.unmount() ++ + + class FATFSTestCase(fstesting.FSAsRoot): + _fs_class = fs.FATFS +-- +2.40.1 + diff --git a/SOURCES/0016-Backport-iSCSI-initiator-name-related-fixes.patch b/SOURCES/0016-Backport-iSCSI-initiator-name-related-fixes.patch new file mode 100644 index 0000000..e212aad --- /dev/null +++ b/SOURCES/0016-Backport-iSCSI-initiator-name-related-fixes.patch @@ -0,0 +1,383 @@ +From 2d26f69abc2d793e753c0cddb3086264ca2b4e71 Mon Sep 17 00:00:00 2001 +From: Vojtech Trefny +Date: Mon, 6 Mar 2023 10:51:42 +0100 +Subject: [PATCH 1/5] Allow changing iSCSI initiator name after setting it + +Resolves: rhbz#2221935 +--- + blivet/iscsi.py | 13 +++++++++++-- + 1 file changed, 11 insertions(+), 2 deletions(-) + +diff --git a/blivet/iscsi.py b/blivet/iscsi.py +index 86451db3..0d063f2a 100644 +--- a/blivet/iscsi.py ++++ b/blivet/iscsi.py +@@ -212,14 +212,23 @@ class iSCSI(object): + @initiator.setter + @storaged_iscsi_required(critical=True, eval_mode=util.EvalMode.onetime) + def initiator(self, val): +- if self.initiator_set and val != self._initiator: +- raise ValueError(_("Unable to change iSCSI initiator name once set")) + if len(val) == 0: + raise ValueError(_("Must provide an iSCSI initiator name")) + ++ active = self._get_active_sessions() ++ if active: ++ raise errors.ISCSIError(_("Cannot change initiator name with an active session")) ++ + log.info("Setting up iSCSI initiator name %s", self.initiator) + args = GLib.Variant("(sa{sv})", (val, None)) + self._call_initiator_method("SetInitiatorName", args) ++ ++ if self.initiator_set and val != self._initiator: ++ log.info("Restarting iscsid after initiator name change") ++ rc = util.run_program(["systemctl", "restart", "iscsid"]) ++ if rc != 0: ++ raise errors.ISCSIError(_("Failed to restart iscsid after initiator name change")) ++ + self._initiator = val + + def active_nodes(self, target=None): +-- +2.40.1 + + +From 7c226ed0e14efcdd6e562e357d8f3465ad43ef33 Mon Sep 17 00:00:00 2001 +From: Vojtech Trefny +Date: Mon, 6 Mar 2023 15:10:28 +0100 +Subject: [PATCH 2/5] Add a basic test case for the iscsi module + +Related: rhbz#2221935 +--- + misc/install-test-dependencies.yml | 3 + + tests/storage_tests/__init__.py | 2 + + tests/storage_tests/iscsi_test.py | 157 +++++++++++++++++++++++++++++ + 3 files changed, 162 insertions(+) + create mode 100644 tests/storage_tests/iscsi_test.py + +diff --git a/tests/storage_tests/__init__.py b/tests/storage_tests/__init__.py +index 3b2a6cc4..e69fcc34 100644 +--- a/tests/storage_tests/__init__.py ++++ b/tests/storage_tests/__init__.py +@@ -3,3 +3,5 @@ from .formats_test import * + + from .partitioning_test import * + from .unsupported_disklabel_test import * ++ ++from .iscsi_test import * +diff --git a/tests/storage_tests/iscsi_test.py b/tests/storage_tests/iscsi_test.py +new file mode 100644 +index 00000000..00cc7c36 +--- /dev/null ++++ b/tests/storage_tests/iscsi_test.py +@@ -0,0 +1,157 @@ ++import glob ++import os ++import re ++import shutil ++import subprocess ++import unittest ++ ++from contextlib import contextmanager ++ ++from .storagetestcase import create_sparse_tempfile ++ ++ ++def read_file(filename, mode="r"): ++ with open(filename, mode) as f: ++ content = f.read() ++ return content ++ ++ ++@contextmanager ++def udev_settle(): ++ try: ++ yield ++ finally: ++ os.system("udevadm settle") ++ ++ ++def _delete_backstore(name): ++ status = subprocess.call(["targetcli", "/backstores/fileio/ delete %s" % name], ++ stdout=subprocess.DEVNULL) ++ if status != 0: ++ raise RuntimeError("Failed to delete the '%s' fileio backstore" % name) ++ ++ ++def delete_iscsi_target(iqn, backstore=None): ++ status = subprocess.call(["targetcli", "/iscsi delete %s" % iqn], ++ stdout=subprocess.DEVNULL) ++ if status != 0: ++ raise RuntimeError("Failed to delete the '%s' iscsi device" % iqn) ++ ++ if backstore is not None: ++ _delete_backstore(backstore) ++ ++ ++def create_iscsi_target(fpath, initiator_name=None): ++ """ ++ Creates a new iSCSI target (using targetcli) on top of the ++ :param:`fpath` backing file. ++ ++ :param str fpath: path of the backing file ++ :returns: iSCSI IQN, backstore name ++ :rtype: tuple of str ++ ++ """ ++ ++ # "register" the backing file as a fileio backstore ++ store_name = os.path.basename(fpath) ++ status = subprocess.call(["targetcli", "/backstores/fileio/ create %s %s" % (store_name, fpath)], stdout=subprocess.DEVNULL) ++ if status != 0: ++ raise RuntimeError("Failed to register '%s' as a fileio backstore" % fpath) ++ ++ out = subprocess.check_output(["targetcli", "/backstores/fileio/%s info" % store_name]) ++ out = out.decode("utf-8") ++ store_wwn = None ++ for line in out.splitlines(): ++ if line.startswith("wwn: "): ++ store_wwn = line[5:] ++ if store_wwn is None: ++ raise RuntimeError("Failed to determine '%s' backstore's wwn" % store_name) ++ ++ # create a new iscsi device ++ out = subprocess.check_output(["targetcli", "/iscsi create"]) ++ out = out.decode("utf-8") ++ match = re.match(r'Created target (.*).', out) ++ if match: ++ iqn = match.groups()[0] ++ else: ++ _delete_backstore(store_name) ++ raise RuntimeError("Failed to create a new iscsi target") ++ ++ if initiator_name: ++ status = subprocess.call(["targetcli", "/iscsi/%s/tpg1/acls create %s" % (iqn, initiator_name)], stdout=subprocess.DEVNULL) ++ if status != 0: ++ delete_iscsi_target(iqn, store_name) ++ raise RuntimeError("Failed to set ACLs for '%s'" % iqn) ++ ++ with udev_settle(): ++ status = subprocess.call(["targetcli", "/iscsi/%s/tpg1/luns create /backstores/fileio/%s" % (iqn, store_name)], stdout=subprocess.DEVNULL) ++ if status != 0: ++ delete_iscsi_target(iqn, store_name) ++ raise RuntimeError("Failed to create a new LUN for '%s' using '%s'" % (iqn, store_name)) ++ ++ status = subprocess.call(["targetcli", "/iscsi/%s/tpg1 set attribute generate_node_acls=1" % iqn], stdout=subprocess.DEVNULL) ++ if status != 0: ++ raise RuntimeError("Failed to set ACLs for '%s'" % iqn) ++ ++ return iqn, store_name ++ ++ ++@unittest.skipUnless(os.geteuid() == 0, "requires root privileges") ++@unittest.skipUnless(os.environ.get("JENKINS_HOME"), "jenkins only test") ++@unittest.skipUnless(shutil.which("iscsiadm"), "iscsiadm not available") ++class ISCSITestCase(unittest.TestCase): ++ ++ _disk_size = 512 * 1024**2 ++ initiator = 'iqn.1994-05.com.redhat:iscsi-test' ++ ++ def setUp(self): ++ self.addCleanup(self._clean_up) ++ ++ self._dev_file = None ++ self.dev = None ++ ++ self._dev_file = create_sparse_tempfile("blivet_test", self._disk_size) ++ try: ++ self.dev, self.backstore = create_iscsi_target(self._dev_file, self.initiator) ++ except RuntimeError as e: ++ raise RuntimeError("Failed to setup targetcli device for testing: %s" % e) ++ ++ def _force_logout(self): ++ subprocess.call(["iscsiadm", "--mode", "node", "--logout", "--name", self.dev], stdout=subprocess.DEVNULL) ++ ++ def _clean_up(self): ++ self._force_logout() ++ delete_iscsi_target(self.dev, self.backstore) ++ os.unlink(self._dev_file) ++ ++ def test_discover_login(self): ++ from blivet.iscsi import iscsi, has_iscsi ++ ++ if not has_iscsi(): ++ self.skipTest("iSCSI not available, skipping") ++ ++ iscsi.initiator = self.initiator ++ nodes = iscsi.discover("127.0.0.1") ++ self.assertTrue(nodes) ++ ++ if len(nodes) > 1: ++ self.skipTest("Discovered more than one iSCSI target on localhost, skipping") ++ ++ self.assertEqual(nodes[0].address, "127.0.0.1") ++ self.assertEqual(nodes[0].port, 3260) ++ self.assertEqual(nodes[0].name, self.dev) ++ ++ # change the initiator name ++ iscsi.initiator = self.initiator + "_1" ++ self.assertEqual(iscsi.initiator, self.initiator + "_1") ++ ++ # try to login ++ ret, err = iscsi.log_into_node(nodes[0]) ++ self.assertTrue(ret, "Login failed: %s" % err) ++ ++ # check the session for initiator name ++ sessions = glob.glob("/sys/class/iscsi_session/*/") ++ self.assertTrue(sessions) ++ self.assertEqual(len(sessions), 1) ++ initiator = read_file(sessions[0] + "initiatorname").strip() ++ self.assertEqual(initiator, iscsi.initiator) +-- +2.40.1 + + +From dfd0c59a901f54ecfd8f538a2bb004a2e5ab6eec Mon Sep 17 00:00:00 2001 +From: Vojtech Trefny +Date: Mon, 6 Mar 2023 15:14:40 +0100 +Subject: [PATCH 3/5] tests: Use blivet-specific prefix for targetcli backing + files + +The code is originally from libblockdev hence the "bd" prefix, we +should use a different prefix for blivet to be able to identify +which test suite failed to clean the files. + +Related: rhbz#2221935 +--- + tests/storage_tests/storagetestcase.py | 2 +- + 1 file changed, 1 insertion(+), 1 deletion(-) + +diff --git a/tests/storage_tests/storagetestcase.py b/tests/storage_tests/storagetestcase.py +index 35d57ce9..9f859977 100644 +--- a/tests/storage_tests/storagetestcase.py ++++ b/tests/storage_tests/storagetestcase.py +@@ -39,7 +39,7 @@ def create_sparse_tempfile(name, size): + :param size: the file size (in bytes) + :returns: the path to the newly created file + """ +- (fd, path) = tempfile.mkstemp(prefix="bd.", suffix="-%s" % name) ++ (fd, path) = tempfile.mkstemp(prefix="blivet.", suffix="-%s" % name) + os.close(fd) + create_sparse_file(path, size) + return path +-- +2.40.1 + + +From 492fc9b8dfc2d4aa7cb44baa4408570babcb5e96 Mon Sep 17 00:00:00 2001 +From: Vojtech Trefny +Date: Wed, 19 Jul 2023 13:57:39 +0200 +Subject: [PATCH 4/5] iscsi: Save firmware initiator name to + /etc/iscsi/initiatorname.iscsi + +Resolves: rhbz#2221932 +--- + blivet/iscsi.py | 5 +++++ + 1 file changed, 5 insertions(+) + +diff --git a/blivet/iscsi.py b/blivet/iscsi.py +index 0d063f2a..8080a671 100644 +--- a/blivet/iscsi.py ++++ b/blivet/iscsi.py +@@ -160,6 +160,11 @@ class iSCSI(object): + self._initiator = initiatorname + except Exception as e: # pylint: disable=broad-except + log.info("failed to get initiator name from iscsi firmware: %s", str(e)) ++ else: ++ # write the firmware initiator to /etc/iscsi/initiatorname.iscsi ++ log.info("Setting up firmware iSCSI initiator name %s", self.initiator) ++ args = GLib.Variant("(sa{sv})", (initiatorname, None)) ++ self._call_initiator_method("SetInitiatorName", args) + + # So that users can write iscsi() to get the singleton instance + def __call__(self): +-- +2.40.1 + + +From 768d90815b7f95d0d6d278397fd6fd12a0490b5d Mon Sep 17 00:00:00 2001 +From: Vojtech Trefny +Date: Wed, 19 Jul 2023 10:38:45 +0200 +Subject: [PATCH 5/5] tests: Improve iscsi_test.ISCSITestCase + +Changed how we create the initiator name ACLs based on RTT test +case for rhbz#2084043 and also improved the test case itself. + +Related: rhbz#2221935 +--- + tests/storage_tests/iscsi_test.py | 36 +++++++++++++++++++++---------- + 1 file changed, 25 insertions(+), 11 deletions(-) + +diff --git a/tests/storage_tests/iscsi_test.py b/tests/storage_tests/iscsi_test.py +index 00cc7c36..6cc83a59 100644 +--- a/tests/storage_tests/iscsi_test.py ++++ b/tests/storage_tests/iscsi_test.py +@@ -77,21 +77,17 @@ def create_iscsi_target(fpath, initiator_name=None): + _delete_backstore(store_name) + raise RuntimeError("Failed to create a new iscsi target") + +- if initiator_name: +- status = subprocess.call(["targetcli", "/iscsi/%s/tpg1/acls create %s" % (iqn, initiator_name)], stdout=subprocess.DEVNULL) +- if status != 0: +- delete_iscsi_target(iqn, store_name) +- raise RuntimeError("Failed to set ACLs for '%s'" % iqn) +- + with udev_settle(): + status = subprocess.call(["targetcli", "/iscsi/%s/tpg1/luns create /backstores/fileio/%s" % (iqn, store_name)], stdout=subprocess.DEVNULL) + if status != 0: + delete_iscsi_target(iqn, store_name) + raise RuntimeError("Failed to create a new LUN for '%s' using '%s'" % (iqn, store_name)) + +- status = subprocess.call(["targetcli", "/iscsi/%s/tpg1 set attribute generate_node_acls=1" % iqn], stdout=subprocess.DEVNULL) +- if status != 0: +- raise RuntimeError("Failed to set ACLs for '%s'" % iqn) ++ if initiator_name: ++ status = subprocess.call(["targetcli", "/iscsi/%s/tpg1/acls create %s" % (iqn, initiator_name)], stdout=subprocess.DEVNULL) ++ if status != 0: ++ delete_iscsi_target(iqn, store_name) ++ raise RuntimeError("Failed to set ACLs for '%s'" % iqn) + + return iqn, store_name + +@@ -130,6 +126,7 @@ class ISCSITestCase(unittest.TestCase): + if not has_iscsi(): + self.skipTest("iSCSI not available, skipping") + ++ # initially set the initiator to the correct/allowed one + iscsi.initiator = self.initiator + nodes = iscsi.discover("127.0.0.1") + self.assertTrue(nodes) +@@ -141,11 +138,28 @@ class ISCSITestCase(unittest.TestCase): + self.assertEqual(nodes[0].port, 3260) + self.assertEqual(nodes[0].name, self.dev) + +- # change the initiator name ++ # change the initiator name to a wrong one + iscsi.initiator = self.initiator + "_1" + self.assertEqual(iscsi.initiator, self.initiator + "_1") + +- # try to login ++ # check the change made it to /etc/iscsi/initiatorname.iscsi ++ initiator_file = read_file("/etc/iscsi/initiatorname.iscsi").strip() ++ self.assertEqual(initiator_file, "InitiatorName=%s" % self.initiator + "_1") ++ ++ # try to login (should fail) ++ ret, err = iscsi.log_into_node(nodes[0]) ++ self.assertFalse(ret) ++ self.assertIn("authorization failure", err) ++ ++ # change the initiator name back to the correct one ++ iscsi.initiator = self.initiator ++ self.assertEqual(iscsi.initiator, self.initiator) ++ ++ # check the change made it to /etc/iscsi/initiatorname.iscsi ++ initiator_file = read_file("/etc/iscsi/initiatorname.iscsi").strip() ++ self.assertEqual(initiator_file, "InitiatorName=%s" % self.initiator) ++ ++ # try to login (should work now) + ret, err = iscsi.log_into_node(nodes[0]) + self.assertTrue(ret, "Login failed: %s" % err) + +-- +2.40.1 + diff --git a/SPECS/python-blivet.spec b/SPECS/python-blivet.spec index b51280b..6ca0a35 100644 --- a/SPECS/python-blivet.spec +++ b/SPECS/python-blivet.spec @@ -23,7 +23,7 @@ Version: 3.6.0 #%%global prerelease .b2 # prerelease, if defined, should be something like .a1, .b1, .b2.dev1, or .c2 -Release: 5%{?prerelease}%{?dist} +Release: 9%{?prerelease}%{?dist} Epoch: 1 License: LGPLv2+ %global realname blivet @@ -41,6 +41,11 @@ Patch7: 0008-Revert-Adjust-to-new-XFS-min-size.patch Patch8: 0009-Catch-BlockDevNotImplementedError-for-btrfs-plugin-c.patch Patch9: 0010-Add-basic-support-for-NVMe-and-NVMe-Fabrics-devices.patch Patch10: 0011-Default-to-encryption-sector-size-512-for-LUKS-devic.patch +Patch11: 0012-Add-support-for-specifying-stripe-size-for-RAID-LVs.patch +Patch12: 0013-Fix-setting-kickstart-data.patch +Patch13: 0014-Do-not-set-memory-limit-for-LUKS2-when-running-in-FI.patch +Patch14: 0015-Add-support-for-filesystem-online-resize.patch +Patch15: 0016-Backport-iSCSI-initiator-name-related-fixes.patch # Versions of required components (done so we make sure the buildrequires # match the requires versions of things). @@ -204,7 +209,35 @@ configuration. %endif %changelog -* Wed Mar 15 2023 MSVSphere Packaging Team - 3.4.0-16 + +* Mon Jul 24 2023 Jan Pokorny - 3.6.0-9 + Backport iSCSI initiator name related fixes: +- Allow changing iSCSI initiator name after setting it + Resolves: rhbz#2221935 +- Add a basic test case for the iscsi module + Resolves: rhbz#2221935 +- tests: Use blivet-specific prefix for targetcli backing files + Resolves: rhbz#2221935 +- iscsi: Save firmware initiator name to /etc/iscsi/initiatorname.iscsi + Resolves: rhbz#2221932 +- tests: Improve iscsi_test.ISCSITestCase + Resolves: rhbz#2221935 + +* Wed May 24 2023 Vojtech Trefny - 3.6.0-8 +- Add support for filesystem online resize + Resolves: RHEL-326 + +* Thu May 18 2023 Vojtech Trefny - 3.6.0-7 +- Fix setting kickstart data + Resolves: rhbz#2174296 +- Do not set memory limit for LUKS2 when running in FIPS mode + Resolves: rhbz#2193096 + +* Tue May 02 2023 Vojtech Trefny - 3.6.0-6 +- Add support for specifying stripe size for RAID LVs + Resolves: RHEL-327 + +* Wed Mar 15 2023 MSVSphere Packaging Team - 3.6.0-5 - Rebuilt for MSVSphere 9.1. * Thu Jan 19 2023 Vojtech Trefny - 3.6.0-5