Files
diskless-workstation/diskless.pkr.hcl
T

205 lines
8.0 KiB
HCL

# =============================================================================
# Diskless fleet image build
#
# Builds a RHEL9-family root filesystem + fleet-generic initramfs for
# PXE/HTTP-booted diskless hosts authenticating with 802.1X EAP-TLS
# (TPM-held key, cert in TPM NV) and MACsec link encryption.
# Root filesystem: erofs + dm-verity over a shared read-only NVMe/TCP
# namespace, with a tmpfs overlay for runtime state.
#
# Outputs:
# ${var.artifact_dir}/${var.image_version}/
# vmlinuz, initramfs.img - intermediate (consumed by ukify)
# fleet-<version>.efi - signed UKI (kernel+initramfs+cmdline+roothash)
# rootfs.erofs - erofs + appended verity tree -> nvmet RO namespace
# verity.json, cmdline.txt, SHA256SUMS
# OCI image ${var.oci_image}:${var.image_version} (FROM scratch)
# /artifacts/{fleet-<version>.efi, rootfs.erofs, verity.json, SHA256SUMS}
# = the atomic fleet release unit; deploy as initContainer / image volume
# feeding the SPDK + HTTPS serving containers, pinned by digest.
#
# Usage:
# packer init .
# packer build -var iso_url=... -var iso_checksum=... .
# =============================================================================
packer {
required_version = ">= 1.10.0"
required_plugins {
qemu = {
source = "github.com/hashicorp/qemu"
version = ">= 1.1.0"
}
}
}
# ------------------------------------------------------------------ variables
variable "iso_url" {
type = string
description = "RHEL 9 / Rocky 9 / Alma 9 install ISO URL or local path. The minimal ISO is sufficient: ks.cfg installs only core+sshd from it and the provisioner pulls the diskless tooling from the online repos."
default = "https://download.rockylinux.org/pub/rocky/9/isos/x86_64/Rocky-9-latest-x86_64-minimal.iso"
}
variable "iso_checksum" {
type = string
description = "ISO checksum. The CHECKSUM manifest covers every variant (minimal/dvd/boot); Packer matches the entry by iso_url's filename, so this stays the same when switching variants."
default = "file:https://download.rockylinux.org/pub/rocky/9/isos/x86_64/CHECKSUM"
}
variable "image_version" {
type = string
description = "Version stamp for the published artifact directory"
default = "0.1.0"
}
variable "artifact_dir" {
type = string
description = "Where extracted boot artifacts and rootfs tar are written"
default = "artifacts"
}
variable "nic_drivers" {
type = string
description = "Kernel modules for the fleet's real NICs (build VM only has virtio!)"
default = "igb igc ixgbe i40e ice mlx5_core"
}
variable "build_password" {
type = string
description = "Throwaway root password used only during the Packer build (must match http/ks.cfg)"
default = "packer-build-only"
sensitive = true
}
variable "oci_image" {
type = string
description = "OCI image ref for the release-artifact image (FROM scratch, holds UKI + rootfs.erofs)"
default = "registry.example.org/fleet-boot"
}
variable "oci_push" {
type = bool
description = "Push the release-artifact image to the registry after building"
default = false
}
variable "disable_dot1x" {
type = bool
description = "TEST ONLY: bake rd.dot1x=0 into the UKI cmdline so the host boots with 802.1X + MACsec disabled (open-port NVMe-oF boot test). Leave false for any real/secure build."
default = false
}
variable "sb_key" {
type = string
description = "Secure Boot signing key (PEM) for the UKI. Empty = unsigned lab build. Production: CI/HSM-held; never enters the build VM."
default = ""
sensitive = true
}
variable "sb_cert" {
type = string
description = "Secure Boot signing certificate (PEM) matching sb_key"
default = ""
}
# --------------------------------------------------------------------- source
source "qemu" "diskless" {
iso_url = var.iso_url
iso_checksum = var.iso_checksum
output_directory = "output-qemu"
vm_name = "diskless-build.qcow2"
disk_size = "12G"
format = "qcow2"
accelerator = "kvm"
memory = 4096
cpus = 2
headless = true
http_directory = "http"
# BIOS/isolinux boot. For an OVMF/UEFI build VM, replace with the grub
# variant: ["c<wait>", "linuxefi /images/pxeboot/vmlinuz inst.text ",
# "inst.ks=http://{{ .HTTPIP }}:{{ .HTTPPort }}/ks.cfg<enter>",
# "initrdefi /images/pxeboot/initrd.img<enter>", "boot<enter>"]
boot_wait = "5s"
boot_command = [
"<up><wait><tab>",
" inst.text inst.ks=http://{{ .HTTPIP }}:{{ .HTTPPort }}/ks.cfg",
"<enter><wait>",
]
ssh_username = "root"
ssh_password = var.build_password
ssh_timeout = "60m"
shutdown_command = "shutdown -P now"
}
# ---------------------------------------------------------------------- build
build {
sources = ["source.qemu.diskless"]
# --- 1. Stage the dracut module, configs, and CA placeholder -------------
provisioner "file" {
source = "files/dracut/95dot1x-macsec"
destination = "/usr/lib/dracut/modules.d/"
}
provisioner "file" {
source = "files/etc/"
destination = "/tmp/etc-stage/"
}
# --- 2. Packages, configs, diskless adaptations --------------------------
provisioner "shell" {
inline = [
"set -euxo pipefail",
"dnf -y install wpa_supplicant tpm2-tools tpm2-tss nvme-cli cryptsetup dracut-network openssl",
# Stage etc files into place
"install -D -m0644 /tmp/etc-stage/dot1x/radius-ca.pem /etc/dot1x/radius-ca.pem",
"install -D -m0644 /tmp/etc-stage/dot1x/wpa_supplicant-dot1x.conf /etc/dot1x/wpa_supplicant-dot1x.conf",
"install -D -m0644 /tmp/etc-stage/dracut.conf.d/90-diskless.conf /etc/dracut.conf.d/90-diskless.conf",
"install -D -m0644 /tmp/etc-stage/NetworkManager/99-dot1x-unmanaged.conf /etc/NetworkManager/conf.d/99-dot1x-unmanaged.conf",
"chmod 0755 /usr/lib/dracut/modules.d/95dot1x-macsec/*.sh",
# Inject the fleet NIC drivers into the dracut config
"sed -i 's/@NIC_DRIVERS@/${var.nic_drivers}/' /etc/dracut.conf.d/90-diskless.conf",
# ---- Diskless adaptations ------------------------------------------
# Per-host machine-id is generated at boot into the overlay
"truncate -s 0 /etc/machine-id",
# Read-only NFS root; runtime state on tmpfs/overlay (rootovl via the
# dracut 'overlayfs' module is pulled in by 90-diskless.conf)
"printf '%s\\n' '# diskless: root is provided by netroot; no local mounts' > /etc/fstab",
# No local bootloader regeneration wanted on kernel updates inside CI
"command -v grub2-mkconfig >/dev/null && echo 'GRUB updates not used on diskless hosts' || true",
# ---- Build the fleet-generic initramfs ------------------------------
# %%{ escapes the rpm query tags so Packer's HCL template engine treats
# them as literals (a bare %{...} is parsed as a template directive).
"KVER=$(rpm -q --qf '%%{VERSION}-%%{RELEASE}.%%{ARCH}\\n' kernel | sort -V | tail -1)",
"dracut -f --no-hostonly --kver \"$KVER\" /boot/initramfs-generic.img",
"lsinitrd /boot/initramfs-generic.img | grep -E 'macsec|wpa_supplicant|tpm2_nvread|nvme.tcp|erofs|veritysetup' ",
# ---- Build hygiene ---------------------------------------------------
"dnf clean all",
"rm -rf /tmp/etc-stage /var/log/anaconda /root/anaconda-ks.cfg",
"truncate -s 0 /var/log/messages /var/log/secure 2>/dev/null || true",
]
}
# --- 3. Extract kernel, initramfs, rootfs from the built disk ------------
# --- 4. Assemble + sign the UKI; build the OCI release-artifact image ----
post-processor "shell-local" {
inline = [
"bash scripts/extract-artifacts.sh output-qemu/diskless-build.qcow2 ${var.artifact_dir}/${var.image_version} ${var.disable_dot1x}",
"bash scripts/build-oci.sh ${var.artifact_dir}/${var.image_version} ${var.oci_image} ${var.image_version} '${var.sb_key}' '${var.sb_cert}' ${var.oci_push}",
]
}
}