]> git.ipfire.org Git - thirdparty/mkosi.git/commitdiff
mkosi-initrd: Port tests from old repository 2120/head
authorDaan De Meyer <daan.j.demeyer@gmail.com>
Mon, 4 Dec 2023 07:12:51 +0000 (08:12 +0100)
committerDaan De Meyer <daan.j.demeyer@gmail.com>
Tue, 5 Dec 2023 12:47:52 +0000 (13:47 +0100)
This commit ports some of the tests from
https://github.com/systemd/mkosi-initrd/blob/main/.github/workflows/build-fedora.sh
over.

The LUKS test is modified to generate the LUKS root partition using
repart instead of doing it manually. For the LVM tests we're forced
to do it manually as systemd-repart doesn't support setting up LVM
(and probably never will).

We also add an initrd size test so we notice when initrds grow due
to distribution packaging changes.

.github/workflows/ci.yml
action.yaml
mkosi.conf
mkosi.conf.d/20-ubuntu.conf
tests/test_boot.py
tests/test_initrd.py
tests/test_sysext.py [new file with mode: 0644]

index 13c2c7346c2e7b2ad6e1573ff856ee4dc1d6bd56..bf8452f86b47a86198001904a3932ea66431ad17 100644 (file)
@@ -105,18 +105,28 @@ jobs:
     - name: Install
       run: |
         sudo apt-get update
-        sudo apt-get install python3-pytest
+        sudo apt-get install python3-pytest lvm2 cryptsetup-bin
         # Make sure the latest changes from the pull request are used.
         sudo ln -svf $PWD/bin/mkosi /usr/bin/mkosi
       working-directory: ./
 
     - name: Configure
       run: |
-        tee mkosi.local.conf <<- EOF
+        tee mkosi.local.conf <<EOF
+        [Content]
+        KernelCommandLine=systemd.default_device_timeout_sec=180
+
         [Host]
         QemuKvm=no
         EOF
 
+        # TODO: Remove once all distros have recent enough systemd that knows systemd.default_device_timeout_sec.
+        mkdir -p mkosi-initrd/mkosi.extra/usr/lib/systemd/system.conf.d
+        tee mkosi-initrd/mkosi.extra/usr/lib/systemd/system.conf.d/device-timeout.conf <<EOF
+        [Manager]
+        DefaultDeviceTimeoutSec=180
+        EOF
+
     - name: Run integration tests
       run: sudo --preserve-env timeout -k 30 1h python3 -m pytest --tb=no -sv -m integration tests/
       env:
index b0315b7e1570214ee8b2d6e5ef63c24632bd8e78..cc37404b5e97bf9306a9e54ec42d18639aaedde4 100644 (file)
@@ -88,6 +88,7 @@ runs:
         systemd-measure
         systemd-nspawn
         systemd-repart
+        udevadm
         ukify
       )
 
index 9cd29d261ac0908093ebdf3eba32cf6c1fa9bc5d..529ab7c3cc41adada1eb05005f107fd1dcd07d84 100644 (file)
@@ -34,3 +34,6 @@ RemoveFiles=
         /usr/lib/kernel/install.d/20-grub.install
         # The dracut install plugin doesn't honor KERNEL_INSTALL_INITRD_GENERATOR.
         /usr/lib/kernel/install.d/50-dracut.install
+
+# Make sure that SELinux doesn't run in enforcing mode even if it's pulled in as a dependency.
+KernelCommandLine=console=ttyS0 enforcing=0
index cd19cff013022ac1f2c53908e2683a9ed1839c78..cf317fab8299d3450429ce35c1df404a60a089d1 100644 (file)
@@ -14,7 +14,7 @@ Packages=
         dbus-broker
         grub-pc
         intel-microcode
-        linux-kvm
+        linux-image-generic # TODO: Switch to linux-kvm once it supports reading credentials from SMBIOS.
         systemd
         systemd-boot
         systemd-sysv
