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
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 byBOOTAPI_UNKNOWN_MAC_FALLBACK:local(default):sanbootthe 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-ndjsonorapplication/json), capped at 1 MiB. - Auth: the same
BOOTAPI_PROVISION_TOKENas/provisioned, same fail-closed behavior —503when no token (or noBOOTAPI_VLINSERT_URL) is configured,401on 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.serialandphaseare constant for a run and low-cardinality, so they key the stream; MAC is per-NIC and goes inextra_fieldsso it stays searchable without multiplying streams. extra_fieldscarries 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:
202once the batch is accepted,400on an empty or oversized body. A vlinsert failure is logged and counted but still returns202— 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 becausepxe_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.