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

View File

@@ -19,6 +19,7 @@ const maxZipArchiveSize = 50 * 1024 * 1024
const maxGzipDecompressedSize = 50 * 1024 * 1024
var supportedArchiveExt = map[string]struct{}{
".ahs": {},
".gz": {},
".tgz": {},
".tar": {},
@@ -45,6 +46,8 @@ func ExtractArchive(archivePath string) ([]ExtractedFile, error) {
ext := strings.ToLower(filepath.Ext(archivePath))
switch ext {
case ".ahs":
return extractSingleFile(archivePath)
case ".gz", ".tgz":
return extractTarGz(archivePath)
case ".tar", ".sds":
@@ -66,6 +69,8 @@ func ExtractArchiveFromReader(r io.Reader, filename string) ([]ExtractedFile, er
ext := strings.ToLower(filepath.Ext(filename))
switch ext {
case ".ahs":
return extractSingleFileFromReader(r, filename)
case ".gz", ".tgz":
return extractTarGzFromReader(r, filename)
case ".tar", ".sds":

View File

@@ -76,6 +76,7 @@ func TestIsSupportedArchiveFilename(t *testing.T) {
name string
want bool
}{
{name: "HPE_CZ2D1X0GS3_20260330.ahs", want: true},
{name: "dump.tar.gz", want: true},
{name: "nvidia-bug-report-1651124000923.log.gz", want: true},
{name: "snapshot.zip", want: true},
@@ -124,3 +125,20 @@ func TestExtractArchiveFromReaderSDS(t *testing.T) {
t.Fatalf("expected bmc/pack.info, got %q", files[0].Path)
}
}
func TestExtractArchiveFromReaderAHS(t *testing.T) {
payload := []byte("ABJRtest")
files, err := ExtractArchiveFromReader(bytes.NewReader(payload), "sample.ahs")
if err != nil {
t.Fatalf("extract ahs from reader: %v", err)
}
if len(files) != 1 {
t.Fatalf("expected 1 extracted file, got %d", len(files))
}
if files[0].Path != "sample.ahs" {
t.Fatalf("expected sample.ahs, got %q", files[0].Path)
}
if string(files[0].Content) != string(payload) {
t.Fatalf("content mismatch")
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,210 @@
package hpe_ilo_ahs
import (
"bytes"
"compress/gzip"
"encoding/binary"
"testing"
"git.mchus.pro/mchus/logpile/internal/parser"
)
func TestDetectAHS(t *testing.T) {
p := &Parser{}
score := p.Detect([]parser.ExtractedFile{{
Path: "HPE_CZ2D1X0GS3_20260330.ahs",
Content: makeAHSArchive(t, []ahsTestEntry{{Name: "CUST_INFO.DAT", Payload: []byte("x")}}),
}})
if score < 80 {
t.Fatalf("expected high confidence detect, got %d", score)
}
}
func TestParseAHSInventory(t *testing.T) {
p := &Parser{}
content := makeAHSArchive(t, []ahsTestEntry{
{Name: "CUST_INFO.DAT", Payload: make([]byte, 16)},
{Name: "0000088-2026-03-30.zbb", Payload: gzipBytes(t, []byte(sampleInventoryBlob()))},
})
result, err := p.Parse([]parser.ExtractedFile{{
Path: "HPE_CZ2D1X0GS3_20260330.ahs",
Content: content,
}})
if err != nil {
t.Fatalf("parse failed: %v", err)
}
if result.Hardware == nil {
t.Fatalf("expected hardware section")
}
board := result.Hardware.BoardInfo
if board.Manufacturer != "HPE" {
t.Fatalf("unexpected board manufacturer: %q", board.Manufacturer)
}
if board.ProductName != "ProLiant DL380 Gen11" {
t.Fatalf("unexpected board product: %q", board.ProductName)
}
if board.SerialNumber != "CZ2D1X0GS3" {
t.Fatalf("unexpected board serial: %q", board.SerialNumber)
}
if board.PartNumber != "P52560-421" {
t.Fatalf("unexpected board part number: %q", board.PartNumber)
}
if len(result.Hardware.CPUs) != 1 || result.Hardware.CPUs[0].Model != "Intel(R) Xeon(R) Gold 6444Y" {
t.Fatalf("unexpected CPUs: %+v", result.Hardware.CPUs)
}
if len(result.Hardware.Memory) != 1 {
t.Fatalf("expected one DIMM, got %d", len(result.Hardware.Memory))
}
if result.Hardware.Memory[0].PartNumber != "HMCG88AEBRA115N" {
t.Fatalf("unexpected DIMM part number: %q", result.Hardware.Memory[0].PartNumber)
}
if len(result.Hardware.NetworkAdapters) != 2 {
t.Fatalf("expected two network adapters, got %d", len(result.Hardware.NetworkAdapters))
}
if len(result.Hardware.PowerSupply) != 1 {
t.Fatalf("expected one PSU, got %d", len(result.Hardware.PowerSupply))
}
if result.Hardware.PowerSupply[0].SerialNumber != "5XUWB0C4DJG4BV" {
t.Fatalf("unexpected PSU serial: %q", result.Hardware.PowerSupply[0].SerialNumber)
}
if len(result.Hardware.Storage) != 1 {
t.Fatalf("expected one physical drive, got %d", len(result.Hardware.Storage))
}
drive := result.Hardware.Storage[0]
if drive.Model != "SAMSUNGMZ7L3480HCHQ-00A07" {
t.Fatalf("unexpected drive model: %q", drive.Model)
}
if drive.SerialNumber != "S664NC0Y502720" {
t.Fatalf("unexpected drive serial: %q", drive.SerialNumber)
}
if drive.SizeGB != 480 {
t.Fatalf("unexpected drive size: %d", drive.SizeGB)
}
if len(result.Hardware.Firmware) == 0 {
t.Fatalf("expected firmware inventory")
}
foundILO := false
foundControllerFW := false
for _, item := range result.Hardware.Firmware {
if item.DeviceName == "iLO 6" && item.Version == "v1.63p20" {
foundILO = true
}
if item.DeviceName == "HPE MR408i-o Gen11" && item.Version == "52.26.3-5379" {
foundControllerFW = true
}
}
if !foundILO {
t.Fatalf("expected iLO firmware entry")
}
if !foundControllerFW {
t.Fatalf("expected controller firmware entry")
}
if len(result.Hardware.Devices) < 6 {
t.Fatalf("expected canonical devices, got %d", len(result.Hardware.Devices))
}
if len(result.Events) == 0 {
t.Fatalf("expected parsed events")
}
}
type ahsTestEntry struct {
Name string
Payload []byte
Flag uint32
}
func makeAHSArchive(t *testing.T, entries []ahsTestEntry) []byte {
t.Helper()
var buf bytes.Buffer
for _, entry := range entries {
header := make([]byte, ahsHeaderSize)
copy(header[:4], []byte("ABJR"))
binary.LittleEndian.PutUint16(header[4:6], 0x0300)
binary.LittleEndian.PutUint16(header[6:8], 0x0002)
binary.LittleEndian.PutUint32(header[8:12], uint32(len(entry.Payload)))
flag := entry.Flag
if flag == 0 {
flag = 0x80000002
if len(entry.Payload) >= 2 && entry.Payload[0] == 0x1f && entry.Payload[1] == 0x8b {
flag = 0x80000001
}
}
binary.LittleEndian.PutUint32(header[16:20], flag)
copy(header[20:52], []byte(entry.Name))
buf.Write(header)
buf.Write(entry.Payload)
}
return buf.Bytes()
}
func gzipBytes(t *testing.T, payload []byte) []byte {
t.Helper()
var buf bytes.Buffer
zw := gzip.NewWriter(&buf)
if _, err := zw.Write(payload); err != nil {
t.Fatalf("gzip payload: %v", err)
}
if err := zw.Close(); err != nil {
t.Fatalf("close gzip writer: %v", err)
}
return buf.Bytes()
}
func sampleInventoryBlob() string {
return stringsJoin(
"iLO 6 v1.63p20 built on Sep 13 2024",
"HPE",
"ProLiant DL380 Gen11",
"CZ2D1X0GS3",
"P52560-421",
"Proc 1",
"Intel(R) Corporation",
"Intel(R) Xeon(R) Gold 6444Y",
"PROC 1 DIMM 3",
"Hynix",
"HMCG88AEBRA115N",
"2B5F92C6",
"Power Supply 1",
"5XUWB0C4DJG4BV",
"P03178-B21",
"PciRoot(0x1)/Pci(0x5,0x0)/Pci(0x0,0x0)",
"NIC.Slot.1.1",
"Network Controller",
"Slot 1",
"MCX512A-ACAT",
"MT2230478382",
"PciRoot(0x3)/Pci(0x1,0x0)/Pci(0x0,0x0)",
"OCP.Slot.15.1",
"Broadcom NetXtreme Gigabit Ethernet - NIC",
"OCP Slot 15",
"P51183-001",
"1CH0150001",
"20.28.41",
"System ROM",
"v2.22 (06/19/2024)",
"03/30/2026 09:47:33",
"iLO network link down.",
`{"@odata.id":"/redfish/v1/Systems/1/Storage/DE00A000/Controllers/0","@odata.type":"#StorageController.v1_7_0.StorageController","Id":"0","Name":"HPE MR408i-o Gen11","FirmwareVersion":"52.26.3-5379","Manufacturer":"HPE","Model":"HPE MR408i-o Gen11","PartNumber":"P58543-001","SKU":"P58335-B21","SerialNumber":"PXSFQ0BBIJY3B3","Status":{"State":"Enabled","Health":"OK"},"Location":{"PartLocation":{"ServiceLabel":"Slot=14","LocationType":"Slot","LocationOrdinalValue":14}},"PCIeInterface":{"PCIeType":"Gen4","LanesInUse":8}}`,
`{"@odata.id":"/redfish/v1/Chassis/DE00A000/Drives/0","@odata.type":"#Drive.v1_17_0.Drive","Id":"0","Name":"480GB 6G SATA SSD","Status":{"State":"StandbyOffline","Health":"OK"},"PhysicalLocation":{"PartLocation":{"ServiceLabel":"Slot=14:Port=1:Box=3:Bay=1","LocationType":"Bay","LocationOrdinalValue":1}},"CapacityBytes":480103981056,"MediaType":"SSD","Model":"SAMSUNGMZ7L3480HCHQ-00A07","Protocol":"SATA","Revision":"JXTC604Q","SerialNumber":"S664NC0Y502720","PredictedMediaLifeLeftPercent":100}`,
`{"@odata.id":"/redfish/v1/Chassis/DE00A000/Drives/64515","@odata.type":"#Drive.v1_17_0.Drive","Id":"64515","Name":"Empty Bay","Status":{"State":"Absent","Health":"OK"}}`,
)
}
func stringsJoin(parts ...string) string {
return string(bytes.Join(func() [][]byte {
out := make([][]byte, 0, len(parts))
for _, part := range parts {
out = append(out, []byte(part))
}
return out
}(), []byte{0}))
}

View File

@@ -7,6 +7,7 @@ import (
_ "git.mchus.pro/mchus/logpile/internal/parser/vendors/dell"
_ "git.mchus.pro/mchus/logpile/internal/parser/vendors/easy_bee"
_ "git.mchus.pro/mchus/logpile/internal/parser/vendors/h3c"
_ "git.mchus.pro/mchus/logpile/internal/parser/vendors/hpe_ilo_ahs"
_ "git.mchus.pro/mchus/logpile/internal/parser/vendors/inspur"
_ "git.mchus.pro/mchus/logpile/internal/parser/vendors/nvidia"
_ "git.mchus.pro/mchus/logpile/internal/parser/vendors/nvidia_bug_report"