fix(parser,exporter): reconcile BMC-dump and live-CD exports of the same server

The audit tool that ingests Reanimator exports treats any per-component field
change between imports as a component replacement. Importing an xFusion BMC dump
and an easy_bee BEE-SP bundle for one server produced large spurious diffs.

xfusion:
- parseMemInfo tolerates a stray 0x0A inside the binary SPD "bom number" column
  (it was splitting a DIMM record in two and emitting a phantom "slot s" module)
- DIMM slot from the "dimm name" column ("DIMM071"), mainboard "location" dropped
- GPU slot = BDF (Reanimator contract)
- NIC emitted per PCI function from netcard_info.txt (BDF + per-port MAC, shared
  card serial) instead of one card-level adapter with the wrong BDF, which had
  been colliding with a GPU and vanishing in dedup
- NIC manufacturer left blank when it is the system OEM so the exporter resolves
  the silicon vendor from pci.ids
- "(U6216)" chip designator stripped from firmware versions

easy_bee:
- PSU bay numbers rebased 0-indexed -> 1-indexed; bare single-letter PSU
  "firmware" (a leaked FRU version) cleared
- board.part_number taken from the bundle's ipmitool-fru.txt chassis
  "Product Part Number" to match the BMC value

exporter (cross-vendor):
- canonicalMemorySlot: drop dmidecode "(J)" channel tag, Memory111 -> DIMM111
- canonicalGPUModel: NVIDIA DC GPUs reduce to the bare chip token
- canonicalStorageMediaAndInterface: NVMe is a bus not a medium
- manufacturerFromStorageModel: fill blank drive vendor from the model prefix
- isRemovableUSBStorageDevice: drop live-CD boot sticks
- isOnboardControllerPCIeDevice: drop SATA/NVMe/MegaRAID/PCIe-switch controller
  functions that only an lspci scan reports (keep add-in cards with an identity)

For the reference server every physical component now appears in both exports
keyed identically; residual diffs are one-sided enrichment only.

