Skip to content
Open
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
3 changes: 2 additions & 1 deletion coriolis/osmorphing/osmount/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -401,7 +401,8 @@ def _get_mounted_devices(self):
mounted_device_numbers.append(
self._exec_cmd(dev_nmb_cmd % dev_name).rstrip())

block_devs = self._exec_cmd("ls -al /dev | grep ^b").splitlines()
block_devs = self._exec_cmd(
"ls -al /dev | grep ^b || true").splitlines()
for dev_line in block_devs:
dev = dev_line.split()
major_minor = "%s:%s" % (
Expand Down
80 changes: 59 additions & 21 deletions coriolis/osmorphing/osmount/luks_mixin.py
Original file line number Diff line number Diff line change
Expand Up @@ -307,6 +307,16 @@ def _get_luks_uuid(self, dev_path):

def _update_crypttab_keyfile(self, os_root_dir, uuid_to_keyfile):
"""Update the keyfile column in crypttab for matching LUKS UUIDs."""
# cryptsetup-initramfs (Ubuntu 22.04+) only embeds crypttab entries
# in the initramfs when the device is verifiable at build time OR when
# the 'initramfs' option is present. Inside a chroot (no udev, no
# /dev/disk/by-uuid/), verification always fails, so we force-add
# 'initramfs' for update-initramfs systems.
# On dracut systems the option is unnecessary and may confuse the
# crypttab parser.
add_initramfs_opt = (
self._detect_initramfs_tool(os_root_dir) == "update-initramfs")

def _set_keyfile(parts):
if len(parts) < 2:
return None
Expand All @@ -316,21 +326,16 @@ def _set_keyfile(parts):
if not m:
return None

keyfile = uuid_to_keyfile.get(m.group(1))
keyfile = uuid_to_keyfile.get(m.group(1).lower())
if keyfile is None:
return None

while len(parts) < 4:
parts.append("")

parts[2] = keyfile
# cryptsetup-initramfs (Ubuntu 22.04+) only embeds crypttab
# entries in the initramfs when the device is verifiable at build
# time OR when the 'initramfs' option is present. Inside a chroot
# (no udev, no /dev/disk/by-uuid/), verification always fails, so
# we force-add 'initramfs' here.
opts_list = [o for o in parts[3].split(",") if o]
if "initramfs" not in opts_list:
if add_initramfs_opt and "initramfs" not in opts_list:
opts_list.append("initramfs")

parts[3] = ",".join(opts_list)
Expand All @@ -357,7 +362,12 @@ def _write_migration_keyfiles(self, os_root_dir):

uuid_to_keyfile = {}
for _, dev_path in self._luks_opened:
luks_uuid = self._get_luks_uuid(dev_path)
try:
luks_uuid = self._get_luks_uuid(dev_path)
except Exception as ex:
raise exception.CoriolisException(
"Could not determine LUKS UUID for '%s'; "
"cannot write migration keyfile." % dev_path) from ex

keyfile_path = self._get_migration_keyfile_path(dev_path)
abs_path = os.path.join(os_root_dir, keyfile_path.lstrip("/"))
Expand Down Expand Up @@ -399,7 +409,16 @@ def _configure_dracut_keyfiles(self, os_root_dir, uuid_to_keyfile):

conf_abs = os.path.join(
os_root_dir, _DRACUT_LUKS_CONF_PATH.lstrip("/"))
conf_content = 'install_items+=" %s "\n' % " ".join(install_items)
# hostonly="no" forces a generic initramfs, so the rebuilt image works
# on the target hypervisor (e.g.: virtio_blk on KVM), regardless of
# what hardware is visible inside the OS morphing minion at build time.
# add_dracutmodules ensures the crypt module is included even when
# dracut's host-detection runs inside a minion without LUKS devices.
conf_content = (
'hostonly="no"\n'
'add_dracutmodules+=" crypt "\n'
'install_items+=" %s "\n'
) % " ".join(install_items)
self._write_remote_file(conf_abs, conf_content)
self._exec_cmd(
"sudo chown root:root %s && sudo chmod 644 %s" % (
Expand Down Expand Up @@ -488,18 +507,7 @@ def _rebuild_initramfs(self, os_root_dir):
self._exec_cmd(
"sudo chroot %s update-initramfs -u -k all" % os_root_dir)
elif tool == "dracut":
# --regenerate-all scans the chroot's own /lib/modules/ for
# installed kernels instead of relying on uname -r
#
# Explicitly --include the crypttab and any LUKS migration keyfiles
# so that systemd-cryptsetup-generator finds them in the initramfs
# and uses the crypttab mapper name (luks-root) and keyfile for
# auto-unlock. install_items in dracut.conf.d embeds the keyfile
# but does NOT guarantee that crypttab ends up in the image.
include_args = self._build_dracut_include_args(os_root_dir)
self._exec_cmd(
"sudo chroot %s dracut --regenerate-all --force %s"
% (os_root_dir, " ".join(include_args)))
self._rebuild_initramfs_dracut(os_root_dir)
else:
raise exception.CoriolisException(
"No initramfs tool found in OS at '%s'; cannot rebuild "
Expand Down Expand Up @@ -530,6 +538,36 @@ def install_encryption_firstboot_setup(
script_content, user_provided=False,
script_filename="luks-firstboot.sh")

def _rebuild_initramfs_dracut(self, os_root_dir):
"""Rebuild all dracut initramfs images inside the OS chroot."""
include_args = self._build_dracut_include_args(os_root_dir)

try:
kvers_out = self._exec_cmd(
"sudo ls -1 '%s/lib/modules/' 2>/dev/null" % os_root_dir
).strip().splitlines()
kvers = [k.strip() for k in kvers_out if k.strip()]
except Exception:
kvers = []

if not kvers:
raise exception.CoriolisException(
"No kernel versions found under '%s/lib/modules/'; "
"cannot rebuild the initramfs for LUKS auto-unlock." %
os_root_dir)

for kver in kvers:
LOG.info("Rebuilding dracut initramfs for kernel %s", kver)
# --no-hostonly and --add crypt override conf.d settings that
# could be ignored in a chroot without running udevd / uname.
# --add-drivers dm-crypt directly embeds dm-crypt.ko, so the
# crypt DM target type is available even when instmods can't
# resolve it via modules.dep (e.g.: stripped cloud images).
self._exec_cmd(
"sudo chroot '%s' dracut -f --kver '%s' "
"--no-hostonly --add crypt --add-drivers dm-crypt %s"
% (os_root_dir, kver, " ".join(include_args)))

def _fix_grub_luks_root(self, os_root_dir):
"""Patch grub.cfg to use crypttab mapper names for LUKS root devices.

Expand Down
18 changes: 17 additions & 1 deletion coriolis/osmorphing/osmount/resources/luks_firstboot_dracut.sh
Original file line number Diff line number Diff line change
Expand Up @@ -144,11 +144,27 @@ rm -f "${keyfiles[@]}"
rmdir "$KEYFILE_DIR" 2>/dev/null || true

echo "Rebuilding initramfs."
# Rebuild every installed kernel so none of them retain the embedded keyfile.
kvers=()
for kdir in /lib/modules/*/; do
kver="${kdir%/}"
kver="${kver##*/}"
[ -n "$kver" ] && kvers+=("$kver")
done

if [ "${#kvers[@]}" -eq 0 ]; then
echo "ERROR: no kernel versions found under /lib/modules/; cannot rebuild initramfs." >&2
exit 1
fi

# Embed the updated crypttab, so systemd-cryptsetup-generator uses the crypttab
# mapper name and tpm2-device=auto for auto-unlock. The Coriolis dracut.conf.d
# entry is deleted after this rebuild, so its install_items (TPM2 plugin + libtss2)
# are still picked up here.
dracut --force --include /etc/crypttab /etc/crypttab
for kver in "${kvers[@]}"; do
echo "Rebuilding dracut initramfs for kernel $kver."
dracut --force --kver "$kver" --include /etc/crypttab /etc/crypttab
done
rm -f "$DRACUT_CONF"

echo "Firstboot LUKS cleanup complete."
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,5 +31,6 @@ class LUKSTestRockyLinuxOSMorphingTools(TestRockyLinuxOSMorphingTools):
("jq", True),
("dracut", False),
("cryptsetup", False),
("device-mapper", False),
],
}
4 changes: 2 additions & 2 deletions coriolis/tests/osmorphing/osmount/test_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -605,7 +605,7 @@ def test__get_mounted_devices(self, mock_exec_cmd):
mock.call("cat /proc/mounts"),
mock.call("readlink -en /dev/sda1"),
mock.call("mountpoint -x /dev/sda1"),
mock.call("ls -al /dev | grep ^b"),
mock.call("ls -al /dev | grep ^b || true"),
])

self.assertEqual(result, ['/dev/sda1', '/dev/sda2'])
Expand All @@ -629,7 +629,7 @@ def test__get_mounted_devices_not_found(self, mock_test_ssh_path,
mock_exec_cmd.assert_has_calls([
mock.call("cat /proc/mounts"),
mock.call("readlink -en /dev/sda1"),
mock.call("ls -al /dev | grep ^b")
mock.call("ls -al /dev | grep ^b || true")
])

mock_test_ssh_path.assert_called_once_with(
Expand Down
96 changes: 81 additions & 15 deletions coriolis/tests/osmorphing/osmount/test_luks_mixin.py
Original file line number Diff line number Diff line change
Expand Up @@ -340,9 +340,11 @@ def test__get_luks_uuid(self, mock_exec_cmd):
"sudo cryptsetup luksUUID %s" % _DEV
)

@mock.patch.object(luks_mixin.LinuxLUKSMixin, "_detect_initramfs_tool")
@mock.patch.object(luks_mixin.LinuxLUKSMixin, "_transform_crypttab")
def test__update_crypttab_keyfile(self, mock_transform):
def test__update_crypttab_keyfile(self, mock_transform, mock_detect_tool):
mock_transform.return_value = True
mock_detect_tool.return_value = "update-initramfs"
self.mixin._update_crypttab_keyfile(_OS_ROOT_DIR, {_UUID: _KEYFILE})
mock_transform.assert_called_once()

Expand All @@ -365,16 +367,32 @@ def test__update_crypttab_keyfile(self, mock_transform):
]
self.assertIsNone(transform(parts))

# UUID= format match; 'initramfs' always appended.
# UUID= format match; update-initramfs -> 'initramfs' option added.
result = transform(["luks-root", "UUID=%s" % _UUID, "none", "none"])
self.assertEqual(result[2], _KEYFILE)
self.assertIn("initramfs", result[3].split(","))

# Case-insensitive UUID match: uppercase UUID in crypttab still matches
# lowercase key in the map.
result = transform(
["luks-root", "UUID=%s" % _UUID.upper(), "none", "none"])
self.assertEqual(result[2], _KEYFILE)

# /by-uuid/ path also matches.
parts = ["luks-root", "/dev/disk/by-uuid/%s" % _UUID, "none", "none"]
result = transform(parts)
self.assertEqual(result[2], _KEYFILE)

# dracut -> 'initramfs' option NOT added.
mock_transform.reset_mock()
mock_detect_tool.return_value = "dracut"
self.mixin._update_crypttab_keyfile(_OS_ROOT_DIR, {_UUID: _KEYFILE})
transform_dracut = mock_transform.call_args[0][1]
result = transform_dracut(
["luks-root", "UUID=%s" % _UUID, "none", "none"])
self.assertEqual(result[2], _KEYFILE)
self.assertNotIn("initramfs", result[3].split(","))

# transform returns False -> exception.
mock_transform.return_value = False
self.assertRaises(
Expand Down Expand Up @@ -452,6 +470,16 @@ def test__write_migration_keyfiles(
_OS_ROOT_DIR,
)

# _get_luks_uuid raises: wrapped in CoriolisException.
mock_detect_tool.return_value = "dracut"
mock_uuid.side_effect = Exception("cryptsetup went boom")
self.mixin._luks_opened = [("coriolis_sda", _DEV)]
self.assertRaises(
exception.CoriolisException,
self.mixin._write_migration_keyfiles,
_OS_ROOT_DIR,
)

@mock.patch.object(luks_mixin.LinuxLUKSMixin, "_write_remote_file")
@mock.patch.object(base.BaseSSHOSMountTools, "_exec_cmd")
@mock.patch.object(luks_mixin.utils, "test_ssh_path")
Expand All @@ -471,6 +499,8 @@ def test__configure_dracut_keyfiles(
written = mock_write.call_args[0][1]
self.assertIn(_KEYFILE, written)
self.assertIn("install_items+=", written)
self.assertIn('hostonly="no"', written)
self.assertIn('add_dracutmodules+=" crypt "', written)
self.assertNotIn(plugin_path, written)
mock_exec.assert_called_once_with(
"sudo chown root:root %s && sudo chmod 644 %s"
Expand Down Expand Up @@ -565,34 +595,27 @@ def test__build_dracut_include_args(self, mock_test, mock_exec):
]
self.assertEqual(result, expected)

@mock.patch.object(luks_mixin.LinuxLUKSMixin, "_build_dracut_include_args")
@mock.patch.object(luks_mixin.LinuxLUKSMixin, "_rebuild_initramfs_dracut")
@mock.patch.object(luks_mixin.LinuxLUKSMixin, "_detect_initramfs_tool")
@mock.patch.object(base.BaseSSHOSMountTools, "_exec_cmd")
def test__rebuild_initramfs(
self, mock_exec, mock_detect, mock_include_args
self, mock_exec, mock_detect, mock_rebuild_dracut
):
# update-initramfs.
mock_detect.return_value = "update-initramfs"
self.mixin._rebuild_initramfs(_OS_ROOT_DIR)
mock_exec.assert_called_once_with(
"sudo chroot %s update-initramfs -u -k all" % _OS_ROOT_DIR
)
mock_rebuild_dracut.assert_not_called()

# dracut: --regenerate-all --force with --include args.
# dracut: delegates to _rebuild_initramfs_dracut.
mock_exec.reset_mock()
mock_detect.return_value = "dracut"
mock_include_args.return_value = [
"--include",
"/etc/crypttab",
"/etc/crypttab",
]
self.mixin._rebuild_initramfs(_OS_ROOT_DIR)

mock_include_args.assert_called_once_with(_OS_ROOT_DIR)
mock_exec.assert_called_once_with(
"sudo chroot %s dracut --regenerate-all --force "
"--include /etc/crypttab /etc/crypttab" % _OS_ROOT_DIR
)
mock_rebuild_dracut.assert_called_once_with(_OS_ROOT_DIR)
mock_exec.assert_not_called()

# no tool found.
mock_detect.return_value = None
Expand All @@ -602,6 +625,49 @@ def test__rebuild_initramfs(
_OS_ROOT_DIR,
)

@mock.patch.object(luks_mixin.LinuxLUKSMixin, "_build_dracut_include_args")
@mock.patch.object(base.BaseSSHOSMountTools, "_exec_cmd")
def test__rebuild_initramfs_dracut(self, mock_exec, mock_include_args):
# ls raises: treated as empty kernel list -> CoriolisException.
mock_exec.side_effect = Exception("ls failed")
self.assertRaises(
exception.CoriolisException,
self.mixin._rebuild_initramfs_dracut,
_OS_ROOT_DIR,
)
mock_include_args.assert_called_once_with(_OS_ROOT_DIR)

# ls succeeds but returns empty output -> CoriolisException.
mock_exec.reset_mock()
mock_exec.side_effect = None
mock_exec.return_value = " \n\n"
self.assertRaises(
exception.CoriolisException,
self.mixin._rebuild_initramfs_dracut,
_OS_ROOT_DIR,
)

# initramfs rebuilt.
mock_include_args.return_value = [
"--include",
"/etc/crypttab",
"/etc/crypttab",
]

mock_exec.reset_mock()
mock_exec.side_effect = [
"5.15.0-generic\n", # ls /lib/modules/
None, # dracut call
]
self.mixin._rebuild_initramfs_dracut(_OS_ROOT_DIR)

self.assertEqual(mock_exec.call_count, 2)
mock_exec.assert_any_call(
"sudo chroot '%s' dracut -f --kver '5.15.0-generic' "
"--no-hostonly --add crypt --add-drivers dm-crypt "
"--include /etc/crypttab /etc/crypttab" % _OS_ROOT_DIR
)

@mock.patch.object(luks_mixin.LinuxLUKSMixin, '_detect_initramfs_tool')
@mock.patch.object(luks_mixin.LinuxLUKSMixin, '_rebuild_initramfs')
@mock.patch.object(luks_mixin.LinuxLUKSMixin, '_fix_grub_luks_root')
Expand Down
Loading