diff --git a/audit/internal/app/app.go b/audit/internal/app/app.go index 4be8033..99656c0 100644 --- a/audit/internal/app/app.go +++ b/audit/internal/app/app.go @@ -145,14 +145,23 @@ func New(platform *platform.System) *App { // ApplySATOverlay parses a raw audit JSON, overlays the latest SAT results, // and returns the updated JSON. Used by the web UI to serve always-fresh status. func ApplySATOverlay(auditJSON []byte) ([]byte, error) { - var snap schema.HardwareIngestRequest - if err := json.Unmarshal(auditJSON, &snap); err != nil { + snap, err := readAuditSnapshot(auditJSON) + if err != nil { return nil, err } applyLatestSATStatuses(&snap.Hardware, DefaultSATBaseDir) return json.MarshalIndent(snap, "", " ") } +func readAuditSnapshot(auditJSON []byte) (schema.HardwareIngestRequest, error) { + var snap schema.HardwareIngestRequest + if err := json.Unmarshal(auditJSON, &snap); err != nil { + return schema.HardwareIngestRequest{}, err + } + collector.NormalizeSnapshot(&snap.Hardware, snap.CollectedAt) + return snap, nil +} + func (a *App) RunAudit(runtimeMode runtimeenv.Mode, output string) (string, error) { if runtimeMode == runtimeenv.ModeLiveCD { if err := a.runtime.CaptureTechnicalDump(DefaultTechDumpDir); err != nil { @@ -276,6 +285,9 @@ func (a *App) ExportLatestAudit(target platform.RemovableTarget) (string, error) if err != nil { return "", err } + if normalized, normErr := ApplySATOverlay(data); normErr == nil { + data = normalized + } if err := os.WriteFile(tmpPath, data, 0644); err != nil { return "", err } @@ -733,6 +745,7 @@ func (a *App) HealthSummaryResult() ActionResult { if err := json.Unmarshal(raw, &snapshot); err != nil { return ActionResult{Title: "Health summary", Body: "Audit JSON is unreadable."} } + collector.NormalizeSnapshot(&snapshot.Hardware, snapshot.CollectedAt) summary := collector.BuildHealthSummary(snapshot.Hardware) var body strings.Builder @@ -767,6 +780,7 @@ func (a *App) MainBanner() string { if err := json.Unmarshal(raw, &snapshot); err != nil { return "" } + collector.NormalizeSnapshot(&snapshot.Hardware, snapshot.CollectedAt) var lines []string if system := formatSystemLine(snapshot.Hardware.Board); system != "" { diff --git a/audit/internal/app/app_test.go b/audit/internal/app/app_test.go index 15e8aa1..cfd1260 100644 --- a/audit/internal/app/app_test.go +++ b/audit/internal/app/app_test.go @@ -660,13 +660,50 @@ func TestHealthSummaryResultIncludesCompactSATSummary(t *testing.T) { } } +func TestApplySATOverlayFiltersIgnoredLegacyDevices(t *testing.T) { + tmp := t.TempDir() + oldSATBaseDir := DefaultSATBaseDir + DefaultSATBaseDir = filepath.Join(tmp, "sat") + t.Cleanup(func() { DefaultSATBaseDir = oldSATBaseDir }) + + raw := `{ + "collected_at": "2026-03-15T10:00:00Z", + "hardware": { + "board": {"serial_number": "SRV123"}, + "storage": [ + {"model": "Virtual HDisk0", "serial_number": "AAAABBBBCCCC3"}, + {"model": "PASCARI", "serial_number": "DISK1", "status": "OK"} + ], + "pcie_devices": [ + {"device_class": "Co-processor", "model": "402xx Series QAT", "status": "OK"}, + {"device_class": "VideoController", "model": "NVIDIA H100", "status": "OK"} + ] + } + }` + + got, err := ApplySATOverlay([]byte(raw)) + if err != nil { + t.Fatalf("ApplySATOverlay error: %v", err) + } + text := string(got) + if contains(text, "Virtual HDisk0") { + t.Fatalf("overlaid audit should drop virtual hdisk:\n%s", text) + } + if contains(text, "\"device_class\": \"Co-processor\"") { + t.Fatalf("overlaid audit should drop co-processors:\n%s", text) + } + if !contains(text, "PASCARI") || !contains(text, "NVIDIA H100") { + t.Fatalf("overlaid audit should keep real devices:\n%s", text) + } +} + func TestBuildSupportBundleIncludesExportDirContents(t *testing.T) { tmp := t.TempDir() exportDir := filepath.Join(tmp, "export") if err := os.MkdirAll(filepath.Join(exportDir, "bee-sat", "memory-run"), 0755); err != nil { t.Fatal(err) } - if err := os.WriteFile(filepath.Join(exportDir, "bee-audit.json"), []byte(`{"ok":true}`), 0644); err != nil { + if err := os.WriteFile(filepath.Join(exportDir, "bee-audit.json"), []byte(`{"collected_at":"2026-03-15T10:00:00Z","hardware":{"board":{"serial_number":"SRV123"},"storage":[{"model":"Virtual HDisk0","serial_number":"AAAABBBBCCCC3"},{"model":"PASCARI","serial_number":"DISK1"}],"pcie_devices":[{"device_class":"Co-processor","model":"402xx Series QAT"},{"device_class":"VideoController","model":"NVIDIA H100"}]}}`), 0644); err != nil { t.Fatal(err) } if err := os.WriteFile(filepath.Join(exportDir, "bee-sat", "memory-run", "verbose.log"), []byte("sat verbose"), 0644); err != nil { @@ -698,6 +735,7 @@ func TestBuildSupportBundleIncludesExportDirContents(t *testing.T) { tr := tar.NewReader(gzr) var names []string + var auditJSON string for { hdr, err := tr.Next() if errors.Is(err, io.EOF) { @@ -707,6 +745,13 @@ func TestBuildSupportBundleIncludesExportDirContents(t *testing.T) { t.Fatalf("read tar entry: %v", err) } names = append(names, hdr.Name) + if contains(hdr.Name, "/export/bee-audit.json") { + body, err := io.ReadAll(tr) + if err != nil { + t.Fatalf("read audit entry: %v", err) + } + auditJSON = string(body) + } } var foundRaw bool @@ -721,6 +766,12 @@ func TestBuildSupportBundleIncludesExportDirContents(t *testing.T) { if !foundRaw { t.Fatalf("support bundle missing raw SAT log, names=%v", names) } + if contains(auditJSON, "Virtual HDisk0") || contains(auditJSON, "\"device_class\": \"Co-processor\"") { + t.Fatalf("support bundle should normalize ignored devices:\n%s", auditJSON) + } + if !contains(auditJSON, "PASCARI") || !contains(auditJSON, "NVIDIA H100") { + t.Fatalf("support bundle should keep real devices:\n%s", auditJSON) + } } func TestMainBanner(t *testing.T) { @@ -734,6 +785,10 @@ func TestMainBanner(t *testing.T) { product := "PowerEdge R760" cpuModel := "Intel Xeon Gold 6430" memoryType := "DDR5" + memorySerialA := "DIMM-A" + memorySerialB := "DIMM-B" + storageSerialA := "DISK-A" + storageSerialB := "DISK-B" gpuClass := "VideoController" gpuModel := "NVIDIA H100" @@ -749,12 +804,12 @@ func TestMainBanner(t *testing.T) { {Model: &cpuModel}, }, Memory: []schema.HardwareMemory{ - {Present: &trueValue, SizeMB: intPtr(524288), Type: &memoryType}, - {Present: &trueValue, SizeMB: intPtr(524288), Type: &memoryType}, + {Present: &trueValue, SizeMB: intPtr(524288), Type: &memoryType, SerialNumber: &memorySerialA}, + {Present: &trueValue, SizeMB: intPtr(524288), Type: &memoryType, SerialNumber: &memorySerialB}, }, Storage: []schema.HardwareStorage{ - {Present: &trueValue, SizeGB: intPtr(3840)}, - {Present: &trueValue, SizeGB: intPtr(3840)}, + {Present: &trueValue, SizeGB: intPtr(3840), SerialNumber: &storageSerialA}, + {Present: &trueValue, SizeGB: intPtr(3840), SerialNumber: &storageSerialB}, }, PCIeDevices: []schema.HardwarePCIeDevice{ {DeviceClass: &gpuClass, Model: &gpuModel}, diff --git a/audit/internal/app/support_bundle.go b/audit/internal/app/support_bundle.go index 12285dd..e942d86 100644 --- a/audit/internal/app/support_bundle.go +++ b/audit/internal/app/support_bundle.go @@ -247,7 +247,7 @@ func copyDirContents(srcDir, dstDir string) error { } func copyExportDirForSupportBundle(srcDir, dstDir string) error { - return copyDirContentsFiltered(srcDir, dstDir, func(rel string, info os.FileInfo) bool { + if err := copyDirContentsFiltered(srcDir, dstDir, func(rel string, info os.FileInfo) bool { cleanRel := filepath.ToSlash(strings.TrimPrefix(filepath.Clean(rel), "./")) if cleanRel == "" { return true @@ -259,7 +259,25 @@ func copyExportDirForSupportBundle(srcDir, dstDir string) error { return false } return true - }) + }); err != nil { + return err + } + return normalizeSupportBundleAuditJSON(filepath.Join(dstDir, "bee-audit.json")) +} + +func normalizeSupportBundleAuditJSON(path string) error { + data, err := os.ReadFile(path) + if err != nil { + if os.IsNotExist(err) { + return nil + } + return err + } + normalized, err := ApplySATOverlay(data) + if err != nil { + return nil + } + return os.WriteFile(path, normalized, 0644) } func copyDirContentsFiltered(srcDir, dstDir string, keep func(rel string, info os.FileInfo) bool) error { diff --git a/audit/internal/collector/finalize.go b/audit/internal/collector/finalize.go index 9d1353a..b9920ed 100644 --- a/audit/internal/collector/finalize.go +++ b/audit/internal/collector/finalize.go @@ -1,10 +1,18 @@ package collector -import "bee/audit/internal/schema" +import ( + "bee/audit/internal/schema" + "strings" +) + +func NormalizeSnapshot(snap *schema.HardwareSnapshot, collectedAt string) { + finalizeSnapshot(snap, collectedAt) +} func finalizeSnapshot(snap *schema.HardwareSnapshot, collectedAt string) { snap.Memory = filterMemory(snap.Memory) snap.Storage = filterStorage(snap.Storage) + snap.PCIeDevices = filterPCIe(snap.PCIeDevices) snap.PowerSupplies = filterPSUs(snap.PowerSupplies) setComponentStatusMetadata(snap, collectedAt) @@ -33,11 +41,25 @@ func filterStorage(disks []schema.HardwareStorage) []schema.HardwareStorage { if disk.SerialNumber == nil || *disk.SerialNumber == "" { continue } + if disk.Model != nil && isVirtualHDiskModel(*disk.Model) { + continue + } out = append(out, disk) } return out } +func filterPCIe(devs []schema.HardwarePCIeDevice) []schema.HardwarePCIeDevice { + out := make([]schema.HardwarePCIeDevice, 0, len(devs)) + for _, dev := range devs { + if dev.DeviceClass != nil && strings.Contains(strings.ToLower(strings.TrimSpace(*dev.DeviceClass)), "co-processor") { + continue + } + out = append(out, dev) + } + return out +} + func filterPSUs(psus []schema.HardwarePowerSupply) []schema.HardwarePowerSupply { out := make([]schema.HardwarePowerSupply, 0, len(psus)) for _, psu := range psus { diff --git a/audit/internal/collector/finalize_test.go b/audit/internal/collector/finalize_test.go index 2a5a156..c20fd04 100644 --- a/audit/internal/collector/finalize_test.go +++ b/audit/internal/collector/finalize_test.go @@ -10,6 +10,10 @@ func TestFinalizeSnapshotFiltersComponentsWithoutRequiredSerials(t *testing.T) { present := true status := statusOK serial := "SN-1" + virtualModel := "Virtual HDisk1" + realModel := "PASCARI" + coProcessorClass := "Co-processor" + gpuClass := "VideoController" snap := schema.HardwareSnapshot{ Memory: []schema.HardwareMemory{ @@ -17,9 +21,15 @@ func TestFinalizeSnapshotFiltersComponentsWithoutRequiredSerials(t *testing.T) { {Present: &present, HardwareComponentStatus: schema.HardwareComponentStatus{Status: &status}}, }, Storage: []schema.HardwareStorage{ + {Model: &virtualModel, SerialNumber: &serial, HardwareComponentStatus: schema.HardwareComponentStatus{Status: &status}}, {SerialNumber: &serial, HardwareComponentStatus: schema.HardwareComponentStatus{Status: &status}}, + {Model: &realModel, SerialNumber: &serial, HardwareComponentStatus: schema.HardwareComponentStatus{Status: &status}}, {HardwareComponentStatus: schema.HardwareComponentStatus{Status: &status}}, }, + PCIeDevices: []schema.HardwarePCIeDevice{ + {DeviceClass: &coProcessorClass, HardwareComponentStatus: schema.HardwareComponentStatus{Status: &status}}, + {DeviceClass: &gpuClass, HardwareComponentStatus: schema.HardwareComponentStatus{Status: &status}}, + }, PowerSupplies: []schema.HardwarePowerSupply{ {SerialNumber: &serial, HardwareComponentStatus: schema.HardwareComponentStatus{Status: &status}}, {HardwareComponentStatus: schema.HardwareComponentStatus{Status: &status}}, @@ -31,9 +41,12 @@ func TestFinalizeSnapshotFiltersComponentsWithoutRequiredSerials(t *testing.T) { if len(snap.Memory) != 1 || snap.Memory[0].StatusCheckedAt == nil || *snap.Memory[0].StatusCheckedAt != collectedAt { t.Fatalf("memory finalize mismatch: %+v", snap.Memory) } - if len(snap.Storage) != 1 || snap.Storage[0].StatusCheckedAt == nil || *snap.Storage[0].StatusCheckedAt != collectedAt { + if len(snap.Storage) != 2 || snap.Storage[0].StatusCheckedAt == nil || *snap.Storage[0].StatusCheckedAt != collectedAt { t.Fatalf("storage finalize mismatch: %+v", snap.Storage) } + if len(snap.PCIeDevices) != 1 || snap.PCIeDevices[0].DeviceClass == nil || *snap.PCIeDevices[0].DeviceClass != gpuClass { + t.Fatalf("pcie finalize mismatch: %+v", snap.PCIeDevices) + } if len(snap.PowerSupplies) != 1 || snap.PowerSupplies[0].StatusCheckedAt == nil || *snap.PowerSupplies[0].StatusCheckedAt != collectedAt { t.Fatalf("psu finalize mismatch: %+v", snap.PowerSupplies) } diff --git a/audit/internal/collector/pcie.go b/audit/internal/collector/pcie.go index 69e04ee..6d91db8 100644 --- a/audit/internal/collector/pcie.go +++ b/audit/internal/collector/pcie.go @@ -59,6 +59,7 @@ func shouldIncludePCIeDevice(class, vendor, device string) bool { "host bridge", "isa bridge", "pci bridge", + "co-processor", "performance counter", "performance counters", "ram memory", diff --git a/audit/internal/collector/pcie_filter_test.go b/audit/internal/collector/pcie_filter_test.go index b32b7c2..8be8b02 100644 --- a/audit/internal/collector/pcie_filter_test.go +++ b/audit/internal/collector/pcie_filter_test.go @@ -19,6 +19,7 @@ func TestShouldIncludePCIeDevice(t *testing.T) { {name: "audio", class: "Audio device", want: false}, {name: "host bridge", class: "Host bridge", want: false}, {name: "pci bridge", class: "PCI bridge", want: false}, + {name: "co-processor", class: "Co-processor", want: false}, {name: "smbus", class: "SMBus", want: false}, {name: "perf", class: "Performance counters", want: false}, {name: "non essential instrumentation", class: "Non-Essential Instrumentation", want: false}, @@ -76,6 +77,20 @@ func TestParseLspci_filtersAMDChipsetNoise(t *testing.T) { } } +func TestParseLspci_filtersCoProcessors(t *testing.T) { + input := "" + + "Slot:\t0000:01:00.0\nClass:\tCo-processor\nVendor:\tIntel Corporation\nDevice:\t402xx Series QAT\n\n" + + "Slot:\t0000:65:00.0\nClass:\tVGA compatible controller\nVendor:\tNVIDIA Corporation\nDevice:\tH100\n\n" + + devs := parseLspci(input) + if len(devs) != 1 { + t.Fatalf("expected 1 remaining device, got %d", len(devs)) + } + if devs[0].Model == nil || *devs[0].Model != "H100" { + t.Fatalf("unexpected remaining device: %+v", devs[0]) + } +} + func TestPCIeJSONUsesSlotNotBDF(t *testing.T) { input := "Slot:\t0000:65:00.0\nClass:\tVGA compatible controller\nVendor:\tNVIDIA Corporation\nDevice:\tH100\n\n" diff --git a/audit/internal/collector/storage.go b/audit/internal/collector/storage.go index f577d9d..7e371ab 100644 --- a/audit/internal/collector/storage.go +++ b/audit/internal/collector/storage.go @@ -91,7 +91,11 @@ func discoverStorageDevices() []lsblkDevice { // These have zero reported size, a generic fake serial, and a model name that // starts with "Virtual HDisk". func isVirtualBMCDisk(dev lsblkDevice) bool { - model := strings.ToLower(strings.TrimSpace(dev.Model)) + return isVirtualHDiskModel(dev.Model) +} + +func isVirtualHDiskModel(model string) bool { + model = strings.ToLower(strings.TrimSpace(model)) return strings.HasPrefix(model, "virtual hdisk") }