Refs ADL-061, ADL-062.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VFy4m7cVv4cqp25jJh2gSB
This commit is contained in:
Mikhail Chusavitin
2026-08-31 22:50:07 +03:00
co-authored by Claude Sonnet 5
parent 5b65c99b0e
commit 9b2c654182
7 changed files with 595 additions and 61 deletions
+50 -16
View File
@@ -207,32 +207,37 @@ Product Name: G5500 V7
if result.Hardware == nil {
t.Fatal("Hardware is nil")
}
if len(result.Hardware.NetworkAdapters) != 1 {
t.Fatalf("expected 1 network adapter, got %d", len(result.Hardware.NetworkAdapters))
// One adapter per PCI function, keyed by that port's BDF, matching an
// OS-level lspci view. Ports share the card's serial and firmware.
if len(result.Hardware.NetworkAdapters) != 2 {
t.Fatalf("expected 2 network adapters (one per port), got %d", len(result.Hardware.NetworkAdapters))
}
adapter := result.Hardware.NetworkAdapters[0]
if adapter.BDF != "0000:27:00.0" {
t.Fatalf("expected network adapter BDF 0000:27:00.0, got %q", adapter.BDF)
byBDF := map[string]models.NetworkAdapter{}
for _, a := range result.Hardware.NetworkAdapters {
byBDF[a.BDF] = a
if a.Firmware != "26.39.2048" {
t.Fatalf("adapter %s firmware = %q, want 26.39.2048", a.BDF, a.Firmware)
}
if a.SerialNumber != "02Y238X6RC000058" {
t.Fatalf("adapter %s serial = %q, want 02Y238X6RC000058", a.BDF, a.SerialNumber)
}
}
if adapter.Firmware != "26.39.2048" {
t.Fatalf("expected network adapter firmware 26.39.2048, got %q", adapter.Firmware)
if got := byBDF["0000:27:00.0"].MACAddresses; len(got) != 1 || got[0] != "44:1A:4C:16:E8:03" {
t.Fatalf("port 0 MACs = %#v", got)
}
if adapter.SerialNumber != "02Y238X6RC000058" {
t.Fatalf("expected network adapter serial from card_info, got %q", adapter.SerialNumber)
}
if len(adapter.MACAddresses) != 2 || adapter.MACAddresses[0] != "44:1A:4C:16:E8:03" || adapter.MACAddresses[1] != "44:1A:4C:16:E8:04" {
t.Fatalf("unexpected MAC addresses: %#v", adapter.MACAddresses)
if got := byBDF["0000:27:00.1"].MACAddresses; len(got) != 1 || got[0] != "44:1A:4C:16:E8:04" {
t.Fatalf("port 1 MACs = %#v", got)
}
fwByDevice := make(map[string]models.FirmwareInfo)
for _, fw := range result.Hardware.Firmware {
fwByDevice[fw.DeviceName] = fw
}
if fwByDevice["iBMC"].Version != "(U68)3.08.05.85" {
t.Fatalf("expected iBMC firmware from app_revision.txt, got %#v", fwByDevice["iBMC"])
if fwByDevice["iBMC"].Version != "3.08.05.85" {
t.Fatalf("expected iBMC firmware (chip designator stripped) from app_revision.txt, got %#v", fwByDevice["iBMC"])
}
if fwByDevice["BIOS"].Version != "(U6216)01.02.08.17" {
t.Fatalf("expected BIOS firmware from app_revision.txt, got %#v", fwByDevice["BIOS"])
if fwByDevice["BIOS"].Version != "01.02.08.17" {
t.Fatalf("expected BIOS firmware (chip designator stripped) from app_revision.txt, got %#v", fwByDevice["BIOS"])
}
if result.Hardware.BoardInfo.ProductName != "G5500 V7" {
t.Fatalf("expected board product fallback from app_revision.txt, got %q", result.Hardware.BoardInfo.ProductName)
@@ -330,3 +335,32 @@ func TestParse_G5500V7_FRU(t *testing.T) {
t.Error("mainboard serial 210619KUGGXGS2000015 not found in FRU")
}
}
// TestParseMemInfo_EmbeddedNewlineInBOM guards against the BMC copying a raw SPD
// "bom number" field containing a 0x0A byte, which splits a DIMM record across
// two physical lines. The row must be rejoined, not turned into a phantom DIMM.
func TestParseMemInfo_EmbeddedNewlineInBOM(t *testing.T) {
const header = "slot(col 1), dimm location(col 2), dimm name(col 3),manufacturer(col 4), size(col 5), speed(col 6), current speed(col 7), memory type(col 8), SN(col 9), minimum voltage(col 10), rank(col 11), bit width(col 12), memory technology(col 13), bom number(col 14), part number(col 15),remaining service life(col 16), firmware version(col 17), medium temp(col 18), controller temp(col 19), volatile capacity(col 20), persistent capacity(col 21), health(col 22)\n"
good := "Memory160 , mainboard, DIMM160, Samsung, 65536 MB, 5600 MT/s, 4400 MT/s, DDR5, 13E79B2E, 1100 mV, 2 rank, 64 bit, Synchronous| Registered (Buffered), \x01\x02, M321R8GA0EB2-CWMXH, Unknown, N/A, Unknown, Unknown, Unknown, Unknown, OK\n"
// Memory170: BOM field carries a stray newline mid-record.
split := "Memory170 , mainboard, DIMM170, Samsung, 65536 MB, 5600 MT/s, 4400 MT/s, DDR5, 13E79D42, 1100 mV, 2 rank, 64 bit, Synchronous| Registered (Buffered), \x9d7h!\x8f\n" +
"s, M321R8GA0EB2-CWMXH, Unknown, N/A, Unknown, Unknown, Unknown, Unknown, OK\n"
dimms := parseMemInfo([]byte(header + good + split))
if len(dimms) != 2 {
t.Fatalf("expected 2 DIMMs, got %d: %+v", len(dimms), dimms)
}
m170 := dimms[1]
if m170.Slot != "DIMM170" {
t.Errorf("slot = %q, want DIMM170", m170.Slot)
}
if m170.SerialNumber != "13E79D42" {
t.Errorf("serial = %q, want 13E79D42", m170.SerialNumber)
}
if m170.PartNumber != "M321R8GA0EB2-CWMXH" {
t.Errorf("part number = %q, want M321R8GA0EB2-CWMXH", m170.PartNumber)
}
if m170.SizeMB != 65536 || m170.Type != "DDR5" || m170.CurrentSpeedMHz != 4400 {
t.Errorf("m170 fields not recovered: %+v", m170)
}
}