VMs and profiles
Every Provium test ultimately drives one or more guest VMs. This page covers the lifecycle: creating a VM, picking its profile, controlling boot, taking snapshots, and tearing down.
The exhaustive reference is on VM.
Creating a VM #
local vm = provium:
The first arg is the VM's name (per-scope unique). The second arg is a profile name from provium.toml. The VM is in Created state until you call :boot().
Two forms. provium:vm(name, profile) creates a VM in the current scope. provium:vm(name) looks one up by name, walking from the current scope to the file root. Inside a test() body, the current scope is the per-test scope; at file top-level, it's the file root. See labs and scope for the full rules.
Optional third arg is a sizing table — the two keys the scheduler and QEMU need before boot:
local vm = provium:
Everything else about a boot — kernel command line, determinism seeds, injected files — is passed to vm:boot(opts) instead:
vm:
The split is deliberate: memory and cpus size the VM, and the scheduler needs them when the VM is declared; the boot opts shape one particular boot and merge per field into the VM's pending boot options.
Booting #
local vm = provium::
:boot() returns self so you can chain. The VM is in Booted state on return. A vm_spawned event fires as soon as the agent has handshaken; consumers see it before :boot() returns.
For multi-VM tests, you can boot each individually:
local a = provium::
local b = provium::
Or batch-boot via the lab:
provium:
provium:
provium: -- boots both
The batch form is mainly useful when you've declared a topology in a fixture builder and want to bring it up atomically.
Profiles #
A profile is a [profiles.<name>] block in provium.toml:
[]
= "/build/peios/bzImage"
= "/build/peios/initrd.cpio.gz"
= "console=ttyS0 quiet"
= "peios"
Each profile names a (kernel, initrd, cmdline) tuple. A test picks which profile to use by name:
provium: -- uses [profiles.peios]
provium: -- uses [profiles.peios-debug]
You can have any number of profiles. Common patterns:
| Pattern | Profiles |
|---|---|
| Test against multiple kernel versions | peios-stable, peios-mainline |
| Compare optimised and debug builds | peios, peios-debug |
| Test pre/post a feature flag | peios-prefeatx, peios-postfeatx |
Multi-profile fixtures invalidate every cached fixture when any profile's kernel or initrd identifier changes — see fixtures and dependencies.
Lifecycle methods #
A VM's lifecycle: :boot() takes it from Created to Booted; :pause() / :resume() toggle between Booted and Paused; :reset() warm-reboots (still Booted); and :shutdown() or :power_button() end in Shutdown. The full state machine and per-method semantics are in the VM reference.
Operations against a VM in the wrong state error cleanly with a hint: VM not booted on a Created VM, VM is paused; use resume() on a Paused one, VM is shutdown; create a new one after shutdown.
You typically don't call :shutdown() explicitly. The harness's resource-graph walker tears every VM down at the appropriate scope boundary: test-scope VMs at the end of their test() body, file-scope VMs at file end. (reset_between_tests = true snapshots and restores instead — see labs and scope.)
Boot opts in detail #
The opts you'll use most often are memory and cpus (sizing, at creation), kernel_cmdline (per-VM loglevel, nokaslr, etc. — replaces the profile's cmdline; at boot), and files (inject config before init runs; at boot):
local vm = provium:
vm:
This is a subset — the full boot-opts table (types, defaults, rng_seed, initial_time) is in the VM reference.
Querying VM state #
The accessors you'll use most are vm:state() (returns "created", "booted", "paused", "shutdown", or "dead") and vm:is_quiescent() (true when there are no in-flight ops, open files, or open streams). The full accessor list — name, profile, cid, open_file_count, open_stream_count — is in the VM reference.
is_quiescent and the open-count accessors are useful for snapshot precondition asserts:
test
Snapshots #
local s = vm: -- writes to a tempfile
local s = vm: -- writes to that path
Returns a Snapshot userdata wrapping the path. Use it to:
- Restore later in the same test:
vm:shutdown(); vm:restore(s). - Inspect size:
s:size(). - Delete:
s:delete()(idempotent).
The snapshot file is what fixture builders return. If the snapshot fails because of an open stream, the error names the stream's creation site — close streams before snapshotting:
test
Restoring #
Restore from a Snapshot userdata or a bare path string:
vm:
vm: -- from snapshot userdata
vm: -- from path
The VM moves through Shutdown → Created → Booted (the restored state is already Booted). Restoring requires the VM to be in Created or Shutdown first.
Determinism patterns #
For tests that depend on randomness or wall-clock time, fix both at boot:
local vm = provium:
vm:
After boot, you can move time forward (or backward) with vm:clock():advance(N) — see Clock reference.
Pausing for inspection #
vm:pause() freezes the guest's vCPUs. Useful for:
- Time-sensitive tests where you need to read multiple bits of state without races.
- Snapshotting (the snapshot path will pause anyway, but explicit pause makes the test's intent clear).
vm:
local before = vm:
vm:
Reset and power-button #
vm:reset() warm-reboots the guest — same VM, same RAM image initially, then init re-runs. Stays in Booted.
vm:power_button() sends ACPI power-button. The guest's init handles it as a graceful shutdown signal (typically: stop services, sync filesystems, kernel halts). Ends in Shutdown.
test
Multi-VM topologies #
The two-VM pattern is the workhorse of networking tests:
local lan = provium:
local a = provium::
local b = provium::
lan:
a:
b:
test
For larger topologies, use sub-labs to keep names organised — see labs and scope.
See also #
- VM reference — every method, every option.
- Snapshot reference — snapshot/lab-snapshot userdata.
- provium.toml reference — profile configuration.
- Bridges and impairments — wiring VMs together.