ci: testcase for new auto-pick mode of sysinstall

This commit is contained in:
Lennart Poettering
2026-09-23 10:14:25 +02:00
parent 0bb2f138f4
commit 236e7d8a4c
@@ -46,6 +46,12 @@ CRED_VALUE_BASE64=$(echo -n "$CRED_VALUE" | base64 -w0)
cleanup() {
set +e
# Stop the installer first: it might still be writing to the devices and files removed below.
if [[ -n "${UNIT:-}" ]]; then
systemctl stop "$UNIT"
systemctl reset-failed "$UNIT"
UNIT=""
fi
if [[ "$MOUNTED" -eq 1 ]]; then
umount -R "$WORKDIR/mnt"
MOUNTED=0
@@ -54,9 +60,92 @@ cleanup() {
systemd-dissect --detach "$LOOPDEV"
LOOPDEV=""
fi
if [[ -n "${TARGETDEV:-}" ]]; then
systemd-dissect --detach "$TARGETDEV"
TARGETDEV=""
fi
if [[ -n "${TARGETDEV2:-}" ]]; then
systemd-dissect --detach "$TARGETDEV2"
TARGETDEV2=""
fi
rm -rf "$WORKDIR"
}
check_device_auto_environment() {
# Safety check for the --device-auto testcases: systemd-sysinstall must be able to determine the disk
# the running system is booted from (it refuses the automatic pick otherwise), and no candidate disk
# may be around initially, since the testcases rely on the disks they attach themselves being the only
# candidates — otherwise the automatic pick would either fail or – worse – install to some unexpected
# disk. Query the candidate list the same way systemd-sysinstall does (i.e. with the very same filter
# set, except that systemd-sysinstall additionally ignores loopback devices unless
# $SYSTEMD_SYSINSTALL_PERMIT_AUTO_TARGET_LOOP is set, which the testcases do), instead of approximating
# the filters here.
#
# Returns non-zero (and the testcase is to be skipped) only if the booted disk cannot be determined,
# i.e. if the feature is genuinely unusable in this environment. Stray candidate disks on the other
# hand mean the test environment is broken (e.g. a loopback device leaked by an earlier testcase),
# which must not silently turn the --device-auto testcases into no-ops, hence fail hard in that case.
local out
if ! bootctl -RR >/dev/null; then
echo "Cannot determine the disk the system is booted from." >&2
return 1
fi
if out="$(varlinkctl call --more "exec:$(command -v systemd-repart)" io.systemd.Repart.ListCandidateDevices '{"ignoreRoot":true,"ignoreEmpty":true}' 2>&1)"; then
echo "Unexpected candidate disks present, test environment is broken: $out" >&2
exit 1
fi
if [[ "$out" != *"io.systemd.Repart.NoCandidateDevices"* ]]; then
echo "Failed to enumerate candidate disks: $out" >&2
exit 1
fi
}
start_device_auto_installer() {
# Starts the installer as a notify service with --device-auto and the specified settle timeout (plus
# any further arguments). systemd-sysinstall sends READY=1 once its device monitor is set up, hence
# systemd-run only returns once we can be sure that a disk appearing from now on will be noticed.
# Loopback devices are not considered as candidates by default, hence explicitly permit them, the
# target disks of the testcases are loopback devices.
local settle_timeout="${1:?}"
shift
UNIT="sysinstall-device-auto-$RANDOM.service"
systemd-run \
--unit="$UNIT" \
--service-type=notify \
--property=RemainAfterExit=yes \
--setenv=SYSTEMD_SYSINSTALL_PERMIT_AUTO_TARGET_LOOP=1 \
--quiet \
-- \
systemd-sysinstall \
--welcome=no \
--chrome=no \
--confirm=no \
--summary=no \
--erase=yes \
--variables=no \
--reboot=no \
--mute-console=no \
--copy-locale=no \
--copy-keymap=no \
--copy-timezone=no \
--device-auto \
--device-auto-timeout="$settle_timeout" \
"$@"
}
wait_for_device_auto_installer() {
# Waits for the installer started by start_device_auto_installer() to finish, and dumps its output, so
# that it ends up in the test log whatever happened — hence don't abort right away if the wait times
# out, the assertions of the caller will catch that case after the output has been dumped.
timeout "${1:?}" bash -c "while [[ \"\$(systemctl show -P SubState '$UNIT')\" == running ]]; do sleep 1; done" || :
journalctl --sync
journalctl --no-pager -u "$UNIT"
}
create_fake_os_source_tree() {
# 1) Build a small fake "OS source" tree. systemd-sysinstall picks this up via
# the repart.sysinstall.d definitions: CopyFiles= seeds the new root
@@ -222,4 +311,306 @@ testcase_sysinstall_varlink_basic() {
validate_image
}
testcase_sysinstall_device_auto() {
WORKDIR="$(mktemp --directory /tmp/test-sysinstall.XXXXXXXXXX)"
LOOPDEV=""
TARGETDEV=""
UNIT=""
MOUNTED=0
echo "WORKDIR=$WORKDIR"
trap cleanup RETURN
if ! check_device_auto_environment; then
echo "Environment not suitable for --device-auto tests, skipping." >&2
return 0
fi
create_fake_os_source_tree
# Put a bare file system on the target image, so that systemd-dissect --attach accepts it as an
# image later on (an all-zero file is not a valid image). Don't put a partition table on it though:
# the resulting loopback device shall have no partition block devices, so that nothing is in the
# way when systemd-repart adds the new ones. systemd-sysinstall erases the file system anyway.
mkfs.ext4 -q -F "$WORKDIR/target.img"
# Start the installer while no candidate disk exists yet.
start_device_auto_installer 30s \
--set-credential="marker:$CRED_VALUE" \
--kernel="$WORKDIR/testuki.efi" \
--definitions="$DEFS"
# Only now, 5s later, make the target disk appear: a partition-scanning loopback device backed by
# the target image. It's the sole candidate disk in the VM, hence it must be picked.
sleep 5
# The installer must still be waiting at this point, there was nothing to install to so far.
assert_eq "$(systemctl show -P SubState "$UNIT")" "running"
TARGETDEV="$(systemd-dissect --attach "$WORKDIR/target.img")"
udevadm wait --settle "$TARGETDEV"
# Wait for the installation to finish, and check that it succeeded.
wait_for_device_auto_installer 180
assert_eq "$(systemctl show -P SubState "$UNIT")" "exited"
assert_eq "$(systemctl show -P Result "$UNIT")" "success"
# Release our loopback device again, validate_image attaches the image on its own.
systemd-dissect --detach "$TARGETDEV"
TARGETDEV=""
validate_image
}
testcase_sysinstall_device_auto_two_candidates() {
WORKDIR="$(mktemp --directory /tmp/test-sysinstall.XXXXXXXXXX)"
LOOPDEV=""
TARGETDEV=""
TARGETDEV2=""
UNIT=""
MOUNTED=0
echo "WORKDIR=$WORKDIR"
trap cleanup RETURN
if ! check_device_auto_environment; then
echo "Environment not suitable for --device-auto tests, skipping." >&2
return 0
fi
create_fake_os_source_tree
# Attach two candidate disks, both carrying a bare file system (see testcase_sysinstall_device_auto
# for why), so that we can verify below that neither of them was touched.
mkfs.ext4 -q -F "$WORKDIR/target.img"
truncate -s 512M "$WORKDIR/target2.img"
mkfs.ext4 -q -F "$WORKDIR/target2.img"
TARGETDEV="$(systemd-dissect --attach "$WORKDIR/target.img")"
TARGETDEV2="$(systemd-dissect --attach "$WORKDIR/target2.img")"
udevadm wait --settle "$TARGETDEV" "$TARGETDEV2"
# With two candidate disks present the automatic pick must refuse right away, i.e. well before the
# settle timeout expires, and without installing anywhere.
local start="$SECONDS" rc=0 out
out="$(SYSTEMD_SYSINSTALL_PERMIT_AUTO_TARGET_LOOP=1 timeout 60 systemd-sysinstall \
--welcome=no \
--chrome=no \
--confirm=no \
--summary=no \
--erase=yes \
--variables=no \
--reboot=no \
--mute-console=no \
--copy-locale=no \
--copy-keymap=no \
--copy-timezone=no \
--set-credential="marker:$CRED_VALUE" \
--kernel="$WORKDIR/testuki.efi" \
--definitions="$DEFS" \
--device-auto \
--device-auto-timeout=30s 2>&1)" || rc=$?
echo "$out"
assert_neq "$rc" 0
assert_neq "$rc" 124
assert_in "Multiple candidate block devices found" "$out"
assert_le "$((SECONDS - start))" 10
# Neither disk may have been touched: the bare file systems must have survived unmodified, in
# particular no partition table may have been written.
assert_eq "$(blkid -p -o value -s TYPE "$TARGETDEV")" "ext4"
assert_eq "$(blkid -p -o value -s TYPE "$TARGETDEV2")" "ext4"
}
testcase_sysinstall_device_auto_late_second_candidate() {
WORKDIR="$(mktemp --directory /tmp/test-sysinstall.XXXXXXXXXX)"
LOOPDEV=""
TARGETDEV=""
TARGETDEV2=""
UNIT=""
MOUNTED=0
echo "WORKDIR=$WORKDIR"
trap cleanup RETURN
if ! check_device_auto_environment; then
echo "Environment not suitable for --device-auto tests, skipping." >&2
return 0
fi
# Same as testcase_sysinstall_device_auto_two_candidates, but the second disk shows up only while the
# installer is already waiting for the settle timeout, i.e. the refusal must come via the uevent path
# rather than the initial enumeration.
truncate -s 512M "$WORKDIR/target.img"
mkfs.ext4 -q -F "$WORKDIR/target.img"
truncate -s 512M "$WORKDIR/target2.img"
mkfs.ext4 -q -F "$WORKDIR/target2.img"
start_device_auto_installer 30s
TARGETDEV="$(systemd-dissect --attach "$WORKDIR/target.img")"
udevadm wait --settle "$TARGETDEV"
sleep 5
# A single candidate must not have been picked yet, the settle timeout is far from expired.
assert_eq "$(systemctl show -P SubState "$UNIT")" "running"
# Now make the second disk appear. This must make the installer refuse right away.
local start="$SECONDS" out
TARGETDEV2="$(systemd-dissect --attach "$WORKDIR/target2.img")"
udevadm wait --settle "$TARGETDEV2"
wait_for_device_auto_installer 60
assert_le "$((SECONDS - start))" 10
assert_neq "$(systemctl show -P SubState "$UNIT")" "running"
assert_neq "$(systemctl show -P Result "$UNIT")" "success"
out="$(journalctl --no-pager -o cat -u "$UNIT")"
assert_in "Multiple candidate block devices found" "$out"
# Neither disk may have been touched.
assert_eq "$(blkid -p -o value -s TYPE "$TARGETDEV")" "ext4"
assert_eq "$(blkid -p -o value -s TYPE "$TARGETDEV2")" "ext4"
}
testcase_sysinstall_device_auto_candidate_disappears() {
WORKDIR="$(mktemp --directory /tmp/test-sysinstall.XXXXXXXXXX)"
LOOPDEV=""
TARGETDEV=""
UNIT=""
MOUNTED=0
echo "WORKDIR=$WORKDIR"
trap cleanup RETURN
if ! check_device_auto_environment; then
echo "Environment not suitable for --device-auto tests, skipping." >&2
return 0
fi
# A candidate that appears and disappears again while the installer is waiting must be forgotten
# about: the installer must not pick the stale device node, but restart the settle clock and
# eventually fail for lack of a candidate.
truncate -s 512M "$WORKDIR/target.img"
mkfs.ext4 -q -F "$WORKDIR/target.img"
start_device_auto_installer 10s
TARGETDEV="$(systemd-dissect --attach "$WORKDIR/target.img")"
udevadm wait --settle "$TARGETDEV"
sleep 3
assert_eq "$(systemctl show -P SubState "$UNIT")" "running"
local start="$SECONDS" out
systemd-dissect --detach "$TARGETDEV"
TARGETDEV=""
# The removal restarts the settle clock, hence the failure may come no earlier than a full settle
# timeout after the disk disappeared.
wait_for_device_auto_installer 60
assert_ge "$((SECONDS - start))" 10
assert_neq "$(systemctl show -P SubState "$UNIT")" "running"
assert_neq "$(systemctl show -P Result "$UNIT")" "success"
out="$(journalctl --no-pager -o cat -u "$UNIT")"
assert_in "disappeared" "$out"
assert_in "No suitable block device" "$out"
}
testcase_sysinstall_device_auto_no_candidate() {
if ! check_device_auto_environment; then
echo "Environment not suitable for --device-auto tests, skipping." >&2
return 0
fi
# With no candidate disk around the automatic pick must fail once the settle timeout expires — not
# before it expires, and not by hanging around forever either.
local start="$SECONDS" rc=0 out
out="$(timeout 60 systemd-sysinstall \
--welcome=no \
--chrome=no \
--confirm=no \
--summary=no \
--erase=yes \
--variables=no \
--reboot=no \
--mute-console=no \
--device-auto \
--device-auto-timeout=5s 2>&1)" || rc=$?
echo "$out"
assert_neq "$rc" 0
assert_neq "$rc" 124
assert_in "No suitable block device" "$out"
assert_ge "$((SECONDS - start))" 5
}
testcase_sysinstall_device_auto_loop_ignored() {
WORKDIR="$(mktemp --directory /tmp/test-sysinstall.XXXXXXXXXX)"
LOOPDEV=""
TARGETDEV=""
UNIT=""
MOUNTED=0
echo "WORKDIR=$WORKDIR"
trap cleanup RETURN
if ! check_device_auto_environment; then
echo "Environment not suitable for --device-auto tests, skipping." >&2
return 0
fi
# Unless $SYSTEMD_SYSINSTALL_PERMIT_AUTO_TARGET_LOOP is set, loopback devices must not be considered as
# candidates, i.e. a lone loopback disk must not be picked, and the automatic pick must fail once the
# settle timeout expires, exactly as if no disk was around.
truncate -s 512M "$WORKDIR/target.img"
mkfs.ext4 -q -F "$WORKDIR/target.img"
TARGETDEV="$(systemd-dissect --attach "$WORKDIR/target.img")"
udevadm wait --settle "$TARGETDEV"
local start="$SECONDS" rc=0 out
out="$(timeout 60 systemd-sysinstall \
--welcome=no \
--chrome=no \
--confirm=no \
--summary=no \
--erase=yes \
--variables=no \
--reboot=no \
--mute-console=no \
--device-auto \
--device-auto-timeout=5s 2>&1)" || rc=$?
echo "$out"
assert_neq "$rc" 0
assert_neq "$rc" 124
assert_in "No suitable block device" "$out"
assert_ge "$((SECONDS - start))" 5
# The loopback disk must not have been touched.
assert_eq "$(blkid -p -o value -s TYPE "$TARGETDEV")" "ext4"
}
testcase_sysinstall_device_auto_argument_errors() {
local rc out
# --device-auto cannot be combined with an explicitly specified target device...
rc=0
out="$(timeout 30 systemd-sysinstall --device-auto /dev/null 2>&1)" || rc=$?
echo "$out"
assert_neq "$rc" 0
assert_neq "$rc" 124
assert_in "Cannot combine --device-auto with an explicitly specified device" "$out"
# ...and the settle timeout cannot be infinite.
rc=0
out="$(timeout 30 systemd-sysinstall --device-auto --device-auto-timeout=infinity 2>&1)" || rc=$?
echo "$out"
assert_neq "$rc" 0
assert_neq "$rc" 124
assert_in "--device-auto-timeout= cannot be infinite" "$out"
}
run_testcases