667f11
From 433d863cd8a57e5fc30948ff905e6a477ed5f17c Mon Sep 17 00:00:00 2001
667f11
From: Vojtech Trefny <vtrefny@redhat.com>
667f11
Date: Tue, 14 Jul 2020 11:27:08 +0200
667f11
Subject: [PATCH 1/4] Add support for XFS format grow
667f11
667f11
---
667f11
 blivet/formats/fs.py         |  2 ++
667f11
 blivet/tasks/availability.py |  1 +
667f11
 blivet/tasks/fsresize.py     | 54 ++++++++++++++++++++++++++++++++++++
667f11
 3 files changed, 57 insertions(+)
667f11
667f11
diff --git a/blivet/formats/fs.py b/blivet/formats/fs.py
667f11
index eee15aaa..12cb9885 100644
667f11
--- a/blivet/formats/fs.py
667f11
+++ b/blivet/formats/fs.py
667f11
@@ -1089,11 +1089,13 @@ class XFS(FS):
667f11
     _formattable = True
667f11
     _linux_native = True
667f11
     _supported = True
667f11
+    _resizable = True
667f11
     _packages = ["xfsprogs"]
667f11
     _info_class = fsinfo.XFSInfo
667f11
     _mkfs_class = fsmkfs.XFSMkfs
667f11
     _readlabel_class = fsreadlabel.XFSReadLabel
667f11
     _size_info_class = fssize.XFSSize
667f11
+    _resize_class = fsresize.XFSResize
667f11
     _sync_class = fssync.XFSSync
667f11
     _writelabel_class = fswritelabel.XFSWriteLabel
667f11
     _writeuuid_class = fswriteuuid.XFSWriteUUID
667f11
diff --git a/blivet/tasks/availability.py b/blivet/tasks/availability.py
667f11
index b6b5955a..df62780c 100644
667f11
--- a/blivet/tasks/availability.py
667f11
+++ b/blivet/tasks/availability.py
667f11
@@ -455,5 +455,6 @@ TUNE2FS_APP = application_by_version("tune2fs", E2FSPROGS_VERSION)
667f11
 XFSADMIN_APP = application("xfs_admin")
667f11
 XFSDB_APP = application("xfs_db")
667f11
 XFSFREEZE_APP = application("xfs_freeze")
667f11
+XFSRESIZE_APP = application("xfs_growfs")
667f11
 
667f11
 MOUNT_APP = application("mount")
667f11
diff --git a/blivet/tasks/fsresize.py b/blivet/tasks/fsresize.py
667f11
index e7e26984..12c0367f 100644
667f11
--- a/blivet/tasks/fsresize.py
667f11
+++ b/blivet/tasks/fsresize.py
667f11
@@ -20,7 +20,10 @@
667f11
 # Red Hat Author(s): Anne Mulhern <amulhern@redhat.com>
667f11
 
667f11
 import abc
667f11
+import os
667f11
+import tempfile
667f11
 
667f11
+from contextlib import contextmanager
667f11
 from six import add_metaclass
667f11
 
667f11
 from ..errors import FSError
667f11
@@ -32,6 +35,9 @@ from . import task
667f11
 from . import fstask
667f11
 from . import dfresize
667f11
 
667f11
+import logging
667f11
+log = logging.getLogger("blivet")
667f11
+
667f11
 
667f11
 @add_metaclass(abc.ABCMeta)
667f11
 class FSResizeTask(fstask.FSTask):
667f11
@@ -115,6 +121,54 @@ class NTFSResize(FSResize):
667f11
         ]
667f11
 
667f11
 
