diff --git a/audit/internal/app/app.go b/audit/internal/app/app.go index 4013f7f..2d4d7ab 100644 --- a/audit/internal/app/app.go +++ b/audit/internal/app/app.go @@ -237,10 +237,12 @@ func readAuditSnapshot(auditJSON []byte) (schema.HardwareIngestRequest, error) { } func (a *App) RunAudit(runtimeMode runtimeenv.Mode, output string) (string, error) { - if runtimeMode == runtimeenv.ModeLiveCD { - if err := a.runtime.CaptureTechnicalDump(DefaultTechDumpDir); err != nil { - slog.Warn("capture technical dump", "err", err) - } + // Capture the raw techdump on every audit, not just LiveCD: it's the only + // place IPMI FRU / Elabel (server model), mc info and LAN config land, and + // downstream views (webui /topo, support bundle) expect it fresh per audit + // cycle. Best-effort — a missing tool or read-only path only warns. + if err := a.runtime.CaptureTechnicalDump(DefaultTechDumpDir); err != nil { + slog.Warn("capture technical dump", "err", err) } result := collector.Run(runtimeMode) applyLatestSATStatuses(&result.Hardware, DefaultSATBaseDir, a.StatusDB) diff --git a/audit/internal/app/app_test.go b/audit/internal/app/app_test.go index 1f10789..a1df766 100644 --- a/audit/internal/app/app_test.go +++ b/audit/internal/app/app_test.go @@ -13,9 +13,35 @@ import ( "testing" "bee/audit/internal/platform" + "bee/audit/internal/runtimeenv" "bee/audit/internal/schema" ) +// TestRunAuditAlwaysCapturesTechnicalDump locks in that the raw techdump — +// the only place IPMI FRU / Elabel, mc info and LAN config are captured — is +// taken on every audit, not just under LiveCD as it used to be. +func TestRunAuditAlwaysCapturesTechnicalDump(t *testing.T) { + for _, mode := range []runtimeenv.Mode{runtimeenv.ModeLocal, runtimeenv.ModeLiveCD, runtimeenv.ModeAuto} { + mode := mode + t.Run(string(mode), func(t *testing.T) { + var dumped string + a := &App{ + runtime: fakeRuntime{ + collectFn: func(string) (schema.RuntimeHealth, error) { return schema.RuntimeHealth{}, nil }, + dumpFn: func(dir string) error { dumped = dir; return nil }, + }, + } + out := filepath.Join(t.TempDir(), "audit.json") + if _, err := a.RunAudit(mode, "file:"+out); err != nil { + t.Fatalf("RunAudit(%s): %v", mode, err) + } + if dumped == "" { + t.Fatalf("RunAudit(%s) skipped the technical dump", mode) + } + }) + } +} + type fakeNetwork struct { listInterfacesFn func() ([]platform.InterfaceInfo, error) defaultRouteFn func() string diff --git a/audit/internal/platform/techdump.go b/audit/internal/platform/techdump.go index eed3d3e..2acc593 100644 --- a/audit/internal/platform/techdump.go +++ b/audit/internal/platform/techdump.go @@ -25,7 +25,14 @@ var techDumpFixedCommands = []struct { {Name: "lsblk", Args: []string{"-J", "-d", "-o", "NAME,TYPE,SIZE,SERIAL,MODEL,TRAN,HCTL"}, File: "lsblk.json"}, {Name: "sh", Args: []string{"-c", StorageControllerMapScript}, File: "storage-controllers.txt"}, {Name: "sensors", Args: []string{"-j"}, File: "sensors.json"}, - {Name: "ipmitool", Args: []string{"fru", "print"}, File: "ipmitool-fru.txt"}, + // FRU / Elabel: the identity fields the Export Tools "FRU / Elabel" editor + // reads and rewrites (Product Name / server model, serial, part numbers). + // "fru print 0" pins FRU device 0 to match that editor exactly. + {Name: "ipmitool", Args: []string{"fru", "print", "0"}, File: "ipmitool-fru.txt"}, + // BMC identity + LAN config, so a support bundle carries the same context + // the FRU editor shows alongside the elabel. + {Name: "ipmitool", Args: []string{"mc", "info"}, File: "ipmitool-mc-info.txt"}, + {Name: "ipmitool", Args: []string{"lan", "print"}, File: "ipmitool-lan-print.txt"}, {Name: "ipmitool", Args: []string{"sdr"}, File: "ipmitool-sdr.txt"}, {Name: "ipmitool", Args: []string{"sensor"}, File: "ipmitool-sensor.txt"}, {Name: "ipmitool", Args: []string{"sel", "list"}, File: "ipmitool-sel.txt"}, diff --git a/audit/internal/platform/techdump_test.go b/audit/internal/platform/techdump_test.go index d8bb4a4..e618c18 100644 --- a/audit/internal/platform/techdump_test.go +++ b/audit/internal/platform/techdump_test.go @@ -7,6 +7,27 @@ import ( "testing" ) +func TestTechDumpCapturesIPMIIdentity(t *testing.T) { + t.Parallel() + + want := map[string][]string{ + "ipmitool-fru.txt": {"fru", "print", "0"}, + "ipmitool-mc-info.txt": {"mc", "info"}, + "ipmitool-lan-print.txt": {"lan", "print"}, + } + for _, cmd := range techDumpFixedCommands { + if args, ok := want[cmd.File]; ok { + if cmd.Name != "ipmitool" || !reflect.DeepEqual(cmd.Args, args) { + t.Fatalf("%s: got %s %v, want ipmitool %v", cmd.File, cmd.Name, cmd.Args, args) + } + delete(want, cmd.File) + } + } + if len(want) != 0 { + t.Fatalf("techdump missing IPMI identity captures: %v", want) + } +} + func TestLSBLKDumpDevices(t *testing.T) { t.Parallel()