index c86e36d9ce1ade46b91232a97db25b9e6f6e09a1..7a55c2f35f3f8f031e6da626839fcb24b5fcd978 100644 (file)
@@ -38,6 +38,10 @@ def test_boot(format: OutputFormat) -> None:
             args = ["systemd.mask=systemd-resolved.service"] if format == OutputFormat.directory else []
             image.boot(options=options, args=args)
 
+        if image.distribution == Distribution.ubuntu and format in (OutputFormat.cpio, OutputFormat.uki, OutputFormat.esp):
+            # https://bugs.launchpad.net/ubuntu/+source/linux-kvm/+bug/2045561
+            pytest.skip("Cannot boot Ubuntu UKI/cpio images in qemu until we switch back to linux-kvm")
+
         if image.distribution == Distribution.rhel_ubi:
             return
 
index e89602eca863bfd9ae4bcf27edd03cca32e1100b..683b139ab2ae4115376c55145f9ebb1ec4b951d5 100644 (file)
 # SPDX-License-Identifier: LGPL-2.1+
 
+import contextlib
+import os
+import subprocess
+import tempfile
+import textwrap
+from collections.abc import Iterator
 from pathlib import Path
 
 import pytest
 
 from mkosi.distributions import Distribution
+from mkosi.mounts import mount
+from mkosi.run import run
+from mkosi.tree import copy_tree
+from mkosi.util import INVOKING_USER
 
 from . import Image
 
+pytestmark = pytest.mark.integration
 
-@pytest.mark.integration
-def test_initrd() -> None:
+
+@pytest.fixture(scope="module")
+def passphrase() -> Iterator[Path]:
+    # We can't use tmp_path fixture because pytest creates it in a nested directory we can't access using our
+    # unprivileged user.
+    # TODO: Use delete_on_close=False and close() instead of flush() when we require Python 3.12 or newer.
+    with tempfile.NamedTemporaryFile(prefix="mkosi.passphrase", mode="w") as passphrase:
+        passphrase.write("mkosi")
+        passphrase.flush()
+        os.fchown(passphrase.fileno(), INVOKING_USER.uid, INVOKING_USER.gid)
+        os.fchmod(passphrase.fileno(), 0o600)
+        yield Path(passphrase.name)
+
+
+@pytest.fixture(scope="module")
+def initrd(passphrase: Path) -> Iterator[Image]:
     with Image(
         options=[
             "--directory", "",
             "--include=mkosi-initrd/",
+            "--extra-tree", passphrase,
         ],
     ) as initrd:
         if initrd.distribution == Distribution.rhel_ubi:
             pytest.skip("Cannot build RHEL-UBI initrds")
 
         initrd.build()
