a7db23cbf680

add test/vm golden-image build harness: configuration.nix, scripted installer, disposable overlay boot

time
2026-05-29 13:41 UTC · 4 months ago
machine
koh2
parent
a336cdb20502
5 files test/vm/README.md+41 −0test/vm/boot-overlay.sh+62 −0test/vm/build-golden.sh+85 −0test/vm/configuration.nix+72 −0test/vm/install.sh+28 −0
test/vm/README.md+41 −0
@@ -0,0 +1,41 @@1+# nina test VM — golden NixOS dev image2+3+A reproducible NixOS 24.11 dev image for exercising nina, built once and booted4+as disposable copy-on-write overlays so every run starts from the exact same5+snapshot. Runs under qemu (TCG) on Apple Silicon — x86_64 emulation, so slow.6+7+## Files8+9+- `configuration.nix` — the golden system's exact starting state (the spec).10+  Serial console + root autologin (headless drivable), BIOS GRUB on `/dev/vda`,11+  the brief's packages, `~/.nina.conf`, hostname `test-primary`.12+- `install.sh` — runs inside the minimal installer: partition, `nixos-install`.13+- `build-golden.sh` — host orchestration: boots the ISO headless and drives the14+  scripted install over serial, producing the golden qcow2.15+- `boot-overlay.sh` — boots a throwaway overlay of the golden image, runs a16+  command (default: nina smoke test), discards the overlay.17+18+## Build the golden image (once, slow)19+20+```sh21+# needs the NixOS 24.11 minimal x86_64 ISO and a linux nina binary22+./build-golden.sh /tmp/nixvm/nixos-24.11-minimal-x86_64.iso \23+                  /tmp/nina-linux/bin/nina \24+                  /tmp/nixvm/golden.qcow225+```26+27+The linux nina binary comes from the nina repo:28+`zig build -Dtarget=x86_64-linux-musl -Doptimize=ReleaseSafe -p /tmp/nina-linux`.29+30+## Boot a disposable copy31+32+```sh33+./boot-overlay.sh /tmp/nixvm/golden.qcow2            # nina smoke test34+./boot-overlay.sh /tmp/nixvm/golden.qcow2 'nina status'35+```36+37+The golden image is never mutated. Kiln can also boot it directly with38+`kiln chamber run --os /tmp/nixvm/golden.qcow2` (fresh overlay per run).39+40+Artifacts (ISO, golden.qcow2) live outside the repo (`/tmp/nixvm`); only the41+build inputs are version-controlled here.

view file

test/vm/boot-overlay.sh+62 −0
@@ -0,0 +1,62 @@1+#!/usr/bin/env bash2+# Boot a THROWAWAY copy-on-write overlay of the golden image, run commands over3+# the serial console, then discard the overlay. The golden image is never4+# mutated, so every boot starts from the exact same snapshot.5+#6+# Usage: boot-overlay.sh <golden.qcow2> [command ...]7+#   With no command, runs a nina smoke test (help / hello / status).8+# Tunables via env: MEM_MB (2048), SMP (2), BOOT_TIMEOUT (600)9+set -euo pipefail10+11+GOLDEN="${1:?usage: boot-overlay.sh <golden.qcow2> [command...]}"12+shift || true13+MEM_MB="${MEM_MB:-2048}"14+SMP="${SMP:-2}"15+BOOT_TIMEOUT="${BOOT_TIMEOUT:-600}"16+[ -f "$GOLDEN" ] || { echo "missing golden: $GOLDEN" >&2; exit 1; }17+18+if [ "$#" -gt 0 ]; then CMD="$*"; else19+  CMD='/root/nina help; echo ---; /root/nina hello; echo ---; /root/nina --version 2>/dev/null || true'20+fi21+22+work="$(mktemp -d)"23+trap 'rm -rf "$work"' EXIT24+overlay="$work/overlay.qcow2"25+qemu-img create -f qcow2 -b "$(cd "$(dirname "$GOLDEN")" && pwd)/$(basename "$GOLDEN")" -F qcow2 "$overlay" >/dev/null26+echo "throwaway overlay: $overlay  (backing: $GOLDEN)"27+28+expect -c '29+  set timeout [lindex $argv 3]30+  set overlay [lindex $argv 0]31+  set mem     [lindex $argv 1]32+  set smp     [lindex $argv 2]33+  set cmd     [lindex $argv 4]34+35+  spawn qemu-system-x86_64 \36+    -machine q35 -accel tcg -cpu max -smp $smp -m $mem \37+    -drive file=$overlay,if=virtio,format=qcow2 \38+    -netdev user,id=n0 -device virtio-net-pci,netdev=n0 \39+    -nographic40+41+  expect {42+    timeout { puts "\n!! no login on serial"; exit 2 }43+    -re {\[root@test-primary[^\]]*\]#} {}44+    -re {login:} { send "root\r"; exp_continue }45+  }46+  send "\r"47+  expect -re {\[root@test-primary[^\]]*\]#}48+49+  send "echo NINA_SMOKE_BEGIN\r"50+  expect "NINA_SMOKE_BEGIN"51+  send "$cmd\r"52+  send "echo NINA_SMOKE_END_RC=\$?\r"53+  expect {54+    timeout { puts "\n!! command timed out"; exit 3 }55+    -re {NINA_SMOKE_END_RC=[0-9]+} {}56+  }57+  send "poweroff\r"58+  set timeout 12059+  expect eof60+' "$overlay" "$MEM_MB" "$SMP" "$BOOT_TIMEOUT" "$CMD"61+62+echo "=== overlay discarded; golden image unchanged ==="

