summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDaniil Baturin <daniil@vyos.io>2020-11-05 05:18:31 +0700
committerGitHub <noreply@github.com>2020-11-05 05:18:31 +0700
commit16783239ad2dd69c28b279b6027eba6c3ad6f05b (patch)
treea76e1408ecdc89c858a13870a18cf2d5961ee91a
parent889ff2dbb7ebcc26e5256b4a9f478e203db7bce0 (diff)
parentd0ec0c1800097250594a17e88cae28b423a8eff2 (diff)
downloadvyos-cloud-init-16783239ad2dd69c28b279b6027eba6c3ad6f05b.tar.gz
vyos-cloud-init-16783239ad2dd69c28b279b6027eba6c3ad6f05b.zip
Merge pull request #25 from zdc/T3039-equuleus
T3039: Enabled growpart and resizefs (modified) modules
-rw-r--r--cloudinit/config/cc_resizefs_vyos.py345
-rw-r--r--config/cloud.cfg.d/10_vyos.cfg12
-rw-r--r--tests/unittests/test_handler/test_handler_resizefs_vyos.py401
3 files changed, 758 insertions, 0 deletions
diff --git a/cloudinit/config/cc_resizefs_vyos.py b/cloudinit/config/cc_resizefs_vyos.py
new file mode 100644
index 00000000..f5555afc
--- /dev/null
+++ b/cloudinit/config/cc_resizefs_vyos.py
@@ -0,0 +1,345 @@
+# Copyright (C) 2011 Canonical Ltd.
+# Copyright (C) 2012 Hewlett-Packard Development Company, L.P.
+#
+# Author: Scott Moser <scott.moser@canonical.com>
+# Author: Juerg Haefliger <juerg.haefliger@hp.com>
+#
+# This file is part of cloud-init. See LICENSE file for license information.
+
+"""Resizefs: cloud-config module which resizes the filesystem"""
+
+import errno
+import getopt
+import os
+import re
+import shlex
+import stat
+from textwrap import dedent
+
+from cloudinit.config.schema import (
+ get_schema_doc, validate_cloudconfig_schema)
+from cloudinit.settings import PER_ALWAYS
+from cloudinit import subp
+from cloudinit import util
+
+NOBLOCK = "noblock"
+RESIZEFS_LIST_DEFAULT = ['/']
+
+frequency = PER_ALWAYS
+distros = ['all']
+
+# Renamed to schema_vyos to pass build tests without modifying upstream sources
+schema_vyos = {
+ 'id': 'cc_resizefs_vyos',
+ 'name': 'Resizefs',
+ 'title': 'Resize filesystem',
+ 'description': dedent("""\
+ Resize filesystems to use all avaliable space on partition. This
+ module is useful along with ``cc_growpart`` and will ensure that if a
+ partition has been resized the filesystem will be resized
+ along with it. By default, ``cc_resizefs`` will resize the root
+ partition and will block the boot process while the resize command is
+ running. Optionally, the resize operation can be performed in the
+ background while cloud-init continues running modules. This can be
+ enabled by setting ``resizefs_enabled`` to ``noblock``. This module can
+ be disabled altogether by setting ``resizefs_enabled`` to ``false``.
+ """),
+ 'distros': distros,
+ 'examples': [
+ 'resizefs_enabled: false # disable filesystems resize operation'
+ 'resize_fs: ["/", "/dev/vda1"]'],
+ 'frequency': PER_ALWAYS,
+ 'type': 'object',
+ 'properties': {
+ 'resizefs_enabled': {
+ 'enum': [True, False, NOBLOCK],
+ 'description': dedent("""\
+ Whether to resize the partitions. Default: 'true'""")
+ },
+ 'resizefs_list': {
+ 'type': 'array',
+ 'items': {'type': 'string'},
+ 'additionalItems': False, # Reject items non-string
+ 'description': dedent("""\
+ List of partitions filesystems on which should be resized.
+ Default: '/'""")
+ }
+ }
+}
+
+# Renamed to schema_vyos to pass build tests without modifying upstream sources
+__doc__ = get_schema_doc(schema_vyos) # Supplement python help()
+
+
+def _resize_btrfs(mount_point, devpth):
+ # If "/" is ro resize will fail. However it should be allowed since resize
+ # makes everything bigger and subvolumes that are not ro will benefit.
+ # Use a subvolume that is not ro to trick the resize operation to do the
+ # "right" thing. The use of ".snapshot" is specific to "snapper" a generic
+ # solution would be walk the subvolumes and find a rw mounted subvolume.
+ if (not util.mount_is_read_write(mount_point) and
+ os.path.isdir("%s/.snapshots" % mount_point)):
+ return ('btrfs', 'filesystem', 'resize', 'max',
+ '%s/.snapshots' % mount_point)
+ else:
+ return ('btrfs', 'filesystem', 'resize', 'max', mount_point)
+
+
+def _resize_ext(mount_point, devpth):
+ return ('resize2fs', devpth)
+
+
+def _resize_xfs(mount_point, devpth):
+ return ('xfs_growfs', mount_point)
+
+
+def _resize_ufs(mount_point, devpth):
+ return ('growfs', '-y', mount_point)
+
+
+def _resize_zfs(mount_point, devpth):
+ return ('zpool', 'online', '-e', mount_point, devpth)
+
+
+def _get_dumpfs_output(mount_point):
+ return subp.subp(['dumpfs', '-m', mount_point])[0]
+
+
+def _get_gpart_output(part):
+ return subp.subp(['gpart', 'show', part])[0]
+
+
+def _can_skip_resize_ufs(mount_point, devpth):
+ # extract the current fs sector size
+ """
+ # dumpfs -m /
+ # newfs command for / (/dev/label/rootfs)
+ newfs -L rootf -O 2 -U -a 4 -b 32768 -d 32768 -e 4096 -f 4096 -g 16384
+ -h 64 -i 8192 -j -k 6408 -m 8 -o time -s 58719232 /dev/label/rootf
+ """
+ cur_fs_sz = None
+ frag_sz = None
+ dumpfs_res = _get_dumpfs_output(mount_point)
+ for line in dumpfs_res.splitlines():
+ if not line.startswith('#'):
+ newfs_cmd = shlex.split(line)
+ opt_value = 'O:Ua:s:b:d:e:f:g:h:i:jk:m:o:L:'
+ optlist, _args = getopt.getopt(newfs_cmd[1:], opt_value)
+ for o, a in optlist:
+ if o == "-s":
+ cur_fs_sz = int(a)
+ if o == "-f":
+ frag_sz = int(a)
+ # check the current partition size
+ # Example output from `gpart show /dev/da0`:
+ # => 40 62914480 da0 GPT (30G)
+ # 40 1024 1 freebsd-boot (512K)
+ # 1064 58719232 2 freebsd-ufs (28G)
+ # 58720296 3145728 3 freebsd-swap (1.5G)
+ # 61866024 1048496 - free - (512M)
+ expect_sz = None
+ m = re.search('^(/dev/.+)p([0-9])$', devpth)
+ gpart_res = _get_gpart_output(m.group(1))
+ for line in gpart_res.splitlines():
+ if re.search(r"freebsd-ufs", line):
+ fields = line.split()
+ expect_sz = int(fields[1])
+ # Normalize the gpart sector size,
+ # because the size is not exactly the same as fs size.
+ normal_expect_sz = (expect_sz - expect_sz % (frag_sz / 512))
+ if normal_expect_sz == cur_fs_sz:
+ return True
+ else:
+ return False
+
+
+# Do not use a dictionary as these commands should be able to be used
+# for multiple filesystem types if possible, e.g. one command for
+# ext2, ext3 and ext4.
+RESIZE_FS_PREFIXES_CMDS = [
+ ('btrfs', _resize_btrfs),
+ ('ext', _resize_ext),
+ ('xfs', _resize_xfs),
+ ('ufs', _resize_ufs),
+ ('zfs', _resize_zfs),
+]
+
+RESIZE_FS_PRECHECK_CMDS = {
+ 'ufs': _can_skip_resize_ufs
+}
+
+
+def can_skip_resize(fs_type, resize_item, devpth):
+ fstype_lc = fs_type.lower()
+ for i, func in RESIZE_FS_PRECHECK_CMDS.items():
+ if fstype_lc.startswith(i):
+ return func(resize_item, devpth)
+ return False
+
+
+def maybe_get_writable_device_path(devpath, info, log):
+ """Return updated devpath if the devpath is a writable block device.
+
+ @param devpath: Requested path to the root device we want to resize.
+ @param info: String representing information about the requested device.
+ @param log: Logger to which logs will be added upon error.
+
+ @returns devpath or updated devpath per kernel commandline if the device
+ path is a writable block device, returns None otherwise.
+ """
+ container = util.is_container()
+
+ # Ensure the path is a block device.
+ if (devpath == "/dev/root" and not os.path.exists(devpath) and
+ not container):
+ devpath = util.rootdev_from_cmdline(util.get_cmdline())
+ if devpath is None:
+ log.warning("Unable to find device '/dev/root'")
+ return None
+ log.debug("Converted /dev/root to '%s' per kernel cmdline", devpath)
+
+ if devpath == 'overlayroot':
+ log.debug("Not attempting to resize devpath '%s': %s", devpath, info)
+ return None
+
+ # FreeBSD zpool can also just use gpt/<label>
+ # with that in mind we can not do an os.stat on "gpt/whatever"
+ # therefore return the devpath already here.
+ if devpath.startswith('gpt/'):
+ log.debug('We have a gpt label - just go ahead')
+ return devpath
+ # Alternatively, our device could simply be a name as returned by gpart,
+ # such as da0p3
+ if not devpath.startswith('/dev/') and not os.path.exists(devpath):
+ fulldevpath = '/dev/' + devpath.lstrip('/')
+ log.debug("'%s' doesn't appear to be a valid device path. Trying '%s'",
+ devpath, fulldevpath)
+ devpath = fulldevpath
+
+ try:
+ statret = os.stat(devpath)
+ except OSError as exc:
+ if container and exc.errno == errno.ENOENT:
+ log.debug("Device '%s' did not exist in container. "
+ "cannot resize: %s", devpath, info)
+ elif exc.errno == errno.ENOENT:
+ log.warning("Device '%s' did not exist. cannot resize: %s",
+ devpath, info)
+ else:
+ raise exc
+ return None
+
+ if not stat.S_ISBLK(statret.st_mode) and not stat.S_ISCHR(statret.st_mode):
+ if container:
+ log.debug("device '%s' not a block device in container."
+ " cannot resize: %s" % (devpath, info))
+ else:
+ log.warning("device '%s' not a block device. cannot resize: %s" %
+ (devpath, info))
+ return None
+ return devpath # The writable block devpath
+
+
+def handle(name, cfg, _cloud, log, args):
+ if len(args) != 0:
+ resize_enabled = args[0]
+ else:
+ resize_enabled = util.get_cfg_option_str(cfg, "resizefs_enabled", True)
+
+ # Warn about the old-style configuration
+ resize_rootfs_option = util.get_cfg_option_str(cfg, "resize_rootfs")
+ if resize_rootfs_option:
+ log.warning("""The resize_rootfs option is deprecated, please use
+ resizefs_enabled instead!""")
+ resize_enabled = resize_rootfs_option
+
+ # Renamed to schema_vyos to pass build tests without modifying upstream
+ validate_cloudconfig_schema(cfg, schema_vyos)
+ if not util.translate_bool(resize_enabled, addons=[NOBLOCK]):
+ log.debug("Skipping module named %s, resizing disabled", name)
+ return
+
+ # Get list of partitions to resize
+ resize_what = util.get_cfg_option_list(cfg, "resizefs_list",
+ RESIZEFS_LIST_DEFAULT)
+ log.debug("Filesystems to resize: %s", resize_what)
+
+ # Resize all filesystems from resize_what
+ for resize_item in resize_what:
+
+ result = util.get_mount_info(resize_item, log)
+ if not result:
+ log.warning("Could not determine filesystem type of %s",
+ resize_item)
+ return
+
+ (devpth, fs_type, mount_point) = result
+
+ # if we have a zfs then our device path at this point
+ # is the zfs label. For example: vmzroot/ROOT/freebsd
+ # we will have to get the zpool name out of this
+ # and set the resize_item variable to the zpool
+ # so the _resize_zfs function gets the right attribute.
+ if fs_type == 'zfs':
+ zpool = devpth.split('/')[0]
+ devpth = util.get_device_info_from_zpool(zpool)
+ if not devpth:
+ return # could not find device from zpool
+ resize_item = zpool
+
+ info = "dev=%s mnt_point=%s path=%s" % (devpth, mount_point,
+ resize_item)
+ log.debug("resize_info: %s" % info)
+
+ devpth = maybe_get_writable_device_path(devpth, info, log)
+ if not devpth:
+ return # devpath was not a writable block device
+
+ resizer = None
+ if can_skip_resize(fs_type, resize_item, devpth):
+ log.debug("Skip resize filesystem type %s for %s",
+ fs_type, resize_item)
+ return
+
+ fstype_lc = fs_type.lower()
+ for (pfix, root_cmd) in RESIZE_FS_PREFIXES_CMDS:
+ if fstype_lc.startswith(pfix):
+ resizer = root_cmd
+ break
+
+ if not resizer:
+ log.warning("Not resizing unknown filesystem type %s for %s",
+ fs_type, resize_item)
+ return
+
+ resize_cmd = resizer(resize_item, devpth)
+ log.debug("Resizing %s (%s) using %s", resize_item, fs_type,
+ ' '.join(resize_cmd))
+
+ if resize_enabled == NOBLOCK:
+ # Fork to a child that will run
+ # the resize command
+ util.fork_cb(
+ util.log_time, logfunc=log.debug, msg="backgrounded Resizing",
+ func=do_resize, args=(resize_cmd, log))
+ else:
+ util.log_time(logfunc=log.debug, msg="Resizing",
+ func=do_resize, args=(resize_cmd, log))
+
+ action = 'Resized'
+ if resize_enabled == NOBLOCK:
+ action = 'Resizing (via forking)'
+ log.debug("%s filesystem on %s (type=%s, val=%s)", action, resize_item,
+ fs_type, resize_enabled)
+
+
+def do_resize(resize_cmd, log):
+ try:
+ subp.subp(resize_cmd)
+ except subp.ProcessExecutionError:
+ util.logexc(log, "Failed to resize filesystem (cmd=%s)", resize_cmd)
+ raise
+ # TODO(harlowja): Should we add a fsck check after this to make
+ # sure we didn't corrupt anything?
+
+# vi: ts=4 expandtab
diff --git a/config/cloud.cfg.d/10_vyos.cfg b/config/cloud.cfg.d/10_vyos.cfg
index 6af79e52..583c263c 100644
--- a/config/cloud.cfg.d/10_vyos.cfg
+++ b/config/cloud.cfg.d/10_vyos.cfg
@@ -19,6 +19,8 @@ disable_vmware_customization: true
# The modules that run in the 'init' stage
cloud_init_modules:
+ - growpart
+ - resizefs_vyos
# The modules that run in the 'config' stage
cloud_config_modules:
@@ -43,3 +45,13 @@ system_info:
templates_dir: /etc/cloud/templates/
upstart_dir: /etc/init/
+
+# Set partitions info for the growpart module
+growpart:
+ mode: auto
+ devices: ["/usr/lib/live/mount/persistence/"]
+ ignore_growroot_disabled: false
+
+# Set partitions info for the resizefs module
+resizefs_list: ["/usr/lib/live/mount/persistence/"]
+
diff --git a/tests/unittests/test_handler/test_handler_resizefs_vyos.py b/tests/unittests/test_handler/test_handler_resizefs_vyos.py
new file mode 100644
index 00000000..d1fafff7
--- /dev/null
+++ b/tests/unittests/test_handler/test_handler_resizefs_vyos.py
@@ -0,0 +1,401 @@
+# This file is part of cloud-init. See LICENSE file for license information.
+
+from cloudinit.config.cc_resizefs_vyos import (
+ can_skip_resize, handle, maybe_get_writable_device_path, _resize_btrfs,
+ _resize_zfs, _resize_xfs, _resize_ext, _resize_ufs)
+
+from collections import namedtuple
+import logging
+import textwrap
+
+from cloudinit.tests.helpers import (
+ CiTestCase, mock, skipUnlessJsonSchema, util, wrap_and_call)
+
+
+LOG = logging.getLogger(__name__)
+
+
+class TestResizefs(CiTestCase):
+ with_logs = True
+
+ def setUp(self):
+ super(TestResizefs, self).setUp()
+ self.name = "resizefs"
+
+ @mock.patch('cloudinit.config.cc_resizefs_vyos._get_dumpfs_output')
+ @mock.patch('cloudinit.config.cc_resizefs_vyos._get_gpart_output')
+ def test_skip_ufs_resize(self, gpart_out, dumpfs_out):
+ fs_type = "ufs"
+ resize_what = "/"
+ devpth = "/dev/da0p2"
+ dumpfs_out.return_value = (
+ "# newfs command for / (/dev/label/rootfs)\n"
+ "newfs -O 2 -U -a 4 -b 32768 -d 32768 -e 4096 "
+ "-f 4096 -g 16384 -h 64 -i 8192 -j -k 6408 -m 8 "
+ "-o time -s 58719232 /dev/label/rootfs\n")
+ gpart_out.return_value = textwrap.dedent("""\
+ => 40 62914480 da0 GPT (30G)
+ 40 1024 1 freebsd-boot (512K)
+ 1064 58719232 2 freebsd-ufs (28G)
+ 58720296 3145728 3 freebsd-swap (1.5G)
+ 61866024 1048496 - free - (512M)
+ """)
+ res = can_skip_resize(fs_type, resize_what, devpth)
+ self.assertTrue(res)
+
+ @mock.patch('cloudinit.config.cc_resizefs_vyos._get_dumpfs_output')
+ @mock.patch('cloudinit.config.cc_resizefs_vyos._get_gpart_output')
+ def test_skip_ufs_resize_roundup(self, gpart_out, dumpfs_out):
+ fs_type = "ufs"
+ resize_what = "/"
+ devpth = "/dev/da0p2"
+ dumpfs_out.return_value = (
+ "# newfs command for / (/dev/label/rootfs)\n"
+ "newfs -O 2 -U -a 4 -b 32768 -d 32768 -e 4096 "
+ "-f 4096 -g 16384 -h 64 -i 8192 -j -k 368 -m 8 "
+ "-o time -s 297080 /dev/label/rootfs\n")
+ gpart_out.return_value = textwrap.dedent("""\
+ => 34 297086 da0 GPT (145M)
+ 34 297086 1 freebsd-ufs (145M)
+ """)
+ res = can_skip_resize(fs_type, resize_what, devpth)
+ self.assertTrue(res)
+
+ def test_can_skip_resize_ext(self):
+ self.assertFalse(can_skip_resize('ext', '/', '/dev/sda1'))
+
+ def test_handle_noops_on_disabled(self):
+ """The handle function logs when the configuration disables resize."""
+ cfg = {'resizefs_enabled': False}
+ handle('cc_resizefs_vyos', cfg, _cloud=None, log=LOG, args=[])
+ self.assertIn(
+ 'DEBUG: Skipping module named cc_resizefs_vyos, resizing disabled\n',
+ self.logs.getvalue())
+
+ @skipUnlessJsonSchema()
+ def test_handle_schema_validation_logs_invalid_resize_enabled_value(self):
+ """The handle reports json schema violations as a warning.
+
+ Invalid values for resizefs_enabled result in disabling the module.
+ """
+ cfg = {'resizefs_enabled': 'junk'}
+ handle('cc_resizefs_vyos', cfg, _cloud=None, log=LOG, args=[])
+ logs = self.logs.getvalue()
+ self.assertIn(
+ "WARNING: Invalid config:\nresizefs_enabled: 'junk' is not one of"
+ " [True, False, 'noblock']",
+ logs)
+ self.assertIn(
+ 'DEBUG: Skipping module named cc_resizefs_vyos, resizing disabled\n',
+ logs)
+
+ @mock.patch('cloudinit.config.cc_resizefs_vyos.util.get_mount_info')
+ def test_handle_warns_on_unknown_mount_info(self, m_get_mount_info):
+ """handle warns when get_mount_info sees unknown filesystem for /."""
+ m_get_mount_info.return_value = None
+ cfg = {'resizefs_enabled': True}
+ handle('cc_resizefs_vyos', cfg, _cloud=None, log=LOG, args=[])
+ logs = self.logs.getvalue()
+ self.assertNotIn("WARNING: Invalid config:\nresizefs_enabled:", logs)
+ self.assertIn(
+ 'WARNING: Could not determine filesystem type of /\n',
+ logs)
+ self.assertEqual(
+ [mock.call('/', LOG)],
+ m_get_mount_info.call_args_list)
+
+ def test_handle_warns_on_undiscoverable_root_path_in_commandline(self):
+ """handle noops when the root path is not found on the commandline."""
+ cfg = {'resizefs_enabled': True}
+ exists_mock_path = 'cloudinit.config.cc_resizefs_vyos.os.path.exists'
+
+ def fake_mount_info(path, log):
+ self.assertEqual('/', path)
+ self.assertEqual(LOG, log)
+ return ('/dev/root', 'ext4', '/')
+
+ with mock.patch(exists_mock_path) as m_exists:
+ m_exists.return_value = False
+ wrap_and_call(
+ 'cloudinit.config.cc_resizefs_vyos.util',
+ {'is_container': {'return_value': False},
+ 'get_mount_info': {'side_effect': fake_mount_info},
+ 'get_cmdline': {'return_value': 'BOOT_IMAGE=/vmlinuz.efi'}},
+ handle, 'cc_resizefs_vyos', cfg, _cloud=None, log=LOG,
+ args=[])
+ logs = self.logs.getvalue()
+ self.assertIn("WARNING: Unable to find device '/dev/root'", logs)
+
+ def test_resize_zfs_cmd_return(self):
+ zpool = 'zroot'
+ devpth = 'gpt/system'
+ self.assertEqual(('zpool', 'online', '-e', zpool, devpth),
+ _resize_zfs(zpool, devpth))
+
+ def test_resize_xfs_cmd_return(self):
+ mount_point = '/mnt/test'
+ devpth = '/dev/sda1'
+ self.assertEqual(('xfs_growfs', mount_point),
+ _resize_xfs(mount_point, devpth))
+
+ def test_resize_ext_cmd_return(self):
+ mount_point = '/'
+ devpth = '/dev/sdb1'
+ self.assertEqual(('resize2fs', devpth),
+ _resize_ext(mount_point, devpth))
+
+ def test_resize_ufs_cmd_return(self):
+ mount_point = '/'
+ devpth = '/dev/sda2'
+ self.assertEqual(('growfs', '-y', mount_point),
+ _resize_ufs(mount_point, devpth))
+
+ @mock.patch('cloudinit.util.is_container', return_value=False)
+ @mock.patch('cloudinit.util.parse_mount')
+ @mock.patch('cloudinit.util.get_device_info_from_zpool')
+ @mock.patch('cloudinit.util.get_mount_info')
+ def test_handle_zfs_root(self, mount_info, zpool_info, parse_mount,
+ is_container):
+ devpth = 'vmzroot/ROOT/freebsd'
+ disk = 'gpt/system'
+ fs_type = 'zfs'
+ mount_point = '/'
+
+ mount_info.return_value = (devpth, fs_type, mount_point)
+ zpool_info.return_value = disk
+ parse_mount.return_value = (devpth, fs_type, mount_point)
+
+ cfg = {'resizefs_enabled': True}
+
+ with mock.patch('cloudinit.config.cc_resizefs_vyos.do_resize') as dresize:
+ handle('cc_resizefs_vyos', cfg, _cloud=None, log=LOG, args=[])
+ ret = dresize.call_args[0][0]
+
+ self.assertEqual(('zpool', 'online', '-e', 'vmzroot', disk), ret)
+
+ @mock.patch('cloudinit.util.is_container', return_value=False)
+ @mock.patch('cloudinit.util.get_mount_info')
+ @mock.patch('cloudinit.util.get_device_info_from_zpool')
+ @mock.patch('cloudinit.util.parse_mount')
+ def test_handle_modern_zfsroot(self, mount_info, zpool_info, parse_mount,
+ is_container):
+ devpth = 'zroot/ROOT/default'
+ disk = 'da0p3'
+ fs_type = 'zfs'
+ mount_point = '/'
+
+ mount_info.return_value = (devpth, fs_type, mount_point)
+ zpool_info.return_value = disk
+ parse_mount.return_value = (devpth, fs_type, mount_point)
+
+ cfg = {'resizefs_enabled': True}
+
+ def fake_stat(devpath):
+ if devpath == disk:
+ raise OSError("not here")
+ FakeStat = namedtuple(
+ 'FakeStat', ['st_mode', 'st_size', 'st_mtime']) # minimal stat
+ return FakeStat(25008, 0, 1) # fake char block device
+
+ with mock.patch('cloudinit.config.cc_resizefs_vyos.do_resize') as dresize:
+ with mock.patch('cloudinit.config.cc_resizefs_vyos.os.stat') as m_stat:
+ m_stat.side_effect = fake_stat
+ handle('cc_resizefs_vyos', cfg, _cloud=None, log=LOG, args=[])
+
+ self.assertEqual(('zpool', 'online', '-e', 'zroot', '/dev/' + disk),
+ dresize.call_args[0][0])
+
+
+class TestRootDevFromCmdline(CiTestCase):
+
+ def test_rootdev_from_cmdline_with_no_root(self):
+ """Return None from rootdev_from_cmdline when root is not present."""
+ invalid_cases = [
+ 'BOOT_IMAGE=/adsf asdfa werasef root adf', 'BOOT_IMAGE=/adsf', '']
+ for case in invalid_cases:
+ self.assertIsNone(util.rootdev_from_cmdline(case))
+
+ def test_rootdev_from_cmdline_with_root_startswith_dev(self):
+ """Return the cmdline root when the path starts with /dev."""
+ self.assertEqual(
+ '/dev/this', util.rootdev_from_cmdline('asdf root=/dev/this'))
+
+ def test_rootdev_from_cmdline_with_root_without_dev_prefix(self):
+ """Add /dev prefix to cmdline root when the path lacks the prefix."""
+ self.assertEqual(
+ '/dev/this', util.rootdev_from_cmdline('asdf root=this'))
+
+ def test_rootdev_from_cmdline_with_root_with_label(self):
+ """When cmdline root contains a LABEL, our root is disk/by-label."""
+ self.assertEqual(
+ '/dev/disk/by-label/unique',
+ util.rootdev_from_cmdline('asdf root=LABEL=unique'))
+
+ def test_rootdev_from_cmdline_with_root_with_uuid(self):
+ """When cmdline root contains a UUID, our root is disk/by-uuid."""
+ self.assertEqual(
+ '/dev/disk/by-uuid/adsfdsaf-adsf',
+ util.rootdev_from_cmdline('asdf root=UUID=adsfdsaf-adsf'))
+
+
+class TestMaybeGetDevicePathAsWritableBlock(CiTestCase):
+
+ with_logs = True
+
+ def test_maybe_get_writable_device_path_none_on_overlayroot(self):
+ """When devpath is overlayroot (on MAAS), is_dev_writable is False."""
+ info = 'does not matter'
+ devpath = wrap_and_call(
+ 'cloudinit.config.cc_resizefs_vyos.util',
+ {'is_container': {'return_value': False}},
+ maybe_get_writable_device_path, 'overlayroot', info, LOG)
+ self.assertIsNone(devpath)
+ self.assertIn(
+ "Not attempting to resize devpath 'overlayroot'",
+ self.logs.getvalue())
+
+ def test_maybe_get_writable_device_path_warns_missing_cmdline_root(self):
+ """When root does not exist isn't in the cmdline, log warning."""
+ info = 'does not matter'
+
+ def fake_mount_info(path, log):
+ self.assertEqual('/', path)
+ self.assertEqual(LOG, log)
+ return ('/dev/root', 'ext4', '/')
+
+ exists_mock_path = 'cloudinit.config.cc_resizefs_vyos.os.path.exists'
+ with mock.patch(exists_mock_path) as m_exists:
+ m_exists.return_value = False
+ devpath = wrap_and_call(
+ 'cloudinit.config.cc_resizefs_vyos.util',
+ {'is_container': {'return_value': False},
+ 'get_mount_info': {'side_effect': fake_mount_info},
+ 'get_cmdline': {'return_value': 'BOOT_IMAGE=/vmlinuz.efi'}},
+ maybe_get_writable_device_path, '/dev/root', info, LOG)
+ self.assertIsNone(devpath)
+ logs = self.logs.getvalue()
+ self.assertIn("WARNING: Unable to find device '/dev/root'", logs)
+
+ def test_maybe_get_writable_device_path_does_not_exist(self):
+ """When devpath does not exist, a warning is logged."""
+ info = 'dev=/dev/I/dont/exist mnt_point=/ path=/dev/none'
+ devpath = wrap_and_call(
+ 'cloudinit.config.cc_resizefs_vyos.util',
+ {'is_container': {'return_value': False}},
+ maybe_get_writable_device_path, '/dev/I/dont/exist', info, LOG)
+ self.assertIsNone(devpath)
+ self.assertIn(
+ "WARNING: Device '/dev/I/dont/exist' did not exist."
+ ' cannot resize: %s' % info,
+ self.logs.getvalue())
+
+ def test_maybe_get_writable_device_path_does_not_exist_in_container(self):
+ """When devpath does not exist in a container, log a debug message."""
+ info = 'dev=/dev/I/dont/exist mnt_point=/ path=/dev/none'
+ devpath = wrap_and_call(
+ 'cloudinit.config.cc_resizefs_vyos.util',
+ {'is_container': {'return_value': True}},
+ maybe_get_writable_device_path, '/dev/I/dont/exist', info, LOG)
+ self.assertIsNone(devpath)
+ self.assertIn(
+ "DEBUG: Device '/dev/I/dont/exist' did not exist in container."
+ ' cannot resize: %s' % info,
+ self.logs.getvalue())
+
+ def test_maybe_get_writable_device_path_raises_oserror(self):
+ """When unexpected OSError is raises by os.stat it is reraised."""
+ info = 'dev=/dev/I/dont/exist mnt_point=/ path=/dev/none'
+ with self.assertRaises(OSError) as context_manager:
+ wrap_and_call(
+ 'cloudinit.config.cc_resizefs_vyos',
+ {'util.is_container': {'return_value': True},
+ 'os.stat': {'side_effect': OSError('Something unexpected')}},
+ maybe_get_writable_device_path, '/dev/I/dont/exist', info, LOG)
+ self.assertEqual(
+ 'Something unexpected', str(context_manager.exception))
+
+ def test_maybe_get_writable_device_path_non_block(self):
+ """When device is not a block device, emit warning return False."""
+ fake_devpath = self.tmp_path('dev/readwrite')
+ util.write_file(fake_devpath, '', mode=0o600) # read-write
+ info = 'dev=/dev/root mnt_point=/ path={0}'.format(fake_devpath)
+
+ devpath = wrap_and_call(
+ 'cloudinit.config.cc_resizefs_vyos.util',
+ {'is_container': {'return_value': False}},
+ maybe_get_writable_device_path, fake_devpath, info, LOG)
+ self.assertIsNone(devpath)
+ self.assertIn(
+ "WARNING: device '{0}' not a block device. cannot resize".format(
+ fake_devpath),
+ self.logs.getvalue())
+
+ def test_maybe_get_writable_device_path_non_block_on_container(self):
+ """When device is non-block device in container, emit debug log."""
+ fake_devpath = self.tmp_path('dev/readwrite')
+ util.write_file(fake_devpath, '', mode=0o600) # read-write
+ info = 'dev=/dev/root mnt_point=/ path={0}'.format(fake_devpath)
+
+ devpath = wrap_and_call(
+ 'cloudinit.config.cc_resizefs_vyos.util',
+ {'is_container': {'return_value': True}},
+ maybe_get_writable_device_path, fake_devpath, info, LOG)
+ self.assertIsNone(devpath)
+ self.assertIn(
+ "DEBUG: device '{0}' not a block device in container."
+ ' cannot resize'.format(fake_devpath),
+ self.logs.getvalue())
+
+ def test_maybe_get_writable_device_path_returns_cmdline_root(self):
+ """When root device is UUID in kernel commandline, update devpath."""
+ # XXX Long-term we want to use FilesystemMocking test to avoid
+ # touching os.stat.
+ FakeStat = namedtuple(
+ 'FakeStat', ['st_mode', 'st_size', 'st_mtime']) # minimal def.
+ info = 'dev=/dev/root mnt_point=/ path=/does/not/matter'
+ devpath = wrap_and_call(
+ 'cloudinit.config.cc_resizefs_vyos',
+ {'util.get_cmdline': {'return_value': 'asdf root=UUID=my-uuid'},
+ 'util.is_container': False,
+ 'os.path.exists': False, # /dev/root doesn't exist
+ 'os.stat': {
+ 'return_value': FakeStat(25008, 0, 1)} # char block device
+ },
+ maybe_get_writable_device_path, '/dev/root', info, LOG)
+ self.assertEqual('/dev/disk/by-uuid/my-uuid', devpath)
+ self.assertIn(
+ "DEBUG: Converted /dev/root to '/dev/disk/by-uuid/my-uuid'"
+ " per kernel cmdline",
+ self.logs.getvalue())
+
+ @mock.patch('cloudinit.util.mount_is_read_write')
+ @mock.patch('cloudinit.config.cc_resizefs_vyos.os.path.isdir')
+ def test_resize_btrfs_mount_is_ro(self, m_is_dir, m_is_rw):
+ """Do not resize / directly if it is read-only. (LP: #1734787)."""
+ m_is_rw.return_value = False
+ m_is_dir.return_value = True
+ self.assertEqual(
+ ('btrfs', 'filesystem', 'resize', 'max', '//.snapshots'),
+ _resize_btrfs("/", "/dev/sda1"))
+
+ @mock.patch('cloudinit.util.mount_is_read_write')
+ @mock.patch('cloudinit.config.cc_resizefs_vyos.os.path.isdir')
+ def test_resize_btrfs_mount_is_rw(self, m_is_dir, m_is_rw):
+ """Do not resize / directly if it is read-only. (LP: #1734787)."""
+ m_is_rw.return_value = True
+ m_is_dir.return_value = True
+ self.assertEqual(
+ ('btrfs', 'filesystem', 'resize', 'max', '/'),
+ _resize_btrfs("/", "/dev/sda1"))
+
+ @mock.patch('cloudinit.util.is_container', return_value=True)
+ @mock.patch('cloudinit.util.is_FreeBSD')
+ def test_maybe_get_writable_device_path_zfs_freebsd(self, freebsd,
+ m_is_container):
+ freebsd.return_value = True
+ info = 'dev=gpt/system mnt_point=/ path=/'
+ devpth = maybe_get_writable_device_path('gpt/system', info, LOG)
+ self.assertEqual('gpt/system', devpth)
+
+
+# vi: ts=4 expandtab