export: align reanimator and enrich redfish metrics

This commit is contained in:
Mikhail Chusavitin
2026-03-15 21:38:28 +03:00
parent 0acdc2b202
commit 9007f1b360
17 changed files with 3756 additions and 650 deletions
+588 -20
View File
@@ -417,11 +417,13 @@ func (c *RedfishConnector) collectStorage(ctx context.Context, client *http.Clie
driveDocs, err := c.getCollectionMembers(ctx, client, req, baseURL, driveCollectionPath)
if err == nil {
for _, driveDoc := range driveDocs {
out = append(out, parseDrive(driveDoc))
supplementalDocs := c.getLinkedSupplementalDocs(ctx, client, req, baseURL, driveDoc, "DriveMetrics", "EnvironmentMetrics", "Metrics")
out = append(out, parseDriveWithSupplementalDocs(driveDoc, supplementalDocs...))
}
if len(driveDocs) == 0 {
for _, driveDoc := range c.probeDirectDiskBayChildren(ctx, client, req, baseURL, driveCollectionPath) {
out = append(out, parseDrive(driveDoc))
supplementalDocs := c.getLinkedSupplementalDocs(ctx, client, req, baseURL, driveDoc, "DriveMetrics", "EnvironmentMetrics", "Metrics")
out = append(out, parseDriveWithSupplementalDocs(driveDoc, supplementalDocs...))
}
}
}
@@ -442,14 +444,16 @@ func (c *RedfishConnector) collectStorage(ctx context.Context, client *http.Clie
if err != nil {
continue
}
out = append(out, parseDrive(driveDoc))
supplementalDocs := c.getLinkedSupplementalDocs(ctx, client, req, baseURL, driveDoc, "DriveMetrics", "EnvironmentMetrics", "Metrics")
out = append(out, parseDriveWithSupplementalDocs(driveDoc, supplementalDocs...))
}
continue
}
// Some implementations return drive fields right in storage member object.
if looksLikeDrive(member) {
out = append(out, parseDrive(member))
supplementalDocs := c.getLinkedSupplementalDocs(ctx, client, req, baseURL, member, "DriveMetrics", "EnvironmentMetrics", "Metrics")
out = append(out, parseDriveWithSupplementalDocs(member, supplementalDocs...))
}
// Supermicro/RAID implementations can expose physical disks under chassis enclosures
@@ -459,7 +463,8 @@ func (c *RedfishConnector) collectStorage(ctx context.Context, client *http.Clie
if err == nil {
for _, driveDoc := range driveDocs {
if looksLikeDrive(driveDoc) {
out = append(out, parseDrive(driveDoc))
supplementalDocs := c.getLinkedSupplementalDocs(ctx, client, req, baseURL, driveDoc, "DriveMetrics", "EnvironmentMetrics", "Metrics")
out = append(out, parseDriveWithSupplementalDocs(driveDoc, supplementalDocs...))
}
}
if len(driveDocs) == 0 {
@@ -477,7 +482,8 @@ func (c *RedfishConnector) collectStorage(ctx context.Context, client *http.Clie
"/Storage/IntelVROC/Controllers/1/Drives",
}) {
if looksLikeDrive(driveDoc) {
out = append(out, parseDrive(driveDoc))
supplementalDocs := c.getLinkedSupplementalDocs(ctx, client, req, baseURL, driveDoc, "DriveMetrics", "EnvironmentMetrics", "Metrics")
out = append(out, parseDriveWithSupplementalDocs(driveDoc, supplementalDocs...))
}
}
@@ -493,7 +499,7 @@ func (c *RedfishConnector) collectStorage(ctx context.Context, client *http.Clie
if !ok || !looksLikeDrive(devDoc) {
continue
}
out = append(out, parseDrive(devDoc))
out = append(out, parseDriveWithSupplementalDocs(devDoc))
}
}
@@ -508,7 +514,8 @@ func (c *RedfishConnector) collectStorage(ctx context.Context, client *http.Clie
if !looksLikeDrive(driveDoc) {
continue
}
out = append(out, parseDrive(driveDoc))
supplementalDocs := c.getLinkedSupplementalDocs(ctx, client, req, baseURL, driveDoc, "DriveMetrics", "EnvironmentMetrics", "Metrics")
out = append(out, parseDriveWithSupplementalDocs(driveDoc, supplementalDocs...))
}
}
for _, chassisPath := range chassisPaths {
@@ -519,7 +526,8 @@ func (c *RedfishConnector) collectStorage(ctx context.Context, client *http.Clie
if !looksLikeDrive(driveDoc) {
continue
}
out = append(out, parseDrive(driveDoc))
supplementalDocs := c.getLinkedSupplementalDocs(ctx, client, req, baseURL, driveDoc, "DriveMetrics", "EnvironmentMetrics", "Metrics")
out = append(out, parseDriveWithSupplementalDocs(driveDoc, supplementalDocs...))
}
}
@@ -575,7 +583,11 @@ func (c *RedfishConnector) collectNICs(ctx context.Context, client *http.Client,
continue
}
functionDocs := c.getLinkedPCIeFunctions(ctx, client, req, baseURL, pcieDoc)
enrichNICFromPCIe(&nic, pcieDoc, functionDocs)
supplementalDocs := c.getLinkedSupplementalDocs(ctx, client, req, baseURL, pcieDoc, "EnvironmentMetrics", "Metrics")
for _, fn := range functionDocs {
supplementalDocs = append(supplementalDocs, c.getLinkedSupplementalDocs(ctx, client, req, baseURL, fn, "EnvironmentMetrics", "Metrics")...)
}
enrichNICFromPCIe(&nic, pcieDoc, functionDocs, supplementalDocs)
}
nics = append(nics, nic)
}
@@ -591,7 +603,8 @@ func (c *RedfishConnector) collectPSUs(ctx context.Context, client *http.Client,
// Redfish 2022+/X14+ commonly uses PowerSubsystem as the primary source.
if memberDocs, err := c.getCollectionMembers(ctx, client, req, baseURL, joinPath(chassisPath, "/PowerSubsystem/PowerSupplies")); err == nil && len(memberDocs) > 0 {
for _, doc := range memberDocs {
idx = appendPSU(&out, seen, parsePSU(doc, idx), idx)
supplementalDocs := c.getLinkedSupplementalDocs(ctx, client, req, baseURL, doc, "EnvironmentMetrics", "Metrics")
idx = appendPSU(&out, seen, parsePSUWithSupplementalDocs(doc, idx, supplementalDocs...), idx)
}
continue
}
@@ -604,7 +617,8 @@ func (c *RedfishConnector) collectPSUs(ctx context.Context, client *http.Client,
if !ok {
continue
}
idx = appendPSU(&out, seen, parsePSU(doc, idx), idx)
supplementalDocs := c.getLinkedSupplementalDocs(ctx, client, req, baseURL, doc, "EnvironmentMetrics", "Metrics")
idx = appendPSU(&out, seen, parsePSUWithSupplementalDocs(doc, idx, supplementalDocs...), idx)
}
}
}
@@ -654,6 +668,37 @@ func redfishLinkedPath(doc map[string]interface{}, key string) string {
return ""
}
func (c *RedfishConnector) getLinkedSupplementalDocs(
ctx context.Context,
client *http.Client,
req Request,
baseURL string,
doc map[string]interface{},
keys ...string,
) []map[string]interface{} {
if len(doc) == 0 || len(keys) == 0 {
return nil
}
var out []map[string]interface{}
seen := make(map[string]struct{})
for _, key := range keys {
path := normalizeRedfishPath(redfishLinkedPath(doc, key))
if path == "" {
continue
}
if _, ok := seen[path]; ok {
continue
}
supplementalDoc, err := c.getJSON(ctx, client, req, baseURL, path)
if err != nil || len(supplementalDoc) == 0 {
continue
}
seen[path] = struct{}{}
out = append(out, supplementalDoc)
}
return out
}
func (c *RedfishConnector) collectGPUs(ctx context.Context, client *http.Client, req Request, baseURL string, systemPaths, chassisPaths []string) []models.GPU {
collections := make([]string, 0, len(systemPaths)*3+len(chassisPaths)*2)
for _, systemPath := range systemPaths {
@@ -681,7 +726,11 @@ func (c *RedfishConnector) collectGPUs(ctx context.Context, client *http.Client,
continue
}
gpu := parseGPU(doc, functionDocs, idx)
supplementalDocs := c.getLinkedSupplementalDocs(ctx, client, req, baseURL, doc, "EnvironmentMetrics", "Metrics")
for _, fn := range functionDocs {
supplementalDocs = append(supplementalDocs, c.getLinkedSupplementalDocs(ctx, client, req, baseURL, fn, "EnvironmentMetrics", "Metrics")...)
}
gpu := parseGPUWithSupplementalDocs(doc, functionDocs, supplementalDocs, idx)
idx++
if shouldSkipGenericGPUDuplicate(out, gpu) {
continue
@@ -723,7 +772,11 @@ func (c *RedfishConnector) collectPCIeDevices(ctx context.Context, client *http.
if looksLikeGPU(doc, functionDocs) {
continue
}
dev := parsePCIeDevice(doc, functionDocs)
supplementalDocs := c.getLinkedSupplementalDocs(ctx, client, req, baseURL, doc, "EnvironmentMetrics", "Metrics")
for _, fn := range functionDocs {
supplementalDocs = append(supplementalDocs, c.getLinkedSupplementalDocs(ctx, client, req, baseURL, fn, "EnvironmentMetrics", "Metrics")...)
}
dev := parsePCIeDeviceWithSupplementalDocs(doc, functionDocs, supplementalDocs)
if isUnidentifiablePCIeDevice(dev) {
continue
}
@@ -738,7 +791,8 @@ func (c *RedfishConnector) collectPCIeDevices(ctx context.Context, client *http.
continue
}
for idx, fn := range functionDocs {
dev := parsePCIeFunction(fn, idx+1)
supplementalDocs := c.getLinkedSupplementalDocs(ctx, client, req, baseURL, fn, "EnvironmentMetrics", "Metrics")
dev := parsePCIeFunctionWithSupplementalDocs(fn, supplementalDocs, idx+1)
out = append(out, dev)
}
}
@@ -1333,7 +1387,7 @@ func shouldAdaptiveNVMeProbe(collectionDoc map[string]interface{}) bool {
// RoT and similar component chassis that expose an empty /Drives collection.
func chassisTypeCanHaveNVMe(chassisType string) bool {
switch strings.ToLower(strings.TrimSpace(chassisType)) {
case "module", // GPU SXM, NVLinkManagementNIC, PCIeRetimer
case "module", // GPU SXM, NVLinkManagementNIC, PCIeRetimer
"component", // ERoT, IRoT, BMC, FPGA sub-chassis
"zone": // HGX_Chassis_0 fabric zone
return false
@@ -1945,9 +1999,15 @@ func shouldCrawlPath(path string) bool {
if strings.Contains(normalized, "/Memory/") {
after := strings.SplitN(normalized, "/Memory/", 2)
if len(after) == 2 && strings.Count(after[1], "/") >= 1 {
// Keep direct DIMM resources (/Memory/<slot>) but skip nested subresources
// like /Memory/<slot>/Assembly and /Memory/<slot>/MemoryMetrics.
return false
// Keep direct DIMM resources and selected metrics subresources, but skip
// unrelated nested branches like Assembly.
return strings.HasSuffix(normalized, "/MemoryMetrics")
}
}
if strings.Contains(normalized, "/Processors/") {
after := strings.SplitN(normalized, "/Processors/", 2)
if len(after) == 2 && strings.Count(after[1], "/") >= 1 {
return strings.HasSuffix(normalized, "/ProcessorMetrics")
}
}
// Non-inventory top-level service branches.
@@ -2551,6 +2611,7 @@ func parseCPUs(docs []map[string]interface{}) []models.CPU {
L2CacheKB: l2,
L3CacheKB: l3,
Status: mapStatus(doc["Status"]),
Details: redfishCPUDetails(doc),
})
}
return cpus
@@ -2614,12 +2675,102 @@ func parseMemory(docs []map[string]interface{}) []models.MemoryDIMM {
SerialNumber: findFirstNormalizedStringByKeys(doc, "SerialNumber"),
PartNumber: asString(doc["PartNumber"]),
Status: mapStatus(doc["Status"]),
Details: redfishMemoryDetails(doc),
})
}
return out
}
func redfishCPUDetails(doc map[string]interface{}) map[string]any {
return redfishCPUDetailsAcrossDocs(doc)
}
func redfishCPUDetailsAcrossDocs(doc map[string]interface{}, supplementalDocs ...map[string]interface{}) map[string]any {
lookupDocs := append([]map[string]interface{}{doc}, supplementalDocs...)
details := make(map[string]any)
addFloatDetail(details, "temperature_c", redfishFirstNumericAcrossDocs(lookupDocs,
"TemperatureCelsius", "TemperatureC", "Temperature", "CurrentTemperature", "temperature",
))
addFloatDetail(details, "power_w", redfishFirstNumericAcrossDocs(lookupDocs,
"PowerConsumedWatts", "PowerWatts", "PowerConsumptionWatts",
))
addBoolDetail(details, "throttled", redfishFirstBoolAcrossDocs(lookupDocs,
"Throttled", "ThermalThrottled", "PerformanceThrottled",
))
addInt64Detail(details, "correctable_error_count", redfishFirstInt64AcrossDocs(lookupDocs,
"CorrectableErrorCount", "CorrectableErrors", "CorrectableECCErrorCount",
))
addInt64Detail(details, "uncorrectable_error_count", redfishFirstInt64AcrossDocs(lookupDocs,
"UncorrectableErrorCount", "UncorrectableErrors", "UncorrectableECCErrorCount",
))
addFloatDetail(details, "life_remaining_pct", redfishFirstNumericAcrossDocs(lookupDocs,
"LifeRemainingPercent", "PredictedLifeLeftPercent",
))
addFloatDetail(details, "life_used_pct", redfishFirstNumericAcrossDocs(lookupDocs,
"LifeUsedPercent", "PercentageLifeUsed",
))
for _, lookupDoc := range lookupDocs {
if microcode, ok := redfishLookupValue(lookupDoc, "MicrocodeVersion"); ok {
if s := strings.TrimSpace(asString(microcode)); s != "" {
details["microcode"] = s
break
}
}
if microcode, ok := redfishLookupValue(lookupDoc, "Microcode"); ok {
if s := strings.TrimSpace(asString(microcode)); s != "" {
details["microcode"] = s
break
}
}
}
if len(details) == 0 {
return nil
}
return details
}
func redfishMemoryDetails(doc map[string]interface{}) map[string]any {
return redfishMemoryDetailsAcrossDocs(doc)
}
func redfishMemoryDetailsAcrossDocs(doc map[string]interface{}, supplementalDocs ...map[string]interface{}) map[string]any {
lookupDocs := append([]map[string]interface{}{doc}, supplementalDocs...)
details := make(map[string]any)
addFloatDetail(details, "temperature_c", redfishFirstNumericAcrossDocs(lookupDocs,
"TemperatureCelsius", "TemperatureC", "Temperature", "CurrentTemperature", "temperature",
))
addInt64Detail(details, "correctable_ecc_error_count", redfishFirstInt64AcrossDocs(lookupDocs,
"CorrectableECCErrorCount", "CorrectableErrorCount", "CorrectableErrors",
))
addInt64Detail(details, "uncorrectable_ecc_error_count", redfishFirstInt64AcrossDocs(lookupDocs,
"UncorrectableECCErrorCount", "UncorrectableErrorCount", "UncorrectableErrors",
))
addFloatDetail(details, "life_remaining_pct", redfishFirstNumericAcrossDocs(lookupDocs,
"LifeRemainingPercent", "PredictedLifeLeftPercent",
))
addFloatDetail(details, "life_used_pct", redfishFirstNumericAcrossDocs(lookupDocs,
"LifeUsedPercent", "PercentageLifeUsed",
))
addFloatDetail(details, "spare_blocks_remaining_pct", redfishFirstNumericAcrossDocs(lookupDocs,
"SpareBlocksRemainingPercent", "SpareBlocksRemainingPct",
))
addBoolDetail(details, "performance_degraded", redfishFirstBoolAcrossDocs(lookupDocs,
"PerformanceDegraded", "Degraded",
))
addBoolDetail(details, "data_loss_detected", redfishFirstBoolAcrossDocs(lookupDocs,
"DataLossDetected", "DataLoss",
))
if len(details) == 0 {
return nil
}
return details
}
func parseDrive(doc map[string]interface{}) models.Storage {
return parseDriveWithSupplementalDocs(doc)
}
func parseDriveWithSupplementalDocs(doc map[string]interface{}, supplementalDocs ...map[string]interface{}) models.Storage {
sizeGB := 0
if capBytes := asInt64(doc["CapacityBytes"]); capBytes > 0 {
sizeGB = int(capBytes / (1024 * 1024 * 1024))
@@ -2644,6 +2795,7 @@ func parseDrive(doc map[string]interface{}) models.Storage {
Firmware: asString(doc["Revision"]),
Interface: asString(doc["Protocol"]),
Present: true,
Details: redfishDriveDetailsWithSupplementalDocs(doc, supplementalDocs...),
}
}
@@ -2744,6 +2896,7 @@ func parseNIC(doc map[string]interface{}) models.NetworkAdapter {
Slot: firstNonEmpty(asString(doc["Id"]), asString(doc["Name"])),
Location: location,
Present: !strings.EqualFold(mapStatus(doc["Status"]), "Absent"),
BDF: asString(doc["BDF"]),
Model: strings.TrimSpace(model),
Vendor: strings.TrimSpace(vendor),
VendorID: vendorID,
@@ -2753,6 +2906,7 @@ func parseNIC(doc map[string]interface{}) models.NetworkAdapter {
Firmware: firmware,
PortCount: portCount,
Status: mapStatus(doc["Status"]),
Details: redfishPCIeDetails(doc, nil),
}
}
@@ -2786,23 +2940,53 @@ func networkAdapterPCIeDevicePaths(doc map[string]interface{}) []string {
return out
}
func enrichNICFromPCIe(nic *models.NetworkAdapter, pcieDoc map[string]interface{}, functionDocs []map[string]interface{}) {
func enrichNICFromPCIe(nic *models.NetworkAdapter, pcieDoc map[string]interface{}, functionDocs []map[string]interface{}, supplementalDocs []map[string]interface{}) {
if nic == nil {
return
}
if strings.TrimSpace(nic.BDF) == "" {
nic.BDF = firstNonEmpty(asString(pcieDoc["BDF"]), buildBDFfromOemPublic(pcieDoc))
}
if nic.VendorID == 0 {
nic.VendorID = asHexOrInt(pcieDoc["VendorId"])
}
if nic.DeviceID == 0 {
nic.DeviceID = asHexOrInt(pcieDoc["DeviceId"])
}
if nic.LinkWidth == 0 {
nic.LinkWidth = asInt(pcieDoc["CurrentLinkWidth"])
}
if nic.MaxLinkWidth == 0 {
nic.MaxLinkWidth = asInt(pcieDoc["MaxLinkWidth"])
}
if strings.TrimSpace(nic.LinkSpeed) == "" {
nic.LinkSpeed = firstNonEmpty(asString(pcieDoc["CurrentLinkSpeedGTs"]), asString(pcieDoc["CurrentLinkSpeed"]))
}
if strings.TrimSpace(nic.MaxLinkSpeed) == "" {
nic.MaxLinkSpeed = firstNonEmpty(asString(pcieDoc["MaxLinkSpeedGTs"]), asString(pcieDoc["MaxLinkSpeed"]))
}
for _, fn := range functionDocs {
if strings.TrimSpace(nic.BDF) == "" {
nic.BDF = asString(fn["FunctionId"])
}
if nic.VendorID == 0 {
nic.VendorID = asHexOrInt(fn["VendorId"])
}
if nic.DeviceID == 0 {
nic.DeviceID = asHexOrInt(fn["DeviceId"])
}
if nic.LinkWidth == 0 {
nic.LinkWidth = asInt(fn["CurrentLinkWidth"])
}
if nic.MaxLinkWidth == 0 {
nic.MaxLinkWidth = asInt(fn["MaxLinkWidth"])
}
if strings.TrimSpace(nic.LinkSpeed) == "" {
nic.LinkSpeed = firstNonEmpty(asString(fn["CurrentLinkSpeedGTs"]), asString(fn["CurrentLinkSpeed"]))
}
if strings.TrimSpace(nic.MaxLinkSpeed) == "" {
nic.MaxLinkSpeed = firstNonEmpty(asString(fn["MaxLinkSpeedGTs"]), asString(fn["MaxLinkSpeed"]))
}
}
if strings.TrimSpace(nic.Vendor) == "" {
nic.Vendor = pciids.VendorName(nic.VendorID)
@@ -2812,9 +2996,14 @@ func enrichNICFromPCIe(nic *models.NetworkAdapter, pcieDoc map[string]interface{
nic.Model = resolved
}
}
nic.Details = mergeGenericDetails(nic.Details, redfishPCIeDetailsWithSupplementalDocs(pcieDoc, functionDocs, supplementalDocs))
}
func parsePSU(doc map[string]interface{}, idx int) models.PSU {
return parsePSUWithSupplementalDocs(doc, idx)
}
func parsePSUWithSupplementalDocs(doc map[string]interface{}, idx int, supplementalDocs ...map[string]interface{}) models.PSU {
status := mapStatus(doc["Status"])
present := true
if statusMap, ok := doc["Status"].(map[string]interface{}); ok {
@@ -2852,10 +3041,334 @@ func parsePSU(doc map[string]interface{}, idx int) models.PSU {
InputPowerW: asInt(doc["PowerInputWatts"]),
OutputPowerW: asInt(doc["LastPowerOutputWatts"]),
InputVoltage: asFloat(doc["LineInputVoltage"]),
Details: redfishPSUDetailsWithSupplementalDocs(doc, supplementalDocs...),
}
}
func redfishDriveDetails(doc map[string]interface{}) map[string]any {
return redfishDriveDetailsWithSupplementalDocs(doc)
}
func redfishDriveDetailsWithSupplementalDocs(doc map[string]interface{}, supplementalDocs ...map[string]interface{}) map[string]any {
details := make(map[string]any)
lookupDocs := append([]map[string]interface{}{doc}, supplementalDocs...)
addFloatDetail(details, "temperature_c", redfishFirstNumericAcrossDocs(lookupDocs,
"TemperatureCelsius", "TemperatureC", "Temperature", "CurrentTemperature", "temperature",
))
addInt64Detail(details, "power_on_hours", redfishFirstInt64AcrossDocs(lookupDocs,
"PowerOnHours", "PowerOnHour",
))
addInt64Detail(details, "power_cycles", redfishFirstInt64AcrossDocs(lookupDocs,
"PowerCycles", "PowerCycleCount",
))
addInt64Detail(details, "unsafe_shutdowns", redfishFirstInt64AcrossDocs(lookupDocs,
"UnsafeShutdowns", "UnsafeShutdownCount",
))
addInt64Detail(details, "media_errors", redfishFirstInt64AcrossDocs(lookupDocs,
"MediaErrors", "MediaErrorCount",
))
addInt64Detail(details, "error_log_entries", redfishFirstInt64AcrossDocs(lookupDocs,
"ErrorLogEntries", "ErrorLogEntryCount",
))
addInt64Detail(details, "written_bytes", redfishFirstInt64AcrossDocs(lookupDocs,
"WrittenBytes", "BytesWritten",
))
addInt64Detail(details, "read_bytes", redfishFirstInt64AcrossDocs(lookupDocs,
"ReadBytes", "BytesRead",
))
addFloatDetail(details, "life_remaining_pct", redfishFirstNumericAcrossDocs(lookupDocs,
"PredictedMediaLifeLeftPercent", "LifeRemainingPercent", "PercentageDriveLifeUsedInverse", "PercentLifeRemaining",
))
addFloatDetail(details, "life_used_pct", redfishFirstNumericAcrossDocs(lookupDocs,
"PercentageDriveLifeUsed", "LifeUsedPercent", "PercentageUsed", "PercentLifeUsed",
))
addFloatDetail(details, "available_spare_pct", redfishFirstNumericAcrossDocs(lookupDocs,
"AvailableSparePercent", "AvailableSpare", "PercentAvailableSpare",
))
addInt64Detail(details, "reallocated_sectors", redfishFirstInt64AcrossDocs(lookupDocs,
"ReallocatedSectors", "ReallocatedSectorCount",
))
addInt64Detail(details, "current_pending_sectors", redfishFirstInt64AcrossDocs(lookupDocs,
"CurrentPendingSectors", "CurrentPendingSectorCount",
))
addInt64Detail(details, "offline_uncorrectable", redfishFirstInt64AcrossDocs(lookupDocs,
"OfflineUncorrectable", "OfflineUncorrectableSectorCount",
))
if len(details) == 0 {
return nil
}
return details
}
func redfishPSUDetails(doc map[string]interface{}) map[string]any {
return redfishPSUDetailsWithSupplementalDocs(doc)
}
func redfishPSUDetailsWithSupplementalDocs(doc map[string]interface{}, supplementalDocs ...map[string]interface{}) map[string]any {
details := make(map[string]any)
lookupDocs := append([]map[string]interface{}{doc}, supplementalDocs...)
addFloatDetail(details, "temperature_c", redfishFirstNumericAcrossDocs(lookupDocs,
"TemperatureCelsius", "TemperatureC", "Temperature",
))
addFloatDetail(details, "life_remaining_pct", redfishFirstNumericAcrossDocs(lookupDocs,
"LifeRemainingPercent", "PredictedLifeLeftPercent",
))
addFloatDetail(details, "life_used_pct", redfishFirstNumericAcrossDocs(lookupDocs,
"LifeUsedPercent", "PercentageLifeUsed",
))
if len(details) == 0 {
return nil
}
return details
}
func redfishPCIeDetails(doc map[string]interface{}, functionDocs []map[string]interface{}) map[string]any {
return redfishPCIeDetailsWithSupplementalDocs(doc, functionDocs, nil)
}
func redfishPCIeDetailsWithSupplementalDocs(doc map[string]interface{}, functionDocs []map[string]interface{}, supplementalDocs []map[string]interface{}) map[string]any {
lookupDocs := make([]map[string]interface{}, 0, 1+len(functionDocs)+len(supplementalDocs))
lookupDocs = append(lookupDocs, doc)
lookupDocs = append(lookupDocs, functionDocs...)
lookupDocs = append(lookupDocs, supplementalDocs...)
details := make(map[string]any)
addFloatDetail(details, "temperature_c", redfishFirstNumericAcrossDocs(lookupDocs,
"TemperatureCelsius", "TemperatureC", "Temperature",
))
addFloatDetail(details, "power_w", redfishFirstNumericAcrossDocs(lookupDocs,
"PowerConsumedWatts", "PowerWatts", "PowerConsumptionWatts",
))
addFloatDetail(details, "life_remaining_pct", redfishFirstNumericAcrossDocs(lookupDocs,
"LifeRemainingPercent", "PredictedLifeLeftPercent",
))
addFloatDetail(details, "life_used_pct", redfishFirstNumericAcrossDocs(lookupDocs,
"LifeUsedPercent", "PercentageLifeUsed",
))
addInt64Detail(details, "ecc_corrected_total", redfishFirstInt64AcrossDocs(lookupDocs,
"ECCCorrectedTotal", "CorrectableECCErrorCount", "CorrectableErrorCount",
))
addInt64Detail(details, "ecc_uncorrected_total", redfishFirstInt64AcrossDocs(lookupDocs,
"ECCUncorrectedTotal", "UncorrectableECCErrorCount", "UncorrectableErrorCount",
))
addBoolDetail(details, "hw_slowdown", redfishFirstBoolAcrossDocs(lookupDocs,
"HWSlowdown", "HardwareSlowdown",
))
addFloatDetail(details, "battery_charge_pct", redfishFirstNumericAcrossDocs(lookupDocs,
"BatteryChargePercent", "BatteryChargePct",
))
addFloatDetail(details, "battery_health_pct", redfishFirstNumericAcrossDocs(lookupDocs,
"BatteryHealthPercent", "BatteryHealthPct",
))
addFloatDetail(details, "battery_temperature_c", redfishFirstNumericAcrossDocs(lookupDocs,
"BatteryTemperatureCelsius", "BatteryTemperatureC",
))
addFloatDetail(details, "battery_voltage_v", redfishFirstNumericAcrossDocs(lookupDocs,
"BatteryVoltage", "BatteryVoltageV",
))
addBoolDetail(details, "battery_replace_required", redfishFirstBoolAcrossDocs(lookupDocs,
"BatteryReplaceRequired", "ReplaceBattery",
))
addFloatDetail(details, "sfp_temperature_c", redfishFirstNumericAcrossDocs(lookupDocs,
"SFPTemperatureCelsius", "SFPTemperatureC", "TransceiverTemperatureCelsius",
))
addFloatDetail(details, "sfp_tx_power_dbm", redfishFirstNumericAcrossDocs(lookupDocs,
"SFPTXPowerDBm", "SFPTransmitPowerDBm", "TxPowerDBm",
))
addFloatDetail(details, "sfp_rx_power_dbm", redfishFirstNumericAcrossDocs(lookupDocs,
"SFPRXPowerDBm", "SFPReceivePowerDBm", "RxPowerDBm",
))
addFloatDetail(details, "sfp_voltage_v", redfishFirstNumericAcrossDocs(lookupDocs,
"SFPVoltageV", "TransceiverVoltageV",
))
addFloatDetail(details, "sfp_bias_ma", redfishFirstNumericAcrossDocs(lookupDocs,
"SFPBiasMA", "BiasCurrentMA", "LaserBiasCurrentMA",
))
if len(details) == 0 {
return nil
}
return details
}
func redfishFirstNumeric(doc map[string]interface{}, keys ...string) float64 {
for _, key := range keys {
if v, ok := redfishLookupValue(doc, key); ok {
if f := asFloat(v); f != 0 {
return f
}
}
}
return 0
}
func redfishFirstNumericAcrossDocs(docs []map[string]interface{}, keys ...string) float64 {
for _, doc := range docs {
if v := redfishFirstNumeric(doc, keys...); v != 0 {
return v
}
}
return 0
}
func redfishFirstNumericWithFunctions(doc map[string]interface{}, functionDocs []map[string]interface{}, keys ...string) float64 {
if v := redfishFirstNumeric(doc, keys...); v != 0 {
return v
}
for _, fn := range functionDocs {
if v := redfishFirstNumeric(fn, keys...); v != 0 {
return v
}
}
return 0
}
func redfishFirstInt64(doc map[string]interface{}, keys ...string) int64 {
for _, key := range keys {
if v, ok := redfishLookupValue(doc, key); ok {
if n := asInt64(v); n != 0 {
return n
}
if n := int64(asInt(v)); n != 0 {
return n
}
}
}
return 0
}
func redfishFirstInt64AcrossDocs(docs []map[string]interface{}, keys ...string) int64 {
for _, doc := range docs {
if v := redfishFirstInt64(doc, keys...); v != 0 {
return v
}
}
return 0
}
func redfishFirstInt64WithFunctions(doc map[string]interface{}, functionDocs []map[string]interface{}, keys ...string) int64 {
if v := redfishFirstInt64(doc, keys...); v != 0 {
return v
}
for _, fn := range functionDocs {
if v := redfishFirstInt64(fn, keys...); v != 0 {
return v
}
}
return 0
}
func redfishFirstBoolAcrossDocs(docs []map[string]interface{}, keys ...string) *bool {
for _, doc := range docs {
if v := redfishFirstBool(doc, keys...); v != nil {
return v
}
}
return nil
}
func redfishLookupValue(doc map[string]interface{}, key string) (any, bool) {
if doc == nil || strings.TrimSpace(key) == "" {
return nil, false
}
if v, ok := doc[key]; ok {
return v, true
}
if oem, ok := doc["Oem"].(map[string]interface{}); ok {
if v, ok := redfishLookupNestedValue(oem, key); ok {
return v, true
}
}
return nil, false
}
func redfishFirstBool(doc map[string]interface{}, keys ...string) *bool {
for _, key := range keys {
if v, ok := redfishLookupValue(doc, key); ok {
if b, ok := asBoolPtr(v); ok {
return &b
}
}
}
return nil
}
func redfishFirstBoolWithFunctions(doc map[string]interface{}, functionDocs []map[string]interface{}, keys ...string) *bool {
if v := redfishFirstBool(doc, keys...); v != nil {
return v
}
for _, fn := range functionDocs {
if v := redfishFirstBool(fn, keys...); v != nil {
return v
}
}
return nil
}
func redfishLookupNestedValue(doc map[string]interface{}, key string) (any, bool) {
if doc == nil {
return nil, false
}
if v, ok := doc[key]; ok {
return v, true
}
for _, value := range doc {
nested, ok := value.(map[string]interface{})
if !ok {
continue
}
if v, ok := redfishLookupNestedValue(nested, key); ok {
return v, true
}
}
return nil, false
}
func addFloatDetail(dst map[string]any, key string, value float64) {
if value == 0 {
return
}
dst[key] = value
}
func addInt64Detail(dst map[string]any, key string, value int64) {
if value == 0 {
return
}
dst[key] = value
}
func addBoolDetail(dst map[string]any, key string, value *bool) {
if value == nil {
return
}
dst[key] = *value
}
func asBoolPtr(v any) (bool, bool) {
switch x := v.(type) {
case bool:
return x, true
case string:
switch strings.ToLower(strings.TrimSpace(x)) {
case "true", "yes", "enabled", "1":
return true, true
case "false", "no", "disabled", "0":
return false, true
}
case float64:
return x != 0, true
case int:
return x != 0, true
case int64:
return x != 0, true
}
return false, false
}
func parseGPU(doc map[string]interface{}, functionDocs []map[string]interface{}, idx int) models.GPU {
return parseGPUWithSupplementalDocs(doc, functionDocs, nil, idx)
}
func parseGPUWithSupplementalDocs(doc map[string]interface{}, functionDocs []map[string]interface{}, supplementalDocs []map[string]interface{}, idx int) models.GPU {
slot := firstNonEmpty(
redfishLocationLabel(doc["Slot"]),
redfishLocationLabel(doc["Location"]),
@@ -2876,6 +3389,7 @@ func parseGPU(doc map[string]interface{}, functionDocs []map[string]interface{},
PartNumber: asString(doc["PartNumber"]),
Firmware: asString(doc["FirmwareVersion"]),
Status: mapStatus(doc["Status"]),
Details: redfishPCIeDetailsWithSupplementalDocs(doc, functionDocs, supplementalDocs),
}
if bdf := asString(doc["BDF"]); bdf != "" {
@@ -2928,6 +3442,10 @@ func parseGPU(doc map[string]interface{}, functionDocs []map[string]interface{},
}
func parsePCIeDevice(doc map[string]interface{}, functionDocs []map[string]interface{}) models.PCIeDevice {
return parsePCIeDeviceWithSupplementalDocs(doc, functionDocs, nil)
}
func parsePCIeDeviceWithSupplementalDocs(doc map[string]interface{}, functionDocs []map[string]interface{}, supplementalDocs []map[string]interface{}) models.PCIeDevice {
dev := models.PCIeDevice{
Slot: firstNonEmpty(redfishLocationLabel(doc["Slot"]), redfishLocationLabel(doc["Location"]), asString(doc["Name"]), asString(doc["Id"])),
BDF: asString(doc["BDF"]),
@@ -2937,6 +3455,7 @@ func parsePCIeDevice(doc map[string]interface{}, functionDocs []map[string]inter
SerialNumber: findFirstNormalizedStringByKeys(doc, "SerialNumber"),
VendorID: asHexOrInt(doc["VendorId"]),
DeviceID: asHexOrInt(doc["DeviceId"]),
Details: redfishPCIeDetailsWithSupplementalDocs(doc, functionDocs, supplementalDocs),
}
if strings.TrimSpace(dev.BDF) == "" {
dev.BDF = buildBDFfromOemPublic(doc)
@@ -2992,6 +3511,10 @@ func parsePCIeDevice(doc map[string]interface{}, functionDocs []map[string]inter
}
func parsePCIeFunction(doc map[string]interface{}, idx int) models.PCIeDevice {
return parsePCIeFunctionWithSupplementalDocs(doc, nil, idx)
}
func parsePCIeFunctionWithSupplementalDocs(doc map[string]interface{}, supplementalDocs []map[string]interface{}, idx int) models.PCIeDevice {
slot := firstNonEmpty(redfishLocationLabel(doc["Location"]), asString(doc["Id"]), asString(doc["Name"]))
if slot == "" {
slot = fmt.Sprintf("PCIeFn%d", idx)
@@ -3009,6 +3532,7 @@ func parsePCIeFunction(doc map[string]interface{}, idx int) models.PCIeDevice {
LinkSpeed: firstNonEmpty(asString(doc["CurrentLinkSpeedGTs"]), asString(doc["CurrentLinkSpeed"])),
MaxLinkWidth: asInt(doc["MaxLinkWidth"]),
MaxLinkSpeed: firstNonEmpty(asString(doc["MaxLinkSpeedGTs"]), asString(doc["MaxLinkSpeed"])),
Details: redfishPCIeDetailsWithSupplementalDocs(doc, nil, supplementalDocs),
}
if isGenericPCIeClassLabel(dev.DeviceClass) {
if resolved := pciids.DeviceName(dev.VendorID, dev.DeviceID); resolved != "" {
@@ -3552,8 +4076,10 @@ func storageIdentityKey(item models.Storage) string {
func richerStorageEntry(a, b models.Storage) models.Storage {
if storageRichnessScore(b) > storageRichnessScore(a) {
b.Details = mergeGenericDetails(b.Details, a.Details)
return b
}
a.Details = mergeGenericDetails(a.Details, b.Details)
return a
}
@@ -3815,6 +4341,9 @@ func networkAdapterRichnessScore(nic models.NetworkAdapter) int {
if normalizeRedfishIdentityField(nic.Firmware) != "" {
score += 8
}
if looksLikeCanonicalBDF(strings.TrimSpace(nic.BDF)) {
score += 10
}
if normalizeRedfishIdentityField(nic.PartNumber) != "" {
score += 6
}
@@ -3827,6 +4356,12 @@ func networkAdapterRichnessScore(nic models.NetworkAdapter) int {
if nic.PortCount > 0 {
score += 4
}
if nic.LinkWidth > 0 || nic.MaxLinkWidth > 0 {
score += 4
}
if strings.TrimSpace(nic.LinkSpeed) != "" || strings.TrimSpace(nic.MaxLinkSpeed) != "" {
score += 4
}
if len(nic.MACAddresses) > 0 {
score += 4
}
@@ -3853,6 +4388,9 @@ func mergeNetworkAdapterEntries(a, b models.NetworkAdapter) models.NetworkAdapte
if strings.TrimSpace(out.Location) == "" && strings.TrimSpace(donor.Location) != "" {
out.Location = donor.Location
}
if strings.TrimSpace(out.BDF) == "" && strings.TrimSpace(donor.BDF) != "" {
out.BDF = donor.BDF
}
if normalizeNetworkAdapterModel(out) == "" && normalizeNetworkAdapterModel(donor) != "" {
out.Model = donor.Model
}
@@ -3883,6 +4421,18 @@ func mergeNetworkAdapterEntries(a, b models.NetworkAdapter) models.NetworkAdapte
if strings.TrimSpace(out.PortType) == "" && strings.TrimSpace(donor.PortType) != "" {
out.PortType = donor.PortType
}
if out.LinkWidth == 0 && donor.LinkWidth > 0 {
out.LinkWidth = donor.LinkWidth
}
if strings.TrimSpace(out.LinkSpeed) == "" && strings.TrimSpace(donor.LinkSpeed) != "" {
out.LinkSpeed = donor.LinkSpeed
}
if out.MaxLinkWidth == 0 && donor.MaxLinkWidth > 0 {
out.MaxLinkWidth = donor.MaxLinkWidth
}
if strings.TrimSpace(out.MaxLinkSpeed) == "" && strings.TrimSpace(donor.MaxLinkSpeed) != "" {
out.MaxLinkSpeed = donor.MaxLinkSpeed
}
if strings.TrimSpace(out.Status) == "" && strings.TrimSpace(donor.Status) != "" {
out.Status = donor.Status
}
@@ -3890,6 +4440,7 @@ func mergeNetworkAdapterEntries(a, b models.NetworkAdapter) models.NetworkAdapte
if len(donor.MACAddresses) > 0 {
out.MACAddresses = dedupeStrings(append(append([]string{}, out.MACAddresses...), donor.MACAddresses...))
}
out.Details = mergeGenericDetails(out.Details, donor.Details)
return out
}
@@ -4041,6 +4592,7 @@ func mergePCIeDeviceEntries(a, b models.PCIeDevice) models.PCIeDevice {
if len(donor.MACAddresses) > 0 {
out.MACAddresses = dedupeStrings(append(append([]string{}, out.MACAddresses...), donor.MACAddresses...))
}
out.Details = mergeGenericDetails(out.Details, donor.Details)
return out
}
@@ -4147,9 +4699,25 @@ func mergePSUEntries(a, b models.PSU) models.PSU {
if out.TemperatureC == 0 && donor.TemperatureC > 0 {
out.TemperatureC = donor.TemperatureC
}
out.Details = mergeGenericDetails(out.Details, donor.Details)
return out
}
func mergeGenericDetails(primary, secondary map[string]any) map[string]any {
if len(secondary) == 0 {
return primary
}
if primary == nil {
primary = make(map[string]any, len(secondary))
}
for key, value := range secondary {
if _, ok := primary[key]; !ok {
primary[key] = value
}
}
return primary
}
func dedupeStorageVolumes(items []models.StorageVolume) []models.StorageVolume {
seen := make(map[string]struct{}, len(items))
out := make([]models.StorageVolume, 0, len(items))