Files
bootapi/internal/model/host.go
T
unkinben 8f356346eb
ci/woodpecker/pr/build Pipeline was successful
ci/woodpecker/pr/pre-commit Pipeline was successful
ci/woodpecker/pr/test Pipeline was successful
Address PR review: PXE gate + callback, git-sync templates, distro catalog, k8s targets, http+https
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
2026-07-28 22:34:44 +10:00

120 lines
4.6 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 {
// DeviceID is the NetBox device id, used by the provisioned-callback to
// PATCH the pxe_enabled custom field.
DeviceID int
// 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
// PXEEnabled gates network install for this host, mirroring Cobbler's
// netboot_enabled. When false, bootapi serves the safe local-boot script
// from /ipxe even for a KNOWN host, so a provisioned machine does not
// re-install on its next PXE. nil means the NetBox custom field is unset,
// which is treated as ENABLED (a host without the field still installs).
// The end-of-kickstart callback (POST /provisioned) flips this to false.
PXEEnabled *bool
// 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
}
// ShouldPXEInstall reports whether bootapi should serve an installer boot script
// for this host. Unset (nil) is treated as enabled so hosts predating the
// custom field still provision.
func (h *Host) ShouldPXEInstall() bool {
return h.PXEEnabled == nil || *h.PXEEnabled
}
// 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
}