+        yield initrd
+
+
+def test_initrd(initrd: Image) -> None:
+    with Image(
+        options=[
+            "--initrd", Path(initrd.output_dir.name) / "initrd",
+            "--kernel-command-line=systemd.unit=mkosi-check-and-shutdown.service",
+            "--incremental",
+            "--ephemeral",
+            "--format=disk",
+        ]
+    ) as image:
+        image.build()
+        image.qemu()
+
+
+@pytest.mark.skipif(os.getuid() != 0, reason="mkosi-initrd LVM test can only be executed as root")
+def test_initrd_lvm(initrd: Image) -> None:
+    with Image(
+        options=[
+            "--initrd", Path(initrd.output_dir.name) / "initrd",
+            "--kernel-command-line=systemd.unit=mkosi-check-and-shutdown.service",
+            "--kernel-command-line=root=LABEL=root",
+            "--kernel-command-line=rw",
+            "--incremental",
+            "--ephemeral",
+            "--qemu-firmware=linux",
+        ]
+    ) as image, contextlib.ExitStack() as stack:
+        image.build(["--format", "directory"])
+
+        drive = Path(image.output_dir.name) / "image.raw"
+        drive.touch()
+        os.truncate(drive, 3000 * 1024**2)
+
+        lodev = run(["losetup", "--show", "--find", "--partscan", drive], stdout=subprocess.PIPE).stdout.strip()
+        stack.callback(lambda: run(["losetup", "--detach", lodev]))
+        run(["sfdisk", "--label", "gpt", lodev], input="type=E6D6D379-F507-44C2-A23C-238F2A3DF928 bootable")
+        run(["lvm", "pvcreate", f"{lodev}p1"])
+        run(["lvm", "pvs"])
+        run(["lvm", "vgcreate", "vg_mkosi", f"{lodev}p1"])
+        run(["lvm", "vgchange", "-ay", "vg_mkosi"])
+        run(["lvm", "vgs"])
+        stack.callback(lambda: run(["vgchange", "-an", "vg_mkosi"]))
+        run(["lvm", "lvcreate", "-l", "100%FREE", "-n", "lv0", "vg_mkosi"])
+        run(["lvm", "lvs"])
+        run(["udevadm", "wait", "/dev/vg_mkosi/lv0"])
+        run([f"mkfs.{image.distribution.filesystem()}", "-L", "root", "/dev/vg_mkosi/lv0"])
+
+        with mount(Path("/dev/vg_mkosi/lv0"), Path("mnt")) as mnt:
+            # The image might have been built unprivileged so we need to fix the file ownership. Making all the
+            # files owned by root isn't completely correct but good enough for the purposes of the test.
+            copy_tree(Path(image.output_dir.name) / "image", mnt, preserve_owner=False)
+
+        stack.close()
+
+        image.qemu(["--format=disk"])
+
+
+def test_initrd_luks(initrd: Image, passphrase: Path) -> None:
+    with tempfile.TemporaryDirectory() as repartd:
+        os.chown(repartd, INVOKING_USER.uid, INVOKING_USER.gid)
+
+        (Path(repartd) / "00-esp.conf").write_text(
+            textwrap.dedent(
+                """\
+                [Partition]
+                Type=esp
+                Format=vfat
+                CopyFiles=/efi:/
+                SizeMinBytes=512M
+                SizeMaxBytes=512M
+                """
+            )
+        )
+
+        (Path(repartd) / "05-bios.conf").write_text(
+            textwrap.dedent(
+                """\
+                [Partition]
+                # UUID of the grub BIOS boot partition which grubs needs on GPT to
+                # embed itself into.
+                Type=21686148-6449-6e6f-744e-656564454649
+                SizeMinBytes=1M
+                SizeMaxBytes=1M
+                """
+            )
+        )
+
+        (Path(repartd) / "10-root.conf").write_text(
+            textwrap.dedent(
+                f"""\
+                [Partition]
+                Type=root
+                Format={initrd.distribution.filesystem()}
+                Minimize=guess
+                Encrypt=key-file
+                CopyFiles=/
+                """
+            )
+        )
 
         with Image(
             options=[
                 "--initrd", Path(initrd.output_dir.name) / "initrd",
+                "--repart-dir", repartd,
+                "--passphrase", passphrase,
                 "--kernel-command-line=systemd.unit=mkosi-check-and-shutdown.service",
+                "--credential=cryptsetup.passphrase=mkosi",
                 "--incremental",
                 "--ephemeral",
                 "--format=disk",
@@ -33,3 +164,67 @@ def test_initrd() -> None:
         ) as image:
             image.build()
             image.qemu()
+
+
+@pytest.mark.skipif(os.getuid() != 0, reason="mkosi-initrd LUKS+LVM test can only be executed as root")
+def test_initrd_luks_lvm(initrd: Image, passphrase: Path) -> None:
+    with Image(
+        options=[
+            "--initrd", Path(initrd.output_dir.name) / "initrd",
+            "--kernel-command-line=systemd.unit=mkosi-check-and-shutdown.service",
+            "--kernel-command-line=root=LABEL=root",
+            "--kernel-command-line=rw",
+            f"--kernel-command-line=rd.luks.key=/{passphrase.name}",
+            "--incremental",
+            "--ephemeral",
+            "--qemu-firmware=linux",
+        ]
+    ) as image, contextlib.ExitStack() as stack:
+        image.build(["--format", "directory"])
+
+        drive = Path(image.output_dir.name) / "image.raw"
+        drive.touch()
+        os.truncate(drive, 3000 * 1024**2)
+
+        lodev = run(["losetup", "--show", "--find", "--partscan", drive], stdout=subprocess.PIPE).stdout.strip()
+        stack.callback(lambda: run(["losetup", "--detach", lodev]))
+        run(["sfdisk", "--label", "gpt", lodev], input="type=E6D6D379-F507-44C2-A23C-238F2A3DF928 bootable")
+        run(["cryptsetup", "--key-file", passphrase, "--use-random", "--pbkdf", "pbkdf2", "--pbkdf-force-iterations", "1000", "luksFormat", f"{lodev}p1"])
+        run(["cryptsetup", "--key-file", passphrase, "luksOpen", f"{lodev}p1", "lvm_root"])
+        stack.callback(lambda: run(["cryptsetup", "close", "lvm_root"]))
+        luks_uuid = run(["cryptsetup", "luksUUID", f"{lodev}p1"], stdout=subprocess.PIPE).stdout.strip()
+        run(["lvm", "pvcreate", "/dev/mapper/lvm_root"])
+        run(["lvm", "pvs"])
+        run(["lvm", "vgcreate", "vg_mkosi", "/dev/mapper/lvm_root"])
+        run(["lvm", "vgchange", "-ay", "vg_mkosi"])
+        run(["lvm", "vgs"])
+        stack.callback(lambda: run(["vgchange", "-an", "vg_mkosi"]))
+        run(["lvm", "lvcreate", "-l", "100%FREE", "-n", "lv0", "vg_mkosi"])
+        run(["lvm", "lvs"])
+        run(["udevadm", "wait", "/dev/vg_mkosi/lv0"])
+        run([f"mkfs.{image.distribution.filesystem()}", "-L", "root", "/dev/vg_mkosi/lv0"])
+
+        with mount(Path("/dev/vg_mkosi/lv0"), Path("mnt")) as mnt:
+            # The image might have been built unprivileged so we need to fix the file ownership. Making all the
+            # files owned by root isn't completely correct but good enough for the purposes of the test.
+            copy_tree(Path(image.output_dir.name) / "image", mnt, preserve_owner=False)
+
+        stack.close()
+
+        image.qemu([
+            "--format=disk",
+            f"--kernel-command-line=rd.luks.uuid={luks_uuid}",
+        ])
+
+
+def test_initrd_size(initrd: Image) -> None:
+    # The fallback value is for CentOS and related distributions.
+    maxsize = 1024**2 * {
+        Distribution.fedora: 46,
+        Distribution.debian: 36,
+        Distribution.ubuntu: 32,
+        Distribution.arch: 47,
+        Distribution.opensuse: 36,
+    }.get(initrd.distribution, 48)
+
+    assert (Path(initrd.output_dir.name) / "initrd").stat().st_size <= maxsize
diff --git a/tests/test_sysext.py b/tests/test_sysext.py
new file mode 100644 (file)
index 0000000..b7f3cf6
--- /dev/null
@@ -0,0 +1,32 @@
+# SPDX-License-Identifier: LGPL-2.1+
+
+from pathlib import Path
+
+import pytest
+
+from . import Image
+
+pytestmark = pytest.mark.integration
+
+
+def test_sysext() -> None:
+    with Image(
+        options=[
+            "--incremental",
+            "--clean-package-metadata=no",
+            "--format=directory",
+        ],
+    ) as image:
+        image.build()
+
+        with Image(
+            options=[
+                "--directory", "",
+                "--base-tree", Path(image.output_dir.name) / "image",
+                "--overlay",
+                "--package=dnsmasq",
+                "--format=disk",
+            ],
+        ) as sysext:
+            sysext.build()
+