From 21006925f17815fc79e9a15f80088b1585e6f1a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mina=20Gali=C4=87?= Date: Tue, 16 May 2023 19:37:07 +0100 Subject: FreeBSD: add ResizeGrowFS class to cc_growpart (#2334) this FreeBSD specific resizer resizes the root partition and grows the Filesystem all in one. All we have to do is call ``service growfs onestart`` Document behaviour: especially that growfs will insert a swap partition if none is present, unless instructed otherwise. Sponsored by: The FreeBSD Foundation --- cloudinit/config/cc_growpart.py | 60 +++++++++++++++++++++++++----- cloudinit/distros/freebsd.py | 1 + tests/unittests/config/test_cc_growpart.py | 35 ++++++++++++++--- 3 files changed, 81 insertions(+), 15 deletions(-) diff --git a/cloudinit/config/cc_growpart.py b/cloudinit/config/cc_growpart.py index 068e678f..903664e3 100644 --- a/cloudinit/config/cc_growpart.py +++ b/cloudinit/config/cc_growpart.py @@ -32,7 +32,9 @@ MODULE_DESCRIPTION = """\ Growpart resizes partitions to fill the available disk space. This is useful for cloud instances with a larger amount of disk space available than the pristine image uses, as it allows the instance to automatically make -use of the extra space. +use of the extra space. Note that this only works if the partition to be +resized is the last one on a disk with classic partitioning scheme (MBR, BSD, +GPT). LVM, Btrfs and ZFS have no such restrictions. The devices on which to run growpart are specified as a list under the ``devices`` key. @@ -48,6 +50,14 @@ it is present. However, this file can be ignored for ``cc_growpart`` by setting ``ignore_growroot_disabled`` to ``true``. For more information on ``cloud-initramfs-tools`` see: https://launchpad.net/cloud-initramfs-tools +On FreeBSD, there is also the ``growfs`` service, which has a lot of overlap +with ``cc_growpart`` and ``cc_resizefs``, but only works on the root partition. +In that configuration, we use it, otherwise, we fall back to ``gpart``. + +Note however, that ``growfs`` may insert a swap partition, if none is present, +unless instructed not to via ``growfs_swap_size=0`` in either ``kenv(1)``, or +``rc.conf(5)``. + Growpart is enabled by default on the root partition. The default config for growpart is:: @@ -108,12 +118,12 @@ class RESIZE: LOG = logging.getLogger(__name__) -def resizer_factory(mode: str, distro: Distro): +def resizer_factory(mode: str, distro: Distro, devices: list): resize_class = None if mode == "auto": for _name, resizer in RESIZERS: cur = resizer(distro) - if cur.available(): + if cur.available(devices=devices): resize_class = cur break @@ -129,7 +139,7 @@ def resizer_factory(mode: str, distro: Distro): raise TypeError("unknown resize mode %s" % mode) mclass = mmap[mode](distro) - if mclass.available(): + if mclass.available(devices=devices): resize_class = mclass if not resize_class: @@ -147,7 +157,7 @@ class Resizer(ABC): self._distro = distro @abstractmethod - def available(self) -> bool: + def available(self, devices: list) -> bool: ... @abstractmethod @@ -156,7 +166,7 @@ class Resizer(ABC): class ResizeGrowPart(Resizer): - def available(self): + def available(self, devices: list): myenv = os.environ.copy() myenv["LANG"] = "C" @@ -206,8 +216,36 @@ class ResizeGrowPart(Resizer): return (before, get_size(partdev)) +class ResizeGrowFS(Resizer): + """ + Use FreeBSD ``growfs`` service to grow root partition to fill available + space, optionally adding a swap partition at the end. + + Note that the service file warns us that it uses ``awk(1)``, and as + such requires ``/usr`` to be present. However, cloud-init is installed + into ``/usr/local``, so we should be fine. + + We invoke the ``growfs`` with ``service growfs onestart``, so it + doesn't need to be enabled in ``rc.conf``. + """ + + def available(self, devices: list): + """growfs only works on the root partition""" + return os.path.isfile("/etc/rc.d/growfs") and devices == ["/"] + + def resize(self, diskdev, partnum, partdev): + before = get_size(partdev) + try: + self._distro.manage_service(action="onestart", service="growfs") + except subp.ProcessExecutionError as e: + util.logexc(LOG, "Failed: service growfs onestart") + raise ResizeFailedException(e) from e + + return (before, get_size(partdev)) + + class ResizeGpart(Resizer): - def available(self): + def available(self, devices: list): myenv = os.environ.copy() myenv["LANG"] = "C" @@ -604,7 +642,7 @@ def handle(name: str, cfg: Config, cloud: Cloud, args: list) -> None: return try: - resizer = resizer_factory(mode, cloud.distro) + resizer = resizer_factory(mode, distro=cloud.distro, devices=devices) except (ValueError, TypeError) as e: LOG.debug("growpart unable to find resizer for '%s': %s", mode, e) if mode != "auto": @@ -624,4 +662,8 @@ def handle(name: str, cfg: Config, cloud: Cloud, args: list) -> None: LOG.debug("'%s' %s: %s", entry, action, msg) -RESIZERS = (("growpart", ResizeGrowPart), ("gpart", ResizeGpart)) +RESIZERS = ( + ("growpart", ResizeGrowPart), + ("growfs", ResizeGrowFS), + ("gpart", ResizeGpart), +) diff --git a/cloudinit/distros/freebsd.py b/cloudinit/distros/freebsd.py index 77a94c61..443d0961 100644 --- a/cloudinit/distros/freebsd.py +++ b/cloudinit/distros/freebsd.py @@ -53,6 +53,7 @@ class Distro(cloudinit.distros.bsd.BSD): "start": [service, "start"], "enable": [service, "enable"], "disable": [service, "disable"], + "onestart": [service, "onestart"], "restart": [service, "restart"], "reload": [service, "restart"], "try-reload": [service, "restart"], diff --git a/tests/unittests/config/test_cc_growpart.py b/tests/unittests/config/test_cc_growpart.py index f9bee391..3a79da62 100644 --- a/tests/unittests/config/test_cc_growpart.py +++ b/tests/unittests/config/test_cc_growpart.py @@ -141,7 +141,6 @@ class TestConfig(TestCase): with mock.patch.object( subp, "subp", return_value=(HELP_GROWPART_NO_RESIZE, "") ) as mockobj: - config = {"growpart": {"mode": "auto"}} self.handle(self.name, config, self.cloud, self.args) @@ -178,7 +177,9 @@ class TestConfig(TestCase): with mock.patch.object( subp, "subp", return_value=(HELP_GROWPART_RESIZE, "") ) as mockobj: - ret = cc_growpart.resizer_factory(mode="auto", distro=mock.Mock()) + ret = cc_growpart.resizer_factory( + mode="auto", distro=mock.Mock(), devices=["/"] + ) self.assertIsInstance(ret, cc_growpart.ResizeGrowPart) mockobj.assert_called_once_with( @@ -203,8 +204,9 @@ class TestConfig(TestCase): with mock.patch.object( subp, "subp", return_value=(HELP_GROWPART_RESIZE, "") ) as mockobj: - - ret = cc_growpart.resizer_factory(mode="auto", distro=mock.Mock()) + ret = cc_growpart.resizer_factory( + mode="auto", distro=mock.Mock(), devices=["/"] + ) self.assertIsInstance(ret, cc_growpart.ResizeGrowPart) diskdev = "/dev/sdb" partnum = 1 @@ -223,12 +225,31 @@ class TestConfig(TestCase): ] ) + @mock.patch.dict("os.environ", clear=True) + @mock.patch.object(os.path, "isfile", return_value=True) + def test_mode_use_growfs_on_root(self, m_isfile): + with mock.patch.object( + subp, "subp", return_value=("File not found", "") + ) as mockobj: + ret = cc_growpart.resizer_factory( + mode="auto", distro=mock.Mock(), devices=["/"] + ) + self.assertIsInstance(ret, cc_growpart.ResizeGrowFS) + + mockobj.assert_has_calls( + [ + mock.call(["growpart", "--help"], env={"LANG": "C"}), + ] + ) + @mock.patch.dict("os.environ", {"LANG": "cs_CZ.UTF-8"}, clear=True) def test_mode_auto_falls_back_to_gpart(self): with mock.patch.object( subp, "subp", return_value=("", HELP_GPART) ) as mockobj: - ret = cc_growpart.resizer_factory(mode="auto", distro=mock.Mock()) + ret = cc_growpart.resizer_factory( + mode="auto", distro=mock.Mock(), devices=["/"] + ) self.assertIsInstance(ret, cc_growpart.ResizeGpart) mockobj.assert_has_calls( @@ -271,7 +292,9 @@ class TestConfig(TestCase): self.handle(self.name, {}, self.cloud, self.args) - factory.assert_called_once_with("auto", self.distro) + factory.assert_called_once_with( + "auto", distro=self.distro, devices=["/"] + ) rsdevs.assert_called_once_with(myresizer, ["/"]) -- cgit v1.2.1