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:
co-authored by
Claude Sonnet 5
parent
5b65c99b0e
commit
9b2c654182
@@ -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
|
||||
|
||||
@@ -2270,3 +2270,75 @@ func TestConvertToReanimator_ExportsLicenses(t *testing.T) {
|
||||
t.Errorf("Status = %q, want %q", scoped.Status, "Warning")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCanonicalMemorySlot(t *testing.T) {
|
||||
cases := map[string]string{
|
||||
"DIMM111(J)": "DIMM111",
|
||||
"DIMM111 (J)": "DIMM111",
|
||||
"Memory111": "DIMM111",
|
||||
"DIMM111": "DIMM111",
|
||||
"P1-DIMMA1": "P1-DIMMA1",
|
||||
"CPU0_C0D0": "CPU0_C0D0",
|
||||
}
|
||||
for in, want := range cases {
|
||||
if got := canonicalMemorySlot(in); got != want {
|
||||
t.Errorf("canonicalMemorySlot(%q) = %q, want %q", in, got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestCanonicalGPUModel(t *testing.T) {
|
||||
cases := []struct{ model, mfr, want string }{
|
||||
{"NVIDIA H200 NVL", "NVIDIA Corporation", "H200"},
|
||||
{"H200", "NVIDIA Corporation", "H200"},
|
||||
{"NVIDIA H200 141G", "", "H200"},
|
||||
{"GH100 [H200 NVL]", "NVIDIA Corporation", "H200"},
|
||||
{"Radeon Instinct MI300X", "AMD", "Radeon Instinct MI300X"},
|
||||
}
|
||||
for _, c := range cases {
|
||||
if got := canonicalGPUModel(c.model, c.mfr); got != c.want {
|
||||
t.Errorf("canonicalGPUModel(%q,%q) = %q, want %q", c.model, c.mfr, got, c.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestCanonicalStorageMediaAndInterface(t *testing.T) {
|
||||
cases := []struct{ inT, inI, wantT, wantI string }{
|
||||
{"NVMe", "PCIe", "SSD", "NVMe"},
|
||||
{"NVMe", "", "SSD", "NVMe"},
|
||||
{"SSD", "PCIe", "SSD", "NVMe"},
|
||||
{"SSD", "SATA", "SSD", "SATA"},
|
||||
{"HDD", "SAS", "HDD", "SAS"},
|
||||
}
|
||||
for _, c := range cases {
|
||||
gotT, gotI := canonicalStorageMediaAndInterface(c.inT, c.inI)
|
||||
if gotT != c.wantT || gotI != c.wantI {
|
||||
t.Errorf("canonicalStorageMediaAndInterface(%q,%q) = %q,%q want %q,%q", c.inT, c.inI, gotT, gotI, c.wantT, c.wantI)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsOnboardControllerPCIeDevice(t *testing.T) {
|
||||
drop := []models.HardwareDevice{
|
||||
{Kind: models.DeviceKindPCIe, DeviceClass: "SATA controller", Model: "Sapphire Rapids SATA AHCI Controller"},
|
||||
{Kind: models.DeviceKindPCIe, DeviceClass: "Non-Volatile memory controller", Model: "NVMe SSD Controller CD8P"},
|
||||
{Kind: models.DeviceKindPCIe, DeviceClass: "MassStorageController", Model: "MegaRAID 12GSAS/PCIe Secure SAS38xx"},
|
||||
{Kind: models.DeviceKindPCIe, DeviceClass: "StorageController", Model: "PCIe Switch management endpoint"},
|
||||
}
|
||||
for _, d := range drop {
|
||||
if !isOnboardControllerPCIeDevice(d) {
|
||||
t.Errorf("expected %q/%q to be dropped", d.DeviceClass, d.Model)
|
||||
}
|
||||
}
|
||||
keep := []models.HardwareDevice{
|
||||
{Kind: models.DeviceKindGPU, DeviceClass: "VideoController", Model: "H200"},
|
||||
{Kind: models.DeviceKindNetwork, DeviceClass: "EthernetController", Model: "ConnectX-6 Lx"},
|
||||
{Kind: models.DeviceKindPCIe, DeviceClass: "raid_controller", Model: "RAID Controller", PartNumber: "XC170-M-8i"},
|
||||
{Kind: models.DeviceKindPCIe, DeviceClass: "MassStorageController", Model: "HBA", SerialNumber: "ABC123"},
|
||||
}
|
||||
for _, d := range keep {
|
||||
if isOnboardControllerPCIeDevice(d) {
|
||||
t.Errorf("expected %q/%q to be kept", d.DeviceClass, d.Model)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user