489 lines
14 KiB
Go
489 lines
14 KiB
Go
package app
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
|
|
"bee/audit/internal/platform"
|
|
"bee/audit/internal/schema"
|
|
)
|
|
|
|
type fakeNetwork struct {
|
|
listInterfacesFn func() ([]platform.InterfaceInfo, error)
|
|
defaultRouteFn func() string
|
|
dhcpOneFn func(string) (string, error)
|
|
dhcpAllFn func() (string, error)
|
|
setStaticIPv4Fn func(platform.StaticIPv4Config) (string, error)
|
|
}
|
|
|
|
func (f fakeNetwork) ListInterfaces() ([]platform.InterfaceInfo, error) {
|
|
return f.listInterfacesFn()
|
|
}
|
|
|
|
func (f fakeNetwork) DefaultRoute() string {
|
|
return f.defaultRouteFn()
|
|
}
|
|
|
|
func (f fakeNetwork) DHCPOne(iface string) (string, error) {
|
|
return f.dhcpOneFn(iface)
|
|
}
|
|
|
|
func (f fakeNetwork) DHCPAll() (string, error) {
|
|
return f.dhcpAllFn()
|
|
}
|
|
|
|
func (f fakeNetwork) SetStaticIPv4(cfg platform.StaticIPv4Config) (string, error) {
|
|
return f.setStaticIPv4Fn(cfg)
|
|
}
|
|
|
|
type fakeServices struct {
|
|
serviceStatusFn func(string) (string, error)
|
|
serviceDoFn func(string, platform.ServiceAction) (string, error)
|
|
}
|
|
|
|
func (f fakeServices) ListBeeServices() ([]string, error) {
|
|
return nil, nil
|
|
}
|
|
|
|
func (f fakeServices) ServiceStatus(name string) (string, error) {
|
|
return f.serviceStatusFn(name)
|
|
}
|
|
|
|
func (f fakeServices) ServiceDo(name string, action platform.ServiceAction) (string, error) {
|
|
return f.serviceDoFn(name, action)
|
|
}
|
|
|
|
type fakeExports struct{}
|
|
|
|
func (f fakeExports) ListRemovableTargets() ([]platform.RemovableTarget, error) {
|
|
return nil, nil
|
|
}
|
|
|
|
func (f fakeExports) ExportFileToTarget(src string, target platform.RemovableTarget) (string, error) {
|
|
return "", nil
|
|
}
|
|
|
|
type fakeTools struct {
|
|
tailFileFn func(string, int) string
|
|
checkToolsFn func([]string) []platform.ToolStatus
|
|
}
|
|
|
|
func (f fakeTools) TailFile(path string, lines int) string {
|
|
return f.tailFileFn(path, lines)
|
|
}
|
|
|
|
func (f fakeTools) CheckTools(names []string) []platform.ToolStatus {
|
|
return f.checkToolsFn(names)
|
|
}
|
|
|
|
type fakeSAT struct {
|
|
runNvidiaFn func(string) (string, error)
|
|
runMemoryFn func(string) (string, error)
|
|
runStorageFn func(string) (string, error)
|
|
}
|
|
|
|
func (f fakeSAT) RunNvidiaAcceptancePack(baseDir string) (string, error) {
|
|
return f.runNvidiaFn(baseDir)
|
|
}
|
|
|
|
func (f fakeSAT) RunMemoryAcceptancePack(baseDir string) (string, error) {
|
|
return f.runMemoryFn(baseDir)
|
|
}
|
|
|
|
func (f fakeSAT) RunStorageAcceptancePack(baseDir string) (string, error) {
|
|
return f.runStorageFn(baseDir)
|
|
}
|
|
|
|
func TestNetworkStatusFormatsInterfacesAndRoute(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
a := &App{
|
|
network: fakeNetwork{
|
|
listInterfacesFn: func() ([]platform.InterfaceInfo, error) {
|
|
return []platform.InterfaceInfo{
|
|
{Name: "eth0", State: "UP", IPv4: []string{"10.0.0.2/24"}},
|
|
{Name: "eth1", State: "DOWN", IPv4: nil},
|
|
}, nil
|
|
},
|
|
defaultRouteFn: func() string { return "10.0.0.1" },
|
|
},
|
|
}
|
|
|
|
result, err := a.NetworkStatus()
|
|
if err != nil {
|
|
t.Fatalf("NetworkStatus error: %v", err)
|
|
}
|
|
if result.Title != "Network status" {
|
|
t.Fatalf("title=%q want %q", result.Title, "Network status")
|
|
}
|
|
if want := "- eth0: state=UP ip=10.0.0.2/24"; !contains(result.Body, want) {
|
|
t.Fatalf("body missing %q\nbody=%s", want, result.Body)
|
|
}
|
|
if want := "- eth1: state=DOWN ip=(no IPv4)"; !contains(result.Body, want) {
|
|
t.Fatalf("body missing %q\nbody=%s", want, result.Body)
|
|
}
|
|
if want := "Default route: 10.0.0.1"; !contains(result.Body, want) {
|
|
t.Fatalf("body missing %q\nbody=%s", want, result.Body)
|
|
}
|
|
}
|
|
|
|
func TestNetworkStatusHandlesNoInterfaces(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
a := &App{
|
|
network: fakeNetwork{
|
|
listInterfacesFn: func() ([]platform.InterfaceInfo, error) { return nil, nil },
|
|
defaultRouteFn: func() string { return "" },
|
|
},
|
|
}
|
|
|
|
result, err := a.NetworkStatus()
|
|
if err != nil {
|
|
t.Fatalf("NetworkStatus error: %v", err)
|
|
}
|
|
if result.Body != "No physical interfaces found." {
|
|
t.Fatalf("body=%q want %q", result.Body, "No physical interfaces found.")
|
|
}
|
|
}
|
|
|
|
func TestNetworkStatusPropagatesListError(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
a := &App{
|
|
network: fakeNetwork{
|
|
listInterfacesFn: func() ([]platform.InterfaceInfo, error) {
|
|
return nil, errors.New("boom")
|
|
},
|
|
defaultRouteFn: func() string { return "" },
|
|
},
|
|
}
|
|
|
|
result, err := a.NetworkStatus()
|
|
if err == nil {
|
|
t.Fatal("expected error")
|
|
}
|
|
if result.Title != "Network status" {
|
|
t.Fatalf("title=%q want %q", result.Title, "Network status")
|
|
}
|
|
}
|
|
|
|
func TestParseStaticIPv4ConfigAndDefaults(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
a := &App{
|
|
network: fakeNetwork{
|
|
defaultRouteFn: func() string { return " 192.168.1.1 " },
|
|
listInterfacesFn: func() ([]platform.InterfaceInfo, error) {
|
|
return nil, nil
|
|
},
|
|
dhcpOneFn: func(string) (string, error) { return "", nil },
|
|
dhcpAllFn: func() (string, error) { return "", nil },
|
|
setStaticIPv4Fn: func(platform.StaticIPv4Config) (string, error) { return "", nil },
|
|
},
|
|
}
|
|
|
|
defaults := a.DefaultStaticIPv4FormFields("eth0")
|
|
if len(defaults) != 4 {
|
|
t.Fatalf("len(defaults)=%d want 4", len(defaults))
|
|
}
|
|
if defaults[1] != "24" || defaults[2] != "192.168.1.1" {
|
|
t.Fatalf("unexpected defaults: %#v", defaults)
|
|
}
|
|
|
|
cfg := a.ParseStaticIPv4Config("eth0", []string{
|
|
" 10.10.0.5 ",
|
|
" 23 ",
|
|
" 10.10.0.1 ",
|
|
" 1.1.1.1 8.8.8.8 ",
|
|
})
|
|
if cfg.Interface != "eth0" || cfg.Address != "10.10.0.5" || cfg.Prefix != "23" || cfg.Gateway != "10.10.0.1" {
|
|
t.Fatalf("unexpected cfg: %#v", cfg)
|
|
}
|
|
if len(cfg.DNS) != 2 || cfg.DNS[0] != "1.1.1.1" || cfg.DNS[1] != "8.8.8.8" {
|
|
t.Fatalf("unexpected dns: %#v", cfg.DNS)
|
|
}
|
|
}
|
|
|
|
func TestServiceActionResults(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
a := &App{
|
|
services: fakeServices{
|
|
serviceStatusFn: func(name string) (string, error) {
|
|
return "active", nil
|
|
},
|
|
serviceDoFn: func(name string, action platform.ServiceAction) (string, error) {
|
|
return string(action) + " ok", nil
|
|
},
|
|
},
|
|
}
|
|
|
|
statusResult, err := a.ServiceStatusResult("bee-audit")
|
|
if err != nil {
|
|
t.Fatalf("ServiceStatusResult error: %v", err)
|
|
}
|
|
if statusResult.Title != "service status: bee-audit" || statusResult.Body != "active" {
|
|
t.Fatalf("unexpected status result: %#v", statusResult)
|
|
}
|
|
|
|
actionResult, err := a.ServiceActionResult("bee-audit", platform.ServiceRestart)
|
|
if err != nil {
|
|
t.Fatalf("ServiceActionResult error: %v", err)
|
|
}
|
|
if actionResult.Title != "service restart: bee-audit" || actionResult.Body != "restart ok" {
|
|
t.Fatalf("unexpected action result: %#v", actionResult)
|
|
}
|
|
}
|
|
|
|
func TestToolCheckAndLogTailResults(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
a := &App{
|
|
tools: fakeTools{
|
|
tailFileFn: func(path string, lines int) string {
|
|
return path
|
|
},
|
|
checkToolsFn: func(names []string) []platform.ToolStatus {
|
|
return []platform.ToolStatus{
|
|
{Name: "dmidecode", OK: true, Path: "/usr/bin/dmidecode"},
|
|
{Name: "smartctl", OK: false},
|
|
}
|
|
},
|
|
},
|
|
}
|
|
|
|
toolsResult := a.ToolCheckResult([]string{"dmidecode", "smartctl"})
|
|
if toolsResult.Title != "Required tools" {
|
|
t.Fatalf("title=%q want %q", toolsResult.Title, "Required tools")
|
|
}
|
|
if want := "- dmidecode: OK (/usr/bin/dmidecode)"; !contains(toolsResult.Body, want) {
|
|
t.Fatalf("body missing %q\nbody=%s", want, toolsResult.Body)
|
|
}
|
|
if want := "- smartctl: MISSING"; !contains(toolsResult.Body, want) {
|
|
t.Fatalf("body missing %q\nbody=%s", want, toolsResult.Body)
|
|
}
|
|
|
|
logResult := a.AuditLogTailResult()
|
|
if logResult.Title != "Audit log tail" {
|
|
t.Fatalf("title=%q want %q", logResult.Title, "Audit log tail")
|
|
}
|
|
if want := DefaultAuditLogPath + "\n\n" + DefaultAuditJSONPath; logResult.Body != want {
|
|
t.Fatalf("body=%q want %q", logResult.Body, want)
|
|
}
|
|
}
|
|
|
|
func TestActionResultsUseFallbackBody(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
a := &App{
|
|
network: fakeNetwork{
|
|
dhcpOneFn: func(string) (string, error) { return " ", nil },
|
|
dhcpAllFn: func() (string, error) { return "", nil },
|
|
setStaticIPv4Fn: func(platform.StaticIPv4Config) (string, error) { return "", nil },
|
|
listInterfacesFn: func() ([]platform.InterfaceInfo, error) {
|
|
return nil, nil
|
|
},
|
|
defaultRouteFn: func() string { return "" },
|
|
},
|
|
services: fakeServices{
|
|
serviceStatusFn: func(string) (string, error) { return "", nil },
|
|
serviceDoFn: func(string, platform.ServiceAction) (string, error) { return "", nil },
|
|
},
|
|
tools: fakeTools{
|
|
tailFileFn: func(string, int) string { return " " },
|
|
checkToolsFn: func([]string) []platform.ToolStatus { return nil },
|
|
},
|
|
sat: fakeSAT{
|
|
runNvidiaFn: func(string) (string, error) { return "", nil },
|
|
runMemoryFn: func(string) (string, error) { return "", nil },
|
|
runStorageFn: func(string) (string, error) { return "", nil },
|
|
},
|
|
}
|
|
|
|
if got, _ := a.DHCPOneResult("eth0"); got.Body != "DHCP completed." {
|
|
t.Fatalf("dhcp one body=%q", got.Body)
|
|
}
|
|
if got, _ := a.DHCPAllResult(); got.Body != "DHCP completed." {
|
|
t.Fatalf("dhcp all body=%q", got.Body)
|
|
}
|
|
if got, _ := a.SetStaticIPv4Result(platform.StaticIPv4Config{Interface: "eth0"}); got.Body != "Static IPv4 updated." {
|
|
t.Fatalf("static body=%q", got.Body)
|
|
}
|
|
if got, _ := a.ServiceStatusResult("bee-audit"); got.Body != "No status output." {
|
|
t.Fatalf("status body=%q", got.Body)
|
|
}
|
|
if got, _ := a.ServiceActionResult("bee-audit", platform.ServiceRestart); got.Body != "Action completed." {
|
|
t.Fatalf("action body=%q", got.Body)
|
|
}
|
|
if got := a.ToolCheckResult(nil); got.Body != "No tools checked." {
|
|
t.Fatalf("tool body=%q", got.Body)
|
|
}
|
|
if got := a.AuditLogTailResult(); got.Body != "No audit logs found." {
|
|
t.Fatalf("log body=%q", got.Body)
|
|
}
|
|
if got, _ := a.RunNvidiaAcceptancePackResult(""); got.Body != "Archive written." {
|
|
t.Fatalf("sat body=%q", got.Body)
|
|
}
|
|
if got, _ := a.RunMemoryAcceptancePackResult(""); got.Body != "Archive written." {
|
|
t.Fatalf("memory sat body=%q", got.Body)
|
|
}
|
|
if got, _ := a.RunStorageAcceptancePackResult(""); got.Body != "Archive written." {
|
|
t.Fatalf("storage sat body=%q", got.Body)
|
|
}
|
|
}
|
|
|
|
func TestRunNvidiaAcceptancePackResult(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
a := &App{
|
|
sat: fakeSAT{
|
|
runNvidiaFn: func(baseDir string) (string, error) {
|
|
if baseDir != "/tmp/sat" {
|
|
t.Fatalf("baseDir=%q want %q", baseDir, "/tmp/sat")
|
|
}
|
|
return "/tmp/sat/out.tar.gz", nil
|
|
},
|
|
runMemoryFn: func(string) (string, error) { return "", nil },
|
|
runStorageFn: func(string) (string, error) { return "", nil },
|
|
},
|
|
}
|
|
|
|
result, err := a.RunNvidiaAcceptancePackResult("/tmp/sat")
|
|
if err != nil {
|
|
t.Fatalf("RunNvidiaAcceptancePackResult error: %v", err)
|
|
}
|
|
if result.Title != "NVIDIA SAT" || result.Body != "Archive written to /tmp/sat/out.tar.gz" {
|
|
t.Fatalf("unexpected result: %#v", result)
|
|
}
|
|
}
|
|
|
|
func TestFormatSATSummary(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
got := formatSATSummary("Memory SAT", "overall_status=PARTIAL\njob_ok=2\njob_failed=0\njob_unsupported=1\ndevices=3\n")
|
|
want := "Memory SAT: PARTIAL ok=2 failed=0 unsupported=1\nDevices: 3"
|
|
if got != want {
|
|
t.Fatalf("got %q want %q", got, want)
|
|
}
|
|
}
|
|
|
|
func TestHealthSummaryResultIncludesCompactSATSummary(t *testing.T) {
|
|
tmp := t.TempDir()
|
|
oldAuditPath := DefaultAuditJSONPath
|
|
oldSATBaseDir := DefaultSATBaseDir
|
|
DefaultAuditJSONPath = filepath.Join(tmp, "audit.json")
|
|
DefaultSATBaseDir = filepath.Join(tmp, "sat")
|
|
t.Cleanup(func() { DefaultAuditJSONPath = oldAuditPath })
|
|
t.Cleanup(func() { DefaultSATBaseDir = oldSATBaseDir })
|
|
|
|
satDir := filepath.Join(DefaultSATBaseDir, "memory-testcase")
|
|
if err := os.MkdirAll(satDir, 0755); err != nil {
|
|
t.Fatalf("mkdir sat dir: %v", err)
|
|
}
|
|
|
|
raw := `{"collected_at":"2026-03-15T10:00:00Z","hardware":{"board":{"serial_number":"SRV123"},"storage":[{"serial_number":"DISK1","status":"Warning"}]}}`
|
|
if err := os.WriteFile(DefaultAuditJSONPath, []byte(raw), 0644); err != nil {
|
|
t.Fatalf("write audit json: %v", err)
|
|
}
|
|
if err := os.WriteFile(filepath.Join(satDir, "summary.txt"), []byte("overall_status=OK\njob_ok=3\njob_failed=0\njob_unsupported=0\n"), 0644); err != nil {
|
|
t.Fatalf("write sat summary: %v", err)
|
|
}
|
|
|
|
result := (&App{}).HealthSummaryResult()
|
|
if !contains(result.Body, "Memory SAT: OK ok=3 failed=0") {
|
|
t.Fatalf("body missing compact sat summary:\n%s", result.Body)
|
|
}
|
|
}
|
|
|
|
func TestMainBanner(t *testing.T) {
|
|
tmp := t.TempDir()
|
|
oldAuditPath := DefaultAuditJSONPath
|
|
DefaultAuditJSONPath = filepath.Join(tmp, "audit.json")
|
|
t.Cleanup(func() { DefaultAuditJSONPath = oldAuditPath })
|
|
|
|
trueValue := true
|
|
manufacturer := "Dell"
|
|
product := "PowerEdge R760"
|
|
cpuModel := "Intel Xeon Gold 6430"
|
|
memoryType := "DDR5"
|
|
gpuClass := "VideoController"
|
|
gpuModel := "NVIDIA H100"
|
|
|
|
payload := schema.HardwareIngestRequest{
|
|
Hardware: schema.HardwareSnapshot{
|
|
Board: schema.HardwareBoard{
|
|
Manufacturer: &manufacturer,
|
|
ProductName: &product,
|
|
SerialNumber: "SRV123",
|
|
},
|
|
CPUs: []schema.HardwareCPU{
|
|
{Model: &cpuModel},
|
|
{Model: &cpuModel},
|
|
},
|
|
Memory: []schema.HardwareMemory{
|
|
{Present: &trueValue, SizeMB: intPtr(524288), Type: &memoryType},
|
|
{Present: &trueValue, SizeMB: intPtr(524288), Type: &memoryType},
|
|
},
|
|
Storage: []schema.HardwareStorage{
|
|
{Present: &trueValue, SizeGB: intPtr(3840)},
|
|
{Present: &trueValue, SizeGB: intPtr(3840)},
|
|
},
|
|
PCIeDevices: []schema.HardwarePCIeDevice{
|
|
{DeviceClass: &gpuClass, Model: &gpuModel},
|
|
{DeviceClass: &gpuClass, Model: &gpuModel},
|
|
},
|
|
},
|
|
}
|
|
|
|
raw, err := json.Marshal(payload)
|
|
if err != nil {
|
|
t.Fatalf("marshal: %v", err)
|
|
}
|
|
if err := os.WriteFile(DefaultAuditJSONPath, raw, 0644); err != nil {
|
|
t.Fatalf("write audit json: %v", err)
|
|
}
|
|
|
|
a := &App{
|
|
network: fakeNetwork{
|
|
listInterfacesFn: func() ([]platform.InterfaceInfo, error) {
|
|
return []platform.InterfaceInfo{
|
|
{Name: "eth0", IPv4: []string{"10.0.0.10"}},
|
|
{Name: "eth1", IPv4: []string{"192.168.1.10"}},
|
|
}, nil
|
|
},
|
|
},
|
|
}
|
|
|
|
got := a.MainBanner()
|
|
for _, want := range []string{
|
|
"System: Dell PowerEdge R760 | S/N SRV123",
|
|
"CPU: 2 x Intel Xeon Gold 6430",
|
|
"Memory: 1.0 TB DDR5 (2 DIMMs)",
|
|
"Storage: 2 drives / 7.5 TB",
|
|
"GPU: 2 x NVIDIA H100",
|
|
"IP: 10.0.0.10, 192.168.1.10",
|
|
} {
|
|
if !contains(got, want) {
|
|
t.Fatalf("banner missing %q:\n%s", want, got)
|
|
}
|
|
}
|
|
}
|
|
|
|
func intPtr(v int) *int { return &v }
|
|
|
|
func contains(haystack, needle string) bool {
|
|
return len(needle) == 0 || (len(haystack) >= len(needle) && (haystack == needle || containsAt(haystack, needle)))
|
|
}
|
|
|
|
func containsAt(haystack, needle string) bool {
|
|
for i := 0; i+len(needle) <= len(haystack); i++ {
|
|
if haystack[i:i+len(needle)] == needle {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|