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:
co-authored by
Claude Sonnet 5
parent
399eca5f49
commit
93f0897b81
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user