diff --git a/bible-local/10-decisions.md b/bible-local/10-decisions.md index 0c79f67..9e4cc77 100644 --- a/bible-local/10-decisions.md +++ b/bible-local/10-decisions.md @@ -1726,12 +1726,44 @@ the work between the two vendor parsers and the shared exporter: taken from the bundle's `ipmitool-fru.txt` chassis "Product Part Number" to match the BMC value. **Consequences:** For the reference server every physical component now appears in -both exports keyed identically (serial, or BDF for PCIe). Residual diffs are -one-sided *enrichment* only — PCIe link speed/width, NVMe block sizes, drive -`size_gb`, CPU clocks, PSU firmware/wattage, `board.uuid`, and the extra firmware -components (CPLD/U-Boot/SDK/…) — where one collector's source simply has data the -other's does not; none is a value that *conflicts*. Whether the audit treats a -null→value transition on an unchanged component as a change is a config question -on the audit side. Tests: `TestCanonicalMemorySlot`, `TestCanonicalGPUModel`, +both exports keyed identically (serial, or BDF for PCIe). Tests: +`TestCanonicalMemorySlot`, `TestCanonicalGPUModel`, `TestCanonicalStorageMediaAndInterface`, `TestIsOnboardControllerPCIeDevice`, `TestNormalizePSUSlots`, updated `TestParse_ServerFileExport_NetworkAdaptersAndFirmware`. + +**Verified against `reanimator/core` (the ingesting audit tool), 2026-08-31.** +Reanimator persists only a narrow slice of what the export carries, so most +residual field diffs cannot generate a change event: +- Component identity = `vendor_serial` only (`parts` table). Fallback chain per + `flattenPCIe`: real serial → MAC → `{board}-PCIE-{slot}`. Memory/storage/PSU: + serial → synthetic-by-slot. `parts` stores only vendor, model, serial — **no + size, speed, link width, block size, clocks, wattage, location, numa/iommu**, so + a null→value transition on any of those is a no-op. +- `vendor`/`model` are re-canonicalized on ingest via the alias resolver **using + `vendor_id`/`device_id`** ("prefer the authoritative pci.ids name"). Both bundles + carry the same PCI IDs, so GPU model and NIC vendor already converge server-side + regardless of the raw strings — the logpile-side `canonicalGPUModel` / OEM-vendor + blanking are belt-and-suspenders for non-reanimator consumers. +- `slot_name` is written once at install and only re-patched when the install + itself changes (`res.Changed`); a component that stays continuously present + never gets a slot-change event, so differing slot labels are harmless once + presence is stable. +- Asset firmware = `machine_firmware_states` keyed by `(machine_id, device_name)`; + an event fires only on a version change for the *same* name. A name absent from + an import is left untouched (no downgrade), so the 6 BMC-only firmware entries + are inert. `iBMC` (BMC dump) vs `BMC` (live CD) stay two independent, non-churning + rows — deliberately **not** normalized, because unifying the name would then + churn on the version (`3.08.05.85` vs `3.08`). +- Reanimator has its own `IsDeviceIgnored(vendor, model)` alias-driven ignore list + for "onboard controllers, service USB flash drives" — the logpile-side + `isRemovableUSBStorageDevice`/`isOnboardControllerPCIeDevice` filters overlap it + but remove the dependency on the alias dict being configured. + +The one real cross-collector reinstall risk was the **OCP NIC**: with the card +serial on it, reanimator collapses both ports to one serial-keyed component, while +a live-CD bundle (no serial) stays two MAC-keyed components → remove + 2×install +on every source switch. Fixed by emitting the xFusion NIC as per-port entries +**with no serial**, so both sources key on the port MAC and produce the same two +components. Net: after this work the only fields that still differ between the two +bundles are ones reanimator does not track, so alternating collection methods for +one server produces no spurious install/remove/firmware events. diff --git a/internal/parser/vendors/xfusion/hardware.go b/internal/parser/vendors/xfusion/hardware.go index 2ad0260..2858bd0 100644 --- a/internal/parser/vendors/xfusion/hardware.go +++ b/internal/parser/vendors/xfusion/hardware.go @@ -710,9 +710,17 @@ func mergeNetworkAdapters(cards []xfusionNICCard, snapshots []xfusionNetcardSnap } // Emit one adapter per PCI function (port), keyed by that port's own BDF, - // so the card matches an OS-level lspci view of the same NIC. Ports share - // the physical card's serial/part number. Fall back to a single - // card-level entry only when no per-port BDF is available. + // so the card matches an OS-level lspci view of the same NIC. Fall back to + // a single card-level entry only when no per-port BDF is available. + // + // The per-port entries deliberately carry NO serial number. The physical + // card has one serial across both ports; an in-band collector (lspci) sees + // none and identifies each port by its MAC. Reanimator keys a NIC on + // serial → MAC → synthetic(slot): if we emitted the card serial here, its + // two ports would collapse to one component keyed by that serial, while + // the same NIC from a live-CD bundle stays two components keyed by MAC — + // producing a spurious remove+reinstall every time the two sources + // alternate. Leaving serial empty makes both sources key on MAC and agree. type portEntry struct { bdf string mac string @@ -734,6 +742,15 @@ func mergeNetworkAdapters(cards []xfusionNICCard, snapshots []xfusionNetcardSnap } else if len(ports) == 1 { portMACs = macs } + // No serial: an lspci-based collector has none, and reanimator keys a + // serial-bearing NIC on the serial (collapsing ports) but a + // serial-less one on the MAC. Keeping this empty makes a BMC dump and + // a live-CD run agree. The card serial still reaches the export via + // this card's FRU record. + serialPerPort := "" + if len(portMACs) == 0 { + serialPerPort = card.SerialNumber + } adapters = append(adapters, models.NetworkAdapter{ Slot: firstNonEmpty(p.bdf, slot), Location: "OCP", @@ -744,7 +761,7 @@ func mergeNetworkAdapters(cards []xfusionNICCard, snapshots []xfusionNetcardSnap Vendor: manufacturer, VendorID: card.VendorID, DeviceID: card.DeviceID, - SerialNumber: card.SerialNumber, + SerialNumber: serialPerPort, PartNumber: card.PartNumber, Firmware: firmware, PortCount: 1, diff --git a/internal/parser/vendors/xfusion/parser_test.go b/internal/parser/vendors/xfusion/parser_test.go index 480a639..8df2db9 100644 --- a/internal/parser/vendors/xfusion/parser_test.go +++ b/internal/parser/vendors/xfusion/parser_test.go @@ -218,8 +218,13 @@ Product Name: G5500 V7 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) + // Ports keyed by MAC, not by the shared card serial, so a BMC dump and an + // lspci-based bundle produce the same components. + if a.SerialNumber != "" { + t.Fatalf("adapter %s serial = %q, want empty (MAC-keyed)", a.BDF, a.SerialNumber) + } + if a.PartNumber != "0302Y238" { + t.Fatalf("adapter %s part number = %q, want 0302Y238", a.BDF, a.PartNumber) } } if got := byBDF["0000:27:00.0"].MACAddresses; len(got) != 1 || got[0] != "44:1A:4C:16:E8:03" {