mirror of
https://github.com/kuhyx/utils.git
synced 2026-11-10 21:27:14 +01:00
Much of testsAndMisc/linux_configuration cannot be tested on the host: 15 files use `chattr +i` (so a bad test is not simply revertable), the shutdown installers write units that power the machine off, and boot_recovery installs pacman hooks. The repo's own CI says as much -- its Ubuntu job runs only the side-effect-free tests, noting the rest "drive pacman, /etc, systemd or adb". vmbox runs them in a throwaway Arch guest and verifies the result from the HOST, which is the part that is not obvious: a successful shutdown kills the connection carrying the report, and ssh returns 255 for a clean poweroff, a crash and a dropped link alike. So the verdict comes from two artefacts that outlive the VM -- a QMP event log written by a recorder attached at launch (attach later and qemu has already exited), and the serial console log. Five outcomes are distinguished: clean poweroff (SHUTDOWN guest:true plus a serial tail reaching "reboot: Power down"), dirty stop, guest-reset (rebooted instead of powering off), GUEST_PANICKED, and hung. A POWERDOWN event proves nothing -- it means the request was delivered, not that anything acted on it. Reset is a qcow2 backing-file trick: the golden image is built once and sealed (chmod 444 plus a sha256 sidecar verified on every launch, since booting the base would silently invalidate every overlay), and each sandbox is a thin overlay, so reset is rm + recreate. No root anywhere -- user-mode networking with per-VM forwarded ssh ports, and a socket multicast segment for VM-to-VM. The guest clock is pinned and re-applied on every launch, because the shutdown installers gate on a 21:00-05:00 window; NTP is masked in the image or it silently undoes the pin. Repos are cloned from a read-only share scoped to an explicit directory, never $HOME, and credentials are sentinel values. `vm build` ends by booting a throwaway sandbox off the sealed image and refusing to ship if ssh does not answer. Several distinct faults produced an identical "boots fine, connection reset" symptom; checks inside the build VM only prove state at seal time, so the gate has to be an end-to-end boot. Tests: 8 bats cases over the verdict table and name/meta/serial handling; shellcheck and ruff clean; every file within the 250-line cap. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VdyMBGCrGVGDWtJ9xe48f3
105 lines
4.2 KiB
Bash
105 lines
4.2 KiB
Bash
#!/bin/bash
|
|
# ============================================================================
|
|
# vmbox: assemble the qemu command line and start a sandbox.
|
|
#
|
|
# Every observation channel the verdict logic depends on is wired here:
|
|
# - QMP socket + a persistent event recorder (started BEFORE the guest can
|
|
# act, so a fast poweroff cannot race it)
|
|
# - per-boot serial log (append, never truncate)
|
|
# - pvpanic, so a kernel panic is distinguishable from a clean stop
|
|
# ============================================================================
|
|
|
|
# shellcheck source=common.sh
|
|
source "$(dirname "${BASH_SOURCE[0]}")/common.sh"
|
|
|
|
readonly VMBOX_MEM="${VMBOX_MEM:-4096}"
|
|
readonly VMBOX_SMP="${VMBOX_SMP:-4}"
|
|
|
|
# $1 = vm name. Echoes the serial-log index it started.
|
|
launch_vm() {
|
|
local name="$1"
|
|
require_vm "$name"
|
|
|
|
vm_is_running "$name" && { log "sandbox '$name' is already running"; return 0; }
|
|
|
|
source "$VMBOX_LIB_DIR/overlay.sh"
|
|
overlay_verify_base
|
|
|
|
local overlay port index rtc serial_n serial qmp share
|
|
overlay="$(vm_overlay "$name")"
|
|
port="$(meta_get "$name" ssh_port)"
|
|
index="$(meta_get "$name" index)"
|
|
rtc="$(meta_get "$name" rtc 2>/dev/null || true)"
|
|
serial_n="$(vm_next_serial "$name")"
|
|
serial="$(vm_serial "$name" "$serial_n")"
|
|
qmp="$(vm_qmp_sock "$name")"
|
|
# Symlink the repos you want testable into here; nothing else is exposed.
|
|
share="${VMBOX_SHARE:-$VMBOX_HOME/share}"
|
|
install -d -m 755 "$share"
|
|
|
|
# Refuse to boot the base itself -- doing so would corrupt every overlay.
|
|
[[ "$(realpath "$overlay")" == "$(realpath "$VMBOX_BASE_IMG")" ]] &&
|
|
die "refusing to boot the base image directly"
|
|
|
|
rm -f "$qmp"
|
|
|
|
# shellcheck disable=SC2054 # commas are qemu option syntax, not separators
|
|
local -a args=(
|
|
-name "vmbox-$name"
|
|
-machine q35,accel=kvm -cpu host
|
|
-smp "$VMBOX_SMP" -m "$VMBOX_MEM"
|
|
-drive file="$overlay",if=virtio,format=qcow2
|
|
-netdev "user,id=net0,hostfwd=tcp:127.0.0.1:${port}-:22"
|
|
-device virtio-net-pci,netdev=net0
|
|
# Shared segment so sandboxes can reach each other. No root, no bridge.
|
|
-netdev "socket,id=net1,mcast=${VMBOX_MCAST}"
|
|
-device "virtio-net-pci,netdev=net1,mac=52:54:00:be:ef:$(printf '%02x' "$index")"
|
|
# Repos under test, mounted PHYSICALLY read-only. Scoped to an
|
|
# explicit share dir -- NEVER $HOME, which would hand the guest
|
|
# ~/.ssh, ~/.claude and ~/.config/crdt-sync. The guest clones out of
|
|
# this into its own home before touching anything.
|
|
-virtfs "local,path=$share,mount_tag=hostrepo,security_model=mapped-xattr,readonly=on"
|
|
-display none
|
|
-device virtio-vga
|
|
-chardev file,id=ser0,path="$serial",append=on
|
|
-serial chardev:ser0
|
|
-qmp "unix:${qmp},server=on,wait=off"
|
|
# A QMP socket serves ONE client, and the recorder holds that one for
|
|
# the VM's whole life. Screenshots and status queries therefore need
|
|
# their own channel or they block until the recorder exits.
|
|
-qmp "unix:${qmp}.ctl,server=on,wait=off"
|
|
-device pvpanic
|
|
# A guest *reset* becomes a SHUTDOWN(guest-reset) event, which is how a
|
|
# script that reboots instead of powering off is caught.
|
|
-no-reboot
|
|
-action panic=pause
|
|
-pidfile "$(vm_pidfile "$name")"
|
|
)
|
|
|
|
[[ -n "$rtc" ]] && args+=(-rtc "base=$rtc")
|
|
|
|
_launch_qemu "$name" "$serial" "$qmp" args
|
|
printf '%s' "$serial_n"
|
|
}
|
|
|
|
_launch_qemu() {
|
|
local name="$1" serial="$2" qmp="$3"
|
|
local -n argref="$4"
|
|
|
|
qemu-system-x86_64 "${argref[@]}" >/dev/null 2>>"$serial.qemu-stderr" &
|
|
disown
|
|
|
|
# Wait for the QMP socket, then attach the recorder BEFORE anything runs in
|
|
# the guest: attaching after the fact loses the SHUTDOWN event entirely
|
|
# (qemu exits, the stream closes, and there is no verdict to read).
|
|
local waited=0
|
|
while [[ ! -S "$qmp" ]] && (( waited < 100 )); do
|
|
sleep 0.1; waited=$(( waited + 1 ))
|
|
done
|
|
[[ -S "$qmp" ]] || die "qemu did not create a QMP socket -- see $serial.qemu-stderr"
|
|
|
|
nohup python3 "$VMBOX_LIB_DIR/recorder.py" "$qmp" "$(vm_events "$name")" \
|
|
>/dev/null 2>&1 &
|
|
disown
|
|
}
|