284 lines
13 KiB
Bash
Executable File
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"
|