667f11
+class XFSResize(FSResize):
667f11
+    ext = availability.XFSRESIZE_APP
667f11
+    unit = B
667f11
+    size_fmt = None
667f11
+
667f11
+    @contextmanager
667f11
+    def _do_temp_mount(self):
667f11
+        if self.fs.status:
667f11
+            yield
667f11
+        else:
667f11
+            dev_name = os.path.basename(self.fs.device)
667f11
+            tmpdir = tempfile.mkdtemp(prefix="xfs-tempmount-%s" % dev_name)
667f11
+            log.debug("mounting XFS on '%s' to '%s' for resize", self.fs.device, tmpdir)
667f11
+            try:
667f11
+                self.fs.mount(mountpoint=tmpdir)
667f11
+            except FSError as e:
667f11
+                raise FSError("Failed to mount XFS filesystem for resize: %s" % str(e))
667f11
+
667f11
+            try:
667f11
+                yield
667f11
+            finally:
667f11
+                util.umount(mountpoint=tmpdir)
667f11
+                os.rmdir(tmpdir)
667f11
+
667f11
+    def _get_block_size(self):
667f11
+        if self.fs._current_info:
667f11
+            # this should be set by update_size_info()
667f11
+            for line in self.fs._current_info.split("\n"):
667f11
+                if line.startswith("blocksize ="):
667f11
+                    return int(line.split("=")[-1])
667f11
+
667f11
+        raise FSError("Failed to get XFS filesystem block size for resize")
667f11
+
667f11
+    def size_spec(self):
667f11
+        # size for xfs_growfs is in blocks
667f11
+        return str(self.fs.target_size.convert_to(self.unit) / self._get_block_size())
667f11
+
667f11
+    @property
667f11
+    def args(self):
667f11
+        return [self.fs.system_mountpoint, "-D", self.size_spec()]
667f11
+
667f11
+    def do_task(self):
667f11
+        """ Resizes the XFS format. """
667f11
+
667f11
+        with self._do_temp_mount():
667f11
+            super(XFSResize, self).do_task()
667f11
+
667f11
+
667f11
 class TmpFSResize(FSResize):
667f11
 
667f11
     ext = availability.MOUNT_APP
667f11
-- 
667f11
2.26.2
667f11
667f11
667f11
From 56d05334231c30699a9c77dedbc23fdb021b9dee Mon Sep 17 00:00:00 2001
667f11
From: Vojtech Trefny <vtrefny@redhat.com>
667f11
Date: Tue, 14 Jul 2020 11:27:51 +0200
667f11
Subject: [PATCH 2/4] Add tests for XFS resize
667f11
667f11
XFS supports only grow so we can't reuse most of the fstesting
667f11
code and we also need to test the resize on partition because
667f11
XFS won't allow grow to size bigger than the underlying block
667f11
device.
667f11
---
667f11
 tests/formats_test/fs_test.py   | 91 +++++++++++++++++++++++++++++++++
667f11
 tests/formats_test/fstesting.py | 33 ++++++------
667f11
 2 files changed, 107 insertions(+), 17 deletions(-)
667f11
667f11
diff --git a/tests/formats_test/fs_test.py b/tests/formats_test/fs_test.py
667f11
index 15fc0c35..9bc5d20d 100644
667f11
--- a/tests/formats_test/fs_test.py
667f11
+++ b/tests/formats_test/fs_test.py
667f11
@@ -2,8 +2,13 @@ import os
667f11
 import tempfile
667f11
 import unittest
667f11
 
667f11
+import parted
667f11
+
667f11
 import blivet.formats.fs as fs
667f11
 from blivet.size import Size, ROUND_DOWN
667f11
+from blivet.errors import DeviceFormatError
667f11
+from blivet.formats import get_format
667f11
+from blivet.devices import PartitionDevice, DiskDevice
667f11
 
667f11
 from tests import loopbackedtestcase
667f11
 
667f11
@@ -50,6 +55,92 @@ class ReiserFSTestCase(fstesting.FSAsRoot):
667f11
 class XFSTestCase(fstesting.FSAsRoot):
667f11
     _fs_class = fs.XFS
667f11
 
