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//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 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.