Skip to content

libvirt-renderer — Architecture

┌─────────────────────────────────────────────────────────────────┐
│ POST /api/v1/vms/from-profile │
│ body: { name, uuid, profile_id, security_overlay, │
│ networking_overlay, vcpu_count, memory_mb, │
│ disks, networks, install_iso, auto_start } │
└────────────────────────────┬────────────────────────────────────┘
┌──────────────────────────────┐
│ vm_profiles::resolve(...) │
│ → ResolvedProfile │
│ (base + security overlay │
│ + networking overlay) │
└──────────────┬───────────────┘
┌──────────────────────────────┐
│ vm_profiles::validate(...) │
│ → host-cap check │
│ (nested virt, OVMF, swtpm, │
│ machine type, RAM, vcpu) │
└──────────────┬───────────────┘
┌──────────────────────────────┐
│ libvirt_renderer:: │
│ render_domain_xml(p, ctx) │
│ │
│ writer::XmlWriter (indent) │
│ elements::domain::render │
│ ├── metadata │
│ ├── memory │
│ ├── cpu │
│ ├── iothreads │
│ ├── os │
│ ├── features │
│ ├── clock │
│ ├── on_poweroff/reboot/... │
│ ├── pm │
│ ├── devices │
│ │ ├── controller │
│ │ ├── disk │
│ │ ├── network │
│ │ ├── input │
│ │ ├── graphics + video │
│ │ ├── sound │
│ │ ├── tpm │
│ │ ├── rng │
│ │ ├── channel │
│ │ ├── memballoon │
│ │ └── serial/console │
│ └── seclabel │
│ → String (UTF-8 XML) │
└──────────────┬───────────────┘
┌──────────────────────────────┐
│ synvirt_libvirt::virsh:: │
│ define_with_stdin(xml) │
│ → libvirt RNG validation │
│ → domain registered │
└──────────────┬───────────────┘
┌──────────────────────────────┐
│ optional: virsh start <name> │
│ if auto_start │
└──────────────────────────────┘

The libvirt RNG schema in domain.rng puts the children of <domain> inside an <interleave> block, but in practice libvirt + virt-manager emit them in a stable order. The renderer matches that order so a virsh dumpxml round-trip diffs cleanly against tools the operator might already be using:

  1. Identifying: name, uuid, title, description, metadata
  2. Memory: memory, currentMemory, memoryBacking
  3. Compute: vcpu, cpu, iothreads
  4. OS: os (with loader + nvram for UEFI)
  5. Features: acpi, apic, hyperv, smm, vmport
  6. Clock: clock with timer children
  7. Lifecycle: on_poweroff, on_reboot, on_crash
  8. Power: pm
  9. Devices: devices block (see below)
  10. Security: seclabel

Inside <devices> the order is:

  1. emulator
  2. controllers (pcie-root + ports → USB → virtio-scsi → virtio-serial)
  3. disks (regular → cdrom → floppy)
  4. network interfaces
  5. input devices (tablet, keyboard)
  6. graphics (SPICE → VNC)
  7. video
  8. sound + audio
  9. TPM
  10. RNG
  11. channels (qemu-guest-agent → SPICE agent)
  12. memballoon
  13. serial + console

The renderer is a flat function pipeline; there is no dynamic dispatch. Each elements/* module exposes one render(...) function with the same signature shape:

pub fn render(w: &mut XmlWriter, profile: &ResolvedProfile, …) -> Result<()>;

The XmlWriter is the only piece of state the modules share. It maintains depth, escapes attribute and text content, and produces a deterministic byte sequence so insta snapshots stay stable.

Profile field Renderer effect
chipset = Q35 emit pcie-root + pcie-root-port controllers
chipset = I440fx emit pci-root index 0
firmware = Uefi { secure_boot=true } OVMF.secboot loader + <smm state='on'/>
firmware = Uefi { secure_boot=false } OVMF_CODE_4M.fd loader
firmware = Bios omit <loader> and <nvram>
vtpm = Some emit <tpm><backend type='emulator'/></tpm>
hyperv = Some(flags) emit only the flags set on flags
clock.utc = false offset=‘localtime’ (pre-Vista Windows)
acpi = false omit <acpi/> and <apic/> (Win 95)
floppy = true emit <disk device='floppy'>
usb.controller = None <controller model='none' type='usb'/>
input.ps2_only = true PS/2 keyboard + mouse, no USB tablet
network.queues = MinVcpusCapped(N) <driver name='vhost' queues='min(vcpu,N)'/>
disk.iothread = PerDisk <iothreads>N</iothreads> + per-disk reference
disk.iothread = Pool(N) <iothreads>N</iothreads>
video.spice = true <graphics type='spice'> + SPICE channel
video.vnc = true additionally <graphics type='vnc'>
audio = None omit <sound> (NT 4.0)

Snapshots only stay stable if the renderer is order-deterministic. The writer enforces this:

  • Attributes are passed in alphabetical order at every callsite.
  • The orchestrator visits children in a fixed order.
  • The <interface> block emits exactly: mac → source → model → driver → vlan → mtu (only when mtu_default != 1500).
  • Optional sections that depend on profile flags expand or contract, but never reshuffle.

If snapshot diffs include attribute reorderings on bumps, the change violates the contract and should not be accepted. cargo insta review is the human gate.

Error HTTP Notes
E_RENDER_INVALID_PROFILE 404 unknown profile_id
E_RENDER_MISSING_OVMF 503 host needs ovmf installed
E_RENDER_MISSING_OVMF_VARS 503 host needs ovmf VARS template
E_RENDER_SWTPM_MISSING 503 host needs swtpm installed
E_RENDER_INVALID_DISK 400 bad source (file path missing, …)
E_RENDER_INVALID_NIC 400 bad NIC source
E_RENDER_LIBVIRT_REJECTED 400 libvirt RNG validation failed
E_RENDER_WRITER 500 writer panic (should not happen)
crates/daemon/src/main.rs
mod libvirt_renderer; ← module declaration
└── crates/daemon/src/libvirt_renderer/
├── mod.rs ← public functions
├── api.rs ← axum routes (merged in web/routes.rs)
├── error.rs ← E_RENDER_* enum
├── types.rs ← VmContext, DiskAttachment, …
├── writer.rs ← XmlWriter
└── elements/ ← per-section modules
  • ELEMENTS.md — element-by-element profile-field map.
  • EXAMPLES.md — full XML for representative profiles.
  • API.md — REST surface details.