667f11
+    def can_resize(self, an_fs):
667f11
+        resize_tasks = (an_fs._resize, an_fs._size_info)
667f11
+        return not any(t.availability_errors for t in resize_tasks)
667f11
+
667f11
+    def _create_partition(self, disk, size):
667f11
+        disk.format = get_format("disklabel", device=disk.path, label_type="msdos")
667f11
+        disk.format.create()
667f11
+        pstart = disk.format.alignment.grainSize
667f11
+        pend = pstart + int(Size(size) / disk.format.parted_device.sectorSize)
667f11
+        disk.format.add_partition(pstart, pend, parted.PARTITION_NORMAL)
667f11
+        disk.format.parted_disk.commit()
667f11
+        part = disk.format.parted_disk.getPartitionBySector(pstart)
667f11
+
667f11
+        device = PartitionDevice(os.path.basename(part.path))
667f11
+        device.disk = disk
667f11
+        device.exists = True
667f11
+        device.parted_partition = part
667f11
+
667f11
+        return device
667f11
+
667f11
+    def _remove_partition(self, partition, disk):
667f11
+        disk.format.remove_partition(partition.parted_partition)
667f11
+        disk.format.parted_disk.commit()
667f11
+
667f11
+    def test_resize(self):
667f11
+        an_fs = self._fs_class()
667f11
+        if not an_fs.formattable:
667f11
+            self.skipTest("can not create filesystem %s" % an_fs.name)
667f11
+        an_fs.device = self.loop_devices[0]
667f11
+        self.assertIsNone(an_fs.create())
667f11
+        an_fs.update_size_info()
667f11
+
667f11
+        self._test_sizes(an_fs)
667f11
+        # CHECKME: target size is still 0 after updated_size_info is called.
667f11
+        self.assertEqual(an_fs.size, Size(0) if an_fs.resizable else an_fs._size)
667f11
+
667f11
+        if not self.can_resize(an_fs):
667f11
+            self.assertFalse(an_fs.resizable)
667f11
+            # Not resizable, so can not do resizing actions.
667f11
+            with self.assertRaises(DeviceFormatError):
667f11
+                an_fs.target_size = Size("64 MiB")
667f11
+            with self.assertRaises(DeviceFormatError):
667f11
+                an_fs.do_resize()
667f11
+        else:
667f11
+            disk = DiskDevice(os.path.basename(self.loop_devices[0]))
667f11
+            part = self._create_partition(disk, Size("50 MiB"))
667f11
+            an_fs = self._fs_class()
667f11
+            an_fs.device = part.path
667f11
+            self.assertIsNone(an_fs.create())
667f11
+            an_fs.update_size_info()
667f11
+
667f11
+            self.assertTrue(an_fs.resizable)
667f11
+
667f11
+            # grow the partition so we can grow the filesystem
667f11
+            self._remove_partition(part, disk)
667f11
+            part = self._create_partition(disk, size=part.size + Size("40 MiB"))
667f11
+
667f11
+            # Try a reasonable target size
667f11
+            TARGET_SIZE = Size("64 MiB")
667f11
+            an_fs.target_size = TARGET_SIZE
667f11
+            self.assertEqual(an_fs.target_size, TARGET_SIZE)
667f11
+            self.assertNotEqual(an_fs._size, TARGET_SIZE)
667f11
+            self.assertIsNone(an_fs.do_resize())
667f11
+            ACTUAL_SIZE = TARGET_SIZE.round_to_nearest(an_fs._resize.unit, rounding=ROUND_DOWN)
667f11
+            self.assertEqual(an_fs.size, ACTUAL_SIZE)
667f11
+            self.assertEqual(an_fs._size, ACTUAL_SIZE)
667f11
+            self._test_sizes(an_fs)
667f11
+
667f11
+            self._remove_partition(part, disk)
667f11
+
667f11
+        # and no errors should occur when checking
667f11
+        self.assertIsNone(an_fs.do_check())
667f11
+
667f11
+    def test_shrink(self):
667f11
+        self.skipTest("Not checking resize for this test category.")
667f11
+
667f11
+    def test_too_small(self):
667f11
+        self.skipTest("Not checking resize for this test category.")
667f11
+
667f11
+    def test_no_explicit_target_size2(self):
667f11
+        self.skipTest("Not checking resize for this test category.")
667f11
+
667f11
+    def test_too_big2(self):
667f11
+        # XXX this tests assumes that resizing to max size - 1 B will fail, but xfs_grow won't
667f11
+        self.skipTest("Not checking resize for this test category.")
667f11
+
667f11
 
