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
+152 -4
View File
@@ -883,7 +883,7 @@ func convertMemoryFromDevices(devices []models.HardwareDevice, collectedAt strin
meta := buildStatusMeta(status, d.StatusCheckedAt, d.StatusChangedAt, d.StatusHistory, d.ErrorDescription, collectedAt)
presentValue := present
result = append(result, ReanimatorMemory{
Slot: d.Slot,
Slot: canonicalMemorySlot(d.Slot),
Location: d.Location,
Present: &presentValue,
SizeMB: d.SizeMB,
@@ -921,6 +921,9 @@ func convertStorageFromDevices(devices []models.HardwareDevice, collectedAt stri
if isVirtualExportStorageDevice(d) {
continue
}
if isRemovableUSBStorageDevice(d) {
continue
}
if !shouldExportStorageDevice(d) {
continue
}
@@ -931,17 +934,22 @@ func convertStorageFromDevices(devices []models.HardwareDevice, collectedAt stri
}
meta := buildStatusMeta(status, d.StatusCheckedAt, d.StatusChangedAt, d.StatusHistory, d.ErrorDescription, collectedAt)
presentValue := present
mediaType, busInterface := canonicalStorageMediaAndInterface(d.Type, d.Interface)
manufacturer := d.Manufacturer
if manufacturer == "" {
manufacturer = manufacturerFromStorageModel(d.Model)
}
result = append(result, ReanimatorStorage{
Slot: d.Slot,
Type: d.Type,
Type: mediaType,
Model: d.Model,
VendorID: d.VendorID,
DeviceID: d.DeviceID,
SizeGB: d.SizeGB,
SerialNumber: d.SerialNumber,
Manufacturer: d.Manufacturer,
Manufacturer: manufacturer,
Firmware: d.Firmware,
Interface: d.Interface,
Interface: busInterface,
Present: &presentValue,
LogicalBlockSizeBytes: int64FromDetailMap(d.Details, "logical_block_size_bytes"),
PhysicalBlockSizeBytes: int64FromDetailMap(d.Details, "physical_block_size_bytes"),
@@ -981,6 +989,9 @@ func convertPCIeFromDevices(devices []models.HardwareDevice, collectedAt string)
if isStorageEndpointPCIeDevice(d) {
continue
}
if isOnboardControllerPCIeDevice(d) {
continue
}
if isPlaceholderPCIeExportDevice(d) {
continue
}
@@ -1000,6 +1011,9 @@ func convertPCIeFromDevices(devices []models.HardwareDevice, collectedAt string)
if manufacturer == "" && d.VendorID != 0 {
manufacturer = pciids.VendorName(d.VendorID)
}
if isGPUClass(deviceClass) || d.Kind == models.DeviceKindGPU {
model = canonicalGPUModel(model, manufacturer)
}
temperatureC := firstNonZeroFloat(
float64(d.TemperatureC),
floatFromDetailMap(d.Details, "temperature_c"),
@@ -1091,6 +1105,52 @@ func isStorageEndpointPCIeDevice(d models.HardwareDevice) bool {
strings.Contains(joined, "drive")
}
// isRemovableUSBStorageDevice reports whether a storage device is a USB-attached
// removable drive (flash stick, external HDD). These come and go with whoever is
// standing at the machine — a live-CD boot stick, a technician's USB key — and
// are not part of the server's tracked inventory, so an OS-level collector that
// sees one must not make it look like a component was installed.
func isRemovableUSBStorageDevice(d models.HardwareDevice) bool {
if d.Kind != models.DeviceKindStorage {
return false
}
return strings.EqualFold(strings.TrimSpace(d.Interface), "USB") ||
strings.EqualFold(strings.TrimSpace(d.Type), "USB")
}
// isOnboardControllerPCIeDevice reports whether a PCIe entry is an onboard
// storage/switch controller function that only an OS-level PCI enumeration ever
// reports — a BMC add-in-card inventory never lists these. Dropping them keeps
// the pcie_devices set identical between a BMC dump and an lspci-based capture.
// Add-in HBA/RAID cards that carry their own serial or part number are kept.
func isOnboardControllerPCIeDevice(d models.HardwareDevice) bool {
if d.Kind == models.DeviceKindGPU || d.Kind == models.DeviceKindNetwork {
return false
}
class := strings.ToLower(strings.TrimSpace(d.DeviceClass))
model := strings.ToLower(strings.TrimSpace(d.Model))
if strings.Contains(model, "pcie switch") || strings.Contains(model, "switch management") ||
strings.Contains(model, "switch upstream") || strings.Contains(model, "switch downstream") {
return true
}
controllerClass := strings.Contains(class, "sata") ||
strings.Contains(class, "ahci") ||
strings.Contains(class, "ide controller") ||
strings.Contains(class, "nonvolatile") || strings.Contains(class, "non-volatile") ||
strings.Contains(class, "nvme") ||
strings.Contains(class, "mass storage") || strings.Contains(class, "massstorage") ||
class == "storagecontroller" || class == "storage controller"
if !controllerClass {
return false
}
if normalizedSerial(d.SerialNumber) != "" || hasMeaningfulExporterText(d.PartNumber) {
return false
}
return true
}
func isVirtualExportStorageDevice(d models.HardwareDevice) bool {
if d.Kind != models.DeviceKindStorage {
return false
@@ -2233,6 +2293,94 @@ func disambiguatePSUSlots(items []ReanimatorPSU) []ReanimatorPSU {
// BEE-SP versions, which label every module "DIMM 0" regardless of its real
// channel/slot. As with disambiguatePSUSlots, positions within a colliding
// group are assigned by ascending serial number for a deterministic result.
// canonicalMemorySlot normalizes a DIMM slot label so the same physical slot
// reads identically regardless of the collector: it drops a trailing
// channel-letter tag that dmidecode appends ("DIMM111(J)" -> "DIMM111") and
// rewrites a bare BMC-style "Memory111" locator to "DIMM111".
func canonicalMemorySlot(slot string) string {
s := strings.TrimSpace(slot)
if i := strings.LastIndexByte(s, '('); i > 0 && strings.HasSuffix(s, ")") {
s = strings.TrimSpace(s[:i])
}
if rest, ok := strings.CutPrefix(s, "Memory"); ok && isNumericExporterSlot(rest) {
s = "DIMM" + rest
}
return s
}
// canonicalStorageMediaAndInterface separates media type from bus interface so
// the same drive reads identically regardless of collector. "NVMe" is a bus, not
// a medium: an NVMe drive is an SSD on the NVMe interface. A drive reported only
// as PCIe-attached is, in practice, NVMe.
func canonicalStorageMediaAndInterface(mediaType, busInterface string) (string, string) {
t := strings.TrimSpace(mediaType)
b := strings.TrimSpace(busInterface)
if strings.EqualFold(t, "nvme") {
if b == "" || strings.EqualFold(b, "pcie") {
b = "NVMe"
}
t = "SSD"
}
if strings.EqualFold(b, "pcie") {
b = "NVMe"
}
return t, b
}
// manufacturerFromStorageModel pulls a leading brand token out of a drive model
// string ("KIOXIA KCD8XPUG7T68" -> "KIOXIA") for collectors that leave the
// manufacturer field empty.
func manufacturerFromStorageModel(model string) string {
fields := strings.Fields(strings.TrimSpace(model))
if len(fields) < 2 {
return ""
}
switch strings.ToUpper(fields[0]) {
case "KIOXIA", "INTEL", "SAMSUNG", "MICRON", "SEAGATE", "TOSHIBA", "WDC", "HGST", "SK", "SOLIDIGM", "SANDISK":
return strings.ToUpper(fields[0])
}
return ""
}
func isGPUClass(deviceClass string) bool {
switch strings.ToLower(strings.TrimSpace(deviceClass)) {
case "videocontroller", "displaycontroller", "processingaccelerator":
return true
}
return false
}
var gpuChipTokenRegex = regexp.MustCompile(`^[A-Z]{1,3}\d{2,3}[A-Z]?$`)
// canonicalGPUModel reduces an NVIDIA data-center GPU model string to a stable
// bare chip name so a BMC inventory ("H200") and an lspci capture
// ("NVIDIA H200 NVL", "NVIDIA Corporation GH100 [H200 NVL]") describe the same
// card identically. Non-NVIDIA or unrecognized models are returned unchanged.
func canonicalGPUModel(model, manufacturer string) string {
m := strings.TrimSpace(model)
if m == "" {
return m
}
lower := strings.ToLower(m + " " + manufacturer)
if !strings.Contains(lower, "nvidia") {
return m
}
// "GH100 [H200 NVL]" -> keep the bracketed marketing name, it holds the SKU.
if i := strings.IndexByte(m, '['); i >= 0 {
if j := strings.IndexByte(m[i:], ']'); j > 0 {
m = m[i+1 : i+j]
}
}
m = strings.ReplaceAll(m, "NVIDIA Corporation", "")
m = strings.ReplaceAll(m, "NVIDIA", "")
for _, tok := range strings.Fields(m) {
if gpuChipTokenRegex.MatchString(strings.ToUpper(tok)) {
return strings.ToUpper(tok)
}
}
return strings.TrimSpace(model)
}
func disambiguateMemorySlots(items []ReanimatorMemory) []ReanimatorMemory {
if len(items) < 2 {
return items