github dmacvicar/terraform-provider-libvirt v0.9.1

7 hours ago

Bugfixes

  • Domains are now undefined with flags VIR_DOMAIN_UNDEFINE_NVRAM and VIR_DOMAIN_UNDEFINE_TPM. These defaults may change in the future and be part of a domain block like create and delete. (#1203 )
  • Fix SIGSEGV when connecting to a hypervisor over qemu+ssh. Fixes #1210 (#1211)
  • Misc documentation fixes #1209, #1210

Features

Support for full libvirt API (XML) (#1208 )

The provider now supports the whole libvirt API 🥳 (* that is supported by libvirtxml), thanks to a code generation engine which generates the whole terraform glue for the schemas and conversions.

For now, the usual resources (domain, network, volume, pool) are included, but this opens the door to handle other resources (secrets, etc) with little effort.

Migration Guide: 0.9.0 → v0.9.1

⚠️ as the schema is now generated, the documentation is now injected into the code generation. As there is no machine readable documentation for libvirt XML, we generated a set of documentation metadata using AI. This process can be improved over time.

Due to the introduction of the generator and some bugs in the 0.9.0 schema, we had to do some changes in the schema.

This document explains how to move Terraform configurations from provider v0.9.0 (the last manual schema) to the current HEAD that uses the libvirt-schema code generator. It only covers resources and attributes that existed in 0.9.0: domains, networks, storage pools, and storage volumes. Anything new that HEAD exposes can simply be added following the generated schema documentation.

What Changed Globally

  1. Attr names now mirror libvirt XML – the generator emits snake_case names derived from the XML schema (e.g., accessmodeaccess_mode, portgroupport_group). Set exactly the fields you care about; anything left null stays absent in the XML.
  2. Value/unit pairs are explicit – whenever libvirt exposes a value with a unit attribute the provider now has two attributes (memory + memory_unit, capacity + capacity_unit, etc.). Leaving the unit unset lets libvirt use its default.
  3. Presence/"yes"/"no" semantics follow libvirt – booleans that previously toggled simple structs may now expect yes/no strings when libvirt models them as attributes (e.g. os.loader_readonly). True presence booleans (like features.acpi) still use Terraform bools.
  4. Nested objects match the XML tree exactly – device sources, interfaces, backing stores, etc. now use the full nested structure. Plan to touch every place where v0.9 flattened things like source.pool or filesystem.source.
  5. Metadata is structured – string blobs became { metadata = { xml = <<EOF ... } } so we can extend later without breaking state.

Domain Resource

Top-level attribute mapping

v0.9 attribute HEAD attribute(s) Notes
unit memory_unit Same semantics, renamed so every value/unit pair is consistent.
max_memory maximum_memory Value only; use maximum_memory_unit if you previously used a non-default unit.
max_memory_slots maximum_memory_slots Same semantics.
current_memory current_memory + optional current_memory_unit Value stays the same; set the unit explicitly if you relied on non-default units.
metadata (string) metadata = { xml = <<EOF ... EOF } Wrap your XML in the nested object.
os.arch os.type_arch The type_* prefix mirrors <os><type arch="..."/>.
os.machine os.type_machine Same rationale as above.
os.kernel_args os.cmdline Field name matches the XML <cmdline> element.
os.loader_path os.loader 0.9 kept the loader path in a separate attribute; now it is the element’s value (see “value + attributes” below).
os.loader_readonly (bool) os.loader_readonly (string) Accepts "yes"/"no" because the XML attribute is a string.
os.nvram.* os.nv_ram = { file, template, format = { type = ... } } Rename plus richer structure.
devices.filesystems[*].accessmode access_mode All camelCase names were converted to snake_case.
devices.filesystems[*].readonly read_only Same semantics.
devices.interfaces[*].source.portgroup source = { network = { port_group = ... } } See below for the full source mapping.
devices.rngs[*].device backend = { random = "/dev/urandom" } or backend = { egd = { ... } } Backends are now modeled exactly like the XML.

OS block specifics

  • os.boot_devices is still a list, but if you previously stored strings you now provide objects: boot_devices = [{ dev = "hd" }, { dev = "network" }].
  • Loader/read-only/secure/stateless flags now accept the literal XML strings ("yes"/"no"). Wrap them in tostring() if you had boolean locals.
  • NVRAM becomes os = { nv_ram = { file = "/var/lib/libvirt/nvram.bin", template = "/usr/share/OVMF/OVMF_VARS.fd", format = { type = "raw" } } }.

Loader value + attributes

<loader> is a “value + attributes” element. The path is the value (os.loader), and every XML attribute becomes a sibling attribute:

os = {
  loader          = "/usr/share/OVMF/OVMF_CODE.fd"
  loader_type     = "pflash"
  loader_readonly = "yes"
  loader_secure   = "no"
  loader_format   = "raw"
}

Leave the attribute unset to let libvirt pick its default (the provider preserves user intent for optional attributes).

Disks and filesystems

0.9 flattened every disk source. HEAD requires you to pick the XML variant explicitly:

# v0.9
source = {
  pool   = libvirt_pool.test.name
  volume = libvirt_volume.test.name
}

# HEAD
source = {
  volume = {
    pool   = libvirt_pool.test.name
    volume = libvirt_volume.test.name
  }
}

# File-based disk (previously source.file)
source = { file = "/var/lib/libvirt/images/disk.qcow2" }

# Block device
# Block device
source = { block = "/dev/sdb" }

Filesystems follow the same pattern. Replace the old flat fields with nested objects:

# v0.9
filesystems = [{
  source     = "/exports/share"
  target     = "shared"
  accessmode = "mapped"
  readonly   = true
}]

# HEAD
filesystems = [{
  source = { mount = { dir = "/exports/share" } }
  target = { dir = "shared" }
  access_mode = "mapped"
  read_only   = true
}]

Variant notation

Every <source> element with mutually exclusive children (files, volumes, blocks, etc.) becomes an object whose attributes map 1:1 to the libvirt XML children. Only set the branch you need:

# Filesystem backed by a block device
source = { block = { dev = "/dev/vdb" } }

# RAM-backed filesystem with extra attributes
source = { ram = { usage = 1024, unit = "MiB" } }

Even if a variant has additional attributes in XML, the generated struct exposes them in that nested object (e.g., ram = { usage = 1024, unit = "MiB" }). This pattern is consistent across disks, filesystems, host devices, etc.

Interfaces

source.network, source.bridge, and source.dev are now mutually exclusive nested objects. Example conversions:

# Network-backed NIC (v0.9)
source = { network = "default" }

# HEAD
source = { network = { network = "default" } }

# Direct/Macvtap NIC (v0.9)
source = { dev = "eth0", mode = "bridge" }

# HEAD
source = { direct = { dev = "eth0", mode = "bridge" } }

portgroup became port_group, wait_for_ip stays the same helper object.

RNG / TPM / other devices

  • RNG devices now mirror <backend>. Use backend = { random = "/dev/urandom" } for /dev/random or backend = { egd = { source = { mode = "connect", host = "unix", service = "..." } } } for EGD sockets.
  • TPM backends are nested (backend = { emulator = { path = "/var/lib/swtpm/sock" } }). Map your previous backend_type to one of the backend objects: emulator, passthrough, or external.
  • Graphics, consoles, serials, and video devices already used nested objects in 0.9; the only change is snake_case attribute names (auto_port, websocket, etc.).

Metadata

0.9 stored raw XML as a string. Now wrap it:

metadata = {
  xml = <<EOFXML
<libosinfo:libosinfo xmlns:libosinfo="http://libosinfo.org/xmlns/libvirt/domain/1.0">
  <libosinfo:os id="http://libosinfo.org/linux/2024" />
</libosinfo:libosinfo>
EOFXML
}

Storage Volume Resource

Key differences:

v0.9 attribute HEAD attribute(s) Notes
format (string) target = { format = { type = "qcow2" } } The format lives under the target block now.
permissions.* target.permissions.* Same keys, just explicitly under target.
backing_store.format backing_store = { format = { type = "qcow2" } } Mirrors libvirt <format> element.
capacity capacity + optional capacity_unit Leave capacity_unit unset to keep KiB.
allocation allocation + allocation_unit (read-only) Useful when libvirt reports GiB/MiB units.
path (computed) still path, but it mirrors target.path You no longer set this manually. Use pool target paths to control locations.

Everything else (name, pool, create/content) behaves exactly like 0.9. Plan/apply will touch terraform state automatically once you update the config.

Storage Pool Resource

The generated schema simply fills in additional optional sub-objects (source.host, source.auth, features, etc.). All attributes that existed in 0.9 keep their names and shapes:

  • target = { path = "/var/lib/libvirt/pools" } works unchanged.
  • target.permissions.* still take strings, not integers.
  • source.device = [{ path = "/dev/sdb" }] keeps the same structure.

Unless you opt into the new nested fields you do not need to change existing pool configurations.

Network Resource

v0.9 attribute HEAD attribute(s) Notes
mode forward = { mode = "nat" } The forwarding mode now lives under the <forward> element.
bridge (string) bridge = { name = "virbr0" } Bridge attributes are grouped together so libvirt can add more knobs.
autostart still autostart Works the same.
ips still ips, but nested attr names now snake_case (local_ptr, dhcp.hosts, etc.) Structures are the same; you may only need to rename portgroupport_group inside DHCP hosts.

Example conversion:

# v0.9
mode   = "nat"
bridge = "virbr1"

# HEAD
forward = { mode = "nat" }
bridge  = { name = "virbr1" }

DHCP ranges/hosts did not change other than automatic snake_case normalisation.

Contributors

Don't miss a new terraform-provider-libvirt release

NewReleases is sending notifications on new releases.