feat(audit): capture IPMI FRU/Elabel, mc info and LAN on every audit
The techdump — the only place the IPMI FRU / Elabel identity (Product Name = server model, serials, part numbers) is recorded — only ran under LiveCD. Run it on every audit so `bee audit` on an installed host captures it too. Also add `ipmitool mc info` and `ipmitool lan print` to the techdump, and pin `ipmitool fru print 0` to FRU device 0 to match the Export Tools "FRU / Elabel" editor exactly. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
319f08ac4b
commit
76db45ee95
@@ -237,10 +237,12 @@ func readAuditSnapshot(auditJSON []byte) (schema.HardwareIngestRequest, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (a *App) RunAudit(runtimeMode runtimeenv.Mode, output string) (string, error) {
|
func (a *App) RunAudit(runtimeMode runtimeenv.Mode, output string) (string, error) {
|
||||||
if runtimeMode == runtimeenv.ModeLiveCD {
|
// Capture the raw techdump on every audit, not just LiveCD: it's the only
|
||||||
if err := a.runtime.CaptureTechnicalDump(DefaultTechDumpDir); err != nil {
|
// place IPMI FRU / Elabel (server model), mc info and LAN config land, and
|
||||||
slog.Warn("capture technical dump", "err", err)
|
// 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)
|
result := collector.Run(runtimeMode)
|
||||||
applyLatestSATStatuses(&result.Hardware, DefaultSATBaseDir, a.StatusDB)
|
applyLatestSATStatuses(&result.Hardware, DefaultSATBaseDir, a.StatusDB)
|
||||||
|
|||||||
@@ -13,9 +13,35 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"bee/audit/internal/platform"
|
"bee/audit/internal/platform"
|
||||||
|
"bee/audit/internal/runtimeenv"
|
||||||
"bee/audit/internal/schema"
|
"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 {
|
type fakeNetwork struct {
|
||||||
listInterfacesFn func() ([]platform.InterfaceInfo, error)
|
listInterfacesFn func() ([]platform.InterfaceInfo, error)
|
||||||
defaultRouteFn func() string
|
defaultRouteFn func() string
|
||||||
|
|||||||
@@ -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: "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: "sh", Args: []string{"-c", StorageControllerMapScript}, File: "storage-controllers.txt"},
|
||||||
{Name: "sensors", Args: []string{"-j"}, File: "sensors.json"},
|
{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{"sdr"}, File: "ipmitool-sdr.txt"},
|
||||||
{Name: "ipmitool", Args: []string{"sensor"}, File: "ipmitool-sensor.txt"},
|
{Name: "ipmitool", Args: []string{"sensor"}, File: "ipmitool-sensor.txt"},
|
||||||
{Name: "ipmitool", Args: []string{"sel", "list"}, File: "ipmitool-sel.txt"},
|
{Name: "ipmitool", Args: []string{"sel", "list"}, File: "ipmitool-sel.txt"},
|
||||||
|
|||||||
@@ -7,6 +7,27 @@ import (
|
|||||||
"testing"
|
"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) {
|
func TestLSBLKDumpDevices(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user