diff --git a/cloudinit/config/cc_growpart.py b/cloudinit/config/cc_growpart.py index 1ddc9dc7080..3ed18ca4e88 100644 --- a/cloudinit/config/cc_growpart.py +++ b/cloudinit/config/cc_growpart.py @@ -63,11 +63,14 @@ - "/dev/vdb1" ignore_growroot_disabled: """ - +import base64 +import copy +import json import os import os.path import re import stat +from contextlib import suppress from cloudinit import log as logging from cloudinit.settings import PER_ALWAYS @@ -83,6 +86,8 @@ 'ignore_growroot_disabled': False, } +KEYDATA_PATH = "/cc_growpart_keydata" + class RESIZE(object): SKIPPED = "SKIPPED" @@ -286,10 +291,83 @@ def devent2dev(devent): return dev +def get_mapped_device(blockdev): + """Returns underlying block device for a mapped device. + + If blockdev is a symlink pointing to a /dev/dm-* device, return + the device pointed to. Otherwise, return None. + """ + realpath = os.path.realpath(blockdev) + if realpath.startswith("/dev/dm-"): + LOG.debug("%s is a mapped device pointing to %s", blockdev, realpath) + return realpath + return None + + +def is_encrypted(blockdev) -> bool: + """ + Check if a device is an encrypted device. blockdev should have + a /dev/dm-* path. + """ + if not subp.which("cryptsetup"): + LOG.debug("cryptsetup not found. Assuming no encrypted partitions") + return False + is_encrypted = False + with suppress(subp.ProcessExecutionError): + subp.subp(["cryptsetup", "status", blockdev]) + is_encrypted = True + LOG.debug( + "Determined that %s is %s encrypted", + blockdev, + "" if is_encrypted else "not" + ) + return is_encrypted + + +def get_underlying_partition(blockdev): + command = ["dmsetup", "deps", "--options=devname", blockdev] + dep = subp.subp(command)[0] + try: + # Returned result should look something like: + # 1 dependencies : (vdb1) + return "/dev/{}".format(dep.split(": (")[1].split(")")[0]) + except IndexError: + raise Exception( + "Ran `{}`, but received unexpected stdout: `{}`".format( + command, dep)) + + +def resize_encrypted(blockdev, partition): + """Use 'cryptsetup resize' to resize LUKS volume. + + The loaded keyfile is json formatted with 'key' and 'slot' keys. + key is base64 encoded. Example: + {"key":"XFmCwX2FHIQp0LBWaLEMiHIyfxt1SGm16VvUAVledlY=","slot":5} + """ + try: + with open(KEYDATA_PATH) as f: + keydata = json.load(f) + key = keydata["key"] + decoded_key = base64.b64decode(key) + slot = keydata["slot"] + except Exception as e: + raise Exception("Could not load encryption key") from e + subp.subp( + ["cryptsetup", "--key-file", "-", "resize", blockdev], + data=decoded_key, + ) + subp.subp([ + "cryptsetup", "luksKillSlot", "--batch-mode", partition, str(slot) + ]) + + def resize_devices(resizer, devices): # returns a tuple of tuples containing (entry-in-devices, action, message) + devices = copy.copy(devices) info = [] - for devent in devices: + + while devices: + devent = devices.pop(0) try: blockdev = devent2dev(devent) except ValueError as e: @@ -310,6 +388,29 @@ def resize_devices(resizer, devices): "device '%s' not a block device" % blockdev,)) continue + underlying_blockdev = get_mapped_device(blockdev) + if underlying_blockdev and is_encrypted(underlying_blockdev): + try: + # We need to resize the underlying partition first + partition = get_underlying_partition(blockdev) + if partition not in [x[0] for x in info]: + # We shouldn't attempt to resize this mapped partition + # until the underlying partition is resized, so re-add + # our device to the beginning of the list we're iterating + # over, then add our underlying partition so it can + # get processed first + devices.insert(0, devent) + devices.insert(0, partition) + continue + resize_encrypted(blockdev, partition) + except Exception as e: + info.append(( + devent, + RESIZE.FAILED, + "Resizing encrypted device ({}) failed: {}".format( + blockdev, e + ) + )) try: (disk, ptnum) = device_part_info(blockdev) except (TypeError, ValueError) as e: diff --git a/cloudinit/subp.py b/cloudinit/subp.py index 024e1a98a9b..ea49af7f41c 100644 --- a/cloudinit/subp.py +++ b/cloudinit/subp.py @@ -4,6 +4,7 @@ import logging import os import subprocess +from typing import Tuple, Any from errno import ENOEXEC @@ -141,10 +142,12 @@ def _indent_text(self, text, indent_level=8): return text.rstrip(cr).replace(cr, cr + indent) -def subp(args, data=None, rcs=None, env=None, capture=True, - combine_capture=False, shell=False, - logstring=False, decode="replace", target=None, update_env=None, - status_cb=None, cwd=None): +def subp( + args, data=None, rcs=None, env=None, capture=True, + combine_capture=False, shell=False, + logstring=False, decode="replace", target=None, update_env=None, + status_cb=None, cwd=None +) -> Tuple[Any, Any]: """Run a subprocess. :param args: command to run in a list. [cmd, arg1, arg2...]