Reference · reference
Lab file: template and source
This chapter is the field-by-field reference for the template {} block and its required source {} child. A template block describes how to build a sealed base image for the store; Templates and the store explains the build flow and vmlab template the verbs that run it. Template blocks live at the top level of a vmlab.wcl, beside a lab {} or in a file of their own with no lab at all. `vmlab template build reads ./vmlab.wcl unless --file` names another.
template {}
One buildable template. The build creates a working disk from the source, boots it with the hardware declared here, runs the provisions and playbooks, shuts the VM down and seals the disk into the store under <arch>/<name>/<version>/. The hardware fields are recorded in the template's metadata and become the inheritance layer for every VM cloned from it.
template "<name>" {
arch = "x86_64"
version = "24.04"
registry = "ghcr.io/owner/ubuntu-24.04"
profile = "linux-modern"
cpus = 2
memory = 4GiB
disk = 20GiB
display = "virtio-vga"
cpu_model = "pentium3"
accel = "tcg"
firmware = "ovmf"
tpm = false
secure_boot = false
nested = false
gui = false
qemu_args = []
first_boot = "scripts/first-boot.ws"
agent = true
source "…" { … }
gpu { … }
media { … }
provision "…" { … }
playbook "…" { … }
nic { … }
disk "…" { … }
variant "…" { … }
}
| Field | Type | Default | Meaning |
|---|---|---|---|
| name | utf8 (label) | required | Template name, for example linux-modern; the inline block label. |
| arch | utf8 | required | Architecture. Selects the QEMU system emulator. |
| version | utf8 | required | Version string, non-empty. Name, arch and version together are unique. |
| registry | utf8 | none | Full OCI repository to publish to and to version-bump against. |
| profile | utf8 | none | Guest OS profile supplying hardware defaults for the build VM. |
| cpus | i64 | from profile | vCPU count for the build VM. Inherited by clones. |
| memory | ByteSize | from profile | RAM for the build VM, for example 8GiB. Inherited by clones. |
| disk | ByteSize | from source | Working disk size for the build, for example 64GiB. Required for a scratch source. |
| display | utf8 | from profile | QEMU display device string for the build VM. |
| cpu_model | utf8 | from profile | QEMU -cpu model for the build VM, for example pentium3, replacing the accelerator default. Inherited by clones. Cannot be combined with nested = true. |
| accel | utf8 | from profile | Accelerator for the build VM: kvm or tcg. Inherited by clones. tcg cannot be combined with nested = true. |
| firmware | utf8 | from profile | Firmware: ovmf or seabios. |
| tpm | bool | from profile | Enable a TPM 2.0 device. |
| secure_boot | bool | from profile | Enable secure boot; OVMF only. |
| secure_boot_keys | utf8 | from profile, else microsoft | The key store the build's secure boot starts from: microsoft or setup (blank store, setup mode). Recorded in the metadata, so clones start from the same store. Needs secure_boot = true. |
| secure_boot_vars | utf8 | none | A pre-enrolled UEFI VARS file, relative to the template root, the build boots with. Stored beside the sealed disk as OVMF_VARS.fd and recorded as secure_boot_keys = "vars", so every clone starts from it; carried by export, import, push and pull. Needs secure_boot = true; exclusive with secure_boot_keys. |
| nested | bool | false | Enable nested virtualisation for the build VM. |
| gui | bool | false | Watch the build VM in a VNC viewer. |
| qemu_args | list<utf8> | none | Raw QEMU flags for the build VM. The escape hatch. |
| first_boot | utf8 | none | wscript run on the first instantiation of a clone, before it turns ready. |
| agent | bool | true | Bake the vmlab-agent service into the image. |
| source {} | child | required | What the build starts from. Exactly one of four forms. |
| gpu {} | child | none | GPU acceleration for the build VM only: passthrough, virgl or vulkan, with an optional host_gpu. Never recorded for clones. |
| media {} | children | none | ISO or floppy images attached to the build. |
| provision {} | children | none | Provision scripts that drive the build. |
| playbook {} | children | none | config-weave playbooks applied to the build VM, interleaved with provisions in declaration order. Steps stream as structured build progress. |
| nic {} | children | none | NICs for the build VM. Optional; the build VM may be air-gapped. |
| disk {} | children | none | Additional disks attached during the build. |
| variant {} | children | none | Sealed variants built on the base after it seals, each its own disk state. See the variant {} section below. |
media, provision, playbook, nic, disk and gpu are the same blocks a vm carries; see vm and its children.
The build VM's GPU
A gpu {} on a template or a variant gives the build VM a GPU and nothing else. Validation checks it as a lab VM's, passthrough included. At the build boot the lab daemon picks the host GPU as it does for a lab VM: the host_gpu named, or the host's default, with its render node, the Vulkan loader filter, the PCI-filter layer and the host config's qemu_binary and qemu_env. A host_gpu this host does not declare warns and falls back to the default. The build log names the pick as host_gpu=<name>, and the build VM's qemu.log header shows the argv and environment it produced.
The block is never recorded in the template's metadata: a clone's GPU is the lab's choice, so a clone that declares no gpu {} boots on the profile's plain display. Use it when an install only completes on a working paravirtual GPU, such as a Windows GPU driver package that verifies its kernel driver started, and leave display alone.
Versions and the registry
The declared version is a fixed prefix naming the upstream identity, such as an OS release. Each build appends a counter: the stored version is <version>.<N>, where N is one higher than the highest existing build with that prefix, or 0 for the first. When registry is set the existing builds are read from its tags, so the counter continues across machines; otherwise the local store decides. Changing the prefix restarts the counter at .0. vmlab template build --version pins an explicit version instead. registry is also the repository vmlab template push publishes to; see Distributing templates over registries.
The first-boot script and the agent
first_boot is read at build time and baked into the template's metadata, so a clone runs it on its first start without the lab needing the file. It runs before the VM turns ready. With agent = true the build waits for the guest-installed agent's handshake before sealing and records the agent version in the metadata. With agent = false, or on a profile with no agent channel, the check is skipped and the build log says so.
Validation
- arch is one of the known architectures: x86_64, x86, aarch64, riscv64, loongarch64, s390x, ppc64.
- version is non-empty, and no other template in the file has the same arch, name and version.
- profile, if set, names a known profile.
- A scratch source requires disk.
- first_boot and every provision script exist under the template's root and compile.
- A playbook on a template whose arch is not x86_64 is rejected.
- Every media and disk source folder exists.
- Variant names match [a-z0-9][a-z0-9-]{0,31}, are not base, and do not repeat; exactly one variant sets default = true, unless there is only one.
// 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" { }
}
source {}
What the build starts from. The inline label is the kind, and the kind decides which fields apply. Exactly one source {} per template; the schema rejects a template without one.
source "iso" { path = "./installer.iso" }
source "iso" { url = "https://…/installer.iso" sha256 = "…" }
source "qcow2" { path = "./base.qcow2" }
source "template" { from = "x86_64/ubuntu-24.04@24.04.4" }
source "scratch" { }
| Field | Type | Default | Meaning |
|---|---|---|---|
| kind | utf8 (label) | required | Source kind: iso, qcow2, template or scratch; the inline label. |
| path | utf8 | none | Local file path, for iso and qcow2. Mutually exclusive with url. |
| url | utf8 | none | Remote artefact URL, for iso and qcow2. Requires sha256. |
| sha256 | utf8 | none | SHA-256 of the remote artefact. Required with url. |
| from | utf8 | none | Source template <arch>/<name>[@<version>][#<variant>], for kind template: a layered build. No # seeds from the default variant, #base from the base. |
The four kinds:
- iso: an installer ISO. The build boots it with the attached media and lets the provision script drive the installer. A remote artefact is downloaded to the cache and verified against sha256 before use.
- qcow2: an existing disk image, imported as the base.
- template: a layered build. An existing store template is the base, more provisioning runs, and the result seals as a new template. from must be a local store reference; a registry reference or scratch is rejected here. On a source with variants, no # seeds from its default variant, #<variant> from that variant, and #base from its base; the build boot runs the first-boot script of what was selected, and none for #base. The selected disk and everything beneath it are flattened into the working disk.
- scratch: a blank disk of the template's disk size. The attached installer media and provision script do everything.
Validation requires exactly one of path and url for iso and qcow2, sha256 whenever url is set, a path file that exists, and a from template that is in the store. An unknown kind is rejected.
variant {}
One variant of the template: a named, sealed disk state built on the base by booting an overlay on it and running the variant's own steps. A template with variants is referenced as <arch>/<name>#<variant>, and a reference without # selects the default variant; see Templates and the store. The motivating case is Windows: the base is installed and updated but not generalized, and each variant generalizes after its own changes.
template "windows-11" {
…base build: source, provisions ending in a clean shutdown, no sysprep…
first_boot = "scripts/firstboot.ws"
variant "standard" {
default = true
provision "scripts/generalize.ws" { }
}
variant "win3d" {
description = "GPU driver installed"
secure_boot = false
gpu { mode = "vulkan" } // the build VM only; host_gpu = "AMD1" to pin
provision "scripts/win3d-setup.ws" { }
provision "scripts/generalize.ws" { }
}
}
| Field | Type | Default | Meaning |
|---|---|---|---|
| name | utf8 (label) | required | Variant name, [a-z0-9][a-z0-9-]{0,31}. base is reserved. |
| default | bool | see meaning | The variant a reference with no # selects. Exactly one variant sets it, unless there is only one. |
| description | utf8 | none | One line saying what the variant is for, shown by vmlab template list. |
| first_boot | utf8 | the template's | wscript run on the first instantiation of a clone of this variant, before it turns ready. |
| profile | utf8 | the base's | Guest OS profile for this variant. |
| cpus | i64 | the base's | vCPU count. |
| memory | ByteSize | the base's | RAM, for example 8GiB. |
| display | utf8 | the base's | QEMU display device string. |
| cpu_model | utf8 | the base's | QEMU -cpu model. |
| accel | utf8 | the base's | Accelerator: kvm or tcg. |
| firmware | utf8 | the base's | Firmware: ovmf or seabios. |
| tpm | bool | the base's | Enable a TPM 2.0 device. |
| secure_boot | bool | the base's | Enable secure boot; OVMF only. |
| secure_boot_keys | utf8 | the base's | microsoft or setup. A variant carries no VARS file of its own; a base's stored file is used unless this names another store. |
| qemu_args | list<utf8> | the template's | Raw QEMU flags for the variant's build VM, replacing the template's. |
| gpu {} | child | the template's | GPU acceleration for the variant's build VM only, replacing the template's. Never recorded for clones; see the build VM's GPU above. |
| media {} | children | none | ISO or floppy images attached to the variant's build. The template's own media stay with the base build. |
| provision {} | children | none | Provision scripts the variant's build runs, interleaved with its playbooks in declaration order. |
| playbook {} | children | none | config-weave playbooks applied to the variant's build VM. |
| nic {} | children | the template's | NICs for the variant's build VM, replacing the template's when any are declared. |
The hardware fields override what the base recorded, field by field, for the variant's build boot and for every clone of the variant; a field the variant leaves out follows the base. secure_boot = false on a variant of a Secure Boot base boots its build, and its clones, on the firmware without Secure Boot. gpu {} is the exception: it reaches the variant's build boot and never its clones.
How a variant builds
vmlab template build seals the base first. On a template with variants the base embeds no first-boot script: the template's first_boot is each variant's to inherit, and a variant's own first_boot replaces it. Then each variant builds, the default first and the rest in file order. A variant build creates an overlay on the sealed base, boots it on the variant's hardware, waits for the agent, runs the variant's provisions and playbooks, and shuts down gracefully. It runs no first-boot script: the base was never generalized, so there is nothing to specialize until a clone of the finished variant starts. The overlay is then pointed at its base by the relative path ../../disk.qcow2, hashed, and installed with its metadata under the store lock.
A failed variant stops the build; the base and every variant installed before it stay installed, and nothing of the failed one is left behind. vmlab template build <name> --variant <v> builds or rebuilds just that variant onto the installed version. A rebuild is refused while a machine's clone is backed by the variant: destroy those machines first.