# The libvirt provider mirrors XML instead of abstracting it, on purpose

> terraform-provider-libvirt was rewritten from scratch on the Terraform Plugin Framework, and its schema is generated directly from the libvirt XML definitions. That choice gives you every field libvirt exposes and moves the complexity into HCL that reads like XML.

**dmacvicar/terraform-provider-libvirt** — Terraform provider to provision infrastructure with Linux's KVM using libvirt

- Repository: https://github.com/dmacvicar/terraform-provider-libvirt
- Stars: 1,898 · Forks: 525
- Language: Go
- License: not declared
- Published: 2026-10-16 · Updated: 2026-10-16 · Language: en
- Canonical page: https://hysenlabs.com/projects/dmacvicar-terraform-provider-libvirt

## Everything in the schema comes from libvirtxml

The resource coverage table is unambiguous. libvirt_domain, libvirt_network, libvirt_pool and libvirt_volume are all marked supported, each with what the README calls full coverage of the corresponding libvirtxml schema. For domains that means devices, CPU, memory, features, RNG and TPM; for networks, forwarding modes, bridge, DHCP, VLAN and virtual ports; for pools, dir, logical and iscsi targets; for volumes, target, backing_store, encryption and timestamps.

The reason is stated in the design principles: the provider supports all fields that libvirt.org/go/libvirtxml implements from the official libvirt schemas, and if libvirtxml does not support a feature yet, neither does the provider, because it depends on that library internally.

So the ceiling is not a design choice but a dependency. libvirtxml is pinned at v1.12005.0 in go.mod, which encodes a specific libvirt release, and anything upstream adds shows up only after that Go library does.

Additional resources such as secrets, nodes and interfaces are described as possible future additions, so four resource types is the whole surface today.

## A minimal domain is short because the rest is generated

The provider block and a small domain together are enough to start:

```hcl
terraform {
  required_providers {
    libvirt = {
      source = "dmacvicar/libvirt"
    }
  }
}

provider "libvirt" {
  uri = "qemu:///system"
}

resource "libvirt_domain" "example" {
  name         = "example-vm"
  type         = "kvm"
  memory       = 512
  memory_unit  = "MiB"
  vcpu         = 1
  
  os = {
    type         = "hvm"
    type_arch    = "x86_64"
    type_machine = "q35"
  }
}
```

Two details carry weight. memory and memory_unit are separate fields rather than one string, because the generated schema mirrors the XML structure instead of inventing a friendlier type. And os is a nested object with the XML attribute names as keys, type_arch and type_machine included, because those are the attribute names in the domain schema.

Both of those are consequences of the no abstraction principle. The result is HCL that maps one to one onto the XML you would otherwise write by hand, which is easier to look up and harder to read.

## Connection URIs pick a transport, and sshcmd behaves differently

The provider accepts the libvirt connection URI forms, and the choice is not cosmetic. Four appear in the README:

```hcl
# Local system socket
provider "libvirt" {
  uri = "qemu:///system"
}
```

```hcl
# Remote via SSH (Go library)
provider "libvirt" {
  uri = "qemu+ssh://user@host.example.com/system"
}
```

The sshcmd form uses the native SSH command and respects ~/.ssh/config. The README does not document key or agent behavior for the other transports.

TLS is the third remote option, given as qemu+tls://host.example.com/system. Detailed transport configuration and examples live in docs/transports.md, which the README points to rather than duplicating.

One caveat worth naming: the provider block shown above sets uri only. The README does not document other provider level settings in the visible text, so anything about credentials, timeouts or read only mode has to be found in the registry documentation.

## create and destroy blocks are not XML, and they are experimental

A small part of the schema is deliberately outside the XML mapping. Some resources expose provider-specific lifecycle controls in nested create and destroy objects. They control post-define and pre-undefine API operations, such as whether a pool is built, started and set to autostart, and whether its backing data is deleted.

```hcl
resource "libvirt_pool" "data" {
  name = "data-pool"
  type = "dir"
  target = {
    path = "/var/lib/libvirt/data"
  }

  create = {
    build     = true
    start     = true
    autostart = true
  }

  destroy = {
    delete = false
  }
}
```

