Files
bootapi/internal/model/host.go
T
unkinben 274c480b09
ci/woodpecker/pr/build Pipeline was successful
ci/woodpecker/pr/pre-commit Pipeline was successful
ci/woodpecker/pr/test Pipeline was successful
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
2026-07-28 20:57:11 +10:00

101 lines
3.7 KiB
Go

// Package model holds the provisioning data model bootapi renders templates
// against. A Host is the normalized view of a NetBox device: enough to build a
// kickstart and an iPXE boot script without the template author needing to know
// anything about NetBox's API shapes.
package model
// Host is the fully-resolved provisioning view of a single machine.
//
// Every field here is safe to reference from a kickstart or iPXE template. The
// zero value of a field means "NetBox did not provide it"; templates should
// guard optional fields (e.g. Gateway) accordingly.
type Host struct {
// Hostname is the short name (NetBox device name), e.g. "web01".
Hostname string
// Domain is the DNS domain the host lives in, e.g. "syd1.au.unkin.net".
Domain string
// FQDN is Hostname joined to Domain when a domain is known, else Hostname.
FQDN string
// Platform is the NetBox platform slug, e.g. "almalinux9". It is the
// primary template-selection key.
Platform string
// OSFamily is a coarse family derived from Platform ("almalinux",
// "fedora", "rocky", ...). Handy for shared template logic.
OSFamily string
// OSVersion is the major version string when derivable, e.g. "9".
OSVersion string
// Arch is the CPU architecture, defaulting to "x86_64".
Arch string
// Role is the NetBox device role slug, e.g. "kubernetes-worker". Available
// as a secondary template-selection key and for %post logic.
Role string
// Interfaces are the host's network interfaces, primary first.
Interfaces []Interface
// PrimaryIP is the address of the primary interface (no prefix length),
// e.g. "10.0.1.20". Empty when NetBox has no primary IP set.
PrimaryIP string
// Nameservers are DNS resolvers to configure, when NetBox provides them
// (via a custom field); otherwise empty and templates fall back to a
// site default.
Nameservers []string
// RootPasswordHash is a crypt(3) hash for the root account, sourced at
// render time (env/Vault), NOT stored in NetBox. Empty means "locked
// account / template default".
RootPasswordHash string
// SSHAuthorizedKeys are public keys to install for root, sourced at render
// time. Empty means none.
SSHAuthorizedKeys []string
// TemplateOverride, when non-empty, names the template to use verbatim,
// bypassing platform/role selection. Sourced from a NetBox custom field.
TemplateOverride string
// Custom carries every NetBox custom field verbatim so templates can read
// site-specific knobs without a code change. Keys are the custom-field
// names as defined in NetBox.
Custom map[string]any
}
// Interface is one network interface of a Host.
type Interface struct {
// Name is the NetBox interface name, e.g. "eth0" / "bond0".
Name string
// MAC is the normalized (lower-case, colon-separated) hardware address.
MAC string
// IP is the interface address without prefix, e.g. "10.0.1.20". Empty for
// interfaces with no assigned address.
IP string
// PrefixLen is the CIDR prefix length of IP, e.g. 24. Zero when unknown.
PrefixLen int
// Netmask is the dotted-quad form of PrefixLen, e.g. "255.255.255.0".
Netmask string
// Gateway is the default gateway for this interface's prefix, when NetBox
// records one on the prefix. Empty otherwise.
Gateway string
// VLAN is the untagged VLAN id of the interface, or 0 when none.
VLAN int
// Primary reports whether this interface holds the device's primary IP.
Primary bool
}
// PrimaryInterface returns the primary interface (the one carrying the primary
// IP), falling back to the first interface, or nil when there are none.
func (h *Host) PrimaryInterface() *Interface {
for i := range h.Interfaces {
if h.Interfaces[i].Primary {
return &h.Interfaces[i]
}
}
if len(h.Interfaces) > 0 {
return &h.Interfaces[0]
}
return nil
}