Getting started · tutorial
Automate a guest with a script
A provision script is a wscript file declared inside the machine it configures. vmlab runs it during vmlab up once that machine is ready. In this tutorial you write one that waits for the guest, runs a command, copies a file in and captures the screen, then run the same script again on demand with vmlab script.
Before you start
- You have completed Your first VM, or have any lab with one Linux VM that reports ready. The lab file below is that tutorial's, extended.
- The guest has a display. Screenshots need one, and every VM has one. A container does not, so the screenshot step would fail there by naming the missing capability.
Start from a real one
The alpine-registry example ships a provision that does the first two things this tutorial needs: it waits for readiness and runs a command. Read it before writing your own.
// Provision for the alpine-registry lab: wait for the guest (which boots from
// a template pulled on-demand from the OCI registry), then prove it is up and
// reachable. `wait_ready` blocks until the vmlab guest agent answers.
use vmlab
let alp = lab.vm?
lab.log
alp.wait_ready?
lab.log
let rel = alp.exec?
lab.log
lab.log
Ok
}
setup.expect
}
Four things in this file are the shape of every provision script.
- use vmlab imports the host module, which provides vmlab::sleep_ms and the other helpers in wscript API: Term, types and the vmlab module.
- fn main(lab: Lab) is the entry point. vmlab calls it with the Lab handle, from which the script reaches every machine and segment by name.
- Methods that can fail return Result. The ? operator propagates an error string out of setup, and expect in main turns it into a failed provision, which fails vmlab up with that message.
- wait_ready(600) blocks until the guest agent answers or 600 seconds pass. A provision runs after its machine is ready, so the call is instant during up, but it makes the script correct under vmlab script too, where nothing has waited for you.
Write the script
In the directory holding the first-vm lab file, create scripts/setup.ws and a file to copy in, scripts/files/motd.
// Wait for the guest, run a command, copy a file in, capture the screen.
use vmlab
let alp = lab.vm?
alp.wait_ready?
lab.log
// exec: program and argument list, captured stdout/stderr/exit code.
let rel = alp.exec?
if rel.exit_code != 0 return Err
}
lab.log
// copy_to: a host path relative to this script, to an absolute guest path.
alp.copy_to?
let motd = alp.exec?
lab.log
// screenshot: a PNG under the lab's .vmlab/screenshots/ when the path is "".
let shot = alp.screenshot?
lab.log
Ok
}
setup.expect
}
Three details are easy to get wrong.
- exec takes a program and a list of arguments, not a shell line. To run a pipeline or use a shell feature, call the shell: alp.exec("/bin/sh", ["-c", "..."]). The default timeout is 120 seconds; exec_timeout takes a third argument in seconds.
- Relative host paths in copy_to, copy_from, screenshot and the image-matching methods resolve against the directory the script lives in, not the lab root. That is why the file is at scripts/files/motd and the script names it as files/motd.
- screenshot("") picks a timestamped name under the lab-local .vmlab/screenshots/ directory and returns the path. Pass a path to choose the file yourself.
Declare it on the machine
Add a provision {} block to the VM. The path is relative to the lab root.
import <vmlab.wcl>
lab "first-vm" {
vm "alp" {
template = "ghcr.io/vmlabdev/vmlab-templates/alpine-3.23"
arch = "x86_64"
memory = 1GiB
nic { nat = true }
provision "scripts/setup.ws" { }
}
}
validate compiles the script as part of validating the lab file, so a syntax error or a call to a method that does not exist is reported before anything boots.
The script compiles
vmlab validate reports the lab as ok with one VM. A typo in the script is reported here, with its line.
Run it with up
up boots the VM, waits until it is ready, and then runs the provision. Each lab.log line appears in the terminal as the script reaches it. Every up runs the steps again, whether or not the VM was already running, so keep scripts safe to repeat. Copying the same file twice is harmless. Creating an account twice is not, and the mixed-lab example shows the guard pattern, checking the guest's state before changing it.
Confirm the two side effects from the host.
The guest is provisioned
/etc/motd in the guest reads provisioned by vmlab, and .vmlab/screenshots/ holds a PNG named after the VM and the time.
Run it again on demand
Any script can be run ad hoc against the running lab. vmlab script takes a path relative to the lab root and calls its main with the same lab handle a provision gets.
The one difference is ownership. A provision belongs to the machine that declared it, and lab.this_vm() returns that machine. Under `vmlab script there is no owning machine and this_vm()` returns an error. The script above uses lab.vm("alp") so it works both ways.
Provisions and playbooks are the two kinds of setup step, and they run interleaved in declaration order. The other place scripts run is an on {} handler, which reacts to an event such as vm.crashed. See Playbooks and Events and handlers.
Provisions run as the agent identity
With no login {} on the machine, exec and copy_to run as root or SYSTEM. To write into a user's home as that user, declare a login and take a second handle with as_login. The dev-container example uses it (see Example labs), and Logins explains the rules.
Next steps
- Guest automation with wscript covers the execution model, error handling, the terminal API and multi-machine orchestration.
- Screens, input and vision shows wait_for_text, wait_for_image, keystrokes and mouse input, for guests where the agent is not there yet.
- wscript API: Machine lists every method on the machine handle, and vmlab script documents the verb.