diff options
author | Daniil Baturin <daniil@vyos.io> | 2020-11-05 05:18:31 +0700 |
---|---|---|
committer | GitHub <noreply@github.com> | 2020-11-05 05:18:31 +0700 |
commit | 16783239ad2dd69c28b279b6027eba6c3ad6f05b (patch) | |
tree | a76e1408ecdc89c858a13870a18cf2d5961ee91a | |
parent | 889ff2dbb7ebcc26e5256b4a9f478e203db7bce0 (diff) | |
parent | d0ec0c1800097250594a17e88cae28b423a8eff2 (diff) | |
download | vyos-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.py | 345 | ||||
-rw-r--r-- | config/cloud.cfg.d/10_vyos.cfg | 12 | ||||
-rw-r--r-- | tests/unittests/test_handler/test_handler_resizefs_vyos.py | 401 |
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 |