Reference · reference

Lab file: template and source

9 min read · 2026-09-02 · vmlab 0.9

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.

wcl
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 "…" { … }
}
FieldTypeDefaultMeaning
nameutf8 (label)requiredTemplate name, for example linux-modern; the inline block label.
archutf8requiredArchitecture. Selects the QEMU system emulator.
versionutf8requiredVersion string, non-empty. Name, arch and version together are unique.
registryutf8noneFull OCI repository to publish to and to version-bump against.
profileutf8noneGuest OS profile supplying hardware defaults for the build VM.
cpusi64from profilevCPU count for the build VM. Inherited by clones.
memoryByteSizefrom profileRAM for the build VM, for example 8GiB. Inherited by clones.
diskByteSizefrom sourceWorking disk size for the build, for example 64GiB. Required for a scratch source.
displayutf8from profileQEMU display device string for the build VM.
cpu_modelutf8from profileQEMU -cpu model for the build VM, for example pentium3, replacing the accelerator default. Inherited by clones. Cannot be combined with nested = true.
accelutf8from profileAccelerator for the build VM: kvm or tcg. Inherited by clones. tcg cannot be combined with nested = true.
firmwareutf8from profileFirmware: ovmf or seabios.
tpmboolfrom profileEnable a TPM 2.0 device.
secure_bootboolfrom profileEnable secure boot; OVMF only.
secure_boot_keysutf8from profile, else microsoftThe 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_varsutf8noneA 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.
nestedboolfalseEnable nested virtualisation for the build VM.
guiboolfalseWatch the build VM in a VNC viewer.
qemu_argslist<utf8>noneRaw QEMU flags for the build VM. The escape hatch.
first_bootutf8nonewscript run on the first instantiation of a clone, before it turns ready.
agentbooltrueBake the vmlab-agent service into the image.
source {}childrequiredWhat the build starts from. Exactly one of four forms.
gpu {}childnoneGPU acceleration for the build VM only: passthrough, virgl or vulkan, with an optional host_gpu. Never recorded for clones.
media {}childrennoneISO or floppy images attached to the build.
provision {}childrennoneProvision scripts that drive the build.
playbook {}childrennoneconfig-weave playbooks applied to the build VM, interleaved with provisions in declaration order. Steps stream as structured build progress.
nic {}childrennoneNICs for the build VM. Optional; the build VM may be air-gapped.
disk {}childrennoneAdditional disks attached during the build.
variant {}childrennoneSealed 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

examples/templates/ubuntu-24.04/vmlab.wclwcl
// 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.

wcl
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"  { }
FieldTypeDefaultMeaning
kindutf8 (label)requiredSource kind: iso, qcow2, template or scratch; the inline label.
pathutf8noneLocal file path, for iso and qcow2. Mutually exclusive with url.
urlutf8noneRemote artefact URL, for iso and qcow2. Requires sha256.
sha256utf8noneSHA-256 of the remote artefact. Required with url.
fromutf8noneSource template <arch>/<name>[@<version>][#<variant>], for kind template: a layered build. No # seeds from the default variant, #base from the base.

The four kinds:

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.

wcl
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" { }
  }
}
FieldTypeDefaultMeaning
nameutf8 (label)requiredVariant name, [a-z0-9][a-z0-9-]{0,31}. base is reserved.
defaultboolsee meaningThe variant a reference with no # selects. Exactly one variant sets it, unless there is only one.
descriptionutf8noneOne line saying what the variant is for, shown by vmlab template list.
first_bootutf8the template'swscript run on the first instantiation of a clone of this variant, before it turns ready.
profileutf8the base'sGuest OS profile for this variant.
cpusi64the base'svCPU count.
memoryByteSizethe base'sRAM, for example 8GiB.
displayutf8the base'sQEMU display device string.
cpu_modelutf8the base'sQEMU -cpu model.
accelutf8the base'sAccelerator: kvm or tcg.
firmwareutf8the base'sFirmware: ovmf or seabios.
tpmboolthe base'sEnable a TPM 2.0 device.
secure_bootboolthe base'sEnable secure boot; OVMF only.
secure_boot_keysutf8the base'smicrosoft or setup. A variant carries no VARS file of its own; a base's stored file is used unless this names another store.
qemu_argslist<utf8>the template'sRaw QEMU flags for the variant's build VM, replacing the template's.
gpu {}childthe template'sGPU acceleration for the variant's build VM only, replacing the template's. Never recorded for clones; see the build VM's GPU above.
media {}childrennoneISO or floppy images attached to the variant's build. The template's own media stay with the base build.
provision {}childrennoneProvision scripts the variant's build runs, interleaved with its playbooks in declaration order.
playbook {}childrennoneconfig-weave playbooks applied to the variant's build VM.
nic {}childrenthe template'sNICs 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.