Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
105 changes: 103 additions & 2 deletions cloudinit/config/cc_growpart.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,11 +63,14 @@
- "/dev/vdb1"
ignore_growroot_disabled: <true/false>
"""

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
Expand All @@ -83,6 +86,8 @@
'ignore_growroot_disabled': False,
}

KEYDATA_PATH = "/cc_growpart_keydata"


class RESIZE(object):
SKIPPED = "SKIPPED"
Expand Down Expand Up @@ -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:
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Based on the source code[1] it looks like it is possible that the output could possibly look like:

1 dependencies : (vdb1)

or

2 dependencies : (vdb1) (vdb2)

or

N dependencies : (vdb1) (vdb2) ...(vdbN)

I don't have a sense for how likely this is, however it looks like this won't be handled currently. I think a warning when N>1 might be more appropriate (or perhaps even handling multiple devices).

[1] Relevant snippet:

2721         for (i = 0; i < deps->count; i++) {
2722                 major = (int) MAJOR(deps->device[i]);
2723                 minor = (int) MINOR(deps->device[i]);
2724 
2725                 if ((_dev_name_type == DN_BLK || _dev_name_type == DN_MAP) &&
2726                     dm_device_get_name(major, minor, _dev_name_type == DN_BLK,
2727                                        dev_name, PATH_MAX))
2728                         printf(" (%s)", dev_name);
2729                 else
2730                         printf(" (%d, %d)", major, minor);
2731         }

# 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:
Expand All @@ -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:
Expand Down
11 changes: 7 additions & 4 deletions cloudinit/subp.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import logging
import os
import subprocess
from typing import Tuple, Any

from errno import ENOEXEC

Expand Down Expand Up @@ -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...]
Expand Down