looksLikeGPU now falls back to resolving VendorId/DeviceId through the pci.ids database when the BMC leaves Name/Model/Manufacturer/ClassCode empty, so GPUs identifiable only by raw PCI IDs (e.g. NVIDIA H100 SXM5 0x10de/0x2330) are no longer misclassified as generic PCIe devices. The replay pipeline's "backed by canonical NIC" dedup used to trust a PCIeDevice's Links.NetworkDeviceFunctions reference at face value and drop the device, assuming a NetworkAdapters record existed elsewhere. On BMCs that expose resource IDs with characters (parentheses) that 404 on fetch, that canonical NIC never gets captured, so the device carrying its actual hardware identity vanished from the export entirely. hasResolvableLinkedMember now verifies the linked resource is actually present in the snapshot before treating it as authoritative. Also normalize PartNumber through normalizeRedfishIdentityField in the GPU/PCIe parsers so a BMC-supplied literal "null" string doesn't leak into exports verbatim. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
600 lines
19 KiB
Go
600 lines
19 KiB
Go
package collector
|
|
|
|
import (
|
|
"strings"
|
|
|
|
"git.mchus.pro/mchus/logpile/internal/models"
|
|
)
|
|
|
|
func (r redfishSnapshotReader) enrichNICsFromNetworkInterfaces(nics *[]models.NetworkAdapter, systemPaths []string) {
|
|
if nics == nil {
|
|
return
|
|
}
|
|
bySlot := make(map[string]int, len(*nics))
|
|
for i, nic := range *nics {
|
|
bySlot[strings.ToLower(strings.TrimSpace(nic.Slot))] = i
|
|
}
|
|
|
|
for _, systemPath := range systemPaths {
|
|
ifaces, err := r.getCollectionMembers(joinPath(systemPath, "/NetworkInterfaces"))
|
|
if err != nil || len(ifaces) == 0 {
|
|
continue
|
|
}
|
|
for _, iface := range ifaces {
|
|
slot := firstNonEmpty(asString(iface["Id"]), asString(iface["Name"]))
|
|
if strings.TrimSpace(slot) == "" {
|
|
continue
|
|
}
|
|
idx, ok := bySlot[strings.ToLower(strings.TrimSpace(slot))]
|
|
if !ok {
|
|
// The NetworkInterface Id (e.g. "2") may not match the display slot of
|
|
// the real NIC that came from Chassis/NetworkAdapters (e.g. "RISER 5
|
|
// slot 1 (7)"). Try to find the real NIC via the Links.NetworkAdapter
|
|
// cross-reference before creating a ghost entry.
|
|
if linkedIdx := r.findNICIndexByLinkedNetworkAdapter(iface, *nics, bySlot); linkedIdx >= 0 {
|
|
idx = linkedIdx
|
|
ok = true
|
|
}
|
|
}
|
|
if !ok {
|
|
*nics = append(*nics, models.NetworkAdapter{
|
|
Slot: slot,
|
|
Present: true,
|
|
Model: firstNonEmpty(asString(iface["Model"]), asString(iface["Name"])),
|
|
Status: mapStatus(iface["Status"]),
|
|
})
|
|
idx = len(*nics) - 1
|
|
bySlot[strings.ToLower(strings.TrimSpace(slot))] = idx
|
|
}
|
|
|
|
portsPath := redfishLinkedPath(iface, "NetworkPorts")
|
|
if portsPath == "" {
|
|
continue
|
|
}
|
|
portDocs, err := r.getCollectionMembers(portsPath)
|
|
if err != nil || len(portDocs) == 0 {
|
|
continue
|
|
}
|
|
macs := append([]string{}, (*nics)[idx].MACAddresses...)
|
|
for _, p := range portDocs {
|
|
macs = append(macs, collectNetworkPortMACs(p)...)
|
|
}
|
|
(*nics)[idx].MACAddresses = dedupeStrings(macs)
|
|
if sanitizeNetworkPortCount((*nics)[idx].PortCount) == 0 {
|
|
(*nics)[idx].PortCount = len(portDocs)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func (r redfishSnapshotReader) collectNICs(chassisPaths []string) []models.NetworkAdapter {
|
|
var nics []models.NetworkAdapter
|
|
for _, chassisPath := range chassisPaths {
|
|
adapterDocs, err := r.getCollectionMembers(joinPath(chassisPath, "/NetworkAdapters"))
|
|
if err != nil {
|
|
continue
|
|
}
|
|
for _, doc := range adapterDocs {
|
|
nics = append(nics, r.buildNICFromAdapterDoc(doc))
|
|
}
|
|
}
|
|
return dedupeNetworkAdapters(nics)
|
|
}
|
|
|
|
func (r redfishSnapshotReader) buildNICFromAdapterDoc(adapterDoc map[string]interface{}) models.NetworkAdapter {
|
|
nic := parseNIC(adapterDoc)
|
|
adapterFunctionDocs := r.getNetworkAdapterFunctionDocs(adapterDoc)
|
|
for _, pciePath := range networkAdapterPCIeDevicePaths(adapterDoc) {
|
|
pcieDoc, err := r.getJSON(pciePath)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
functionDocs := r.getLinkedPCIeFunctions(pcieDoc)
|
|
for _, adapterFnDoc := range adapterFunctionDocs {
|
|
functionDocs = append(functionDocs, r.getLinkedPCIeFunctions(adapterFnDoc)...)
|
|
}
|
|
functionDocs = dedupeJSONDocsByPath(functionDocs)
|
|
supplementalDocs := r.getLinkedSupplementalDocs(pcieDoc, "EnvironmentMetrics", "Metrics")
|
|
for _, fn := range functionDocs {
|
|
supplementalDocs = append(supplementalDocs, r.getLinkedSupplementalDocs(fn, "EnvironmentMetrics", "Metrics")...)
|
|
}
|
|
enrichNICFromPCIe(&nic, pcieDoc, functionDocs, supplementalDocs)
|
|
}
|
|
if len(nic.MACAddresses) == 0 {
|
|
r.enrichNICMACsFromNetworkDeviceFunctions(&nic, adapterDoc)
|
|
}
|
|
return nic
|
|
}
|
|
|
|
func (r redfishSnapshotReader) getNetworkAdapterFunctionDocs(adapterDoc map[string]interface{}) []map[string]interface{} {
|
|
ndfCol, ok := adapterDoc["NetworkDeviceFunctions"].(map[string]interface{})
|
|
if !ok {
|
|
return nil
|
|
}
|
|
colPath := asString(ndfCol["@odata.id"])
|
|
if colPath == "" {
|
|
return nil
|
|
}
|
|
funcDocs, err := r.getCollectionMembers(colPath)
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
return funcDocs
|
|
}
|
|
|
|
func (r redfishSnapshotReader) collectPCIeDevices(systemPaths, chassisPaths []string) []models.PCIeDevice {
|
|
collections := make([]string, 0, len(systemPaths)+len(chassisPaths))
|
|
for _, systemPath := range systemPaths {
|
|
collections = append(collections, joinPath(systemPath, "/PCIeDevices"))
|
|
}
|
|
for _, chassisPath := range chassisPaths {
|
|
collections = append(collections, joinPath(chassisPath, "/PCIeDevices"))
|
|
}
|
|
var out []models.PCIeDevice
|
|
for _, collectionPath := range collections {
|
|
memberDocs, err := r.getCollectionMembers(collectionPath)
|
|
if err != nil || len(memberDocs) == 0 {
|
|
continue
|
|
}
|
|
for _, doc := range memberDocs {
|
|
functionDocs := r.getLinkedPCIeFunctions(doc)
|
|
if looksLikeGPU(doc, functionDocs) {
|
|
continue
|
|
}
|
|
if r.replayPCIeDeviceBackedByCanonicalNIC(doc, functionDocs) {
|
|
continue
|
|
}
|
|
supplementalDocs := r.getLinkedSupplementalDocs(doc, "EnvironmentMetrics", "Metrics")
|
|
supplementalDocs = append(supplementalDocs, r.getChassisScopedPCIeSupplementalDocs(doc)...)
|
|
for _, fn := range functionDocs {
|
|
supplementalDocs = append(supplementalDocs, r.getLinkedSupplementalDocs(fn, "EnvironmentMetrics", "Metrics")...)
|
|
}
|
|
dev := parsePCIeDeviceWithSupplementalDocs(doc, functionDocs, supplementalDocs)
|
|
if r.shouldSkipReplayPCIeDevice(doc, dev) {
|
|
continue
|
|
}
|
|
out = append(out, dev)
|
|
}
|
|
}
|
|
for _, systemPath := range systemPaths {
|
|
functionDocs, err := r.getCollectionMembers(joinPath(systemPath, "/PCIeFunctions"))
|
|
if err != nil || len(functionDocs) == 0 {
|
|
continue
|
|
}
|
|
for idx, fn := range functionDocs {
|
|
supplementalDocs := r.getLinkedSupplementalDocs(fn, "EnvironmentMetrics", "Metrics")
|
|
dev := parsePCIeFunctionWithSupplementalDocs(fn, supplementalDocs, idx+1)
|
|
if r.shouldSkipReplayPCIeDevice(fn, dev) {
|
|
continue
|
|
}
|
|
out = append(out, dev)
|
|
}
|
|
}
|
|
return dedupePCIeDevices(out)
|
|
}
|
|
|
|
func (r redfishSnapshotReader) shouldSkipReplayPCIeDevice(doc map[string]interface{}, dev models.PCIeDevice) bool {
|
|
if isUnidentifiablePCIeDevice(dev) {
|
|
return true
|
|
}
|
|
if r.replayNetworkFunctionBackedByCanonicalNIC(doc, dev) {
|
|
return true
|
|
}
|
|
if isReplayStorageServiceEndpoint(doc, dev) {
|
|
return true
|
|
}
|
|
if isReplayNoisePCIeClass(dev.DeviceClass) {
|
|
return true
|
|
}
|
|
if isReplayDisplayDeviceDuplicate(doc, dev) {
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
func (r redfishSnapshotReader) replayPCIeDeviceBackedByCanonicalNIC(doc map[string]interface{}, functionDocs []map[string]interface{}) bool {
|
|
if !looksLikeReplayNetworkPCIeDevice(doc, functionDocs) {
|
|
return false
|
|
}
|
|
for _, fn := range functionDocs {
|
|
if r.hasResolvableLinkedMember(fn, "NetworkDeviceFunctions") {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func (r redfishSnapshotReader) replayNetworkFunctionBackedByCanonicalNIC(doc map[string]interface{}, dev models.PCIeDevice) bool {
|
|
if !looksLikeReplayNetworkClass(dev.DeviceClass) {
|
|
return false
|
|
}
|
|
return r.hasResolvableLinkedMember(doc, "NetworkDeviceFunctions")
|
|
}
|
|
|
|
// hasResolvableLinkedMember reports whether the resource(s) linked under
|
|
// doc.Links[key] were actually captured in the snapshot. A Links reference
|
|
// alone is not enough: some BMCs (e.g. xFusion) advertise linked
|
|
// NetworkAdapters/NetworkDeviceFunctions resources whose IDs contain
|
|
// characters (like parentheses) that 404 when fetched, so the "canonical"
|
|
// NIC never makes it into the snapshot even though the link exists. In that
|
|
// case the PCIe device carrying the NIC's hardware identity must not be
|
|
// dropped, or the NIC disappears from the inventory entirely.
|
|
func (r redfishSnapshotReader) hasResolvableLinkedMember(doc map[string]interface{}, key string) bool {
|
|
links, ok := doc["Links"].(map[string]interface{})
|
|
if !ok {
|
|
return false
|
|
}
|
|
linked, ok := links[key]
|
|
if !ok {
|
|
return false
|
|
}
|
|
for _, path := range extractODataIDs(linked) {
|
|
if _, err := r.getJSON(path); err == nil {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func looksLikeReplayNetworkPCIeDevice(doc map[string]interface{}, functionDocs []map[string]interface{}) bool {
|
|
for _, fn := range functionDocs {
|
|
if looksLikeReplayNetworkClass(asString(fn["DeviceClass"])) {
|
|
return true
|
|
}
|
|
}
|
|
joined := strings.ToLower(strings.TrimSpace(strings.Join([]string{
|
|
asString(doc["DeviceType"]),
|
|
asString(doc["Description"]),
|
|
asString(doc["Name"]),
|
|
asString(doc["Model"]),
|
|
}, " ")))
|
|
return strings.Contains(joined, "network")
|
|
}
|
|
|
|
func looksLikeReplayNetworkClass(class string) bool {
|
|
class = strings.ToLower(strings.TrimSpace(class))
|
|
return strings.Contains(class, "network") || strings.Contains(class, "ethernet")
|
|
}
|
|
|
|
func isReplayStorageServiceEndpoint(doc map[string]interface{}, dev models.PCIeDevice) bool {
|
|
class := strings.ToLower(strings.TrimSpace(dev.DeviceClass))
|
|
if class != "massstoragecontroller" && class != "mass storage controller" {
|
|
return false
|
|
}
|
|
name := strings.ToLower(strings.TrimSpace(firstNonEmpty(
|
|
dev.PartNumber,
|
|
asString(doc["PartNumber"]),
|
|
asString(doc["Description"]),
|
|
)))
|
|
if strings.Contains(name, "pcie switch management endpoint") {
|
|
return true
|
|
}
|
|
if strings.Contains(name, "volume management device") {
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
func isReplayNoisePCIeClass(class string) bool {
|
|
switch strings.ToLower(strings.TrimSpace(class)) {
|
|
case "bridge", "processor", "signalprocessingcontroller", "signal processing controller", "serialbuscontroller", "serial bus controller":
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
func isReplayDisplayDeviceDuplicate(doc map[string]interface{}, dev models.PCIeDevice) bool {
|
|
class := strings.ToLower(strings.TrimSpace(dev.DeviceClass))
|
|
if class != "displaycontroller" && class != "display controller" {
|
|
return false
|
|
}
|
|
return strings.EqualFold(strings.TrimSpace(asString(doc["Description"])), "Display Device")
|
|
}
|
|
|
|
func (r redfishSnapshotReader) getChassisScopedPCIeSupplementalDocs(doc map[string]interface{}) []map[string]interface{} {
|
|
docPath := normalizeRedfishPath(asString(doc["@odata.id"]))
|
|
chassisPath := chassisPathForPCIeDoc(docPath)
|
|
if chassisPath == "" {
|
|
return nil
|
|
}
|
|
|
|
out := make([]map[string]interface{}, 0, 6)
|
|
if looksLikeNVSwitchPCIeDoc(doc) {
|
|
for _, path := range []string{
|
|
joinPath(chassisPath, "/EnvironmentMetrics"),
|
|
joinPath(chassisPath, "/ThermalSubsystem/ThermalMetrics"),
|
|
} {
|
|
supplementalDoc, err := r.getJSON(path)
|
|
if err != nil || len(supplementalDoc) == 0 {
|
|
continue
|
|
}
|
|
out = append(out, supplementalDoc)
|
|
}
|
|
}
|
|
deviceDocs, err := r.getCollectionMembers(joinPath(chassisPath, "/Devices"))
|
|
if err == nil {
|
|
for _, deviceDoc := range deviceDocs {
|
|
if !redfishPCIeMatchesChassisDeviceDoc(doc, deviceDoc) {
|
|
continue
|
|
}
|
|
out = append(out, deviceDoc)
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
// collectBMCMAC returns the MAC address of the best BMC management interface
|
|
// found in Managers/*/EthernetInterfaces. Prefer an active link with an IP
|
|
// address over a passive sideband interface.
|
|
func (r redfishSnapshotReader) collectBMCMAC(managerPaths []string) string {
|
|
summary := r.collectBMCManagementSummary(managerPaths)
|
|
if len(summary) == 0 {
|
|
return ""
|
|
}
|
|
return strings.ToUpper(strings.TrimSpace(asString(summary["mac_address"])))
|
|
}
|
|
|
|
func (r redfishSnapshotReader) collectBMCManagementSummary(managerPaths []string) map[string]any {
|
|
bestScore := -1
|
|
var best map[string]any
|
|
for _, managerPath := range managerPaths {
|
|
collectionPath := joinPath(managerPath, "/EthernetInterfaces")
|
|
collectionDoc, _ := r.getJSON(collectionPath)
|
|
ncsiEnabled, lldpMode, lldpByEth := redfishManagerEthernetCollectionHints(collectionDoc)
|
|
members, err := r.getCollectionMembers(collectionPath)
|
|
if err != nil || len(members) == 0 {
|
|
continue
|
|
}
|
|
for _, doc := range members {
|
|
mac := strings.TrimSpace(firstNonEmpty(
|
|
asString(doc["PermanentMACAddress"]),
|
|
asString(doc["MACAddress"]),
|
|
))
|
|
if mac == "" || strings.EqualFold(mac, "00:00:00:00:00:00") {
|
|
continue
|
|
}
|
|
ifaceID := strings.TrimSpace(firstNonEmpty(asString(doc["Id"]), asString(doc["Name"])))
|
|
summary := map[string]any{
|
|
"manager_path": managerPath,
|
|
"interface_id": ifaceID,
|
|
"hostname": strings.TrimSpace(asString(doc["HostName"])),
|
|
"fqdn": strings.TrimSpace(asString(doc["FQDN"])),
|
|
"mac_address": strings.ToUpper(mac),
|
|
"link_status": strings.TrimSpace(asString(doc["LinkStatus"])),
|
|
"speed_mbps": asInt(doc["SpeedMbps"]),
|
|
"interface_name": strings.TrimSpace(asString(doc["Name"])),
|
|
"interface_desc": strings.TrimSpace(asString(doc["Description"])),
|
|
"ncsi_enabled": ncsiEnabled,
|
|
"lldp_mode": lldpMode,
|
|
"ipv4_address": redfishManagerIPv4Field(doc, "Address"),
|
|
"ipv4_gateway": redfishManagerIPv4Field(doc, "Gateway"),
|
|
"ipv4_subnet": redfishManagerIPv4Field(doc, "SubnetMask"),
|
|
"ipv6_address": redfishManagerIPv6Field(doc, "Address"),
|
|
"link_is_active": strings.EqualFold(strings.TrimSpace(asString(doc["LinkStatus"])), "LinkActive"),
|
|
"interface_score": 0,
|
|
}
|
|
if lldp, ok := lldpByEth[strings.ToLower(ifaceID)]; ok {
|
|
summary["lldp_chassis_name"] = lldp["ChassisName"]
|
|
summary["lldp_port_desc"] = lldp["PortDesc"]
|
|
summary["lldp_port_id"] = lldp["PortId"]
|
|
if vlan := asInt(lldp["VlanId"]); vlan > 0 {
|
|
summary["lldp_vlan_id"] = vlan
|
|
}
|
|
}
|
|
score := redfishManagerInterfaceScore(summary)
|
|
summary["interface_score"] = score
|
|
if score > bestScore {
|
|
bestScore = score
|
|
best = summary
|
|
}
|
|
}
|
|
}
|
|
return best
|
|
}
|
|
|
|
func redfishManagerEthernetCollectionHints(collectionDoc map[string]interface{}) (bool, string, map[string]map[string]interface{}) {
|
|
lldpByEth := make(map[string]map[string]interface{})
|
|
if len(collectionDoc) == 0 {
|
|
return false, "", lldpByEth
|
|
}
|
|
oem, _ := collectionDoc["Oem"].(map[string]interface{})
|
|
public, _ := oem["Public"].(map[string]interface{})
|
|
ncsiEnabled := asBool(public["NcsiEnabled"])
|
|
lldp, _ := public["LLDP"].(map[string]interface{})
|
|
lldpMode := strings.TrimSpace(asString(lldp["LLDPMode"]))
|
|
if members, ok := lldp["Members"].([]interface{}); ok {
|
|
for _, item := range members {
|
|
member, ok := item.(map[string]interface{})
|
|
if !ok {
|
|
continue
|
|
}
|
|
ethIndex := strings.ToLower(strings.TrimSpace(asString(member["EthIndex"])))
|
|
if ethIndex == "" {
|
|
continue
|
|
}
|
|
lldpByEth[ethIndex] = member
|
|
}
|
|
}
|
|
return ncsiEnabled, lldpMode, lldpByEth
|
|
}
|
|
|
|
func redfishManagerIPv4Field(doc map[string]interface{}, key string) string {
|
|
if len(doc) == 0 {
|
|
return ""
|
|
}
|
|
for _, field := range []string{"IPv4Addresses", "IPv4StaticAddresses"} {
|
|
list, ok := doc[field].([]interface{})
|
|
if !ok {
|
|
continue
|
|
}
|
|
for _, item := range list {
|
|
entry, ok := item.(map[string]interface{})
|
|
if !ok {
|
|
continue
|
|
}
|
|
value := strings.TrimSpace(asString(entry[key]))
|
|
if value != "" {
|
|
return value
|
|
}
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func redfishManagerIPv6Field(doc map[string]interface{}, key string) string {
|
|
if len(doc) == 0 {
|
|
return ""
|
|
}
|
|
list, ok := doc["IPv6Addresses"].([]interface{})
|
|
if !ok {
|
|
return ""
|
|
}
|
|
for _, item := range list {
|
|
entry, ok := item.(map[string]interface{})
|
|
if !ok {
|
|
continue
|
|
}
|
|
value := strings.TrimSpace(asString(entry[key]))
|
|
if value != "" {
|
|
return value
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func redfishManagerInterfaceScore(summary map[string]any) int {
|
|
score := 0
|
|
if strings.EqualFold(strings.TrimSpace(asString(summary["link_status"])), "LinkActive") {
|
|
score += 100
|
|
}
|
|
if strings.TrimSpace(asString(summary["ipv4_address"])) != "" {
|
|
score += 40
|
|
}
|
|
if strings.TrimSpace(asString(summary["ipv6_address"])) != "" {
|
|
score += 10
|
|
}
|
|
if strings.TrimSpace(asString(summary["mac_address"])) != "" {
|
|
score += 10
|
|
}
|
|
if asInt(summary["speed_mbps"]) > 0 {
|
|
score += 5
|
|
}
|
|
if ifaceID := strings.ToLower(strings.TrimSpace(asString(summary["interface_id"]))); ifaceID != "" && !strings.HasPrefix(ifaceID, "usb") {
|
|
score += 3
|
|
}
|
|
if asBool(summary["ncsi_enabled"]) {
|
|
score += 1
|
|
}
|
|
return score
|
|
}
|
|
|
|
// findNICIndexByLinkedNetworkAdapter resolves a NetworkInterface document to an
|
|
// existing NIC in bySlot by following Links.NetworkAdapter → the Chassis
|
|
// NetworkAdapter doc and reconstructing the canonical NIC identity. Returns -1
|
|
// if no match is found.
|
|
func (r redfishSnapshotReader) findNICIndexByLinkedNetworkAdapter(iface map[string]interface{}, existing []models.NetworkAdapter, bySlot map[string]int) int {
|
|
links, ok := iface["Links"].(map[string]interface{})
|
|
if !ok {
|
|
return -1
|
|
}
|
|
adapterRef, ok := links["NetworkAdapter"].(map[string]interface{})
|
|
if !ok {
|
|
return -1
|
|
}
|
|
adapterPath := normalizeRedfishPath(asString(adapterRef["@odata.id"]))
|
|
if adapterPath == "" {
|
|
return -1
|
|
}
|
|
adapterDoc, err := r.getJSON(adapterPath)
|
|
if err != nil || len(adapterDoc) == 0 {
|
|
return -1
|
|
}
|
|
adapterNIC := r.buildNICFromAdapterDoc(adapterDoc)
|
|
if serial := normalizeRedfishIdentityField(adapterNIC.SerialNumber); serial != "" {
|
|
for idx, nic := range existing {
|
|
if strings.EqualFold(normalizeRedfishIdentityField(nic.SerialNumber), serial) {
|
|
return idx
|
|
}
|
|
}
|
|
}
|
|
if bdf := strings.TrimSpace(adapterNIC.BDF); bdf != "" {
|
|
for idx, nic := range existing {
|
|
if strings.EqualFold(strings.TrimSpace(nic.BDF), bdf) {
|
|
return idx
|
|
}
|
|
}
|
|
}
|
|
if slot := strings.ToLower(strings.TrimSpace(adapterNIC.Slot)); slot != "" {
|
|
if idx, ok := bySlot[slot]; ok {
|
|
return idx
|
|
}
|
|
}
|
|
for idx, nic := range existing {
|
|
if networkAdaptersShareMACs(nic, adapterNIC) {
|
|
return idx
|
|
}
|
|
}
|
|
return -1
|
|
}
|
|
|
|
func networkAdaptersShareMACs(a, b models.NetworkAdapter) bool {
|
|
if len(a.MACAddresses) == 0 || len(b.MACAddresses) == 0 {
|
|
return false
|
|
}
|
|
seen := make(map[string]struct{}, len(a.MACAddresses))
|
|
for _, mac := range a.MACAddresses {
|
|
normalized := strings.ToUpper(strings.TrimSpace(mac))
|
|
if normalized == "" {
|
|
continue
|
|
}
|
|
seen[normalized] = struct{}{}
|
|
}
|
|
for _, mac := range b.MACAddresses {
|
|
normalized := strings.ToUpper(strings.TrimSpace(mac))
|
|
if normalized == "" {
|
|
continue
|
|
}
|
|
if _, ok := seen[normalized]; ok {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// enrichNICMACsFromNetworkDeviceFunctions reads the NetworkDeviceFunctions
|
|
// collection linked from a NetworkAdapter document and populates the NIC's
|
|
// MACAddresses from each function's Ethernet.PermanentMACAddress / MACAddress.
|
|
// Called when PCIe-path enrichment does not produce any MACs.
|
|
func (r redfishSnapshotReader) enrichNICMACsFromNetworkDeviceFunctions(nic *models.NetworkAdapter, adapterDoc map[string]interface{}) {
|
|
ndfCol, ok := adapterDoc["NetworkDeviceFunctions"].(map[string]interface{})
|
|
if !ok {
|
|
return
|
|
}
|
|
colPath := asString(ndfCol["@odata.id"])
|
|
if colPath == "" {
|
|
return
|
|
}
|
|
funcDocs, err := r.getCollectionMembers(colPath)
|
|
if err != nil || len(funcDocs) == 0 {
|
|
return
|
|
}
|
|
for _, fn := range funcDocs {
|
|
eth, _ := fn["Ethernet"].(map[string]interface{})
|
|
if eth == nil {
|
|
continue
|
|
}
|
|
mac := strings.TrimSpace(firstNonEmpty(
|
|
asString(eth["PermanentMACAddress"]),
|
|
asString(eth["MACAddress"]),
|
|
))
|
|
if mac == "" {
|
|
continue
|
|
}
|
|
nic.MACAddresses = dedupeStrings(append(nic.MACAddresses, strings.ToUpper(mac)))
|
|
}
|
|
if len(funcDocs) > 0 && nic.PortCount == 0 {
|
|
nic.PortCount = sanitizeNetworkPortCount(len(funcDocs))
|
|
}
|
|
}
|