397 lines
12 KiB
Go
397 lines
12 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, 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))
|
|
}
|
|
}
|