Getting started · tutorial
Build a template from an ISO
A template is a sealed base disk in your local store, keyed by architecture, name and version. Every VM in a lab is a linked clone of one. In this tutorial you build a template from the Ubuntu Server 24.04 installer ISO using the example shipped with vmlab, watch the build run, and then clone a VM from it in a lab of your own.
The example does an unattended install. A cloudinit/ folder is packed into a small ISO that Ubuntu's installer picks up as an autoinstall source, and a short wscript answers the one confirmation prompt by reading the screen. The same pattern, installer media plus answer file plus a script that drives the screen, builds every template under examples/templates/, including Windows Server.
Before you start
- vmlab is installed with the runtime tools. xorriso and mtools are needed here, because the build packs two ISOs: the cloud-init folder and vmlab's own bootstrap ISO.
- The agent binary for Linux x86_64 is installed under ~/.local/share/vmlab/guest/agent/linux-x86_64/. The build stages it on the bootstrap ISO and refuses to start without it. The installer places it; Install explains how to build the guest assets from source instead.
- /dev/kvm is available. The install takes several minutes under KVM and far longer under emulation.
- About 3 GB of free disk for the ISO download and 20 GB for the build disk. The sealed template is much smaller than the working disk.
- A checkout of the vmlab source, for the example directory: git clone https://github.com/VMLabDev/vmlab.
Read the template definition
Change into the example directory and open its lab file.
// 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" { }
}
A template {} block has four parts.
- Identity and hardware. arch selects the QEMU emulator. version is the version prefix the build stamps. profile = "linux-modern" supplies the machine type, firmware and device models for the build VM, and cpus, memory and disk size it. The hardware is recorded in the template's metadata and becomes the inheritance layer every clone starts from.
- source "iso". What the build boots. A url with a sha256 is downloaded into the artefact cache and verified before use. A local path works too. The other three source kinds are qcow2, an existing disk image; template, a layered build on top of a template already in the store; and scratch, a blank disk.
- Attachments. media { kind = "iso" from = "./cloudinit/" label = "CIDATA" } builds the folder into an ISO with that volume label at build time. nic { nat = true } gives the build VM egress so the installer can fetch packages.
- provision "scripts/install.ws". The wscript that drives the build. It runs against a lab containing the single build VM, named build.
The script is short. It uses the screen, not the agent, because the agent does not exist until the installer has put it there.
// Build provision for the ubuntu-24.04 template (PRD §6.1, §10.4).
// Subiquity finds the autoinstall config on the CIDATA ISO but, without
// `autoinstall` on the kernel command line, asks for confirmation first —
// answer it, then wait for the installer to power the VM off
// (`shutdown: poweroff` in cloudinit/user-data). The sealed image carries
// the vmlab guest agent (installed offline from the VMLAB ISO; the build
// verifies it with one extra boot), so lab clones come up "ready".
use vmlab
let vm = lab.vm?
// Nudge GRUB past its menu timeout if it is on screen.
match vm.wait_for_text Ok => vm.send_keys?
lab.log
}
Err => lab.log,
}
// Subiquity: "Continue with autoinstall? (yes|no)".
match vm.wait_for_text Ok => vm.type_text?
lab.log
}
Err => lab.log,
}
lab.log
vm.wait_shutdown?
lab.log
Ok
}
install.expect
}
wait_for_text OCRs the screen until a regular expression matches or the timeout passes. send_keys and type_text inject input. wait_shutdown blocks until the guest powers itself off, which the autoinstall's shutdown: poweroff line does at the end of the install. The agent is installed by the guest itself: a late-commands entry in cloudinit/user-data mounts the bootstrap ISO vmlab attaches under the label VMLAB and runs its install.sh into the target filesystem.
The agent comes from the guest's own install hook
vmlab attaches the bootstrap ISO to every template build and verifies the agent's handshake before sealing. It does not push the agent in itself, because there is no channel into a guest until the agent is running. Each OS has its own hook: a cloud-init late-commands or runcmd, or a Windows FirstLogonCommands entry in autounattend.xml.
Build it
template build reads every template {} block in ./vmlab.wcl, or the file named with -f, and builds each in turn. Pass a name to build only one. The build streams its progress, and each lab.log line in the script appears as it runs. To watch the installer, set gui = true on the template block and a viewer opens on the build VM.
The build proceeds through these stages.
- Download the ISO into the artefact cache and verify its SHA-256, or reuse the cached copy.
- Pack cloudinit/ and the bootstrap ISO. Create the 20 GiB working disk.
- Boot the build VM from the installer ISO with the hardware from the profile and the two built ISOs attached.
- Run scripts/install.ws: confirm the autoinstall, then wait for the guest to power off.
- Boot once more and wait for the agent's handshake. This proves the sealed image will report ready as a clone.
- Flatten the working disk and move it with its metadata into the store.
The whole run takes several minutes. A failed build leaves nothing in the store, so you can fix the definition and run it again. `vmlab template stop ubuntu-24.04` cancels a build in progress.
The version the build stamps is the block's version with a build counter appended: the first build of 24.04.4 is 24.04.4.0, the next is 24.04.4.1. Pass --version to pin an exact string instead. A version already in the store is refused rather than overwritten.
The template is in the store
vmlab template list shows a row for ubuntu-24.04 with arch x86_64, version 24.04.4.0 and its size on disk.
Use it in a lab
In a new directory, declare a VM that names the template by its store reference.
import <vmlab.wcl>
lab "ubuntu-lab" {
segment "lan" {
subnet = "10.91.0.0/24"
nat = true
forward { host_port = 12222 to = "srv:22" }
}
vm "srv" {
template = "x86_64/ubuntu-24.04"
memory = 2GiB
nic { segment = "lan" ip = "10.91.0.10" }
}
}
A store reference is <arch>/<name>[@<version>]. With no version the newest build is used. @24.04.4 selects the newest build under that prefix, and @24.04.4.0 pins one exactly. cpus, the firmware and the device models are inherited from the template, which recorded them at build time. Only memory is overridden here.
validate confirms the template exists in the store before up tries to clone it. The clone boots and reports ready as soon as the baked agent answers. The template's autoinstall created a user vmlab with password vmlab, so the forward on port 12222 gives you SSH into the guest too.
A VM runs from your own template
vmlab status shows srv ready at 10.91.0.10, and lsb_release reports Ubuntu 24.04.
Keep the store tidy
Every build adds a version. vmlab template clean prunes superseded builds, keeping the newest per template. It only prints what it would remove until you add --yes, and it skips a build that still backs a clone unless you add --force.
Next steps
- Automate a guest with a script puts a provision script on a VM so the guest is configured on every up.
- Templates and the store explains layered builds, the scratch source, first-boot scripts, export and import.
- Distributing templates over registries shows how to push this template to a registry so a lab file can reference it by URL.
- The other definitions under examples/templates/ build Fedora, AlmaLinux, Arch, openSUSE and Windows Server 2025. Example labs describes each.
- Lab file: template and source and vmlab template are the reference for what you used here.