Initial bootapi: NetBox-driven PXE/kickstart boot service
ci/woodpecker/pr/build Pipeline was successful
ci/woodpecker/pr/pre-commit Pipeline was successful
ci/woodpecker/pr/test Pipeline was successful

bootapi replaces Cobbler's PXE/kickstart side. It resolves a PXE-booting host
from NetBox (by MAC or hostname), renders an iPXE boot script and a kickstart
from Go text/templates, and serves them over HTTP. The ENC half already moved to
encapi; this covers the provisioning/boot half.

What's here:
- cmd/bootapi + internal/{config,model,netbox,render,server}; embedded default
  templates under templates/ (AlmaLinux 9 + Fedora kickstarts, iPXE boot +
  unknown-MAC fallbacks) ported from Cobbler's boot/bootstrap contract.
- NetBox client (v4.x API) behind a Resolver interface with a short-TTL cache;
  tested against httptest fixtures using real NetBox JSON shapes.
- chi HTTP server: /ipxe/{mac}, /boot/ipxe?mac=, /ks/{ident}, healthz/readyz,
  Prometheus /metrics. Unknown MAC -> safe fallback iPXE (200), unknown KS -> 404.
- Secrets (root pw hash, ssh keys) injected at render time from env/Vault, never
  NetBox. Config is env-based per estate convention.
- Makefile (build/test/lint/docker + patch/minor/major), Dockerfile (distroless),
  .woodpecker (pre-commit, golangci-lint v2 + go test -race, docker build on PR;
  image push + Gitea binary release on v* tag), docs/ and example config.

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 17:20:06 +10:00
parent 98e69d2fcb
commit 274c480b09
37 changed files with 3290 additions and 1 deletions
+68
View File
@@ -0,0 +1,68 @@
# bootapi HTTP endpoints
bootapi speaks plain HTTP. It is fronted by the same Vault-issued TLS the Cobbler
server used; the booting firmware reaches it at the DHCP `next-server` (see
[deployment.md](deployment.md)).
## The PXE flow
```
DHCP ── next-server + filename (ipxe.efi / undionly.kpxe) ──▶ firmware loads iPXE
iPXE ── GET /ipxe/<mac> ───────────────────────────────────▶ bootapi renders a boot script
boot ── kernel + initrd + inst.ks=<BASE_URL>/ks/<host> ────▶ Anaconda fetches the kickstart
KS ── GET /ks/<host> ────────────────────────────────────▶ bootapi renders the kickstart
```
This mirrors Cobbler, which chained iPXE to `/cblr/svc/op/gpxe/mac/<mac>` and
served a per-system script carrying `inst.ks=`.
## 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}` (some firmware finds this shape easier to template). |
| GET | `/ks/{ident}` | Rendered kickstart. `{ident}` is a MAC (auto-detected) or a hostname; trailing `.ks`/`.cfg` is stripped. |
| GET | `/healthz` | Liveness: always `200 ok`. |
| GET | `/readyz` | Readiness: `200` once templates parsed. Does **not** probe NetBox (a NetBox outage still lets iPXE serve the safe fallback). |
| 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}`), which bootapi resolves via NetBox
`GET /api/dcim/interfaces/?mac_address=<mac>` → device → primary IP, platform,
role, interfaces. `/ks/{ident}` additionally accepts a **hostname** (NetBox
device name), for hand-testing and for installers that template the hostname
into the kickstart URL.
## Error behavior (important, and deliberate)
The two 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 or
the boot chain simply errors. An unknown MAC — or *any* NetBox error — returns
HTTP 200 with the **fallback script** selected by `BOOTAPI_UNKNOWN_MAC_FALLBACK`:
- `local` (default): `sanboot` the local disk. Safe: a machine that PXE-booted
by accident (or a NetBox blip) just boots its installed OS; a genuinely new
machine loops back to PXE next time, by which point NetBox should know it. We
deliberately do **not** start an installer for a machine we can't identify —
that could wipe a production box.
- `shell`: drop to an interactive iPXE shell so an operator racking a new box
can 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 (and 502 on a NetBox error).
By the time Anaconda fetches the kickstart it has already committed to
installing; a clear failure is safer than serving an empty or wrong kickstart.
## 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`.
- standard Go/process collectors.