Files
unkinben 8f356346eb
ci/woodpecker/pr/build Pipeline was successful
ci/woodpecker/pr/pre-commit Pipeline was successful
ci/woodpecker/pr/test Pipeline was successful
Address PR review: PXE gate + callback, git-sync templates, distro catalog, k8s targets, http+https
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
2026-07-28 22:34:44 +10:00

5.4 KiB

bootapi HTTP endpoints

HTTP and HTTPS — the boot path is plain HTTP by design

bootapi always serves the boot path (/ipxe, /boot/ipxe, /ks) over plain HTTP on BOOTAPI_LISTEN_ADDR. A PXE installer environment has no internal-CA trust, so an HTTPS-only boot URL (with our private CA cert) would fail the TLS handshake. iPXE and the kickstart therefore use http:// URLs (from BOOTAPI_BASE_URL).

Optionally bootapi also serves HTTPS in parallel (BOOTAPI_TLS_LISTEN_ADDR + cert/key), for clients that do trust the CA. The Kubernetes exposure must not blanket-301 HTTP→HTTPS for the boot endpoints — see deployment.md.

The PXE flow

DHCP  ── next-server + filename (ipxe.efi / undionly.kpxe) ──▶  firmware loads iPXE
iPXE  ── GET  http://<base>/ipxe/<mac> ─────────────────────▶  bootapi renders a boot script
boot  ── kernel + initrd + inst.ks=http://<base>/ks/<host> ─▶  Anaconda fetches the kickstart
KS    ── GET  http://<base>/ks/<host> ──────────────────────▶  bootapi renders the kickstart
post  ── POST http://<base>/provisioned/<host>  (token) ────▶  bootapi clears pxe_enabled in NetBox

This mirrors Cobbler, which chained iPXE to /cblr/svc/op/gpxe/mac/<mac>, served a per-system script carrying inst.ks=, and cleared netboot_enabled at the end of the install.

Endpoints

Method Path Purpose
GET /ipxe/{mac} iPXE boot script for the host owning {mac}. {mac} may use :/-/. separators or be bare hex; a trailing .ipxe is stripped.
GET /boot/ipxe?mac=... Query-string alias of /ipxe/{mac}.
GET /ks/{ident} Rendered kickstart. {ident} is a MAC (auto-detected) or a hostname; trailing .ks/.cfg is stripped.
POST /provisioned/{ident} End-of-kickstart callback; clears pxe_enabled in NetBox. Token-guarded (Authorization: Bearer <BOOTAPI_PROVISION_TOKEN>).
GET /healthz Liveness: always 200 ok.
GET /readyz Readiness: 200 once templates parsed. Does not probe NetBox.
GET /metrics Prometheus metrics (see below).

Host identification

A booting host is identified by the MAC of the NIC it PXE-booted from (/ipxe/{mac}), resolved via NetBox GET /api/dcim/interfaces/?mac_address=<mac> → device → primary IP, platform, role, interfaces. /ks/{ident} and /provisioned/{ident} also accept a hostname (NetBox device name).

Per-host PXE-enable gate (pxe_enabled)

/ipxe/{mac} checks the device's pxe_enabled NetBox custom field (Cobbler's netboot_enabled):

  • unset or true → normal installer boot script.
  • false → the safe local-boot fallback, even for a known host, so a machine that has already been provisioned does not re-install on its next PXE.

The /provisioned/{ident} callback (called from the kickstart %post) sets the field to false when the install finishes; so a host installs once, then gates itself off. Flip it back to true in NetBox to re-image.

Error behavior (deliberate)

The boot endpoints fail differently on an unknown host, because the cost of a wrong answer differs:

  • /ipxe/{mac} never returns 404. iPXE needs a syntactically valid script. An unknown MAC — or any NetBox error, or a gated host — returns HTTP 200 with the fallback script selected by BOOTAPI_UNKNOWN_MAC_FALLBACK:
    • local (default): sanboot the local disk. Safe: an accidental PXE (or a NetBox blip) boots the installed OS; a genuinely new machine loops back to PXE next time. We never start an installer for a machine we can't identify.
    • shell: interactive iPXE shell for an operator to read ${net0/mac} and register it. Opt-in; unsafe as a default because it halts the boot.
  • /ks/{ident} returns 404 for an unknown host (502 on a NetBox error). By the time Anaconda fetches the kickstart it has committed to installing; a clear failure beats an empty/wrong kickstart.

The provisioned callback

POST /provisioned/{ident} requires the shared token in an Authorization: Bearer (or bare token) header. Responses: 204 on success, 401 on a bad/missing token, 404 for an unknown host, 503 when no BOOTAPI_PROVISION_TOKEN is configured (fail closed), 502 on a NetBox write failure. The default kickstart templates call it from %post over plain HTTP (the token authenticates the call; no CA trust needed at install time).

Metrics

All on /metrics, prefix bootapi_:

  • bootapi_http_requests_total{endpoint,status} — endpoint = ipxe|ks|healthz|readyz, status = 2xx|3xx|4xx|5xx.
  • bootapi_render_total{kind,result} — kind = kickstart|ipxe, result = ok|error.
  • bootapi_netbox_lookups_total{field,result} — field = mac|name, result = ok|notfound|error.
  • bootapi_netbox_lookup_duration_seconds{field} — histogram.
  • bootapi_netbox_cache_hits_total / bootapi_netbox_cache_misses_total.
  • bootapi_provisioned_total{result} — result = ok|unauthorized|notfound|error|disabled.
  • bootapi_ipxe_gated_total — known hosts served local-boot because pxe_enabled=false.
  • bootapi_template_sync_total / bootapi_template_sync_failures_total / bootapi_template_generation — template git-sync (see template-authoring.md).
  • standard Go/process collectors.