mirror of
https://github.com/kuhyx/utils.git
synced 2026-11-10 20:27:15 +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
104 lines
4.7 KiB
Markdown
104 lines
4.7 KiB
Markdown
# vmbox — a disposable Arch VM sandbox
|
||
|
||
Run destructive system scripts — shutdown installers, `chattr +i` hosts guards,
|
||
pacman hooks — against a throwaway Arch guest, and **verify the outcome from the
|
||
host**. Break it as thoroughly as you like; reset takes under a second.
|
||
|
||
```bash
|
||
./install.sh # installs deps (libisoburn), links `vm` onto PATH
|
||
vm build # builds the golden image, once (~10 min)
|
||
vm new demo # instant: a ~200 KB overlay on the sealed base
|
||
vm run demo 'sudo systemctl poweroff'
|
||
vm reset demo # back to pristine
|
||
```
|
||
|
||
## Why this exists
|
||
|
||
Much of `testsAndMisc/linux_configuration` cannot be tested on the real machine.
|
||
The repo's own CI says so — its Ubuntu job runs only the "side-effect-free"
|
||
tests and notes the rest *"drive pacman, /etc, systemd or adb and cannot run on
|
||
an Ubuntu runner without root and an Arch container"*. Locally they probe the
|
||
**live** system (`test_results.log` asserts `PASS: /etc/hosts is immutable`).
|
||
|
||
Three things make testing them on the host genuinely unsafe:
|
||
|
||
- **`chattr +i` immutability** — 15 files make `/etc/hosts` and friends
|
||
immutable. A bad test is not simply revertable; `rm` fails with EPERM.
|
||
- **Auto-poweroff** — the shutdown installers write systemd units that power the
|
||
machine off on a schedule.
|
||
- **Pacman hooks** — `boot_recovery/install.sh` installs pre/post-transaction
|
||
hooks and touches mkinitcpio. Blast radius is "your next update".
|
||
|
||
## The hard part: a successful shutdown kills your observer
|
||
|
||
Running a shutdown script over SSH returns a broken pipe. That is
|
||
**indistinguishable** from a crash, a hang, or a network blip — ssh returns 255
|
||
for all of them. So "did it actually shut down?" is answered from the host,
|
||
using two artefacts that outlive the VM:
|
||
|
||
| Channel | What it proves |
|
||
|---|---|
|
||
| QMP event log (`events.jsonl`) | *That* the machine stopped, and who stopped it |
|
||
| Serial console log | *How far* the shutdown sequence got |
|
||
|
||
A persistent recorder attaches at launch, **before** anything runs in the guest.
|
||
This ordering matters: attach afterwards and QEMU has already exited, the stream
|
||
is closed, and there is no verdict to read.
|
||
|
||
### Five outcomes, all host-observable
|
||
|
||
| Observation | Verdict | Exit |
|
||
|---|---|---|
|
||
| `SHUTDOWN guest:true` + serial reaches `reboot: Power down` | clean poweroff | 0 |
|
||
| `SHUTDOWN guest:true`, serial truncated | **dirty** — stopped mid-sequence | 3 |
|
||
| `SHUTDOWN reason=guest-reset` | **rebooted**, did not power off | 4 |
|
||
| `GUEST_PANICKED` | **crashed** | 5 |
|
||
| no event, liveness probe fails | **hung** | 6 |
|
||
|
||
`guest:true` alone is not enough for "shuts down *completely*" — `poweroff -f`
|
||
and a script that kills systemd mid-sequence both produce it. The serial tail is
|
||
the discriminator, and it is asserted by grep, not read by eye.
|
||
|
||
> A `POWERDOWN` event is **not** proof of anything: it means the ACPI request was
|
||
> delivered. With no OS booted, QEMU emits `POWERDOWN` and then runs forever.
|
||
|
||
## Design notes
|
||
|
||
- **Reset is a backing-file trick.** The golden image is built once and sealed
|
||
(`chmod 444` + a sha256 sidecar verified on every launch). Each sandbox is a
|
||
thin qcow2 overlay, so `vm reset` is `rm` + recreate. Booting the base itself
|
||
would silently invalidate every overlay, so the launcher refuses to.
|
||
- **No root anywhere.** User-mode networking with a per-VM forwarded SSH port;
|
||
VM-to-VM traffic rides a socket multicast segment. No bridge, no libvirt, no
|
||
daemon.
|
||
- **The guest clock is pinned** (`--rtc`) and re-applied on *every* launch. The
|
||
shutdown installers gate on a 21:00–05:00 window, so an unpinned clock makes
|
||
their tests pass or fail depending on the time of day. NTP is masked in the
|
||
image, or it would quietly undo the pin.
|
||
- **The guest gets its own clone** of the repos from a read-only mount. Real
|
||
credentials are never shared; the guest carries sentinel values instead.
|
||
|
||
## Commands
|
||
|
||
| Command | Effect |
|
||
|---|---|
|
||
| `vm build [--force]` | Build/rebuild the golden image |
|
||
| `vm new <name> [--rtc <ts>]` | Create a sandbox |
|
||
| `vm run <name> <cmd...>` | Run a command, print the verdict |
|
||
| `vm ssh <name>` | Interactive shell (starts the VM if needed) |
|
||
| `vm screenshot <name> [out.png]` | Capture the screen (locker/X11 tests) |
|
||
| `vm reset <name>` | Wipe back to pristine |
|
||
| `vm rm <name>` / `vm list` | Delete / list sandboxes |
|
||
|
||
## Out of scope
|
||
|
||
A VM cannot cover everything: adb/phone tests (~36 files), GPU monitors, the
|
||
i2c RGB in `setup_night_lockdown.sh`, and the Nextcloud/SearXNG suites (whose
|
||
fixtures assume a Debian layout) all stay on the host or stay untested.
|
||
|
||
## Tests
|
||
|
||
`bats tests/test_vmbox.bats` — host-side unit tests for name validation, meta
|
||
handling, serial rotation and the full verdict table. They do not boot a VM;
|
||
booting is covered by the destructive end-to-end demo above.
|