export: align reanimator and enrich redfish metrics
This commit is contained in:
+588
-20
@@ -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))
|
||||
|
||||
@@ -62,8 +62,8 @@ func ReplayRedfishFromRawPayloads(rawPayloads map[string]any, emit ProgressFn) (
|
||||
if emit != nil {
|
||||
emit(Progress{Status: "running", Progress: 55, Message: "Redfish snapshot: replay CPU/RAM/Storage..."})
|
||||
}
|
||||
processors, _ := r.getCollectionMembers(joinPath(primarySystem, "/Processors"))
|
||||
memory, _ := r.getCollectionMembers(joinPath(primarySystem, "/Memory"))
|
||||
processors := r.collectProcessors(primarySystem)
|
||||
memory := r.collectMemory(primarySystem)
|
||||
storageDevices := r.collectStorage(primarySystem)
|
||||
storageVolumes := r.collectStorageVolumes(primarySystem)
|
||||
|
||||
@@ -101,8 +101,8 @@ func ReplayRedfishFromRawPayloads(rawPayloads map[string]any, emit ProgressFn) (
|
||||
RawPayloads: cloneRawPayloads(rawPayloads),
|
||||
Hardware: &models.HardwareConfig{
|
||||
BoardInfo: boardInfo,
|
||||
CPUs: parseCPUs(processors),
|
||||
Memory: parseMemory(memory),
|
||||
CPUs: processors,
|
||||
Memory: memory,
|
||||
Storage: storageDevices,
|
||||
Volumes: storageVolumes,
|
||||
PCIeDevices: pcieDevices,
|
||||
@@ -977,6 +977,77 @@ func (r redfishSnapshotReader) getLinkedPCIeFunctions(doc map[string]interface{}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r redfishSnapshotReader) getLinkedSupplementalDocs(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 := r.getJSON(path)
|
||||
if err != nil || len(supplementalDoc) == 0 {
|
||||
continue
|
||||
}
|
||||
seen[path] = struct{}{}
|
||||
out = append(out, supplementalDoc)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func (r redfishSnapshotReader) collectProcessors(systemPath string) []models.CPU {
|
||||
memberDocs, err := r.getCollectionMembers(joinPath(systemPath, "/Processors"))
|
||||
if err != nil || len(memberDocs) == 0 {
|
||||
return nil
|
||||
}
|
||||
out := make([]models.CPU, 0, len(memberDocs))
|
||||
socketIdx := 0
|
||||
for _, doc := range memberDocs {
|
||||
if pt := strings.TrimSpace(asString(doc["ProcessorType"])); pt != "" &&
|
||||
!strings.EqualFold(pt, "CPU") && !strings.EqualFold(pt, "General") {
|
||||
continue
|
||||
}
|
||||
cpu := parseCPUs([]map[string]interface{}{doc})[0]
|
||||
if cpu.Socket == 0 && socketIdx > 0 && strings.TrimSpace(asString(doc["Socket"])) == "" {
|
||||
cpu.Socket = socketIdx
|
||||
if cpu.Details == nil {
|
||||
cpu.Details = map[string]any{}
|
||||
}
|
||||
cpu.Details["socket"] = cpu.Socket
|
||||
}
|
||||
supplementalDocs := r.getLinkedSupplementalDocs(doc, "ProcessorMetrics", "EnvironmentMetrics", "Metrics")
|
||||
if len(supplementalDocs) > 0 {
|
||||
cpu.Details = mergeGenericDetails(cpu.Details, redfishCPUDetailsAcrossDocs(doc, supplementalDocs...))
|
||||
}
|
||||
out = append(out, cpu)
|
||||
socketIdx++
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func (r redfishSnapshotReader) collectMemory(systemPath string) []models.MemoryDIMM {
|
||||
memberDocs, err := r.getCollectionMembers(joinPath(systemPath, "/Memory"))
|
||||
if err != nil || len(memberDocs) == 0 {
|
||||
return nil
|
||||
}
|
||||
out := make([]models.MemoryDIMM, 0, len(memberDocs))
|
||||
for _, doc := range memberDocs {
|
||||
dimm := parseMemory([]map[string]interface{}{doc})[0]
|
||||
supplementalDocs := r.getLinkedSupplementalDocs(doc, "MemoryMetrics", "EnvironmentMetrics", "Metrics")
|
||||
if len(supplementalDocs) > 0 {
|
||||
dimm.Details = mergeGenericDetails(dimm.Details, redfishMemoryDetailsAcrossDocs(doc, supplementalDocs...))
|
||||
}
|
||||
out = append(out, dimm)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func (r redfishSnapshotReader) collectStorage(systemPath string) []models.Storage {
|
||||
var out []models.Storage
|
||||
storageMembers, _ := r.getCollectionMembers(joinPath(systemPath, "/Storage"))
|
||||
@@ -987,12 +1058,14 @@ func (r redfishSnapshotReader) collectStorage(systemPath string) []models.Storag
|
||||
if err == nil {
|
||||
for _, driveDoc := range driveDocs {
|
||||
if !isVirtualStorageDrive(driveDoc) {
|
||||
out = append(out, parseDrive(driveDoc))
|
||||
supplementalDocs := r.getLinkedSupplementalDocs(driveDoc, "DriveMetrics", "EnvironmentMetrics", "Metrics")
|
||||
out = append(out, parseDriveWithSupplementalDocs(driveDoc, supplementalDocs...))
|
||||
}
|
||||
}
|
||||
if len(driveDocs) == 0 {
|
||||
for _, driveDoc := range r.probeDirectDiskBayChildren(driveCollectionPath) {
|
||||
out = append(out, parseDrive(driveDoc))
|
||||
supplementalDocs := r.getLinkedSupplementalDocs(driveDoc, "DriveMetrics", "EnvironmentMetrics", "Metrics")
|
||||
out = append(out, parseDriveWithSupplementalDocs(driveDoc, supplementalDocs...))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1014,13 +1087,15 @@ func (r redfishSnapshotReader) collectStorage(systemPath string) []models.Storag
|
||||
continue
|
||||
}
|
||||
if !isVirtualStorageDrive(driveDoc) {
|
||||
out = append(out, parseDrive(driveDoc))
|
||||
supplementalDocs := r.getLinkedSupplementalDocs(driveDoc, "DriveMetrics", "EnvironmentMetrics", "Metrics")
|
||||
out = append(out, parseDriveWithSupplementalDocs(driveDoc, supplementalDocs...))
|
||||
}
|
||||
}
|
||||
continue
|
||||
}
|
||||
if looksLikeDrive(member) {
|
||||
out = append(out, parseDrive(member))
|
||||
supplementalDocs := r.getLinkedSupplementalDocs(member, "DriveMetrics", "EnvironmentMetrics", "Metrics")
|
||||
out = append(out, parseDriveWithSupplementalDocs(member, supplementalDocs...))
|
||||
}
|
||||
|
||||
for _, enclosurePath := range redfishLinkRefs(member, "Links", "Enclosures") {
|
||||
@@ -1028,7 +1103,8 @@ func (r redfishSnapshotReader) collectStorage(systemPath string) []models.Storag
|
||||
if err == nil {
|
||||
for _, driveDoc := range driveDocs {
|
||||
if looksLikeDrive(driveDoc) {
|
||||
out = append(out, parseDrive(driveDoc))
|
||||
supplementalDocs := r.getLinkedSupplementalDocs(driveDoc, "DriveMetrics", "EnvironmentMetrics", "Metrics")
|
||||
out = append(out, parseDriveWithSupplementalDocs(driveDoc, supplementalDocs...))
|
||||
}
|
||||
}
|
||||
if len(driveDocs) == 0 {
|
||||
@@ -1045,7 +1121,8 @@ func (r redfishSnapshotReader) collectStorage(systemPath string) []models.Storag
|
||||
"/Storage/IntelVROC/Controllers/1/Drives",
|
||||
}) {
|
||||
if looksLikeDrive(driveDoc) {
|
||||
out = append(out, parseDrive(driveDoc))
|
||||
supplementalDocs := r.getLinkedSupplementalDocs(driveDoc, "DriveMetrics", "EnvironmentMetrics", "Metrics")
|
||||
out = append(out, parseDriveWithSupplementalDocs(driveDoc, supplementalDocs...))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1165,7 +1242,11 @@ func (r redfishSnapshotReader) collectNICs(chassisPaths []string) []models.Netwo
|
||||
continue
|
||||
}
|
||||
functionDocs := r.getLinkedPCIeFunctions(pcieDoc)
|
||||
enrichNICFromPCIe(&nic, pcieDoc, functionDocs)
|
||||
supplementalDocs := r.getLinkedSupplementalDocs(pcieDoc, "EnvironmentMetrics", "Metrics")
|
||||
for _, fn := range functionDocs {
|
||||
supplementalDocs = append(supplementalDocs, r.getLinkedSupplementalDocs(fn, "EnvironmentMetrics", "Metrics")...)
|
||||
}
|
||||
enrichNICFromPCIe(&nic, pcieDoc, functionDocs, supplementalDocs)
|
||||
}
|
||||
// Collect MACs from NetworkDeviceFunctions when not found via PCIe path.
|
||||
if len(nic.MACAddresses) == 0 {
|
||||
@@ -1184,7 +1265,8 @@ func (r redfishSnapshotReader) collectPSUs(chassisPaths []string) []models.PSU {
|
||||
for _, chassisPath := range chassisPaths {
|
||||
if memberDocs, err := r.getCollectionMembers(joinPath(chassisPath, "/PowerSubsystem/PowerSupplies")); err == nil && len(memberDocs) > 0 {
|
||||
for _, doc := range memberDocs {
|
||||
idx = appendPSU(&out, seen, parsePSU(doc, idx), idx)
|
||||
supplementalDocs := r.getLinkedSupplementalDocs(doc, "EnvironmentMetrics", "Metrics")
|
||||
idx = appendPSU(&out, seen, parsePSUWithSupplementalDocs(doc, idx, supplementalDocs...), idx)
|
||||
}
|
||||
continue
|
||||
}
|
||||
@@ -1195,7 +1277,8 @@ func (r redfishSnapshotReader) collectPSUs(chassisPaths []string) []models.PSU {
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
idx = appendPSU(&out, seen, parsePSU(doc, idx), idx)
|
||||
supplementalDocs := r.getLinkedSupplementalDocs(doc, "EnvironmentMetrics", "Metrics")
|
||||
idx = appendPSU(&out, seen, parsePSUWithSupplementalDocs(doc, idx, supplementalDocs...), idx)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1227,7 +1310,11 @@ func (r redfishSnapshotReader) collectGPUs(systemPaths, chassisPaths []string) [
|
||||
if !looksLikeGPU(doc, functionDocs) {
|
||||
continue
|
||||
}
|
||||
gpu := parseGPU(doc, functionDocs, idx)
|
||||
supplementalDocs := r.getLinkedSupplementalDocs(doc, "EnvironmentMetrics", "Metrics")
|
||||
for _, fn := range functionDocs {
|
||||
supplementalDocs = append(supplementalDocs, r.getLinkedSupplementalDocs(fn, "EnvironmentMetrics", "Metrics")...)
|
||||
}
|
||||
gpu := parseGPUWithSupplementalDocs(doc, functionDocs, supplementalDocs, idx)
|
||||
idx++
|
||||
if shouldSkipGenericGPUDuplicate(out, gpu) {
|
||||
continue
|
||||
@@ -1265,7 +1352,11 @@ func (r redfishSnapshotReader) collectPCIeDevices(systemPaths, chassisPaths []st
|
||||
if looksLikeGPU(doc, functionDocs) {
|
||||
continue
|
||||
}
|
||||
dev := parsePCIeDevice(doc, functionDocs)
|
||||
supplementalDocs := r.getLinkedSupplementalDocs(doc, "EnvironmentMetrics", "Metrics")
|
||||
for _, fn := range functionDocs {
|
||||
supplementalDocs = append(supplementalDocs, r.getLinkedSupplementalDocs(fn, "EnvironmentMetrics", "Metrics")...)
|
||||
}
|
||||
dev := parsePCIeDeviceWithSupplementalDocs(doc, functionDocs, supplementalDocs)
|
||||
if isUnidentifiablePCIeDevice(dev) {
|
||||
continue
|
||||
}
|
||||
@@ -1278,7 +1369,8 @@ func (r redfishSnapshotReader) collectPCIeDevices(systemPaths, chassisPaths []st
|
||||
continue
|
||||
}
|
||||
for idx, fn := range functionDocs {
|
||||
dev := parsePCIeFunction(fn, idx+1)
|
||||
supplementalDocs := r.getLinkedSupplementalDocs(fn, "EnvironmentMetrics", "Metrics")
|
||||
dev := parsePCIeFunctionWithSupplementalDocs(fn, supplementalDocs, idx+1)
|
||||
out = append(out, dev)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -584,18 +584,32 @@ func TestEnrichNICFromPCIeFunctions(t *testing.T) {
|
||||
}
|
||||
functionDocs := []map[string]interface{}{
|
||||
{
|
||||
"VendorId": "0x15b3",
|
||||
"DeviceId": "0x1021",
|
||||
"FunctionId": "0000:17:00.0",
|
||||
"VendorId": "0x15b3",
|
||||
"DeviceId": "0x1021",
|
||||
"CurrentLinkWidth": 16,
|
||||
"CurrentLinkSpeedGTs": "32 GT/s",
|
||||
"MaxLinkWidth": 16,
|
||||
"MaxLinkSpeedGTs": "32 GT/s",
|
||||
},
|
||||
}
|
||||
|
||||
enrichNICFromPCIe(&nic, pcieDoc, functionDocs)
|
||||
enrichNICFromPCIe(&nic, pcieDoc, functionDocs, nil)
|
||||
if nic.VendorID != 0x15b3 || nic.DeviceID != 0x1021 {
|
||||
t.Fatalf("unexpected NIC IDs: vendor=%#x device=%#x", nic.VendorID, nic.DeviceID)
|
||||
}
|
||||
if nic.Location != "PCIe Slot 1 (1)" {
|
||||
t.Fatalf("unexpected NIC location: %q", nic.Location)
|
||||
}
|
||||
if nic.BDF != "0000:17:00.0" {
|
||||
t.Fatalf("unexpected NIC BDF: %q", nic.BDF)
|
||||
}
|
||||
if nic.LinkWidth != 16 || nic.MaxLinkWidth != 16 {
|
||||
t.Fatalf("unexpected NIC link width state: current=%d max=%d", nic.LinkWidth, nic.MaxLinkWidth)
|
||||
}
|
||||
if nic.LinkSpeed != "32 GT/s" || nic.MaxLinkSpeed != "32 GT/s" {
|
||||
t.Fatalf("unexpected NIC link speed state: current=%q max=%q", nic.LinkSpeed, nic.MaxLinkSpeed)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseNIC_PortCountFromControllerCapabilities(t *testing.T) {
|
||||
@@ -704,6 +718,208 @@ func TestParseComponents_UseNestedSerialNumberFallback(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseCPUAndMemory_CollectOemDetails(t *testing.T) {
|
||||
cpus := parseCPUs([]map[string]interface{}{
|
||||
{
|
||||
"Id": "CPU0",
|
||||
"Model": "Intel Xeon",
|
||||
"CorrectableErrors": 7,
|
||||
"TemperatureCelsius": 63,
|
||||
"Oem": map[string]interface{}{
|
||||
"VendorX": map[string]interface{}{
|
||||
"MicrocodeVersion": "0x2b000643",
|
||||
"UncorrectableErrors": 1,
|
||||
"ThermalThrottled": true,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
if len(cpus) != 1 || cpus[0].Details == nil {
|
||||
t.Fatalf("expected CPU details, got %+v", cpus)
|
||||
}
|
||||
if cpus[0].Details["microcode"] != "0x2b000643" {
|
||||
t.Fatalf("expected CPU microcode detail, got %#v", cpus[0].Details)
|
||||
}
|
||||
if cpus[0].Details["correctable_error_count"] != int64(7) || cpus[0].Details["uncorrectable_error_count"] != int64(1) {
|
||||
t.Fatalf("expected CPU error counters, got %#v", cpus[0].Details)
|
||||
}
|
||||
if cpus[0].Details["throttled"] != true || cpus[0].Details["temperature_c"] != 63.0 {
|
||||
t.Fatalf("expected CPU thermal details, got %#v", cpus[0].Details)
|
||||
}
|
||||
|
||||
dimms := parseMemory([]map[string]interface{}{
|
||||
{
|
||||
"Id": "DIMM0",
|
||||
"DeviceLocator": "CPU0_C0D0",
|
||||
"CapacityMiB": 32768,
|
||||
"SerialNumber": "DIMM-001",
|
||||
"Oem": map[string]interface{}{
|
||||
"VendorX": map[string]interface{}{
|
||||
"CorrectableECCErrorCount": 12,
|
||||
"UncorrectableECCErrorCount": 2,
|
||||
"TemperatureC": 41.5,
|
||||
"SpareBlocksRemainingPercent": 88,
|
||||
"PerformanceDegraded": true,
|
||||
"DataLossDetected": false,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
if len(dimms) != 1 || dimms[0].Details == nil {
|
||||
t.Fatalf("expected DIMM details, got %+v", dimms)
|
||||
}
|
||||
if dimms[0].Details["correctable_ecc_error_count"] != int64(12) || dimms[0].Details["uncorrectable_ecc_error_count"] != int64(2) {
|
||||
t.Fatalf("expected DIMM ECC counters, got %#v", dimms[0].Details)
|
||||
}
|
||||
if dimms[0].Details["temperature_c"] != 41.5 || dimms[0].Details["spare_blocks_remaining_pct"] != 88.0 {
|
||||
t.Fatalf("expected DIMM telemetry details, got %#v", dimms[0].Details)
|
||||
}
|
||||
if dimms[0].Details["performance_degraded"] != true || dimms[0].Details["data_loss_detected"] != false {
|
||||
t.Fatalf("expected DIMM boolean health details, got %#v", dimms[0].Details)
|
||||
}
|
||||
}
|
||||
|
||||
func TestReplayRedfishFromRawPayloads_UsesProcessorAndMemoryMetrics(t *testing.T) {
|
||||
rawPayloads := map[string]any{
|
||||
"redfish_tree": map[string]interface{}{
|
||||
"/redfish/v1": map[string]interface{}{},
|
||||
"/redfish/v1/Systems": map[string]interface{}{
|
||||
"Members": []interface{}{
|
||||
map[string]interface{}{"@odata.id": "/redfish/v1/Systems/1"},
|
||||
},
|
||||
},
|
||||
"/redfish/v1/Systems/1": map[string]interface{}{
|
||||
"Id": "1",
|
||||
"Processors": map[string]interface{}{
|
||||
"@odata.id": "/redfish/v1/Systems/1/Processors",
|
||||
},
|
||||
"Memory": map[string]interface{}{
|
||||
"@odata.id": "/redfish/v1/Systems/1/Memory",
|
||||
},
|
||||
},
|
||||
"/redfish/v1/Systems/1/Processors": map[string]interface{}{
|
||||
"Members": []interface{}{
|
||||
map[string]interface{}{"@odata.id": "/redfish/v1/Systems/1/Processors/CPU0"},
|
||||
},
|
||||
},
|
||||
"/redfish/v1/Systems/1/Processors/CPU0": map[string]interface{}{
|
||||
"Id": "CPU0",
|
||||
"ProcessorType": "CPU",
|
||||
"Model": "Intel Xeon",
|
||||
"ProcessorMetrics": map[string]interface{}{
|
||||
"@odata.id": "/redfish/v1/Systems/1/Processors/CPU0/ProcessorMetrics",
|
||||
},
|
||||
},
|
||||
"/redfish/v1/Systems/1/Processors/CPU0/ProcessorMetrics": map[string]interface{}{
|
||||
"CorrectableErrors": 10,
|
||||
"ThermalThrottled": true,
|
||||
"MicrocodeVersion": "0x2b000643",
|
||||
"TemperatureCelsius": 66,
|
||||
},
|
||||
"/redfish/v1/Systems/1/Memory": map[string]interface{}{
|
||||
"Members": []interface{}{
|
||||
map[string]interface{}{"@odata.id": "/redfish/v1/Systems/1/Memory/DIMM0"},
|
||||
},
|
||||
},
|
||||
"/redfish/v1/Systems/1/Memory/DIMM0": map[string]interface{}{
|
||||
"Id": "DIMM0",
|
||||
"DeviceLocator": "CPU0_C0D0",
|
||||
"CapacityMiB": 32768,
|
||||
"SerialNumber": "DIMM-001",
|
||||
"MemoryMetrics": map[string]interface{}{
|
||||
"@odata.id": "/redfish/v1/Systems/1/Memory/DIMM0/MemoryMetrics",
|
||||
},
|
||||
},
|
||||
"/redfish/v1/Systems/1/Memory/DIMM0/MemoryMetrics": map[string]interface{}{
|
||||
"CorrectableECCErrorCount": 14,
|
||||
"TemperatureCelsius": 42,
|
||||
"PerformanceDegraded": true,
|
||||
"SpareBlocksRemainingPercent": 91,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
result, err := ReplayRedfishFromRawPayloads(rawPayloads, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("ReplayRedfishFromRawPayloads() failed: %v", err)
|
||||
}
|
||||
if len(result.Hardware.CPUs) != 1 || result.Hardware.CPUs[0].Details == nil {
|
||||
t.Fatalf("expected CPU details from replay metrics, got %+v", result.Hardware.CPUs)
|
||||
}
|
||||
if result.Hardware.CPUs[0].Details["correctable_error_count"] != int64(10) || result.Hardware.CPUs[0].Details["microcode"] != "0x2b000643" {
|
||||
t.Fatalf("expected CPU replay metrics details, got %#v", result.Hardware.CPUs[0].Details)
|
||||
}
|
||||
if len(result.Hardware.Memory) != 1 || result.Hardware.Memory[0].Details == nil {
|
||||
t.Fatalf("expected memory details from replay metrics, got %+v", result.Hardware.Memory)
|
||||
}
|
||||
if result.Hardware.Memory[0].Details["correctable_ecc_error_count"] != int64(14) || result.Hardware.Memory[0].Details["performance_degraded"] != true {
|
||||
t.Fatalf("expected DIMM replay metrics details, got %#v", result.Hardware.Memory[0].Details)
|
||||
}
|
||||
}
|
||||
|
||||
func TestReplayRedfishFromRawPayloads_UsesDriveMetrics(t *testing.T) {
|
||||
rawPayloads := map[string]any{
|
||||
"redfish_tree": map[string]interface{}{
|
||||
"/redfish/v1": map[string]interface{}{},
|
||||
"/redfish/v1/Systems": map[string]interface{}{
|
||||
"Members": []interface{}{
|
||||
map[string]interface{}{"@odata.id": "/redfish/v1/Systems/1"},
|
||||
},
|
||||
},
|
||||
"/redfish/v1/Systems/1": map[string]interface{}{
|
||||
"Id": "1",
|
||||
"Storage": map[string]interface{}{
|
||||
"@odata.id": "/redfish/v1/Systems/1/Storage",
|
||||
},
|
||||
},
|
||||
"/redfish/v1/Systems/1/Storage": map[string]interface{}{
|
||||
"Members": []interface{}{
|
||||
map[string]interface{}{"@odata.id": "/redfish/v1/Systems/1/Storage/RAID1"},
|
||||
},
|
||||
},
|
||||
"/redfish/v1/Systems/1/Storage/RAID1": map[string]interface{}{
|
||||
"Id": "RAID1",
|
||||
"Drives": map[string]interface{}{
|
||||
"@odata.id": "/redfish/v1/Systems/1/Storage/RAID1/Drives",
|
||||
},
|
||||
},
|
||||
"/redfish/v1/Systems/1/Storage/RAID1/Drives": map[string]interface{}{
|
||||
"Members": []interface{}{
|
||||
map[string]interface{}{"@odata.id": "/redfish/v1/Systems/1/Storage/RAID1/Drives/Drive0"},
|
||||
},
|
||||
},
|
||||
"/redfish/v1/Systems/1/Storage/RAID1/Drives/Drive0": map[string]interface{}{
|
||||
"Id": "Drive0",
|
||||
"Model": "NVMe SSD",
|
||||
"SerialNumber": "SSD-001",
|
||||
"DriveMetrics": map[string]interface{}{
|
||||
"@odata.id": "/redfish/v1/Systems/1/Storage/RAID1/Drives/Drive0/DriveMetrics",
|
||||
},
|
||||
},
|
||||
"/redfish/v1/Systems/1/Storage/RAID1/Drives/Drive0/DriveMetrics": map[string]interface{}{
|
||||
"PowerOnHours": 1001,
|
||||
"MediaErrors": 3,
|
||||
"AvailableSparePercent": 92,
|
||||
"TemperatureCelsius": 37,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
result, err := ReplayRedfishFromRawPayloads(rawPayloads, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("ReplayRedfishFromRawPayloads() failed: %v", err)
|
||||
}
|
||||
if len(result.Hardware.Storage) != 1 || result.Hardware.Storage[0].Details == nil {
|
||||
t.Fatalf("expected storage details from replay drive metrics, got %+v", result.Hardware.Storage)
|
||||
}
|
||||
if result.Hardware.Storage[0].Details["power_on_hours"] != int64(1001) || result.Hardware.Storage[0].Details["media_errors"] != int64(3) {
|
||||
t.Fatalf("expected drive metrics counters, got %#v", result.Hardware.Storage[0].Details)
|
||||
}
|
||||
if result.Hardware.Storage[0].Details["available_spare_pct"] != 92.0 || result.Hardware.Storage[0].Details["temperature_c"] != 37.0 {
|
||||
t.Fatalf("expected drive metrics telemetry, got %#v", result.Hardware.Storage[0].Details)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRedfishCollectionMemberRefs_IncludesOemPublicMembers(t *testing.T) {
|
||||
collection := map[string]interface{}{
|
||||
"Members": []interface{}{
|
||||
@@ -725,6 +941,186 @@ func TestRedfishCollectionMemberRefs_IncludesOemPublicMembers(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseDriveAndPSU_CollectComponentMetricsIntoDetails(t *testing.T) {
|
||||
drive := parseDrive(map[string]interface{}{
|
||||
"Id": "Drive0",
|
||||
"Model": "NVMe SSD",
|
||||
"SerialNumber": "SSD-001",
|
||||
"TemperatureCelsius": 38.5,
|
||||
"PowerOnHours": 12450,
|
||||
"UnsafeShutdowns": 3,
|
||||
"PredictedMediaLifeLeftPercent": 91,
|
||||
"Oem": map[string]interface{}{
|
||||
"Public": map[string]interface{}{
|
||||
"AvailableSparePercent": 87,
|
||||
},
|
||||
},
|
||||
})
|
||||
if drive.Details == nil {
|
||||
t.Fatalf("expected drive details to be populated")
|
||||
}
|
||||
if got := drive.Details["temperature_c"]; got != 38.5 {
|
||||
t.Fatalf("expected drive temperature detail 38.5, got %#v", got)
|
||||
}
|
||||
if got := drive.Details["power_on_hours"]; got != int64(12450) {
|
||||
t.Fatalf("expected drive power_on_hours detail, got %#v", got)
|
||||
}
|
||||
if got := drive.Details["life_remaining_pct"]; got != 91.0 {
|
||||
t.Fatalf("expected drive life_remaining_pct detail, got %#v", got)
|
||||
}
|
||||
if got := drive.Details["available_spare_pct"]; got != 87.0 {
|
||||
t.Fatalf("expected drive available_spare_pct detail from Oem/Public, got %#v", got)
|
||||
}
|
||||
|
||||
driveOEM := parseDrive(map[string]interface{}{
|
||||
"Id": "Drive1",
|
||||
"Model": "NVMe SSD 2",
|
||||
"SerialNumber": "SSD-002",
|
||||
"Oem": map[string]interface{}{
|
||||
"Public": map[string]interface{}{
|
||||
"temperature": 19,
|
||||
"PercentAvailableSpare": 93,
|
||||
"PercentageUsed": 7,
|
||||
},
|
||||
},
|
||||
})
|
||||
if driveOEM.Details == nil {
|
||||
t.Fatalf("expected oem drive details to be populated")
|
||||
}
|
||||
if got := driveOEM.Details["temperature_c"]; got != 19.0 {
|
||||
t.Fatalf("expected lowercase OEM drive temperature 19, got %#v", got)
|
||||
}
|
||||
if got := driveOEM.Details["available_spare_pct"]; got != 93.0 {
|
||||
t.Fatalf("expected OEM available_spare_pct 93, got %#v", got)
|
||||
}
|
||||
if got := driveOEM.Details["life_used_pct"]; got != 7.0 {
|
||||
t.Fatalf("expected OEM life_used_pct 7, got %#v", got)
|
||||
}
|
||||
|
||||
psu := parsePSU(map[string]interface{}{
|
||||
"MemberId": "PSU0",
|
||||
"SerialNumber": "PSU-001",
|
||||
"TemperatureCelsius": 41,
|
||||
"Oem": map[string]interface{}{
|
||||
"Public": map[string]interface{}{
|
||||
"LifeRemainingPercent": 96,
|
||||
},
|
||||
},
|
||||
}, 1)
|
||||
if psu.Details == nil {
|
||||
t.Fatalf("expected psu details to be populated")
|
||||
}
|
||||
if got := psu.Details["temperature_c"]; got != 41.0 {
|
||||
t.Fatalf("expected psu temperature detail 41, got %#v", got)
|
||||
}
|
||||
if got := psu.Details["life_remaining_pct"]; got != 96.0 {
|
||||
t.Fatalf("expected psu life_remaining_pct detail, got %#v", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseGPUPCIeAndNIC_CollectComponentMetricsIntoDetails(t *testing.T) {
|
||||
functionDocs := []map[string]interface{}{
|
||||
{
|
||||
"FunctionId": "0000:17:00.0",
|
||||
"VendorId": "0x10de",
|
||||
"DeviceId": "0x2331",
|
||||
"TemperatureCelsius": 48.5,
|
||||
"PowerConsumedWatts": 315.0,
|
||||
"ECCCorrectedTotal": 12,
|
||||
"BatteryHealthPercent": 87,
|
||||
"SFPTemperatureCelsius": 36.2,
|
||||
},
|
||||
}
|
||||
gpu := parseGPU(map[string]interface{}{
|
||||
"Id": "GPU0",
|
||||
"Model": "NVIDIA H100",
|
||||
"Manufacturer": "NVIDIA",
|
||||
}, functionDocs, 1)
|
||||
if gpu.Details == nil || gpu.Details["temperature_c"] != 48.5 || gpu.Details["power_w"] != 315.0 {
|
||||
t.Fatalf("expected gpu details from function docs, got %#v", gpu.Details)
|
||||
}
|
||||
|
||||
pcie := parsePCIeDevice(map[string]interface{}{
|
||||
"Id": "NIC1",
|
||||
}, []map[string]interface{}{
|
||||
{
|
||||
"FunctionId": "0000:18:00.0",
|
||||
"VendorId": "0x15b3",
|
||||
"DeviceId": "0x1021",
|
||||
"SFPTXPowerDBm": -1.8,
|
||||
"SFPRXPowerDBm": -2.1,
|
||||
"SFPBiasMA": 5.5,
|
||||
"BatteryReplaceRequired": true,
|
||||
},
|
||||
})
|
||||
if pcie.Details == nil || pcie.Details["sfp_tx_power_dbm"] != -1.8 || pcie.Details["battery_replace_required"] != true {
|
||||
t.Fatalf("expected pcie details from function docs, got %#v", pcie.Details)
|
||||
}
|
||||
|
||||
nic := parseNIC(map[string]interface{}{"Id": "1"})
|
||||
enrichNICFromPCIe(&nic, map[string]interface{}{}, []map[string]interface{}{
|
||||
{
|
||||
"FunctionId": "0000:19:00.0",
|
||||
"SFPTemperatureCelsius": 34.0,
|
||||
},
|
||||
}, nil)
|
||||
if nic.Details == nil || nic.Details["sfp_temperature_c"] != 34.0 {
|
||||
t.Fatalf("expected nic details from linked pcie function, got %#v", nic.Details)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseComponentDetails_UseLinkedSupplementalMetrics(t *testing.T) {
|
||||
drive := parseDriveWithSupplementalDocs(
|
||||
map[string]interface{}{
|
||||
"Id": "Drive0",
|
||||
"SerialNumber": "SSD-001",
|
||||
},
|
||||
map[string]interface{}{
|
||||
"PowerOnHours": 5001,
|
||||
"MediaErrors": 2,
|
||||
"TemperatureC": 39.5,
|
||||
"LifeUsedPercent": 9,
|
||||
},
|
||||
)
|
||||
if drive.Details == nil || drive.Details["power_on_hours"] != int64(5001) || drive.Details["temperature_c"] != 39.5 {
|
||||
t.Fatalf("expected drive details from supplemental metrics, got %#v", drive.Details)
|
||||
}
|
||||
|
||||
psu := parsePSUWithSupplementalDocs(
|
||||
map[string]interface{}{
|
||||
"MemberId": "PSU0",
|
||||
"SerialNumber": "PSU-001",
|
||||
},
|
||||
1,
|
||||
map[string]interface{}{
|
||||
"Temperature": 44,
|
||||
"LifeRemainingPercent": 97,
|
||||
},
|
||||
)
|
||||
if psu.Details == nil || psu.Details["temperature_c"] != 44.0 || psu.Details["life_remaining_pct"] != 97.0 {
|
||||
t.Fatalf("expected psu details from supplemental metrics, got %#v", psu.Details)
|
||||
}
|
||||
|
||||
gpu := parseGPUWithSupplementalDocs(
|
||||
map[string]interface{}{
|
||||
"Id": "GPU0",
|
||||
"Model": "NVIDIA H100",
|
||||
"Manufacturer": "NVIDIA",
|
||||
},
|
||||
nil,
|
||||
[]map[string]interface{}{
|
||||
{
|
||||
"PowerConsumptionWatts": 305.0,
|
||||
"HWSlowdown": true,
|
||||
},
|
||||
},
|
||||
1,
|
||||
)
|
||||
if gpu.Details == nil || gpu.Details["power_w"] != 305.0 || gpu.Details["hw_slowdown"] != true {
|
||||
t.Fatalf("expected gpu details from supplemental metrics, got %#v", gpu.Details)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRecoverCriticalRedfishDocsPlanB_RetriesMembersFromExistingCollection(t *testing.T) {
|
||||
t.Setenv("LOGPILE_REDFISH_CRITICAL_COOLDOWN", "0s")
|
||||
t.Setenv("LOGPILE_REDFISH_CRITICAL_SLOW_GAP", "0s")
|
||||
@@ -1621,11 +2017,11 @@ func TestCollectGPUsFromProcessors_SupermicroHGX(t *testing.T) {
|
||||
},
|
||||
},
|
||||
"/redfish/v1/Chassis/1/PCIeDevices/GPU1": map[string]interface{}{
|
||||
"Id": "GPU1",
|
||||
"Name": "GPU1",
|
||||
"Model": "NVIDIA H200",
|
||||
"Manufacturer": "NVIDIA",
|
||||
"SerialNumber": "SN001",
|
||||
"Id": "GPU1",
|
||||
"Name": "GPU1",
|
||||
"Model": "NVIDIA H200",
|
||||
"Manufacturer": "NVIDIA",
|
||||
"SerialNumber": "SN001",
|
||||
"FirmwareVersion": "96.00.D9.00.02",
|
||||
"PCIeFunctions": map[string]interface{}{
|
||||
"@odata.id": "/redfish/v1/Chassis/1/PCIeDevices/GPU1/PCIeFunctions",
|
||||
@@ -1810,15 +2206,18 @@ func TestLooksLikeGPU_NVSwitchExcluded(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestShouldCrawlPath_MemorySubresourcesAreSkipped(t *testing.T) {
|
||||
func TestShouldCrawlPath_MemoryAndProcessorMetricsAreAllowed(t *testing.T) {
|
||||
if !shouldCrawlPath("/redfish/v1/Systems/1/Memory/CPU0_C0D0") {
|
||||
t.Fatalf("expected direct DIMM resource to be crawlable")
|
||||
}
|
||||
if shouldCrawlPath("/redfish/v1/Systems/1/Memory/CPU0_C0D0/Assembly") {
|
||||
t.Fatalf("expected DIMM assembly subresource to be skipped")
|
||||
}
|
||||
if shouldCrawlPath("/redfish/v1/Systems/1/Memory/CPU0_C0D0/MemoryMetrics") {
|
||||
t.Fatalf("expected DIMM metrics subresource to be skipped")
|
||||
if !shouldCrawlPath("/redfish/v1/Systems/1/Memory/CPU0_C0D0/MemoryMetrics") {
|
||||
t.Fatalf("expected DIMM metrics subresource to be crawlable")
|
||||
}
|
||||
if !shouldCrawlPath("/redfish/v1/Systems/1/Processors/CPU0/ProcessorMetrics") {
|
||||
t.Fatalf("expected CPU metrics subresource to be crawlable")
|
||||
}
|
||||
if shouldCrawlPath("/redfish/v1/Chassis/1/PCIeDevices/0/PCIeFunctions/1") {
|
||||
t.Fatalf("expected noisy chassis pciefunctions branch to be skipped")
|
||||
|
||||
Reference in New Issue
Block a user