Files
2026-03-30 15:04:17 +03:00

1707 lines
46 KiB
Go

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)<PowerSupplySlot id="(\d+)">(.*?)</PowerSupplySlot>`)
firmwareLockdownRE = regexp.MustCompile(`(?s)<FirmwareLockdown>(.*?)</FirmwareLockdown>`)
xmlFieldRE = regexp.MustCompile(`(?s)<([A-Za-z0-9_-]+)>([^<]*)</[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
}