667f11
 class HFSTestCase(fstesting.FSAsRoot):
667f11
     _fs_class = fs.HFS
667f11
diff --git a/tests/formats_test/fstesting.py b/tests/formats_test/fstesting.py
667f11
index 62f806f9..86b2a116 100644
667f11
--- a/tests/formats_test/fstesting.py
667f11
+++ b/tests/formats_test/fstesting.py
667f11
@@ -11,16 +11,6 @@ from blivet.size import Size, ROUND_DOWN
667f11
 from blivet.formats import fs
667f11
 
667f11
 
667f11
-def can_resize(an_fs):
667f11
-    """ Returns True if this filesystem has all necessary resizing tools
667f11
-        available.
667f11
-
667f11
-        :param an_fs: a filesystem object
667f11
-    """
667f11
-    resize_tasks = (an_fs._resize, an_fs._size_info, an_fs._minsize)
667f11
-    return not any(t.availability_errors for t in resize_tasks)
667f11
-
667f11
-
667f11
 @add_metaclass(abc.ABCMeta)
667f11
 class FSAsRoot(loopbackedtestcase.LoopBackedTestCase):
667f11
 
667f11
@@ -32,6 +22,15 @@ class FSAsRoot(loopbackedtestcase.LoopBackedTestCase):
667f11
     def __init__(self, methodName='run_test'):
667f11
         super(FSAsRoot, self).__init__(methodName=methodName, device_spec=[self._DEVICE_SIZE])
667f11
 
667f11
+    def can_resize(self, an_fs):
667f11
+        """ Returns True if this filesystem has all necessary resizing tools
667f11
+            available.
667f11
+
667f11
+            :param an_fs: a filesystem object
667f11
+        """
667f11
+        resize_tasks = (an_fs._resize, an_fs._size_info, an_fs._minsize)
667f11
+        return not any(t.availability_errors for t in resize_tasks)
667f11
+
667f11
     def _test_sizes(self, an_fs):
667f11
         """ Test relationships between different size values.
667f11
 
667f11
@@ -190,7 +189,7 @@ class FSAsRoot(loopbackedtestcase.LoopBackedTestCase):
667f11
         # CHECKME: target size is still 0 after updated_size_info is called.
667f11
         self.assertEqual(an_fs.size, Size(0) if an_fs.resizable else an_fs._size)
667f11
 
667f11
-        if not can_resize(an_fs):
667f11
+        if not self.can_resize(an_fs):
667f11
             self.assertFalse(an_fs.resizable)
667f11
             # Not resizable, so can not do resizing actions.
667f11
             with self.assertRaises(DeviceFormatError):
667f11
@@ -221,7 +220,7 @@ class FSAsRoot(loopbackedtestcase.LoopBackedTestCase):
667f11
         # in constructor call behavior would be different.
667f11
 
667f11
         an_fs = self._fs_class()
667f11
-        if not can_resize(an_fs):
667f11
+        if not self.can_resize(an_fs):
667f11
             self.skipTest("Not checking resize for this test category.")
667f11
         if not an_fs.formattable:
667f11
             self.skipTest("can not create filesystem %s" % an_fs.name)
667f11
@@ -244,7 +243,7 @@ class FSAsRoot(loopbackedtestcase.LoopBackedTestCase):
667f11
         """
667f11
         SIZE = Size("64 MiB")
667f11
         an_fs = self._fs_class(size=SIZE)
667f11
-        if not can_resize(an_fs):
667f11
+        if not self.can_resize(an_fs):
667f11
             self.skipTest("Not checking resize for this test category.")
667f11
         if not an_fs.formattable:
667f11
             self.skipTest("can not create filesystem %s" % an_fs.name)
