Blob Blame History Raw
From 433d863cd8a57e5fc30948ff905e6a477ed5f17c Mon Sep 17 00:00:00 2001
From: Vojtech Trefny <vtrefny@redhat.com>
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 <amulhern@redhat.com>
 
 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 <vtrefny@redhat.com>
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 <vtrefny@redhat.com>
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 <vtrefny@redhat.com>
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