Allow libvirt_volume URL uploads to work when the source server does not provide Content-Length, as long as capacity is set explicitly (including acceptance tests).
Refresh GitHub Actions automation and release tooling.
What's Changed
Volume upload behavior
Allow create.content.url uploads to fall back to user-provided capacity when the HTTP source does not include Content-Length.
Keep explicit failure behavior when neither the server nor the configuration provides a usable volume size.
CI and release maintenance
Add Dependabot configuration for GitHub Actions updates.
Update GitHub Actions used by the test and release workflows.
Expanded libvirt XML code generation coverage by handling anonymous embedded fields and hypervisor namespace fields that derive their XML names from child XMLName metadata.
Added domain schema coverage and curated descriptions for hypervisor-specific namespace fields including QEMU, Xen, bhyve, LXC, and VMware extensions.
Made VLAN tag IDs configurable.
What's Changed
Schema and code generation
Process anonymous embedded libvirt XML fields in the code generator, improving coverage for embedded state-style structs such as Hyper-V feature fields.
Apply Terraform field policies after reflection, keeping reflection logic focused on libvirt structure analysis and policy logic focused on Terraform semantics.
Support hypervisor namespace fields discovered via child XMLName metadata, exposing:
qemu_commandline
qemu_capabilities
qemu_override
qemu_deprecation
lxc_namespace
bhyve_commandline
vmware_data_center_path
xen_commandline
Add parser, generated, and provider schema tests covering the new codegen behavior.
Domain and network improvements
Make VLAN tag IDs configurable, fixing the read-only vlan-id handling reported in issue #1236.
Documentation
Add curated descriptions for the new hypervisor namespace fields, using official libvirt references where available and conservative text for fields that are not publicly documented on libvirt.org.
Included changes
docs: add hypervisor namespace field descriptions
fix: support hypervisor XMLName fields in codegen
refactor: apply codegen field policies after reflection
test: cover anonymous embedded XML fields
fix: Process anonymous fields in libvirt XML (#1222 by @atopuzov)
fix: make vlan tag ids configurable (fixes #1236)
Domain graceful shutdown on destroy — new destroy.shutdown.timeout option on libvirt_domain sends an ACPI shutdown signal and waits for the guest to power off cleanly before destroying it, instead of force-killing. Subject to change in future releases.
Pool lifecycle operation controls — new create and destroy blocks on libvirt_pool give explicit control over build, start, autostart, and backing storage deletion behavior during pool create/destroy. Subject to change in future releases.
Bug Fixes
NVRAM/TPM files deleted on domain update — updating a domain (e.g. changing memory) would undefine and re-define it, deleting NVRAM and TPM state files in the process. The provider now passes KEEP_NVRAM and KEEP_TPM undefine flags during updates. (#1232, @atopuzov)
capacity_unit not preserved on volume readback — specifying capacity_unit = "GiB" on a volume would cause "Provider produced inconsistent result after apply" on every apply, because libvirt normalizes to bytes in XML. The codegen now preserves the user's unit when the value was explicitly set. (#1253)
Dir pool backing path deleted on destroy — terraform destroy of a dir pool was calling StoragePoolDelete which removed the backing directory. The default behavior now preserves the directory (matching virsh pool-destroy + pool-undefine), with an explicit destroy.delete = true override available. (#1285)
Dependencies
Bumped github.com/cloudflare/circl from 1.6.1 to 1.6.3. (#1284)
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)
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
Attr names now mirror libvirt XML – the generator emits snake_case names derived from the XML schema (e.g., accessmode → access_mode, portgroup → port_group). Set exactly the fields you care about; anything left null stays absent in the XML.
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.
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.
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.
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:
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.).
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 portgroup → port_group inside DHCP hosts.
⚠️⚠️⚠️⚠️This version of the provider breaks compatibility⚠️⚠️⚠️⚠️
Background
When this provider was developed, the idea was to mimic a cloud experience on top of libvirt. Because of this, the schema was done as flat as possible, features were abstracted and some features like disks from remote sources were added as convenience.
The initial users of the provider were usually makers of infrastructure software who needed complex network setups. Lot of code was contributed which added complexity outside of its initial design.
So for long time I wanted to restart the provider under a new design principles where:
HCL maps almost 1:1 to libvirt XML and therefore almost any libvirt feature can be supported
Most of the validation work is left to libvirt which is already doing it
No abstractions or extra features, and when they do, they should be designed in a way that are quite independent
More consistency, for example, most libvirt APIs can be mostly separated in lifecycle (create, destroy) which map quite well to terraform resources, and then some query APIs, which map well to data sources. This was not the case how we implemented for example querying the IP addresses
No unnecessary defensive code, for example, checking that a volume exist when referenced, is a problem that terraform solves if the ID is interpolated and libvirt solves with its own checks, if the volume is referenced by a hardcoded strings.
I knew 1.0 would never come in the current form.
The new provider
The new provider is based on the new plugin framework. This gives us some room for better diagnostics and better plans.
It makes definitions more verbose, but it also means we can implement any libvirt feature. Defaults work as long as they are defaults in libvirt.
Migration plan
You can find the legacy provider in the v0.8 branch. New releases can be done of 0.8.x versions to add bugfixes, so people who rely on it have a path forward. I'd likely not maintain much of 0.8.x, but I guess many people will help here, as they do today with different PRs.
There is no automated way of migrating the HCL of previous providers, but given that it is documented how the new schema is defined, which was not the case with the previous schema, it should be much easier to drive LLMs to perform a conversion.
You should check the documentation and README, which will give you an idea of the main differences and equivalences, but here is an example of the new schema to get an idea:
terraform {
required_providers {
libvirt= {
source="dmacvicar/libvirt"
}
}
}
provider"libvirt" {
uri="qemu:///system"
}# Base Alpine Linux cloud image stored in the default pool.
resource"libvirt_volume" "alpine_base" {
name="alpine-3.22-base.qcow2" pool="default" format="qcow2" create= {
content= {
url="https://dl-cdn.alpinelinux.org/alpine/v3.22/releases/cloud/generic_alpine-3.22.2-x86_64-bios-cloudinit-r0.qcow2"
}
}
}# Writable copy-on-write layer for the VM.
resource"libvirt_volume" "alpine_disk" {
name="alpine-vm.qcow2" pool="default" format="qcow2" capacity=2147483648 backing_store= {
path=libvirt_volume.alpine_base.path format="qcow2"
}
}# Cloud-init seed ISO.
resource"libvirt_cloudinit_disk" "alpine_seed" {
name="alpine-cloudinit" user_data=<<-EOF #cloud-config
chpasswd:list:|root:passwordexpire:falsessh_pwauth:truepackages:-openssh-servertimezone:UTCEOF meta_data=<<-EOFinstance-id:alpine-001local-hostname:alpine-vmEOF network_config=<<-EOFversion:2ethernets:eth0:dhcp4:trueEOF
}# Upload the cloud-init ISO into the pool.
resource"libvirt_volume" "alpine_seed_volume" {
name="alpine-cloudinit.iso" pool="default" create= {
content= {
url=libvirt_cloudinit_disk.alpine_seed.path
}
}
}# Virtual machine definition.
resource"libvirt_domain" "alpine" {
name="alpine-vm" memory=1048576 vcpu=1 os= {
type="hvm" arch="x86_64" machine="q35"
}
features= {
acpi=true
}
devices= {
disks=[
{
source= {
pool=libvirt_volume.alpine_disk.pool volume=libvirt_volume.alpine_disk.name
}
target= {
dev="vda" bus="virtio"
}
},
{
device="cdrom" source= {
pool=libvirt_volume.alpine_seed_volume.pool volume=libvirt_volume.alpine_seed_volume.name
}
target= {
dev="sdb" bus="sata"
}
}
] interfaces=[
{
type="network" model="virtio" # e1000 is more compatible than virtio for Alpine
source= {
network="default"
} # TODO: wait_for_ip not implemented yet (Phase 2)
# This will wait during creation until the interface gets an IP
wait_for_ip= {
timeout=300 # seconds, default 300
source="any" # "lease" (DHCP), "agent" (qemu-guest-agent), or "any"(tryboth)
}
}
] graphics= {
vnc= {
autoport="yes" listen="127.0.0.1"
}
}
}
running=true
}# Query the domain's interface addresses
# This data source can be used at any time to retrieve current IP addresses
# without blocking operations like Delete
data"libvirt_domain_interface_addresses" "alpine" {
domain=libvirt_domain.alpine.name source="lease" # optional: "lease" (DHCP), "agent" (qemu-guest-agent), or "any"
}# Output all interface information
output"vm_interfaces" {
description="All network interfaces with their IP addresses" value=data.libvirt_domain_interface_addresses.alpine.interfaces
}# Output the first IP address found
output"vm_ip" {
description="First IP address of the VM" value=length(data.libvirt_domain_interface_addresses.alpine.interfaces)>0&&length(data.libvirt_domain_interface_addresses.alpine.interfaces[0].addrs)>0?data.libvirt_domain_interface_addresses.alpine.interfaces[0].addrs[0].addr:"No IP address found"
}# Output all IP addresses across all interfaces
output"vm_all_ips" {
description="All IP addresses across all interfaces" value=flatten([forifaceindata.libvirt_domain_interface_addresses.alpine.interfaces:[foraddriniface.addrs:addr.addr]])
}
Feedback is appreciated. There will be a long journey for people to port and iron all the issues, but it is clear this is the path to go.
The provider no longer detects the image format qcow2 using content sniffing for remote HTTP images. If you leave it blank, it will just set it based on the extension. This allows to use HTTP servers without HTTP Range support.
Add ability to set mode=private to a macvlan interface. by @farsonic in #1154
Upgrade dependencies
Bump golang.org/x/crypto from 0.27.0 to 0.31.0 by @dependabot in #1138
allow for multiple default identity key files by @memetb in #1112
Experimental LVM storage pool support
There is a new experimental feature, support for LVM storage pools. I don't use myself this type of pools, so I put together all the contributions and made the code ready for release mostly based on integration tests. Try it and give feedback.
This PR contains the following updates:
| Package | Type | Update | Change |
|---|---|---|---|
| [libvirt](https://registry.terraform.io/providers/dmacvicar/libvirt) ([source](https://github.com/dmacvicar/terraform-provider-libvirt)) | required_provider | minor | `0.7.6` → `0.9.8` |
---
### Release Notes
<details>
<summary>dmacvicar/terraform-provider-libvirt (libvirt)</summary>
### [`v0.9.8`](https://github.com/dmacvicar/terraform-provider-libvirt/releases/tag/v0.9.8)
[Compare Source](https://github.com/dmacvicar/terraform-provider-libvirt/compare/v0.9.7...v0.9.8)
#### What's Changed
##### Domain lifecycle and readback
- Reconcile `running = true` drift by starting an existing stopped domain directly and waiting until libvirt reports it running.
- Preserve planned channel UNIX source paths on readback to avoid inconsistent results after apply.
- Preserve interface target device names on readback to avoid unnecessary diffs.
- Fix domain runtime id consistency when updating a running domain.
##### Volume behavior
- Replace volumes when creation options change, ensuring immutable creation-time inputs are handled as recreate operations.
- Support importing storage volumes by key.
##### Network behavior
- Force network replacement when resource attributes change, except for `autostart`.
#### Included changes
- `fix: replace volume when the creation options change` ([#​1299](https://github.com/dmacvicar/terraform-provider-libvirt/issues/1299))
- `fix: preserve interface target dev on readback`
- `fix: preserve planned channel source unix path on readback` ([#​1338](https://github.com/dmacvicar/terraform-provider-libvirt/issues/1338))
- `Fix domain id inconsistency on update` ([#​1337](https://github.com/dmacvicar/terraform-provider-libvirt/issues/1337))
- `fix: support importing volume by key` ([#​1334](https://github.com/dmacvicar/terraform-provider-libvirt/issues/1334))
- `Force network replacement on any attr change except autostart.` ([#​1331](https://github.com/dmacvicar/terraform-provider-libvirt/issues/1331))
- `fix: reconcile domain running drift` (Resolves: [#​1339](https://github.com/dmacvicar/terraform-provider-libvirt/issues/1339))
#### Contributors
- [@​carl-bjork](https://github.com/carl-bjork)
- [@​dmacvicar](https://github.com/dmacvicar)
- [@​erdnaxeli](https://github.com/erdnaxeli)
- [@​rst0070](https://github.com/rst0070)
- [@​sgpinkus](https://github.com/sgpinkus)
#### Full Changelog
**Full Changelog**: <https://github.com/dmacvicar/terraform-provider-libvirt/compare/v0.9.7...v0.9.8>
### [`v0.9.7`](https://github.com/dmacvicar/terraform-provider-libvirt/releases/tag/v0.9.7)
[Compare Source](https://github.com/dmacvicar/terraform-provider-libvirt/compare/v0.9.6...v0.9.7)
#### Highlights
- Allow `libvirt_volume` URL uploads to work when the source server does not provide `Content-Length`, as long as `capacity` is set explicitly (including acceptance tests).
- Refresh GitHub Actions automation and release tooling.
#### What's Changed
##### Volume upload behavior
- Allow `create.content.url` uploads to fall back to user-provided `capacity` when the HTTP source does not include `Content-Length`.
- Keep explicit failure behavior when neither the server nor the configuration provides a usable volume size.
##### CI and release maintenance
- Add Dependabot configuration for GitHub Actions updates.
- Update GitHub Actions used by the test and release workflows.
#### Included changes
- `chore: add dependabot config for github actions`
- `chore(deps): update GitHub Actions dependencies` ([#​1306](https://github.com/dmacvicar/terraform-provider-libvirt/issues/1306), [#​1307](https://github.com/dmacvicar/terraform-provider-libvirt/issues/1307), [#​1308](https://github.com/dmacvicar/terraform-provider-libvirt/issues/1308), [#​1309](https://github.com/dmacvicar/terraform-provider-libvirt/issues/1309), [#​1310](https://github.com/dmacvicar/terraform-provider-libvirt/issues/1310), [#​1312](https://github.com/dmacvicar/terraform-provider-libvirt/issues/1312), [#​1313](https://github.com/dmacvicar/terraform-provider-libvirt/issues/1313), [#​1314](https://github.com/dmacvicar/terraform-provider-libvirt/issues/1314), [#​1315](https://github.com/dmacvicar/terraform-provider-libvirt/issues/1315))
- `fix: allow URL uploads without content length` (closes [#​1316](https://github.com/dmacvicar/terraform-provider-libvirt/issues/1316))
#### Contributors
- [@​dmacvicar](https://github.com/dmacvicar)
#### Full Changelog
**Full Changelog**: <https://github.com/dmacvicar/terraform-provider-libvirt/compare/v0.9.6...v0.9.7>
### [`v0.9.6`](https://github.com/dmacvicar/terraform-provider-libvirt/releases/tag/v0.9.6)
[Compare Source](https://github.com/dmacvicar/terraform-provider-libvirt/compare/v0.9.5...v0.9.6)
#### Highlights
- Added `terraform import` support for `libvirt_domain` using an existing domain UUID.
- Added configurable shutdown timeout handling for domain updates that need to stop a running guest before redefine.
- Improved readback preservation for optional nested objects to reduce unintended diffs.
#### What's Changed
##### Domain lifecycle and import
- Add `terraform import` support for `libvirt_domain` by UUID, allowing Terraform state to be populated from an existing libvirt domain definition.
- Add configurable update shutdown behavior so domain updates can request guest shutdown and wait with a configurable timeout before forcing a stop.
- Add lifecycle tests covering the new shutdown timeout behavior during updates.
##### Readback and state preservation
- Preserve optional nested objects on readback so unset nested state is not synthesized from libvirt defaults when the user did not configure it.
- Move readback preservation behavior into code generation policy, making the preservation rules more consistent across generated conversions.
- Add tests covering nested object preservation, CPU host-model preservation, and SPICE listen preservation.
##### Maintenance
- Update `google.golang.org/grpc` from `1.75.1` to `1.79.3`.
- Refactor codegen overrides to use named field policies.
#### Contributors
- [@​calibrae](https://github.com/calibrae)
- [@​dmacvicar](https://github.com/dmacvicar)
- [@​dependabot](https://github.com/dependabot)\[bot]
#### Full Changelog
**Full Changelog**: <https://github.com/dmacvicar/terraform-provider-libvirt/compare/v0.9.5...v0.9.6>
### [`v0.9.5`](https://github.com/dmacvicar/terraform-provider-libvirt/releases/tag/v0.9.5)
[Compare Source](https://github.com/dmacvicar/terraform-provider-libvirt/compare/v0.9.4...v0.9.5)
#### Highlights
- Expanded libvirt XML code generation coverage by handling anonymous embedded fields and hypervisor namespace fields that derive their XML names from child `XMLName` metadata.
- Added domain schema coverage and curated descriptions for hypervisor-specific namespace fields including QEMU, Xen, bhyve, LXC, and VMware extensions.
- Made VLAN tag IDs configurable.
#### What's Changed
##### Schema and code generation
- Process anonymous embedded libvirt XML fields in the code generator, improving coverage for embedded state-style structs such as Hyper-V feature fields.
- Apply Terraform field policies after reflection, keeping reflection logic focused on libvirt structure analysis and policy logic focused on Terraform semantics.
- Support hypervisor namespace fields discovered via child `XMLName` metadata, exposing:
- `qemu_commandline`
- `qemu_capabilities`
- `qemu_override`
- `qemu_deprecation`
- `lxc_namespace`
- `bhyve_commandline`
- `vmware_data_center_path`
- `xen_commandline`
- Add parser, generated, and provider schema tests covering the new codegen behavior.
##### Domain and network improvements
- Make VLAN tag IDs configurable, fixing the read-only `vlan-id` handling reported in issue [#​1236](https://github.com/dmacvicar/terraform-provider-libvirt/issues/1236).
##### Documentation
- Add curated descriptions for the new hypervisor namespace fields, using official libvirt references where available and conservative text for fields that are not publicly documented on libvirt.org.
#### Included changes
- `docs: add hypervisor namespace field descriptions`
- `fix: support hypervisor XMLName fields in codegen`
- `refactor: apply codegen field policies after reflection`
- `test: cover anonymous embedded XML fields`
- `fix: Process anonymous fields in libvirt XML` ([#​1222](https://github.com/dmacvicar/terraform-provider-libvirt/issues/1222) by [@​atopuzov](https://github.com/atopuzov))
- `fix: make vlan tag ids configurable` (fixes [#​1236](https://github.com/dmacvicar/terraform-provider-libvirt/issues/1236))
### [`v0.9.4`](https://github.com/dmacvicar/terraform-provider-libvirt/releases/tag/v0.9.4)
[Compare Source](https://github.com/dmacvicar/terraform-provider-libvirt/compare/v0.9.3...v0.9.4)
##### New Features (Experimental)
- **Domain graceful shutdown on destroy** — new `destroy.shutdown.timeout` option on `libvirt_domain` sends an ACPI shutdown signal and waits for the guest to power off cleanly before destroying it, instead of force-killing. Subject to change in future releases.
- **Pool lifecycle operation controls** — new `create` and `destroy` blocks on `libvirt_pool` give explicit control over build, start, autostart, and backing storage deletion behavior during pool create/destroy. Subject to change in future releases.
##### Bug Fixes
- **NVRAM/TPM files deleted on domain update** — updating a domain (e.g. changing memory) would undefine and re-define it, deleting NVRAM and TPM state files in the process. The provider now passes `KEEP_NVRAM` and `KEEP_TPM` undefine flags during updates. ([#​1232](https://github.com/dmacvicar/terraform-provider-libvirt/issues/1232), [@​atopuzov](https://github.com/atopuzov))
- **`capacity_unit` not preserved on volume readback** — specifying `capacity_unit = "GiB"` on a volume would cause "Provider produced inconsistent result after apply" on every apply, because libvirt normalizes to bytes in XML. The codegen now preserves the user's unit when the value was explicitly set. ([#​1253](https://github.com/dmacvicar/terraform-provider-libvirt/issues/1253))
- **Dir pool backing path deleted on destroy** — `terraform destroy` of a `dir` pool was calling `StoragePoolDelete` which removed the backing directory. The default behavior now preserves the directory (matching `virsh pool-destroy` + `pool-undefine`), with an explicit `destroy.delete = true` override available. ([#​1285](https://github.com/dmacvicar/terraform-provider-libvirt/issues/1285))
##### Dependencies
- Bumped `github.com/cloudflare/circl` from 1.6.1 to 1.6.3. ([#​1284](https://github.com/dmacvicar/terraform-provider-libvirt/issues/1284))
##### Contributors
- [@​atopuzov](https://github.com/atopuzov) (Aleksandar Topuzović)
- [@​dmacvicar](https://github.com/dmacvicar) (Duncan Mac-Vicar P.)
### [`v0.9.3`](https://github.com/dmacvicar/terraform-provider-libvirt/releases/tag/v0.9.3)
[Compare Source](https://github.com/dmacvicar/terraform-provider-libvirt/compare/v0.9.2...v0.9.3)
#### What's Changed
- docs: update alpine\_cloudinit.tf example for v0.9.1 schema changes by [@​hpst3r](https://github.com/hpst3r) in [#​1244](https://github.com/dmacvicar/terraform-provider-libvirt/pull/1244)
- Add type field to libvirt\_domain resource in README by [@​br0cken](https://github.com/br0cken) in [#​1278](https://github.com/dmacvicar/terraform-provider-libvirt/pull/1278)
- fix: honor destroy.graceful via domain destroy flags by [@​dmacvicar](https://github.com/dmacvicar) in [#​1280](https://github.com/dmacvicar/terraform-provider-libvirt/pull/1280) (partial fix for [#​1060](https://github.com/dmacvicar/terraform-provider-libvirt/issues/1060))
- [fix: preserve cpu mode user intent in generated conversion](https://github.com/dmacvicar/terraform-provider-libvirt/commit/073f2c34b422d82861c98700abea576ba8e82fd2)
- Switch to Go 1.26
- [fix: allow sshcmd control master settings](https://github.com/dmacvicar/terraform-provider-libvirt/commit/c6215eb378cb42daa66ab077b796d38554368dc9) (fixes [#​1272](https://github.com/dmacvicar/terraform-provider-libvirt/issues/1272))
- [fix: correct libvirt\_volume format examples](https://github.com/dmacvicar/terraform-provider-libvirt/commit/16f1f008a3132e830ce41183fdff59f09f730417)
#### New Contributors
- [@​hpst3r](https://github.com/hpst3r) made their first contribution in [#​1244](https://github.com/dmacvicar/terraform-provider-libvirt/pull/1244)
- [@​br0cken](https://github.com/br0cken) made their first contribution in [#​1278](https://github.com/dmacvicar/terraform-provider-libvirt/pull/1278)
And thanks for those unmerged PRs [@​SkinGad](https://github.com/SkinGad) [@​yannlambret](https://github.com/yannlambret) [@​nicholas-rees](https://github.com/nicholas-rees)
**Full Changelog**: <https://github.com/dmacvicar/terraform-provider-libvirt/compare/v0.9.2...v0.9.3>
### [`v0.9.2`](https://github.com/dmacvicar/terraform-provider-libvirt/releases/tag/v0.9.2)
[Compare Source](https://github.com/dmacvicar/terraform-provider-libvirt/compare/v0.9.1...v0.9.2)
**Full Changelog**: <https://github.com/dmacvicar/terraform-provider-libvirt/compare/v0.9.1...v0.9.2>
- [fix: non-null zero int value to be read as null.](https://github.com/dmacvicar/terraform-provider-libvirt/commit/c93965ccc288887328fc11b10a2a2714f842ba7d) [#​1246](https://github.com/dmacvicar/terraform-provider-libvirt/issues/1246)
- [fix: explicit set empty list being read as nil](https://github.com/dmacvicar/terraform-provider-libvirt/commit/0cad3532aa7b72caad97a5d366ce0dd855a5b455) [#​1247](https://github.com/dmacvicar/terraform-provider-libvirt/issues/1247)
- [fix: normalization of file mode (770 vs 0770)](https://github.com/dmacvicar/terraform-provider-libvirt/commit/98febac1b8b22129808e8248b5ed917a4a0eb8f9) [#​1252](https://github.com/dmacvicar/terraform-provider-libvirt/issues/1252)
- [fix: disk list order preservation on read](https://github.com/dmacvicar/terraform-provider-libvirt/commit/30902335f53a0ce5c63378d82b2b0988e6a9ee74) [#​1263](https://github.com/dmacvicar/terraform-provider-libvirt/issues/1263)
Thanks to [@​BohdanTkachenko](https://github.com/BohdanTkachenko) for the feedback, reports and PR suggestions.
### [`v0.9.1`](https://github.com/dmacvicar/terraform-provider-libvirt/releases/tag/v0.9.1)
[Compare Source](https://github.com/dmacvicar/terraform-provider-libvirt/compare/v0.9.0...v0.9.1)
### 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](https://github.com/dmacvicar/terraform-provider-libvirt/issues/1203) )
- Fix SIGSEGV when connecting to a hypervisor over qemu+ssh. Fixes [#​1210](https://github.com/dmacvicar/terraform-provider-libvirt/issues/1210) ([#​1211](https://github.com/dmacvicar/terraform-provider-libvirt/issues/1211))
- Misc documentation fixes [#​1209](https://github.com/dmacvicar/terraform-provider-libvirt/issues/1209), [#​1210](https://github.com/dmacvicar/terraform-provider-libvirt/issues/1210)
### Features
#### Support for full libvirt API (XML) ([#​1208](https://github.com/dmacvicar/terraform-provider-libvirt/issues/1208) )
The provider now supports the whole libvirt API :partying\_face: (\* that is supported by [libvirtxml](https://pkg.go.dev/libvirt.org/go/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
:warning: 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., `accessmode` → `access_mode`, `portgroup` → `port_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:
```hcl
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:
```hcl
# 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:
```hcl
# 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:
```hcl
# 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:
```hcl
# 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:
```hcl
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 `portgroup` → `port_group` inside DHCP hosts. |
Example conversion:
```hcl
# 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
- [@​cyril-s](https://github.com/cyril-s)
- [@​cod3mas0n](https://github.com/cod3mas0n)
- [@​hellsingley](https://github.com/hellsingley)
### [`v0.9.0`](https://github.com/dmacvicar/terraform-provider-libvirt/releases/tag/v0.9.0)
[Compare Source](https://github.com/dmacvicar/terraform-provider-libvirt/compare/v0.8.3...v0.9.0)
:warning: :warning: :warning: :warning: **This version of the provider breaks compatibility** :warning: :warning: :warning: :warning:
##### Background
When this provider was developed, the idea was to mimic a cloud experience on top of libvirt. Because of this, the schema was done as flat as possible, features were abstracted and some features like disks from remote sources were added as convenience.
The initial users of the provider were usually makers of infrastructure software who needed complex network setups. Lot of code was contributed which added complexity outside of its initial design.
So for long time I wanted to restart the provider under a new design principles where:
- HCL maps almost 1:1 to libvirt XML and therefore almost any libvirt feature can be supported
- Most of the validation work is left to libvirt which is already doing it
- No abstractions or extra features, and when they do, they should be designed in a way that are quite independent
- More consistency, for example, most libvirt APIs can be mostly separated in lifecycle (create, destroy) which map quite well to terraform resources, and then some query APIs, which map well to data sources. This was not the case how we implemented for example querying the IP addresses
- No unnecessary defensive code, for example, checking that a volume exist when referenced, is a problem that terraform solves if the ID is interpolated and libvirt solves with its own checks, if the volume is referenced by a hardcoded strings.
I knew 1.0 would never come in the current form.
##### The new provider
The new provider is based on the new plugin framework. This gives us some room for better diagnostics and better plans.
It makes definitions more verbose, but it also means we can implement any libvirt feature. Defaults work as long as they are defaults in libvirt.
##### Migration plan
You can find the legacy provider in the v0.8 branch. New releases can be done of 0.8.x versions to add bugfixes, so people who rely on it have a path forward. I'd likely not maintain much of 0.8.x, but I guess many people will help here, as they do today with different PRs.
There is no automated way of migrating the HCL of previous providers, but given that it is documented how the new schema is defined, which was not the case with the previous schema, it should be much easier to drive LLMs to perform a conversion.
You should check the documentation and README, which will give you an idea of the main differences and equivalences, but here is an example of the new schema to get an idea:
```hcl
terraform {
required_providers {
libvirt = {
source = "dmacvicar/libvirt"
}
}
}
provider "libvirt" {
uri = "qemu:///system"
}
# Base Alpine Linux cloud image stored in the default pool.
resource "libvirt_volume" "alpine_base" {
name = "alpine-3.22-base.qcow2"
pool = "default"
format = "qcow2"
create = {
content = {
url = "https://dl-cdn.alpinelinux.org/alpine/v3.22/releases/cloud/generic_alpine-3.22.2-x86_64-bios-cloudinit-r0.qcow2"
}
}
}
# Writable copy-on-write layer for the VM.
resource "libvirt_volume" "alpine_disk" {
name = "alpine-vm.qcow2"
pool = "default"
format = "qcow2"
capacity = 2147483648
backing_store = {
path = libvirt_volume.alpine_base.path
format = "qcow2"
}
}
# Cloud-init seed ISO.
resource "libvirt_cloudinit_disk" "alpine_seed" {
name = "alpine-cloudinit"
user_data = <<-EOF
#cloud-config
chpasswd:
list: |
root:password
expire: false
ssh_pwauth: true
packages:
- openssh-server
timezone: UTC
EOF
meta_data = <<-EOF
instance-id: alpine-001
local-hostname: alpine-vm
EOF
network_config = <<-EOF
version: 2
ethernets:
eth0:
dhcp4: true
EOF
}
# Upload the cloud-init ISO into the pool.
resource "libvirt_volume" "alpine_seed_volume" {
name = "alpine-cloudinit.iso"
pool = "default"
create = {
content = {
url = libvirt_cloudinit_disk.alpine_seed.path
}
}
}
# Virtual machine definition.
resource "libvirt_domain" "alpine" {
name = "alpine-vm"
memory = 1048576
vcpu = 1
os = {
type = "hvm"
arch = "x86_64"
machine = "q35"
}
features = {
acpi = true
}
devices = {
disks = [
{
source = {
pool = libvirt_volume.alpine_disk.pool
volume = libvirt_volume.alpine_disk.name
}
target = {
dev = "vda"
bus = "virtio"
}
},
{
device = "cdrom"
source = {
pool = libvirt_volume.alpine_seed_volume.pool
volume = libvirt_volume.alpine_seed_volume.name
}
target = {
dev = "sdb"
bus = "sata"
}
}
]
interfaces = [
{
type = "network"
model = "virtio" # e1000 is more compatible than virtio for Alpine
source = {
network = "default"
}
# TODO: wait_for_ip not implemented yet (Phase 2)
# This will wait during creation until the interface gets an IP
wait_for_ip = {
timeout = 300 # seconds, default 300
source = "any" # "lease" (DHCP), "agent" (qemu-guest-agent), or "any" (try both)
}
}
]
graphics = {
vnc = {
autoport = "yes"
listen = "127.0.0.1"
}
}
}
running = true
}
# Query the domain's interface addresses
# This data source can be used at any time to retrieve current IP addresses
# without blocking operations like Delete
data "libvirt_domain_interface_addresses" "alpine" {
domain = libvirt_domain.alpine.name
source = "lease" # optional: "lease" (DHCP), "agent" (qemu-guest-agent), or "any"
}
# Output all interface information
output "vm_interfaces" {
description = "All network interfaces with their IP addresses"
value = data.libvirt_domain_interface_addresses.alpine.interfaces
}
# Output the first IP address found
output "vm_ip" {
description = "First IP address of the VM"
value = length(data.libvirt_domain_interface_addresses.alpine.interfaces) > 0 && length(data.libvirt_domain_interface_addresses.alpine.interfaces[0].addrs) > 0 ? data.libvirt_domain_interface_addresses.alpine.interfaces[0].addrs[0].addr : "No IP address found"
}
# Output all IP addresses across all interfaces
output "vm_all_ips" {
description = "All IP addresses across all interfaces"
value = flatten([
for iface in data.libvirt_domain_interface_addresses.alpine.interfaces : [
for addr in iface.addrs : addr.addr
]
])
}
```
Feedback is appreciated. There will be a long journey for people to port and iron all the issues, but it is clear this is the path to go.
Docs: <https://registry.terraform.io/providers/dmacvicar/libvirt/latest/docs>
### [`v0.8.3`](https://github.com/dmacvicar/terraform-provider-libvirt/releases/tag/v0.8.3)
[Compare Source](https://github.com/dmacvicar/terraform-provider-libvirt/compare/v0.8.2...v0.8.3)
- Fix for ssh bug [#​1165](https://github.com/dmacvicar/terraform-provider-libvirt/issues/1165)
**Full Changelog**: <https://github.com/dmacvicar/terraform-provider-libvirt/compare/v0.8.2...v0.8.3>
### [`v0.8.2`](https://github.com/dmacvicar/terraform-provider-libvirt/releases/tag/v0.8.2)
[Compare Source](https://github.com/dmacvicar/terraform-provider-libvirt/compare/v0.8.1...v0.8.2)
#### What's Changed
##### Content sniffing
- The provider no longer detects the image format qcow2 using content sniffing for remote HTTP images. If you leave it blank, it will just set it based on the extension. This allows to use HTTP servers without HTTP Range support.
* Add ability to set mode=private to a macvlan interface. by [@​farsonic](https://github.com/farsonic) in [#​1154](https://github.com/dmacvicar/terraform-provider-libvirt/pull/1154)
##### Upgrade dependencies
- Bump golang.org/x/crypto from 0.27.0 to 0.31.0 by [@​dependabot](https://github.com/dependabot) in [#​1138](https://github.com/dmacvicar/terraform-provider-libvirt/pull/1138)
- Bump golang.org/x/net from 0.29.0 to 0.33.0 by [@​dependabot](https://github.com/dependabot) in [#​1157](https://github.com/dmacvicar/terraform-provider-libvirt/pull/1157)
##### Bug fixes
- Bugfix: ssh port override for [#​1116](https://github.com/dmacvicar/terraform-provider-libvirt/issues/1116) by [@​memetb](https://github.com/memetb) in [#​1117](https://github.com/dmacvicar/terraform-provider-libvirt/pull/1117)
- fix(ci): failing terraform fmt ([#​1158](https://github.com/dmacvicar/terraform-provider-libvirt/issues/1158)) by [@​dmacvicar](https://github.com/dmacvicar) in [#​1159](https://github.com/dmacvicar/terraform-provider-libvirt/pull/1159)
- fix(domain): restore error handling for network operations by [@​SJFCS](https://github.com/SJFCS) in [#​1144](https://github.com/dmacvicar/terraform-provider-libvirt/pull/1144)
- fix: fix the wrong error return value by [@​cangqiaoyuzhuo](https://github.com/cangqiaoyuzhuo) in [#​1161](https://github.com/dmacvicar/terraform-provider-libvirt/pull/1161)
#### New Contributors
- [@​SJFCS](https://github.com/SJFCS) made their first contribution in [#​1144](https://github.com/dmacvicar/terraform-provider-libvirt/pull/1144)
- [@​farsonic](https://github.com/farsonic) made their first contribution in [#​1154](https://github.com/dmacvicar/terraform-provider-libvirt/pull/1154)
- [@​cangqiaoyuzhuo](https://github.com/cangqiaoyuzhuo) made their first contribution in [#​1161](https://github.com/dmacvicar/terraform-provider-libvirt/pull/1161)
**Full Changelog**: <https://github.com/dmacvicar/terraform-provider-libvirt/compare/v0.8.1...v0.8.2>
### [`v0.8.1`](https://github.com/dmacvicar/terraform-provider-libvirt/releases/tag/v0.8.1)
[Compare Source](https://github.com/dmacvicar/terraform-provider-libvirt/compare/v0.8.0...v0.8.1)
#### What's Changed
This release is mostly about fixes for the SSH transport, which was released with many bugs in v0.8.0
- Do not panic on invalid SSH key by [@​scabala](https://github.com/scabala) in [#​1103](https://github.com/dmacvicar/terraform-provider-libvirt/pull/1103)
- Sshconfig missing bugfix - addresses issue [#​1105](https://github.com/dmacvicar/terraform-provider-libvirt/issues/1105) by [@​memetb](https://github.com/memetb) in [#​1109](https://github.com/dmacvicar/terraform-provider-libvirt/pull/1109)
- allow for multiple default identity key files by [@​memetb](https://github.com/memetb) in [#​1112](https://github.com/dmacvicar/terraform-provider-libvirt/pull/1112)
##### Experimental LVM storage pool support
There is a new experimental feature, support for LVM storage pools. I don't use myself this type of pools, so I put together all the contributions and made the code ready for release mostly based on integration tests. Try it and give feedback.
- feat: lvm storage pool impl by [@​jimnydev](https://github.com/jimnydev) in [#​1088](https://github.com/dmacvicar/terraform-provider-libvirt/pull/1088)
#### New Contributors
- [@​scabala](https://github.com/scabala) made their first contribution in [#​1103](https://github.com/dmacvicar/terraform-provider-libvirt/pull/1103)
- [@​jimnydev](https://github.com/jimnydev) made their first contribution in [#​1088](https://github.com/dmacvicar/terraform-provider-libvirt/pull/1088)
**Full Changelog**: <https://github.com/dmacvicar/terraform-provider-libvirt/compare/v0.8.0...v0.8.1>
### [`v0.8.0`](https://github.com/dmacvicar/terraform-provider-libvirt/releases/tag/v0.8.0)
[Compare Source](https://github.com/dmacvicar/terraform-provider-libvirt/compare/v0.7.6...v0.8.0)
#### What's Changed
Two big features include improved ssh config support (for example for supporting jump hosts) and a new data source for host information.
- expanded ssh\_config parameters for qemu+ssh uri option by [@​memetb](https://github.com/memetb) in [#​1059](https://github.com/dmacvicar/terraform-provider-libvirt/pull/1059)
- feat: add data sources to extract node and device information by [@​muresan](https://github.com/muresan) in [#​1042](https://github.com/dmacvicar/terraform-provider-libvirt/pull/1042)
##### Breaking changes
- DNS is enabled by default, like in libvirt. [#​1100](https://github.com/dmacvicar/terraform-provider-libvirt/pull/1100)
- Wait intervals for polling libvirt are reduced, making everything faster (including testsuite)
##### Other highlights:
- Acceptance testsuite is finally fully passing again
- Many code cleanups
- Updated golangci-lint
- Many updated dependencies
- Mark disk wwn and nvram arguments as computed by [@​wfdewith](https://github.com/wfdewith) in [#​1064](https://github.com/dmacvicar/terraform-provider-libvirt/pull/1064)
- Default machine by [@​e4t](https://github.com/e4t) in [#​1014](https://github.com/dmacvicar/terraform-provider-libvirt/pull/1014)
- Add Combustion resource to use instead of the ignition one by [@​cbosdo](https://github.com/cbosdo) in [#​1068](https://github.com/dmacvicar/terraform-provider-libvirt/pull/1068)
##### Community
We activated discussions, so that the community can share useful files, help each other and also get announcements.
- <https://github.com/dmacvicar/terraform-provider-libvirt/discussions>
#### Contributors
Thanks to all the community for their contributions and for supporting other users:
- [@​muresan](https://github.com/muresan) made their first contribution in [#​1042](https://github.com/dmacvicar/terraform-provider-libvirt/pull/1042)
- [@​wfdewith](https://github.com/wfdewith) made their first contribution in [#​1064](https://github.com/dmacvicar/terraform-provider-libvirt/pull/1064)
- [@​kubealex](https://github.com/kubealex) made their first contribution in [#​1056](https://github.com/dmacvicar/terraform-provider-libvirt/pull/1056)
- [@​shafer](https://github.com/shafer) made their first contribution in [#​927](https://github.com/dmacvicar/terraform-provider-libvirt/pull/927)
- [@​testwill](https://github.com/testwill) made their first contribution in [#​1086](https://github.com/dmacvicar/terraform-provider-libvirt/pull/1086)
- [@​memetb](https://github.com/memetb) made their first contribution in [#​1059](https://github.com/dmacvicar/terraform-provider-libvirt/pull/1059)
- [@​michaelbeaumont](https://github.com/michaelbeaumont)
- [@​cbosdo](https://github.com/cbosdo)
- and others... (let me know if I missed you)
**Full Changelog**: <https://github.com/dmacvicar/terraform-provider-libvirt/compare/v0.7.6...v0.8.0>
</details>
---
### Configuration
📅 **Schedule**: (UTC)
- Branch creation
- At any time (no schedule defined)
- Automerge
- At any time (no schedule defined)
🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied.
♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.
🔕 **Ignore**: Close this PR and you won't be reminded about this update again.
---
- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box
---
This PR has been generated by [Mend Renovate CLI](https://github.com/renovatebot/renovate).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0My4xMjkuMCIsInVwZGF0ZWRJblZlciI6IjQ0LjQuNSIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOltdfQ==-->
renovate
changed title from Update Terraform libvirt to v0.9.7 to Update Terraform libvirt to v0.9.82026-07-15 10:26:46 +00:00
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
This PR contains the following updates:
0.7.6→0.9.8Release Notes
dmacvicar/terraform-provider-libvirt (libvirt)
v0.9.8Compare Source
What's Changed
Domain lifecycle and readback
running = truedrift by starting an existing stopped domain directly and waiting until libvirt reports it running.Volume behavior
Network behavior
autostart.Included changes
fix: replace volume when the creation options change(#1299)fix: preserve interface target dev on readbackfix: preserve planned channel source unix path on readback(#1338)Fix domain id inconsistency on update(#1337)fix: support importing volume by key(#1334)Force network replacement on any attr change except autostart.(#1331)fix: reconcile domain running drift(Resolves: #1339)Contributors
Full Changelog
Full Changelog: https://github.com/dmacvicar/terraform-provider-libvirt/compare/v0.9.7...v0.9.8
v0.9.7Compare Source
Highlights
libvirt_volumeURL uploads to work when the source server does not provideContent-Length, as long ascapacityis set explicitly (including acceptance tests).What's Changed
Volume upload behavior
create.content.urluploads to fall back to user-providedcapacitywhen the HTTP source does not includeContent-Length.CI and release maintenance
Included changes
chore: add dependabot config for github actionschore(deps): update GitHub Actions dependencies(#1306, #1307, #1308, #1309, #1310, #1312, #1313, #1314, #1315)fix: allow URL uploads without content length(closes #1316)Contributors
Full Changelog
Full Changelog: https://github.com/dmacvicar/terraform-provider-libvirt/compare/v0.9.6...v0.9.7
v0.9.6Compare Source
Highlights
terraform importsupport forlibvirt_domainusing an existing domain UUID.What's Changed
Domain lifecycle and import
terraform importsupport forlibvirt_domainby UUID, allowing Terraform state to be populated from an existing libvirt domain definition.Readback and state preservation
Maintenance
google.golang.org/grpcfrom1.75.1to1.79.3.Contributors
Full Changelog
Full Changelog: https://github.com/dmacvicar/terraform-provider-libvirt/compare/v0.9.5...v0.9.6
v0.9.5Compare Source
Highlights
XMLNamemetadata.What's Changed
Schema and code generation
XMLNamemetadata, exposing:qemu_commandlineqemu_capabilitiesqemu_overrideqemu_deprecationlxc_namespacebhyve_commandlinevmware_data_center_pathxen_commandlineDomain and network improvements
vlan-idhandling reported in issue #1236.Documentation
Included changes
docs: add hypervisor namespace field descriptionsfix: support hypervisor XMLName fields in codegenrefactor: apply codegen field policies after reflectiontest: cover anonymous embedded XML fieldsfix: Process anonymous fields in libvirt XML(#1222 by @atopuzov)fix: make vlan tag ids configurable(fixes #1236)v0.9.4Compare Source
New Features (Experimental)
destroy.shutdown.timeoutoption onlibvirt_domainsends an ACPI shutdown signal and waits for the guest to power off cleanly before destroying it, instead of force-killing. Subject to change in future releases.createanddestroyblocks onlibvirt_poolgive explicit control over build, start, autostart, and backing storage deletion behavior during pool create/destroy. Subject to change in future releases.Bug Fixes
KEEP_NVRAMandKEEP_TPMundefine flags during updates. (#1232, @atopuzov)capacity_unitnot preserved on volume readback — specifyingcapacity_unit = "GiB"on a volume would cause "Provider produced inconsistent result after apply" on every apply, because libvirt normalizes to bytes in XML. The codegen now preserves the user's unit when the value was explicitly set. (#1253)terraform destroyof adirpool was callingStoragePoolDeletewhich removed the backing directory. The default behavior now preserves the directory (matchingvirsh pool-destroy+pool-undefine), with an explicitdestroy.delete = trueoverride available. (#1285)Dependencies
github.com/cloudflare/circlfrom 1.6.1 to 1.6.3. (#1284)Contributors
v0.9.3Compare Source
What's Changed
New Contributors
And thanks for those unmerged PRs @SkinGad @yannlambret @nicholas-rees
Full Changelog: https://github.com/dmacvicar/terraform-provider-libvirt/compare/v0.9.2...v0.9.3
v0.9.2Compare Source
Full Changelog: https://github.com/dmacvicar/terraform-provider-libvirt/compare/v0.9.1...v0.9.2
Thanks to @BohdanTkachenko for the feedback, reports and PR suggestions.
v0.9.1Compare Source
Bugfixes
VIR_DOMAIN_UNDEFINE_NVRAMandVIR_DOMAIN_UNDEFINE_TPM. These defaults may change in the future and be part of a domain block likecreateanddelete. (#1203 )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
accessmode→access_mode,portgroup→port_group). Set exactly the fields you care about; anything left null stays absent in the XML.memory+memory_unit,capacity+capacity_unit, etc.). Leaving the unit unset lets libvirt use its default.yes/nostrings when libvirt models them as attributes (e.g.os.loader_readonly). True presence booleans (likefeatures.acpi) still use Terraform bools.source.poolorfilesystem.source.{ metadata = { xml = <<EOF ... } }so we can extend later without breaking state.Domain Resource
Top-level attribute mapping
unitmemory_unitmax_memorymaximum_memorymaximum_memory_unitif you previously used a non-default unit.max_memory_slotsmaximum_memory_slotscurrent_memorycurrent_memory+ optionalcurrent_memory_unitmetadata(string)metadata = { xml = <<EOF ... EOF }os.archos.type_archtype_*prefix mirrors<os><type arch="..."/>.os.machineos.type_machineos.kernel_argsos.cmdline<cmdline>element.os.loader_pathos.loaderos.loader_readonly(bool)os.loader_readonly(string)"yes"/"no"because the XML attribute is a string.os.nvram.*os.nv_ram = { file, template, format = { type = ... } }devices.filesystems[*].accessmodeaccess_modedevices.filesystems[*].readonlyread_onlydevices.interfaces[*].source.portgroupsource = { network = { port_group = ... } }devices.rngs[*].devicebackend = { random = "/dev/urandom" }orbackend = { egd = { ... } }OS block specifics
os.boot_devicesis still a list, but if you previously stored strings you now provide objects:boot_devices = [{ dev = "hd" }, { dev = "network" }]."yes"/"no"). Wrap them intostring()if you had boolean locals.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: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:
Filesystems follow the same pattern. Replace the old flat fields with nested objects:
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: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, andsource.devare now mutually exclusive nested objects. Example conversions:portgroupbecameport_group,wait_for_ipstays the same helper object.RNG / TPM / other devices
<backend>. Usebackend = { random = "/dev/urandom" }for /dev/random orbackend = { egd = { source = { mode = "connect", host = "unix", service = "..." } } }for EGD sockets.backend = { emulator = { path = "/var/lib/swtpm/sock" } }). Map your previousbackend_typeto one of the backend objects:emulator,passthrough, orexternal.auto_port,websocket, etc.).Metadata
0.9 stored raw XML as a string. Now wrap it:
Storage Volume Resource
Key differences:
format(string)target = { format = { type = "qcow2" } }permissions.*target.permissions.*target.backing_store.formatbacking_store = { format = { type = "qcow2" } }<format>element.capacitycapacity+ optionalcapacity_unitcapacity_unitunset to keep KiB.allocationallocation+allocation_unit(read-only)path(computed)path, but it mirrorstarget.pathEverything else (name, pool, create/content) behaves exactly like 0.9. Plan/apply will touch
terraform stateautomatically 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
modeforward = { mode = "nat" }<forward>element.bridge(string)bridge = { name = "virbr0" }autostartautostartipsips, but nested attr names now snake_case (local_ptr,dhcp.hosts, etc.)portgroup→port_groupinside DHCP hosts.Example conversion:
DHCP ranges/hosts did not change other than automatic snake_case normalisation.
Contributors
v0.9.0Compare Source
⚠️ ⚠️ ⚠️ ⚠️ This version of the provider breaks compatibility ⚠️ ⚠️ ⚠️ ⚠️
Background
When this provider was developed, the idea was to mimic a cloud experience on top of libvirt. Because of this, the schema was done as flat as possible, features were abstracted and some features like disks from remote sources were added as convenience.
The initial users of the provider were usually makers of infrastructure software who needed complex network setups. Lot of code was contributed which added complexity outside of its initial design.
So for long time I wanted to restart the provider under a new design principles where:
I knew 1.0 would never come in the current form.
The new provider
The new provider is based on the new plugin framework. This gives us some room for better diagnostics and better plans.
It makes definitions more verbose, but it also means we can implement any libvirt feature. Defaults work as long as they are defaults in libvirt.
Migration plan
You can find the legacy provider in the v0.8 branch. New releases can be done of 0.8.x versions to add bugfixes, so people who rely on it have a path forward. I'd likely not maintain much of 0.8.x, but I guess many people will help here, as they do today with different PRs.
There is no automated way of migrating the HCL of previous providers, but given that it is documented how the new schema is defined, which was not the case with the previous schema, it should be much easier to drive LLMs to perform a conversion.
You should check the documentation and README, which will give you an idea of the main differences and equivalences, but here is an example of the new schema to get an idea:
Feedback is appreciated. There will be a long journey for people to port and iron all the issues, but it is clear this is the path to go.
Docs: https://registry.terraform.io/providers/dmacvicar/libvirt/latest/docs
v0.8.3Compare Source
Full Changelog: https://github.com/dmacvicar/terraform-provider-libvirt/compare/v0.8.2...v0.8.3
v0.8.2Compare Source
What's Changed
Content sniffing
Upgrade dependencies
Bug fixes
New Contributors
Full Changelog: https://github.com/dmacvicar/terraform-provider-libvirt/compare/v0.8.1...v0.8.2
v0.8.1Compare Source
What's Changed
This release is mostly about fixes for the SSH transport, which was released with many bugs in v0.8.0
Experimental LVM storage pool support
There is a new experimental feature, support for LVM storage pools. I don't use myself this type of pools, so I put together all the contributions and made the code ready for release mostly based on integration tests. Try it and give feedback.
New Contributors
Full Changelog: https://github.com/dmacvicar/terraform-provider-libvirt/compare/v0.8.0...v0.8.1
v0.8.0Compare Source
What's Changed
Two big features include improved ssh config support (for example for supporting jump hosts) and a new data source for host information.
Breaking changes
Other highlights:
Community
We activated discussions, so that the community can share useful files, help each other and also get announcements.
Contributors
Thanks to all the community for their contributions and for supporting other users:
Full Changelog: https://github.com/dmacvicar/terraform-provider-libvirt/compare/v0.7.6...v0.8.0
Configuration
📅 Schedule: (UTC)
🚦 Automerge: Disabled by config. Please merge this manually once you are satisfied.
♻ Rebasing: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.
🔕 Ignore: Close this PR and you won't be reminded about this update again.
This PR has been generated by Mend Renovate CLI.
Update Terraform libvirt to v0.9.7to Update Terraform libvirt to v0.9.83ddfe7431eto3aba16f34dView command line instructions
Checkout
From your project repository, check out a new branch and test the changes.