667f11
@@ -264,7 +263,7 @@ class FSAsRoot(loopbackedtestcase.LoopBackedTestCase):
667f11
 
667f11
     def test_shrink(self):
667f11
         an_fs = self._fs_class()
667f11
-        if not can_resize(an_fs):
667f11
+        if not self.can_resize(an_fs):
667f11
             self.skipTest("Not checking resize for this test category.")
667f11
         if not an_fs.formattable:
667f11
             self.skipTest("can not create filesystem %s" % an_fs.name)
667f11
@@ -296,7 +295,7 @@ class FSAsRoot(loopbackedtestcase.LoopBackedTestCase):
667f11
 
667f11
     def test_too_small(self):
667f11
         an_fs = self._fs_class()
667f11
-        if not can_resize(an_fs):
667f11
+        if not self.can_resize(an_fs):
667f11
             self.skipTest("Not checking resize for this test category.")
667f11
         if not an_fs.formattable:
667f11
             self.skipTest("can not create or resize filesystem %s" % an_fs.name)
667f11
@@ -315,7 +314,7 @@ class FSAsRoot(loopbackedtestcase.LoopBackedTestCase):
667f11
 
667f11
     def test_too_big(self):
667f11
         an_fs = self._fs_class()
667f11
-        if not can_resize(an_fs):
667f11
+        if not self.can_resize(an_fs):
667f11
             self.skipTest("Not checking resize for this test category.")
667f11
         if not an_fs.formattable:
667f11
             self.skipTest("can not create filesystem %s" % an_fs.name)
667f11
@@ -334,7 +333,7 @@ class FSAsRoot(loopbackedtestcase.LoopBackedTestCase):
667f11
 
667f11
     def test_too_big2(self):
667f11
         an_fs = self._fs_class()
667f11
-        if not can_resize(an_fs):
667f11
+        if not self.can_resize(an_fs):
667f11
             self.skipTest("Not checking resize for this test category.")
667f11
         if not an_fs.formattable:
667f11
             self.skipTest("can not create filesystem %s" % an_fs.name)
667f11
-- 
667f11
2.26.2
667f11
667f11
667f11
From 51acc04f4639f143b55789a06a68aae988a91296 Mon Sep 17 00:00:00 2001
667f11
From: Vojtech Trefny <vtrefny@redhat.com>
667f11
Date: Wed, 15 Jul 2020 12:59:04 +0200
667f11
Subject: [PATCH 3/4] Add support for checking and fixing XFS using xfs_repair
667f11
667f11
---
667f11
 blivet/formats/fs.py          |  1 +
667f11
 blivet/tasks/availability.py  |  1 +
667f11
 blivet/tasks/fsck.py          | 12 ++++++++++++
667f11
 tests/formats_test/fs_test.py |  6 +++---
667f11
 4 files changed, 17 insertions(+), 3 deletions(-)
667f11
667f11
diff --git a/blivet/formats/fs.py b/blivet/formats/fs.py
667f11
index 12cb9885..06fbdf10 100644
667f11
--- a/blivet/formats/fs.py
667f11
+++ b/blivet/formats/fs.py
667f11
@@ -1091,6 +1091,7 @@ class XFS(FS):
667f11
     _supported = True
667f11
     _resizable = True
667f11
     _packages = ["xfsprogs"]
667f11
+    _fsck_class = fsck.XFSCK
667f11
     _info_class = fsinfo.XFSInfo
667f11
     _mkfs_class = fsmkfs.XFSMkfs
667f11
     _readlabel_class = fsreadlabel.XFSReadLabel
667f11
diff --git a/blivet/tasks/availability.py b/blivet/tasks/availability.py
667f11
index df62780c..f3b76650 100644
667f11
--- a/blivet/tasks/availability.py
667f11
+++ b/blivet/tasks/availability.py
667f11
@@ -456,5 +456,6 @@ XFSADMIN_APP = application("xfs_admin")
667f11
 XFSDB_APP = application("xfs_db")
667f11
 XFSFREEZE_APP = application("xfs_freeze")
