my-homelab-configs/bootstrap/provisioning/main.tf

408 lines
13 KiB
HCL

terraform {
required_version = ">= 1.0"
required_providers {
null = {
source = "hashicorp/null"
version = "~> 3.2"
}
}
}
locals {
tftp_root = "${var.provisioning_install_dir}/tftp"
http_root = "${var.provisioning_install_dir}/html"
debian_netboot_base_url = var.debian_netboot_base_url != "" ? var.debian_netboot_base_url : "http://${var.debian_mirror_host}${var.debian_mirror_directory}/dists/${var.debian_suite}/main/installer-arm64/current/images/netboot/debian-installer/arm64"
preseed_url = "http://${var.http_host}:${var.http_port}/preseed/debian13-arm64-worker.cfg"
template_packages = distinct(concat([
"sudo",
"openssh-server",
"curl",
"ca-certificates",
"gnupg",
"apt-transport-https",
"qemu-guest-agent",
"cloud-init",
"containerd",
"open-iscsi",
"nfs-common",
"iptables",
"iproute2",
"conntrack",
"socat",
"ebtables",
"ethtool",
"ipset",
"ipvsadm",
"jq",
"vim-tiny",
"chrony",
"lvm2",
"xfsprogs",
], var.additional_template_packages))
template_user_ssh_keys = distinct(compact(concat(var.template_user_ssh_authorized_keys, [try(trimspace(file(var.template_user_ssh_public_key_path)), "")])))
ssh_authorized_keys_base64 = base64encode(join("\n", local.template_user_ssh_keys))
node_dns_servers = join(" ", var.node_dns_servers)
kernel_cgroup_boot_options = join(" ", var.kernel_cgroup_boot_options)
pimox_template_net0 = var.pimox_template_mac != "" ? "virtio=${var.pimox_template_mac},bridge=${var.pimox_template_bridge}" : "virtio,bridge=${var.pimox_template_bridge}"
template_package_list = join(" ", local.template_packages)
provisioning_http_base_url = "http://${var.http_host}:${var.http_port}"
provisioning_script_url = "${local.provisioning_http_base_url}/scripts/golden-node-prepare.sh"
prepare_template_script_url = "${local.provisioning_http_base_url}/scripts/prepare-template.sh"
dnsmasq_conf = templatefile("${path.module}/templates/dnsmasq.conf.tftpl", {
provisioning_interface = var.provisioning_interface
proxy_dhcp_range = var.proxy_dhcp_range
pxe_boot_file = var.pxe_boot_file
tftp_root = local.tftp_root
})
nginx_conf = templatefile("${path.module}/templates/nginx.conf.tftpl", {
http_port = tostring(var.http_port)
http_root = local.http_root
})
grub_cfg = templatefile("${path.module}/templates/grub.cfg.tftpl", {
preseed_url = local.preseed_url
template_hostname = var.template_hostname
template_domain = var.template_domain
})
preseed_cfg = templatefile("${path.module}/templates/preseed.cfg.tftpl", {
locale = var.locale
keyboard = var.keyboard
timezone = var.timezone
template_hostname = var.template_hostname
template_domain = var.template_domain
template_disk = var.template_disk
template_user = var.template_user
template_user_full_name = var.template_user_full_name
template_user_password_hash = var.template_user_password_hash
debian_mirror_host = var.debian_mirror_host
debian_mirror_directory = var.debian_mirror_directory
template_package_list = local.template_package_list
provisioning_script_url = local.provisioning_script_url
prepare_template_script_url = local.prepare_template_script_url
})
golden_node_prepare = templatefile("${path.module}/templates/golden-node-prepare.sh.tftpl", {
template_user = var.template_user
ssh_authorized_keys_base64 = local.ssh_authorized_keys_base64
kubernetes_minor_version = var.kubernetes_minor_version
kernel_cgroup_boot_options = local.kernel_cgroup_boot_options
registry_endpoint = var.registry_endpoint
node_dns_servers = local.node_dns_servers
})
prepare_template = templatefile("${path.module}/templates/prepare-template.sh.tftpl", {
template_hostname = var.template_hostname
clone_hostname_prefix = var.clone_hostname_prefix
template_domain = var.template_domain
kernel_cgroup_boot_options = local.kernel_cgroup_boot_options
})
config_hash = sha256(join("\n---\n", [
local.dnsmasq_conf,
local.nginx_conf,
local.grub_cfg,
local.preseed_cfg,
local.golden_node_prepare,
local.prepare_template,
local.debian_netboot_base_url,
]))
}
resource "null_resource" "pimox_template_vm_create" {
count = var.pimox_template_builder_enabled ? 1 : 0
depends_on = [null_resource.provisioning_host]
triggers = {
pimox_host = var.pimox_host
pimox_user = var.pimox_user
ssh_key_path = var.pimox_ssh_key_path
vmid = tostring(var.pimox_template_vmid)
name = var.pimox_template_name
cores = tostring(var.pimox_template_cores)
memory = tostring(var.pimox_template_memory)
net0 = local.pimox_template_net0
scsi0 = var.pimox_template_scsi0
efidisk0 = var.pimox_template_efidisk0
replace_existing = tostring(var.pimox_template_replace_existing)
}
connection {
type = "ssh"
user = self.triggers.pimox_user
private_key = file(self.triggers.ssh_key_path)
host = self.triggers.pimox_host
}
provisioner "remote-exec" {
inline = [
<<EOT
set -eu
vmid="${self.triggers.vmid}"
replace_existing="${self.triggers.replace_existing}"
if ! command -v qm >/dev/null 2>&1; then
echo "qm is not installed on this Pimox host" >&2
exit 1
fi
if sudo qm status "$vmid" >/dev/null 2>&1; then
if sudo qm config "$vmid" | grep -q '^template: 1$'; then
exit 0
fi
if [ "$replace_existing" != "true" ]; then
echo "VM $vmid already exists and is not a template. Set pimox_template_replace_existing=true to rebuild it." >&2
exit 1
fi
sudo qm stop "$vmid" >/dev/null 2>&1 || true
elapsed=0
while [ "$elapsed" -lt 300 ]; do
if sudo qm status "$vmid" | grep -q 'status: stopped'; then
break
fi
sleep 5
elapsed=$((elapsed + 5))
done
sudo qm destroy "$vmid" --purge 1 >/dev/null 2>&1 || sudo qm destroy "$vmid"
fi
sudo qm create "$vmid" \
--name "${self.triggers.name}" \
--bios ovmf \
--boot "order=net0;scsi0" \
--cores "${self.triggers.cores}" \
--memory "${self.triggers.memory}" \
--net0 "${self.triggers.net0}" \
--numa 0 \
--ostype l26 \
--scsihw virtio-scsi-pci \
--sockets 1 \
--vga virtio
sudo qm set "$vmid" --efidisk0 "${self.triggers.efidisk0}"
sudo qm set "$vmid" --scsi0 "${self.triggers.scsi0}"
sudo qm start "$vmid"
EOT
]
}
}
resource "null_resource" "pimox_template_vm_seal" {
count = var.pimox_template_builder_enabled ? 1 : 0
depends_on = [null_resource.pimox_template_vm_create]
triggers = {
host = var.pimox_template_build_host
user = var.pimox_template_build_user
ssh_key_path = var.pimox_template_build_ssh_key_path
timeout = var.pimox_template_build_timeout
vmid = tostring(var.pimox_template_vmid)
}
connection {
type = "ssh"
user = self.triggers.user
private_key = file(self.triggers.ssh_key_path)
host = self.triggers.host
timeout = self.triggers.timeout
}
provisioner "remote-exec" {
inline = [
<<EOT
set -eu
if [ -z "${self.triggers.host}" ]; then
echo "pimox_template_build_host must point to the installed VM before template sealing can run" >&2
exit 1
fi
sudo /usr/local/sbin/homelab-prepare-template.sh
sudo nohup sh -c 'sleep 2; poweroff' >/dev/null 2>&1 &
EOT
]
}
}
resource "null_resource" "pimox_template_vm_finalize" {
count = var.pimox_template_builder_enabled ? 1 : 0
depends_on = [null_resource.pimox_template_vm_seal]
triggers = {
pimox_host = var.pimox_host
pimox_user = var.pimox_user
ssh_key_path = var.pimox_ssh_key_path
vmid = tostring(var.pimox_template_vmid)
}
connection {
type = "ssh"
user = self.triggers.pimox_user
private_key = file(self.triggers.ssh_key_path)
host = self.triggers.pimox_host
}
provisioner "remote-exec" {
inline = [
<<EOT
set -eu
vmid="${self.triggers.vmid}"
elapsed=0
while [ "$elapsed" -lt 600 ]; do
if sudo qm status "$vmid" | grep -q 'status: stopped'; then
break
fi
sleep 5
elapsed=$((elapsed + 5))
done
if ! sudo qm status "$vmid" | grep -q 'status: stopped'; then
echo "Timed out waiting for VM $vmid to stop before template conversion" >&2
exit 1
fi
sudo qm set "$vmid" --boot "order=scsi0;net0"
sudo qm template "$vmid"
EOT
]
}
}
resource "null_resource" "provisioning_host" {
triggers = {
host = var.provisioning_host
user = var.provisioning_user
ssh_key_path = var.provisioning_ssh_key_path
install_dir = var.provisioning_install_dir
tftp_root = local.tftp_root
http_root = local.http_root
netboot_base_url = local.debian_netboot_base_url
pxe_boot_file = var.pxe_boot_file
http_port = tostring(var.http_port)
config_hash = local.config_hash
provisioning_layer = "1"
}
connection {
type = "ssh"
user = self.triggers.user
private_key = file(self.triggers.ssh_key_path)
host = self.triggers.host
}
provisioner "remote-exec" {
inline = [
"rm -rf /tmp/homelab-provisioning",
"mkdir -p /tmp/homelab-provisioning",
]
}
provisioner "file" {
content = local.dnsmasq_conf
destination = "/tmp/homelab-provisioning/dnsmasq.conf"
}
provisioner "file" {
content = local.nginx_conf
destination = "/tmp/homelab-provisioning/nginx.conf"
}
provisioner "file" {
content = local.grub_cfg
destination = "/tmp/homelab-provisioning/grub.cfg"
}
provisioner "file" {
content = local.preseed_cfg
destination = "/tmp/homelab-provisioning/preseed.cfg"
}
provisioner "file" {
content = local.golden_node_prepare
destination = "/tmp/homelab-provisioning/golden-node-prepare.sh"
}
provisioner "file" {
content = local.prepare_template
destination = "/tmp/homelab-provisioning/prepare-template.sh"
}
provisioner "remote-exec" {
inline = [
<<EOT
set -eu
install_dir="${self.triggers.install_dir}"
tftp_root="${self.triggers.tftp_root}"
http_root="${self.triggers.http_root}"
netboot_base_url="${self.triggers.netboot_base_url}"
pxe_boot_file="${self.triggers.pxe_boot_file}"
tmp_dir="/tmp/homelab-provisioning"
install_missing_packages() {
missing_packages=""
for package in "$@"; do
if ! dpkg-query -W -f='$${Status}' "$package" 2>/dev/null | grep -q "install ok installed"; then
missing_packages="$missing_packages $package"
fi
done
if [ -n "$missing_packages" ]; then
sudo apt-get update
sudo apt-get install -y --no-install-recommends $missing_packages
fi
}
install_missing_packages ca-certificates curl dnsmasq nginx
sudo install -d -m 0755 \
"$install_dir" \
"$tftp_root" \
"$tftp_root/debian-installer/arm64" \
"$tftp_root/grub" \
"$http_root" \
"$http_root/preseed" \
"$http_root/scripts" \
"$http_root/debian-installer/arm64"
sudo cp "$tmp_dir/grub.cfg" "$tftp_root/grub/grub.cfg"
sudo cp "$tmp_dir/preseed.cfg" "$http_root/preseed/debian13-arm64-worker.cfg"
sudo cp "$tmp_dir/golden-node-prepare.sh" "$http_root/scripts/golden-node-prepare.sh"
sudo cp "$tmp_dir/prepare-template.sh" "$http_root/scripts/prepare-template.sh"
sudo chmod 0644 "$tftp_root/grub/grub.cfg" "$http_root/preseed/debian13-arm64-worker.cfg"
sudo chmod 0755 "$http_root/scripts/golden-node-prepare.sh" "$http_root/scripts/prepare-template.sh"
for asset in linux initrd.gz "$pxe_boot_file"; do
sudo curl -fsSL "$netboot_base_url/$asset" -o "$tftp_root/debian-installer/arm64/$asset.tmp"
sudo mv "$tftp_root/debian-installer/arm64/$asset.tmp" "$tftp_root/debian-installer/arm64/$asset"
done
sudo cp "$tftp_root/debian-installer/arm64/$pxe_boot_file" "$tftp_root/$pxe_boot_file"
sudo cp "$tftp_root/debian-installer/arm64/linux" "$http_root/debian-installer/arm64/linux"
sudo cp "$tftp_root/debian-installer/arm64/initrd.gz" "$http_root/debian-installer/arm64/initrd.gz"
sudo cp "$tmp_dir/dnsmasq.conf" /etc/dnsmasq.d/homelab-pxe.conf
sudo cp "$tmp_dir/nginx.conf" /etc/nginx/sites-available/homelab-provisioning.conf
sudo ln -sfn ../sites-available/homelab-provisioning.conf /etc/nginx/sites-enabled/homelab-provisioning.conf
sudo nginx -t
sudo systemctl enable --now nginx >/dev/null
sudo systemctl restart nginx
sudo dnsmasq --test --conf-file=/etc/dnsmasq.d/homelab-pxe.conf
sudo systemctl enable --now dnsmasq >/dev/null
sudo systemctl restart dnsmasq
EOT
]
}
}