Initial bootapi: NetBox-driven PXE/kickstart boot service
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:
@@ -0,0 +1,76 @@
|
||||
// Command bootapi is the PXE/kickstart boot service: it renders kickstart files
|
||||
// and iPXE boot scripts from NetBox device data and serves them to PXE-booting
|
||||
// hosts, replacing Cobbler's provisioning side.
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log/slog"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
|
||||
"git.unkin.net/unkin/bootapi/internal/config"
|
||||
"git.unkin.net/unkin/bootapi/internal/netbox"
|
||||
"git.unkin.net/unkin/bootapi/internal/render"
|
||||
"git.unkin.net/unkin/bootapi/internal/server"
|
||||
"git.unkin.net/unkin/bootapi/templates"
|
||||
)
|
||||
|
||||
var version = "dev"
|
||||
|
||||
func main() {
|
||||
slog.SetDefault(slog.New(slog.NewJSONHandler(os.Stdout, nil)))
|
||||
slog.Info("starting bootapi", "version", version)
|
||||
|
||||
cfg, err := config.Load()
|
||||
if err != nil {
|
||||
slog.Error("load config", "err", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
if cfg.NetBoxURL == "" {
|
||||
slog.Warn("BOOTAPI_NETBOX_URL is empty; every host lookup will fail and iPXE will serve the fallback")
|
||||
}
|
||||
if cfg.NetBoxToken == "" {
|
||||
slog.Warn("no NetBox token set (BOOTAPI_NETBOX_TOKEN/_FILE); NetBox reads will likely be denied")
|
||||
}
|
||||
|
||||
engine, err := render.NewEngine(templates.FS, cfg.TemplateDir, render.RenderConfig{
|
||||
PuppetServer: cfg.PuppetServer,
|
||||
PuppetCAServer: cfg.PuppetCAServer,
|
||||
BaseURL: cfg.BaseURL,
|
||||
BootBaseURL: cfg.BootBaseURL,
|
||||
DefaultDomain: cfg.Domain,
|
||||
DefaultNS: cfg.Nameservers,
|
||||
RootPasswordHash: cfg.RootPasswordHash,
|
||||
SSHAuthorizedKeys: cfg.SSHAuthorizedKeys,
|
||||
DefaultTemplate: cfg.DefaultTemplate,
|
||||
})
|
||||
if err != nil {
|
||||
slog.Error("load templates", "err", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
nb := netbox.New(netbox.Options{
|
||||
BaseURL: cfg.NetBoxURL,
|
||||
Token: cfg.NetBoxToken,
|
||||
Timeout: cfg.NetBoxTimeout,
|
||||
Insecure: cfg.NetBoxInsecure,
|
||||
})
|
||||
cache := netbox.NewCache(nb, cfg.CacheTTL)
|
||||
|
||||
srv := server.New(server.Options{
|
||||
Resolver: cache,
|
||||
Engine: engine,
|
||||
Cache: cache,
|
||||
UnknownMACFallback: cfg.UnknownMACFallback,
|
||||
})
|
||||
|
||||
ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
|
||||
defer stop()
|
||||
|
||||
if err := srv.ListenAndServe(ctx, cfg.ListenAddr); err != nil {
|
||||
slog.Error("server", "err", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user