667f11
 XFSRESIZE_APP = application("xfs_growfs")
667f11
+XFSREPAIR_APP = application("xfs_repair")
667f11
 
667f11
 MOUNT_APP = application("mount")
667f11
diff --git a/blivet/tasks/fsck.py b/blivet/tasks/fsck.py
667f11
index 5274f13a..8477f5f8 100644
667f11
--- a/blivet/tasks/fsck.py
667f11
+++ b/blivet/tasks/fsck.py
667f11
@@ -123,6 +123,18 @@ class Ext2FSCK(FSCK):
667f11
         return "\n".join(msgs) or None
667f11
 
667f11
 
667f11
+class XFSCK(FSCK):
667f11
+    _fsck_errors = {1: "Runtime error encountered during repair operation.",
667f11
+                    2: "XFS repair was unable to proceed due to a dirty log."}
667f11
+
667f11
+    ext = availability.XFSREPAIR_APP
667f11
+    options = []
667f11
+
667f11
+    def _error_message(self, rc):
667f11
+        msgs = (self._fsck_errors[c] for c in self._fsck_errors.keys() if rc & c)
667f11
+        return "\n".join(msgs) or None
667f11
+
667f11
+
667f11
 class HFSPlusFSCK(FSCK):
667f11
     _fsck_errors = {3: "Quick check found a dirty filesystem; no repairs done.",
667f11
                     4: "Root filesystem was dirty. System should be rebooted.",
667f11
diff --git a/tests/formats_test/fs_test.py b/tests/formats_test/fs_test.py
667f11
index 9bc5d20d..8fb099fd 100644
667f11
--- a/tests/formats_test/fs_test.py
667f11
+++ b/tests/formats_test/fs_test.py
667f11
@@ -123,10 +123,10 @@ class XFSTestCase(fstesting.FSAsRoot):
667f11
             self.assertEqual(an_fs._size, ACTUAL_SIZE)
667f11
             self._test_sizes(an_fs)
667f11
 
667f11
-            self._remove_partition(part, disk)
667f11
+            # and no errors should occur when checking
667f11
+            self.assertIsNone(an_fs.do_check())
667f11
 
667f11
-        # and no errors should occur when checking
667f11
-        self.assertIsNone(an_fs.do_check())
667f11
+            self._remove_partition(part, disk)
667f11
 
667f11
     def test_shrink(self):
667f11
         self.skipTest("Not checking resize for this test category.")
667f11
-- 
667f11
2.26.2
667f11
667f11
667f11
From 2a6947098e66f880193f3bac2282a6c7857ca5f7 Mon Sep 17 00:00:00 2001
667f11
From: Vojtech Trefny <vtrefny@redhat.com>
667f11
Date: Thu, 16 Jul 2020 09:05:35 +0200
667f11
Subject: [PATCH 4/4] Use xfs_db in read-only mode when getting XFS information
667f11
667f11
This way it will also work on mounted filesystems.
667f11
---
667f11
 blivet/tasks/fsinfo.py | 2 +-
667f11
 1 file changed, 1 insertion(+), 1 deletion(-)
667f11
667f11
diff --git a/blivet/tasks/fsinfo.py b/blivet/tasks/fsinfo.py
667f11
index af208f5d..41ff700f 100644
667f11
--- a/blivet/tasks/fsinfo.py
667f11
+++ b/blivet/tasks/fsinfo.py
667f11
@@ -95,7 +95,7 @@ class ReiserFSInfo(FSInfo):
667f11
 
667f11
 class XFSInfo(FSInfo):
667f11
     ext = availability.XFSDB_APP
667f11
-    options = ["-c", "sb 0", "-c", "p dblocks", "-c", "p blocksize"]
667f11
+    options = ["-c", "sb 0", "-c", "p dblocks", "-c", "p blocksize", "-r"]
667f11
 
667f11
 
667f11
 class UnimplementedFSInfo(fstask.UnimplementedFSTask):
667f11
-- 
667f11
2.26.2
667f11