Improve Multillect Redfish replay and power detection
This commit is contained in:
@@ -96,17 +96,23 @@ func ReplayRedfishFromRawPayloads(rawPayloads map[string]any, emit ProgressFn) (
|
||||
networkProtocolDoc, _ := r.getJSON(joinPath(primaryManager, "/NetworkProtocol"))
|
||||
firmware := parseFirmware(systemDoc, biosDoc, managerDoc, networkProtocolDoc)
|
||||
firmware = dedupeFirmwareInfo(append(firmware, r.collectFirmwareInventory()...))
|
||||
boardInfo.BMCMACAddress = r.collectBMCMAC(managerPaths)
|
||||
bmcManagementSummary := r.collectBMCManagementSummary(managerPaths)
|
||||
boardInfo.BMCMACAddress = strings.TrimSpace(firstNonEmpty(
|
||||
asString(bmcManagementSummary["mac_address"]),
|
||||
r.collectBMCMAC(managerPaths),
|
||||
))
|
||||
assemblyFRU := r.collectAssemblyFRU(chassisPaths)
|
||||
collectedAt, sourceTimezone := inferRedfishCollectionTime(managerDoc, rawPayloads)
|
||||
inventoryLastModifiedAt := inferInventoryLastModifiedTime(r.tree)
|
||||
logEntryEvents := parseRedfishLogEntries(rawPayloads, collectedAt)
|
||||
sensorHintSummary, sensorHintEvents := r.collectSensorsListHints(chassisPaths, collectedAt)
|
||||
bmcManagementEvent := buildBMCManagementSummaryEvent(bmcManagementSummary, collectedAt)
|
||||
|
||||
result := &models.AnalysisResult{
|
||||
CollectedAt: collectedAt,
|
||||
InventoryLastModifiedAt: inventoryLastModifiedAt,
|
||||
SourceTimezone: sourceTimezone,
|
||||
Events: append(append(append(append(make([]models.Event, 0, len(discreteEvents)+len(healthEvents)+len(driveFetchWarningEvents)+len(logEntryEvents)+1), healthEvents...), discreteEvents...), driveFetchWarningEvents...), logEntryEvents...),
|
||||
Events: append(append(append(append(append(append(make([]models.Event, 0, len(discreteEvents)+len(healthEvents)+len(driveFetchWarningEvents)+len(logEntryEvents)+len(sensorHintEvents)+2), healthEvents...), discreteEvents...), driveFetchWarningEvents...), logEntryEvents...), sensorHintEvents...), bmcManagementEvent...),
|
||||
FRU: assemblyFRU,
|
||||
Sensors: dedupeSensorReadings(append(append(thresholdSensors, thermalSensors...), powerSensors...)),
|
||||
RawPayloads: cloneRawPayloads(rawPayloads),
|
||||
@@ -155,6 +161,12 @@ func ReplayRedfishFromRawPayloads(rawPayloads map[string]any, emit ProgressFn) (
|
||||
if strings.TrimSpace(sourceTimezone) != "" {
|
||||
result.RawPayloads["source_timezone"] = sourceTimezone
|
||||
}
|
||||
if len(sensorHintSummary) > 0 {
|
||||
result.RawPayloads["redfish_sensor_hints"] = sensorHintSummary
|
||||
}
|
||||
if len(bmcManagementSummary) > 0 {
|
||||
result.RawPayloads["redfish_bmc_network_summary"] = bmcManagementSummary
|
||||
}
|
||||
appendMissingServerModelWarning(result, systemDoc, joinPath(primarySystem, "/Oem/Public/FRU"), joinPath(primaryChassis, "/Oem/Public/FRU"))
|
||||
return result, nil
|
||||
}
|
||||
@@ -324,6 +336,153 @@ func buildDriveFetchWarningEvents(rawPayloads map[string]any) []models.Event {
|
||||
}
|
||||
}
|
||||
|
||||
func (r redfishSnapshotReader) collectSensorsListHints(chassisPaths []string, collectedAt time.Time) (map[string]any, []models.Event) {
|
||||
summary := make(map[string]any)
|
||||
var events []models.Event
|
||||
var presentDIMMs []string
|
||||
dimmTotal := 0
|
||||
dimmPresent := 0
|
||||
physicalDriveSlots := 0
|
||||
activePhysicalDriveSlots := 0
|
||||
logicalDriveStatus := ""
|
||||
|
||||
for _, chassisPath := range chassisPaths {
|
||||
doc, err := r.getJSON(joinPath(chassisPath, "/SensorsList"))
|
||||
if err != nil || len(doc) == 0 {
|
||||
continue
|
||||
}
|
||||
sensors, ok := doc["SensorsList"].([]interface{})
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
for _, item := range sensors {
|
||||
sensor, ok := item.(map[string]interface{})
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
name := strings.TrimSpace(asString(sensor["SensorName"]))
|
||||
sensorType := strings.TrimSpace(asString(sensor["SensorType"]))
|
||||
status := strings.TrimSpace(asString(sensor["Status"]))
|
||||
switch {
|
||||
case strings.HasPrefix(name, "DIMM") && strings.HasSuffix(name, "_Status") && strings.EqualFold(sensorType, "Memory"):
|
||||
dimmTotal++
|
||||
if redfishSlotStatusLooksPresent(status) {
|
||||
dimmPresent++
|
||||
presentDIMMs = append(presentDIMMs, strings.TrimSuffix(name, "_Status"))
|
||||
}
|
||||
case strings.EqualFold(sensorType, "Drive Slot"):
|
||||
if strings.EqualFold(name, "Logical_Drive") {
|
||||
logicalDriveStatus = firstNonEmpty(logicalDriveStatus, status)
|
||||
continue
|
||||
}
|
||||
physicalDriveSlots++
|
||||
if redfishSlotStatusLooksPresent(status) {
|
||||
activePhysicalDriveSlots++
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if dimmTotal > 0 {
|
||||
sort.Strings(presentDIMMs)
|
||||
summary["memory_slots"] = map[string]any{
|
||||
"total": dimmTotal,
|
||||
"present_count": dimmPresent,
|
||||
"present_slots": presentDIMMs,
|
||||
"source": "SensorsList",
|
||||
}
|
||||
events = append(events, models.Event{
|
||||
Timestamp: replayEventTimestamp(collectedAt),
|
||||
Source: "Redfish",
|
||||
EventType: "Collection Info",
|
||||
Severity: models.SeverityInfo,
|
||||
Description: fmt.Sprintf("Memory slot sensors report %d populated positions out of %d", dimmPresent, dimmTotal),
|
||||
RawData: firstNonEmpty(strings.Join(presentDIMMs, ", "), "no populated DIMM slots reported"),
|
||||
})
|
||||
}
|
||||
if physicalDriveSlots > 0 || logicalDriveStatus != "" {
|
||||
summary["drive_slots"] = map[string]any{
|
||||
"physical_total": physicalDriveSlots,
|
||||
"physical_active_count": activePhysicalDriveSlots,
|
||||
"logical_drive_status": logicalDriveStatus,
|
||||
"source": "SensorsList",
|
||||
}
|
||||
rawParts := []string{
|
||||
fmt.Sprintf("physical_active=%d/%d", activePhysicalDriveSlots, physicalDriveSlots),
|
||||
}
|
||||
if logicalDriveStatus != "" {
|
||||
rawParts = append(rawParts, "logical_drive="+logicalDriveStatus)
|
||||
}
|
||||
events = append(events, models.Event{
|
||||
Timestamp: replayEventTimestamp(collectedAt),
|
||||
Source: "Redfish",
|
||||
EventType: "Collection Info",
|
||||
Severity: models.SeverityInfo,
|
||||
Description: fmt.Sprintf("Drive slot sensors report %d active physical slots out of %d", activePhysicalDriveSlots, physicalDriveSlots),
|
||||
RawData: strings.Join(rawParts, "; "),
|
||||
})
|
||||
}
|
||||
|
||||
return summary, events
|
||||
}
|
||||
|
||||
func buildBMCManagementSummaryEvent(summary map[string]any, collectedAt time.Time) []models.Event {
|
||||
if len(summary) == 0 {
|
||||
return nil
|
||||
}
|
||||
desc := fmt.Sprintf(
|
||||
"BMC management interface %s link=%s ip=%s",
|
||||
firstNonEmpty(asString(summary["interface_id"]), "unknown"),
|
||||
firstNonEmpty(asString(summary["link_status"]), "unknown"),
|
||||
firstNonEmpty(asString(summary["ipv4_address"]), "n/a"),
|
||||
)
|
||||
rawParts := make([]string, 0, 8)
|
||||
for _, part := range []string{
|
||||
"mac_address=" + strings.TrimSpace(asString(summary["mac_address"])),
|
||||
"speed_mbps=" + strings.TrimSpace(asString(summary["speed_mbps"])),
|
||||
"lldp_chassis_name=" + strings.TrimSpace(asString(summary["lldp_chassis_name"])),
|
||||
"lldp_port_desc=" + strings.TrimSpace(asString(summary["lldp_port_desc"])),
|
||||
"lldp_port_id=" + strings.TrimSpace(asString(summary["lldp_port_id"])),
|
||||
"ipv4_gateway=" + strings.TrimSpace(asString(summary["ipv4_gateway"])),
|
||||
} {
|
||||
if !strings.HasSuffix(part, "=") {
|
||||
rawParts = append(rawParts, part)
|
||||
}
|
||||
}
|
||||
if vlan := asInt(summary["lldp_vlan_id"]); vlan > 0 {
|
||||
rawParts = append(rawParts, fmt.Sprintf("lldp_vlan_id=%d", vlan))
|
||||
}
|
||||
if asBool(summary["ncsi_enabled"]) {
|
||||
rawParts = append(rawParts, "ncsi_enabled=true")
|
||||
}
|
||||
return []models.Event{
|
||||
{
|
||||
Timestamp: replayEventTimestamp(collectedAt),
|
||||
Source: "Redfish",
|
||||
EventType: "Collection Info",
|
||||
Severity: models.SeverityInfo,
|
||||
Description: desc,
|
||||
RawData: strings.Join(rawParts, "; "),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func redfishSlotStatusLooksPresent(status string) bool {
|
||||
switch strings.ToLower(strings.TrimSpace(status)) {
|
||||
case "ok", "enabled", "present", "warning", "critical":
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func replayEventTimestamp(collectedAt time.Time) time.Time {
|
||||
if !collectedAt.IsZero() {
|
||||
return collectedAt
|
||||
}
|
||||
return time.Now()
|
||||
}
|
||||
|
||||
func (r redfishSnapshotReader) collectFirmwareInventory() []models.FirmwareInfo {
|
||||
docs, err := r.getCollectionMembers("/redfish/v1/UpdateService/FirmwareInventory")
|
||||
if err != nil || len(docs) == 0 {
|
||||
@@ -856,6 +1015,9 @@ func (r redfishSnapshotReader) fallbackCollectionMembers(collectionPath string,
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if redfishFallbackMemberLooksLikePlaceholder(collectionPath, doc) {
|
||||
continue
|
||||
}
|
||||
if strings.TrimSpace(asString(doc["@odata.id"])) == "" {
|
||||
doc["@odata.id"] = normalizeRedfishPath(p)
|
||||
}
|
||||
@@ -864,6 +1026,135 @@ func (r redfishSnapshotReader) fallbackCollectionMembers(collectionPath string,
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func redfishFallbackMemberLooksLikePlaceholder(collectionPath string, doc map[string]interface{}) bool {
|
||||
if len(doc) == 0 {
|
||||
return true
|
||||
}
|
||||
path := normalizeRedfishPath(collectionPath)
|
||||
switch {
|
||||
case strings.HasSuffix(path, "/NetworkAdapters"):
|
||||
return redfishNetworkAdapterPlaceholderDoc(doc)
|
||||
case strings.HasSuffix(path, "/PCIeDevices"):
|
||||
return redfishPCIePlaceholderDoc(doc)
|
||||
case strings.Contains(path, "/Storage"):
|
||||
return redfishStoragePlaceholderDoc(doc)
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func redfishNetworkAdapterPlaceholderDoc(doc map[string]interface{}) bool {
|
||||
if normalizeRedfishIdentityField(asString(doc["Model"])) != "" ||
|
||||
normalizeRedfishIdentityField(asString(doc["Manufacturer"])) != "" ||
|
||||
normalizeRedfishIdentityField(asString(doc["SerialNumber"])) != "" ||
|
||||
normalizeRedfishIdentityField(asString(doc["PartNumber"])) != "" ||
|
||||
normalizeRedfishIdentityField(asString(doc["BDF"])) != "" ||
|
||||
asHexOrInt(doc["VendorId"]) != 0 ||
|
||||
asHexOrInt(doc["DeviceId"]) != 0 {
|
||||
return false
|
||||
}
|
||||
return redfishDocHasOnlyAllowedKeys(doc,
|
||||
"@odata.context",
|
||||
"@odata.id",
|
||||
"@odata.type",
|
||||
"Id",
|
||||
"Name",
|
||||
)
|
||||
}
|
||||
|
||||
func redfishPCIePlaceholderDoc(doc map[string]interface{}) bool {
|
||||
if normalizeRedfishIdentityField(asString(doc["Model"])) != "" ||
|
||||
normalizeRedfishIdentityField(asString(doc["Manufacturer"])) != "" ||
|
||||
normalizeRedfishIdentityField(asString(doc["SerialNumber"])) != "" ||
|
||||
normalizeRedfishIdentityField(asString(doc["PartNumber"])) != "" ||
|
||||
normalizeRedfishIdentityField(asString(doc["BDF"])) != "" ||
|
||||
asHexOrInt(doc["VendorId"]) != 0 ||
|
||||
asHexOrInt(doc["DeviceId"]) != 0 {
|
||||
return false
|
||||
}
|
||||
return redfishDocHasOnlyAllowedKeys(doc,
|
||||
"@odata.context",
|
||||
"@odata.id",
|
||||
"@odata.type",
|
||||
"Id",
|
||||
"Name",
|
||||
)
|
||||
}
|
||||
|
||||
func redfishStoragePlaceholderDoc(doc map[string]interface{}) bool {
|
||||
if normalizeRedfishIdentityField(asString(doc["Model"])) != "" ||
|
||||
normalizeRedfishIdentityField(asString(doc["Manufacturer"])) != "" ||
|
||||
normalizeRedfishIdentityField(asString(doc["SerialNumber"])) != "" ||
|
||||
normalizeRedfishIdentityField(asString(doc["PartNumber"])) != "" ||
|
||||
normalizeRedfishIdentityField(asString(doc["BDF"])) != "" ||
|
||||
asHexOrInt(doc["VendorId"]) != 0 ||
|
||||
asHexOrInt(doc["DeviceId"]) != 0 {
|
||||
return false
|
||||
}
|
||||
if !redfishDocHasOnlyAllowedKeys(doc,
|
||||
"@odata.id",
|
||||
"@odata.type",
|
||||
"Drives",
|
||||
"Drives@odata.count",
|
||||
"LogicalDisk",
|
||||
"PhysicalDisk",
|
||||
"Name",
|
||||
) {
|
||||
return false
|
||||
}
|
||||
return redfishFieldIsEmptyCollection(doc["Drives"]) &&
|
||||
redfishFieldIsZeroLike(doc["Drives@odata.count"]) &&
|
||||
redfishFieldIsEmptyCollection(doc["LogicalDisk"]) &&
|
||||
redfishFieldIsEmptyCollection(doc["PhysicalDisk"])
|
||||
}
|
||||
|
||||
func redfishDocHasOnlyAllowedKeys(doc map[string]interface{}, allowed ...string) bool {
|
||||
if len(doc) == 0 {
|
||||
return false
|
||||
}
|
||||
allowedSet := make(map[string]struct{}, len(allowed))
|
||||
for _, key := range allowed {
|
||||
allowedSet[key] = struct{}{}
|
||||
}
|
||||
for key := range doc {
|
||||
if _, ok := allowedSet[key]; !ok {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func redfishFieldIsEmptyCollection(v any) bool {
|
||||
switch x := v.(type) {
|
||||
case nil:
|
||||
return true
|
||||
case []interface{}:
|
||||
return len(x) == 0
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func redfishFieldIsZeroLike(v any) bool {
|
||||
switch x := v.(type) {
|
||||
case nil:
|
||||
return true
|
||||
case int:
|
||||
return x == 0
|
||||
case int32:
|
||||
return x == 0
|
||||
case int64:
|
||||
return x == 0
|
||||
case float64:
|
||||
return x == 0
|
||||
case string:
|
||||
x = strings.TrimSpace(x)
|
||||
return x == "" || x == "0"
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func cloneRawPayloads(src map[string]any) map[string]any {
|
||||
if len(src) == 0 {
|
||||
return nil
|
||||
|
||||
Reference in New Issue
Block a user