230 lines
9.4 KiB
HCL
230 lines
9.4 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. Defaults to the fast internal mirror (version-pinned to match iso_checksum)."
|
|
default = "http://iso-server.bitasylum.org/isos/Rocky-9.8-x86_64-minimal.iso"
|
|
}
|
|
|
|
variable "iso_checksum" {
|
|
type = string
|
|
# Pinned sha256 of Rocky-9.8-x86_64-minimal.iso on the internal mirror.
|
|
# When moving to a new point release, bump iso_url AND this hash together:
|
|
# curl -fsSL http://iso-server.bitasylum.org/isos/Rocky-<ver>-x86_64-minimal.iso | sha256sum
|
|
# (or `sha256sum` the file on iso-server). Set "none" to skip verification.
|
|
description = "ISO checksum as sha256:<hex> (or file:<CHECKSUM-url>, or none)."
|
|
default = "sha256:d338032cd1cdd41c67139f2f71b4c832c8e4a21943106519db9c7137df7a63d4"
|
|
}
|
|
|
|
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
|
|
vnc_bind_address = "0.0.0.0"
|
|
qemuargs = [["-cpu", "host"],
|
|
[ "-serial", "stdio" ]]
|
|
|
|
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 -------------
|
|
# Pre-create the staging dir. Packer's directory-contents upload (a file
|
|
# provisioner whose source ends in "/") requires the destination to already
|
|
# exist; otherwise the uploaded tree lands outside /tmp/etc-stage and the
|
|
# install steps below can't find it.
|
|
provisioner "shell" {
|
|
inline = ["mkdir -p /tmp/etc-stage"]
|
|
}
|
|
|
|
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",
|
|
|
|
# dhcp-client provides dhclient, which the dracut 'network-legacy' module
|
|
# (and therefore the 95dot1x-macsec hook's ifup step) requires. RHEL9
|
|
# deprecated dhclient in favour of NetworkManager, so the minimal ISO
|
|
# omits it; without it network-legacy fails dracut's check() and the
|
|
# diskless module 'cannot be installed'. (Strategic path: port the hook to
|
|
# the 'network-manager' dracut module — see CLAUDE.md bench-verify notes.)
|
|
# zstd: dracut compresses the initramfs with compress="zstd"
|
|
# (90-diskless.conf); the minimal ISO lacks the zstd binary, so without it
|
|
# dracut warns and silently falls back to gzip (a larger initramfs).
|
|
"dnf -y install wpa_supplicant tpm2-tools tpm2-tss nvme-cli cryptsetup dracut-network dhcp-client zstd 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 erofs/verity root; runtime state on a tmpfs overlay provided
|
|
# by systemd.volatile=overlay (set in the cmdline by extract-artifacts.sh).
|
|
# RHEL/Rocky dracut has no 'overlayfs'/'rootovl' module.
|
|
"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}",
|
|
]
|
|
}
|
|
}
|