The guide · explanation
Templates and the store
A template is a sealed, read-only disk image that VMs boot linked clones of. It is vmlab's answer to the Vagrant box: build it once from an installer ISO or an existing disk, store it under a name and version, and clone it into as many labs as you like. This chapter explains what a template holds, how the store is laid out, how a build runs, and the two ways a template leaves the machine it was built on. The fields of a template block are in Lab file: template and source, and every verb in vmlab template.
What a template is
A store entry is two files: disk.qcow2, the sealed image, and template.wcl, its metadata. The metadata records the hardware the template was built with (profile, CPUs, memory, disk size, firmware, TPM, secure boot, display), where it came from, the version of vmlab-agent baked into it, the wscript surface version its embedded scripts were written against, an optional first-boot script, and the OCI repository it publishes to. The hardware fields form the template layer of the resolution chain described in The lab file: a VM that does not set memory inherits it from here.
Templates are keyed by arch, name and version, and a reference is written <arch>/<name>[@<version>]. The arch is mandatory and never inferred from the host, because it selects which QEMU system emulator boots the image. Omitting the version means the highest in the store. vmlab validate rejects an archless reference.
The store
The store lives at ~/.local/share/vmlab/templates/, laid out as <arch>/<name>/<version>/. Reads are lock-free. Every mutation, whether an install, a removal or an import, holds an exclusive lock on the store, and content only ever enters it by an atomic rename of a fully staged directory. A build or import that fails part way leaves nothing behind.
Since the supervisor became the store's owner, every vmlab template verb is a protocol client: the CLI reads its own surroundings, such as a relative path or the git remote of the current directory, and sends them to the supervisor, which is the only process that opens the store or dials a registry. Two consequences follow. A build started in one terminal can be listed and stopped from another with `vmlab template stop`. And a build needs a supervisor, which the CLI starts for you.
vmlab template list shows what is installed. rm removes one version and refuses, without --force, when that disk still backs a linked clone in some lab. clean prunes superseded builds per family, keeping the newest by default, and is a dry run until you pass --yes.
Building a template
Before building, check whether the template is already published: pulling one takes minutes, building it can take much longer. A template is declared in a template block, in its own WCL file or beside a lab, and built with vmlab template build. The block names the build's source, the hardware the build VM boots with, any media to attach, and the provision scripts and playbooks that drive the install.
// Ubuntu Server 24.04 template (PRD §6.1). The installer ISO is downloaded
// and sha256-verified into the artefact cache; cloudinit/ is packed into a
// CIDATA ISO that subiquity picks up as a NoCloud autoinstall source. The
// provision script answers the autoinstall confirmation and waits for the
// installer to power the VM off. Build with:
//
// vmlab template build (run from this directory)
import <vmlab.wcl>
template "ubuntu-24.04" {
arch = "x86_64"
version = "24.04.4"
profile = "linux-modern"
cpus = 2
memory = 4GiB
disk = 20GiB
source "iso" {
url = "https://releases.ubuntu.com/24.04/ubuntu-24.04.4-live-server-amd64.iso"
sha256 = "e907d92eeec9df64163a7e454cbc8d7755e8ddc7ed42f99dbc80c40f1a138433"
}
media { kind = "iso" from = "./cloudinit/" label = "CIDATA" }
nic { nat = true }
provision "scripts/install.ws" { }
}
The source is exactly one of four forms:
- iso — an installer ISO, by local path or by URL with a required sha256. A URL is downloaded into the artefact cache and verified before use.
- qcow2 — an existing disk image, by path or URL and hash, imported as the base.
- template — another template in the store, named by from. A layered build starts from that template's disk and its recorded hardware, runs more provisioning, and seals a new template.
- scratch — a blank disk of the declared disk size. The attached media and the provision script do everything.
The build runs as a synthetic one-VM lab: a scratch VM whose primary disk is pre-seeded from the source, on the hardware the block declares over the source template's recorded hardware. It reuses the whole lab runtime, so a build script has the same API a lab provision has. The flow is: create the working qcow2, boot it, run the provision steps, wait for the guest to shut down, verify the agent's handshake, then move the disk and its metadata into the store under the new version. Build output streams to your terminal as it happens.
The agent bootstrap ISO
vmlab attaches one extra ISO to every build VM, labelled VMLAB. It carries the agent binaries and an install script per OS. The template's own unattended-install hook, such as a cloud-init runcmd, a subiquity late command, or an autounattend first-logon command, mounts that ISO and runs the script. The guest installs the agent itself, so no host channel is needed before the agent exists, and the build verifies the handshake before sealing. Clones never see this ISO. Set agent = false on the block to skip baking the agent, at the cost of a template that never reports ready.
The build VM always runs with prevent_sleep on. A build is unattended, and a guest that idles into ACPI S3 mid-install, as a Windows client edition does, would stall it exactly like a hang; instead the lab daemon wakes it the moment it sleeps.
For an x86 guest the ISO also carries the legacy agent (see Guest OS profiles, agent_transport = "isa-serial") under legacy/nt, legacy/9x and legacy/dos, every name 8.3 because DOS reads no Joliet. install.cmd defers to install-nt.cmd on a 4.x or 5.x kernel; install-9x.bat registers the agent under RunServices; INSTALL.BAT appends it to FDAUTO.BAT on FreeDOS (which never reads AUTOEXEC.BAT), else to AUTOEXEC.BAT, and starts it. A DOS or 9x template has no unattended hook, so its provision types the install line over the screen, for example D:\INSTALL.BAT from the CD or A:\INSTALL.BAT from a floppy media block. The scripts probe drives A, D, E and F for their source.
Versions
The block's version is a fixed prefix that identifies the upstream release. vmlab appends a build counter, so building the Ubuntu example above once produces 24.04.4.0 and again 24.04.4.1. The counter continues from the highest tag already published when the block names a registry, and from the local store otherwise. Change the prefix and the counter restarts at zero. Pass --version to pin a version instead; a version already in the store is refused.
A failed build leaves nothing in the store
The build's working directory is removed on success and on failure, and the supervisor sweeps any build directory a killed process left behind the next time it starts. A supervisor restart fails every build in flight; there is no resumption.
Media built from folders
A media block turns a folder on disk into an ISO or a floppy image and attaches it to the build VM, or to a lab VM. This is how unattend files, driver bundles and payloads reach a guest that has no network. Built images land in the lab's .vmlab/media and are content-addressed by a digest over the folder's contents, the kind and the label, so an unchanged folder never rebuilds.
ISOs are built with xorriso, falling back to genisoimage and then mkisofs, with Joliet and Rock Ridge extensions so both Windows unattend media and Linux payloads read correctly. Floppies are 1.44 MB FAT12 images built with mformat and mcopy from mtools. A disk block with from does the same for a larger FAT disk.
Linked clones and scratch VMs
vmlab up creates each VM's disk as a qcow2 overlay whose backing file is the template's disk in the store. The template is never written to. Clones live in .vmlab/, survive down, and are deleted by destroy, after which the next up makes fresh ones. Clones grow as the guest writes, which is why the supervisor watches free space on the filesystems holding .vmlab/ and the store and emits host.disk_low.
template = "scratch" is a reserved name meaning no backing image. The VM gets a freshly created blank qcow2 instead of a clone, and its hardware chain collapses to VM block then profile. Validation therefore requires an explicit arch, profile and disk. Boot media is yours to supply, typically a cdrom or a media block. scratch never appears in the store and cannot be pushed or pulled. It is for installer development and OS builds, where starting with no OS is the point.
Moving a template between machines
Two paths carry a template off the host it was built on. Both preserve the metadata, so a moved template inherits into VMs exactly as it did at home.
- Export and import. vmlab template export writes one portable .tar.zst archive holding the disk and its metadata, and import installs it into another store. This is the offline path.
- Registries. vmlab template push publishes a template to any OCI registry, and pull installs it from one. Distributing templates over registries explains how a template is laid out as an OCI artifact.
A lab's template = may also name a registry reference directly, with an explicit arch on the VM. vmlab up pulls it when it is absent from the store and never re-pulls it implicitly. Updates are explicit, through vmlab pull or vmlab template pull.