view file

test/vm/build-golden.sh+85 −0
@@ -0,0 +1,85 @@1+#!/usr/bin/env bash2+# Bake the golden NixOS qcow2: boot the minimal ISO headless under qemu, drive a3+# scripted nixos-install over the serial console, power off. Output is a golden4+# disk image; Kiln (or boot-overlay.sh) boots throwaway overlays from it.5+#6+# Usage: build-golden.sh <nixos.iso> <nina-linux-binary> <out-golden.qcow2>7+# Tunables via env: DISK_GB (20), MEM_MB (4096), SMP (2), INSTALL_TIMEOUT (2400)8+set -euo pipefail9+10+ISO="${1:?usage: build-golden.sh <iso> <nina-bin> <out.qcow2>}"11+NINA="${2:?need nina linux binary}"12+OUT="${3:?need output qcow2 path}"13+HERE="$(cd "$(dirname "$0")" && pwd)"14+DISK_GB="${DISK_GB:-20}"15+MEM_MB="${MEM_MB:-4096}"16+SMP="${SMP:-2}"17+INSTALL_TIMEOUT="${INSTALL_TIMEOUT:-2400}"18+19+for f in "$ISO" "$NINA"; do [ -f "$f" ] || { echo "missing: $f" >&2; exit 1; }; done20+21+work="$(mktemp -d)"22+trap 'rm -rf "$work"' EXIT23+24+# Seed media: our config + install script + nina, as an ISO the installer mounts.25+seed="$work/seed"26+mkdir -p "$seed"27+cp "$HERE/configuration.nix" "$seed/configuration.nix"28+cp "$HERE/install.sh" "$seed/install.sh"29+cp "$NINA" "$seed/nina"30+SEED_ISO="$work/seed.iso"31+hdiutil makehybrid -quiet -iso -joliet -default-volume-name NINASEED -o "$SEED_ISO" "$seed"32+33+# Fresh blank disk for the install target.34+qemu-img create -f qcow2 "$OUT" "${DISK_GB}G" >/dev/null35+echo "golden target: $OUT (${DISK_GB}G)  ·  seed: $SEED_ISO"36+37+expect -c '38+  set timeout 60039+  set iso       [lindex $argv 0]40+  set seed_iso  [lindex $argv 1]41+  set out       [lindex $argv 2]42+  set mem       [lindex $argv 3]43+  set smp       [lindex $argv 4]44+  set install_timeout [lindex $argv 5]45+46+  spawn qemu-system-x86_64 \47+    -machine q35 -accel tcg -cpu max -smp $smp -m $mem \48+    -drive file=$out,if=virtio,format=qcow2 \49+    -drive file=$iso,media=cdrom,readonly=on \50+    -drive file=$seed_iso,media=cdrom,readonly=on \51+    -boot d \52+    -netdev user,id=n0 -device virtio-net-pci,netdev=n0 \53+    -nographic54+55+  # Reach an installer root shell (autologin on the ISO serial console).56+  expect {57+    timeout { puts "\n!! no shell prompt from installer"; exit 2 }58+    -re {\[root@nixos[^\]]*\]#} {}59+    -re {nixos@nixos} { send "sudo -i\r"; exp_continue }60+    -re {login:} { send "root\r"; exp_continue }61+  }62+  send "\r"63+  expect -re {\[root@nixos[^\]]*\]#}64+65+  # Mount seed media and run the installer script.66+  send "mkdir -p /mnt-seed && mount /dev/sr1 /mnt-seed && echo NINA_SEED_OK\r"67+  expect {68+    timeout { puts "\n!! seed mount failed"; exit 3 }69+    "NINA_SEED_OK" {}70+  }71+  send "bash /mnt-seed/install.sh\r"72+73+  set timeout $install_timeout74+  expect {75+    timeout { puts "\n!! install timed out"; exit 4 }76+    "NINA_INSTALL_DONE" {}77+    "NINA_INSTALL_START" { exp_continue }78+  }79+  # qemu exits when the guest powers off.80+  set timeout 12081+  expect eof82+' "$ISO" "$SEED_ISO" "$OUT" "$MEM_MB" "$SMP" "$INSTALL_TIMEOUT"83+84+echo "=== golden image built: $OUT ==="85+qemu-img info "$OUT" | sed 's/^/  /'

