Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
75c922b
Update public ssh key path at base level
maximenoel8 Mar 12, 2026
6cf9f16
Try force ipv4 in libvirt host resource configuration
maximenoel8 Mar 12, 2026
72e3e7a
Improve syntax for IPv4 selection in libvirt host resource configuration
maximenoel8 Mar 12, 2026
9ed4e08
Skip local links in libvirt host resource configuration
maximenoel8 Mar 12, 2026
4f625e5
Add a loop to wait for libvirt domain to report a routable IP before …
maximenoel8 Mar 12, 2026
b428d5f
Increase timeout for waiting for libvirt domain IP to be routable
maximenoel8 Mar 12, 2026
f888578
Try device random
maximenoel8 Mar 12, 2026
e911d8f
Increase more
maximenoel8 Mar 12, 2026
1c5b82e
Try rng.xls
maximenoel8 Mar 12, 2026
dddfc29
Try using tcp
maximenoel8 Mar 12, 2026
9186c2f
Use env variable for hypervisor URI
maximenoel8 Mar 12, 2026
7a0f285
Use env variable for hypervisor URI
maximenoel8 Mar 12, 2026
a32e138
Parse the hypervisor
maximenoel8 Mar 12, 2026
937a96b
Reduce timeout for waiting for
maximenoel8 Mar 12, 2026
fba4618
Check for hypervisor and virsh availability before waiting for IP
maximenoel8 Mar 12, 2026
9bf2f6f
Add other non routable IP
maximenoel8 Mar 12, 2026
7d2592b
Read the IP from the file written by wait_for_ip.sh and store it for …
maximenoel8 Mar 12, 2026
f3433ca
Use file to store and reference IP from wait_for_ip.sh output
maximenoel8 Mar 13, 2026
07349fb
Use file to store and reference IP from wait_for_ip.sh output
maximenoel8 Mar 13, 2026
1b0de43
Doesn't work revert.
maximenoel8 Mar 13, 2026
e8f374e
fix regex to exclude non-routable IP addresses
maximenoel8 Mar 13, 2026
050fc62
Remove lease check and simplify IP retrieval logic.
maximenoel8 Mar 13, 2026
2f1dbaa
Try to use data external to read IP from wait_for_ip.sh and avoid rel…
maximenoel8 Mar 13, 2026
18c769e
Data is not order correctly
maximenoel8 Mar 13, 2026
070ac49
Update with review. Remove unnecessary conditional checks and simplif…
maximenoel8 Mar 25, 2026
76f4350
Update the regex
maximenoel8 Mar 25, 2026
8a0bd80
update regex
maximenoel8 Mar 25, 2026
6422c46
Try fix
maximenoel8 Apr 16, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions backend_modules/libvirt/base/main.tf
Original file line number Diff line number Diff line change
Expand Up @@ -118,5 +118,6 @@ output "configuration" {
bastion_password = lookup(var.provider_settings, "bastion_password", null)
bastion_private_key = lookup(var.provider_settings, "bastion_private_key", null)
bastion_certificate = lookup(var.provider_settings, "bastion_certificate", null)
libvirt_uri = lookup(var.provider_settings, "libvirt_uri", "qemu:///system")
}
}
26 changes: 26 additions & 0 deletions backend_modules/libvirt/host/get_ip.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
#!/bin/bash
# get_ip.sh <domain_name>
# Reads the IP written by wait_for_ip.sh and returns it as JSON.
# Called by Terraform "data external" source after wait_for_ip completes.

DOMAIN=$1
IP_FILE="/tmp/${DOMAIN}.ip"

if [ -z "$DOMAIN" ]; then
echo "{\"ip\": \"\"}"
exit 1
fi

if [ ! -f "$IP_FILE" ]; then
echo "Error: IP file not found at $IP_FILE" >&2
exit 1
fi

IP=$(tr -d '[:space:]' < "$IP_FILE")

