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.) may be added in the future.
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"
type_arch = "x86_64"
type_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://user@host.example.com/system"
}
# Remote via SSH (native command, respects ~/.ssh/config)
provider "libvirt" {
uri = "qemu+sshcmd://user@host.example.com/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.
You can find an overview of the XML <-> HCL mapping and the resource schema in the documentation hosted at the Terraform registry and OpenTofu registry.
For non-released versions, see XML <-> HCL mapping and for an overview of the HCL to XML mapping patterns and the documentation source.
- Schema Coverage: We support all fields that
libvirt.org/go/libvirtxmlimplements from the official libvirt schemas (located at https://gitlab.com/libvirt/libvirt/-/tree/master/src/conf/schemas). If libvirtxml doesn't support a feature yet, neither do we, as we depend on libvirtxml internally. - No Abstraction: The Terraform schema mirrors the libvirt XML structure as closely as possible, providing full access to underlying features rather than simplified abstractions.
- Preserve intent: We try to generate state for what the user specified. That is, if the user did not specify something, we let libvirt handle it. We ignore then those when diffing state.
All resources use a single XML <-> HCL mapping spec (flattening rules, unions, presence booleans, nested attributes). See XML <-> HCL mapping documentation for the canonical rules.
This is the first project where I leveraged AI quite heavily not only to do a major cleanup and rewrite of pieces of code, and to implement a new design, but we also use it to inject documentation into the schema.
I am aware of the consequences, advantages and drawbacks. It is my learning platform, and I own the outcome.
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
See codegen documentation for architecture and for the documentation tooling (docindex/docgen) used to inject schema descriptions.
git clone https://github.com/dmacvicar/terraform-provider-libvirt
cd terraform-provider-libvirt
make buildTo install the provider locally:
make installThis installs to ~/.terraform.d/plugins/registry.terraform.io/dmacvicar/libvirt/dev/linux_amd64/
Then override the provider like this in $HOME/.terraformrc:
provider_installation {
dev_overrides {
"registry.terraform.io/dmacvicar/libvirt" = "/home/duncan/src/terraform-provider-libvirt"
}
# For all other providers, install them directly from their origin provider
# registries as normal.
direct {}
}# 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.
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.
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 are required to disclose it, including full details (tools, prompts).
- Duncan Mac-Vicar P.
- Apache 2.0