Files
owui-tools/tests/test_vm_freeze.sh
T
2026-09-15 15:24:03 -04:00

284 lines
13 KiB
Bash
Executable File

#!/bin/bash
# =====================================================================
# The freeze must happen, and must happen first.
#
# ./tests/test_vm_freeze.sh
#
# WHY THIS FILE EXISTS
#
# Freezing a base image is one `chmod 0444`, which makes it look like
# the kind of thing that cannot go wrong. It went wrong: a later line in
# the same function used `du --apparent-size=0`, which is not valid -
# --apparent-size takes no argument - and under `set -euo pipefail` that
# killed the script before the chmod ran. Every clean shutdown left the
# base image WRITABLE, and the only sign was
# "Control process exited, status=1/FAILURE" in the journal.
#
# So this runs the real script against a scratch directory and checks
# the mode afterwards, rather than checking that the line is present.
# =====================================================================
set -uo pipefail
PREPARE=/usr/local/libexec/owuivm-maint-prepare
fails=0
ok() { printf ' ok %s\n' "$*"; }
bad() { printf ' FAIL %s\n' "$*"; fails=$((fails+1)); }
work=$(mktemp -d); trap 'rm -rf "$work"' EXIT
base="$work/base"; isos="$work/iso"
mkdir -p "$base" "$isos"
# EVERY DIRECTORY THIS TOUCHES IS ITS OWN, INCLUDING THE RUN DIRECTORY.
#
# It used to use the real /run/owuivm/<constant session id>. Run as root,
# that left a root:root directory behind, and the operator's next
# maintenance boot died in ExecStartPre on "cp: cannot remove
# .../OVMF_VARS.fd: Permission denied" - a tool that looked broken
# because a test had littered.
run_dir="$work/run"
mkdir -p "$run_dir"
run_verb() {
OWUIVM_BASE_DIR="$base" OWUIVM_ISO_DIR="$isos" OWUIVM_OVERLAY_DIR="$work" \
OWUIVM_RUN_DIR="$run_dir" \
"$PREPARE" ubuntu "$1" 2>&1
}
# `close` is what the unit runs on the way out; `freeze` and `thaw` are
# the operator's own buttons. They used to be one verb, which is why
# looking at an image meant unfreezing it.
close() { run_verb close; }
freeze() { run_verb freeze; }
thaw() { run_verb thaw; }
echo "== a disk with an OS on it"
# 600 MB: over the "something was installed" threshold.
dd if=/dev/zero of="$base/ubuntu.qcow2" bs=1M count=600 status=none
cp /usr/share/OVMF/OVMF_VARS_4M.fd "$base/ubuntu-OVMF_VARS.fd"
chmod 0664 "$base/ubuntu.qcow2" "$base/ubuntu-OVMF_VARS.fd"
echo "$isos/ubuntu-x.iso" > "$base/ubuntu.install"
out=$(freeze)
mode=$(stat -c %a "$base/ubuntu.qcow2")
[ "$mode" = "444" ] && ok "the disk is frozen (0444)" || bad "the disk is $mode, not 444 — $out"
out=$(close)
# The marker is SUPPOSED to survive now. It used to be removed once the
# disk passed half a gigabyte, which declared a partial install finished,
# took its media away, and left a disk that was neither blank enough to
# restart nor complete enough to boot. Size cannot tell "installed" from
# "interrupted halfway", so nothing guesses: the boot order in
# owuivm-launch tries the disk first and falls through to the media, and
# detaching it is an explicit act by the operator.
[ -f "$base/ubuntu.install" ] && ok "the install marker survives a freeze (nothing guesses)" \
|| bad "the install marker was removed - a partial install cannot be resumed"
[ -f "$base/ubuntu.sha256" ] && ok "a checksum was recorded" || bad "no checksum recorded"
echo
echo "== a disk nothing was installed on yet"
rm -rf "$base"/*; mkdir -p "$base"
dd if=/dev/zero of="$base/ubuntu.qcow2" bs=1M count=1 status=none
cp /usr/share/OVMF/OVMF_VARS_4M.fd "$base/ubuntu-OVMF_VARS.fd"
chmod 0664 "$base/ubuntu.qcow2" "$base/ubuntu-OVMF_VARS.fd"
echo "$isos/ubuntu-x.iso" > "$base/ubuntu.install"
out=$(freeze)
mode=$(stat -c %a "$base/ubuntu.qcow2")
[ "$mode" = "444" ] && ok "the disk is frozen even when nothing was installed" \
|| bad "the disk is $mode, not 444 — $out"
out=$(close)
# An interrupted install must be resumable; removing the marker here is
# what left a blank disk with no installer attached and no way back.
[ -f "$base/ubuntu.install" ] && ok "the install marker survived an interrupted install" \
|| bad "the install marker was removed after an interrupted install"
echo
echo "== firmware variables are only kept when there is an OS to keep them for"
rm -rf "$base"/*; mkdir -p "$base"
dd if=/dev/zero of="$base/ubuntu.qcow2" bs=1M count=1 status=none
printf 'ORIGINAL' > "$base/ubuntu-OVMF_VARS.fd"
chmod 0664 "$base/ubuntu.qcow2" "$base/ubuntu-OVMF_VARS.fd"
if true; then
printf 'REWRITTEN-BY-A-BOOT-THAT-INSTALLED-NOTHING' > "$run_dir/OVMF_VARS.fd"
close >/dev/null
if grep -q ORIGINAL "$base/ubuntu-OVMF_VARS.fd" 2>/dev/null; then
ok "a boot that installed nothing did not rewrite the frozen firmware variables"
else
bad "firmware variables from an empty boot were carried into the base image"
fi
rm -f "$run_dir/OVMF_VARS.fd"
fi
echo
echo "== the freeze survives a broken step after it"
rm -rf "$base"/*; mkdir -p "$base"
dd if=/dev/zero of="$base/ubuntu.qcow2" bs=1M count=600 status=none
cp /usr/share/OVMF/OVMF_VARS_4M.fd "$base/ubuntu-OVMF_VARS.fd"
chmod 0664 "$base/ubuntu.qcow2" "$base/ubuntu-OVMF_VARS.fd"
# A directory where a file is expected: the checksum step will fail.
mkdir -p "$base/ubuntu.sha256.previous"
out=$(freeze)
mode=$(stat -c %a "$base/ubuntu.qcow2")
[ "$mode" = "444" ] && ok "still frozen despite a later step failing" \
|| bad "a failure after the chmod undid the freeze — $out"
echo
echo "== a frozen boot leaves nothing behind"
# The point of the whole split: the operator can start a base image to
# see that it still comes up, exactly as the agent will get it, and
# being there cannot change it. OVMF rewrites its boot order on every
# boot, so "unchanged" has to include the firmware variables.
rm -rf "$base"/*; mkdir -p "$base"
dd if=/dev/zero of="$base/ubuntu.qcow2" bs=1M count=600 status=none
printf 'ORIGINAL-FIRMWARE' > "$base/ubuntu-OVMF_VARS.fd"
chmod 0444 "$base/ubuntu.qcow2" "$base/ubuntu-OVMF_VARS.fd"
disk_sum=$(sha256sum "$base/ubuntu.qcow2" | cut -d' ' -f1)
touch "$work/maint-ubuntu.qcow2"
if [ -d "$run_dir" ] || mkdir -p "$run_dir" 2>/dev/null; then
printf 'REWRITTEN-BY-A-LOOK-AT-IT-BOOT' > "$run_dir/OVMF_VARS.fd"
out=$(close)
grep -q ORIGINAL-FIRMWARE "$base/ubuntu-OVMF_VARS.fd" \
&& ok "a frozen boot did not rewrite the firmware variables" \
|| bad "a frozen boot changed the base image's firmware — $out"
[ "$(sha256sum "$base/ubuntu.qcow2" | cut -d' ' -f1)" = "$disk_sum" ] \
&& ok "the base image is byte-identical after a frozen boot" \
|| bad "the base image changed during a frozen boot"
[ -f "$work/maint-ubuntu.qcow2" ] \
&& bad "the throwaway disk survived the boot - its changes would come back" \
|| ok "the throwaway disk was removed"
rm -f "$run_dir/OVMF_VARS.fd"
fi
echo
echo "== the operator's own verbs, which nothing else may do for them"
rm -rf "$base"/*; mkdir -p "$base"
dd if=/dev/zero of="$base/ubuntu.qcow2" bs=1M count=600 status=none
cp /usr/share/OVMF/OVMF_VARS_4M.fd "$base/ubuntu-OVMF_VARS.fd"
chmod 0444 "$base/ubuntu.qcow2" "$base/ubuntu-OVMF_VARS.fd"
out=$(thaw)
[ "$(stat -c %a "$base/ubuntu.qcow2")" = "660" ] \
&& ok "thaw makes the image writable" || bad "thaw left it $(stat -c %a "$base/ubuntu.qcow2") — $out"
# And `close` must NOT put it back. That auto-freeze is what made
# looking at an image mean unfreezing it, and it hid a forgotten thaw;
# the refusal now lives in vmd, where a session cannot start without it.
out=$(close)
[ "$(stat -c %a "$base/ubuntu.qcow2")" = "660" ] \
&& ok "closing an unfrozen boot leaves it unfrozen, and says so" \
|| bad "close re-froze the image behind the operator — $out"
case "$out" in
*WRITABLE*) ok "and the log says the image is still writable" ;;
*) bad "close said nothing about leaving a writable image: $out" ;;
esac
out=$(freeze)
[ "$(stat -c %a "$base/ubuntu.qcow2")" = "444" ] \
&& ok "freeze puts it back" || bad "freeze left it $(stat -c %a "$base/ubuntu.qcow2") — $out"
[ -f "$base/ubuntu.sha256" ] && ok "and re-records the checksum" || bad "no checksum after freeze"
echo
echo "== the desktop tool starts units and touches nothing"
# WHY: the tool created the blank disk, the firmware variables and the
# install marker itself, as the operator. Pressing "Install from ISO" for
# Windows failed with a bare permission error, because `operator` is a
# member of owuivmadm in /etc/group and the desktop SESSION predates the
# usermod - a unix group only reaches a process at its next login, so the
# group exists, `id` agrees, and nothing running has it.
#
# It was wrong a second way that would have surfaced much later: it
# copied the plain OVMF variables for both images, where Windows needs
# the store with Microsoft's keys enrolled or Secure Boot has nothing it
# trusts and setup refuses the machine.
gui=/usr/local/bin/owuivm-maintenance
for forbidden in "qemu-img" "shutil.copyfile" "OVMF_VARS ="; do
grep -q "$forbidden" "$gui" 2>/dev/null \
&& bad "the desktop tool still does '$forbidden' itself - it cannot, and it gets it wrong" \
|| ok "the desktop tool does not do '$forbidden' itself"
done
echo
echo "== what the unit makes for a brand new image"
rm -rf "$base"/*; mkdir -p "$base"
for name in ubuntu windows; do
out=$(OWUIVM_BASE_DIR="$base" OWUIVM_ISO_DIR=/var/lib/owuivm/iso \
OWUIVM_OVERLAY_DIR="$work" OWUIVM_RUN_DIR="$run_dir" \
"$PREPARE" "$name" open 2>&1)
if [ ! -f "$base/$name.qcow2" ]; then
printf ' skip %s: no media in /var/lib/owuivm/iso (%s)\n' "$name" "${out##*$'\n'}"
continue
fi
ok "$name: a blank disk was created"
[ $(( 8#$(stat -c %a "$base/$name.qcow2") & 8#200 )) -ne 0 ] \
&& ok "$name: and it is writable, because an install has to write to it" \
|| bad "$name: the new disk is read-only, so the installer cannot use it"
grep -q '^/var/lib/owuivm/iso/' "$base/$name.install" 2>/dev/null \
&& ok "$name: the media it will boot is recorded ($(basename "$(cat "$base/$name.install")"))" \
|| bad "$name: no install marker, so the installer is never attached"
# The one difference between the two images that is easy to get wrong
# and impossible to see until Windows setup refuses the machine.
case "$name" in
windows) want=/usr/share/OVMF/OVMF_VARS_4M.ms.fd
label="the store with Microsoft's keys enrolled" ;;
*) want=/usr/share/OVMF/OVMF_VARS_4M.fd
label="the plain store" ;;
esac
cmp -s "$base/$name-OVMF_VARS.fd" "$want" \
&& ok "$name: firmware variables are $label" \
|| bad "$name: firmware variables are NOT $label - Secure Boot will not behave"
done
rm -f "$base"/*.install
echo
echo "== and the operator can actually do that explicit act"
# The line above says detaching the media is the operator's decision.
# It was a sentence in a comment for a while and nothing more: the
# operator's only way to make it was to delete a file in a directory
# their own account cannot write. These four check that the control
# exists, that it is reachable, and that it does one thing.
unit=/etc/systemd/system/owuivm-detach@.service
if [ -f "$unit" ]; then
ok "there is a unit for it"
exec_line=$(sed -n 's/^ExecStart=//p' "$unit")
case "$exec_line" in
*"rm -f /var/lib/owuivm/base/%i.install") ok "it removes the marker and nothing else" ;;
*) bad "the detach unit does more than remove the marker: $exec_line" ;;
esac
grep -q '^ConditionPathExists=/var/lib/owuivm/base/%i.install' "$unit" \
&& ok "pressing it twice is not an error" \
|| bad "no ConditionPathExists - a second press reports a failure"
else
bad "no owuivm-detach@.service - detaching the media is still a shell command"
fi
for verb in thaw freeze; do
u="/etc/systemd/system/owuivm-$verb@.service"
[ -f "$u" ] && ok "there is a unit for $verb" \
|| bad "no owuivm-$verb@.service - the operator cannot $verb an image"
grep -q "owuivm-$verb@" /etc/polkit-1/rules.d/50-owuivm.rules \
&& ok "polkit authorises $verb" \
|| bad "owuivm-$verb@ is not in the polkit rule, so the desktop tool cannot start it"
done
# One button, two verbs, and the label has to be the state - a toggle
# that does not say which way it points is worse than two buttons.
grep -q 'owuivm-{verb}@{name}.service' /usr/local/bin/owuivm-maintenance 2>/dev/null \
&& ok "the maintenance tool has the freeze/unfreeze control" \
|| bad "the desktop tool cannot freeze or unfreeze anything"
grep -q '"Unfreeze" if state\["frozen"\] else "Freeze"' /usr/local/bin/owuivm-maintenance 2>/dev/null \
&& ok "and its label is the image's state" \
|| bad "the toggle does not say which way it points"
grep -q 'owuivm-detach@' /etc/polkit-1/rules.d/50-owuivm.rules \
&& ok "polkit authorises it for an active session of the operator" \
|| bad "the detach unit is not in the polkit rule, so the desktop tool cannot start it"
# The service without a button is the same sentence in a different file.
gui=/usr/local/bin/owuivm-maintenance
grep -q 'owuivm-detach@{name}.service' "$gui" 2>/dev/null \
&& ok "the maintenance tool has a control wired to it" \
|| bad "the desktop tool still has no way to press it"
# The one thing pressing it early would cost: a machine shut down
# mid-install with no disc to go back to.
grep -q 'state\["installing"\] and not state\["running"\]' "$gui" 2>/dev/null \
&& ok "the control is dead while the machine is running" \
|| bad "the control can be pressed mid-install"
echo
if [ "$fails" -gt 0 ]; then echo "FAILED $fails"; exit 1; fi
echo "the freeze holds"