if [ -z "$IP" ]; then
echo "Error: IP file is empty at $IP_FILE" >&2
exit 1
fi

echo "{\"ip\": \"${IP}\"}"
Comment thread
maximenoel8 marked this conversation as resolved.
114 changes: 61 additions & 53 deletions backend_modules/libvirt/host/main.tf
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,11 @@ locals {
combustion_images = ["slmicro60o", "slmicro61o", "slmicro62o", "sles160o"]
gpg_keys = [
for key in fileset("salt/default/gpg_keys/", "*.key"): {
path = "/etc/gpg_keys/${key}"
content = filebase64("salt/default/gpg_keys/${key}")
encoding = "b64"
owner = "root:root"
permissions = "0700"
path = "/etc/gpg_keys/${key}"
content = filebase64("salt/default/gpg_keys/${key}")
encoding = "b64"
owner = "root:root"
permissions = "0700"
}
]
container_runtime = lookup(var.grains, "container_runtime", "")
Expand All @@ -25,34 +25,36 @@ locals {
mac = null
cpu_model = "custom"
xslt = null
},
contains(local.x86_64_v2_images, var.image) ? { cpu_model = "host-model", xslt = file("${path.module}/cpu_features.xsl") } : {},
contains(var.roles, "server") ? { memory = 4096, vcpu = 2 } : {},
contains(var.roles, "server_containerized") ? { memory = 16384, vcpu = 4 } : {},
contains(var.roles, "server_kubernetes") ? { memory = 16384, vcpu = 4 } : {},
contains(var.roles, "server") && lookup(var.base_configuration, "testsuite", false) ? { memory = 8192, vcpu = 4 } : {},
contains(var.roles, "server_containerized") && lookup(var.base_configuration, "testsuite", false) ? { memory = 16384, vcpu = 4 } : {},
contains(var.roles, "server_kubernetes") && lookup(var.base_configuration, "testsuite", false) ? { memory = 16384, vcpu = 4 } : {},
contains(var.roles, "proxy") ? { memory = 2048, vcpu = 2 } : {},
contains(var.roles, "proxy_containerized") ? { memory = 2048, vcpu = 2 } : {},
contains(var.roles, "proxy_kubernetes") ? { memory = 2048, vcpu = 2 } : {},
contains(var.roles, "proxy") && lookup(var.base_configuration, "testsuite", false) ? { memory = 2048, vcpu = 2 } : {},
contains(var.roles, "proxy_containerized") && lookup(var.base_configuration, "testsuite", false) ? { memory = 2048, vcpu = 2 } : {},
contains(var.roles, "proxy_kubernetes") && lookup(var.base_configuration, "testsuite", false) ? { memory = 2048, vcpu = 2 } : {},
contains(var.roles, "pxe_boot")? { memory = 2048} : {},
contains(var.roles, "mirror") ? { memory = 1024 } : {},
contains(var.roles, "build_host") ? { vcpu = 2 } : {},
contains(var.roles, "controller") ? { memory = 2048 } : {},
contains(var.roles, "grafana") ? { memory = 4096 } : {},
contains(var.roles, "salt_testenv") ? { memory = 4096, vcpu = 2 } : {},
contains(var.roles, "virthost") ? { memory = 4096, vcpu = 3 } : {},
contains(var.roles, "jenkins") ? { memory = 16384, vcpu = 4 } : {},
var.provider_settings,
contains(var.roles, "virthost") ? { cpu_model = "host-passthrough", xslt = file("${path.module}/virthost.xsl") } : {},
contains(var.roles, "pxe_boot") ? { xslt = templatefile("${path.module}/pxe_boot.xsl", { manufacturer = local.manufacturer, product = local.product }) } : {})
cloud_init = length(regexall("o$", var.image)) > 0 && !contains(local.combustion_images, var.image)
ignition = length(regexall("-ign$", var.image)) > 0
add_net = var.base_configuration["additional_network"] != null ? slice(split(".", var.base_configuration["additional_network"]), 0, 3) : []
},
contains(local.x86_64_v2_images, var.image) ? { cpu_model = "host-model", xslt = file("${path.module}/cpu_features.xsl") } : {},
// Inject RNG via XSLT for SLES15 to solve boot latency/entropy starvation
length(regexall("sles15", var.image)) > 0 ? { xslt = file("${path.module}/rng.xsl") } : {},
contains(var.roles, "server") ? { memory = 4096, vcpu = 2 } : {},
contains(var.roles, "server_containerized") ? { memory = 16384, vcpu = 4 } : {},
contains(var.roles, "server_kubernetes") ? { memory = 16384, vcpu = 4 } : {},
contains(var.roles, "server") && lookup(var.base_configuration, "testsuite", false) ? { memory = 8192, vcpu = 4 } : {},
contains(var.roles, "server_containerized") && lookup(var.base_configuration, "testsuite", false) ? { memory = 16384, vcpu = 4 } : {},
contains(var.roles, "server_kubernetes") && lookup(var.base_configuration, "testsuite", false) ? { memory = 16384, vcpu = 4 } : {},
contains(var.roles, "proxy") ? { memory = 2048, vcpu = 2 } : {},
contains(var.roles, "proxy_containerized") ? { memory = 2048, vcpu = 2 } : {},
contains(var.roles, "proxy_kubernetes") ? { memory = 2048, vcpu = 2 } : {},
contains(var.roles, "proxy") && lookup(var.base_configuration, "testsuite", false) ? { memory = 2048, vcpu = 2 } : {},
contains(var.roles, "proxy_containerized") && lookup(var.base_configuration, "testsuite", false) ? { memory = 2048, vcpu = 2 } : {},
contains(var.roles, "proxy_kubernetes") && lookup(var.base_configuration, "testsuite", false) ? { memory = 2048, vcpu = 2 } : {},
contains(var.roles, "pxe_boot") ? { memory = 2048 } : {},
contains(var.roles, "mirror") ? { memory = 1024 } : {},
contains(var.roles, "build_host") ? { vcpu = 2 } : {},
contains(var.roles, "controller") ? { memory = 2048 } : {},
contains(var.roles, "grafana") ? { memory = 4096 } : {},
contains(var.roles, "salt_testenv") ? { memory = 4096, vcpu = 2 } : {},
contains(var.roles, "virthost") ? { memory = 4096, vcpu = 3 } : {},
contains(var.roles, "jenkins") ? { memory = 16384, vcpu = 4 } : {},
var.provider_settings,
contains(var.roles, "virthost") ? { cpu_model = "host-passthrough", xslt = file("${path.module}/virthost.xsl") } : {},
contains(var.roles, "pxe_boot") ? { xslt = templatefile("${path.module}/pxe_boot.xsl", { manufacturer = local.manufacturer, product = local.product }) } : {})
cloud_init = length(regexall("o$", var.image)) > 0 && !contains(local.combustion_images, var.image)
ignition = length(regexall("-ign$", var.image)) > 0
add_net = var.base_configuration["additional_network"] != null ? slice(split(".", var.base_configuration["additional_network"]), 0, 3) : []

