This provider allows managing libvirt resources (virtual machines, storage pools, networks) using Terraform. It communicates with libvirt using its API to define, configure, and manage virtualization resources.
This is a complete rewrite of the legacy provider. The legacy provider (v0.8.x and earlier) is maintained in the v0.8 branch. Starting from v0.9.0, all releases will be based on this new rewrite.
This rewrite improves upon the legacy provider in several ways:
- API Fidelity - Models the libvirt XML schemas directly instead of abstracting them, giving users full access to libvirt features. Schema coverage is bounded by what libvirtxml supports.
- Current Framework - Built with Terraform Plugin Framework, as the SDK v2 used in the legacy provider is deprecated
- Best Practices - Follows HashiCorp's provider design principles
| Resource | Status | XML Coverage |
|---|---|---|
libvirt_domain |
β Supported | Full coverage of libvirtxmlβs domain schema (devices, CPU, memory, features, RNG, TPM, etc.). |
libvirt_network |
β Supported | Full coverage of libvirtxml network schema (forwarding modes, bridge, DHCP, VLAN, virtual ports, etc.). |
libvirt_pool |
β Supported | Full coverage of libvirtxml storage pool schema (dir/logical/iscsi/etc.). |
libvirt_volume |
β Supported | Full coverage of libvirtxml storage volume schema (target, backing_store, encryption, timestamps). |
Everything exposed in these resources maps directly to the corresponding libvirt XML; if libvirtxml adds new fields we regenerate and pick them up automatically. Additional libvirt resources (secrets, nodes, interfaces, etc.) will be added later following the same patternβsee TODO.md for the roadmap.
- Schema Coverage: We support all fields that
libvirt.org/go/libvirtxmlimplements from the official libvirt schemas (located at/usr/share/libvirt/schemas/). If libvirtxml doesn't support a feature yet, neither do we - we don't create custom XML structs. - No Abstraction: The Terraform schema mirrors the libvirt XML structure as closely as possible, providing full access to underlying features rather than simplified abstractions.
- User Input Preservation: For optional+computed fields, we preserve the user's input value even when libvirt normalizes it (e.g., "q35" vs "pc-q35-10.1") to avoid unnecessary diffs.
This provider maps libvirt's XML structure to Terraform's HCL configuration language using a consistent, predictable pattern: XML elements become nested attributes, attributes/text become scalar attributes, repeated elements become lists, and elements with chardata plus attributes flatten into sibling fields. The examples below show these rules in practice.
Libvirt XML:
<domain type="kvm">
<name>example-vm</name>
<memory unit="MiB">512</memory>
<vcpu>1</vcpu>
<clock offset="utc">
<timer name="rtc" tickpolicy="catchup">
<catchup threshold="123" slew="120" limit="10000"/>
</timer>
<timer name="pit" tickpolicy="delay"/>
</clock>
</domain>Terraform HCL:
resource "libvirt_domain" "example" {
name = "example-vm"
type = "kvm"
memory = 512 # Flattened from <memory unit='MiB'>512</memory>
memory_unit = "MiB" # Optional, defaults based on libvirt
vcpu = 1 # Flattened from <vcpu>1</vcpu>
clock {
offset = "utc"
timer {
name = "rtc"
tickpolicy = "catchup"
catchup {
threshold = 123
slew = 120
limit = 10000
}
}
timer {
name = "pit"
tickpolicy = "delay"
}
}
}- Elements β Nested Attributes
<clock><timer name="rtc"/></clock>clock = { timers = [{ name = "rtc" }] }- Value-with-Unit Flattening
<memory unit='KiB'>524288</memory>memory = 524288
memory_unit = "KiB"Leave the _unit attribute unset to let libvirt pick its default.
- Boolean to Presence
<acpi/>features = { acpi = true }These fields are presence-only: false (or null) omits the XML element entirely.
- Type-Dependent Sources
<source file="/var/lib/libvirt/images/disk.qcow2"/>source = { file = "/var/lib/libvirt/images/disk.qcow2" }Only set the branch you need (file, block, volume = { ... }, etc.).
- Lists Map to Arrays
<disk/><disk/><disk/>disks = [{ ... }, { ... }, { ... }]Order matters: Terraform diffs trigger when you reorder array elements.
Snake_case naming: Every XML element/attribute name is converted to snake_case automatically (e.g.,
accessmodeβaccess_mode,portgroupβport_group). Common acronyms stay intact (MAC,UUID,VLAN, etc.), so libvirtβsMACAddressbecomesmac_address, notm_a_c_address.
Example with Devices (Disks and Network Interfaces):
Libvirt XML:
<domain type="kvm">
<name>example-vm</name>
<memory unit="MiB">512</memory>
<vcpu>1</vcpu>
<devices>
<disk type="file" device="disk">
<source file="/var/lib/libvirt/images/disk.qcow2"/>
<target dev="vda" bus="virtio"/>
</disk>
<interface type="network">
<source network="default"/>
<model type="virtio"/>
</interface>
</devices>
</domain>Terraform HCL:
resource "libvirt_domain" "example" {
name = "example-vm"
type = "kvm"
memory = 512
vcpu = 1
devices = {
disks = [
{
source = {
file = "/var/lib/libvirt/images/disk.qcow2"
}
target = {
dev = "vda"
bus = "virtio"
}
}
]
interfaces = [
{
model = {
type = "virtio"
}
source = {
network = {
network = "default"
}
}
}
]
}
}In this mapping:
devices.disks.sourceis a nested object whose attributes (e.g.,file,pool,volume,block) mirror the<source>element attributes in libvirt XML. Only one source variant may be provided at a time.devices.disks.targetis a nested object withdevand optionalbus, matching<target dev="..." bus="..."/>.- Disk backing chains are configured on the storage volume (
libvirt_volume.backing_store); libvirt ignores<backingStore>input on domains unless the hypervisor advertises thebackingStoreInputcapability.
Some libvirt XML elements have both text content and attributes. For better ergonomics, we apply these patterns:
XML: <memory unit="MiB">512</memory>
The unit is fixed and the value becomes a simple attribute:
memory = 512 # Always MiBThis applies to all scaledInteger fields (memory, hard_limit, soft_limit, etc.). We pick a sensible default unit per field.
XML: <maxMemory unit="MiB" slots="16">2048</maxMemory>
The value is flattened with a fixed unit, the other attribute becomes a separate field:
max_memory = 2048
max_memory_slots = 16XML: <vcpu placement="static" cpuset="0-3" current="2">4</vcpu>
A nested block is used with the value and all attributes:
vcpu {
value = 4
placement = "static"
cpuset = "0-3"
current = 2
}When a source element has different attribute sets depending on a type, we use a nested block:
XML:
<interface type="network">
<source network="default" portgroup="web"/>
</interface>HCL:
interface {
type = "network"
source = {
network = {
network = "default"
portgroup = "web"
}
}
}
#### Interface Schema Migration
The legacy provider exposed network interfaces with flattened attributes such as:
```hcl
devices = {
interfaces = [{
type = "network"
model = "virtio"
source = {
network = "default"
}
}]
}This rewrite models the libvirt XML more faithfully. Each interface entry is now a nested object where model, source, and type-specific fields map directly to their XML counterparts:
devices = {
interfaces = [
{
model = {
type = "virtio"
}
source = {
network = {
network = "default"
}
}
wait_for_ip = {
timeout = 300
source = "lease"
}
},
{
source = {
direct = {
dev = "eth0"
mode = "bridge"
}
}
}
]
}Instead of a type attribute, the provider derives the interface type from the populated source variant (network, direct, bridge, etc.). This keeps the Terraform schema aligned with libvirtxml and unlocks all interface features (macvtap, passthrough, portgroups, wait_for_ip) without ad-hoc flattening.
If the source always has the same pattern, it can be flattened to a simple attribute.
- We don't distinguish between XML attributes and elements in HCL - both become HCL attributes
- The same XML structure always maps to the same HCL structure
- This consistent mapping enables automated migration from the legacy provider or from raw libvirt XML
- Nested Attributes vs Blocks: Following HashiCorp's guidance, new features use nested attributes (e.g.,
devices = { ... }) instead of blocks. Some existing features (os,features,clock, etc.) incorrectly use blocks and need conversion (see TODO.md).
For detailed XML schemas, see the libvirt domain format documentation.
Terraform providers are largely scaffolding and domain conversion (Terraform HCL β Provider API). This project leverages AI agents to accelerate development while maintaining code quality through automated linting and testing.
To reduce boilerplate and ensure consistency, this provider uses a code generation system that automatically produces:
- Terraform models with proper
tfsdktags - Plugin Framework schemas with correct types and optionality
- XML conversion functions implementing the "preserve user intent" pattern
Benefits:
- Reduces manual coding by ~80% for new fields
- Ensures uniform implementation across resources
- Automatically maintains 1:1 mapping with libvirtxml
- Implements best practices consistently
Current Status:
- β
Core resources (
libvirt_domain,libvirt_volume,libvirt_network,libvirt_pool) use 100% generated models, schemas, and conversions. - π§© In progress: documentation/validator generation, polishing remaining manual helpers (wait_for_ip overrides, provider-specific fields).
- π Next: leverage RelaxNG data for automatic validators/docs and extend generation to future resources (secrets, node devices, etc.).
Running the generator:
go run ./internal/codegenFor detailed architecture, usage, and extension guide, see internal/codegen/README.md.
git clone https://github.com/dmacvicar/terraform-provider-libvirt
cd terraform-provider-libvirt
make buildOr manually:
go build -o terraform-provider-libvirtTo install the provider locally:
make installThis installs to ~/.terraform.d/plugins/registry.terraform.io/dmacvicar/libvirt/dev/linux_amd64/
terraform {
required_providers {
libvirt = {
source = "dmacvicar/libvirt"
}
}
}
provider "libvirt" {
uri = "qemu:///system"
}
resource "libvirt_domain" "example" {
name = "example-vm"
memory = 512
memory_unit = "MiB"
vcpu = 1
os {
type = "hvm"
arch = "x86_64"
machine = "q35"
}
}The provider supports multiple connection transports:
# Local system socket
provider "libvirt" {
uri = "qemu:///system"
}
# Remote via SSH (Go library)
provider "libvirt" {
uri = "qemu+ssh://[email protected]/system"
}
# Remote via SSH (native command, respects ~/.ssh/config)
provider "libvirt" {
uri = "qemu+sshcmd://[email protected]/system"
}
# Remote via TLS
provider "libvirt" {
uri = "qemu+tls://host.example.com/system"
}See docs/transports.md for detailed transport configuration and examples.
See the examples directory for more usage examples.
The legacy provider exposed IP addresses directly on the domain resource via network_interface.*.addresses. The new provider uses a separate data source for querying IP addresses:
Legacy provider (v0.8.x):
resource "libvirt_domain" "example" {
# ... domain config ...
}
output "ip" {
value = libvirt_domain.example.network_interface[0].addresses[0]
}New provider (v0.9+):
resource "libvirt_domain" "example" {
# ... domain config ...
}
data "libvirt_domain_interface_addresses" "example" {
domain = libvirt_domain.example.id
source = "lease" # or "agent" or "any"
}
output "ip" {
value = data.libvirt_domain_interface_addresses.example.interfaces[0].addrs[0].addr
}Alternatively, use the wait_for_ip property on the domain's interface configuration to ensure the domain has an IP before creation completes:
resource "libvirt_domain" "example" {
name = "example-vm"
memory = 512
vcpu = 1
devices = {
interfaces = [
{
type = "network"
source = {
network = "default"
}
wait_for_ip = {
timeout = 300 # seconds
source = "lease"
}
}
]
}
}If you're migrating from the legacy provider and used the source attribute on volumes to download cloud images, note that this feature is now available via the create.content.url block:
Legacy provider (v0.8.x):
resource "libvirt_volume" "ubuntu" {
name = "ubuntu.qcow2"
pool = "default"
source = "https://cloud-images.ubuntu.com/releases/22.04/release/ubuntu-22.04-server-cloudimg-amd64.img"
# size was automatically detected from Content-Length
}New provider (v0.9+):
resource "libvirt_volume" "ubuntu" {
name = "ubuntu.qcow2"
pool = "default"
format = "qcow2" # Must specify format
create = {
content = {
url = "https://cloud-images.ubuntu.com/releases/22.04/release/ubuntu-22.04-server-cloudimg-amd64.img"
}
}
# capacity is automatically detected from Content-Length header
}Important notes:
- Format is required: You must explicitly specify the
formatattribute (e.g.,"qcow2","raw"). The legacy provider auto-detected format from file extension, but the new provider requires it. - Capacity is computed: Like the legacy provider,
capacityis automatically computed from the HTTPContent-Lengthheader (or file size for local files). You don't need to specify it. - Local files supported: You can use absolute paths or
file://URIs for local files:url = "/path/to/local.qcow2"orurl = "file:///path/to/local.qcow2" - Content-Length required: For HTTPS URLs, the server must provide a
Content-Lengthheader. If it doesn't, volume creation will fail.
- Go 1.21+
- libvirt daemon running (for acceptance tests)
# Run linter
make lint
# Run unit tests
make test
# Run acceptance tests (requires libvirt)
make testaccOn Github, the tests use a hack we have in place to override the domain type (TF_PROVIDER_LIBVIRT_DOMAIN_TYPE=qemu), which allows to run acceptance tests without nested virtualization, but using the tcg accelerator instead of KVM.
All code must pass linting before being committed:
make check # Runs lint, vet, and testsFormat code with:
make fmtRun make help to see all targets.
This is early stage development. The focus is on getting core functionality working before accepting contributions.
Issues should be open for clearly actionable bugs. For getting help on your stack not working, please use the discussions first.
In general, you should not contribute significant features or code without a previous discussion and agreement with the maintainers. This involve transferring code maintenance burden to the maintainers and in general is not desired.
The author uses AI for this project, but as the maintainer, he owns the outcome and consequences.
If you contribute code or issues and used AI, you have to disclose it, including full details (tools, prompts).
Duncan Mac-Vicar P.
Same as the legacy provider (Apache 2.0).