vmm
===
A QEMU/KVM virtual machine manager in one POSIX shell script. Each VM is
a directory of plain files with a KEY=VALUE config edited by hand.

    cp vmm /usr/local/bin/vmm
    vmm                          the verbs, their arguments, the environment

Needs qemu-img and qemu-system-x86_64 or qemu-system-aarch64, from QEMU
6.0 or newer, Linux /proc, the POSIX tools, and flock, readlink, setsid,
timeout, grep -m1 and du -h, which are not POSIX. Alpine BusyBox and
Debian base tools provide them. Native acceleration needs /dev/kvm;
HWACCEL=no uses emulation instead. `vmm viewer` needs remote-viewer, or
whatever client $VIEWER names.


Usage
-----
    vmm create web 20G           writes the config, opens $EDITOR
    vmm start web
    vmm console web              Ctrl-] detaches
    vmm stop web

Install an OS by pointing CDROM at an ISO and booting it first:

    CDROM="/srv/iso/alpine-virt-x86_64.iso"
    BOOT_ORDER=dc

The CD is a SCSI device, so an installer with no virtio drivers will not
see it. Drop both lines once the disk is bootable.

A GRAPHICS=vnc or spice guest puts its display on a unix socket in the VM
directory, and `vmm viewer web` hands that socket to remote-viewer. The
path goes into a URI unencoded, so a VMMDIR holding a space, #, ? or %
may not survive the client's parser. The client runs wherever vmm runs,
so from another machine forward the socket and point a viewer at the
forward instead:

    ssh -N -L 5900:/home/you/.vm/web/vnc.sock vmhost &
    remote-viewer vnc://127.0.0.1:5900

Every VM owns `$VMMDIR/<name>/disk.qcow2`; its path is not configurable.
To run many VMs from one immutable golden image, replace each new disk
with an overlay. The overlay takes its size from the backing file:

    rm ~/.vm/web/disk.qcow2
    qemu-img create -f qcow2 -b /srv/img/alpine.qcow2 -F qcow2 \
        ~/.vm/web/disk.qcow2


Configuration
-------------
One KEY=VALUE per line. A `#` in the first non-blank column starts a
comment; a line carrying a value cannot also carry one. Values may be
quoted to protect leading or trailing blanks. Unknown keys, duplicates,
unbalanced quotes and non-printable bytes are errors naming the line.
Nothing here is sourced or eval'd.

    ARCH          x86_64 | aarch64                            default host
    HWACCEL       yes | no                                    default yes
    CPUS          positive integer                            default 2
    MEM           positive integer, MiB                       default 2048
    IMAGE_FORMAT  qcow2 | raw                                 default qcow2
    FIRMWARE      absolute path                               aarch64 needs it
    GRAPHICS      no | vnc | spice                            default no
    NETWORK       yes | no | hostonly                         default yes
    SNAPSHOT      yes | no                                    default no
    BOOT_ORDER    1-3 distinct of c(disk) d(cdrom) n(net)     default c
    BALLOON       yes | no                                    default yes
    CDROM         absolute path                               optional
    HOSTFWD       comma list of HOST:GUEST or ADDR:HOST:GUEST optional

  - BOOT_ORDER is a restriction, not a preference: QEMU runs with
    -boot strict=on, so a device left out of the list never boots. A
    list naming no device the config creates would restrict nothing,
    and is refused.
  - SNAPSHOT=yes runs the guest on an overlay that is unlinked as soon
    as QEMU holds it open, so guest writes have nowhere to survive.
  - GRAPHICS adds a display and USB tablet, reached with `vmm viewer`.
    vnc is built into QEMU, spice is a separate module. Clipboard
    sharing and guest agents are out of scope. Stop the VM before
    changing graphics.
  - NETWORK=hostonly isolates the guest from the host and the internet.
    QEMU's own DHCP and DNS still answer, and HOSTFWD still reaches in.
  - HOSTFWD binds 127.0.0.1 unless the entry names an IPv4 address of
    its own, so a two field entry is unreachable from another machine.
  - BALLOON=yes lets the guest return freed pages to the host as it
    frees them. There is no ballooning policy to set.
  - Only the host's own ARCH can use KVM. Anything else is emulated, and
    aarch64 has no built-in firmware to emulate without.
  - IMAGE_FORMAT describes the fixed disk. `vmm create` makes qcow2; set
    raw only after deliberately replacing that file with a raw image.


State
-----
One directory per VM, mode 0700, under $VMMDIR:

    config           KEY=VALUE, hand-edited configuration
    uuid             identity, feeds -uuid and the NIC MAC
    disk.qcow2       created by `vmm create`
    pid              written and locked by QEMU itself, never by vmm
    qmp.in qmp.out   FIFOs carrying QMP to and from QEMU
    stdout stderr    QEMU's own two streams, truncated at start
    console.log      guest serial, truncated at start, attached or not
    vnc.sock         only when GRAPHICS=vnc
    spice.sock       only when GRAPHICS=spice

The pidfile is authoritative. Do not remove it while QEMU is running. A
VM reading as unknown has a pidfile naming a live process vmm cannot
identify, usually a pid something else took after a crash. Nothing that
would touch a running guest acts on that VM until the pidfile is removed
by hand.

status and list report the QEMU process, not the guest. A guest that
panics is paused rather than killed, so it still reads as running; ask
`vmm monitor <name> info status` for the guest's own view.

Nothing caps console.log. It is truncated when the VM starts and appended
to for as long as the VM runs, so a guest that streams to its serial port
fills the filesystem it lives on.

The MAC is derived from the uuid, so a VM keeps its DHCP lease across
restarts and a clone never collides with the VM it came from.

create and clone write config last. A failure may leave an incomplete
directory; inspect and remove it manually before retrying.


Security
--------
The VM directory is the only authentication. Whoever can write qmp.in
owns the monitor, and the monitor can attach a block device pointing at
any file QEMU can read. QEMU runs under its seccomp sandbox with
spawning denied, so the monitor cannot start a process on the host.
Nothing is ever bound to a TCP port for control.

Disk images are trusted input: a qcow2 names its backing file in its own
header and QEMU opens it. Run `qemu-img info --backing-chain` on an image
from anywhere else. A backing file must be immutable while its overlays
exist. Never back one VM with another VM's writable disk.


Exit codes
----------
    0   success, including every idempotent no-op
    1   usage, config, dependency, or operation failure
    2   no such VM
    3   VM is not running, or its monitor did not answer
    4   VM is busy, locked, or has live state vmm cannot identify
    5   VM was stopped only by force

`vmm stop` returning 0 means the guest shut itself down. 5 means it
ignored ACPI and was killed.


Debugging
---------
    vmm dryrun web               what would run, and whether the config parses
    vmm logs web                 what QEMU said, what the guest printed
    vmm monitor web info status  ask the running guest
    tail -f ~/.vm/web/console.log

dryrun launches nothing and needs no QEMU installed, so it doubles as a
config checker anywhere. Errors name the file and the line:

    vmm: /home/you/.vm/web/config:7: unknown key: MEMROY

A VM that will not start prints the last of QEMU's stderr. vmm removes
runtime files only after proving QEMU is gone. A busy VM has another vmm
invocation holding its lock; the kernel releases it when that process exits.
An incomplete VM directory is deliberately left for manual inspection.


Testing
-------
    ./test                       every file in tests/
    ./test config argv           those two only

TAP, no framework. ./test makes a throwaway VMMDIR, defines the
assertions, and sources each file in tests/ in turn. Tests that boot a
guest on KVM skip without /dev/kvm; the emulated ones need only the
matching qemu-system binary, and its firmware for aarch64.
