On this page
- Constructing
- State machine
- Boot opts
- Lifecycle methods
- vm:boot(opts?)
- vm:pause()
- vm:resume()
- vm:shutdown()
- vm:reset()
- vm:power_button()
- vm:close()
- Snapshot and restore
- vm:snapshot(path?)
- vm:restore(snap_or_path)
- Layer-1 ops
- vm:run(cmd_or_args, opts?)
- vm:run_async(cmd, opts?)
- vm:read_file(path)
- vm:write_file(path, data)
- vm:push_file(host_path, guest_path, opts?)
- vm:stat(path)
- vm:listdir(path)
- vm:mkdir(path, opts?)
- vm:unlink(path)
- vm:rename(from, to)
- vm:open_file(path, mode_table)
- vm:tail_file(path, opts?)
- vm:fd_stream(fd_or_file)
- Layer-0 ops
- vm:syscall(nr, ...)
- vm:ioctl(fd, cmd, data?, opts?)
- Hypervisor resources
- vm:nic(name)
- vm:disk(id)
- vm:attach_disk(opts?)
- Console and clock
- vm:console()
- vm:clock()
- Workers
- vm:spawn_worker(opts?)
- Batch operations
- vm:batch(fn)
- Accessors
- vm:name() — Returns the VM's name as given to lab:vm.
- vm:profile() — Returns the profile name.
- vm:state() — Returns "created", "booted", "paused", "shutdown", or "dead".
- vm:cid() — Returns the assigned vsock CID, or nil before boot.
- vm:is_quiescent() — true if no in-flight ops, no open files or streams.
- vm:open_file_count() — Number of files currently open through the agent.
- vm:open_stream_count() — Number of streams currently active (tails, fd-streams, console-reads, captures).
- RunResult
- See also
VM
A VM userdata wraps one guest. You get one from lab:vm("name", "profile") (or the dot-sugar lab.<name> after creation), and every operation against the guest dispatches through it.
VM handles are cheap to clone — passing them around or storing them in tables doesn't duplicate state.
Constructing
| Source | Returns |
|---|---|
lab:vm("name", "profile") |
New VM in lab, in the Created state. |
lab:vm("name", "profile", opts) |
Same, with boot opts staged for the next :boot(). See boot opts. |
lab:vm("name") |
Lookup the VM previously declared with that name. Errors if absent. |
lab.name |
Same lookup as lab:vm("name"), returns nil on miss. |
provium:vm_fixture("path") |
Restored from a cached fixture; already booted. |
State machine
Created ──:boot()──> Booted ──:pause()──> Paused
│ │
├─:reset()→Booted ├─:resume()→Booted
├─:power_button()──> Shutdown
└─:shutdown()─────> Shutdown
vm:state() returns "created", "booted", "paused", "shutdown", or "dead". Operations against a VM in the wrong state error cleanly with the offending state in the message.
Boot opts
Pass as the third arg to lab:vm(name, profile, opts) (creation-time defaults) or as the only arg to vm:boot(opts) (per-boot overrides). Per-boot opts merge with creation-time opts.
| Key | Type | Description |
|---|---|---|
memory |
int (bytes) or string "512M"/"2G" |
VM memory cap. |
cpus |
int | vCPU count. |
kernel_cmdline |
string | Replaces the profile's cmdline. |
rng_seed |
int (u64) | Seeds the guest's virtio-rng. Use for determinism. |
initial_time |
int or float (seconds since epoch) | Sets the guest's wall clock at boot. |
files |
array of {path=string, content=string} |
Files to inject into the guest's filesystem before init runs. |
Example:
local vm = provium:vm("v", "peios", {
memory = "2G",
cpus = 4,
rng_seed = 0xdeadbeef,
initial_time = 1700000000,
files = {
{path = "/etc/hostname", content = "v"},
},
})
vm:boot()
Lifecycle methods
vm:boot(opts?)
Boots the guest. Returns self so calls chain (provium:vm("a", "peios"):boot()). Optional opts table merges into the boot opts staged at creation time. Errors if already booted.
Emits a vm_spawned event after the guest is up and the agent has handshaken. The event payload carries {file, vm_name, profile, memory_bytes, cid}.
vm:pause()
Pauses the guest's vCPUs. Returns nothing. Errors if not in Booted.
vm:resume()
Resumes a paused guest. Returns nothing. Errors if not in Paused.
vm:shutdown()
Tears down the guest. Idempotent in the sense that Created, Booted, Paused, and Dead all transition to Shutdown. Emits a vm_shutdown event with {file, vm_name, duration_ns}.
vm:reset()
Warm reboot — guest re-runs its boot path. Errors if not in Booted. The post-reset state is still Booted.
vm:power_button()
Sends ACPI power-button to the guest. Triggers a graceful shutdown sequence. Errors if not in Booted. Post-call state is Shutdown.
vm:close()
Auto-close hook used by the resource walker. Idempotent. Calls :shutdown() if not already in Shutdown. Test code rarely calls this directly — the harness fires it for every VM at file end (or per-test if provium.reset_between_tests = true).
Snapshot and restore
vm:snapshot(path?)
Take a snapshot. With an explicit path, writes there; without, writes to a tempfile. Returns a Snapshot userdata wrapping the path.
local s = vm:snapshot()
local s2 = vm:snapshot("/tmp/before-mutation.snap")
vm:restore(snap_or_path)
Restore from a Snapshot userdata (preferred) or a bare path string. Errors if not in Created or Shutdown.
vm:shutdown()
vm:restore(s)
Layer-1 ops
Layer-1 ops are the high-level "do it like SSH" primitives. They dispatch through the agent's exec / file / stat handlers.
vm:run(cmd_or_args, opts?)
Run a command. Two forms:
vm:run("echo hi")— runs through/bin/sh -c "echo hi". Shell metacharacters work.vm:run("echo", {"hi"})— direct exec. No shell. The second arg is a positional-arg array.vm:run("echo", {args={"hi"}, env={K="v"}, cwd="/tmp", stdin="…", env_clear=true, timeout="5s"})— opts form. Auto-detected by the presence of any non-array key. Mixed array entries withargs=are not allowed; use one form or the other.
Returns a RunResult userdata.
| Opts key | Type | Description |
|---|---|---|
args |
array of strings | Direct positional args (only used with the table-as-opts form). |
env |
string→string map | Environment variables. |
env_clear |
bool | When true, the guest sees only env; otherwise env merges with the agent's environment. |
cwd |
string | Working directory inside the guest. |
stdin |
string | Bytes piped to the process's stdin. |
timeout_ms |
int | Hard wall-clock timeout in milliseconds. |
timeout |
int / float / string | Same, expressed as seconds or "5s" / "500ms" / "2m" / "1h". Negative or NaN errors. |
vm:run_async(cmd, opts?)
Like :run, but returns a Process userdata immediately. The agent does not auto-kill; you control the lifetime via proc:kill() / proc:wait(). Passing timeout here is rejected — use proc:wait(timeout) instead.
vm:read_file(path)
Returns the file contents as a Lua string. Errors on agent-side read failure (ENOENT, EACCES, etc.).
vm:write_file(path, data)
Replaces the file's contents with data. Creates the file if absent. Returns nothing.
vm:push_file(host_path, guest_path, opts?)
Read a file from the host filesystem and write its bytes to guest_path in the guest. Inside a fixture, the host file is folded into the fixture's cache key automatically — rebuilding the host file (typical case: a binary under test) invalidates the snapshot. Relative host_path is resolved against the directory of the file containing the call. Pass {auto_dep = false} to skip the auto-fold for a single call. See files and handles and fixtures and dependencies.
vm:stat(path)
Returns a table:
| Field | Type | Description |
|---|---|---|
size |
int (bytes) | File size. |
mtime |
float (seconds since epoch) | Modification time. |
mtime_ns |
int (ns since epoch) | Same, full precision. |
perm |
int | POSIX mode bits (≤ 4095, i.e. 0o7777). |
entry_type |
string | One of "file", "directory", "symlink", "fifo", "socket", "block_device", "char_device", "other". |
vm:listdir(path)
Returns an array of {name=string, entry_type=string} tables.
vm:mkdir(path, opts?)
Create a directory. Opts: {parents=bool, perm=int}. With parents=true, intermediate directories are created (mkdir -p). perm is the POSIX mode for the new directory.
vm:unlink(path)
Remove a file or empty directory.
vm:rename(from, to)
Atomic rename within the guest filesystem.
vm:open_file(path, mode_table)
Returns a File userdata.
The mode table accepts:
| Key | Type | Effect |
|---|---|---|
read |
bool | Open for reading. |
write |
bool | Open for writing. |
create |
bool | Create if absent. |
truncate |
bool | Truncate to zero bytes on open. |
append |
bool | Append-only writes. |
exclusive |
bool | Combine with create=true to require the file not already exist (O_EXCL). |
perm |
int | POSIX mode for newly-created files. |
At least one of read, write, or append must be true; an empty mode table errors at open time with a pointer.
vm:tail_file(path, opts?)
Open a streaming subscription to a file. Returns a Tail userdata. Opts: {start = "beginning" | "end" | <offset>}. Default is "end" — only bytes appended after the call are streamed. <offset> may be a non-negative integer (absolute byte offset), a negative integer (N bytes before EOF — provium stats and resolves), or a finite float (truncated toward zero, same sign semantics).
vm:fd_stream(fd_or_file)
Open a streaming subscription to an existing file handle. Accepts either an integer handle id (from file:fd()) or a File userdata directly. Returns a Tail.
Layer-0 ops
Layer-0 ops are direct kernel-level primitives — syscall(2) and ioctl(2). Use these to exercise driver paths or to test syscall semantics directly.
vm:syscall(nr, ...)
Two call shapes:
- Integer-only:
vm:syscall(nr, a1, a2, a3, a4, a5, a6)— up to 6 integer arguments. - Table form:
vm:syscall(nr, {args={a1, a2, …}, bufs={"…", …}, ptrs={1, 3}}). Buffers are byte arrays;ptrs[i]says which arg slot (0-indexed)bufs[i]should be spliced into. The buffer's address is filled in for the kernel.
Returns a table:
| Field | Type | Description |
|---|---|---|
ret |
int | Syscall return value. |
result |
int | Same as ret (alias for clarity). |
errno |
int | Errno if the syscall returned negative; 0 otherwise. |
out_bufs |
array of strings | Post-syscall contents of the supplied bufs. Useful for read-style syscalls. |
vm:ioctl(fd, cmd, data?, opts?)
Direct ioctl(2). fd and cmd are integers. data is an optional byte string passed as the third arg's pointer. opts.bufs and opts.ptr_offsets work like :syscall's bufs / ptrs.
Returns {ret, result, out_data, out_bufs}. out_data is the post-call value of data (for ioctls that write back through the same buffer).
Hypervisor resources
vm:nic(name)
Returns a Nic userdata for the named bridge attachment, or for a guest-style interface name (eth0, enp0s3, ens3). Guest-name lookup maps to the Nth attached bridge sorted by bridge name.
vm:disk(id)
Returns a Disk userdata for an already-attached disk. Errors if no disk with that id.
vm:attach_disk(opts?)
Attaches a new disk and returns a Disk userdata. Opts: {id=string, size=int, image=string}. Defaults: id = attached-<vm_name>, size = 4 GiB, image = no backing file.
Console and clock
vm:console()
Returns a Console userdata.
vm:clock()
Returns a Clock userdata.
Workers
vm:spawn_worker(opts?)
Spawns a sub-agent connection so test code can dispatch concurrent ops against the same guest. Returns a Worker userdata. Opts are reserved for future use; passing {thread=true} errors with a future-work pointer.
Batch operations
vm:batch(fn)
Collect multiple ops inside fn(b) and dispatch them as one wire round-trip. b exposes:
b:run(...)(with the same shape asvm:run)b:read_file(path)b:write_file(path, data)b:stat(path)b:listdir(path)b:mkdir(path, opts)b:unlink(path)b:rename(from, to)b:syscall(nr, a, b, …)— integer-args form only; the table form is rejected here.
Returns a Lua array of {ok=value} or {err=msg} entries, one per op. A failure on op N does not short-circuit the rest of the batch.
local results = vm:batch(function(b)
b:write_file("/tmp/a", "1")
b:write_file("/tmp/b", "2")
b:read_file("/tmp/a")
b:stat("/tmp/missing")
end)
-- results = {{ok=nil}, {ok=nil}, {ok="1"}, {err="No such file or directory"}}
Accessors
vm:name() — Returns the VM's name as given to lab:vm.
vm:profile() — Returns the profile name.
vm:state() — Returns "created", "booted", "paused", "shutdown", or "dead".
vm:cid() — Returns the assigned vsock CID, or nil before boot.
vm:is_quiescent() — true if no in-flight ops, no open files or streams.
vm:open_file_count() — Number of files currently open through the agent.
vm:open_stream_count() — Number of streams currently active (tails, fd-streams, console-reads, captures).
RunResult
The return value of vm:run(...) (and worker:run(...), proc:wait()).
Fields:
| Field | Type | Description |
|---|---|---|
exit_code |
int | Exit code. -1 for signalled, -2 for timed_out. Use signal and timed_out to disambiguate. |
stdout |
string | Captured stdout. |
stderr |
string | Captured stderr. |
status |
string | "exited", "signalled", "timed_out". |
timed_out |
bool | true if the timeout fired. |
signal |
int or nil | Signal number when status is signalled, else nil. |
Methods:
result:ok()— convenience forresult.exit_code == 0.result:assert_ok()— raises if not OK; the error message includes status, stdout, and stderr (with signal name when signalled).