From: Lennart Poettering Date: Fri, 17 Apr 2026 13:01:00 +0000 (+0200) Subject: ci: add integration test for new bootctl functionality X-Git-Url: http://git.ipfire.org/gitweb.cgi?a=commitdiff_plain;h=c68b8ff4d192b47d1b99a52b6217c1dc3f7a39ea;p=thirdparty%2Fsystemd.git ci: add integration test for new bootctl functionality --- diff --git a/test/units/TEST-87-AUX-UTILS-VM.bootctl.sh b/test/units/TEST-87-AUX-UTILS-VM.bootctl.sh index 668c0cfac45..90daaf52ead 100755 --- a/test/units/TEST-87-AUX-UTILS-VM.bootctl.sh +++ b/test/units/TEST-87-AUX-UTILS-VM.bootctl.sh @@ -397,4 +397,275 @@ testcase_install_varlink() { bootctl is-installed } +cleanup_link() { + if [[ -n "${LINK_WORKDIR:-}" ]]; then + rm -rf "$LINK_WORKDIR" + unset LINK_WORKDIR + fi + restore_esp +} + +testcase_bootctl_link() { + if ! command -v ukify >/dev/null; then + echo "ukify not found, skipping." + return 0 + fi + + backup_esp + LINK_WORKDIR="$(mktemp --directory /tmp/test-bootctl-link.XXXXXXXXXX)" + trap cleanup_link RETURN ERR + + # Ensure loader/entries directory is present + bootctl install --make-entry-directory=yes + + local ESP + ESP="$(bootctl --print-esp-path)" + + # Build a minimal UKI via ukify. The .linux content does not need to be a + # real kernel — bootctl link only requires a valid PE with .osrel (and the + # systemd-stub SBAT marker that pe_is_uki() checks for). + cat >"$LINK_WORKDIR/os-release" <<'EOF' +ID=testos +NAME="Test OS" +PRETTY_NAME="Test OS" +EOF + echo "fake-kernel" >"$LINK_WORKDIR/vmlinuz" + echo "fake-initrd" >"$LINK_WORKDIR/initrd" + echo "fake-sysext-data" >"$LINK_WORKDIR/hello.sysext.raw" + echo "fake-confext-data" >"$LINK_WORKDIR/hello.confext.raw" + echo "fake-credential" >"$LINK_WORKDIR/hello.cred" + + ukify build \ + --linux "$LINK_WORKDIR/vmlinuz" \ + --initrd "$LINK_WORKDIR/initrd" \ + --os-release "@$LINK_WORKDIR/os-release" \ + --uname "1.2.3-testkernel" \ + --cmdline "quiet" \ + --output "$LINK_WORKDIR/testuki.efi" + + # Pin an explicit entry token so the resulting filenames are deterministic + local TOKEN="systemdtest" + local BOOTCTL=(bootctl "--entry-token=literal:$TOKEN") + + # --- Test 1: basic link/unlink --- + "${BOOTCTL[@]}" link "$LINK_WORKDIR/testuki.efi" + + # Exactly one entry file should exist, named "${TOKEN}-commit_1.conf" + local ENTRY="$ESP/loader/entries/${TOKEN}-commit_1.conf" + test -f "$ENTRY" + test -f "$ESP/$TOKEN/testuki.efi" + + # Verify the entry file contents + grep "^title " "$ENTRY" >/dev/null + grep "^uki /${TOKEN}/testuki.efi\$" "$ENTRY" >/dev/null + grep "^version 1\$" "$ENTRY" >/dev/null + + # Make sure bootctl list sees it + bootctl list --json=short | grep -F "${TOKEN}-commit_1.conf" >/dev/null + + # Remove it again using the ID (entry IDs include the .conf suffix) + "${BOOTCTL[@]}" unlink "${TOKEN}-commit_1.conf" + test ! -e "$ENTRY" + test ! -e "$ESP/$TOKEN/testuki.efi" + + # --- Test 2: link with --entry-title/--entry-version/--entry-commit/--tries-left --- + "${BOOTCTL[@]}" link "$LINK_WORKDIR/testuki.efi" \ + --entry-title="My Funky Entry" \ + --entry-version="9.8.7" \ + --entry-commit=42 \ + --tries-left=3 + + ENTRY="$ESP/loader/entries/${TOKEN}-commit_42.9.8.7+3.conf" + test -f "$ENTRY" + test -f "$ESP/$TOKEN/testuki.efi" + + grep "^title My Funky Entry\$" "$ENTRY" >/dev/null + grep "^version 42.9.8.7\$" "$ENTRY" >/dev/null + grep "^uki /${TOKEN}/testuki.efi\$" "$ENTRY" >/dev/null + + # Unlink using the ID (the tries counter "+3" is stripped from the canonical ID) + "${BOOTCTL[@]}" unlink "${TOKEN}-commit_42.9.8.7.conf" + test ! -e "$ENTRY" + test ! -e "$ESP/$TOKEN/testuki.efi" + + # --- Test 3: link with extras (-X and --extra=) --- + "${BOOTCTL[@]}" link "$LINK_WORKDIR/testuki.efi" \ + --entry-commit=50 \ + -X "$LINK_WORKDIR/hello.sysext.raw" \ + --extra="$LINK_WORKDIR/hello.confext.raw" \ + -X "$LINK_WORKDIR/hello.cred" + + ENTRY="$ESP/loader/entries/${TOKEN}-commit_50.conf" + test -f "$ENTRY" + test -f "$ESP/$TOKEN/testuki.efi" + test -f "$ESP/$TOKEN/hello.sysext.raw" + test -f "$ESP/$TOKEN/hello.confext.raw" + test -f "$ESP/$TOKEN/hello.cred" + + grep "^extra /${TOKEN}/hello.sysext.raw\$" "$ENTRY" >/dev/null + grep "^extra /${TOKEN}/hello.confext.raw\$" "$ENTRY" >/dev/null + grep "^extra /${TOKEN}/hello.cred\$" "$ENTRY" >/dev/null + + # Unlink must also clean up the extra resources + "${BOOTCTL[@]}" unlink "${TOKEN}-commit_50.conf" + test ! -e "$ENTRY" + test ! -e "$ESP/$TOKEN/testuki.efi" + test ! -e "$ESP/$TOKEN/hello.sysext.raw" + test ! -e "$ESP/$TOKEN/hello.confext.raw" + test ! -e "$ESP/$TOKEN/hello.cred" + + # --- Test 4: --oldest drops the lowest commit first --- + "${BOOTCTL[@]}" link "$LINK_WORKDIR/testuki.efi" --entry-commit=10 + "${BOOTCTL[@]}" link "$LINK_WORKDIR/testuki.efi" --entry-commit=20 + "${BOOTCTL[@]}" link "$LINK_WORKDIR/testuki.efi" --entry-commit=30 + + test -f "$ESP/loader/entries/${TOKEN}-commit_10.conf" + test -f "$ESP/loader/entries/${TOKEN}-commit_20.conf" + test -f "$ESP/loader/entries/${TOKEN}-commit_30.conf" + test -f "$ESP/$TOKEN/testuki.efi" + + "${BOOTCTL[@]}" unlink --oldest=yes + test ! -e "$ESP/loader/entries/${TOKEN}-commit_10.conf" + test -f "$ESP/loader/entries/${TOKEN}-commit_20.conf" + test -f "$ESP/loader/entries/${TOKEN}-commit_30.conf" + test -f "$ESP/$TOKEN/testuki.efi" + + "${BOOTCTL[@]}" unlink --oldest=yes + test ! -e "$ESP/loader/entries/${TOKEN}-commit_20.conf" + test -f "$ESP/loader/entries/${TOKEN}-commit_30.conf" + test -f "$ESP/$TOKEN/testuki.efi" + + # --- Test 5: --dry-run leaves everything in place --- + "${BOOTCTL[@]}" --dry-run unlink "${TOKEN}-commit_30.conf" + test -f "$ESP/loader/entries/${TOKEN}-commit_30.conf" + test -f "$ESP/$TOKEN/testuki.efi" + + # Actually remove it now + "${BOOTCTL[@]}" unlink "${TOKEN}-commit_30.conf" + test ! -e "$ESP/loader/entries/${TOKEN}-commit_30.conf" + test ! -e "$ESP/$TOKEN/testuki.efi" + + # --- Test 6: invalid combinations are rejected --- + # Neither an ID nor --oldest + (! "${BOOTCTL[@]}" unlink) + # Both an ID and --oldest + (! "${BOOTCTL[@]}" unlink --oldest=yes "${TOKEN}-commit_1.conf") + + # --- Test 7: refusing to link when --keep-free cannot be satisfied --- + (! "${BOOTCTL[@]}" link "$LINK_WORKDIR/testuki.efi" --entry-commit=99 --keep-free=1T) + test ! -e "$ESP/loader/entries/${TOKEN}-commit_99.conf" + + # --- Test 8: refusing to re-link the same commit number --- + "${BOOTCTL[@]}" link "$LINK_WORKDIR/testuki.efi" --entry-commit=77 + (! "${BOOTCTL[@]}" link "$LINK_WORKDIR/testuki.efi" --entry-commit=77) + "${BOOTCTL[@]}" unlink "${TOKEN}-commit_77.conf" + + # --- Test 9: passing a non-UKI is rejected --- + (! "${BOOTCTL[@]}" link "$LINK_WORKDIR/vmlinuz") + + # === Varlink coverage === + # + # Exercise io.systemd.BootControl.Link/Unlink by forking bootctl as a + # varlink server via 'varlinkctl call '. Note the Varlink schema + # has no way to supply a literal entry token (unlike --entry-token= on + # the command line), so the token is chosen by bootctl from + # machine-id/os-release — we recover it from the returned id. + local BOOTCTL_BIN vreply vid vtoken + BOOTCTL_BIN="$(type -p bootctl)" + + # --- Test 10: Link + Unlink via varlink --- + vreply="$(varlinkctl call --json=short \ + --push-fd="$LINK_WORKDIR/testuki.efi" \ + "$BOOTCTL_BIN" io.systemd.BootControl.Link \ + '{"kernelFilename":"vluki.efi","kernelFileDescriptor":0}')" + vid="$(echo "$vreply" | jq -r '.ids[0]')" + test -n "$vid" + test "$vid" != "null" + vtoken="${vid%%-commit_*}" + test -n "$vtoken" + + test -f "$ESP/loader/entries/$vid" + test -f "$ESP/$vtoken/vluki.efi" + grep "^uki /$vtoken/vluki.efi\$" "$ESP/loader/entries/$vid" >/dev/null + + varlinkctl call --quiet "$BOOTCTL_BIN" io.systemd.BootControl.Unlink \ + "{\"id\":\"$vid\"}" + test ! -e "$ESP/loader/entries/$vid" + test ! -e "$ESP/$vtoken/vluki.efi" + + # --- Test 11: Link with entryTitle/entryVersion/entryCommit/triesLeft + extraFiles via varlink --- + vreply="$(varlinkctl call --json=short \ + --push-fd="$LINK_WORKDIR/testuki.efi" \ + --push-fd="$LINK_WORKDIR/hello.sysext.raw" \ + --push-fd="$LINK_WORKDIR/hello.cred" \ + "$BOOTCTL_BIN" io.systemd.BootControl.Link \ + '{"kernelFilename":"vluki2.efi","kernelFileDescriptor":0,"entryTitle":"Varlink Title","entryVersion":"2.3.4","entryCommit":111,"triesLeft":2,"extraFiles":[{"filename":"hello.sysext.raw","fileDescriptor":1},{"filename":"hello.cred","fileDescriptor":2}]}')" + vid="$(echo "$vreply" | jq -r '.ids[0]')" + # The returned id has the tries counter ("+2") stripped + assert_eq "$vid" "$vtoken-commit_111.2.3.4.conf" + # The on-disk entry filename includes the tries counter + local VENTRY="$ESP/loader/entries/$vtoken-commit_111.2.3.4+2.conf" + test -f "$VENTRY" + test -f "$ESP/$vtoken/vluki2.efi" + test -f "$ESP/$vtoken/hello.sysext.raw" + test -f "$ESP/$vtoken/hello.cred" + + grep "^title Varlink Title\$" "$VENTRY" >/dev/null + grep "^version 111.2.3.4\$" "$VENTRY" >/dev/null + grep "^extra /$vtoken/hello.sysext.raw\$" "$VENTRY" >/dev/null + grep "^extra /$vtoken/hello.cred\$" "$VENTRY" >/dev/null + + varlinkctl call --quiet "$BOOTCTL_BIN" io.systemd.BootControl.Unlink \ + "{\"id\":\"$vid\"}" + test ! -e "$VENTRY" + test ! -e "$ESP/$vtoken/vluki2.efi" + test ! -e "$ESP/$vtoken/hello.sysext.raw" + test ! -e "$ESP/$vtoken/hello.cred" + + # --- Test 12: Unlink oldest via varlink --- + local c + for c in 210 220 230; do + varlinkctl call --quiet \ + --push-fd="$LINK_WORKDIR/testuki.efi" \ + "$BOOTCTL_BIN" io.systemd.BootControl.Link \ + "{\"kernelFilename\":\"vluki3.efi\",\"kernelFileDescriptor\":0,\"entryCommit\":$c}" + done + test -f "$ESP/loader/entries/$vtoken-commit_210.conf" + test -f "$ESP/loader/entries/$vtoken-commit_220.conf" + test -f "$ESP/loader/entries/$vtoken-commit_230.conf" + + varlinkctl call --quiet "$BOOTCTL_BIN" io.systemd.BootControl.Unlink \ + '{"oldest":true}' + test ! -e "$ESP/loader/entries/$vtoken-commit_210.conf" + test -f "$ESP/loader/entries/$vtoken-commit_220.conf" + test -f "$ESP/loader/entries/$vtoken-commit_230.conf" + test -f "$ESP/$vtoken/vluki3.efi" + + # Clean up remaining entries + varlinkctl call --quiet "$BOOTCTL_BIN" io.systemd.BootControl.Unlink \ + "{\"id\":\"$vtoken-commit_220.conf\"}" + varlinkctl call --quiet "$BOOTCTL_BIN" io.systemd.BootControl.Unlink \ + "{\"id\":\"$vtoken-commit_230.conf\"}" + test ! -e "$ESP/loader/entries/$vtoken-commit_220.conf" + test ! -e "$ESP/loader/entries/$vtoken-commit_230.conf" + test ! -e "$ESP/$vtoken/vluki3.efi" + + # --- Test 13: Link with a non-UKI via varlink returns InvalidKernelImage --- + varlinkctl call --quiet \ + --push-fd="$LINK_WORKDIR/vmlinuz" \ + --graceful=io.systemd.BootControl.InvalidKernelImage \ + "$BOOTCTL_BIN" io.systemd.BootControl.Link \ + '{"kernelFilename":"notauki.efi","kernelFileDescriptor":0}' + + # --- Test 14: Unlink with invalid argument combinations is rejected --- + # Both id and oldest=true + (! varlinkctl call "$BOOTCTL_BIN" io.systemd.BootControl.Unlink \ + '{"id":"foo.conf","oldest":true}') + # Neither id nor oldest + (! varlinkctl call "$BOOTCTL_BIN" io.systemd.BootControl.Unlink '{}') + # Invalid id characters (e.g. a glob) + (! varlinkctl call "$BOOTCTL_BIN" io.systemd.BootControl.Unlink \ + '{"id":"foo*.conf"}') +} + run_testcases