]> git.ipfire.org Git - thirdparty/systemd.git/commitdiff
ci: add integration test for new bootctl functionality
authorLennart Poettering <lennart@amutable.com>
Fri, 17 Apr 2026 13:01:00 +0000 (15:01 +0200)
committerLennart Poettering <lennart@amutable.com>
Fri, 1 May 2026 05:10:31 +0000 (07:10 +0200)
test/units/TEST-87-AUX-UTILS-VM.bootctl.sh

index 668c0cfac45804b61386b57c014bd6e8be68b8a3..90daaf52eadc2e1320d1ce123c0a455343e7644e 100755 (executable)
@@ -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 <binary>'. 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