Initial bootapi: NetBox-driven PXE/kickstart boot service #1

Merged
benvin merged 2 commits from benvin/bootapi-initial into main 2026-07-28 23:35:04 +10:00
Owner

Cobbler's ENC role moved to encapi; bootapi replaces the remaining PXE/kickstart side. It resolves a booting host by MAC (or hostname) from NetBox and serves iPXE boot scripts and rendered kickstart files, so adding a host becomes terraform netbox + encapi and the machine self-installs against this service.

  • add internal/netbox: NetBox v4 client behind a Resolver interface with short-TTL caching (interfaces-by-MAC, devices-by-name)
  • add internal/render: text/template engine with embedded AlmaLinux 9 + Fedora kickstart and iPXE defaults, overlayable via BOOTAPI_TEMPLATE_DIR, selection via provision_template custom field, platform, then OS family
  • add internal/server: /ipxe/{mac}, /boot/ipxe?mac=, /ks/{ident}, healthz/readyz/metrics; unknown MAC boots local disk (safe fallback), unknown kickstart host 404s
  • add internal/model: normalized Host/Interface data model documented in docs/data-model.md
  • add docs: endpoints, template authoring, security (secrets render from Vault/env, never NetBox), deployment incl. dhcpd next-server cutover
  • add Makefile, nfpm, .woodpecker (PR build/test/pre-commit; image + binary release on v* tags)