These controls let configuration specify whether a pool is built, started and set to autostart. The corresponding destroy controls govern operations performed before undefinition.

The README states plainly that current lifecycle controls are experimental and may change in future releases. Pin your provider version before using them in a state file you care about.

## Building from source means generating first

The Makefile makes the generation step a dependency of the build rather than a separate chore. The generate target runs the code generator directly, and build depends on it:

The generate target invokes the generator with `go run./internal/codegen`.

build then compiles the binary with a version stamped through ldflags, where VERSION defaults to dev and can be overridden on the command line. install depends on build as well, and copies the binary into the local plugin directory under registry.terraform.io/dmacvicar/libvirt, so the version segment and the platform directory linux_amd64 are both part of the path.

Point Terraform at that build with a dev override in ~/.terraformrc:

```hcl
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 {}
  }
```

Tests come in three flavours: make lint, make test for unit tests and make testacc for acceptance tests, which the README notes require libvirt. The acceptance configuration uses CirrOS 0.6.2 and Alpine 3.23.3 test images cached under.cache/test-images.

## A rewrite, a migration path, and an unusual admission

This is a complete rewrite, and that has consequences for anyone already using it. Releases up to and including v0.8.x are maintained on a separate v0.8 branch, and from v0.9.0 onward all releases come from the new implementation. A provider upgrade here is a migration, not a version bump, and HCL written for the old provider will not read the same way against the generated schema.

go.mod names the module as github.com/dmacvicar/terraform-provider-libvirt/v2 with a go directive of 1.26, while the released tags are v0.9.9, v0.9.8 and v0.9.7. Documentation lives on the Terraform registry and the OpenTofu registry, and for unreleased versions the mapping rules are in docs/schema-mapping.md, which the README calls canonical for flattening rules, unions, presence booleans and nested attributes.

The development section deserves reading on its own. The maintainer states that this is the first project where AI was used heavily, both for the cleanup and rewrite and for injecting documentation into the schema, and adds that the project is his learning platform and he owns the outcome. That is unusually candid about how the generated schema descriptions came to exist, and it tells you where to look first if a field description reads oddly.

The last push was on 2026-08-30 and v0.9.9 was published the same day.

## Conclusion

This provider fits teams that manage KVM estates and want access to libvirt features the old abstraction hid, and it will frustrate teams that want three friendly arguments instead of the full XML surface. Before adopting it for production, read the create and destroy lifecycle blocks, because the README marks those controls as experimental and subject to change, and check your libvirtxml version, since schema coverage stops wherever that Go library stops.

## FAQ

### How do I configure the libvirt provider in Terraform?

Declare the required provider with source = "dmacvicar/libvirt" and set the connection URI on the provider block, for example `uri = "qemu:///system"` for the local system socket. Remote transports use qemu+ssh://, qemu+sshcmd:// or qemu+tls://.

### Which libvirt resources does this provider support?

libvirt_domain, libvirt_network, libvirt_pool and libvirt_volume, each with what the README describes as full coverage of the corresponding libvirtxml schema. Resources such as secrets, nodes and interfaces are mentioned as possible future additions.

### How do I build and test the provider from source?

Clone the repository and run `make build`, which runs code generation first through `go run ./internal/codegen`. Use `make lint`, `make test` for unit tests and `make testacc` for acceptance tests, which require libvirt.

### What is the difference between qemu+ssh and qemu+sshcmd?

Both are remote SSH transports. The README describes qemu+ssh as remote via SSH using the Go library, while qemu+sshcmd uses the native SSH command and respects ~/.ssh/config.

## Sources

- [dmacvicar/terraform-provider-libvirt on GitHub](https://github.com/dmacvicar/terraform-provider-libvirt)
- [Issues](https://github.com/dmacvicar/terraform-provider-libvirt/issues)
- [README](https://github.com/dmacvicar/terraform-provider-libvirt/blob/main/README.md)
- [Releases](https://github.com/dmacvicar/terraform-provider-libvirt/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/dmacvicar-terraform-provider-libvirt