user_data = templatefile("${path.module}/user_data.yaml", {
image = var.image
Expand All @@ -62,7 +64,7 @@ locals {
container_server = contains(var.roles, "server_containerized")
server_kubernetes = contains(var.roles, "server_kubernetes")
container_proxy = contains(var.roles, "proxy_containerized")
proxy_kubernetes = contains(var.roles, "proxy_kubernetes")
proxy_kubernetes = contains(var.roles, "proxy_kubernetes")
testsuite = lookup(var.base_configuration, "testsuite", false)
files = jsonencode(local.gpg_keys)
additional_repos = jsonencode(var.additional_repos)
Expand Down Expand Up @@ -117,7 +119,6 @@ resource "libvirt_volume" "data_disk" {

resource "libvirt_volume" "database_disk" {
name = "${local.resource_name_prefix}${var.quantity > 1 ? "-${count.index + 1}" : ""}-database-disk"
// needs to be converted to bytes
size = var.second_additional_disk_size * 1024 * 1024 * 1024
pool = lookup(var.volume_provider_settings, "pool", var.base_configuration["pool"])
count = var.second_additional_disk_size > 0 ? var.quantity : 0
Expand Down Expand Up @@ -161,9 +162,9 @@ resource "libvirt_domain" "domain" {
// base disk + additional disks if any
dynamic "disk" {
for_each = concat(
length(libvirt_volume.main_disk) == var.quantity ? [{"volume_id" : libvirt_volume.main_disk[count.index].id}] : [],
length(libvirt_volume.data_disk) == var.quantity ? [{"volume_id" : libvirt_volume.data_disk[count.index].id}] : [],
length(libvirt_volume.database_disk) == var.quantity ? [{"volume_id" : libvirt_volume.database_disk[count.index].id}] : []
length(libvirt_volume.main_disk) == var.quantity ? [{"volume_id" : libvirt_volume.main_disk[count.index].id}] : [],
length(libvirt_volume.data_disk) == var.quantity ? [{"volume_id" : libvirt_volume.data_disk[count.index].id}] : [],
length(libvirt_volume.database_disk) == var.quantity ? [{"volume_id" : libvirt_volume.database_disk[count.index].id}] : []
)
content {
volume_id = disk.value.volume_id
Expand All @@ -178,7 +179,7 @@ resource "libvirt_domain" "domain" {
for_each = slice(
[
{
"wait_for_lease" = true
"wait_for_lease" = false
"network_name" = var.base_configuration["network_name"]
"network_id" = null
"bridge" = var.base_configuration["bridge"]
Expand All @@ -192,8 +193,8 @@ resource "libvirt_domain" "domain" {
"mac" = null
},
],
var.connect_to_base_network ? 0 : 1,
var.base_configuration["additional_network"] != null && var.connect_to_additional_network ? 2 : 1,
var.connect_to_base_network ? 0 : 1,
var.base_configuration["additional_network"] != null && var.connect_to_additional_network ? 2 : 1,
)
content {
wait_for_lease = network_interface.value.wait_for_lease
Expand All @@ -208,32 +209,37 @@ resource "libvirt_domain" "domain" {
type = "pty"
target_port = "0"
target_type = "serial"
source_host = null
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why those lanes were remove ?

Answer:
They are redundant defaults that can occasionally cause validation warnings or clutter in modern OpenTofu/Terraform providers

source_service = null
}

console {
type = "pty"
target_port = "1"
target_type = "virtio"
source_host = null
source_service = null
}

graphics {
type = "spice"
listen_type = "address"
type = "spice"
listen_type = "address"
listen_address = "0.0.0.0"
autoport = true
autoport = true
}

xml {
xslt = local.provider_settings["xslt"]
}
}

resource "terraform_data" "provisioning" {
// data external calls wait_for_ip.sh which polls the QEMU agent until a routable
// IP is found and returns it as JSON. This avoids reading network_interface[0].addresses
// from Terraform state which is unreliable on bridged networks.
data "external" "ip" {
count = var.provision ? var.quantity : 0
depends_on = [libvirt_domain.domain]
program = ["bash", "${path.module}/wait_for_ip.sh", libvirt_domain.domain[count.index].name, var.base_configuration["libvirt_uri"]]
}

resource "terraform_data" "provisioning" {
depends_on = [data.external.ip]

triggers_replace = {
main_volume_id = length(libvirt_volume.main_disk) == var.quantity ? libvirt_volume.main_disk[count.index].id : null
Expand All @@ -256,13 +262,15 @@ resource "terraform_data" "provisioning" {
authorized_keys = var.ssh_key_path
gpg_keys = var.gpg_keys
ipv6 = var.ipv6
})
})
}

count = var.provision ? var.quantity : 0

connection {
host = libvirt_domain.domain[count.index].network_interface[0].addresses[0]
// Use IP found by wait_for_ip.sh via QEMU agent, passed through data external get_ip.sh
Comment thread
maximenoel8 marked this conversation as resolved.
host = data.external.ip[count.index].result["ip"]

user = "root"
password = "linux"
// ssh connection through a bastion host
Expand Down Expand Up @@ -300,8 +308,8 @@ resource "terraform_data" "provisioning" {
swap_file_size = var.swap_file_size
product_version = local.product_version
authorized_keys = concat(
var.base_configuration["ssh_key_path"] != null ? [trimspace(file(var.base_configuration["ssh_key_path"]))] : [],
var.ssh_key_path != null ? [trimspace(file(var.ssh_key_path))] : [],
var.base_configuration["ssh_key_path"] != null ? [trimspace(file(var.base_configuration["ssh_key_path"]))] : [],
var.ssh_key_path != null ? [trimspace(file(var.ssh_key_path))] : [],
)
gpg_keys = var.gpg_keys
connect_to_base_network = var.connect_to_base_network
Expand Down
19 changes: 19 additions & 0 deletions backend_modules/libvirt/host/rng.xsl
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<?xml version="1.0" ?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output omit-xml-declaration="yes" indent="yes"/>

<xsl:template match="/domain/devices">
<xsl:copy>
<xsl:apply-templates select="node()|@*"/>
<rng model='virtio'>
<backend model='random'>/dev/urandom</backend>
</rng>
</xsl:copy>
</xsl:template>

<xsl:template match="node()|@*">
<xsl:copy>
<xsl:apply-templates select="node()|@*"/>
</xsl:copy>
</xsl:template>
</xsl:stylesheet>
48 changes: 48 additions & 0 deletions backend_modules/libvirt/host/wait_for_ip.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
#!/bin/bash
# wait_for_ip.sh <domain_name> [hypervisor_uri]
# Waits until a routable IP is visible via the QEMU agent.
# Returns JSON {"ip": "x.x.x.x"} for use with Terraform data "external" source.
# Note: --source lease is not used as bridged networks do not have libvirt-managed DHCP.

DOMAIN=$1
HYPERVISOR_URI=${2:-$LIBVIRT_DEFAULT_URI}
RETRIES=100
SLEEP_INTERVAL=10

if [ -z "$DOMAIN" ]; then
echo "Error: domain name is required" >&2
exit 1
fi

if [ -z "$HYPERVISOR_URI" ]; then
echo "Error: No hypervisor URI provided and LIBVIRT_DEFAULT_URI is not set" >&2
exit 1
fi

if ! command -v virsh &>/dev/null; then
echo "Error: virsh not found. Please install libvirt-client." >&2
exit 1
Comment thread
maximenoel8 marked this conversation as resolved.
fi

echo "Starting wait for routable IP on domain: $DOMAIN via $HYPERVISOR_URI" >&2

for ((i=1; i<=RETRIES; i++)); do
AGENT_IP=$(virsh -c "$HYPERVISOR_URI" domifaddr "$DOMAIN" --source agent 2>&1 | \
awk '/ipv4|ipv6/ {print $4}' | \
cut -d/ -f1 | \
Comment thread
Bischoff marked this conversation as resolved.
grep -Ev '^fe80:|^169\.254\.|^127\.|^::1$|^10\.89\.|^192\.168\.' | \
head -n1)

if [ -n "$AGENT_IP" ]; then
echo "Success: Found routable IP $AGENT_IP" >&2
# Return JSON to stdout for Terraform data external
echo "{\"ip\": \"${AGENT_IP}\"}"
exit 0
fi

echo "[$i/$RETRIES] Waiting for routable IP for $DOMAIN..." >&2
sleep "$SLEEP_INTERVAL"
done

echo "Error: Timed out waiting for routable IP for $DOMAIN." >&2
exit 1
Loading