package render import ( "os" "path/filepath" "strings" "testing" "git.unkin.net/unkin/bootapi/internal/model" "git.unkin.net/unkin/bootapi/templates" ) func testEngine(t *testing.T, override string) *Engine { t.Helper() e, err := NewEngine(templates.FS, override, RenderConfig{ PuppetServer: "puppet.query.consul", PuppetCAServer: "puppetca.query.consul", BaseURL: "http://bootapi.example.net", BootBaseURL: "http://mirror.example.net/almalinux/9", DefaultDomain: "main.unkin.net", DefaultNS: []string{"10.0.0.1"}, RootPasswordHash: "$6$rounds=4096$abc$deadbeef", SSHAuthorizedKeys: []string{"ssh-ed25519 AAAAC3xxx root@ops"}, DefaultTemplate: "almalinux9", }) if err != nil { t.Fatalf("NewEngine: %v", err) } return e } func almaHost() *model.Host { return &model.Host{ Hostname: "web01", Domain: "syd1.au.unkin.net", FQDN: "web01.syd1.au.unkin.net", Platform: "almalinux9", OSFamily: "almalinux", OSVersion: "9", Arch: "x86_64", Role: "kubernetes-worker", PrimaryIP: "10.0.1.20", Interfaces: []model.Interface{ {Name: "eth0", MAC: "aa:bb:cc:00:11:22", IP: "10.0.1.20", PrefixLen: 24, Netmask: "255.255.255.0", Gateway: "10.0.1.254", VLAN: 100, Primary: true}, {Name: "eth1", MAC: "aa:bb:cc:00:11:33"}, // no IP -> must be skipped in network stanza }, } } func TestRenderKickstartAlma(t *testing.T) { e := testEngine(t, "") out, name, err := e.RenderKickstart(almaHost()) if err != nil { t.Fatalf("RenderKickstart: %v", err) } if name != "almalinux9" { t.Errorf("selected template = %q, want almalinux9", name) } ks := string(out) mustContain(t, ks, "rootpw --iscrypted $6$rounds=4096$abc$deadbeef") // The primary interface must produce a full static network line incl hostname. mustContain(t, ks, "network --bootproto=static --device=aa:bb:cc:00:11:22 --ip=10.0.1.20 --netmask=255.255.255.0 --gateway=10.0.1.254 --nameserver=10.0.0.1 --hostname=web01.syd1.au.unkin.net") mustContain(t, ks, `"$PUPPET_BIN" config set --section main server "puppet.query.consul"`) mustContain(t, ks, `config set --section main ca_server "puppetca.query.consul"`) mustContain(t, ks, "url --url=http://mirror.example.net/almalinux/9/BaseOS/x86_64/os/") mustContain(t, ks, "ssh-ed25519 AAAAC3xxx root@ops") mustContain(t, ks, "dnf install -y puppet-agent") mustContain(t, ks, "%packages") mustContain(t, ks, "%post") // eth1 has no IP, so it must NOT appear as a network device line. if strings.Contains(ks, "--device=aa:bb:cc:00:11:33") { t.Error("interface without an IP leaked into a network stanza") } } func TestRenderKickstartLockedRoot(t *testing.T) { // With no root hash configured, the account must be locked, not blank. e, err := NewEngine(templates.FS, "", RenderConfig{DefaultTemplate: "almalinux9", BootBaseURL: "http://m/9"}) if err != nil { t.Fatal(err) } out, _, err := e.RenderKickstart(almaHost()) if err != nil { t.Fatal(err) } ks := string(out) mustContain(t, ks, "rootpw --lock") if strings.Contains(ks, "--iscrypted") { t.Error("expected locked root, got an --iscrypted line") } } func TestSelectKickstartPrecedence(t *testing.T) { e := testEngine(t, "") cases := []struct { host *model.Host want string }{ {&model.Host{TemplateOverride: "fedora", Platform: "almalinux9"}, "fedora"}, // override wins {&model.Host{Platform: "almalinux9"}, "almalinux9"}, // platform {&model.Host{Platform: "fedora42", OSFamily: "fedora"}, "fedora"}, // family fallback {&model.Host{Platform: "unknownos"}, "almalinux9"}, // default } for _, c := range cases { got, ok := e.SelectKickstart(c.host) if !ok || got != c.want { t.Errorf("SelectKickstart(%+v) = (%q,%v), want %q", c.host, got, ok, c.want) } } } func TestRenderIPXE(t *testing.T) { e := testEngine(t, "") out, err := e.RenderIPXE(almaHost()) if err != nil { t.Fatalf("RenderIPXE: %v", err) } s := string(out) mustContain(t, s, "#!ipxe") mustContain(t, s, "kernel http://mirror.example.net/almalinux/9/images/pxeboot/vmlinuz") mustContain(t, s, "inst.ks=http://bootapi.example.net/ks/web01") mustContain(t, s, "initrd http://mirror.example.net/almalinux/9/images/pxeboot/initrd.img") } func TestRenderFallback(t *testing.T) { e := testEngine(t, "") local, err := e.RenderFallback("local") if err != nil { t.Fatal(err) } mustContain(t, string(local), "sanboot") shell, err := e.RenderFallback("shell") if err != nil { t.Fatal(err) } mustContain(t, string(shell), "shell") } func TestOverrideDirWins(t *testing.T) { dir := t.TempDir() if err := os.WriteFile(filepath.Join(dir, "almalinux9.ks.tmpl"), []byte("OVERRIDDEN {{ .Hostname }}\n"), 0o600); err != nil { t.Fatal(err) } e := testEngine(t, dir) out, _, err := e.RenderKickstart(almaHost()) if err != nil { t.Fatal(err) } if !strings.HasPrefix(string(out), "OVERRIDDEN web01") { t.Errorf("override not applied: %q", string(out)) } } func mustContain(t *testing.T, haystack, needle string) { t.Helper() if !strings.Contains(haystack, needle) { t.Errorf("output missing %q\n--- output ---\n%s", needle, haystack) } }