feat: HPE iLO support — profile, post-probe hang fix, replay parser fixes, AHS parser

- Add HPE iLO Redfish profile (priority 20): matches on manufacturer/OEM/iLO signals,
  adds SmartStorage/SmartStorageConfig to critical paths, sets realistic ETA baseline
  and rate policy for iLO's known slowness
- Fix post-probe hang on HPE iLO: skip numeric probing of collections where
  Members@odata.count == len(Members); add 4s postProbeClient timeout as safety net
- Exclude /WorkloadPerformanceAdvisor from crawl paths
- Fix replay parser: skip absent CPU sockets, absent DIMM slots, absent drive bays
- Filter N/A version entries from firmware inventory
- Remove drive firmware from general firmware list (already in Storage[].Firmware)
- Add HPE AHS (.ahs) archive parser with hybrid SMBIOS/Redfish extraction

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Mikhail Chusavitin
2026-03-30 13:39:14 +03:00
parent 3547ef9083
commit 0f310d57c4
14 changed files with 1729 additions and 12 deletions
+41
View File
@@ -96,6 +96,7 @@ 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()...))
firmware = filterStorageDriveFirmware(firmware, storageDevices)
bmcManagementSummary := r.collectBMCManagementSummary(managerPaths)
boardInfo.BMCMACAddress = strings.TrimSpace(firstNonEmpty(
asString(bmcManagementSummary["mac_address"]),
@@ -498,6 +499,10 @@ func (r redfishSnapshotReader) collectFirmwareInventory() []models.FirmwareInfo
if strings.TrimSpace(version) == "" {
continue
}
// Skip placeholder version strings that carry no useful information.
if strings.EqualFold(strings.TrimSpace(version), "N/A") {
continue
}
name := firmwareInventoryDeviceName(doc)
name = strings.TrimSpace(name)
if name == "" {
@@ -550,6 +555,32 @@ func dedupeFirmwareInfo(items []models.FirmwareInfo) []models.FirmwareInfo {
return out
}
// filterStorageDriveFirmware removes from fw any entries whose DeviceName+Version
// already appear as a storage drive's Model+Firmware. Drive firmware is already
// represented in the Storage section and should not be duplicated in the general
// firmware list.
func filterStorageDriveFirmware(fw []models.FirmwareInfo, storage []models.Storage) []models.FirmwareInfo {
if len(storage) == 0 {
return fw
}
driveFW := make(map[string]struct{}, len(storage))
for _, d := range storage {
model := strings.ToLower(strings.TrimSpace(d.Model))
rev := strings.ToLower(strings.TrimSpace(d.Firmware))
if model != "" && rev != "" {
driveFW[model+"|"+rev] = struct{}{}
}
}
out := fw[:0:0]
for _, f := range fw {
key := strings.ToLower(strings.TrimSpace(f.DeviceName)) + "|" + strings.ToLower(strings.TrimSpace(f.Version))
if _, skip := driveFW[key]; !skip {
out = append(out, f)
}
}
return out
}
func (r redfishSnapshotReader) collectThresholdSensors(chassisPaths []string) []models.SensorReading {
out := make([]models.SensorReading, 0)
seen := make(map[string]struct{})
@@ -1261,6 +1292,12 @@ func (r redfishSnapshotReader) collectProcessors(systemPath string) []models.CPU
!strings.EqualFold(pt, "CPU") && !strings.EqualFold(pt, "General") {
continue
}
// Skip absent processor sockets — empty slots with no CPU installed.
if status, ok := doc["Status"].(map[string]interface{}); ok {
if strings.EqualFold(asString(status["State"]), "Absent") {
continue
}
}
cpu := parseCPUs([]map[string]interface{}{doc})[0]
if cpu.Socket == 0 && socketIdx > 0 && strings.TrimSpace(asString(doc["Socket"])) == "" {
cpu.Socket = socketIdx
@@ -1287,6 +1324,10 @@ func (r redfishSnapshotReader) collectMemory(systemPath string) []models.MemoryD
out := make([]models.MemoryDIMM, 0, len(memberDocs))
for _, doc := range memberDocs {
dimm := parseMemory([]map[string]interface{}{doc})[0]
// Skip empty DIMM slots — no installed memory.
if !dimm.Present {
continue
}
supplementalDocs := r.getLinkedSupplementalDocs(doc, "MemoryMetrics", "EnvironmentMetrics", "Metrics")
if len(supplementalDocs) > 0 {
dimm.Details = mergeGenericDetails(dimm.Details, redfishMemoryDetailsAcrossDocs(doc, supplementalDocs...))