Przeglądaj źródła

Improves LUKS initramfs rebuild for dracut and chroot environments

- Conditionally add 'initramfs' crypttab option only for update-initramfs
  systems (not dracut, where it may confuse the parser)
- Case-insensitive UUID matching in crypttab keyfile updates
- Per-kernel dracut rebuild instead of --regenerate-all for reliable
  --include handling across distributions
- Add --no-hostonly and --add crypt flags for generic initramfs in chroot
- Wrap LUKS UUID lookup errors with a descriptive CoriolisException
Claudiu Belu 1 tydzień temu
rodzic
commit
a04e2958c2

+ 2 - 1
coriolis/osmorphing/osmount/base.py

@@ -401,7 +401,8 @@ class BaseLinuxOSMountTools(luks_mixin.LinuxLUKSMixin, BaseSSHOSMountTools):
                 mounted_device_numbers.append(
                 mounted_device_numbers.append(
                     self._exec_cmd(dev_nmb_cmd % dev_name).rstrip())
                     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:
         for dev_line in block_devs:
             dev = dev_line.split()
             dev = dev_line.split()
             major_minor = "%s:%s" % (
             major_minor = "%s:%s" % (

+ 59 - 21
coriolis/osmorphing/osmount/luks_mixin.py

@@ -307,6 +307,16 @@ class LinuxLUKSMixin:
 
 
     def _update_crypttab_keyfile(self, os_root_dir, uuid_to_keyfile):
     def _update_crypttab_keyfile(self, os_root_dir, uuid_to_keyfile):
         """Update the keyfile column in crypttab for matching LUKS UUIDs."""
         """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):
         def _set_keyfile(parts):
             if len(parts) < 2:
             if len(parts) < 2:
                 return None
                 return None
@@ -316,7 +326,7 @@ class LinuxLUKSMixin:
             if not m:
             if not m:
                 return None
                 return None
 
 
-            keyfile = uuid_to_keyfile.get(m.group(1))
+            keyfile = uuid_to_keyfile.get(m.group(1).lower())
             if keyfile is None:
             if keyfile is None:
                 return None
                 return None
 
 
@@ -324,13 +334,8 @@ class LinuxLUKSMixin:
                 parts.append("")
                 parts.append("")
 
 
             parts[2] = keyfile
             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]
             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")
                 opts_list.append("initramfs")
 
 
             parts[3] = ",".join(opts_list)
             parts[3] = ",".join(opts_list)
@@ -357,7 +362,12 @@ class LinuxLUKSMixin:
 
 
         uuid_to_keyfile = {}
         uuid_to_keyfile = {}
         for _, dev_path in self._luks_opened:
         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)
             keyfile_path = self._get_migration_keyfile_path(dev_path)
             abs_path = os.path.join(os_root_dir, keyfile_path.lstrip("/"))
             abs_path = os.path.join(os_root_dir, keyfile_path.lstrip("/"))
@@ -399,7 +409,16 @@ class LinuxLUKSMixin:
 
 
         conf_abs = os.path.join(
         conf_abs = os.path.join(
             os_root_dir, _DRACUT_LUKS_CONF_PATH.lstrip("/"))
             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._write_remote_file(conf_abs, conf_content)
         self._exec_cmd(
         self._exec_cmd(
             "sudo chown root:root %s && sudo chmod 644 %s" % (
             "sudo chown root:root %s && sudo chmod 644 %s" % (
@@ -488,18 +507,7 @@ class LinuxLUKSMixin:
             self._exec_cmd(
             self._exec_cmd(
                 "sudo chroot %s update-initramfs -u -k all" % os_root_dir)
                 "sudo chroot %s update-initramfs -u -k all" % os_root_dir)
         elif tool == "dracut":
         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:
         else:
             raise exception.CoriolisException(
             raise exception.CoriolisException(
                 "No initramfs tool found in OS at '%s'; cannot rebuild "
                 "No initramfs tool found in OS at '%s'; cannot rebuild "
@@ -530,6 +538,36 @@ class LinuxLUKSMixin:
             script_content, user_provided=False,
             script_content, user_provided=False,
             script_filename="luks-firstboot.sh")
             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):
     def _fix_grub_luks_root(self, os_root_dir):
         """Patch grub.cfg to use crypttab mapper names for LUKS root devices.
         """Patch grub.cfg to use crypttab mapper names for LUKS root devices.
 
 

+ 17 - 1
coriolis/osmorphing/osmount/resources/luks_firstboot_dracut.sh

@@ -144,11 +144,27 @@ rm -f "${keyfiles[@]}"
 rmdir "$KEYFILE_DIR" 2>/dev/null || true
 rmdir "$KEYFILE_DIR" 2>/dev/null || true
 
 
 echo "Rebuilding initramfs."
 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
 # 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
 # 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)
 # entry is deleted after this rebuild, so its install_items (TPM2 plugin + libtss2)
 # are still picked up here.
 # 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"
 rm -f "$DRACUT_CONF"
 
 
 echo "Firstboot LUKS cleanup complete."
 echo "Firstboot LUKS cleanup complete."

+ 4 - 0
coriolis/tests/integration/deployments/test_luks_osmorphing.py

@@ -116,6 +116,10 @@ class LUKSRockyLinuxOSMorphingDeploymentTest(
         _LUKSOSMorphingMixin, integration_base.ReplicaIntegrationTestBase):
         _LUKSOSMorphingMixin, integration_base.ReplicaIntegrationTestBase):
     """LUKS + dracut OS morphing test using Rocky Linux 9."""
     """LUKS + dracut OS morphing test using Rocky Linux 9."""
 
 
+    # kernel-core (~150 MB installed) needs extra room on top of the base
+    # container image and the other morphing packages.
+    _SCSI_DEBUG_SIZE_MB = 777
+
     def _prepare_src_device(self):
     def _prepare_src_device(self):
         test_utils.make_luks_device(
         test_utils.make_luks_device(
             self._src_device, self._key_file, "rockylinux:9")
             self._src_device, self._key_file, "rockylinux:9")

+ 19 - 3
coriolis/tests/integration/test_provider/osmorphing/rocky.py

@@ -21,9 +21,15 @@ class TestRockyLinuxOSMorphingTools(rocky.BaseRockyLinuxMorphingTools):
 class LUKSTestRockyLinuxOSMorphingTools(TestRockyLinuxOSMorphingTools):
 class LUKSTestRockyLinuxOSMorphingTools(TestRockyLinuxOSMorphingTools):
     """Rocky Linux morphing tools for LUKS integration tests.
     """Rocky Linux morphing tools for LUKS integration tests.
 
 
-    Extends the base test tools with dracut and cryptsetup, which provide
-    the initramfs rebuild tool and LUKS support. The base Rocky Linux Docker
-    image omits them; they must be installed so initramfs can be rebuilt.
+    Extends the base test tools with dracut, cryptsetup, and kernel-core.
+    The base Rocky Linux Docker image omits them; kernel-core provides the
+    lib/modules/<kver>/ tree that dracut needs to rebuild the initramfs for
+    LUKS auto-unlock.
+
+    linux-firmware is a large (~150 MB) weak dependency of kernel-core that
+    is not needed for initramfs rebuilds and would overflow the test device
+    during download. install_weak_deps is therefore disabled in the chroot's
+    dnf.conf before any packages are installed.
     """
     """
 
 
     _packages = {
     _packages = {
@@ -31,5 +37,15 @@ class LUKSTestRockyLinuxOSMorphingTools(TestRockyLinuxOSMorphingTools):
             ("jq", True),
             ("jq", True),
             ("dracut", False),
             ("dracut", False),
             ("cryptsetup", False),
             ("cryptsetup", False),
+            ("device-mapper", False),
+            ("kernel-core", False),
         ],
         ],
     }
     }
+
+    def pre_packages_install(self, package_names):
+        # Disable weak deps so kernel-core does not pull in linux-firmware.
+        self._exec_cmd_chroot(
+            "bash -c 'mkdir -p /etc/dnf && "
+            "echo install_weak_deps=False >> /etc/dnf/dnf.conf'"
+        )
+        super().pre_packages_install(package_names)

+ 2 - 2
coriolis/tests/osmorphing/osmount/test_base.py

@@ -605,7 +605,7 @@ class BaseLinuxOSMountToolsTestCase(test_base.CoriolisBaseTestCase):
             mock.call("cat /proc/mounts"),
             mock.call("cat /proc/mounts"),
             mock.call("readlink -en /dev/sda1"),
             mock.call("readlink -en /dev/sda1"),
             mock.call("mountpoint -x /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'])
         self.assertEqual(result, ['/dev/sda1', '/dev/sda2'])
@@ -629,7 +629,7 @@ class BaseLinuxOSMountToolsTestCase(test_base.CoriolisBaseTestCase):
         mock_exec_cmd.assert_has_calls([
         mock_exec_cmd.assert_has_calls([
             mock.call("cat /proc/mounts"),
             mock.call("cat /proc/mounts"),
             mock.call("readlink -en /dev/sda1"),
             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(
         mock_test_ssh_path.assert_called_once_with(

+ 81 - 15
coriolis/tests/osmorphing/osmount/test_luks_mixin.py

@@ -340,9 +340,11 @@ class LinuxLUKSMixinTestCase(test_base.CoriolisBaseTestCase):
             "sudo cryptsetup luksUUID %s" % _DEV
             "sudo cryptsetup luksUUID %s" % _DEV
         )
         )
 
 
+    @mock.patch.object(luks_mixin.LinuxLUKSMixin, "_detect_initramfs_tool")
     @mock.patch.object(luks_mixin.LinuxLUKSMixin, "_transform_crypttab")
     @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_transform.return_value = True
+        mock_detect_tool.return_value = "update-initramfs"
         self.mixin._update_crypttab_keyfile(_OS_ROOT_DIR, {_UUID: _KEYFILE})
         self.mixin._update_crypttab_keyfile(_OS_ROOT_DIR, {_UUID: _KEYFILE})
         mock_transform.assert_called_once()
         mock_transform.assert_called_once()
 
 
@@ -365,16 +367,32 @@ class LinuxLUKSMixinTestCase(test_base.CoriolisBaseTestCase):
         ]
         ]
         self.assertIsNone(transform(parts))
         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"])
         result = transform(["luks-root", "UUID=%s" % _UUID, "none", "none"])
         self.assertEqual(result[2], _KEYFILE)
         self.assertEqual(result[2], _KEYFILE)
         self.assertIn("initramfs", result[3].split(","))
         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.
         # /by-uuid/ path also matches.
         parts = ["luks-root", "/dev/disk/by-uuid/%s" % _UUID, "none", "none"]
         parts = ["luks-root", "/dev/disk/by-uuid/%s" % _UUID, "none", "none"]
         result = transform(parts)
         result = transform(parts)
         self.assertEqual(result[2], _KEYFILE)
         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.
         # transform returns False -> exception.
         mock_transform.return_value = False
         mock_transform.return_value = False
         self.assertRaises(
         self.assertRaises(
@@ -452,6 +470,16 @@ class LinuxLUKSMixinTestCase(test_base.CoriolisBaseTestCase):
             _OS_ROOT_DIR,
             _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(luks_mixin.LinuxLUKSMixin, "_write_remote_file")
     @mock.patch.object(base.BaseSSHOSMountTools, "_exec_cmd")
     @mock.patch.object(base.BaseSSHOSMountTools, "_exec_cmd")
     @mock.patch.object(luks_mixin.utils, "test_ssh_path")
     @mock.patch.object(luks_mixin.utils, "test_ssh_path")
@@ -471,6 +499,8 @@ class LinuxLUKSMixinTestCase(test_base.CoriolisBaseTestCase):
         written = mock_write.call_args[0][1]
         written = mock_write.call_args[0][1]
         self.assertIn(_KEYFILE, written)
         self.assertIn(_KEYFILE, written)
         self.assertIn("install_items+=", written)
         self.assertIn("install_items+=", written)
+        self.assertIn('hostonly="no"', written)
+        self.assertIn('add_dracutmodules+=" crypt "', written)
         self.assertNotIn(plugin_path, written)
         self.assertNotIn(plugin_path, written)
         mock_exec.assert_called_once_with(
         mock_exec.assert_called_once_with(
             "sudo chown root:root %s && sudo chmod 644 %s"
             "sudo chown root:root %s && sudo chmod 644 %s"
@@ -565,11 +595,11 @@ class LinuxLUKSMixinTestCase(test_base.CoriolisBaseTestCase):
         ]
         ]
         self.assertEqual(result, expected)
         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(luks_mixin.LinuxLUKSMixin, "_detect_initramfs_tool")
     @mock.patch.object(base.BaseSSHOSMountTools, "_exec_cmd")
     @mock.patch.object(base.BaseSSHOSMountTools, "_exec_cmd")
     def test__rebuild_initramfs(
     def test__rebuild_initramfs(
-        self, mock_exec, mock_detect, mock_include_args
+        self, mock_exec, mock_detect, mock_rebuild_dracut
     ):
     ):
         # update-initramfs.
         # update-initramfs.
         mock_detect.return_value = "update-initramfs"
         mock_detect.return_value = "update-initramfs"
@@ -577,22 +607,15 @@ class LinuxLUKSMixinTestCase(test_base.CoriolisBaseTestCase):
         mock_exec.assert_called_once_with(
         mock_exec.assert_called_once_with(
             "sudo chroot %s update-initramfs -u -k all" % _OS_ROOT_DIR
             "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_exec.reset_mock()
         mock_detect.return_value = "dracut"
         mock_detect.return_value = "dracut"
-        mock_include_args.return_value = [
-            "--include",
-            "/etc/crypttab",
-            "/etc/crypttab",
-        ]
         self.mixin._rebuild_initramfs(_OS_ROOT_DIR)
         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.
         # no tool found.
         mock_detect.return_value = None
         mock_detect.return_value = None
@@ -602,6 +625,49 @@ class LinuxLUKSMixinTestCase(test_base.CoriolisBaseTestCase):
             _OS_ROOT_DIR,
             _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, '_detect_initramfs_tool')
     @mock.patch.object(luks_mixin.LinuxLUKSMixin, '_rebuild_initramfs')
     @mock.patch.object(luks_mixin.LinuxLUKSMixin, '_rebuild_initramfs')
     @mock.patch.object(luks_mixin.LinuxLUKSMixin, '_fix_grub_luks_root')
     @mock.patch.object(luks_mixin.LinuxLUKSMixin, '_fix_grub_luks_root')