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,
|
PartNumber: pcie.PartNumber,
|
||||||
Manufacturer: pcie.Manufacturer,
|
Manufacturer: pcie.Manufacturer,
|
||||||
SerialNumber: pcie.SerialNumber,
|
SerialNumber: pcie.SerialNumber,
|
||||||
MACAddresses: append([]string(nil), pcie.MACAddresses...),
|
MACAddresses: normalizeMACAddresses(pcie.MACAddresses),
|
||||||
LinkWidth: pcie.LinkWidth,
|
LinkWidth: pcie.LinkWidth,
|
||||||
LinkSpeed: pcie.LinkSpeed,
|
LinkSpeed: pcie.LinkSpeed,
|
||||||
MaxLinkWidth: pcie.MaxLinkWidth,
|
MaxLinkWidth: pcie.MaxLinkWidth,
|
||||||
@@ -312,7 +312,7 @@ func buildDevicesFromLegacy(hw *models.HardwareConfig) []models.HardwareDevice {
|
|||||||
Firmware: nic.Firmware,
|
Firmware: nic.Firmware,
|
||||||
PortCount: nic.PortCount,
|
PortCount: nic.PortCount,
|
||||||
PortType: nic.PortType,
|
PortType: nic.PortType,
|
||||||
MACAddresses: nic.MACAddresses,
|
MACAddresses: normalizeMACAddresses(nic.MACAddresses),
|
||||||
LinkWidth: nic.LinkWidth,
|
LinkWidth: nic.LinkWidth,
|
||||||
LinkSpeed: nic.LinkSpeed,
|
LinkSpeed: nic.LinkSpeed,
|
||||||
MaxLinkWidth: nic.MaxLinkWidth,
|
MaxLinkWidth: nic.MaxLinkWidth,
|
||||||
@@ -357,7 +357,157 @@ func buildDevicesFromLegacy(hw *models.HardwareConfig) []models.HardwareDevice {
|
|||||||
return dedupeCanonicalDevices(all)
|
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 {
|
func dedupeCanonicalDevices(items []models.HardwareDevice) []models.HardwareDevice {
|
||||||
|
items = mergeDevicesBySharedMAC(items)
|
||||||
type scored struct {
|
type scored struct {
|
||||||
item models.HardwareDevice
|
item models.HardwareDevice
|
||||||
score int
|
score int
|
||||||
|
|||||||
+5
-2
@@ -212,8 +212,11 @@ func extractRedisInlineValue(content []byte, start int) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func extractRedisCandidateValue(content []byte, start int) string {
|
func extractRedisCandidateValue(content []byte, start int) string {
|
||||||
// Fast-path for simple inline string values.
|
// Fast-path for simple inline string values. A cleanly-read inline value is
|
||||||
if v := extractRedisInlineValue(content, start); normalizeRedisValue(v) != "" {
|
// authoritative even when it normalizes to a placeholder like "N/A" —
|
||||||
|
// falling through to the window scan below in that case would risk
|
||||||
|
// mistaking an unrelated adjacent Redis key name for the real value.
|
||||||
|
if v := extractRedisInlineValue(content, start); v != "" {
|
||||||
return v
|
return v
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user