summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMina Galić <freebsd@igalic.co>2023-05-16 19:37:07 +0100
committerGitHub <noreply@github.com>2023-05-16 13:37:07 -0500
commit21006925f17815fc79e9a15f80088b1585e6f1a6 (patch)
tree8274fc5220345b73a4470291735b17cfe25f46cd
parentffdb7a7d5fd78b9f64d2f4d9686236b56fa9503d (diff)
downloadcloud-init-git-21006925f17815fc79e9a15f80088b1585e6f1a6.tar.gz
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
-rw-r--r--cloudinit/config/cc_growpart.py60
-rw-r--r--cloudinit/distros/freebsd.py1
-rw-r--r--tests/unittests/config/test_cc_growpart.py35
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, ["/"])