Skip to content

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

TypeDescription
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.

libvirt.create
  image:
    source: https://example.com/disk.qcow2
    digest: blake3:0123...
  os: :LINUX:

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

NameTypeDescription
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

NameTypeDescription
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

NameTypeDescription
...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

NameTypeDescription
block (() -> R) Block to run in the guest VFS.
...options Domain options and steps.

Example

libvirt.with
  image: freebsd.qcow2.xz
  os: :FREEBSD:
  do
    run uname -a