# Copyright (C) 2011 Canonical Ltd. # Copyright (C) 2012 Hewlett-Packard Development Company, L.P. # # Author: Scott Moser # Author: Juerg Haefliger # # 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" frequency = PER_ALWAYS distros = ['all'] schema = { 'id': 'cc_resizefs', 'name': 'Resizefs', 'title': 'Resize filesystem', 'description': dedent("""\ Resize a filesystem to use all avaliable space on partition. This module is useful along with ``cc_growpart`` and will ensure that if the root partition has been resized the root 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 ``resize_rootfs`` to ``true``. This module can be disabled altogether by setting ``resize_rootfs`` to ``false``."""), 'distros': distros, 'examples': [ 'resize_rootfs: false # disable root filesystem resize operation'], 'frequency': PER_ALWAYS, 'type': 'object', 'properties': { 'resize_rootfs': { 'enum': [True, False, NOBLOCK], 'description': dedent("""\ Whether to resize the root partition. Default: 'true'""") } } } __doc__ = get_schema_doc(schema) # 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_what, devpth): fstype_lc = fs_type.lower() for i, func in RESIZE_FS_PRECHECK_CMDS.items(): if fstype_lc.startswith(i): return func(resize_what, 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/