package pxe import ( "net" "os" "path/filepath" "strings" "testing" ) // existingInterface returns any real interface on the host so the // Validate tests can exercise the happy path without hardcoding // "lo" (which exists on Linux but might be gated elsewhere). func existingInterface(t *testing.T) string { t.Helper() ifaces, err := net.Interfaces() if err != nil || len(ifaces) == 0 { t.Skipf("no network interfaces: %v", err) } return ifaces[0].Name } // seedTFTP drops zero-byte ipxe.efi + undionly.kpxe into dir so the // stat check passes. Callers can omit a name to simulate "missing". func seedTFTP(t *testing.T, dir string, names ...string) { t.Helper() if err := os.MkdirAll(dir, 0o755); err != nil { t.Fatalf("mkdir tftp: %v", err) } for _, name := range names { if err := os.WriteFile(filepath.Join(dir, name), nil, 0o644); err != nil { t.Fatalf("seed %s: %v", name, err) } } } func goodCfg(t *testing.T, tftpRoot string) SupervisorConfig { t.Helper() return SupervisorConfig{ Enabled: true, Interface: existingInterface(t), DHCPRange: "10.77.0.100,10.77.0.200,12h", OrchestratorURL: "http://10.77.0.1:8080", TFTPRoot: tftpRoot, } } func TestValidate_DisabledSkipsChecks(t *testing.T) { s := NewSupervisor(SupervisorConfig{Enabled: false}) if err := s.Validate(); err != nil { t.Fatalf("disabled supervisor should skip validation, got: %v", err) } } func TestValidate_HappyPath(t *testing.T) { tftp := t.TempDir() seedTFTP(t, tftp, "ipxe.efi", "undionly.kpxe") s := NewSupervisor(goodCfg(t, tftp)) if err := s.Validate(); err != nil { t.Fatalf("happy-path validate: %v", err) } } func TestValidate_MissingIPXEBinary(t *testing.T) { tftp := t.TempDir() // Only seed one of the two required files. seedTFTP(t, tftp, "undionly.kpxe") s := NewSupervisor(goodCfg(t, tftp)) err := s.Validate() if err == nil { t.Fatalf("expected error for missing ipxe.efi") } if !strings.Contains(err.Error(), "ipxe.efi") { t.Fatalf("error should name the missing file, got: %v", err) } if !strings.Contains(err.Error(), "pxe-setup.sh") { t.Fatalf("error should point operator at pxe-setup.sh, got: %v", err) } } func TestValidate_MissingUndionly(t *testing.T) { tftp := t.TempDir() seedTFTP(t, tftp, "ipxe.efi") s := NewSupervisor(goodCfg(t, tftp)) err := s.Validate() if err == nil || !strings.Contains(err.Error(), "undionly.kpxe") { t.Fatalf("expected undionly.kpxe error, got: %v", err) } } func TestValidate_MissingInterface(t *testing.T) { tftp := t.TempDir() seedTFTP(t, tftp, "ipxe.efi", "undionly.kpxe") cfg := goodCfg(t, tftp) cfg.Interface = "definitely-not-a-real-iface-9999" s := NewSupervisor(cfg) err := s.Validate() if err == nil || !strings.Contains(err.Error(), "pxe.interface") { t.Fatalf("expected interface error, got: %v", err) } } func TestValidate_MissingLiveImage(t *testing.T) { tftp := t.TempDir() seedTFTP(t, tftp, "ipxe.efi", "undionly.kpxe") cfg := goodCfg(t, tftp) cfg.LiveDir = t.TempDir() // empty dir; vmlinuz + initrd.img missing s := NewSupervisor(cfg) err := s.Validate() if err == nil { t.Fatalf("expected live image error") } for _, want := range []string{"vmlinuz", "initrd.img"} { if !strings.Contains(err.Error(), want) { t.Fatalf("error should name %s, got: %v", want, err) } } } func TestValidate_LiveDirEmptySkipsLiveChecks(t *testing.T) { tftp := t.TempDir() seedTFTP(t, tftp, "ipxe.efi", "undionly.kpxe") cfg := goodCfg(t, tftp) cfg.LiveDir = "" // explicit opt-out; HTTP /live just 404s s := NewSupervisor(cfg) if err := s.Validate(); err != nil { t.Fatalf("empty LiveDir should not trigger live checks, got: %v", err) } } func TestValidate_MalformedDHCPRange(t *testing.T) { tftp := t.TempDir() seedTFTP(t, tftp, "ipxe.efi", "undionly.kpxe") cases := []struct { name string dhcp string }{ {"single field", "10.77.0.100"}, {"two fields", "10.77.0.100,10.77.0.200"}, {"non-ip start", "hello,10.77.0.200,12h"}, {"empty lease", "10.77.0.100,10.77.0.200,"}, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { cfg := goodCfg(t, tftp) cfg.DHCPRange = tc.dhcp s := NewSupervisor(cfg) err := s.Validate() if err == nil || !strings.Contains(err.Error(), "dhcp_range") { t.Fatalf("expected dhcp_range error for %q, got: %v", tc.dhcp, err) } }) } } func TestValidate_BadOrchestratorURL(t *testing.T) { tftp := t.TempDir() seedTFTP(t, tftp, "ipxe.efi", "undionly.kpxe") cases := []string{"", "not a url", "ftp://10.0.0.1", "http://"} for _, u := range cases { t.Run(u, func(t *testing.T) { cfg := goodCfg(t, tftp) cfg.OrchestratorURL = u s := NewSupervisor(cfg) err := s.Validate() if err == nil || !strings.Contains(err.Error(), "orchestrator_url") { t.Fatalf("expected orchestrator_url error for %q, got: %v", u, err) } }) } } func TestValidate_AggregatesErrors(t *testing.T) { // Multiple problems at once: Validate must report them all in // one pass so the operator sees the full picture instead of // whack-a-mole-ing one error per restart. cfg := SupervisorConfig{ Enabled: true, // Everything else zero. } s := NewSupervisor(cfg) err := s.Validate() if err == nil { t.Fatalf("expected aggregated error") } for _, want := range []string{"pxe.interface", "pxe.tftp_root", "pxe.dhcp_range", "pxe.orchestrator_url"} { if !strings.Contains(err.Error(), want) { t.Fatalf("expected %q in aggregated error, got: %v", want, err) } } }