Cobbler's ENC role moved to encapi; bootapi replaces the remaining PXE/kickstart side. It resolves a booting host by MAC (or hostname) from NetBox and serves iPXE boot scripts and rendered kickstart files, so adding a host becomes terraform netbox + encapi and the machine self-installs against this service. - add internal/netbox: NetBox v4 client behind a Resolver interface with short-TTL caching (interfaces-by-MAC, devices-by-name) - add internal/render: text/template engine with embedded AlmaLinux 9 + Fedora kickstart and iPXE defaults, overlayable via BOOTAPI_TEMPLATE_DIR, selection via provision_template custom field, platform, then OS family - add internal/server: /ipxe/{mac}, /boot/ipxe?mac=, /ks/{ident}, healthz/readyz/metrics; unknown MAC boots local disk (safe fallback), unknown kickstart host 404s - add internal/model: normalized Host/Interface data model documented in docs/data-model.md - add docs: endpoints, template authoring, security (secrets render from Vault/env, never NetBox), deployment incl. dhcpd next-server cutover - add Makefile, nfpm, .woodpecker (PR build/test/pre-commit; image + binary release on v* tags)
unkinben added 1 commit 2026-07-28 20:57:29 +10:00
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
274c480b09
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
benvin reviewed 2026-07-28 21:59:13 +10:00
@@ -4,0 +16,4 @@
## How it works
```
DHCP next-server ─▶ iPXE ─▶ GET /ipxe/<mac> ─▶ boot kernel+initrd, inst.ks=<bootapi>/ks/<host>
Owner

there should be an option to set if the host should pxeboot or not, something the client unticks at the end of kickstart.

there should be an option to set if the host should pxeboot or not, something the client unticks at the end of kickstart.
benvin marked this conversation as resolved
benvin reviewed 2026-07-28 22:00:59 +10:00
@@ -4,0 +24,4 @@
interface lookup → device → primary IP, platform, role, interfaces), renders a
Go `text/template` selected from the host's platform/role/custom-field, and
serves it. Templates are embedded defaults, overridable from a directory
(ConfigMap in k8s).
Owner

can we use a repo in git? have bootapi pull that repo every x minutes (do 3, like arogcd). the repo should have CI to ensure that the templates match strict requirements.

can we use a repo in git? have bootapi pull that repo every x minutes (do 3, like arogcd). the repo should have CI to ensure that the templates match strict requirements.
benvin marked this conversation as resolved
benvin reviewed 2026-07-28 22:01:58 +10:00
@@ -0,0 +39,4 @@
# --- network defaults (used when NetBox does not record them per-device) ---
BOOTAPI_DOMAIN=main.unkin.net
BOOTAPI_NAMESERVERS=198.18.19.19
Owner

this should be the bind resolvers in k8s now

this should be the bind resolvers in k8s now
benvin marked this conversation as resolved
benvin reviewed 2026-07-28 22:02:10 +10:00
@@ -0,0 +34,4 @@
BOOTAPI_BOOT_BASE_URL=http://mirror.k8s.syd1.au.unkin.net/almalinux/9
# --- puppet bootstrap targets (baked into kickstart %post) ---
BOOTAPI_PUPPET_SERVER=puppet.query.consul
Owner

puppet in kubernetes instead

puppet in kubernetes instead
benvin marked this conversation as resolved
benvin reviewed 2026-07-28 22:04:24 +10:00
@@ -0,0 +31,4 @@
# bootapi's own externally-reachable base URL (goes into the iPXE inst.ks=).
BOOTAPI_BASE_URL=http://bootapi.k8s.syd1.au.unkin.net
# Base URL of the OS install trees (kernel/initrd + inst.repo).
BOOTAPI_BOOT_BASE_URL=http://mirror.k8s.syd1.au.unkin.net/almalinux/9
Owner

there is already an almalinux remote in artifactapi. almalinux/${version}/BaseOS/${arch}/os/images/pxeboot/

but I also want to be able to boot other os, like fedora. is there a way we can describe distros in netbox? or should this be environment variables? What happens if we want debian or tailos later?

there is already an almalinux remote in artifactapi. `almalinux/${version}/BaseOS/${arch}/os/images/pxeboot/` but I also want to be able to boot other os, like fedora. is there a way we can describe distros in netbox? or should this be environment variables? What happens if we want debian or tailos later?
benvin marked this conversation as resolved
benvin reviewed 2026-07-28 22:05:41 +10:00
@@ -0,0 +1,68 @@
# bootapi HTTP endpoints
bootapi speaks plain HTTP. It is fronted by the same Vault-issued TLS the Cobbler
Owner

it should be accessible via http and https, since pxebooting hosts may not trust the https cert we use.

it should be accessible via http and https, since pxebooting hosts may not trust the https cert we use.
benvin marked this conversation as resolved
unkinben added 1 commit 2026-07-28 22:34:57 +10:00
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
8f356346eb
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
unkinben reviewed 2026-07-28 22:36:19 +10:00
Author
Owner

Done. Added a NetBox pxe_enabled boolean custom field (Cobbler's netboot_enabled): a known host with it false now gets the safe local-boot script from /ipxe. And 'something the client unticks at the end of kickstart' -> new token-guarded POST /provisioned/{ident} that clears pxe_enabled in NetBox, called from a %post snippet in the default kickstarts. NetBox token now needs write on that one custom field (flagged in docs/security.md + deployment.md).

Done. Added a NetBox `pxe_enabled` boolean custom field (Cobbler's netboot_enabled): a known host with it false now gets the safe local-boot script from /ipxe. And 'something the client unticks at the end of kickstart' -> new token-guarded `POST /provisioned/{ident}` that clears pxe_enabled in NetBox, called from a %post snippet in the default kickstarts. NetBox token now needs write on that one custom field (flagged in docs/security.md + deployment.md).
benvin marked this conversation as resolved
unkinben reviewed 2026-07-28 22:36:20 +10:00
Author
Owner

Done. bootapi now git-syncs a templates repo (unkin/bootapi-templates, terraform-git PR #50) every BOOTAPI_TEMPLATE_GIT_INTERVAL (default 3m, like argocd), atomically swapping the template set; last-good is kept on a parse failure and embedded defaults are the startup fallback. The templates repo gets CI that validates every template/catalog renders (bootapi validate) + shellcheck on %post.

Done. bootapi now git-syncs a templates repo (`unkin/bootapi-templates`, terraform-git PR #50) every BOOTAPI_TEMPLATE_GIT_INTERVAL (default 3m, like argocd), atomically swapping the template set; last-good is kept on a parse failure and embedded defaults are the startup fallback. The templates repo gets CI that validates every template/catalog renders (`bootapi validate`) + shellcheck on %post.
benvin marked this conversation as resolved
unkinben reviewed 2026-07-28 22:36:20 +10:00
Author
Owner

Done. Default nameserver is now the k8s bind-resolvers LB 198.18.200.7 (BOOTAPI_NAMESERVERS).

Done. Default nameserver is now the k8s bind-resolvers LB 198.18.200.7 (BOOTAPI_NAMESERVERS).
benvin marked this conversation as resolved
unkinben reviewed 2026-07-28 22:36:20 +10:00
Author
Owner

Done. Puppet now targets the k8s puppetserver: server=puppet.k8s.syd1.au.unkin.net, ca_server=puppetca.k8s.syd1.au.unkin.net, and %post writes /etc/sysconfig/puppet-initial with PUPPETCA_URL (default puppetca.k8s.syd1.au.unkin.net) for the puppet-initial unit instead of hardcoding.

Done. Puppet now targets the k8s puppetserver: server=puppet.k8s.syd1.au.unkin.net, ca_server=puppetca.k8s.syd1.au.unkin.net, and %post writes /etc/sysconfig/puppet-initial with PUPPETCA_URL (default puppetca.k8s.syd1.au.unkin.net) for the puppet-initial unit instead of hardcoding.
benvin marked this conversation as resolved
unkinben reviewed 2026-07-28 22:36:35 +10:00
Author
Owner

Done. Kernel/initrd now come from the artifactapi almalinux remote: {ArtifactBase}/almalinux/${version}/BaseOS/${arch}/os/images/pxeboot/. On 'describe distros in NetBox or env?': neither hardcoded - implemented a distro catalog (catalog/*.yaml in the templates repo), one file per OS (match, kernel/initrd URL templates -> artifactapi remotes, kickstart ref, kernel args). Host->distro selection stays NetBox-driven (platform slug / family / provision_template). Shipped almalinux9 + fedora (fedora remote exists). Adding debian/talos later = a yaml + template (+ maybe a new artifactapi remote), no bootapi code change; their differing artifact shapes are documented in docs/template-authoring.md and the catalog README. Env vars are now only for bootapi's own endpoints.

Done. Kernel/initrd now come from the artifactapi almalinux remote: {ArtifactBase}/almalinux/${version}/BaseOS/${arch}/os/images/pxeboot/. On 'describe distros in NetBox or env?': neither hardcoded - implemented a **distro catalog** (catalog/*.yaml in the templates repo), one file per OS (match, kernel/initrd URL templates -> artifactapi remotes, kickstart ref, kernel args). Host->distro selection stays NetBox-driven (platform slug / family / provision_template). Shipped almalinux9 + fedora (fedora remote exists). Adding debian/talos later = a yaml + template (+ maybe a new artifactapi remote), no bootapi code change; their differing artifact shapes are documented in docs/template-authoring.md and the catalog README. Env vars are now only for bootapi's own endpoints.
benvin marked this conversation as resolved
unkinben reviewed 2026-07-28 22:36:35 +10:00
Author
Owner

Done. The boot path (/ipxe, /boot/ipxe, /ks) is always served over plain HTTP so installers with no internal-CA trust can reach it; iPXE/kickstart URLs are http:// (BOOTAPI_BASE_URL). bootapi also serves HTTPS in parallel when BOOTAPI_TLS_* is set, for clients that trust the CA. The %post provisioned callback also runs over plain HTTP by default (the token authenticates it, before any CA is installed); BOOTAPI_CALLBACK_BASE_URL can point it at https if you install the CA early. docs/deployment.md now says the Gateway/HTTPRoute must NOT 301 HTTP->HTTPS for the boot endpoints.

Done. The boot path (/ipxe, /boot/ipxe, /ks) is always served over plain HTTP so installers with no internal-CA trust can reach it; iPXE/kickstart URLs are http:// (BOOTAPI_BASE_URL). bootapi also serves HTTPS in parallel when BOOTAPI_TLS_* is set, for clients that trust the CA. The %post provisioned callback also runs over plain HTTP by default (the token authenticates it, before any CA is installed); BOOTAPI_CALLBACK_BASE_URL can point it at https if you install the CA early. docs/deployment.md now says the Gateway/HTTPRoute must NOT 301 HTTP->HTTPS for the boot endpoints.
benvin marked this conversation as resolved
benvin merged commit b14ccdc74e into main 2026-07-28 23:35:04 +10:00
benvin deleted branch benvin/bootapi-initial 2026-07-28 23:35:04 +10:00
Sign in to join this conversation.
No Reviewers
2 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: unkin/bootapi#1