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
+80 -1
View File
@@ -3,6 +3,7 @@ package easy_bee
import (
"encoding/json"
"fmt"
"strconv"
"strings"
"time"
@@ -122,10 +123,14 @@ func (p *Parser) Parse(files []parser.ExtractedFile) (*models.AnalysisResult, er
GPUs: append([]models.GPU(nil), snapshot.Hardware.GPUs...),
NetworkCards: append([]models.NIC(nil), snapshot.Hardware.NetworkCards...),
NetworkAdapters: normalizeNetworkAdapters(snapshot.Hardware.NetworkAdapters),
PowerSupply: append([]models.PSU(nil), snapshot.Hardware.PowerSupply...),
PowerSupply: normalizePSUSlots(snapshot.Hardware.PowerSupply),
},
}
if pn := chassisProductPartNumber(files, result.Hardware.BoardInfo.ProductName); pn != "" {
result.Hardware.BoardInfo.PartNumber = pn
}
result.Events = append(result.Events, snapshot.Events...)
result.Events = append(result.Events, convertRuntimeToEvents(snapshot.Runtime, result.CollectedAt)...)
result.Events = append(result.Events, convertEventLogs(snapshot.Hardware.EventLogs)...)
@@ -524,6 +529,80 @@ func normalizePCIeDevices(items []models.PCIeDevice) []models.PCIeDevice {
return out
}
// normalizePSUSlots rebases 0-indexed PSU bay numbers to 1-indexed, matching the
// BMC/vendor convention (bays are labelled PSU1..PSUn on the chassis). The
// easy-bee snapshot numbers them from 0; without this, the same server imported
// from a BMC dump vs. a live-CD run would show every PSU as moved to a new bay.
// chassisProductPartNumber pulls the chassis-level "Product Part Number" out of
// the bundle's ipmitool-fru.txt for the FRU whose "Product Name" matches the
// board product name. The easy-bee snapshot records the baseboard's own product
// name ("BC15MBSF") as board.part_number, while a BMC dump reports the chassis
// product part number ("0619KUGG") — this aligns easy-bee to the BMC value.
func chassisProductPartNumber(files []parser.ExtractedFile, boardProductName string) string {
boardProductName = strings.TrimSpace(boardProductName)
if boardProductName == "" {
return ""
}
var fru *parser.ExtractedFile
for i := range files {
if strings.HasSuffix(strings.ToLower(files[i].Path), "ipmitool-fru.txt") {
fru = &files[i]
break
}
}
if fru == nil {
return ""
}
var lastProductName string
for _, line := range strings.Split(string(fru.Content), "\n") {
key, value, ok := strings.Cut(line, ":")
if !ok {
continue
}
key = strings.TrimSpace(key)
value = strings.TrimSpace(value)
switch key {
case "Product Name":
lastProductName = value
case "Product Part Number":
if strings.EqualFold(lastProductName, boardProductName) {
return value
}
}
}
return ""
}
func normalizePSUSlots(items []models.PSU) []models.PSU {
out := append([]models.PSU(nil), items...)
for i := range out {
// The easy-bee snapshot leaks the FRU "Product Version" (a bare letter like
// "B") into the firmware field; that is not a firmware revision.
if fw := strings.TrimSpace(out[i].Firmware); len(fw) == 1 {
out[i].Firmware = ""
}
}
minSlot, allNumeric := -1, len(out) > 0
for _, p := range out {
n, err := strconv.Atoi(strings.TrimSpace(p.Slot))
if err != nil {
allNumeric = false
break
}
if minSlot == -1 || n < minSlot {
minSlot = n
}
}
if !allNumeric || minSlot != 0 {
return out
}
for i := range out {
n, _ := strconv.Atoi(strings.TrimSpace(out[i].Slot))
out[i].Slot = strconv.Itoa(n + 1)
}
return out
}
func normalizeNetworkAdapters(items []models.NetworkAdapter) []models.NetworkAdapter {
out := append([]models.NetworkAdapter(nil), items...)
for i := range out {