view file

test/vm/configuration.nix+72 −0
@@ -0,0 +1,72 @@1+# Golden NixOS dev image for nina testing — the reproducible "exact starting2+# snapshot". Built once into a qcow2 (see build-golden.sh); Kiln boots a fresh3+# throwaway overlay from it per run.4+#5+# Mirrors the test environment from the Kiln brief (§2.3): NixOS 24.11,6+# test-primary host, git/curl/vim/htop/ripgrep, America/New_York.7+{ config, pkgs, ... }:8+9+{10+  imports = [ ./hardware-configuration.nix ];11+12+  # BIOS GRUB on the virtio disk — avoids needing UEFI/OVMF firmware under qemu.13+  boot.loader.grub = {14+    enable = true;15+    device = "/dev/vda";16+  };17+  # Serial console so the installed image is drivable headless (qemu -nographic).18+  # Keep a normal vga console too.19+  boot.kernelParams = [ "console=ttyS0,115200" "console=tty0" ];20+21+  networking.hostName = "test-primary";22+  networking.useDHCP = true;23+  time.timeZone = "America/New_York";24+25+  environment.systemPackages = with pkgs; [26+    git27+    curl28+    vim29+    htop30+    ripgrep31+  ];32+33+  # Headless driving: autologin root on the serial console so the build/boot34+  # harness can run commands without credentials.35+  services.getty.autologinUser = "root";36+37+  services.openssh = {38+    enable = true;39+    settings.PermitRootLogin = "yes";40+    settings.PasswordAuthentication = true;41+  };42+43+  # Test VM only — known password, never a real secret.44+  users.users.root.password = "nixos";45+46+  # nina's config (brief §2.2). Written on activation so it stays in this file47+  # rather than as imperative state. confirm=false auto-accepts; teach=true48+  # echoes the underlying command; color=true enables ANSI the checks verify.49+  system.activationScripts.ninaConf.text = ''50+    cat > /root/.nina.conf <<'EOF'51+    editor      = vim52+    generations = 553+    confirm     = false54+    teach       = true55+    color       = true56+57+    [machine]58+    name    = test-primary59+    config  = /etc/nixos60+    local   = true61+    default = true62+63+    [machine]64+    name    = test-remote65+    host    = 192.168.100.266+    user    = testuser67+    ssh_key = /home/testuser/.ssh/test_key68+    EOF69+  '';70+71+  system.stateVersion = "24.11";72+}

view file

test/vm/install.sh+28 −0
@@ -0,0 +1,28 @@1+#!/usr/bin/env bash2+# Runs INSIDE the NixOS minimal installer (over serial) to lay down the golden3+# system onto /dev/vda from the seed media. Driven by build-golden.sh.4+set -euo pipefail5+6+SEED_DIR="$(cd "$(dirname "$0")" && pwd)"7+echo "=== NINA_INSTALL_START ==="8+9+# Single BIOS-bootable ext4 root (MBR) — matches grub.device=/dev/vda.10+parted -s /dev/vda mklabel msdos11+parted -s /dev/vda mkpart primary ext4 1MiB 100%12+parted -s /dev/vda set 1 boot on13+mkfs.ext4 -F -L nixos /dev/vda114+15+mount /dev/disk/by-label/nixos /mnt16+17+# Generate hardware-configuration.nix for this disk, then drop in our config.18+nixos-generate-config --root /mnt19+cp "$SEED_DIR/configuration.nix" /mnt/etc/nixos/configuration.nix20+21+# Bake the injected nina binary (prove-the-loop: absolute-path /root/nina).22+install -Dm755 "$SEED_DIR/nina" /mnt/root/nina23+24+nixos-install --no-root-passwd --root /mnt25+26+echo "=== NINA_INSTALL_DONE ==="27+sync28+poweroff

view file