libvirt
Creates, provisions, exports, and restores libvirt domains.
Domain Options
create, with, and
build take these options as keyword arguments. Those
marked * may repeat.
| Option | Type | Description |
|---|---|---|
image |
ArtifactSpec |
Base disk image, optionally gzip-, xz-, or zstd-compressed |
bundle |
ArtifactSpec |
Bundle written by Domain.export |
installer* |
InstallerSpec |
Install media to boot |
drivers* |
ArtifactSpec |
Driver media read by Setup |
name |
Str |
Domain name; generated when omitted |
os |
Sym |
Guest OS, using sys's symbol vocabulary |
arch |
Sym |
Guest CPU architecture; defaults to and must match the host |
app |
Str |
Cache and data namespace; derived from shell.program by default |
user |
Str |
Guest user to create |
dolang |
DolangSpec |
Do build to install |
memory |
Int |
Guest memory in MiB; defaults to the bundle or OS value |
vcpus |
Int |
Guest vCPU count; defaults to the bundle or OS value |
disk_size |
Str |
Disk size; defaults to the OS value, while a bundle keeps its size |
cache |
DiskCache |
Disk host cache mode |
io |
DiskIo |
Disk I/O submission mode |
connect_uri |
Str |
libvirt connection URI |
init |
InitSpec |
Early boot provisioning |
wait_timeout |
(Int \| Float) |
Seconds to wait for the guest; defaults to the OS timeout |
add* |
AddSpec |
Step: uploads a file |
run* |
() -> Value |
Step: runs a block in the guest VFS |
Exactly one of image, bundle, and installer is required.
bundle
A bundle produced by Domain.export: a ZIP holding
the domain's flattened disk and a manifest of the configuration that goes with
it. Restoring one skips provisioning entirely — no seed, no cloud-init, no
payload install — so the domain is ready as soon as it accepts SSH, and the
steps then run as usual.
A bundle already supplies os, arch, user, dolang, and init, so
passing any of them with bundle is an error. The SSH key a bundle's guest
trusts is the one generated for the app it was exported under, so app
must match; a mismatch is an error rather than a guest that never accepts a
connection.
installer
Install media for a guest that has no cloud image to lay an overlay over. The
disk starts blank and Setup writes it, driven by an unattended answer file
this module generates and puts on a provisioning disc of its own, alongside
the guest payload. Only os: :WINDOWS: installs this way.
The first installer is what the domain boots; any others are attached in
the order given, for media split across more than one disc. drivers media
is read by Setup and never booted — attach the virtio driver ISO here,
without which Setup does not see a virtio disk at all — and is an error
without installer.
libvirt.create
installer:
source: ./Win11_24H2_English_x64.iso
edition: Windows 11 Pro
drivers: ./virtio-win.iso
os: :WINDOWS:
What create returns is a guest sitting in audit mode with a working
sshd and VFS, and nothing else. Windows Update, hibernation and trim policy,
account removal, and sysprep /generalize are all decisions with no single
right answer, so they are the caller's, expressed as
steps.
init is an error here: it names cloud-init provisioning semantics, and an
unattended install has no cloud-init. The host needs swtpm, swtpm_setup,
and one of xorrisofs or mkisofs, all checked before anything is defined.
Steps
add and run options, and the bare positional item reboot, are steps.
They run after the guest accepts SSH, in the order written, including repeats
and interleaving:
libvirt.create
installer: ./Win11_24H2_English_x64.iso
drivers: ./virtio-win.iso
os: :WINDOWS:
run: do
run powercfg /h off
reboot
run: do
run defrag "C:" /L
reboot reboots the guest and waits for it. Power state belongs to this
module: a step must reboot with reboot rather than rebooting the guest
itself, and must not shut the guest down — build does that, and a step that
powers the guest off is an error rather than a case that is tolerated.
reboot is also the only place a lost connection is not an error; a guest
that vanishes during a run block propagates.
Types
| Type | Description |
|---|---|
Domain |
References a libvirt domain by name and connection URI. |
NoDomainError |
Raised when a requested libvirt domain does not exist. |
AddSpec |
A file to write into the guest, from an artifact or as direct content. |
ArtifactSpec |
A file this module fetches: a bare source, or a source with an
algorithm:hex digest to verify it against. |
Compress |
qcow2 compression for an exported disk. |
DiskCache |
A host cache mode for the domain disk. The hypervisor's own default, which
honors guest flushes, applies when none is given. A Str is matched
ignoring case. |
DiskIo |
A disk I/O submission mode. Defaults to :IO_URING:. A Str is matched
ignoring case. |
DolangSpec |
The Do build to install into a guest. |
DomainState |
A domain's state. |
InitAddSpec |
A file for cloud-init to write during early boot. owner, append, and
defer are passed to cloud-init's write_files. |
InitSpec |
Boot-time provisioning, interpreted by cloud-init on the guest's first boot, before any steps. Each key may repeat. |
InstallerSpec |
Install media, as an artifact spec that may also select an edition. |
AddSpec = (Dict[{target: Str, source: ArtifactSpec, ?chmod: Int | fs.unix.Mode}] | Dict[{target: Str, content: Str | Bin, ?chmod: Int | fs.unix.Mode}])
A file to write into the guest, from an artifact or as direct content.
chmod is a Unix file mode, and is rejected for a guest provisioned by
unattended install.
ArtifactSpec = (Str | fs.Path | url.Url | Dict[{source: Str | fs.Path | url.Url, ?digest: Str}])
A file this module fetches: a bare source, or a source with an
algorithm:hex digest to verify it against.
A Path source is verified in place. A Url is downloaded, verified, and
cached, and a previously verified digest-pinned entry is returned without
contacting the server.
Compress = (Bool | :ZSTD: | :ZLIB:)
qcow2 compression for an exported disk.
| Value | Behavior |
|---|---|
true |
Compress with the default algorithm, currently :ZSTD: |
:ZSTD: |
zstd; needs QEMU 5.1+ to read the result |
:ZLIB: |
zlib; readable by any QEMU, and around nine times slower to write |
false |
No compression |
The disk is compressed rather than its ZIP member. It stays compressed when extracted into the image store, where it backs every domain laid over it.
DiskCache = (:NONE: | :WRITEBACK: | :WRITETHROUGH: | :DIRECTSYNC: | :UNSAFE: | Str)
A host cache mode for the domain disk. The hypervisor's own default, which
honors guest flushes, applies when none is given. A Str is matched
ignoring case.
| Value | Behavior |
|---|---|
:NONE: |
O_DIRECT; no host page cache, guest flushes honored |
:WRITEBACK: |
Host page cache, guest flushes honored |
:WRITETHROUGH: |
Host page cache, every write flushed |
:DIRECTSYNC: |
O_DIRECT and every write flushed |
:UNSAFE: |
Host page cache, guest flushes ignored |
:UNSAFE: loses the disk's contents if the host or the domain dies
uncleanly, since nothing the guest wrote is guaranteed to have reached
storage until the domain shuts down. That is a fair trade for a disposable
build domain — dropping flushes removes them from the critical path, and
files written and deleted within one run need never reach storage at all —
but it is not a safe default for a domain whose disk matters.
DiskIo = (:IO_URING: | :THREADS: | :NATIVE: | Str)
A disk I/O submission mode. Defaults to :IO_URING:. A Str is matched
ignoring case.
| Value | Behavior |
|---|---|
:IO_URING: |
io_uring; needs QEMU 6.0+ on a 5.1+ kernel |
:THREADS: |
Thread pool |
:NATIVE: |
Linux AIO; only asynchronous with cache: :NONE: or :DIRECTSYNC: |
An older host fails at domain start rather than falling back, so select
:THREADS: there. :NATIVE: needs O_DIRECT to be asynchronous; paired
with a cache mode that uses the host page cache it quietly degrades to
blocking submission.
DolangSpec = (Str | fs.Path | url.Url | Dict[{version: Str}] | Dict[{source: Str | fs.Path | url.Url, ?digest: Str}])
The Do build to install into a guest.
Do is installed into every guest: dolang-vfs is the transport with,
upload, and download run over, and an unattended install has no guest at
all without it. This selects which build, not whether; when omitted, it is
the release matching this interpreter's version.
| Value | Installs |
|---|---|
a Str, e.g. "v0.1.1" |
The release with that version tag |
{version: tag} |
The same, spelled explicitly |
a Path or Url |
The given release archive |
| a dictionary spec | The given release archive, digest-pinned |
DomainState = (:RUNNING: | :IDLE: | :PAUSED: | :IN_SHUTDOWN: | :SHUTOFF: | :CRASHED: | :PMSUSPENDED: | :BLOCKED:)
A domain's state.
InitAddSpec = (Dict[{target: Str, source: ArtifactSpec, ?chmod: Int | fs.unix.Mode, ?owner: Str, ?append: Bool, ?defer: Bool}] | Dict[{target: Str, content: Str | Bin, ?chmod: Int | fs.unix.Mode, ?owner: Str, ?append: Bool, ?defer: Bool}])
A file for cloud-init to write during early boot. owner, append, and
defer are passed to cloud-init's write_files.
InitSpec = Dict[{...:add:: InitAddSpec, ...:run:: Str | Array[Str], ...:packages:: Str | Array[Str]}]
Boot-time provisioning, interpreted by cloud-init on the guest's first boot, before any steps. Each key may repeat.
| Key | Description |
|---|---|
add |
Writes a file |
run |
Runs a command, split with shlex when given as a Str |
packages |
Installs guest packages |
InstallerSpec = (Str | fs.Path | url.Url | Dict[{source: Str | fs.Path | url.Url, ?digest: Str, ?edition: Str}] | Dict[{source: Str | fs.Path | url.Url, ?digest: Str, ?index: Int}])
Install media, as an artifact spec that may also select an edition.
Multi-edition media otherwise stops to ask which edition to install, which on
an unattended install means stopping for good. edition selects one by name,
such as "Windows 11 Pro", and index by position in the media's image
list; they are alternatives.
Functions
attach name … -> Domain
Attaches to an existing domain.
Parameters
| Name | Type | Description |
|---|---|---|
name |
Str |
Domain name. |
:connect_uri? |
Str |
libvirt connection URI. |
Errors
NoDomainError if the domain does not exist.
build destination ...options … -> fs.Path
Provisions a domain, shuts it down cleanly, exports it as a bundle, and tears it down.
This is the minting half of the gold-image workflow: build produces the
bundle, and create bundle: consumes it. Keeping the two passes separate is
the point — a caller that provisions on a cache miss and restores on a hit
would otherwise run its real work against two different guests, one of them
freshly cloud-inited, and only discover the divergence on the rarer path.
Build the bundle in its own pass and always start from it.
Preparation is the caller's to specify through the same init:, add:, and
run: arguments create takes; run: in particular is where
anything the guest must do for itself belongs, such as reclaiming free space
before the disk is flattened.
Parameters
| Name | Type | Description |
|---|---|---|
destination |
(Str | fs.Path) |
Bundle to write; .dolvm is the
conventional extension. |
:compress? |
Compress |
qcow2 compression. |
...options |
Domain options and steps. |
Example
libvirt.build (CACHE_DIR / "freebsd-gold.dolvm")
image: freebsd.qcow2.xz
os: :FREEBSD:
init:
packages:
- git
create ...options -> Domain
Creates and starts a domain, provisioning it with cloud-init from a base
image:, installing it from installer: media, or restoring it from an
already-provisioned bundle:.
Parameters
| Name | Type | Description |
|---|---|---|
...options |
Domain options and steps. |
Errors
| Error | Condition |
|---|---|
ValueError |
None or more than one of image, bundle, and installer are given, os is omitted with image, an argument is given that bundle already answers or that the guest's provisioning method cannot act on, arch is not the host's, the bundle was exported from another app, or a domain named name already exists |
RuntimeError |
No forwarded SSH port could be allocated, or a host program the requested configuration needs is not on PATH |
TimedOutError |
The guest was not reachable within wait_timeout |
TypeError |
Invalid argument type |
Example
let domain = libvirt.create
image: freebsd.qcow2.xz
os: :FREEBSD:
name: freebsd-build
init:
packages:
- sqlite3
add:
target: /etc/example.conf
content: |
enabled=yes
run: do
run uname -a
echo $domain.name
Restoring a bundle instead, which runs no provisioning:
let domain = libvirt.create
bundle: ./freebsd-gold.dolvm
app: dolang-libvirt-test
name: freebsd-build
run: do
run uname -a
with[R] block ...options -> R
Creates a domain from an image: or a bundle:, runs block through its
VFS, and tears it down — even if block throws.
Parameters
| Name | Type | Description |
|---|---|---|
block |
(() -> R) |
Block to run in the guest VFS. |
...options |
Domain options and steps. |