#!/usr/bin/env bash
# Boot the Alloy installer ISO in qemu, headless, with ssh forwarded to :2222.
#   run-vm.sh live      boot the ISO (cdrom first)
#   run-vm.sh installed boot the target disk only (no cdrom)
set -euo pipefail

HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(cd "$HERE/../.." && pwd)"

# Everything the run writes lives here, gitignored, and is safe to delete
# between runs. Not under output/, which build-iso.sh clears at the start of
# every build and would take a half-finished install with it.
SCRATCH="${VM_STATE:-$HERE/state}"; mkdir -p "$SCRATCH"
ISO=${ALLOY_ISO:-$REPO_ROOT/output/install.iso}
DISK="$SCRATCH/target.qcow2"
VARS="$SCRATCH/OVMF_VARS.fd"
MODE="${1:-live}"

[ -f "$VARS" ] || cp /usr/share/OVMF/OVMF_VARS_4M.fd "$VARS"

# A software TPM, because `bootc install --block-setup tpm2-luks` enrols the
# key into one and there is nothing to enrol into otherwise. State persists in
# tpm/ so an installed disk can unlock on the next boot the way real hardware
# would; delete that directory to simulate a cleared TPM.
mkdir -p "$SCRATCH/tpm"
if [ ! -S "$SCRATCH/tpm/swtpm-sock" ]; then
  # Only mint a TPM when there is not one already. The setup call carries
  # `--overwrite`, and this branch is taken every time the swtpm daemon has been
  # stopped rather than only on a first run, so running it unconditionally gives
  # the machine a new seed behind its own back. An installed disk that unlocks
  # from the TPM then cannot load its sealed key and falls back to asking for the
  # passphrase, reporting `Failed to unseal secret using TPM2: State not
  # recoverable` — which reads as a broken enrolment rather than as hardware the
  # harness swapped out. Deleting state/tpm/ is still how you clear a TPM on
  # purpose; this only stops it happening by accident.
  if [ ! -f "$SCRATCH/tpm/tpm2-00.permall" ]; then
    swtpm_setup --tpm2 --tpmstate "$SCRATCH/tpm" --createek --create-ek-cert \
      --create-platform-cert --lock-nvram --overwrite >/dev/null 2>&1 || true
  fi
  swtpm socket --tpm2 --tpmstate dir="$SCRATCH/tpm" \
    --ctrl type=unixio,path="$SCRATCH/tpm/swtpm-sock" \
    --flags startup-clear --daemon
fi
[ -f "$DISK" ] || qemu-img create -f qcow2 "$DISK" 40G >/dev/null

# How the target disk is attached, which matters to more than performance.
# `virtio` gives the guest a /dev/vda whose lsblk `tran` is null; `nvme` gives it
# a /dev/nvme0n1 that reports `tran: nvme`. A recipe whose disk rule is
# `single-internal-nvme` (build/hosts/fw12.env) can only be exercised against
# the second, so a scenario testing a prefilled install asks for it. Default
# stays virtio, because every scenario written before this one assumes vda.
TARGET_BUS="${TARGET_BUS:-virtio}"
case "$TARGET_BUS" in
  virtio) TARGET_ARGS=(-drive "file=$DISK,if=virtio,format=qcow2") ;;
  nvme)   TARGET_ARGS=(
            -drive "file=$DISK,if=none,id=target,format=qcow2"
            -device "nvme,serial=alloyvmtest,drive=target"
          ) ;;
  *) echo "unknown TARGET_BUS '$TARGET_BUS'; expected virtio or nvme" >&2; exit 1 ;;
esac

# What keyboards the machine has, which is hardware the gate's bench tests need
# and no machine here can be. `usr/bin/alloy-usb-gate` suspends enforcement
# whenever the count of ID_INPUT_KEYBOARD devices reaches zero, so exercising it
# means a machine that can have zero, and every physical machine here has an
# i8042 keyboard soldered to it that usbguard has no jurisdiction over.
#
#   ps2   q35's own i8042 controller. The default, and what every scenario
#         written before this one assumes.
#   none  i8042=off. Zero keyboards of any kind, which is the machine the gate
#         is supposed to open for at boot.
#   usb   i8042=off plus a USB keyboard on an xHCI controller, so the only
#         keyboard on the machine is one usbguard CAN take away. `id=kbd0` is
#         load-bearing: unplugging it mid-session is `device_del kbd0` over the
#         monitor, which is the third bench test and cannot be done from inside
#         the guest.
#
# q35 carries no USB controller unless one is asked for, so the `usb` case adds
# an xHCI rather than relying on a default that is not there.
KEYBOARD="${KEYBOARD:-ps2}"
case "$KEYBOARD" in
  ps2)  MACHINE="q35"          ; KBD_ARGS=() ;;
  none) MACHINE="q35,i8042=off"; KBD_ARGS=() ;;
  usb)  MACHINE="q35,i8042=off"
        KBD_ARGS=(-device qemu-xhci,id=xhci -device usb-kbd,bus=xhci.0,id=kbd0) ;;
  *) echo "unknown KEYBOARD '$KEYBOARD'; expected ps2, none or usb" >&2; exit 1 ;;
esac

args=(
  -enable-kvm -machine "$MACHINE" -cpu host -m 4608 -smp 4
  "${KBD_ARGS[@]}"
  -drive if=pflash,format=raw,unit=0,readonly=on,file=/usr/share/OVMF/OVMF_CODE_4M.fd
  -drive "if=pflash,format=raw,unit=1,file=$VARS"
  "${TARGET_ARGS[@]}"
  # Two forwards, and the second one is the firewall probe. User-mode
  # networking gives the guest no inbound path except a hostfwd, so a port the
  # host cannot reach at all proves nothing about the firewall: :2223 is a real
  # inbound path to guest :5555, which nothing in `50-alloy.preset` enables.
  # Refused there while ssh answers on :2222 is what the zone claims. See
  # build/vmtest/README.md, "Checking the firewall".
  -netdev "user,id=n0,hostfwd=tcp:127.0.0.1:2222-:22,hostfwd=tcp:127.0.0.1:${PROBE_PORT:-2223}-:5555"
  # `id=` so the frontend can be named from outside. QMP's `set_link` takes a
  # device id, and taking the guest's route away from the monitor is the only
  # way to do it that does not depend on the guest: from inside, `ip link set
  # <dev> down` travels over the ssh session it arrived on and kills it. See
  # build/vmtest/qmp.py and offline-first-boot.sh.
  -device virtio-net-pci,netdev=n0,id=nic0
  -chardev "socket,id=chrtpm,path=$SCRATCH/tpm/swtpm-sock"
  -tpmdev emulator,id=tpm0,chardev=chrtpm
  -device tpm-crb,tpmdev=tpm0
  -display none
  -chardev "socket,id=ser0,path=$SCRATCH/serial.sock,server=on,wait=off,logfile=$SCRATCH/serial-$MODE.log"
  -serial chardev:ser0
  -monitor "unix:$SCRATCH/monitor.sock,server,nowait"
  # The machine monitor, next to the human one rather than instead of it. HMP
  # is what vm.py wants (sendkey, screendump); QMP is the only one that can say
  # WHEN something happened inside the guest, because it emits events. A
  # scripted first-boot test needs to know the machine rebooted, and `info
  # status` reports running on both sides of a reset. See build/vmtest/qmp.py.
  -qmp "unix:$SCRATCH/qmp.sock,server=on,wait=off"
)

if [ "$MODE" = live ]; then
  args+=(-drive "file=$ISO,media=cdrom,readonly=on" -boot d)
fi

exec qemu-system-x86_64 "${args[@]}"
