Address PR review: PXE gate + callback, git-sync templates, distro catalog, k8s targets, http+https
ci/woodpecker/pr/build Pipeline was successful
ci/woodpecker/pr/pre-commit Pipeline was successful
ci/woodpecker/pr/test Pipeline was successful

Implements the six review comments on PR #1:

- Per-host PXE-enable gate: read NetBox pxe_enabled custom field; a known host
  with it false gets the safe local-boot script (Cobbler netboot_enabled). Add a
  token-guarded POST /provisioned/{ident} callback that clears pxe_enabled in
  NetBox, plus a %post snippet in the default kickstarts that calls it.
- Templates from a git repo: bootapi clones a templates repo and re-pulls every
  BOOTAPI_TEMPLATE_GIT_INTERVAL (default 3m), atomically swapping the template
  set (last-good kept on parse failure; embedded defaults are the startup
  fallback). Metrics for syncs/failures/generation.
- Distro catalog (catalog/*.yaml): NetBox host -> boot images/kickstart, so
  adding an OS is a YAML + template change. Ships almalinux + fedora entries
  (artifactapi remotes); debian/talos path documented.
- Boot images from the artifactapi almalinux/fedora remotes via the catalog.
- Bind resolvers, puppet server/CA and PUPPETCA_URL env file now target the k8s
  services (198.18.200.7; puppet(ca).k8s.syd1.au.unkin.net).
- Boot path served over plain HTTP (installers lack CA trust) with an optional
  parallel HTTPS listener; docs say do not 301 the boot endpoints.

New packages: internal/catalog, internal/gitsync. NetBox client gains a
pxe_enabled write (token needs that scope - noted in docs). `bootapi validate`
subcommand validates a template/catalog set for the templates-repo CI.

go build/vet clean, go test -race green, golangci-lint v2 clean, pre-commit clean.

Claude-Session: https://claude.ai/code/session_015ur3i7D2azsMAWTSVABApv
This commit is contained in:
2026-07-28 22:34:44 +10:00
parent 274c480b09
commit 8f356346eb
32 changed files with 2119 additions and 357 deletions
+18
View File
@@ -0,0 +1,18 @@
# Distro catalog entry: AlmaLinux 9.
# Boot images are proxied through the artifactapi "almalinux" remote. Host ->
# distro selection is NetBox-driven (platform slug almalinux9, or the almalinux
# family, or a provision_template override naming "almalinux9").
name: almalinux9
match:
platforms: [almalinux9]
family: almalinux
kickstart: almalinux9
version_default: "9"
kernel_url: "{{.ArtifactBase}}/almalinux/{{.Version}}/BaseOS/{{.Arch}}/os/images/pxeboot/vmlinuz"
initrd_url: "{{.ArtifactBase}}/almalinux/{{.Version}}/BaseOS/{{.Arch}}/os/images/pxeboot/initrd.img"
kernel_args:
- inst.text
- net.ifnames=0
vars:
# Version-level mirror base; the kickstart appends BaseOS/AppStream under it.
mirror: "{{.ArtifactBase}}/almalinux/{{.Version}}"
+16
View File
@@ -0,0 +1,16 @@
# Distro catalog entry: Fedora (family-level, matches any fedoraNN platform).
# Boot images are proxied through the artifactapi "fedora" remote, whose tree
# lives under releases/<ver>/Everything/<arch>/os/.
name: fedora
match:
family: fedora
kickstart: fedora
version_default: "41"
kernel_url: "{{.ArtifactBase}}/fedora/releases/{{.Version}}/Everything/{{.Arch}}/os/images/pxeboot/vmlinuz"
initrd_url: "{{.ArtifactBase}}/fedora/releases/{{.Version}}/Everything/{{.Arch}}/os/images/pxeboot/initrd.img"
kernel_args:
- inst.text
- net.ifnames=0
vars:
# Install-tree root; the kickstart appends <arch>/os/ under it.
mirror: "{{.ArtifactBase}}/fedora/releases/{{.Version}}/Everything"
+3 -2
View File
@@ -5,7 +5,8 @@ package templates
import "embed"
// FS holds the default template tree: kickstart/*.ks.tmpl and ipxe/*.ipxe.tmpl.
// FS holds the default template tree: kickstart/*.ks.tmpl, ipxe/*.ipxe.tmpl and
// catalog/*.yaml (the distro catalog).
//
//go:embed kickstart ipxe
//go:embed kickstart ipxe catalog
var FS embed.FS
+10 -10
View File
@@ -1,20 +1,20 @@
{{- /*
iPXE boot script for a known host. Chains the OS installer kernel+initrd and
points inst.ks= back at bootapi's /ks/<hostname> endpoint, mirroring how Cobbler
generated a per-MAC gPXE script that carried inst.ks=.
iPXE boot script for a known, PXE-enabled host. Chains the OS installer
kernel+initrd (from the distro catalog) and points inst.ks= back at bootapi's
/ks/<host> over plain HTTP, so an installer with no internal-CA trust can fetch
it. Mirrors how Cobbler generated a per-MAC gPXE script carrying inst.ks=.
Requires BOOTAPI_BOOT_BASE_URL (KernelURL/InitrdURL) and BOOTAPI_BASE_URL
(KickstartURL) to be configured.
KernelURL/InitrdURL/RepoURL come from the selected catalog entry (artifactapi
remote); KernelArgs are the catalog's extra args. KickstartURL uses
BOOTAPI_BASE_URL (http://).
*/ -}}
#!ipxe
echo bootapi: provisioning {{ .FQDN }} ({{ .Platform }})
{{ if and .KernelURL .InitrdURL -}}
kernel {{ .KernelURL }} initrd=initrd.img inst.repo={{ .BootBaseURL }} inst.ks={{ .KickstartURL }} inst.text ip=dhcp net.ifnames=0
kernel {{ .KernelURL }} initrd=initrd.img{{ if .RepoURL }} inst.repo={{ .RepoURL }}{{ end }} inst.ks={{ .KickstartURL }} ip=dhcp{{ range .KernelArgs }} {{ . }}{{ end }}
initrd {{ .InitrdURL }}
boot
{{- else -}}
echo bootapi: BOOTAPI_BOOT_BASE_URL not configured; cannot build a boot line
echo Falling back to local disk in 5s
sleep 5
exit
echo bootapi: no boot images resolved for {{ .Platform }} (no catalog entry / BOOTAPI_BOOT_BASE_URL); booting local disk
sanboot --no-describe --drive 0x80 || exit
{{- end }}
+35 -18
View File
@@ -1,15 +1,17 @@
{{- /*
AlmaLinux 9 kickstart, ported from the Cobbler default.ks contract.
Rendered by bootapi from NetBox data + render-time secrets. The %post hands off
to the existing Puppet firstrun bootstrap: it installs the agent, points it at
the Consul-discovered puppet servers, and triggers the first run. Autosign
(*.main.unkin.net + the PXE subnets) and the `profiles::firstrun` class do the
rest, exactly as they did under Cobbler.
Rendered by bootapi from NetBox data + render-time secrets + the distro catalog.
Install source comes from the artifactapi almalinux remote (via the catalog
mirror var). The %post installs the Puppet agent and points it at the k8s
puppetserver (puppet.k8s.syd1.au.unkin.net / puppetca.k8s...), writes the
puppet-initial PUPPETCA_URL env file, then posts back to bootapi so pxe_enabled
flips off (Cobbler's netboot_enabled flow).
Data model: see docs/data-model.md. `.RootPasswordHash` comes from Vault at
render time, never from NetBox.
Data model: see docs/data-model.md. `.RootPasswordHash` and `.ProvisionToken`
come from Vault/env at render time, never from NetBox.
*/ -}}
{{- $mirror := .DistroVars.mirror -}}
#version=RHEL9
# Rendered by bootapi for {{ .FQDN }} (platform {{ .Platform }}, role {{ default "none" .Role }})
text
@@ -17,9 +19,9 @@ eula --agreed
firstboot --disable
reboot
# --- install source (served by bootapi's configured mirror) ---
url --url={{ .BootBaseURL }}/BaseOS/{{ .Arch }}/os/
repo --name=AppStream --baseurl={{ .BootBaseURL }}/AppStream/{{ .Arch }}/os/
# --- install source (artifactapi almalinux remote, from the distro catalog) ---
url --url={{ $mirror }}/BaseOS/{{ .Arch }}/os/
repo --name=AppStream --baseurl={{ $mirror }}/AppStream/{{ .Arch }}/os/
# --- localization ---
keyboard --xlayouts='us'
@@ -61,7 +63,7 @@ git
-iwl*-firmware
%end
# --- bootstrap: hand off to Puppet firstrun ---
# --- bootstrap: puppet (k8s) + end-of-install callback ---
%post --log=/root/bootapi-post.log
set -x
@@ -82,15 +84,30 @@ rpm -q puppet-agent >/dev/null 2>&1 || \
dnf install -y https://yum.puppet.com/puppet8-release-el-9.noarch.rpm
dnf install -y puppet-agent
# Point the agent at the Consul-discovered servers (matches the pre-bootapi
# Cobbler kickstart + hieradata/roles/infra/puppet).
# Point the agent at the k8s puppetserver / CA.
PUPPET_BIN=/opt/puppetlabs/bin/puppet
"$PUPPET_BIN" config set --section main certname "{{ .FQDN }}"
"$PUPPET_BIN" config set --section main server "{{ .PuppetServer }}"
"$PUPPET_BIN" config set --section main ca_server "{{ .PuppetCAServer }}"
"$PUPPET_BIN" config set --section main certname "{{ .FQDN }}"
"$PUPPET_BIN" config set --section main server "{{ .PuppetServer }}"
"$PUPPET_BIN" config set --section main ca_server "{{ .PuppetCAServer }}"
"$PUPPET_BIN" config set --section main report_server "{{ .PuppetServer }}"
"$PUPPET_BIN" config set --section main environment production
"$PUPPET_BIN" config set --section main environment production
# Enable the agent; the first boot triggers firstrun (autosign handles the CSR).
# puppet-initial bootstrap unit reads PUPPETCA_URL from this EnvironmentFile.
install -d -m0755 /etc/sysconfig
cat > /etc/sysconfig/puppet-initial <<'EOF'
PUPPETCA_URL={{ .PuppetCAURL }}
EOF
# Enable the agent; first boot triggers firstrun (autosign handles the CSR).
systemctl enable puppet
{{ if and .ProvisionToken .CallbackURL -}}
# Tell bootapi the install is done so it clears pxe_enabled in NetBox and the
# next PXE boots local disk. Runs over plain HTTP (no internal CA trust yet);
# the token authenticates the call. Non-fatal if it fails (the local-disk
# fallback still protects a re-provisioned host on the following boot).
curl -fsS -m 15 -X POST \
-H "Authorization: Bearer {{ .ProvisionToken }}" \
"{{ .CallbackURL }}" || echo "bootapi: provisioned callback failed (non-fatal)"
{{- end }}
%end
+20 -6
View File
@@ -1,17 +1,19 @@
{{- /*
Fedora kickstart (family-level template: matches any "fedoraNN" platform slug
via the OS-family selection fallback). Kept close to the AlmaLinux template so
the two stay comparable; the differences are the install tree layout and that
Fedora ships a recent-enough dnf/agent story out of the box.
via the catalog family match). Kept close to the AlmaLinux template so the two
stay comparable; the differences are the install-tree layout (releases/.../
Everything) and the puppet release RPM. Install source + boot images come from
the artifactapi fedora remote via the distro catalog.
*/ -}}
{{- $mirror := .DistroVars.mirror -}}
#version=F{{ default "" .OSVersion }}
# Rendered by bootapi for {{ .FQDN }} (platform {{ .Platform }})
text
firstboot --disable
reboot
# --- install source ---
url --url={{ .BootBaseURL }}/releases/{{ default "rawhide" .OSVersion }}/Everything/{{ .Arch }}/os/
# --- install source (artifactapi fedora remote, from the distro catalog) ---
url --url={{ $mirror }}/{{ .Arch }}/os/
keyboard --xlayouts='us'
lang en_AU.UTF-8
@@ -56,11 +58,23 @@ cat > /root/.ssh/authorized_keys <<'EOF'
{{ end }}EOF
chmod 0600 /root/.ssh/authorized_keys
{{- end }}
dnf install -y https://yum.puppet.com/puppet8-release-fedora-{{ default "40" .OSVersion }}.noarch.rpm || true
dnf install -y "https://yum.puppet.com/puppet8-release-fedora-{{ default "40" .OSVersion }}.noarch.rpm" || true
dnf install -y puppet-agent
PUPPET_BIN=/opt/puppetlabs/bin/puppet
"$PUPPET_BIN" config set --section main certname "{{ .FQDN }}"
"$PUPPET_BIN" config set --section main server "{{ .PuppetServer }}"
"$PUPPET_BIN" config set --section main ca_server "{{ .PuppetCAServer }}"
install -d -m0755 /etc/sysconfig
cat > /etc/sysconfig/puppet-initial <<'EOF'
PUPPETCA_URL={{ .PuppetCAURL }}
EOF
systemctl enable puppet
{{ if and .ProvisionToken .CallbackURL -}}
curl -fsS -m 15 -X POST \
-H "Authorization: Bearer {{ .ProvisionToken }}" \
"{{ .CallbackURL }}" || echo "bootapi: provisioned callback failed (non-fatal)"
{{- end }}
%end