Files
bootapi/docs/endpoints.md
T
unkin-agent 3c77895788
ci/woodpecker/pr/pre-commit Pipeline was successful
ci/woodpecker/pr/build Pipeline was successful
ci/woodpecker/pr/test Pipeline was successful
add POST /logs installer log relay to VictoriaLogs
A host being PXE-discovered or installed is not in Kubernetes, so vlagent
cannot collect its logs and a failed install leaves no record; the installer
environment has no internal-CA trust or credentials for the HTTPS log ingest,
and bootapi is already the plain-HTTP broker it can reach.

- add POST /logs, token-guarded like POST /provisioned, relaying ndjson to
  vlinsert's jsonline endpoint keyed on serial+phase
- stamp observed source IP and resolved NetBox device name into extra_fields
- return 202 on a sink failure so logs never block an install
- add BOOTAPI_VLINSERT_URL/_TIMEOUT and bootapi_log_relay metrics
2026-10-03 19:44:27 +10:00

7.2 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>).
POST /logs Relays a host's newline-delimited JSON install logs to VictoriaLogs. Token-guarded (same 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).

The installer log relay

POST /logs exists because a host being PXE-discovered or installed is not in Kubernetes, so the cluster's vlagent cannot collect its logs — if an install fails, the only record dies with the machine. That environment also has no internal-CA trust and no credentials for the HTTPS-only log ingest, and bootapi is already the plain-HTTP broker it can reach, so bootapi forwards for it.

  • Body: newline-delimited JSON, one log record per line (application/x-ndjson or application/json), capped at 1 MiB.
  • Auth: the same BOOTAPI_PROVISION_TOKEN as /provisioned, same fail-closed behavior — 503 when no token (or no BOOTAPI_VLINSERT_URL) is configured, 401 on a bad/missing token.
  • Forwarded as one short-timeout POST to {BOOTAPI_VLINSERT_URL}/insert/jsonline?_stream_fields=serial,phase&_msg_field=msg&_time_field=time. serial and phase are constant for a run and low-cardinality, so they key the stream; MAC is per-NIC and goes in extra_fields so it stays searchable without multiplying streams.
  • extra_fields carries only what bootapi observes rather than what the client claims: the request's source IP (src_ip), plus the resolved NetBox device name (device) when ?mac= resolves. Resolution is optional — a miss just omits the field.
  • Responses: 202 once the batch is accepted, 400 on an empty or oversized body. A vlinsert failure is logged and counted but still returns 202 — no retries, no buffering: a host must never block its install because the log sink is down.

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_log_relay_total{result} — result = ok|error|unauthorized|disabled.
  • bootapi_log_relay_lines_total — installer log lines successfully relayed.
  • 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.