mirror of
https://github.com/kuhyx/utils.git
synced 2026-11-10 19:07:17 +01:00
Some checks failed
dependency freshness / dependencies on newest stable (push) Has been cancelled
file length / file-length (push) Has been cancelled
md-naming / markdown naming convention (push) Has been cancelled
palette drift / palette (push) Has been cancelled
shared gate tests / gate unit tests (push) Has been cancelled
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
147 lines
6.4 KiB
Bash
147 lines
6.4 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"
|
|
# shellcheck source=firmware.sh
|
|
source "$(dirname "${BASH_SOURCE[0]}")/firmware.sh"
|
|
|
|
readonly VMBOX_MEM="${VMBOX_MEM:-4096}"
|
|
readonly VMBOX_SMP="${VMBOX_SMP:-4}"
|
|
|
|
# $1 = vm name. Echoes the serial-log index it started.
|
|
# VM-to-VM segment, addressed by the VM's own index.
|
|
#
|
|
# History, so nobody re-tries these: `mcast=` is silently dead on this host
|
|
# (`lo` has no MULTICAST flag, no multicast route -- qemu accepts it and drops
|
|
# every frame), and a listen=/connect= socket hub connected but never passed
|
|
# traffic. `dgram` with explicit UDP endpoints is the third approach: each VM
|
|
# binds its own port and points at its peer, so a PAIR of sandboxes can talk.
|
|
# This is a pair-wise link, not a switch: VMs 1 and 2 are wired together, 3
|
|
# and 4 are wired together, and so on.
|
|
_peer_netdev() {
|
|
# Pair (1,2), (3,4), (5,6)... An index pairs UP when odd and DOWN when
|
|
# even. Getting this backwards points a VM at a port nobody bound, which
|
|
# looks exactly like a broken network: both ends up, zero frames, ARP
|
|
# fails, and nothing in any log says why.
|
|
local index="$1" base="$VMBOX_HUB_PORT" mine theirs
|
|
mine=$(( base + index ))
|
|
if (( index % 2 == 1 )); then
|
|
theirs=$(( base + index + 1 ))
|
|
else
|
|
theirs=$(( base + index - 1 ))
|
|
fi
|
|
# Type first, then id: qemu rejects `id=` before the netdev type.
|
|
printf 'dgram,id=net1,local.type=inet,local.host=127.0.0.1,local.port=%s,remote.type=inet,remote.host=127.0.0.1,remote.port=%s' \
|
|
"$mine" "$theirs"
|
|
}
|
|
|
|
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)"
|
|
# VMBOX_RTC_ONCE is a one-launch override and is never written to meta:
|
|
# `vm wake` powers the guest on at the instant its alarm fired, without
|
|
# moving the sandbox's own pin.
|
|
rtc="${VMBOX_RTC_ONCE:-$(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")"
|
|
# Repos you want testable go here. Use `vm share <path>` -- it BIND-MOUNTS
|
|
# rather than symlinks, because 9p exports a directory tree and will not
|
|
# follow a symlink pointing outside it (the guest would see a dangling
|
|
# link). Nothing outside this directory is ever 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.
|
|
# listen= for the first VM up, connect= for the rest (see _peer_netdev).
|
|
-netdev "$(_peer_netdev "$index")"
|
|
-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
|
|
# ttyS1: interactive console on a unix socket. Separate from ttyS0 so
|
|
# driving a shell never interleaves with the log the verdict reads.
|
|
-chardev "socket,id=ser1,path=$(vm_console "$name"),server=on,wait=off"
|
|
-serial chardev:ser1
|
|
-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")
|
|
# One-shot, for `vm wake`: see DOCS-rtc-wake.md ("TCO watchdog").
|
|
[[ -n "${VMBOX_WATCHDOG_ACTION:-}" ]] && args+=(-action "watchdog=$VMBOX_WATCHDOG_ACTION")
|
|
firmware_qemu_args "$name" args
|
|
|
|
_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
|
|
}
|