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
127 lines
3.6 KiB
Go
127 lines
3.6 KiB
Go
package catalog
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
|
|
"git.unkin.net/unkin/bootapi/internal/model"
|
|
)
|
|
|
|
const almaYAML = `
|
|
name: almalinux9
|
|
match:
|
|
platforms: [almalinux9]
|
|
family: almalinux
|
|
kickstart: almalinux9
|
|
version_default: "9"
|
|
kernel_url: "{{.ArtifactBase}}/almalinux/{{.Version}}/BaseOS/{{.Arch}}/os/images/pxeboot/vmlinuz"
|
|
initrd_url: "{{.ArtifactBase}}/almalinux/{{.Version}}/BaseOS/{{.Arch}}/os/images/pxeboot/initrd.img"
|
|
kernel_args: [inst.text]
|
|
vars:
|
|
mirror: "{{.ArtifactBase}}/almalinux/{{.Version}}"
|
|
`
|
|
|
|
const fedoraYAML = `
|
|
name: fedora
|
|
match:
|
|
family: fedora
|
|
kickstart: fedora
|
|
version_default: "41"
|
|
kernel_url: "{{.ArtifactBase}}/fedora/releases/{{.Version}}/Everything/{{.Arch}}/os/images/pxeboot/vmlinuz"
|
|
initrd_url: "{{.ArtifactBase}}/fedora/releases/{{.Version}}/Everything/{{.Arch}}/os/images/pxeboot/initrd.img"
|
|
`
|
|
|
|
func testCatalog(t *testing.T) *Catalog {
|
|
t.Helper()
|
|
c, err := Parse(map[string][]byte{
|
|
"almalinux9.yaml": []byte(almaYAML),
|
|
"fedora.yaml": []byte(fedoraYAML),
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("Parse: %v", err)
|
|
}
|
|
return c
|
|
}
|
|
|
|
func TestSelect(t *testing.T) {
|
|
c := testCatalog(t)
|
|
cases := []struct {
|
|
host *model.Host
|
|
want string
|
|
ok bool
|
|
}{
|
|
{&model.Host{Platform: "almalinux9", OSFamily: "almalinux"}, "almalinux9", true}, // exact platform
|
|
{&model.Host{Platform: "fedora42", OSFamily: "fedora"}, "fedora", true}, // family
|
|
{&model.Host{Platform: "almalinux9", TemplateOverride: "fedora"}, "fedora", true}, // override wins
|
|
{&model.Host{Platform: "debian12", OSFamily: "debian"}, "", false}, // no match
|
|
}
|
|
for _, tc := range cases {
|
|
d, ok := c.Select(tc.host)
|
|
if ok != tc.ok {
|
|
t.Errorf("Select(%+v) ok=%v, want %v", tc.host, ok, tc.ok)
|
|
continue
|
|
}
|
|
if ok && d.Name != tc.want {
|
|
t.Errorf("Select(%+v) = %q, want %q", tc.host, d.Name, tc.want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestResolve(t *testing.T) {
|
|
c := testCatalog(t)
|
|
h := &model.Host{Platform: "almalinux9", OSFamily: "almalinux", OSVersion: "9", Arch: "x86_64"}
|
|
d, ok := c.Select(h)
|
|
if !ok {
|
|
t.Fatal("expected a match")
|
|
}
|
|
r, err := d.Resolve(h, "https://af/api/v1/remote")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if r.KernelURL != "https://af/api/v1/remote/almalinux/9/BaseOS/x86_64/os/images/pxeboot/vmlinuz" {
|
|
t.Errorf("kernel = %q", r.KernelURL)
|
|
}
|
|
if r.Vars["mirror"] != "https://af/api/v1/remote/almalinux/9" {
|
|
t.Errorf("mirror = %q", r.Vars["mirror"])
|
|
}
|
|
if len(r.KernelArgs) != 1 || r.KernelArgs[0] != "inst.text" {
|
|
t.Errorf("kernel_args = %v", r.KernelArgs)
|
|
}
|
|
}
|
|
|
|
func TestResolveVersionDefault(t *testing.T) {
|
|
c := testCatalog(t)
|
|
// Host with no OSVersion falls back to the catalog's version_default.
|
|
h := &model.Host{Platform: "fedora", OSFamily: "fedora", Arch: "x86_64"}
|
|
d, _ := c.Select(h)
|
|
r, err := d.Resolve(h, "https://af")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !strings.Contains(r.KernelURL, "/releases/41/") {
|
|
t.Errorf("expected version_default 41 in %q", r.KernelURL)
|
|
}
|
|
}
|
|
|
|
func TestParseValidation(t *testing.T) {
|
|
bad := map[string]string{
|
|
"no-kernel": "name: x\nmatch: {platforms: [x]}\nkickstart: x\ninitrd_url: y",
|
|
"no-match": "name: x\nkickstart: x\nkernel_url: k\ninitrd_url: i",
|
|
"no-name": "kickstart: x\nmatch: {family: x}\nkernel_url: k\ninitrd_url: i",
|
|
"bad-template": "name: x\nmatch: {family: x}\nkickstart: x\nkernel_url: \"{{ .Nope\"\ninitrd_url: i",
|
|
}
|
|
for name, y := range bad {
|
|
if _, err := Parse(map[string][]byte{name + ".yaml": []byte(y)}); err == nil {
|
|
t.Errorf("%s: expected a validation error, got nil", name)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestNames(t *testing.T) {
|
|
c := testCatalog(t)
|
|
got := strings.Join(c.Names(), ",")
|
|
if got != "almalinux9,fedora" {
|
|
t.Errorf("Names() = %q", got)
|
|
}
|
|
}
|