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.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}$`) ) 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)) 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+2 < len(tokens); i++ { match := psuSlotRE.FindStringSubmatch(tokens[i]) if len(match) != 2 { continue } slot := "PSU " + match[1] serial := tokens[i+1] partNumber := tokens[i+2] if isUnavailable(serial) && isUnavailable(partNumber) { continue } psu := models.PSU{ Slot: slot, Present: true, Model: valueOr(partNumber, "Power Supply"), Vendor: "HPE", SerialNumber: cleanUnavailable(serial), PartNumber: cleanUnavailable(partNumber), Status: "ok", } out = append(out, psu) } 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, 4) firmware := make([]models.FirmwareInfo, 0, 4) for _, path := range paths { doc := docs[path] docType := asString(doc["@odata.type"]) switch { 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, }, } 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, }, } 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 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 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 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 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 valueOr(v, fallback string) string { if strings.TrimSpace(v) != "" { return strings.TrimSpace(v) } return strings.TrimSpace(fallback) } func boolPtr(v bool) *bool { out := v return &out }