From: Daan De Meyer Date: Mon, 4 Dec 2023 07:12:51 +0000 (+0100) Subject: mkosi-initrd: Port tests from old repository X-Git-Tag: v20~114^2 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=refs%2Fpull%2F2120%2Fhead;p=thirdparty%2Fmkosi.git mkosi-initrd: Port tests from old repository 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. --- diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 13c2c7346..bf8452f86 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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 < 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 diff --git a/tests/test_initrd.py b/tests/test_initrd.py index e89602eca..683b139ab 100644 --- a/tests/test_initrd.py +++ b/tests/test_initrd.py @@ -1,31 +1,162 @@ # 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 index 000000000..b7f3cf6aa --- /dev/null +++ b/tests/test_sysext.py @@ -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() +