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, 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 { nic := parseNIC(doc) for _, pciePath := range networkAdapterPCIeDevicePaths(doc) { pcieDoc, err := r.getJSON(pciePath) if err != nil { continue } functionDocs := r.getLinkedPCIeFunctions(pcieDoc) 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, doc) } nics = append(nics, nic) } } return dedupeNetworkAdapters(nics) } 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 } 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 isUnidentifiablePCIeDevice(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) out = append(out, dev) } } return dedupePCIeDevices(out) } func (r redfishSnapshotReader) getChassisScopedPCIeSupplementalDocs(doc map[string]interface{}) []map[string]interface{} { if !looksLikeNVSwitchPCIeDoc(doc) { return nil } docPath := normalizeRedfishPath(asString(doc["@odata.id"])) chassisPath := chassisPathForPCIeDoc(docPath) if chassisPath == "" { return nil } out := make([]map[string]interface{}, 0, 4) 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) } 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 → its slot label. Returns -1 if no match is found. func (r redfishSnapshotReader) findNICIndexByLinkedNetworkAdapter(iface map[string]interface{}, 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 := parseNIC(adapterDoc) if slot := strings.ToLower(strings.TrimSpace(adapterNIC.Slot)); slot != "" { if idx, ok := bySlot[slot]; ok { return idx } } return -1 } // 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)) } }