From cd4b88f26566c7d5b797528c928f474181695275 Mon Sep 17 00:00:00 2001 From: James Falcon Date: Wed, 22 Sep 2021 11:14:37 -0500 Subject: [PATCH 1/3] in progress... --- cloudinit/config/cc_growpart.py | 85 +++++++++++++++++++++++++++++++-- cloudinit/subp.py | 11 +++-- 2 files changed, 87 insertions(+), 9 deletions(-) diff --git a/cloudinit/config/cc_growpart.py b/cloudinit/config/cc_growpart.py index 1ddc9dc7080..878d8528340 100644 --- a/cloudinit/config/cc_growpart.py +++ b/cloudinit/config/cc_growpart.py @@ -63,11 +63,12 @@ - "/dev/vdb1" ignore_growroot_disabled: """ - +import copy 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 @@ -286,12 +287,69 @@ def devent2dev(devent): return dev +def is_mapped_device(blockdev) -> bool: + """ + Check if a device is a mapped device. + + blockdev should look something like '/dev/mapper/disk1' if True, + otherwise something like /dev/vdb1. + """ + is_mapped = blockdev.startswith('/dev/mapper/') + LOG.debug("%s is %s a mapped device", blockdev, "" if is_mapped else "not") + return is_mapped + + +def is_encrypted(blockdev) -> bool: + """ + Check if a device is an encrypted device. + + Will work for both mapped and raw devices. + """ + is_encrypted = False + if not subp.which('cryptsetup'): + LOG.debug('cryptsetup not found. Assuming no encrypted partitions') + return False + with suppress(subp.ProcessExecutionError): + subp.subp(['cryptsetup', 'status', blockdev]) + is_encrypted = True + if not is_encrypted: + with suppress(subp.ProcessExecutionError): + subp.subp(['cryptsetup', 'isLuks', 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 -o devname {}'.format(blockdev) + dep = subp.subp(command.split())[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): + subp.subp(['cryptsetup', '--key-file', '', 'resize', blockdev]) + + 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) # /mnt TODO: DELETE ME try: - blockdev = devent2dev(devent) + blockdev = devent2dev(devent) # /dev/mapper/disk2 TODO: DELETE ME except ValueError as e: info.append((devent, RESIZE.SKIPPED, "unable to convert to device: %s" % e,)) @@ -313,8 +371,25 @@ def resize_devices(resizer, devices): try: (disk, ptnum) = device_part_info(blockdev) except (TypeError, ValueError) as e: - info.append((devent, RESIZE.SKIPPED, - "device_part_info(%s) failed: %s" % (blockdev, e),)) + if is_mapped_device(blockdev) and is_encrypted(blockdev): + # 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) + else: + info.append(( + devent, + RESIZE.SKIPPED, + "device_part_info(%s) failed: %s" % (blockdev, e), + )) continue try: 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...] From c0849d1a723a0da90a7c9682e566720cd4a70f0c Mon Sep 17 00:00:00 2001 From: James Falcon Date: Wed, 22 Sep 2021 11:34:44 -0500 Subject: [PATCH 2/3] Remove keyfile argument from cryptsetup --- cloudinit/config/cc_growpart.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cloudinit/config/cc_growpart.py b/cloudinit/config/cc_growpart.py index 878d8528340..6e52a9f20f3 100644 --- a/cloudinit/config/cc_growpart.py +++ b/cloudinit/config/cc_growpart.py @@ -338,7 +338,7 @@ def get_underlying_partition(blockdev): def resize_encrypted(blockdev): - subp.subp(['cryptsetup', '--key-file', '', 'resize', blockdev]) + subp.subp(['cryptsetup', 'resize', blockdev]) def resize_devices(resizer, devices): From e3fb9d5924c53efbf5782910b35da3838064f129 Mon Sep 17 00:00:00 2001 From: James Falcon Date: Thu, 4 Nov 2021 11:59:04 -0500 Subject: [PATCH 3/3] Update based on review comments --- cloudinit/config/cc_growpart.py | 94 +++++++++++++++++++++------------ 1 file changed, 60 insertions(+), 34 deletions(-) diff --git a/cloudinit/config/cc_growpart.py b/cloudinit/config/cc_growpart.py index 6e52a9f20f3..3ed18ca4e88 100644 --- a/cloudinit/config/cc_growpart.py +++ b/cloudinit/config/cc_growpart.py @@ -63,7 +63,9 @@ - "/dev/vdb1" ignore_growroot_disabled: """ +import base64 import copy +import json import os import os.path import re @@ -84,6 +86,8 @@ 'ignore_growroot_disabled': False, } +KEYDATA_PATH = "/cc_growpart_keydata" + class RESIZE(object): SKIPPED = "SKIPPED" @@ -287,35 +291,31 @@ def devent2dev(devent): return dev -def is_mapped_device(blockdev) -> bool: - """ - Check if a device is a mapped device. +def get_mapped_device(blockdev): + """Returns underlying block device for a mapped device. - blockdev should look something like '/dev/mapper/disk1' if True, - otherwise something like /dev/vdb1. + If blockdev is a symlink pointing to a /dev/dm-* device, return + the device pointed to. Otherwise, return None. """ - is_mapped = blockdev.startswith('/dev/mapper/') - LOG.debug("%s is %s a mapped device", blockdev, "" if is_mapped else "not") - return is_mapped + 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. - - Will work for both mapped and raw devices. + Check if a device is an encrypted device. blockdev should have + a /dev/dm-* path. """ - is_encrypted = False - if not subp.which('cryptsetup'): - LOG.debug('cryptsetup not found. Assuming no encrypted partitions') + 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]) + subp.subp(["cryptsetup", "status", blockdev]) is_encrypted = True - if not is_encrypted: - with suppress(subp.ProcessExecutionError): - subp.subp(['cryptsetup', 'isLuks', blockdev]) - is_encrypted = True LOG.debug( "Determined that %s is %s encrypted", blockdev, @@ -325,20 +325,40 @@ def is_encrypted(blockdev) -> bool: def get_underlying_partition(blockdev): - command = 'dmsetup deps -o devname {}'.format(blockdev) - dep = subp.subp(command.split())[0] + 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]) + return "/dev/{}".format(dep.split(": (")[1].split(")")[0]) except IndexError: raise Exception( "Ran `{}`, but received unexpected stdout: `{}`".format( command, dep)) -def resize_encrypted(blockdev): - subp.subp(['cryptsetup', 'resize', blockdev]) +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): @@ -347,9 +367,9 @@ def resize_devices(resizer, devices): info = [] while devices: - devent = devices.pop(0) # /mnt TODO: DELETE ME + devent = devices.pop(0) try: - blockdev = devent2dev(devent) # /dev/mapper/disk2 TODO: DELETE ME + blockdev = devent2dev(devent) except ValueError as e: info.append((devent, RESIZE.SKIPPED, "unable to convert to device: %s" % e,)) @@ -368,10 +388,9 @@ def resize_devices(resizer, devices): "device '%s' not a block device" % blockdev,)) continue - try: - (disk, ptnum) = device_part_info(blockdev) - except (TypeError, ValueError) as e: - if is_mapped_device(blockdev) and is_encrypted(blockdev): + 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]: @@ -383,13 +402,20 @@ def resize_devices(resizer, devices): devices.insert(0, devent) devices.insert(0, partition) continue - resize_encrypted(blockdev) - else: + resize_encrypted(blockdev, partition) + except Exception as e: info.append(( devent, - RESIZE.SKIPPED, - "device_part_info(%s) failed: %s" % (blockdev, e), + RESIZE.FAILED, + "Resizing encrypted device ({}) failed: {}".format( + blockdev, e + ) )) + try: + (disk, ptnum) = device_part_info(blockdev) + except (TypeError, ValueError) as e: + info.append((devent, RESIZE.SKIPPED, + "device_part_info(%s) failed: %s" % (blockdev, e),)) continue try: