Commands · reference
vmlab up
vmlab up brings the lab in the current directory to life: it validates the lab file, starts the lab daemon, downloads any registry template or image that is not in the store yet, creates each machine's clone, boots the machines in dependency order and runs their provision scripts and playbooks. With machine names it does the same for a subset of the lab.
| Option | Meaning |
|---|---|
| [VMS]... | Machines to bring up. With none given, every VM and container in the lab. |
| -h, --help | Print help. |
What it does
The verb runs a full validation of the lab file before it has any side effect. A lab that does not validate is never started, and the issues are printed with the offending text underlined in the source.
It then connects to the supervisor, starting it if none is running, and asks it for the lab daemon. The lab daemon owns everything that follows and outlives this process: the workspace syncer, the network fabric and the machines themselves keep running after vmlab up returns. See How vmlab runs a lab for the daemon layout.
Inside the daemon, up works through a plan computed before anything is touched:
- Machines the subset drags in through depends_on are added, and the machines are grouped into waves. A machine's dependencies must be ready, and the provisions scoped to them complete, before the machine's wave starts. Anything the plan skips is printed with its reason.
- Deferred downloads run first, with progress streamed to the terminal. This is the same code path vmlab pull runs on its own.
- The host's QEMU, firmware and helper binaries are checked for every target machine.
- The bundled SMB server starts when the lab declares shared folders, so shares are reachable during provisioning.
- Each wave boots in parallel. A machine carrying a first-boot script runs it before it counts as ready. If one machine in a wave fails, the rest of the wave is aborted and the verb fails; the machine that failed is left running for inspection.
- For a VM that up waits on anyway, because it has a first-boot script, a provision or playbook, or a dependent, up refreshes the agent if it is out of date once it answers: after the first-boot script and before any provision (see below).
- Between waves the daemon runs every provision script and playbook whose machine has started, in declaration order, waiting for each machine's readiness first.
- Port forwards are installed, and the workspace syncer starts for every dev machine that declares a workspace (see Dev machines and the workspace syncer).
Output from provision scripts streams to the terminal as it happens. On success the last line is lab "<name>" is up. A VM declared with gui = true (or a lab that sets it) gets a detached VNC viewer opened from this terminal, since the daemon is headless; closing the viewer only disconnects, the VM keeps running.
up is idempotent for a machine that is already running: the daemon leaves it alone. Clones persist across vmlab down and are only removed by vmlab destroy, so a second up after a down boots the same disks and skips the first-boot script.
Editing the lab file between runs
up always runs the lab file as it is on disk now. The lab daemon reads vmlab.wcl once, when it starts. A full vmlab down reaps it, so down, an edit and up start a fresh daemon that reads the edit. A daemon also outlives a partial down, a vm stop of every machine and an up that failed, and such a daemon may hold an older version of the file. Before it starts anything, up compares what the file declares now with what a running daemon loaded. If they differ and none of the lab's machines is running, up replaces the daemon with one that reads the file and prints one line:
lab "ad-lab": vmlab.wcl changed — restarting the lab daemon to load itOnly what the file declares counts: an edit to a comment or to blank lines does not replace the daemon. Replacing the daemon keeps everything on disk: clones, snapshots, .vmlab/state.json (generated MACs and image pins) and the workspace ledgers. It is the same replacement vmlab lab restart makes. vmlab pull, vmlab vm start, vmlab vm restart and their container spellings check the file the same way, because each builds a machine from the daemon's configuration.
A new daemon cannot take over a machine the old one is running, nor a download the old one is doing. When the file has changed and any machine in the lab is running, starting, stopping or suspended, up refuses, names those machines, and changes nothing:
lab "ad-lab": vmlab.wcl has changed since the lab daemon loaded it, and dc01 is still running — the daemon cannot load the new file without stopping it. Run `vmlab down`, then this command again, to apply the edit (or undo the edit)A download still in flight refuses the same way and names the artefact; wait for it to finish, or interrupt the vmlab pull that started it.
Most edits simply take effect on the next boot of the machine. One kind cannot: a VM's clone is an overlay on the template it was made from, so it cannot move to a different template. vmlab records the template reference a clone was made from in .vmlab/vms/<vm>/clone-source. When the lab file names a different template while that clone still exists, up refuses before downloading or starting anything and keeps the clone:
vm "web": its disk is a clone of "x86_64/alpine-3.23", but vmlab.wcl now declares "x86_64/debian-13" — a clone cannot move to another template. Run `vmlab vm destroy web` (or `vmlab destroy`) to discard the disk and recreate it from "x86_64/debian-13", or put the template line backThe reference is compared as written. An unpinned <arch>/<name> keeps its clone when a newer version of the same template is built, while pinning a different @<version> counts as a different template. A container's image line is different: changing it drops the image pin, and the next up pulls the new image (see Containers).
Agent refresh
A VM's guest agent comes from its template, so a template built by an older vmlab carries an older agent. When a VM's agent first answers, up compares the agent's version stamp (agent=<rev>) with the stamp of the agent this vmlab ships. If they differ, in either direction, up pushes the shipped agent into the VM the way vmlab machine repair-agent does, and prints one line:
agent: updated "winsrv" (agent=57fa802 → agent=2284722)The VM is then diverged: its template's sealed agent_version no longer describes it, and vmlab status -v shows diverged=yes until its disks are destroyed. The refresh also emits machine.agent_updated (see the event list). The refresh runs after the first-boot script, because that script belongs to the template and was written for the agent the template sealed, and before every provision script, so provisions see the current agent.
up never starts waiting on a VM just to refresh its agent. A VM with no first-boot script, no provision or playbook and nothing depending on it is refreshed in the background when its agent first answers, after up has returned. Its line goes to the lab daemon's log instead of your terminal; the event and diverged=yes report it as usual. vmlab vm start always refreshes this way. A guest that never answers is never waited for.
Such a VM is not reported ready until its refresh has finished, whether it succeeded or failed, because the refresh restarts the agent. Until then vmlab status shows it booting, and anything that waits for readiness, such as a script's wait_ready or a dependent machine, keeps waiting. vm.ready arrives, and port forwards are installed, once the refresh is done. A VM whose agent is already current is held only for the comparison. A VM that cannot be refreshed is not held at all. The hold is capped at five minutes, after which the VM is reported ready and the refresh finishes in the background.
A refresh never fails up. If it fails, up prints a warning: line that names the VM and the reason, emits machine.agent_updated with the error, and continues. The new binary is staged beside the installed one, so a push that fails leaves the old agent running. Containers are never refreshed, because their agent ships with this vmlab. Guests on the legacy agent tier are never refreshed either, because nothing can replace that agent over its own channel. A VM whose template sealed no agent is skipped too.
Set agent_update = false on a vm block to keep the agent its template sealed. Set it on the lab block to do the same for every VM; a VM's own setting wins. See vm and its children.
A failed first-boot leaves the machine running
A first-boot provision that errors, or that takes longer than 30 minutes, fails up but does not stop the machine, so you can open a console or a shell and look at what went wrong.
Examples
Bring up the whole lab in the current directory:
Bring up one machine and whatever it depends on:
Run the ad-lab example end to end:
Exit status
Exit status is 0 when every target machine started and every provision step completed. A lab file that fails validation, a lab directory that cannot be found, a supervisor that does not come up, a boot or provision failure, an edit that cannot be applied while machines are running, or a VM whose template changed under its clone all exit 1, the failed code from the error table. Exit 5 (conflict) means the supervisor already tracks a lab with this name from a different directory; stop that lab or rename this one. A usage error the argument parser rejects exits 2.