fix(exporter): dedup NICs by normalized MAC, fix garbage redis serial fallback

MAC-format mismatches between collector sources (dash vs colon separators)
were preventing duplicate NIC/PCIe entries from merging in the canonical
device dedup pass. Add MAC address normalization and merge devices that
share a normalized MAC before the existing serial/BDF-based dedup runs.

Also fix a bug in the Inspur redis-dump serial fallback parser: when a
field's inline value was the placeholder "N/A", the code incorrectly fell
through to a window-scan fallback that could pick up an unrelated adjacent
Redis key name (e.g. "AssetInfoPCIEMMIOSpace") as a fake serial number.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Mikhail Chusavitin
2026-08-13 18:09:49 +03:00
co-authored by Claude Sonnet 5
parent 399eca5f49
commit 93f0897b81
2 changed files with 157 additions and 4 deletions
+152 -2
View File
@@ -237,7 +237,7 @@ func buildDevicesFromLegacy(hw *models.HardwareConfig) []models.HardwareDevice {
PartNumber: pcie.PartNumber,
Manufacturer: pcie.Manufacturer,
SerialNumber: pcie.SerialNumber,
MACAddresses: append([]string(nil), pcie.MACAddresses...),
MACAddresses: normalizeMACAddresses(pcie.MACAddresses),
LinkWidth: pcie.LinkWidth,
LinkSpeed: pcie.LinkSpeed,
MaxLinkWidth: pcie.MaxLinkWidth,
@@ -312,7 +312,7 @@ func buildDevicesFromLegacy(hw *models.HardwareConfig) []models.HardwareDevice {
Firmware: nic.Firmware,
PortCount: nic.PortCount,
PortType: nic.PortType,
MACAddresses: nic.MACAddresses,
MACAddresses: normalizeMACAddresses(nic.MACAddresses),
LinkWidth: nic.LinkWidth,
LinkSpeed: nic.LinkSpeed,
MaxLinkWidth: nic.MaxLinkWidth,
@@ -357,7 +357,157 @@ func buildDevicesFromLegacy(hw *models.HardwareConfig) []models.HardwareDevice {
return dedupeCanonicalDevices(all)
}
// normalizeMACAddress converts a MAC address to a canonical "AA:BB:CC:DD:EE:FF"
// form regardless of the separator/casing used by the source (colon, dash, dot,
// or none). Returns "" if the value doesn't contain exactly 12 hex digits.
func normalizeMACAddress(mac string) string {
var hexDigits [12]byte
n := 0
for i := 0; i < len(mac); i++ {
c := mac[i]
switch {
case c >= '0' && c <= '9', c >= 'a' && c <= 'f', c >= 'A' && c <= 'F':
if n == 12 {
return ""
}
hexDigits[n] = c
n++
}
}
if n != 12 {
return ""
}
var b strings.Builder
b.Grow(17)
for i := 0; i < 12; i += 2 {
if i > 0 {
b.WriteByte(':')
}
b.WriteByte(hexDigits[i])
b.WriteByte(hexDigits[i+1])
}
return strings.ToUpper(b.String())
}
// normalizeMACAddresses normalizes each address to canonical form, dropping
// unparsable entries and de-duplicating while preserving order.
func normalizeMACAddresses(macs []string) []string {
if len(macs) == 0 {
return nil
}
seen := make(map[string]struct{}, len(macs))
out := make([]string, 0, len(macs))
for _, mac := range macs {
nm := normalizeMACAddress(mac)
if nm == "" {
continue
}
if _, ok := seen[nm]; ok {
continue
}
seen[nm] = struct{}{}
out = append(out, nm)
}
if len(out) == 0 {
return nil
}
return out
}
// mergeDevicesBySharedMAC merges network-class devices that share at least one
// normalized MAC address, regardless of their Serial/BDF/slot identity. This
// runs before the serial/BDF based canonicalKey grouping so that a device with
// a bogus or mismatched serial/slot (e.g. from a different collector source)
// still merges with its true duplicate as long as the hardware MAC overlaps.
func mergeDevicesBySharedMAC(items []models.HardwareDevice) []models.HardwareDevice {
if len(items) <= 1 {
return items
}
eligible := func(kind string) bool {
return kind == models.DeviceKindNetwork || kind == models.DeviceKindPCIe
}
parent := make([]int, len(items))
for i := range parent {
parent[i] = i
}
var find func(int) int
find = func(x int) int {
for parent[x] != x {
parent[x] = parent[parent[x]]
x = parent[x]
}
return x
}
union := func(a, b int) {
ra, rb := find(a), find(b)
if ra != rb {
parent[ra] = rb
}
}
macToIndices := make(map[string][]int)
for i, item := range items {
if !eligible(item.Kind) {
continue
}
for _, mac := range item.MACAddresses {
nm := normalizeMACAddress(mac)
if nm == "" {
continue
}
macToIndices[nm] = append(macToIndices[nm], i)
}
}
merged := false
for _, idxs := range macToIndices {
for k := 1; k < len(idxs); k++ {
union(idxs[0], idxs[k])
merged = true
}
}
if !merged {
return items
}
order := make([]int, 0, len(items))
groups := make(map[int][]int, len(items))
for i := range items {
root := find(i)
if _, ok := groups[root]; !ok {
order = append(order, root)
}
groups[root] = append(groups[root], i)
}
out := make([]models.HardwareDevice, 0, len(order))
for _, root := range order {
members := groups[root]
if len(members) == 1 {
out = append(out, items[members[0]])
continue
}
best := members[0]
for _, idx := range members[1:] {
if canonicalScore(items[idx]) > canonicalScore(items[best]) {
best = idx
}
}
mergedItem := items[best]
for _, idx := range members {
if idx == best {
continue
}
mergedItem = mergeCanonicalDevice(mergedItem, items[idx])
}
out = append(out, mergedItem)
}
return out
}
func dedupeCanonicalDevices(items []models.HardwareDevice) []models.HardwareDevice {
items = mergeDevicesBySharedMAC(items)
type scored struct {
item models.HardwareDevice
score int