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