package hpe_ilo_ahs import ( "bytes" "compress/gzip" "encoding/binary" "encoding/json" "fmt" "io" "path/filepath" "regexp" "sort" "strconv" "strings" "time" "git.mchus.pro/mchus/logpile/internal/models" "git.mchus.pro/mchus/logpile/internal/parser" ) const ( parserVersion = "1.0" ahsHeaderSize = 116 maxGzipSize = 50 * 1024 * 1024 ) var ( partNumberPattern = regexp.MustCompile(`(?i)^[a-z0-9]{1,4}\d{4,6}-[a-z0-9]{2,4}$`) serverSerialRE = regexp.MustCompile(`(?i)(?:^|[_-])([a-z0-9]{10})(?:[_-]|\.)`) dimmSlotRE = regexp.MustCompile(`^PROC\s+(\d+)\s+DIMM\s+(\d+)$`) procSlotRE = regexp.MustCompile(`^Proc\s+(\d+)$`) psuSlotRE = regexp.MustCompile(`^Power Supply\s+(\d+)$`) eventTimeRE = regexp.MustCompile(`^\d{2}/\d{2}/\d{4} \d{2}:\d{2}:\d{2}$`) psuXMLRE = regexp.MustCompile(`(?s)(.*?)`) firmwareLockdownRE = regexp.MustCompile(`(?s)(.*?)`) xmlFieldRE = regexp.MustCompile(`(?s)<([A-Za-z0-9_-]+)>([^<]*)`) psuLogRE = regexp.MustCompile(`Update bay (\d+) (SPN|Serial Number|Model Number|fw ver\.), value = ([A-Za-z0-9._-]+)`) versionFragmentRE = regexp.MustCompile(`\d+(?:\.\d+)+`) ) func init() { parser.Register(&Parser{}) } type Parser struct{} func (p *Parser) Name() string { return "HPE iLO AHS Parser" } func (p *Parser) Vendor() string { return "hpe_ilo_ahs" } func (p *Parser) Version() string { return parserVersion } func (p *Parser) Detect(files []parser.ExtractedFile) int { if len(files) != 1 { return 0 } file := files[0] if len(file.Content) < ahsHeaderSize || !bytes.HasPrefix(file.Content, []byte("ABJR")) { return 0 } score := 55 name := strings.ToLower(file.Path) if strings.HasSuffix(name, ".ahs") { score += 30 } if bytes.Contains(file.Content, []byte("CUST_INFO.DAT")) { score += 10 } if bytes.Contains(file.Content, []byte(".zbb")) || bytes.Contains(file.Content, []byte("ilo_boot_support.zbb")) { score += 10 } if score > 100 { score = 100 } return score } func (p *Parser) Parse(files []parser.ExtractedFile) (*models.AnalysisResult, error) { if len(files) == 0 { return emptyResult(), nil } entries, err := parseAHSContainer(files[0].Content) if err != nil { return nil, fmt.Errorf("parse ahs container: %w", err) } result := emptyResult() result.SourceType = models.SourceTypeArchive tokens := make([]string, 0, 2048) redfishDocs := make(map[string]map[string]any) rawMetadata := make([]map[string]any, 0, len(entries)) for _, entry := range entries { rawMetadata = append(rawMetadata, map[string]any{ "name": entry.Name, "compressed": entry.Compressed, "compressed_size": len(entry.Payload), "uncompressed_size": len(entry.Content), "flag": entry.Flag, }) if len(entry.Content) == 0 { continue } tokens = append(tokens, printableTokens(entry.Content, 3)...) for path, doc := range extractEmbeddedRedfishDocs(entry.Content) { redfishDocs[path] = doc } } if len(rawMetadata) > 0 { result.RawPayloads = map[string]any{ "hpe_ahs_entries": rawMetadata, } } board := parseBoardInfo(tokens, files[0].Path) result.Hardware.BoardInfo = board if board.ProductName != "" || board.SerialNumber != "" || board.PartNumber != "" { result.FRU = append(result.FRU, models.FRUInfo{ Description: "System", Manufacturer: board.Manufacturer, ProductName: board.ProductName, SerialNumber: board.SerialNumber, PartNumber: board.PartNumber, Version: board.Version, }) } result.Hardware.CPUs = dedupeCPUs(parseCPUs(tokens)) result.Hardware.Memory = dedupeMemory(parseDIMMs(tokens)) result.Hardware.PowerSupply = dedupePSUs(parsePSUs(tokens)) result.Hardware.NetworkAdapters = dedupeNetworkAdapters(parseNetworkAdapters(tokens)) result.Hardware.Firmware = dedupeFirmware(parseFirmware(tokens)) psuSupplements := parsePSUSupplements(entries) result.Hardware.PowerSupply = dedupePSUs(mergePSUs(result.Hardware.PowerSupply, psuSupplements)) lockdownFW, nicFirmwareByVendor := parseBCertFirmware(entries) result.Hardware.NetworkAdapters = dedupeNetworkAdapters(enrichNetworkAdapters(result.Hardware.NetworkAdapters, nicFirmwareByVendor)) result.Hardware.Firmware = dedupeFirmware(append(result.Hardware.Firmware, lockdownFW...)) storage, volumes, controllerDevices, controllerFW := parseRedfishStorage(redfishDocs) result.Hardware.Storage = dedupeStorage(storage) result.Hardware.Volumes = volumes result.Hardware.Firmware = dedupeFirmware(append(result.Hardware.Firmware, controllerFW...)) result.Events = dedupeEvents(parseEvents(tokens)) if result.CollectedAt.IsZero() { for _, ev := range result.Events { if ev.Timestamp.After(result.CollectedAt) { result.CollectedAt = ev.Timestamp.UTC() } } } result.Hardware.Devices = buildDevices( result.Hardware.BoardInfo, result.Hardware.CPUs, result.Hardware.Memory, result.Hardware.Storage, result.Hardware.NetworkAdapters, result.Hardware.PowerSupply, controllerDevices, ) return result, nil } type ahsEntry struct { Name string Flag uint32 Payload []byte Content []byte Compressed bool } func emptyResult() *models.AnalysisResult { return &models.AnalysisResult{ Events: make([]models.Event, 0), FRU: make([]models.FRUInfo, 0), Sensors: make([]models.SensorReading, 0), Hardware: &models.HardwareConfig{ Firmware: make([]models.FirmwareInfo, 0), Devices: make([]models.HardwareDevice, 0), CPUs: make([]models.CPU, 0), Memory: make([]models.MemoryDIMM, 0), Storage: make([]models.Storage, 0), Volumes: make([]models.StorageVolume, 0), PCIeDevices: make([]models.PCIeDevice, 0), GPUs: make([]models.GPU, 0), NetworkCards: make([]models.NIC, 0), NetworkAdapters: make([]models.NetworkAdapter, 0), PowerSupply: make([]models.PSU, 0), }, } } func parseAHSContainer(data []byte) ([]ahsEntry, error) { entries := make([]ahsEntry, 0, 8) offset := 0 for offset < len(data) { if offset+ahsHeaderSize > len(data) { return nil, fmt.Errorf("truncated header at offset %d", offset) } if !bytes.Equal(data[offset:offset+4], []byte("ABJR")) { return nil, fmt.Errorf("invalid magic at offset %d", offset) } size := int(binary.LittleEndian.Uint32(data[offset+8 : offset+12])) flag := binary.LittleEndian.Uint32(data[offset+16 : offset+20]) name := strings.TrimRight(string(data[offset+20:offset+52]), "\x00") start := offset + ahsHeaderSize end := start + size if size < 0 || end > len(data) { return nil, fmt.Errorf("invalid payload size for %q", name) } payload := append([]byte(nil), data[start:end]...) content := payload compressed := len(payload) >= 2 && payload[0] == 0x1f && payload[1] == 0x8b if compressed { decoded, err := gunzipLimited(payload) if err == nil { content = decoded } } entries = append(entries, ahsEntry{ Name: name, Flag: flag, Payload: payload, Content: content, Compressed: compressed, }) offset = end } return entries, nil } func gunzipLimited(payload []byte) ([]byte, error) { gr, err := gzip.NewReader(bytes.NewReader(payload)) if err != nil { return nil, err } defer gr.Close() buf, err := io.ReadAll(io.LimitReader(gr, maxGzipSize+1)) if err != nil { return nil, err } if len(buf) > maxGzipSize { return nil, fmt.Errorf("gzip payload exceeded %d bytes", maxGzipSize) } return buf, nil } func printableTokens(data []byte, minLen int) []string { out := make([]string, 0, 256) start := -1 for i, b := range data { if b >= 32 && b <= 126 { if start == -1 { start = i } continue } if start != -1 && i-start >= minLen { token := strings.TrimSpace(string(data[start:i])) if token != "" { out = append(out, token) } } start = -1 } if start != -1 && len(data)-start >= minLen { token := strings.TrimSpace(string(data[start:])) if token != "" { out = append(out, token) } } return out } func extractEmbeddedRedfishDocs(data []byte) map[string]map[string]any { out := make(map[string]map[string]any) marker := []byte(`{"@odata`) for offset := 0; offset < len(data); { idx := bytes.Index(data[offset:], marker) if idx < 0 { break } start := offset + idx end, ok := findBalancedJSONObject(data, start) if !ok { offset = start + 1 continue } var doc map[string]any if err := json.Unmarshal(data[start:end], &doc); err == nil { path := strings.TrimSpace(asString(doc["@odata.id"])) if strings.HasPrefix(path, "/redfish/") { out[path] = doc } } offset = end } return out } func findBalancedJSONObject(data []byte, start int) (int, bool) { if start >= len(data) || data[start] != '{' { return 0, false } depth := 0 inString := false escaped := false for i := start; i < len(data); i++ { c := data[i] if inString { switch { case escaped: escaped = false case c == '\\': escaped = true case c == '"': inString = false } continue } switch c { case '"': inString = true case '{': depth++ case '}': depth-- if depth == 0 { return i + 1, true } } } return 0, false } func parseBoardInfo(tokens []string, path string) models.BoardInfo { var board models.BoardInfo for i := 0; i+3 < len(tokens); i++ { manufacturer := strings.TrimSpace(tokens[i]) model := sanitizeModel(tokens[i+1]) if !isHPEManufacturer(manufacturer) || !looksLikeServerModel(model) { continue } board.Manufacturer = "HPE" board.ProductName = model if isLikelySerial(tokens[i+2]) { board.SerialNumber = tokens[i+2] } if looksLikePartNumber(tokens[i+3]) { board.PartNumber = tokens[i+3] } break } if board.Manufacturer == "" && strings.Contains(strings.ToUpper(filepath.Base(path)), "HPE") { board.Manufacturer = "HPE" } if board.SerialNumber == "" { if match := serverSerialRE.FindStringSubmatch(strings.ToUpper(filepath.Base(path))); len(match) == 2 { board.SerialNumber = match[1] } } if board.ProductName == "" { for _, token := range tokens { if looksLikeServerModel(token) { board.ProductName = sanitizeModel(token) break } } } return board } func parseCPUs(tokens []string) []models.CPU { out := make([]models.CPU, 0, 2) for i := 0; i+2 < len(tokens); i++ { match := procSlotRE.FindStringSubmatch(tokens[i]) if len(match) != 2 { continue } socket, _ := strconv.Atoi(match[1]) model := "" manufacturer := "" for j := i + 1; j < len(tokens) && j <= i+5; j++ { if strings.HasPrefix(tokens[j], "PROC ") || procSlotRE.MatchString(tokens[j]) { break } if manufacturer == "" && looksLikeCPUVendor(tokens[j]) { manufacturer = tokens[j] continue } if looksLikeCPUModel(tokens[j]) { model = tokens[j] break } } if model == "" { continue } cpu := models.CPU{ Socket: socket, Model: model, Description: manufacturer, Status: "ok", } out = append(out, cpu) } return out } func parseDIMMs(tokens []string) []models.MemoryDIMM { out := make([]models.MemoryDIMM, 0, 16) for i := 0; i+3 < len(tokens); i++ { match := dimmSlotRE.FindStringSubmatch(tokens[i]) if len(match) != 3 { continue } slot := tokens[i] manufacturer := tokens[i+1] partNumber := tokens[i+2] serial := tokens[i+3] if isUnavailable(partNumber) || isUnavailable(serial) { continue } if isUnavailable(manufacturer) || strings.EqualFold(manufacturer, "unknown") { manufacturer = "" } out = append(out, models.MemoryDIMM{ Slot: slot, Location: slot, Present: true, Manufacturer: manufacturer, PartNumber: partNumber, SerialNumber: serial, Status: "ok", }) } return out } func parsePSUs(tokens []string) []models.PSU { out := make([]models.PSU, 0, 4) for i := 0; i < len(tokens); i++ { match := psuSlotRE.FindStringSubmatch(tokens[i]) if len(match) != 2 { continue } slot := "PSU " + match[1] vendor := "" serial := "" partNumber := "" for j := i + 1; j < len(tokens) && j <= i+5; j++ { field := strings.TrimSpace(tokens[j]) if strings.HasPrefix(field, "PciRoot(") || psuSlotRE.MatchString(field) || dimmSlotRE.MatchString(field) || procSlotRE.MatchString(field) || eventTimeRE.MatchString(field) { break } switch { case vendor == "" && looksLikePSUVendor(field): vendor = field case partNumber == "" && looksLikePartNumber(field): partNumber = field case serial == "" && isLikelySerial(field): serial = field } } if serial == "" && partNumber == "" { continue } psu := models.PSU{ Slot: slot, Present: true, Model: valueOr(partNumber, "Power Supply"), Vendor: valueOr(cleanUnavailable(vendor), "HPE"), SerialNumber: cleanUnavailable(serial), PartNumber: cleanUnavailable(partNumber), Status: "ok", } out = append(out, psu) } return out } func parsePSUSupplements(entries []ahsEntry) []models.PSU { bySlot := make(map[string]models.PSU) for _, entry := range entries { text := string(entry.Content) if text == "" { continue } if strings.EqualFold(entry.Name, "bcert.pkg") { for _, match := range psuXMLRE.FindAllStringSubmatch(text, -1) { slotNum, _ := strconv.Atoi(match[1]) slot := fmt.Sprintf("PSU %d", slotNum+1) fields := parseXMLFields(match[2]) item := bySlot[slot] item.Slot = slot item.Present = strings.EqualFold(fields["Present"], "Yes") || item.Present if serial := strings.TrimSpace(fields["SerialNumber"]); serial != "" { item.SerialNumber = serial } if fw := strings.TrimSpace(fields["FirmwareVersion"]); fw != "" { item.Firmware = fw } if spare := strings.TrimSpace(fields["SparePartNumber"]); spare != "" { if item.Details == nil { item.Details = make(map[string]any) } item.Details["spare_part_number"] = spare } bySlot[slot] = item } } for _, match := range psuLogRE.FindAllStringSubmatch(text, -1) { slotNum, _ := strconv.Atoi(match[1]) slot := fmt.Sprintf("PSU %d", slotNum+1) item := bySlot[slot] item.Slot = slot item.Present = true value := strings.TrimSpace(match[3]) switch match[2] { case "SPN": if item.Details == nil { item.Details = make(map[string]any) } item.Details["spare_part_number"] = value case "Serial Number": item.SerialNumber = value case "Model Number": item.Model = value item.PartNumber = value case "fw ver.": item.Firmware = normalizeLooseVersion(value) } bySlot[slot] = item } } out := make([]models.PSU, 0, len(bySlot)) for _, item := range bySlot { if item.Slot == "" { continue } item.Vendor = valueOr(item.Vendor, "HPE") item.Status = valueOr(item.Status, "ok") if item.Model == "" { item.Model = valueOr(item.PartNumber, "Power Supply") } out = append(out, item) } sort.Slice(out, func(i, j int) bool { return out[i].Slot < out[j].Slot }) return out } type pcieSequence struct { UEFIPath string Code string Fields []string } func parseNetworkAdapters(tokens []string) []models.NetworkAdapter { sequences := collectPCIeSequences(tokens) out := make([]models.NetworkAdapter, 0, 4) for _, seq := range sequences { if strings.Contains(seq.Code, "DriveBay") { continue } if len(seq.Fields) == 0 { continue } title := seq.Fields[0] if strings.Contains(strings.ToLower(title), "empty") { continue } if !looksLikeNetworkTitle(seq.Code, title, seq.Fields) { continue } location := "" model := title description := "" partNumber := "" serial := "" firmware := "" for _, field := range seq.Fields[1:] { switch { case location == "" && looksLikeLocation(field): location = field case partNumber == "" && looksLikePartNumber(field): partNumber = field case serial == "" && isLikelySerial(field): serial = field case firmware == "" && looksLikeVersion(field): firmware = field case model == title && looksLikeConcreteModel(field): model = field case description == "" && field != model: description = field } } if model == "Network Controller" && description != "" { model, description = description, title } out = append(out, models.NetworkAdapter{ Slot: slotLabelFromCode(seq.Code), Location: valueOr(location, slotLabelFromCode(seq.Code)), Present: true, Model: model, Description: description, Vendor: inferVendor(model), SerialNumber: serial, PartNumber: partNumber, Firmware: firmware, Status: "ok", Details: map[string]any{ "uefi_path": seq.UEFIPath, "source": "smbios_slot_inventory", }, }) } return out } func collectPCIeSequences(tokens []string) []pcieSequence { out := make([]pcieSequence, 0, 16) for i := 0; i < len(tokens); i++ { if !strings.HasPrefix(tokens[i], "PciRoot(") { continue } if i+1 >= len(tokens) { continue } seq := pcieSequence{ UEFIPath: tokens[i], Code: tokens[i+1], Fields: make([]string, 0, 6), } for j := i + 2; j < len(tokens) && len(seq.Fields) < 6; j++ { if strings.HasPrefix(tokens[j], "PciRoot(") || dimmSlotRE.MatchString(tokens[j]) || procSlotRE.MatchString(tokens[j]) || psuSlotRE.MatchString(tokens[j]) { break } seq.Fields = append(seq.Fields, tokens[j]) } out = append(out, seq) } return out } func parseFirmware(tokens []string) []models.FirmwareInfo { out := make([]models.FirmwareInfo, 0, 8) seen := make(map[string]bool) for _, token := range tokens { if strings.HasPrefix(token, "iLO ") && strings.Contains(token, " built on ") { version := token build := "" if idx := strings.Index(token, " built on "); idx > 0 { version = strings.TrimSpace(token[:idx]) build = strings.TrimSpace(token[idx+10:]) } name := version if fields := strings.Fields(version); len(fields) >= 2 { name = strings.Join(fields[:2], " ") version = strings.TrimSpace(strings.TrimPrefix(version, name)) } appendFirmware(&out, seen, models.FirmwareInfo{ DeviceName: name, Version: strings.TrimSpace(version), BuildTime: build, }) } } for i := 0; i+1 < len(tokens); i++ { name := tokens[i] version := tokens[i+1] if !isTopLevelFirmwareLabel(name) || !looksLikeVersion(version) { continue } appendFirmware(&out, seen, models.FirmwareInfo{ DeviceName: name, Version: version, }) } return out } func parseRedfishStorage(docs map[string]map[string]any) ([]models.Storage, []models.StorageVolume, []models.HardwareDevice, []models.FirmwareInfo) { paths := make([]string, 0, len(docs)) for path := range docs { paths = append(paths, path) } sort.Strings(paths) storage := make([]models.Storage, 0, 8) volumes := make([]models.StorageVolume, 0, 4) devices := make([]models.HardwareDevice, 0, 6) firmware := make([]models.FirmwareInfo, 0, 8) fabricNames := make(map[string]string) fabricTypes := make(map[string]string) for _, path := range paths { doc := docs[path] docType := asString(doc["@odata.type"]) switch { case strings.Contains(docType, "#Fabric."): fabricID := redfishID(path) fabricNames[fabricID] = strings.TrimSpace(asString(doc["Name"])) fabricTypes[fabricID] = strings.TrimSpace(asString(doc["FabricType"])) case strings.Contains(docType, "#Switch."): fabricID := fabricIDFromPath(path) name := valueOr(fabricNames[fabricID], strings.TrimSpace(asString(doc["Name"]))) model := strings.TrimSpace(asString(doc["Model"])) fw := strings.TrimSpace(asString(doc["FirmwareVersion"])) device := models.HardwareDevice{ ID: "hpe-fabric-" + redfishID(path), Kind: models.DeviceKindStorage, Source: "redfish", Slot: valueOr(fabricID, redfishID(path)), DeviceClass: "storage_backplane", Model: valueOr(name, model), PartNumber: model, Firmware: fw, Status: redfishStatus(doc["Status"]), Details: map[string]any{ "odata_id": path, "fabric_type": valueOr(fabricTypes[fabricID], strings.TrimSpace(asString(doc["FabricType"]))), "switch_type": strings.TrimSpace(asString(doc["SwitchType"])), "supported_protocols": stringSlice(doc["SupportedProtocols"]), "domain_id": asInt64(doc["DomainID"]), "fabric_name": fabricNames[fabricID], "connected_chassis_id": asString(nested(doc, "Links", "Chassis", "@odata.id")), }, } devices = append(devices, device) if fw != "" { firmware = append(firmware, models.FirmwareInfo{ DeviceName: valueOr(name, model), Version: fw, }) } case strings.Contains(docType, "#StorageController."): slot := redfishServiceLabel(doc, "Location", "PartLocation", "ServiceLabel") model := valueOr(asString(doc["Model"]), asString(doc["Name"])) partNumber := strings.TrimSpace(asString(doc["PartNumber"])) sku := strings.TrimSpace(asString(doc["SKU"])) serial := strings.TrimSpace(asString(doc["SerialNumber"])) fw := strings.TrimSpace(asString(doc["FirmwareVersion"])) device := models.HardwareDevice{ ID: "hpe-ctrl-" + redfishID(path), Kind: models.DeviceKindStorage, Source: "redfish", Slot: slot, Location: slot, DeviceClass: "storage_controller", Model: model, PartNumber: valueOr(partNumber, sku), Manufacturer: strings.TrimSpace(asString(doc["Manufacturer"])), SerialNumber: serial, Firmware: fw, Status: redfishStatus(doc["Status"]), Details: map[string]any{ "odata_id": path, "part_number": partNumber, "sku": sku, "speed_gbps": asFloat64(doc["SpeedGbps"]), "supported_controller_protocols": stringSlice(doc["SupportedControllerProtocols"]), "supported_device_protocols": stringSlice(doc["SupportedDeviceProtocols"]), "supported_raid_types": stringSlice(doc["SupportedRAIDTypes"]), "cache_total_mib": asInt64(nested(doc, "CacheSummary", "TotalCacheSizeMiB")), "persistent_cache_mib": asInt64(nested(doc, "CacheSummary", "PersistentCacheSizeMiB")), "durable_name": firstDurableName(doc), }, } if width := asInt(doc, "PCIeInterface", "LanesInUse"); width > 0 { device.LinkWidth = width } if speed := strings.TrimSpace(asString(nested(doc, "PCIeInterface", "PCIeType"))); speed != "" { device.LinkSpeed = speed } devices = append(devices, device) if fw != "" { firmware = append(firmware, models.FirmwareInfo{ DeviceName: model, Description: slot, Version: fw, }) } case strings.Contains(docType, "#Drive."): if strings.EqualFold(redfishStatus(doc["Status"]), "absent") { continue } capacity := asInt64(doc["CapacityBytes"]) slot := redfishServiceLabel(doc, "PhysicalLocation", "PartLocation", "ServiceLabel") if slot == "" { slot = redfishServiceLabel(doc, "Location", "PartLocation", "ServiceLabel") } endurance := asOptionalInt(doc["PredictedMediaLifeLeftPercent"]) entry := models.Storage{ Slot: slot, Type: valueOr(asString(doc["MediaType"]), "Drive"), Model: valueOr(asString(doc["Model"]), asString(doc["Name"])), Description: strings.TrimSpace(asString(doc["Name"])), SizeGB: bytesToDecimalGB(capacity), SerialNumber: strings.TrimSpace(asString(doc["SerialNumber"])), Firmware: strings.TrimSpace(asString(doc["Revision"])), Interface: valueOr(asString(doc["Protocol"]), asString(doc["MediaType"])), Present: true, RemainingEndurancePct: endurance, Status: redfishStatus(doc["Status"]), Details: map[string]any{ "odata_id": path, "capacity_bytes": capacity, "failure_predicted": asBool(doc["FailurePredicted"]), "negotiated_speed_gbps": asFloat64(doc["NegotiatedSpeedGbs"]), "capable_speed_gbps": asFloat64(doc["CapableSpeedGbs"]), "location_indicator_active": asBool(doc["LocationIndicatorActive"]), }, } storage = append(storage, entry) case strings.Contains(docType, "#Volume.") && !strings.HasSuffix(path, "/Capabilities"): volumes = append(volumes, models.StorageVolume{ ID: strings.TrimSpace(asString(doc["Id"])), Name: strings.TrimSpace(asString(doc["Name"])), RAIDLevel: strings.TrimSpace(asString(doc["RAIDType"])), CapacityBytes: asInt64(doc["CapacityBytes"]), SizeGB: bytesToDecimalGB(asInt64(doc["CapacityBytes"])), Status: redfishStatus(doc["Status"]), }) } } return storage, dedupeVolumes(volumes), dedupeDevices(devices), dedupeFirmware(firmware) } func buildDevices(board models.BoardInfo, cpus []models.CPU, memory []models.MemoryDIMM, storage []models.Storage, adapters []models.NetworkAdapter, psus []models.PSU, extras []models.HardwareDevice) []models.HardwareDevice { devices := make([]models.HardwareDevice, 0, 1+len(cpus)+len(memory)+len(storage)+len(adapters)+len(psus)+len(extras)) if board.ProductName != "" || board.SerialNumber != "" { devices = append(devices, models.HardwareDevice{ ID: "hpe-board", Kind: models.DeviceKindBoard, Source: "smbios", Model: board.ProductName, Manufacturer: board.Manufacturer, SerialNumber: board.SerialNumber, PartNumber: board.PartNumber, Status: "ok", }) } for _, cpu := range cpus { devices = append(devices, models.HardwareDevice{ ID: fmt.Sprintf("hpe-cpu-%d", cpu.Socket), Kind: models.DeviceKindCPU, Source: "smbios", Slot: fmt.Sprintf("CPU %d", cpu.Socket), Model: cpu.Model, Manufacturer: strings.TrimSpace(cpu.Description), Cores: cpu.Cores, Threads: cpu.Threads, FrequencyMHz: cpu.FrequencyMHz, MaxFreqMHz: cpu.MaxFreqMHz, Status: cpu.Status, }) } for _, dimm := range memory { devices = append(devices, models.HardwareDevice{ ID: "hpe-mem-" + sanitizeID(dimm.Slot), Kind: models.DeviceKindMemory, Source: "smbios", Slot: dimm.Slot, Location: dimm.Location, Model: dimm.PartNumber, Manufacturer: dimm.Manufacturer, SerialNumber: dimm.SerialNumber, PartNumber: dimm.PartNumber, Present: boolPtr(dimm.Present), Status: dimm.Status, }) } for _, disk := range storage { devices = append(devices, models.HardwareDevice{ ID: "hpe-disk-" + sanitizeID(valueOr(disk.SerialNumber, disk.Slot)), Kind: models.DeviceKindStorage, Source: "redfish", Slot: disk.Slot, Location: disk.Location, Model: disk.Model, Manufacturer: disk.Manufacturer, SerialNumber: disk.SerialNumber, Firmware: disk.Firmware, Type: disk.Type, Interface: disk.Interface, Present: boolPtr(disk.Present), SizeGB: disk.SizeGB, Status: disk.Status, RemainingEndurancePct: disk.RemainingEndurancePct, }) } for _, nic := range adapters { devices = append(devices, models.HardwareDevice{ ID: "hpe-net-" + sanitizeID(valueOr(nic.SerialNumber, nic.Slot+"-"+nic.Model)), Kind: models.DeviceKindNetwork, Source: "smbios", Slot: nic.Slot, Location: nic.Location, Model: nic.Model, Manufacturer: nic.Vendor, SerialNumber: nic.SerialNumber, PartNumber: nic.PartNumber, Firmware: nic.Firmware, PortCount: nic.PortCount, PortType: nic.PortType, MACAddresses: append([]string(nil), nic.MACAddresses...), Present: boolPtr(nic.Present), Status: nic.Status, }) } for _, psu := range psus { devices = append(devices, models.HardwareDevice{ ID: "hpe-psu-" + sanitizeID(valueOr(psu.SerialNumber, psu.Slot)), Kind: models.DeviceKindPSU, Source: "smbios", Slot: psu.Slot, Model: psu.Model, Manufacturer: psu.Vendor, SerialNumber: psu.SerialNumber, PartNumber: psu.PartNumber, Firmware: psu.Firmware, WattageW: psu.WattageW, InputType: psu.InputType, Present: boolPtr(psu.Present), Status: psu.Status, }) } devices = append(devices, extras...) return dedupeDevices(devices) } func parseEvents(tokens []string) []models.Event { out := make([]models.Event, 0, 16) for i := 0; i+1 < len(tokens); i++ { if !eventTimeRE.MatchString(tokens[i]) { continue } ts, err := time.ParseInLocation("01/02/2006 15:04:05", tokens[i], time.UTC) if err != nil { continue } message := "" for j := i + 1; j < len(tokens) && j <= i+4; j++ { if eventTimeRE.MatchString(tokens[j]) { break } if looksLikeEventMessage(tokens[j]) { message = tokens[j] break } } if message == "" { continue } out = append(out, models.Event{ Timestamp: ts.UTC(), Source: "HPE iLO", EventType: inferEventType(message), Severity: inferSeverity(message), Description: message, RawData: message, }) } return out } func appendFirmware(dst *[]models.FirmwareInfo, seen map[string]bool, item models.FirmwareInfo) { item.DeviceName = strings.TrimSpace(item.DeviceName) item.Version = strings.TrimSpace(item.Version) if item.DeviceName == "" || item.Version == "" { return } key := item.DeviceName + "|" + item.Version + "|" + item.Description if seen[key] { return } seen[key] = true *dst = append(*dst, item) } func dedupeCPUs(items []models.CPU) []models.CPU { seen := make(map[string]bool) out := make([]models.CPU, 0, len(items)) for _, item := range items { key := fmt.Sprintf("%d|%s", item.Socket, item.Model) if seen[key] { continue } seen[key] = true out = append(out, item) } return out } func dedupeMemory(items []models.MemoryDIMM) []models.MemoryDIMM { seen := make(map[string]bool) out := make([]models.MemoryDIMM, 0, len(items)) for _, item := range items { key := valueOr(item.SerialNumber, item.Slot+"|"+item.PartNumber) if seen[key] { continue } seen[key] = true out = append(out, item) } return out } func dedupePSUs(items []models.PSU) []models.PSU { seen := make(map[string]bool) out := make([]models.PSU, 0, len(items)) for _, item := range items { key := valueOr(item.SerialNumber, item.Slot+"|"+item.PartNumber) if seen[key] { continue } seen[key] = true out = append(out, item) } return out } func dedupeNetworkAdapters(items []models.NetworkAdapter) []models.NetworkAdapter { seen := make(map[string]bool) out := make([]models.NetworkAdapter, 0, len(items)) for _, item := range items { key := valueOr(item.SerialNumber, item.Slot+"|"+item.Model) if seen[key] { continue } seen[key] = true out = append(out, item) } return out } func dedupeStorage(items []models.Storage) []models.Storage { seen := make(map[string]bool) out := make([]models.Storage, 0, len(items)) for _, item := range items { key := valueOr(item.SerialNumber, item.Slot+"|"+item.Model) if seen[key] { continue } seen[key] = true out = append(out, item) } return out } func dedupeFirmware(items []models.FirmwareInfo) []models.FirmwareInfo { seen := make(map[string]bool) out := make([]models.FirmwareInfo, 0, len(items)) for _, item := range items { key := item.DeviceName + "|" + item.Version + "|" + item.Description if seen[key] { continue } seen[key] = true out = append(out, item) } return out } func dedupeVolumes(items []models.StorageVolume) []models.StorageVolume { seen := make(map[string]bool) out := make([]models.StorageVolume, 0, len(items)) for _, item := range items { key := valueOr(item.ID, item.Name+"|"+item.Controller) if seen[key] { continue } seen[key] = true out = append(out, item) } return out } func dedupeDevices(items []models.HardwareDevice) []models.HardwareDevice { seen := make(map[string]bool) out := make([]models.HardwareDevice, 0, len(items)) for _, item := range items { key := valueOr(item.SerialNumber, item.Kind+"|"+item.Slot+"|"+item.Model) if seen[key] { continue } seen[key] = true out = append(out, item) } return out } func dedupeEvents(items []models.Event) []models.Event { seen := make(map[string]bool) out := make([]models.Event, 0, len(items)) for _, item := range items { key := item.Timestamp.Format(time.RFC3339) + "|" + item.Description if seen[key] { continue } seen[key] = true out = append(out, item) } return out } func isHPEManufacturer(v string) bool { v = strings.TrimSpace(strings.ToUpper(v)) return v == "HPE" || v == "HP" } func looksLikePSUVendor(v string) bool { v = strings.TrimSpace(strings.ToUpper(v)) switch v { case "HPE", "HP", "DELTA", "LITEON", "LTEON": return true default: return false } } func looksLikeServerModel(v string) bool { v = sanitizeModel(v) if v == "" { return false } lower := strings.ToLower(v) return strings.Contains(lower, "proliant") || strings.Contains(lower, "apollo") || strings.Contains(lower, "synergy") || strings.Contains(lower, "edgeline") } func looksLikeCPUVendor(v string) bool { return strings.Contains(v, "Intel") || strings.Contains(v, "AMD") } func looksLikeCPUModel(v string) bool { return strings.Contains(v, "Xeon") || strings.Contains(v, "EPYC") || strings.Contains(v, "Opteron") } func isUnavailable(v string) bool { v = strings.TrimSpace(strings.ToUpper(v)) return v == "" || v == "NOT AVAILABLE" || v == "UNKNOWN" || v == "N/A" } func cleanUnavailable(v string) string { if isUnavailable(v) { return "" } return strings.TrimSpace(v) } func looksLikePartNumber(v string) bool { return partNumberPattern.MatchString(strings.TrimSpace(v)) } func isLikelySerial(v string) bool { v = strings.TrimSpace(v) if len(v) < 6 || len(v) > 24 || strings.Contains(v, "-") || isUnavailable(v) { return false } for _, r := range v { if (r < '0' || r > '9') && (r < 'A' || r > 'Z') && (r < 'a' || r > 'z') { return false } } return true } func looksLikeLocation(v string) bool { lower := strings.ToLower(strings.TrimSpace(v)) return strings.HasPrefix(lower, "slot ") || strings.HasPrefix(lower, "ocp slot") || strings.HasPrefix(lower, "pci-e slot") || strings.HasPrefix(lower, "pci-e") || strings.HasPrefix(lower, "nvme drive") } func looksLikeVersion(v string) bool { v = strings.TrimSpace(v) if len(v) < 3 || len(v) > 48 || isUnavailable(v) { return false } if strings.HasPrefix(v, "v") && len(v) > 1 && v[1] >= '0' && v[1] <= '9' { return true } digit := false for _, r := range v { if r >= '0' && r <= '9' { digit = true break } } if !digit { return false } return strings.Contains(v, ".") || strings.Contains(strings.ToLower(v), "build") } func looksLikeConcreteModel(v string) bool { if isUnavailable(v) || looksLikeVersion(v) || looksLikePartNumber(v) || isLikelySerial(v) { return false } if looksLikeLocation(v) { return false } return true } func looksLikeNetworkTitle(code, title string, fields []string) bool { lower := strings.ToLower(code + " " + title + " " + strings.Join(fields, " ")) return strings.Contains(lower, "nic.") || strings.Contains(lower, "network controller") || strings.Contains(lower, "ethernet") || strings.Contains(lower, "broadcom") || strings.Contains(lower, "connectx") || strings.Contains(lower, "mellanox") || strings.Contains(lower, "ocp.slot.15") } func isTopLevelFirmwareLabel(v string) bool { switch strings.TrimSpace(v) { case "System ROM", "Redundant System ROM", "Server Platform Services (SPS) Firmware", "Intelligent Platform Abstraction Data": return true default: return false } } func inferVendor(model string) string { lower := strings.ToLower(model) switch { case strings.Contains(lower, "broadcom"): return "Broadcom" case strings.Contains(lower, "mellanox"), strings.Contains(lower, "connectx"), strings.Contains(lower, "mcx"): return "NVIDIA" case strings.Contains(lower, "hpe"): return "HPE" default: return "" } } func mergePSUs(base, extra []models.PSU) []models.PSU { merged := make(map[string]models.PSU) order := make([]string, 0, len(base)+len(extra)) mergeOne := func(item models.PSU) { key := strings.ToLower(strings.TrimSpace(item.Slot)) if key == "" { key = strings.ToLower(strings.TrimSpace(valueOr(item.SerialNumber, item.Model+"|"+item.PartNumber))) } if key == "" { return } current, exists := merged[key] if !exists { merged[key] = item order = append(order, key) return } if current.Slot == "" { current.Slot = item.Slot } current.Present = current.Present || item.Present current.Model = valueOr(current.Model, item.Model) current.Description = valueOr(current.Description, item.Description) current.Vendor = valueOr(current.Vendor, item.Vendor) if current.WattageW == 0 { current.WattageW = item.WattageW } current.SerialNumber = valueOr(current.SerialNumber, item.SerialNumber) current.PartNumber = valueOr(current.PartNumber, item.PartNumber) current.Firmware = valueOr(current.Firmware, item.Firmware) current.Status = valueOr(current.Status, item.Status) current.InputType = valueOr(current.InputType, item.InputType) if current.InputPowerW == 0 { current.InputPowerW = item.InputPowerW } if current.OutputPowerW == 0 { current.OutputPowerW = item.OutputPowerW } if current.InputVoltage == 0 { current.InputVoltage = item.InputVoltage } if current.OutputVoltage == 0 { current.OutputVoltage = item.OutputVoltage } if current.TemperatureC == 0 { current.TemperatureC = item.TemperatureC } current.Details = mergeDetailMaps(current.Details, item.Details) merged[key] = current } for _, item := range base { mergeOne(item) } for _, item := range extra { mergeOne(item) } out := make([]models.PSU, 0, len(order)) for _, key := range order { out = append(out, merged[key]) } return out } func enrichNetworkAdapters(items []models.NetworkAdapter, firmwareByVendor map[string]string) []models.NetworkAdapter { out := make([]models.NetworkAdapter, 0, len(items)) for _, item := range items { if item.Firmware == "" { if fw := firmwareByVendor[strings.ToLower(strings.TrimSpace(item.Vendor))]; fw != "" { item.Firmware = fw } } out = append(out, item) } return out } func parseBCertFirmware(entries []ahsEntry) ([]models.FirmwareInfo, map[string]string) { out := make([]models.FirmwareInfo, 0, 8) nicFirmwareByVendor := make(map[string]string) seen := make(map[string]bool) tagNames := map[string]string{ "SystemProgrammableLogicDevice": "System Programmable Logic Device", "ServerPlatformServicesSPSFirmware": "Server Platform Services (SPS) Firmware", "STMicroGen11TPM": "TPM Firmware", "PrimaryR012U3x16slotsriserx8-x16-x8": "PCIe Riser 1 Programmable Logic Device", "HPEMR408i-oGen11": "HPE MR408i-o Gen11", "UBM3": "8 SFF 24G x1NVMe/SAS UBM3 BC BP", "BCM57191Gb4pBASE-T": "BCM 5719 1Gb 4p BASE-T OCP Adptr", "BCM57191Gb4pBASE-TOCP3": "BCM 5719 1Gb 4p BASE-T OCP Adptr", } for _, entry := range entries { if !strings.EqualFold(entry.Name, "bcert.pkg") { continue } text := string(entry.Content) for _, match := range firmwareLockdownRE.FindAllStringSubmatch(text, -1) { fields := parseXMLFields(match[1]) for tag, value := range fields { name := tagNames[tag] if name == "" { continue } version := normalizeBCertVersion(tag, value) if version == "" { continue } appendFirmware(&out, seen, models.FirmwareInfo{ DeviceName: name, Version: version, }) if strings.Contains(name, "BCM 5719") { nicFirmwareByVendor["broadcom"] = version } } } } return out, nicFirmwareByVendor } func parseXMLFields(block string) map[string]string { out := make(map[string]string) for _, match := range xmlFieldRE.FindAllStringSubmatch(block, -1) { out[match[1]] = strings.TrimSpace(match[2]) } return out } func normalizeBCertVersion(tag, value string) string { value = strings.TrimSpace(value) if value == "" || strings.EqualFold(value, "NA") { return "" } switch tag { case "UBM3": if idx := strings.LastIndex(value, "/"); idx >= 0 && idx+1 < len(value) { return strings.TrimSpace(value[idx+1:]) } case "IntegratedLights-OutVI": if idx := strings.Index(value, " - "); idx > 0 { return strings.TrimSpace(value[:idx]) } case "U54": return value } return value } func normalizeLooseVersion(value string) string { if match := versionFragmentRE.FindString(strings.TrimSpace(value)); match != "" { return match } return strings.TrimSpace(value) } func slotLabelFromCode(code string) string { parts := strings.Split(code, ".") if len(parts) < 3 { return code } switch parts[0] { case "NIC": return "Slot " + parts[2] case "OCP": return "OCP Slot " + parts[2] case "PCI": return "PCI-E Slot " + parts[2] default: return code } } func fabricIDFromPath(path string) string { parts := strings.Split(strings.Trim(path, "/"), "/") for i := 0; i+1 < len(parts); i++ { if parts[i] == "Fabrics" { return parts[i+1] } } return "" } func inferSeverity(message string) models.Severity { lower := strings.ToLower(message) switch { case strings.Contains(lower, " down"), strings.Contains(lower, "warning"), strings.Contains(lower, "fail"), strings.Contains(lower, "error"): return models.SeverityWarning default: return models.SeverityInfo } } func inferEventType(message string) string { lower := strings.ToLower(message) switch { case strings.Contains(lower, "login"): return "Login" case strings.Contains(lower, "logout"): return "Logout" case strings.Contains(lower, "network"): return "Network" case strings.Contains(lower, "license"): return "License" default: return "Event" } } func looksLikeEventMessage(v string) bool { if len(v) < 8 || strings.HasPrefix(v, "src/") || strings.HasPrefix(v, "PciRoot(") { return false } lower := strings.ToLower(v) return strings.Contains(lower, "login") || strings.Contains(lower, "logout") || strings.Contains(lower, "link") || strings.Contains(lower, "license") || strings.Contains(lower, "security state") } func sanitizeModel(v string) string { return strings.TrimSuffix(strings.TrimSpace(v), ":") } func sanitizeID(v string) string { v = strings.ToLower(strings.TrimSpace(v)) v = strings.ReplaceAll(v, " ", "-") v = strings.ReplaceAll(v, "/", "-") v = strings.ReplaceAll(v, ".", "-") return v } func bytesToDecimalGB(size int64) int { if size <= 0 { return 0 } return int((size + 500_000_000) / 1_000_000_000) } func redfishServiceLabel(doc map[string]any, path ...string) string { return strings.TrimSpace(asString(nested(doc, path...))) } func redfishStatus(v any) string { status, _ := v.(map[string]any) state := strings.TrimSpace(asString(status["State"])) health := strings.TrimSpace(asString(status["Health"])) if strings.EqualFold(state, "Absent") { return "absent" } if strings.EqualFold(health, "Warning") || strings.EqualFold(health, "Critical") { return strings.ToLower(health) } if state != "" { return strings.ToLower(state) } if health != "" { return strings.ToLower(health) } return "" } func redfishID(path string) string { parts := strings.Split(strings.Trim(path, "/"), "/") if len(parts) == 0 { return "unknown" } return sanitizeID(parts[len(parts)-1]) } func nested(v any, path ...string) any { cur := v for _, key := range path { m, ok := cur.(map[string]any) if !ok { return nil } cur = m[key] } return cur } func asString(v any) string { switch value := v.(type) { case string: return value case fmt.Stringer: return value.String() default: return "" } } func asInt(doc map[string]any, path ...string) int { return int(asInt64(nested(doc, path...))) } func asInt64(v any) int64 { switch value := v.(type) { case float64: return int64(value) case float32: return int64(value) case int: return int64(value) case int64: return value case json.Number: n, _ := value.Int64() return n default: return 0 } } func asFloat64(v any) float64 { switch t := v.(type) { case float64: return t case float32: return float64(t) case int: return float64(t) case int64: return float64(t) case json.Number: f, _ := t.Float64() return f default: return 0 } } func asOptionalInt(v any) *int { switch value := v.(type) { case float64: out := int(value) return &out case int: out := value return &out default: return nil } } func asBool(v any) bool { b, ok := v.(bool) return ok && b } func valueOr(v, fallback string) string { if strings.TrimSpace(v) != "" { return strings.TrimSpace(v) } return strings.TrimSpace(fallback) } func stringSlice(v any) []string { items, ok := v.([]any) if !ok { return nil } out := make([]string, 0, len(items)) for _, item := range items { value := strings.TrimSpace(asString(item)) if value == "" { continue } out = append(out, value) } return out } func firstDurableName(doc map[string]any) string { items, ok := doc["Identifiers"].([]any) if !ok { return "" } for _, item := range items { entry, ok := item.(map[string]any) if !ok { continue } if value := strings.TrimSpace(asString(entry["DurableName"])); value != "" { return value } } return "" } func mergeDetailMaps(base, extra map[string]any) map[string]any { if len(extra) == 0 { return base } if base == nil { base = make(map[string]any, len(extra)) } for key, value := range extra { if _, exists := base[key]; !exists || isZeroValue(base[key]) { base[key] = value } } return base } func isZeroValue(v any) bool { switch t := v.(type) { case nil: return true case string: return strings.TrimSpace(t) == "" case int: return t == 0 case int64: return t == 0 case float64: return t == 0 case bool: return !t default: return false } } func boolPtr(v bool) *bool { out := v return &out }