Files
logpile/internal/parser/vendors/h3c/parser.go
2026-03-15 23:27:32 +03:00

3536 lines
96 KiB
Go

// Package h3c provides parser for H3C SDS diagnostic archives.
package h3c
import (
"bufio"
"encoding/csv"
"encoding/json"
"encoding/xml"
"fmt"
"io"
"regexp"
"sort"
"strconv"
"strings"
"time"
"git.mchus.pro/mchus/logpile/internal/models"
"git.mchus.pro/mchus/logpile/internal/parser"
"git.mchus.pro/mchus/logpile/internal/parser/vendors/pciids"
)
const (
parserVersionG5 = "2.1"
parserVersionG6 = "2.1"
)
func init() {
parser.Register(&G5Parser{})
parser.Register(&G6Parser{})
}
// G5Parser implements VendorParser for H3C G5 SDS archives.
type G5Parser struct{}
func (p *G5Parser) Name() string { return "H3C SDS Parser G5" }
func (p *G5Parser) Vendor() string { return "h3c_g5" }
func (p *G5Parser) Version() string { return parserVersionG5 }
func (p *G5Parser) Detect(files []parser.ExtractedFile) int {
return detectH3CG5(files)
}
func (p *G5Parser) Parse(files []parser.ExtractedFile) (*models.AnalysisResult, error) {
return parseH3CG5(files), nil
}
// G6Parser implements VendorParser for H3C G6 SDS archives.
type G6Parser struct{}
func (p *G6Parser) Name() string { return "H3C SDS Parser G6" }
func (p *G6Parser) Vendor() string { return "h3c_g6" }
func (p *G6Parser) Version() string { return parserVersionG6 }
func (p *G6Parser) Detect(files []parser.ExtractedFile) int {
return detectH3CG6(files)
}
func (p *G6Parser) Parse(files []parser.ExtractedFile) (*models.AnalysisResult, error) {
return parseH3CG6(files), nil
}
func detectH3CG5(files []parser.ExtractedFile) int {
confidence := 0
for _, f := range files {
path := strings.ToLower(f.Path)
switch {
case path == "bmc/pack.info":
confidence += 6
case strings.HasSuffix(path, "/fruinfo.ini") || strings.HasSuffix(path, "fruinfo.ini"):
confidence += 10
case strings.HasSuffix(path, "/hardware_info.ini") || strings.HasSuffix(path, "hardware_info.ini"):
confidence += 35
case strings.HasSuffix(path, "/hardware.info") || strings.HasSuffix(path, "hardware.info"):
confidence += 24
case strings.HasSuffix(path, "/firmware_version.ini") || strings.HasSuffix(path, "firmware_version.ini"):
confidence += 20
case strings.HasSuffix(path, "/test.csv") || strings.HasSuffix(path, "/test1.csv"):
confidence += 20
case strings.HasSuffix(path, "/raid.json") || strings.HasSuffix(path, "raid.json"):
confidence += 8
}
if strings.Contains(path, "fruinfo.ini") && containsH3CMarkers(f.Content) {
confidence += 10
}
if confidence >= 100 {
return 100
}
}
return minInt(confidence, 100)
}
func detectH3CG6(files []parser.ExtractedFile) int {
confidence := 0
for _, f := range files {
path := strings.ToLower(f.Path)
switch {
case path == "bmc/pack.info":
confidence += 10
case strings.HasSuffix(path, "/fruinfo.ini") || strings.HasSuffix(path, "fruinfo.ini"):
confidence += 12
case strings.HasSuffix(path, "/board_info.ini") || strings.HasSuffix(path, "board_info.ini"):
confidence += 14
case strings.HasSuffix(path, "/firmware_version.json") || strings.HasSuffix(path, "firmware_version.json"):
confidence += 24
case strings.HasSuffix(path, "/cpudetailinfo.xml") || strings.HasSuffix(path, "cpudetailinfo.xml"):
confidence += 22
case strings.HasSuffix(path, "/memorydetailinfo.xml") || strings.HasSuffix(path, "memorydetailinfo.xml"):
confidence += 22
case strings.HasSuffix(path, "/sel.json") || strings.HasSuffix(path, "sel.json"):
confidence += 18
case strings.HasSuffix(path, "/sensor_info.ini") || strings.HasSuffix(path, "sensor_info.ini"):
confidence += 8
}
if (strings.Contains(path, "board_info.ini") || strings.Contains(path, "fruinfo.ini")) && containsH3CMarkers(f.Content) {
confidence += 10
}
if confidence >= 100 {
return 100
}
}
return minInt(confidence, 100)
}
func containsH3CMarkers(content []byte) bool {
s := strings.ToLower(string(content))
return strings.Contains(s, "h3c") || strings.Contains(s, "new h3c technologies")
}
func newAnalysisResult() *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),
CPUs: make([]models.CPU, 0),
Memory: make([]models.MemoryDIMM, 0),
Storage: make([]models.Storage, 0),
Volumes: make([]models.StorageVolume, 0),
Devices: make([]models.HardwareDevice, 0),
NetworkCards: make([]models.NIC, 0),
NetworkAdapters: make([]models.NetworkAdapter, 0),
PowerSupply: make([]models.PSU, 0),
},
}
}
func parseH3CG5(files []parser.ExtractedFile) *models.AnalysisResult {
result := newAnalysisResult()
firmwareSeen := make(map[string]struct{})
if f := parser.FindFileByName(files, "FRUInfo.ini"); f != nil {
parseFRUInfoINI(f.Content, result)
}
if f := parser.FindFileByName(files, "board_info.ini"); f != nil {
parseBoardInfoINI(f.Content, result, firmwareSeen)
}
if f := parser.FindFileByName(files, "board_cfg.ini"); f != nil {
parseBoardCfgINI(f.Content, result)
}
if f := parser.FindFileByName(files, "firmware_version.ini"); f != nil {
parseFirmwareVersionINI(f.Content, result, firmwareSeen)
}
if f := parser.FindFileByName(files, "hardware_info.ini"); f != nil {
parseHardwareInfoINI(f.Content, result)
}
if f := parser.FindFileByName(files, "hardware.info"); f != nil {
appendUniqueStorages(&result.Hardware.Storage, parseHardwareInfoStorageINI(f.Content))
}
if f := parser.FindFileByName(files, "storage_disk.ini"); f != nil {
appendUniqueStorages(&result.Hardware.Storage, parseStorageINI(f.Content))
}
if f := parser.FindFileByName(files, "raid.json"); f != nil {
result.Hardware.Volumes = append(result.Hardware.Volumes, parseRAIDJSONVolumes(f.Content)...)
}
for _, f := range files {
path := strings.ToLower(f.Path)
if strings.Contains(path, "storage_raid-") && strings.HasSuffix(path, ".txt") {
storages, volumes := parseRAIDDetailTXT(f.Content)
appendUniqueStorages(&result.Hardware.Storage, storages)
result.Hardware.Volumes = append(result.Hardware.Volumes, volumes...)
}
}
if f := parser.FindFileByName(files, "NVMe_info.txt"); f != nil {
appendUniqueStorages(&result.Hardware.Storage, parseNVMeInfo(f.Content))
}
if f := parser.FindFileByName(files, "Raid_BP_Conf_Info.ini"); f != nil {
result.Hardware.Devices = append(result.Hardware.Devices, parseRaidBPConfDevices(f.Content)...)
}
if f := parser.FindFileByName(files, "psu_cfg.ini"); f != nil {
appendUniquePSUs(&result.Hardware.PowerSupply, parsePSUCfgINI(f.Content))
}
if f := parser.FindFileByName(files, "net_cfg.ini"); f != nil {
adapters := parseNetCfgNetworkAdapters(f.Content)
if len(result.Hardware.NetworkAdapters) == 0 {
appendUniqueNetworkAdapters(&result.Hardware.NetworkAdapters, adapters)
appendUniqueNICs(&result.Hardware.NetworkCards, networkCardsFromAdapters(adapters))
}
}
enrichStorageWithSmartdata(&result.Hardware.Storage, files)
if f := parser.FindFileByName(files, "PCIe_arguments_table.xml"); f != nil {
enrichStorageFromPCIeArguments(&result.Hardware.Storage, f.Content)
}
if f := parser.FindFileByName(files, "sensor_info.ini"); f != nil {
result.Sensors = parseSensorInfoINI(f.Content)
}
result.Events = parseSELCSVFiles(files)
if len(result.Events) == 0 {
if f := parser.FindFileByName(files, "Sel.json"); f != nil {
result.Events = parseSELJSON(f.Content)
}
}
if len(result.Events) == 0 {
if f := parser.FindFileByName(files, "sel_list.txt"); f != nil {
result.Events = parseSELListTXT(f.Content)
}
}
result.Hardware.Storage = dedupeStorage(result.Hardware.Storage)
result.Hardware.Volumes = dedupeVolumes(result.Hardware.Volumes)
parser.ApplyManufacturedYearWeekFromFRU(result.FRU, result.Hardware)
return result
}
func parseH3CG6(files []parser.ExtractedFile) *models.AnalysisResult {
result := newAnalysisResult()
firmwareSeen := make(map[string]struct{})
if f := parser.FindFileByName(files, "FRUInfo.ini"); f != nil {
parseFRUInfoINI(f.Content, result)
}
if f := parser.FindFileByName(files, "board_info.ini"); f != nil {
parseBoardInfoINI(f.Content, result, firmwareSeen)
}
if f := parser.FindFileByName(files, "firmware_version.json"); f != nil {
parseFirmwareJSON(f.Content, result, firmwareSeen)
}
if f := parser.FindFileByName(files, "CPUDetailInfo.xml"); f != nil {
parseCPUXML(f.Content, result)
}
if f := parser.FindFileByName(files, "MemoryDetailInfo.xml"); f != nil {
parseMemoryXML(f.Content, result)
}
if f := parser.FindFileByName(files, "hardware_info.ini"); f != nil {
parseHardwareInfoINI(f.Content, result)
}
if f := parser.FindFileByName(files, "storage_disk.ini"); f != nil {
appendUniqueStorages(&result.Hardware.Storage, parseStorageINI(f.Content))
}
if f := parser.FindFileByName(files, "raid.json"); f != nil {
result.Hardware.Volumes = append(result.Hardware.Volumes, parseRAIDJSONVolumes(f.Content)...)
}
for _, f := range files {
path := strings.ToLower(f.Path)
if strings.Contains(path, "storage_raid-") && strings.HasSuffix(path, ".txt") {
storages, volumes := parseRAIDDetailTXT(f.Content)
appendUniqueStorages(&result.Hardware.Storage, storages)
result.Hardware.Volumes = append(result.Hardware.Volumes, volumes...)
}
}
if f := parser.FindFileByName(files, "NVMe_info.txt"); f != nil {
appendUniqueStorages(&result.Hardware.Storage, parseNVMeInfo(f.Content))
}
if f := parser.FindFileByName(files, "psu_cfg.ini"); f != nil {
appendUniquePSUs(&result.Hardware.PowerSupply, parsePSUCfgINI(f.Content))
}
if f := parser.FindFileByName(files, "net_cfg.ini"); f != nil {
adapters := parseNetCfgNetworkAdapters(f.Content)
if len(result.Hardware.NetworkAdapters) == 0 {
appendUniqueNetworkAdapters(&result.Hardware.NetworkAdapters, adapters)
appendUniqueNICs(&result.Hardware.NetworkCards, networkCardsFromAdapters(adapters))
}
}
if f := parser.FindFileByName(files, "PCIe_arguments_table.xml"); f != nil {
enrichStorageFromPCIeArguments(&result.Hardware.Storage, f.Content)
}
if f := parser.FindFileByName(files, "sensor_info.ini"); f != nil {
result.Sensors = parseSensorInfoINI(f.Content)
}
if f := parser.FindFileByName(files, "Sel.json"); f != nil {
result.Events = parseSELJSON(f.Content)
}
if len(result.Events) == 0 {
if f := parser.FindFileByName(files, "sel_list.txt"); f != nil {
result.Events = parseSELListTXT(f.Content)
}
}
result.Hardware.Storage = dedupeStorage(result.Hardware.Storage)
result.Hardware.Volumes = dedupeVolumes(result.Hardware.Volumes)
parser.ApplyManufacturedYearWeekFromFRU(result.FRU, result.Hardware)
return result
}
func parseFRUInfoINI(content []byte, result *models.AnalysisResult) {
sections := parseLooseINI(string(content))
names := sortedSectionNames(sections)
for _, sectionName := range names {
section := sections[sectionName]
fru := models.FRUInfo{
DeviceID: sectionName,
Description: sectionName,
ChassisType: getSectionValue(section, "Chassis Type"),
Manufacturer: getSectionValue(section, "Board Manufacturer", "Product Manufacturer"),
ProductName: getSectionValue(section, "Product Product Name", "Board Product Name"),
SerialNumber: getSectionValue(section, "Product Serial Number", "Board Top Serial Number", "Board Serial Number", "Chassis Serial Number"),
PartNumber: getSectionValue(section, "Product Part Number", "Board Part Number", "Chassis Part Number"),
Version: getSectionValue(section, "Product Version"),
MfgDate: getSectionValue(section, "Mfg.Date"),
AssetTag: getSectionValue(section, "Product Asset Tag"),
}
if isEmptyFRU(fru) {
continue
}
result.FRU = append(result.FRU, fru)
if strings.EqualFold(sectionName, "baseboard") {
applyBoardFromFRU(&result.Hardware.BoardInfo, fru)
}
}
}
func parseBoardInfoINI(content []byte, result *models.AnalysisResult, firmwareSeen map[string]struct{}) {
sections := parseLooseINI(string(content))
names := sortedSectionNames(sections)
for _, sectionName := range names {
section := sections[sectionName]
if strings.EqualFold(sectionName, "System board") {
board := &result.Hardware.BoardInfo
setIfEmpty(&board.Manufacturer, getSectionValue(section, "BoardMfr"))
setIfEmpty(&board.ProductName, getSectionValue(section, "BoardProductName"))
setIfEmpty(&board.SerialNumber, getSectionValue(section, "BoardSerialNum"))
setIfEmpty(&board.PartNumber, getSectionValue(section, "BoardPartNum"))
setIfEmpty(&board.Version, getSectionValue(section, "PCB Version"))
}
for key, value := range section {
keyTrim := strings.TrimSpace(key)
valueTrim := strings.TrimSpace(value)
if valueTrim == "" || strings.EqualFold(valueTrim, "N/A") {
continue
}
if !strings.Contains(strings.ToLower(keyTrim), "version") {
continue
}
deviceName := strings.TrimSpace(fmt.Sprintf("%s %s", sectionName, keyTrim))
appendFirmwareUnique(result, firmwareSeen, models.FirmwareInfo{
DeviceName: deviceName,
Version: valueTrim,
})
}
}
}
func parseFirmwareVersionINI(content []byte, result *models.AnalysisResult, firmwareSeen map[string]struct{}) {
sections := parseLooseINI(string(content))
names := sortedSectionNames(sections)
for _, sectionName := range names {
section := sections[sectionName]
if strings.EqualFold(sectionName, "System board") {
setIfEmpty(&result.Hardware.BoardInfo.Version, getSectionValue(section, "PCB Version"))
}
for key, value := range section {
keyTrim := strings.TrimSpace(key)
valueTrim := strings.TrimSpace(value)
if valueTrim == "" || strings.EqualFold(valueTrim, "N/A") {
continue
}
normKey := normalizeKey(keyTrim)
if !strings.Contains(normKey, "version") {
continue
}
appendFirmwareUnique(result, firmwareSeen, models.FirmwareInfo{
DeviceName: strings.TrimSpace(fmt.Sprintf("%s %s", sectionName, keyTrim)),
Version: valueTrim,
})
}
}
}
func parseHardwareInfoINI(content []byte, result *models.AnalysisResult) {
sections := parseLooseINI(string(content))
names := sortedSectionNames(sections)
for _, sectionName := range names {
section := sections[sectionName]
normSection := normalizeKey(sectionName)
switch {
case strings.HasPrefix(normSection, "processorsprocessor"):
cpu := parseCPUFromHardwareInfoSection(sectionName, section)
if cpu.Model == "" {
continue
}
if idx := findCPUIndex(result.Hardware.CPUs, cpu); idx >= 0 {
mergeCPU(&result.Hardware.CPUs[idx], cpu)
continue
}
result.Hardware.CPUs = append(result.Hardware.CPUs, cpu)
case strings.HasPrefix(normSection, "memorydetailsdimmindex"):
dimm := parseDIMMFromHardwareInfoSection(sectionName, section)
if dimm.Slot == "" && dimm.SerialNumber == "" {
continue
}
if idx := findMemoryIndex(result.Hardware.Memory, dimm); idx >= 0 {
mergeMemoryDIMM(&result.Hardware.Memory[idx], dimm)
continue
}
result.Hardware.Memory = append(result.Hardware.Memory, dimm)
}
}
adapters := parseHardwareInfoNetworkAdapters(content)
appendUniqueNetworkAdapters(&result.Hardware.NetworkAdapters, adapters)
appendUniqueNICs(&result.Hardware.NetworkCards, networkCardsFromAdapters(result.Hardware.NetworkAdapters))
}
func parseHardwareInfoNetworkAdapters(content []byte) []models.NetworkAdapter {
blocks := parseKeyValueBlocks(string(content))
out := make([]models.NetworkAdapter, 0)
for _, block := range blocks {
deviceType := strings.ToLower(strings.TrimSpace(getSectionValue(block, "Device Type")))
hasNetworkHints := deviceType == "nic" ||
getSectionValue(block, "Network Port") != "" ||
getSectionValue(block, "MAC Address") != "" ||
getSectionValue(block, "Vendor ID") != "" ||
getSectionValue(block, "Device ID") != ""
if !hasNetworkHints {
continue
}
slot := normalizePCIeSlotLabel(getSectionValue(block, "Location", "Slot", "PCIe Slot"))
if slot == "" {
slot = firstNonEmpty(getSectionValue(block, "Network Port"), "NIC")
}
mac := normalizeMACAddress(getSectionValue(block, "MAC Address"))
productName := normalizeUnknownString(getSectionValue(block, "Product Name", "Model"))
vendorID := parseMaybeInt(getSectionValue(block, "Vendor ID"))
deviceID := parseMaybeInt(getSectionValue(block, "Device ID"))
vendor := normalizeUnknownString(getSectionValue(block, "Vendor", "Manufacturer"))
if vendor == "" && vendorID > 0 {
vendor = strings.TrimSpace(pciids.VendorName(vendorID))
}
networkPort := normalizeUnknownString(getSectionValue(block, "Network Port"))
speed := normalizeUnknownString(firstNonEmpty(getSectionValue(block, "Current Speed"), getSectionValue(block, "Speed")))
bandwidth := normalizeUnknownString(getSectionValue(block, "Current Bandwidth", "Link Width"))
status := normalizeComponentStatus(firstNonEmpty(getSectionValue(block, "Network Status"), getSectionValue(block, "Status")))
if status == "" {
status = "ok"
}
descParts := make([]string, 0, 3)
if networkPort != "" {
descParts = append(descParts, networkPort)
}
if speed != "" {
descParts = append(descParts, "speed "+speed)
}
if bandwidth != "" {
descParts = append(descParts, "link "+bandwidth)
}
adapter := models.NetworkAdapter{
Slot: slot,
Location: slot,
Present: status != "absent",
Model: firstNonEmpty(productName, "Ethernet Adapter"),
Description: strings.Join(descParts, "; "),
Vendor: vendor,
VendorID: vendorID,
DeviceID: deviceID,
SerialNumber: normalizeUnknownString(getSectionValue(block, "Serial Number", "SerialNumber", "SN")),
PartNumber: normalizeUnknownString(getSectionValue(block, "Part Number", "PartNumber", "PN")),
Firmware: normalizeUnknownString(getSectionValue(block, "Firmware Version", "Firmware")),
PortCount: inferPortCountFromNetworkFields(productName, networkPort),
Status: status,
}
if mac != "" {
adapter.MACAddresses = []string{mac}
if adapter.PortCount == 0 {
adapter.PortCount = 1
}
}
if isNetworkAdapterEmpty(adapter) {
continue
}
out = append(out, adapter)
}
return out
}
func parseCPUFromHardwareInfoSection(sectionName string, section map[string]string) models.CPU {
socket := parseSectionIndex(sectionName, `(?i)processor\s+(\d+)`)
statusRaw := getSectionValue(section, "Status")
return models.CPU{
Socket: socket,
Model: getSectionValue(section, "Model"),
Cores: parseMaybeIntLoose(getSectionValue(section, "Cores")),
Threads: parseMaybeIntLoose(getSectionValue(section, "Threads")),
FrequencyMHz: parseMaybeIntLoose(getSectionValue(section, "Frequency", "Processor Speed")),
L1CacheKB: parseMaybeIntLoose(getSectionValue(section, "L1 Cache")),
L2CacheKB: parseMaybeIntLoose(getSectionValue(section, "L2 Cache")),
L3CacheKB: parseMaybeIntLoose(getSectionValue(section, "L3 Cache")),
PPIN: getSectionValue(section, "CPU PPIN", "PPIN"),
SerialNumber: getSectionValue(section, "Processor ID", "Serial Number"),
Status: normalizeComponentStatus(statusRaw),
}
}
func parseDIMMFromHardwareInfoSection(sectionName string, section map[string]string) models.MemoryDIMM {
statusRaw := getSectionValue(section, "Status")
status := normalizeComponentStatus(statusRaw)
present := status != "absent"
slot := firstNonEmpty(
getSectionValue(section, "Socket ID", "Slot", "Name"),
sectionName,
)
location := strings.TrimSpace(getSectionValue(section, "Location"))
channel := strings.TrimSpace(getSectionValue(section, "Channel"))
if location != "" && channel != "" {
location = location + " CH" + channel
}
return models.MemoryDIMM{
Slot: slot,
Location: location,
Present: present,
SizeMB: parseMaybeIntLoose(getSectionValue(section, "Size", "DIMM Size")),
Type: firstNonEmpty(getSectionValue(section, "Type"), getSectionValue(section, "Technology"), "DIMM"),
Technology: getSectionValue(section, "Technology"),
MaxSpeedMHz: parseMaybeIntLoose(getSectionValue(section, "Maximum Frequency", "Max Frequency")),
CurrentSpeedMHz: parseMaybeIntLoose(getSectionValue(section, "Current Frequency", "Operating Frequency")),
Manufacturer: getSectionValue(section, "Manufacture", "Manufacturer"),
SerialNumber: getSectionValue(section, "Serial Number", "SerialNumber"),
PartNumber: getSectionValue(section, "Part Number"),
Status: status,
Ranks: parseMaybeIntLoose(getSectionValue(section, "Ranks")),
}
}
func parseHardwareInfoStorageINI(content []byte) []models.Storage {
sections := parseLooseINI(string(content))
names := sortedSectionNames(sections)
out := make([]models.Storage, 0)
for _, sectionName := range names {
section := sections[sectionName]
normSection := normalizeKey(sectionName)
if !strings.HasPrefix(normSection, "disk") && !strings.HasPrefix(normSection, "nvme") {
continue
}
slotNum := getSectionValue(section, "SlotNum")
frontRear := getSectionValue(section, "FrontOrRear")
slot := firstNonEmpty(
getSectionValue(section, "NvmePhySlot", "DiskSlotDesc", "Position", "Slot"),
formatSlot(frontRear, slotNum),
slotNum,
sectionName,
)
presentRaw := getSectionValue(section, "Present")
present := true
if strings.TrimSpace(presentRaw) != "" {
present = isTruthy(presentRaw)
}
status := normalizeStorageStatus(getSectionValue(section, "Status"), present)
if status == "" {
status = normalizeStorageStatus(presentRaw, present)
}
storageType := "disk"
if strings.HasPrefix(normSection, "nvme") || strings.Contains(strings.ToLower(slot), "nvme") {
storageType = "nvme"
}
location := getSectionValue(section, "FrontOrRear", "Location")
if location == "" {
slotLower := strings.ToLower(slot)
switch {
case strings.Contains(slotLower, "front"):
location = "Front"
case strings.Contains(slotLower, "rear"):
location = "Rear"
}
}
item := models.Storage{
Slot: slot,
Type: storageType,
SerialNumber: getSectionValue(section, "SerialNumber"),
Present: present,
Location: location,
Status: status,
}
if isStorageEmpty(item) {
continue
}
out = append(out, item)
}
return dedupeStorage(out)
}
func parseSmartdataStorages(files []parser.ExtractedFile) []models.Storage {
out := make([]models.Storage, 0)
for _, f := range files {
path := strings.ToLower(strings.TrimSpace(f.Path))
if !strings.HasPrefix(path, "static/smartdata/") || !strings.HasSuffix(path, ".txt") {
continue
}
model, serial := parseSmartdataDevice(f.Content)
if serial == "" {
continue
}
slot := smartdataSlotFromPath(f.Path)
st := models.Storage{
Slot: slot,
Type: inferStorageTypeFromValues(slot, "", "", model),
Model: model,
SerialNumber: serial,
Present: true,
Status: "ok",
}
if st.Type == "" {
st.Type = "disk"
}
out = append(out, st)
}
return dedupeStorage(out)
}
func enrichStorageWithSmartdata(storages *[]models.Storage, files []parser.ExtractedFile) {
for _, add := range parseSmartdataStorages(files) {
idx := findStorageIndex(*storages, add)
if idx < 0 {
continue
}
mergeStorage(&(*storages)[idx], add)
}
}
type pcieStorageProfile struct {
Model string
Manufacturer string
IsNVMe bool
}
func enrichStorageFromPCIeArguments(storages *[]models.Storage, content []byte) {
profiles := parseStorageProfilesFromPCIeArgumentsXML(content)
if len(profiles) == 0 {
return
}
nvmeVendor, nvmeModel := chooseNVMeProfileFallback(*storages, profiles)
nonNVMeVendor, nonNVMeModel := selectUniqueVendorModel(filterPCIeStorageProfilesByNVMe(profiles, false))
for i := range *storages {
s := &(*storages)[i]
isNVMeStorage := strings.EqualFold(strings.TrimSpace(s.Type), "nvme")
vendor, model := nonNVMeVendor, nonNVMeModel
if isNVMeStorage {
vendor, model = nvmeVendor, nvmeModel
}
if vendor == "" && model == "" {
continue
}
if strings.TrimSpace(s.Manufacturer) == "" && vendor != "" {
s.Manufacturer = vendor
}
if strings.TrimSpace(s.Model) == "" {
if model != "" {
s.Model = model
} else if vendor != "" && isNVMeStorage {
s.Model = "NVMe SSD"
}
}
}
}
func chooseNVMeProfileFallback(storages []models.Storage, profiles []pcieStorageProfile) (vendor, model string) {
nvmeProfiles := filterPCIeStorageProfilesByNVMe(profiles, true)
if len(nvmeProfiles) == 0 {
return "", ""
}
vendor, model = selectUniqueVendorModel(nvmeProfiles)
if vendor != "" || model != "" {
return vendor, model
}
frontBayNVMe := false
for _, s := range storages {
if strings.ToLower(strings.TrimSpace(s.Type)) != "nvme" {
continue
}
slotLower := strings.ToLower(strings.TrimSpace(s.Slot))
if strings.Contains(slotLower, "front") {
frontBayNVMe = true
break
}
if n := parseMaybeInt(slotLower); n >= 100 {
frontBayNVMe = true
break
}
}
if !frontBayNVMe {
return "", ""
}
sffProfiles := make([]pcieStorageProfile, 0)
for _, p := range nvmeProfiles {
nameLower := strings.ToLower(strings.TrimSpace(p.Model))
if strings.Contains(nameLower, "sff") {
sffProfiles = append(sffProfiles, p)
}
}
if len(sffProfiles) == 0 {
return "", ""
}
return selectUniqueVendorModel(sffProfiles)
}
func selectUniqueVendorModel(profiles []pcieStorageProfile) (vendor, model string) {
vendorSet := make(map[string]struct{})
modelSet := make(map[string]struct{})
for _, p := range profiles {
if strings.TrimSpace(p.Manufacturer) != "" {
vendorSet[p.Manufacturer] = struct{}{}
}
if strings.TrimSpace(p.Model) != "" && !strings.EqualFold(strings.TrimSpace(p.Model), "N/A") {
modelSet[p.Model] = struct{}{}
}
}
if len(vendorSet) == 1 {
for v := range vendorSet {
vendor = v
}
}
if len(modelSet) == 1 {
for m := range modelSet {
model = m
}
}
return vendor, model
}
func filterPCIeStorageProfilesByNVMe(profiles []pcieStorageProfile, isNVMe bool) []pcieStorageProfile {
out := make([]pcieStorageProfile, 0, len(profiles))
for _, p := range profiles {
if p.IsNVMe == isNVMe {
out = append(out, p)
}
}
return out
}
func parseStorageProfilesFromPCIeArgumentsXML(content []byte) []pcieStorageProfile {
root, ok := parseXMLRoot(content)
if !ok {
return nil
}
out := make([]pcieStorageProfile, 0)
seen := make(map[string]struct{})
for _, node := range root.Nodes {
name := strings.ToUpper(strings.TrimSpace(node.XMLName.Local))
if !strings.HasPrefix(name, "PCIE") {
continue
}
base := findChildXMLNode(node, "base_args")
typeGet := findChildXMLNode(node, "type_get_args")
if base == nil || typeGet == nil {
continue
}
baseFields := xmlNodeFields(*base)
itemName := strings.TrimSpace(baseFields["name"])
itemType := strings.ToLower(strings.TrimSpace(baseFields["type"]))
if itemName == "" {
continue
}
lowerName := strings.ToLower(itemName)
if strings.HasPrefix(strings.ToUpper(itemName), "EX-") {
continue
}
isNVMe := strings.Contains(lowerName, "nvme") || strings.Contains(itemType, "nvme")
if !isNVMe &&
!containsAny(lowerName, "ssd", "hdd", "disk", "sas", "sata") &&
!containsAny(itemType, "ssd", "hdd", "disk", "sas", "sata") {
continue
}
bios := findChildXMLNode(*typeGet, "bios_args")
if bios == nil {
continue
}
vendorRaw := strings.TrimSpace(xmlNodeFields(*bios)["vendor_id"])
vendorID := parseMaybeInt(vendorRaw)
if vendorID == 0 || strings.EqualFold(vendorRaw, "0xFFFF") {
continue
}
vendorName := strings.TrimSpace(pciids.VendorName(vendorID))
if vendorName == "" {
vendorName = fmt.Sprintf("0x%X", vendorID)
}
profile := pcieStorageProfile{
Model: itemName,
Manufacturer: vendorName,
IsNVMe: isNVMe,
}
key := strings.ToLower(profile.Model + "|" + profile.Manufacturer + "|" + strconv.FormatBool(profile.IsNVMe))
if _, exists := seen[key]; exists {
continue
}
seen[key] = struct{}{}
out = append(out, profile)
}
return out
}
func findChildXMLNode(parent xmlNode, childName string) *xmlNode {
target := strings.ToLower(strings.TrimSpace(childName))
for i := range parent.Nodes {
if strings.ToLower(strings.TrimSpace(parent.Nodes[i].XMLName.Local)) == target {
return &parent.Nodes[i]
}
}
return nil
}
var (
netCfgHeaderRE = regexp.MustCompile(`^([^\s]+)\s+Link\s+encap:`)
netCfgHWAddrRE = regexp.MustCompile(`(?i)\bHWaddr\s+([0-9a-f]{2}(?::[0-9a-f]{2}){5})`)
netCfgInet4OldRE = regexp.MustCompile(`\binet addr:([0-9]+\.[0-9]+\.[0-9]+\.[0-9]+)`)
netCfgInet4NewRE = regexp.MustCompile(`\binet\s+([0-9]+\.[0-9]+\.[0-9]+\.[0-9]+)(?:/[0-9]+)?`)
netCfgInet6OldRE = regexp.MustCompile(`\binet6 addr:\s*([0-9a-f:]+(?:/[0-9]+)?)`)
netCfgInet6NewRE = regexp.MustCompile(`\binet6\s+([0-9a-f:]+(?:/[0-9]+)?)`)
pcieSlotDigitsRE = regexp.MustCompile(`(?i)(\d+)`)
portCountPattRE = regexp.MustCompile(`(?i)(\d+)\s*p\b`)
portCountMulRE = regexp.MustCompile(`(?i)(\d+)\s*\*\s*\d+\s*g`)
portSuffixRE = regexp.MustCompile(`(?i)port\s+(\d+)`)
)
type netCfgAdapterState struct {
Slot string
Names []string
MACs []string
IPv4 []string
IPv6 []string
Up bool
}
func parseNetCfgNetworkAdapters(content []byte) []models.NetworkAdapter {
lines := strings.Split(strings.ReplaceAll(string(content), "\r\n", "\n"), "\n")
byKey := make(map[string]*netCfgAdapterState)
order := make([]string, 0)
var current *netCfgAdapterState
var currentName string
for _, raw := range lines {
line := strings.TrimRight(raw, "\r")
trimmed := strings.TrimSpace(line)
if trimmed == "" {
current = nil
currentName = ""
continue
}
if strings.HasPrefix(trimmed, "[") && strings.HasSuffix(trimmed, "]") {
current = nil
currentName = ""
continue
}
if !strings.HasPrefix(line, " ") && !strings.HasPrefix(line, "\t") {
m := netCfgHeaderRE.FindStringSubmatch(line)
if len(m) != 2 {
current = nil
currentName = ""
continue
}
ifaceName := strings.TrimSpace(m[1])
baseName := baseInterfaceName(ifaceName)
if strings.EqualFold(baseName, "lo") {
current = nil
currentName = ""
continue
}
mac := normalizeMACAddress(extractFirstRegexpGroup(line, netCfgHWAddrRE))
key := "if:" + strings.ToLower(baseName)
if mac != "" {
key = "mac:" + strings.ToLower(mac)
}
state, ok := byKey[key]
if !ok {
state = &netCfgAdapterState{Slot: baseName}
byKey[key] = state
order = append(order, key)
}
if state.Slot == "" {
state.Slot = baseName
}
appendStringUnique(&state.Names, ifaceName)
if mac != "" {
appendStringUnique(&state.MACs, mac)
}
current = state
currentName = ifaceName
continue
}
if current == nil {
continue
}
upper := strings.ToUpper(trimmed)
if strings.Contains(upper, "UP") && !strings.Contains(upper, "DOWN") {
current.Up = true
}
appendMatchesUnique(&current.IPv4, trimmed, netCfgInet4OldRE)
appendMatchesUnique(&current.IPv4, trimmed, netCfgInet4NewRE)
appendMatchesUnique(&current.IPv6, strings.ToLower(trimmed), netCfgInet6OldRE)
appendMatchesUnique(&current.IPv6, strings.ToLower(trimmed), netCfgInet6NewRE)
if currentName != "" {
appendStringUnique(&current.Names, currentName)
}
}
out := make([]models.NetworkAdapter, 0, len(order))
for _, key := range order {
state := byKey[key]
if state == nil {
continue
}
if strings.TrimSpace(state.Slot) == "" && len(state.MACs) == 0 {
continue
}
status := "down"
if state.Up {
status = "ok"
}
descParts := make([]string, 0, 3)
if len(state.Names) > 0 {
descParts = append(descParts, "interfaces: "+strings.Join(state.Names, ", "))
}
if len(state.IPv4) > 0 {
descParts = append(descParts, "ipv4: "+strings.Join(state.IPv4, ", "))
}
if len(state.IPv6) > 0 {
descParts = append(descParts, "ipv6: "+strings.Join(state.IPv6, ", "))
}
out = append(out, models.NetworkAdapter{
Slot: strings.TrimSpace(state.Slot),
Location: strings.TrimSpace(firstNonEmpty(state.Slot, firstSliceValue(state.Names))),
Present: true,
Model: "Ethernet Interface",
Description: strings.Join(descParts, "; "),
PortCount: maxInt(1, len(state.MACs)),
MACAddresses: append([]string(nil), state.MACs...),
Status: status,
})
}
return out
}
func networkCardsFromAdapters(items []models.NetworkAdapter) []models.NIC {
out := make([]models.NIC, 0, len(items))
for _, item := range items {
mac := firstSliceValue(item.MACAddresses)
if strings.TrimSpace(item.Model) == "" && strings.TrimSpace(mac) == "" {
continue
}
out = append(out, models.NIC{
Name: firstNonEmpty(item.Slot, item.Location, "NIC"),
Model: firstNonEmpty(item.Model, "Ethernet Interface"),
Description: item.Description,
MACAddress: mac,
})
}
return out
}
func extractFirstRegexpGroup(s string, re *regexp.Regexp) string {
if re == nil {
return ""
}
m := re.FindStringSubmatch(s)
if len(m) < 2 {
return ""
}
return strings.TrimSpace(m[1])
}
func appendMatchesUnique(dst *[]string, text string, re *regexp.Regexp) {
if re == nil {
return
}
matches := re.FindAllStringSubmatch(text, -1)
for _, m := range matches {
if len(m) < 2 {
continue
}
appendStringUnique(dst, strings.TrimSpace(m[1]))
}
}
func appendStringUnique(dst *[]string, v string) {
v = strings.TrimSpace(v)
if v == "" {
return
}
for _, cur := range *dst {
if strings.EqualFold(strings.TrimSpace(cur), v) {
return
}
}
*dst = append(*dst, v)
}
func normalizeMACAddress(s string) string {
s = strings.ToUpper(strings.TrimSpace(s))
if len(s) == 17 {
return s
}
s = strings.ReplaceAll(s, "-", "")
s = strings.ReplaceAll(s, ":", "")
if len(s) != 12 {
return ""
}
parts := make([]string, 0, 6)
for i := 0; i < len(s); i += 2 {
parts = append(parts, s[i:i+2])
}
return strings.Join(parts, ":")
}
func baseInterfaceName(name string) string {
name = strings.TrimSpace(name)
if idx := strings.Index(name, "."); idx > 0 {
return strings.TrimSpace(name[:idx])
}
return name
}
func firstSliceValue(items []string) string {
for _, item := range items {
if strings.TrimSpace(item) != "" {
return strings.TrimSpace(item)
}
}
return ""
}
func normalizeUnknownString(s string) string {
s = strings.TrimSpace(s)
if s == "" {
return ""
}
lower := strings.ToLower(s)
if lower == "n/a" || lower == "na" || lower == "-" || lower == "unknown" || lower == "none" {
return ""
}
return s
}
func normalizePCIeSlotLabel(raw string) string {
raw = strings.TrimSpace(raw)
if raw == "" {
return ""
}
if strings.EqualFold(raw, "N/A") || strings.EqualFold(raw, "-") {
return ""
}
m := pcieSlotDigitsRE.FindStringSubmatch(raw)
if len(m) >= 2 {
if n := parseMaybeInt(m[1]); n > 0 {
return fmt.Sprintf("PCIe %d", n)
}
}
return raw
}
func inferPortCountFromNetworkFields(model, networkPort string) int {
modelLower := strings.ToLower(strings.TrimSpace(model))
if modelLower != "" {
if m := portCountPattRE.FindStringSubmatch(modelLower); len(m) == 2 {
if n := parseMaybeInt(m[1]); n > 0 {
return n
}
}
if m := portCountMulRE.FindStringSubmatch(modelLower); len(m) == 2 {
if n := parseMaybeInt(m[1]); n > 0 {
return n
}
}
}
if m := portSuffixRE.FindStringSubmatch(strings.ToLower(strings.TrimSpace(networkPort))); len(m) == 2 {
if n := parseMaybeInt(m[1]); n > 0 {
return n
}
}
return 0
}
func parseSmartdataDevice(content []byte) (model, serial string) {
scanner := bufio.NewScanner(strings.NewReader(string(content)))
for scanner.Scan() {
line := strings.TrimSpace(scanner.Text())
if line == "" || !strings.Contains(line, ":") {
continue
}
key, value, ok := parseColonKVLine(line)
if !ok {
continue
}
keyNorm := normalizeKey(key)
val := strings.TrimSpace(value)
if val == "" {
continue
}
switch keyNorm {
case "modelinfo", "devicemodel", "modelnumber":
if model == "" {
model = val
}
case "serialnumber":
if serial == "" {
serial = val
}
}
}
return strings.TrimSpace(model), strings.TrimSpace(serial)
}
func smartdataSlotFromPath(path string) string {
path = strings.ReplaceAll(path, "\\", "/")
parts := strings.Split(path, "/")
for i := range parts {
if strings.EqualFold(parts[i], "smartdata") && i+1 < len(parts) {
return strings.TrimSpace(parts[i+1])
}
}
return ""
}
func parsePSUCfgINI(content []byte) []models.PSU {
section := ""
current := make(map[string]string)
records := make([]map[string]string, 0)
isPSUSection := func(name string) bool {
norm := normalizeKey(name)
return norm == "activestandbyconfiguration" || strings.HasPrefix(norm, "psu")
}
flush := func() {
if len(current) == 0 {
return
}
records = append(records, current)
current = make(map[string]string)
}
scanner := bufio.NewScanner(strings.NewReader(string(content)))
for scanner.Scan() {
line := strings.TrimSpace(scanner.Text())
if line == "" || strings.HasPrefix(line, "#") || strings.HasPrefix(line, ";") {
continue
}
if strings.HasPrefix(line, "[") && strings.HasSuffix(line, "]") {
if isPSUSection(section) {
flush()
}
section = strings.TrimSpace(line[1 : len(line)-1])
continue
}
if !isPSUSection(section) {
continue
}
key, value, ok := parseFlexibleKVLine(line)
if !ok {
continue
}
keyNorm := normalizeKey(key)
if keyNorm == "powerid" && len(current) > 0 && getSectionValue(current, "Power ID", "PowerID") != "" {
flush()
}
current[key] = value
}
if isPSUSection(section) {
flush()
}
out := make([]models.PSU, 0, len(records))
for _, rec := range records {
id := strings.TrimSpace(getSectionValue(rec, "Power ID", "PowerID"))
slot := firstNonEmpty(id, getSectionValue(rec, "Slot", "Position"))
if slot != "" && !strings.HasPrefix(strings.ToUpper(slot), "PSU") {
slot = "PSU" + slot
}
present := true
presentRaw := strings.TrimSpace(getSectionValue(rec, "Present Status", "Present"))
if presentRaw != "" {
present = containsAny(strings.ToLower(presentRaw), "present", "yes", "ok", "active")
}
status := "absent"
if present {
status = "ok"
}
cold := strings.TrimSpace(getSectionValue(rec, "Cold Status"))
if strings.Contains(strings.ToLower(cold), "active") {
status = "ok"
}
psu := models.PSU{
Slot: slot,
Present: present,
Model: strings.TrimSpace(getSectionValue(rec, "Model")),
SerialNumber: strings.TrimSpace(getSectionValue(rec, "SN", "Serial Number")),
WattageW: parseMaybeIntLoose(getSectionValue(rec, "Max Power(W)", "Max Power")),
Status: status,
Description: cold,
}
if strings.TrimSpace(psu.Slot) == "" && strings.TrimSpace(psu.SerialNumber) == "" && strings.TrimSpace(psu.Model) == "" {
continue
}
out = append(out, psu)
}
return out
}
func parseBoardCfgINI(content []byte, result *models.AnalysisResult) {
sections := parseLooseINI(string(content))
if len(sections) == 0 {
return
}
board := &result.Hardware.BoardInfo
boardType := getSectionValue(findINISection(sections, "Board Type"), "Board Type")
boardVersion := getSectionValue(findINISection(sections, "Board Version"), "Board Version")
customerID := getSectionValue(findINISection(sections, "Customer ID"), "CustomerID", "Customer ID")
oemFlag := getSectionValue(findINISection(sections, "OEM ID"), "OEM Flag", "OEMID")
setIfEmpty(&board.ProductName, boardType)
setIfEmpty(&board.Version, boardVersion)
extras := make([]string, 0, 2)
if customerID != "" {
extras = append(extras, "CustomerID: "+customerID)
}
if oemFlag != "" {
extras = append(extras, "OEM Flag: "+oemFlag)
}
if len(extras) > 0 {
if strings.TrimSpace(board.Description) == "" {
board.Description = strings.Join(extras, "; ")
} else if !strings.Contains(strings.ToLower(board.Description), strings.ToLower(extras[0])) {
board.Description = strings.TrimSpace(board.Description) + "; " + strings.Join(extras, "; ")
}
}
}
func findINISection(sections map[string]map[string]string, name string) map[string]string {
target := normalizeKey(name)
for sectionName, section := range sections {
if normalizeKey(sectionName) == target {
return section
}
}
return nil
}
func parseRaidBPConfDevices(content []byte) []models.HardwareDevice {
lines := strings.Split(string(content), "\n")
section := ""
devices := make([]models.HardwareDevice, 0)
backplaneIndex := 0
for _, raw := range lines {
line := strings.TrimSpace(raw)
if line == "" {
continue
}
if strings.HasPrefix(line, "[") && strings.HasSuffix(line, "]") {
section = strings.ToLower(strings.TrimSpace(line[1 : len(line)-1]))
continue
}
switch section {
case "bp information":
if strings.Contains(strings.ToLower(line), "description") && strings.Contains(line, "|") {
continue
}
if !strings.Contains(line, "|") {
continue
}
cols := parsePipeColumns(line)
if len(cols) < 3 {
continue
}
backplaneIndex++
desc := cols[0]
bpType := cols[1]
slot := cols[2]
if slot == "" || slot == "~" {
slot = fmt.Sprintf("BP-%d", backplaneIndex)
}
dev := models.HardwareDevice{
ID: fmt.Sprintf("h3c-bp-%d-%s", backplaneIndex, strings.ToLower(strings.ReplaceAll(slot, " ", "-"))),
Kind: models.DeviceKindStorage,
Slot: slot,
Model: bpType,
DeviceClass: "backplane",
Status: "ok",
Details: map[string]any{
"source": "Raid_BP_Conf_Info.ini",
"description": desc,
},
}
if len(cols) > 4 && cols[4] != "" && cols[4] != "~" {
dev.Location = cols[4]
}
devices = append(devices, dev)
case "raid information":
if strings.Contains(strings.ToLower(line), "pcie slot") && strings.Contains(line, "|") {
continue
}
if !strings.Contains(line, "|") {
continue
}
cols := parsePipeColumns(line)
if len(cols) < 2 {
continue
}
slot := strings.TrimSpace(cols[0])
if slot == "" || slot == "~" {
continue
}
sasNum := strings.TrimSpace(cols[1])
desc := "RAID Controller"
if sasNum != "" && sasNum != "~" {
desc = "SAS ports: " + sasNum
}
dev := models.HardwareDevice{
ID: "h3c-raid-slot-" + slot,
Kind: models.DeviceKindPCIe,
Slot: "PCIe " + slot,
Model: "RAID Controller",
DeviceClass: "raid_controller",
Status: "ok",
Details: map[string]any{
"source": "Raid_BP_Conf_Info.ini",
"description": desc,
"pcie_slot": slot,
"raid_sasnum": sasNum,
},
}
devices = append(devices, dev)
}
}
return dedupeHardwareDevices(devices)
}
func parsePipeColumns(line string) []string {
parts := strings.Split(line, "|")
out := make([]string, 0, len(parts))
for _, part := range parts {
v := strings.TrimSpace(part)
if v == "" {
continue
}
out = append(out, v)
}
return out
}
func dedupeHardwareDevices(items []models.HardwareDevice) []models.HardwareDevice {
out := make([]models.HardwareDevice, 0, len(items))
seen := make(map[string]struct{}, len(items))
for _, item := range items {
key := strings.ToLower(strings.TrimSpace(item.ID))
if key == "" {
key = strings.ToLower(strings.TrimSpace(item.Kind + "|" + item.Slot + "|" + item.Model))
}
if _, ok := seen[key]; ok {
continue
}
seen[key] = struct{}{}
out = append(out, item)
}
return out
}
func parseFirmwareJSON(content []byte, result *models.AnalysisResult, firmwareSeen map[string]struct{}) {
type fwEntry struct {
FirmwareName string `json:"Firmware Name"`
FirmwareVersion string `json:"Firmware Version"`
Location string `json:"Location"`
PartModel string `json:"Part Model"`
}
var payload map[string]fwEntry
if err := json.Unmarshal(content, &payload); err != nil {
return
}
keys := make([]string, 0, len(payload))
for key := range payload {
keys = append(keys, key)
}
sort.Strings(keys)
for _, key := range keys {
entry := payload[key]
version := strings.TrimSpace(entry.FirmwareVersion)
if version == "" || strings.EqualFold(version, "N/A") {
continue
}
name := strings.TrimSpace(entry.FirmwareName)
if name == "" {
name = key
}
descParts := make([]string, 0, 2)
if loc := strings.TrimSpace(entry.Location); loc != "" && loc != "-" {
descParts = append(descParts, "location: "+loc)
}
if part := strings.TrimSpace(entry.PartModel); part != "" && part != "-" {
descParts = append(descParts, "model: "+part)
}
appendFirmwareUnique(result, firmwareSeen, models.FirmwareInfo{
DeviceName: name,
Version: version,
Description: strings.Join(descParts, "; "),
})
}
}
func parseCPUXML(content []byte, result *models.AnalysisResult) {
root, ok := parseXMLRoot(content)
if !ok {
return
}
for _, node := range root.Nodes {
if !strings.HasPrefix(strings.ToLower(node.XMLName.Local), "cpu") {
continue
}
fields := xmlNodeFields(node)
model := firstNonEmpty(fields["Model"], fields["CPUName"])
if model == "" {
continue
}
socket := parseSocketID(node.XMLName.Local)
cpu := models.CPU{
Socket: socket,
Model: model,
Description: strings.TrimSpace(fields["Manufacturer"]),
Cores: parseMaybeInt(fields["TotalCores"]),
Threads: parseMaybeInt(fields["TotalThreads"]),
FrequencyMHz: parseMaybeInt(fields["ProcessorSpeed"]),
MaxFreqMHz: parseMaybeInt(fields["ProcessorMaxSpeed"]),
SerialNumber: strings.TrimSpace(fields["SerialNumber"]),
PPIN: strings.TrimSpace(fields["PPIN"]),
Status: normalizePresenceStatus(fields["Status"]),
}
result.Hardware.CPUs = append(result.Hardware.CPUs, cpu)
}
}
func parseMemoryXML(content []byte, result *models.AnalysisResult) {
root, ok := parseXMLRoot(content)
if !ok {
return
}
for _, node := range root.Nodes {
name := strings.ToLower(node.XMLName.Local)
if !strings.HasPrefix(name, "dimm") {
continue
}
fields := xmlNodeFields(node)
slot := strings.TrimSpace(fields["Name"])
if slot == "" {
slot = node.XMLName.Local
}
statusRaw := strings.TrimSpace(fields["Status"])
present := strings.Contains(strings.ToLower(statusRaw), "presence")
status := "absent"
if present {
status = "ok"
}
dimm := models.MemoryDIMM{
Slot: slot,
Location: strings.TrimSpace(fields["DIMMSilk"]),
Present: present,
SizeMB: parseMaybeInt(fields["DIMMSize"]),
Type: strings.TrimSpace(fields["DIMMTech"]),
MaxSpeedMHz: parseMaybeInt(fields["MaxFreq"]),
CurrentSpeedMHz: parseMaybeInt(fields["CurFreq"]),
SerialNumber: strings.TrimSpace(fields["SerialNumber"]),
PartNumber: strings.TrimSpace(fields["PartNumber"]),
Status: status,
Ranks: parseMaybeInt(fields["DIMMRanks"]),
}
if dimm.Type == "" {
dimm.Type = "DIMM"
}
result.Hardware.Memory = append(result.Hardware.Memory, dimm)
}
}
var diskSectionRE = regexp.MustCompile(`(?i)^disk_\d+$`)
func parseStorageINI(content []byte) []models.Storage {
sections := parseLooseINI(string(content))
names := sortedSectionNames(sections)
out := make([]models.Storage, 0)
for _, sectionName := range names {
if !diskSectionRE.MatchString(strings.TrimSpace(sectionName)) {
continue
}
section := sections[sectionName]
slot := getSectionValue(section, "DiskSlotDesc")
if slot == "" {
slot = sectionName
}
present := isTruthy(getSectionValue(section, "Present"))
status := "absent"
if present {
status = "ok"
}
storage := models.Storage{
Slot: slot,
SerialNumber: getSectionValue(section, "SerialNumber"),
Present: present,
Status: status,
Type: inferStorageType(slot),
}
if storage.Type == "" {
storage.Type = "disk"
}
out = append(out, storage)
}
return out
}
func parseRAIDJSONVolumes(content []byte) []models.StorageVolume {
var root map[string]any
if err := json.Unmarshal(content, &root); err != nil {
return nil
}
raidCfg := toAnyMap(lookupAnyCase(root, "RaidConfig"))
ctrlInfo := toAnySlice(lookupAnyCase(raidCfg, "CtrlInfo", "ControllerInfo"))
volumes := make([]models.StorageVolume, 0)
for ctrlIdx, ctrlAny := range ctrlInfo {
ctrl := toAnyMap(ctrlAny)
ctrlSlot := firstNonEmpty(
toStringAny(lookupAnyCase(ctrl, "CtrlSlot", "CtrlDevice Slot", "Slot")),
toStringAny(lookupAnyByPrefix(ctrl, "ctrldeviceslot")),
)
ctrlName := firstNonEmpty(
toStringAny(lookupAnyCase(ctrl, "CtrlName", "CtrlDevice Name", "Name", "ControllerName")),
toStringAny(lookupAnyByPrefix(ctrl, "ctrldevicename")),
fmt.Sprintf("RAID Controller %d", ctrlIdx+1),
)
controller := ctrlName
if ctrlSlot != "" {
controller = fmt.Sprintf("%s (slot %s)", ctrlName, ctrlSlot)
}
ldInfo := toAnySlice(lookupAnyCase(ctrl, "LDInfo", "LogicalDeviceInfo", "LogicalDiskInfo", "VirtualDriveInfo"))
for ldIdx, ldAny := range ldInfo {
ld := toAnyMap(ldAny)
vol := parseVolumeFromAnyMap(ld, controller, ldIdx+1)
if isVolumeEmpty(vol) {
continue
}
volumes = append(volumes, vol)
}
}
return dedupeVolumes(volumes)
}
func parseRAIDDetailTXT(content []byte) ([]models.Storage, []models.StorageVolume) {
lines := strings.Split(string(content), "\n")
controllerInfo := make(map[string]string)
logicalRecords := make([]map[string]string, 0)
physicalRecords := make([]map[string]string, 0)
section := ""
currentLogical := make(map[string]string)
currentPhysical := make(map[string]string)
flushLogical := func() {
if len(currentLogical) == 0 {
return
}
logicalRecords = append(logicalRecords, currentLogical)
currentLogical = make(map[string]string)
}
flushPhysical := func() {
if len(currentPhysical) == 0 {
return
}
physicalRecords = append(physicalRecords, currentPhysical)
currentPhysical = make(map[string]string)
}
for _, rawLine := range lines {
line := strings.TrimSpace(rawLine)
if line == "" {
continue
}
lower := strings.ToLower(line)
switch {
case strings.Contains(lower, "controller information"):
flushLogical()
flushPhysical()
section = "controller"
continue
case strings.Contains(lower, "logical device information"):
flushLogical()
flushPhysical()
section = "logical"
continue
case strings.Contains(lower, "physical device information"):
flushLogical()
flushPhysical()
section = "physical"
continue
case strings.HasPrefix(line, "[") && strings.HasSuffix(line, "]"):
continue
case strings.HasPrefix(line, "---"):
continue
}
key, value, ok := parseColonKVLine(line)
if !ok {
continue
}
normKey := normalizeKey(key)
switch section {
case "controller":
controllerInfo[key] = value
case "logical":
if len(currentLogical) > 0 && startsNewLogicalRecord(currentLogical, normKey) {
flushLogical()
}
currentLogical[key] = value
case "physical":
if len(currentPhysical) > 0 && normKey == "connectionid" {
flushPhysical()
}
currentPhysical[key] = value
}
}
flushLogical()
flushPhysical()
controller := firstNonEmpty(
getSectionValue(controllerInfo, "AssetTag", "CtrlName", "ControllerName", "ChipModel"),
"RAID Controller",
)
storages := make([]models.Storage, 0, len(physicalRecords))
for _, rec := range physicalRecords {
slot := firstNonEmpty(getSectionValue(rec, "Position", "Slot", "Location"))
if slot == "" {
slot = "Connection " + getSectionValue(rec, "ConnectionID")
}
mediaType := getSectionValue(rec, "MediaType", "Type")
protocol := getSectionValue(rec, "Protocol")
model := getSectionValue(rec, "Model")
statusRaw := firstNonEmpty(getSectionValue(rec, "StatusIndicator", "FirmwareStatus", "Status"), "ok")
capacityBytes, sizeGB := parseCapacityFields(getSectionValue(rec, "CapacityBytes"), getSectionValue(rec, "Capacity"), "")
if sizeGB == 0 && capacityBytes > 0 {
sizeGB = int(capacityBytes / 1_000_000_000)
}
present := true
statusLower := strings.ToLower(statusRaw)
if containsAny(statusLower, "missing", "absent", "removed", "not present") {
present = false
}
storage := models.Storage{
Slot: slot,
Type: inferStorageTypeFromValues(slot, mediaType, protocol, model),
Model: strings.TrimSpace(model),
SizeGB: sizeGB,
SerialNumber: getSectionValue(rec, "SerialNumber"),
Manufacturer: getSectionValue(rec, "Manufacturer", "Vendor"),
Firmware: getSectionValue(rec, "Revision", "FirmwareVersion"),
Interface: protocol,
Present: present,
Location: firstNonEmpty(getSectionValue(rec, "Position"), controller),
Status: normalizeStorageStatus(statusRaw, present),
}
if storage.Type == "" {
storage.Type = "disk"
}
storages = append(storages, storage)
}
volumes := make([]models.StorageVolume, 0, len(logicalRecords))
for idx, rec := range logicalRecords {
vol := parseVolumeFromStringMap(rec, controller, idx+1)
if isVolumeEmpty(vol) {
continue
}
volumes = append(volumes, vol)
}
return dedupeStorage(storages), dedupeVolumes(volumes)
}
func parseNVMeInfo(content []byte) []models.Storage {
text := strings.TrimSpace(string(content))
if text == "" {
return nil
}
lower := strings.ToLower(text)
if strings.Contains(lower, "note: no nvme info") {
return nil
}
storages := make([]models.Storage, 0)
sections := parseLooseINI(text)
for _, sectionName := range sortedSectionNames(sections) {
section := sections[sectionName]
storage := parseStorageFromSection(section, sectionName, "nvme")
if isStorageEmpty(storage) {
continue
}
storages = append(storages, storage)
}
if len(storages) > 0 {
return dedupeStorage(storages)
}
for _, block := range parseKeyValueBlocks(text) {
storage := parseStorageFromSection(block, "NVMe", "nvme")
if isStorageEmpty(storage) {
continue
}
storages = append(storages, storage)
}
return dedupeStorage(storages)
}
func parseStorageFromSection(section map[string]string, fallbackSlot, defaultType string) models.Storage {
slot := firstNonEmpty(getSectionValue(section, "DiskSlotDesc", "Position", "Slot", "Name"), fallbackSlot)
model := firstNonEmpty(getSectionValue(section, "Model", "ProductName", "DeviceModel", "PartNumber"), "")
media := firstNonEmpty(getSectionValue(section, "MediaType", "Type"), defaultType)
protocol := firstNonEmpty(getSectionValue(section, "Protocol", "Interface"), "")
statusRaw := firstNonEmpty(getSectionValue(section, "Status", "State", "Health"), "")
presentRaw := firstNonEmpty(getSectionValue(section, "Present"), "")
present := true
if presentRaw != "" {
present = isTruthy(presentRaw)
if !present && !containsAny(strings.ToLower(presentRaw), "absent", "missing", "no") {
present = true
}
}
if statusRaw != "" && containsAny(strings.ToLower(statusRaw), "absent", "missing", "removed") {
present = false
}
capacityBytes, sizeGB := parseCapacityFields(
getSectionValue(section, "CapacityBytes", "SizeBytes"),
getSectionValue(section, "Capacity", "Size"),
getSectionValue(section, "SizeGB", "CapacityGB"),
)
if sizeGB == 0 && capacityBytes > 0 {
sizeGB = int(capacityBytes / 1_000_000_000)
}
storage := models.Storage{
Slot: slot,
Type: inferStorageTypeFromValues(slot, media, protocol, model),
Model: model,
SizeGB: sizeGB,
SerialNumber: getSectionValue(section, "SerialNumber"),
Manufacturer: getSectionValue(section, "Manufacturer", "Vendor"),
Firmware: getSectionValue(section, "Revision", "FirmwareVersion", "Firmware"),
Interface: protocol,
Present: present,
Location: firstNonEmpty(getSectionValue(section, "Position", "Location"), slot),
Status: normalizeStorageStatus(statusRaw, present),
}
if storage.Type == "" {
storage.Type = defaultType
}
return storage
}
func parseVolumeFromAnyMap(rec map[string]any, controller string, idx int) models.StorageVolume {
id := firstNonEmpty(
toStringAny(lookupAnyCase(rec, "LDID", "LD ID", "Id", "ID", "VDID", "LogicalDeviceID", "VirtualDriveID")),
fmt.Sprintf("ld-%d", idx),
)
name := firstNonEmpty(
toStringAny(lookupAnyCase(rec, "LDName", "LD_name", "Name", "VolumeName", "LogicalDeviceName")),
id,
)
capacityBytes := parseLDCapacity(rec)
sizeGB := parseMaybeInt(toStringAny(lookupAnyCase(rec, "SizeGB", "CapacityGB")))
if sizeGB == 0 && capacityBytes > 0 {
sizeGB = int(capacityBytes / 1_000_000_000)
}
bootable, _ := toBoolAny(lookupAnyCase(rec, "Bootable", "IsBootable"))
encrypted, _ := toBoolAny(lookupAnyCase(rec, "Encrypted", "IsEncrypted", "EncryptionEnabled"))
return models.StorageVolume{
ID: id,
Name: name,
Controller: controller,
RAIDLevel: normalizeRAIDLevel(firstNonEmpty(toStringAny(lookupAnyCase(rec, "RAIDLevel", "RaidLevel", "RAID", "Level")), toStringAny(lookupAnyByPrefix(rec, "raidlevel")))),
SizeGB: sizeGB,
CapacityBytes: capacityBytes,
Status: normalizeVolumeStatus(toStringAny(lookupAnyCase(rec, "Status", "State", "HealthStatus"))),
Bootable: bootable,
Encrypted: encrypted,
}
}
func parseVolumeFromStringMap(rec map[string]string, controller string, idx int) models.StorageVolume {
id := firstNonEmpty(
getSectionValue(rec, "LDID", "Id", "ID", "VDID", "LogicalDeviceID", "VirtualDriveID"),
fmt.Sprintf("ld-%d", idx),
)
name := firstNonEmpty(
getSectionValue(rec, "LDName", "Name", "VolumeName", "LogicalDeviceName"),
id,
)
capacityBytes, sizeGB := parseCapacityFields(
getSectionValue(rec, "CapacityBytes", "SizeBytes"),
getSectionValue(rec, "Capacity", "Size"),
getSectionValue(rec, "SizeGB", "CapacityGB"),
)
if sizeGB == 0 && capacityBytes > 0 {
sizeGB = int(capacityBytes / 1_000_000_000)
}
bootable, _ := parseBoolString(getSectionValue(rec, "Bootable", "IsBootable"))
encrypted, _ := parseBoolString(getSectionValue(rec, "Encrypted", "IsEncrypted", "EncryptionEnabled"))
return models.StorageVolume{
ID: id,
Name: name,
Controller: controller,
RAIDLevel: normalizeRAIDLevel(getSectionValue(rec, "RAIDLevel", "RaidLevel", "RAID", "Level")),
SizeGB: sizeGB,
CapacityBytes: capacityBytes,
Status: normalizeVolumeStatus(getSectionValue(rec, "Status", "State", "HealthStatus")),
Bootable: bootable,
Encrypted: encrypted,
}
}
func parseSensorInfoINI(content []byte) []models.SensorReading {
sensors := make([]models.SensorReading, 0)
scanner := bufio.NewScanner(strings.NewReader(string(content)))
for scanner.Scan() {
line := strings.TrimSpace(scanner.Text())
if line == "" || !strings.Contains(line, "|") {
continue
}
if strings.Contains(strings.ToLower(line), "sensor name") && strings.Contains(strings.ToLower(line), "reading") {
continue
}
parts := strings.Split(line, "|")
if len(parts) < 4 {
continue
}
name := strings.TrimSpace(parts[0])
reading := strings.TrimSpace(parts[1])
unit := strings.TrimSpace(parts[2])
status := strings.TrimSpace(parts[3])
if name == "" {
continue
}
sensor := models.SensorReading{
Name: name,
Type: inferSensorType(unit, reading),
Unit: unit,
RawValue: reading,
Status: normalizeSensorStatus(status),
}
if v, ok := parseMaybeFloat(reading); ok {
sensor.Value = v
}
sensors = append(sensors, sensor)
}
return sensors
}
type selEntry struct {
Created string `json:"Created"`
Description string `json:"Description"`
Severity string `json:"Severity"`
EntryCode string `json:"EntryCode"`
EntryType string `json:"EntryType"`
ID int `json:"Id"`
Level string `json:"Level"`
Message string `json:"Message"`
SensorName string `json:"SensorName"`
SensorType string `json:"SensorType"`
}
func parseSELJSON(content []byte) []models.Event {
text := strings.TrimSpace(string(content))
if text == "" {
return nil
}
if !strings.HasPrefix(text, "[") {
text = strings.TrimRight(text, ", \n\r\t")
text = "[" + text + "]"
}
var entries []selEntry
if err := json.Unmarshal([]byte(text), &entries); err != nil {
return nil
}
events := make([]models.Event, 0, len(entries))
for _, entry := range entries {
timestamp := parseH3CTimestamp(entry.Created)
if timestamp.IsZero() {
timestamp = time.Now()
}
description := firstNonEmpty(entry.Message, entry.Description)
eventType := firstNonEmpty(entry.SensorType, entry.EntryType, "SEL")
events = append(events, models.Event{
ID: strconv.Itoa(entry.ID),
Timestamp: timestamp,
Source: "SEL",
SensorType: strings.TrimSpace(entry.SensorType),
SensorName: strings.TrimSpace(entry.SensorName),
EventType: strings.TrimSpace(eventType),
Severity: mapH3CSeverity(entry.Severity, entry.Level, entry.Message),
Description: strings.TrimSpace(description),
RawData: strings.TrimSpace(entry.EntryCode),
})
}
return events
}
func parseSELListTXT(content []byte) []models.Event {
lines := strings.Split(string(content), "\n")
events := make([]models.Event, 0, len(lines))
location := parser.DefaultArchiveLocation()
for _, line := range lines {
line = strings.TrimSpace(line)
if line == "" || !strings.Contains(line, "|") {
continue
}
parts := strings.Split(line, "|")
if len(parts) < 6 {
continue
}
id := strings.TrimSpace(parts[0])
date := strings.TrimSpace(parts[1])
tod := strings.TrimSpace(parts[2])
sensor := strings.TrimSpace(parts[3])
message := strings.TrimSpace(parts[4])
state := strings.TrimSpace(parts[5])
if strings.EqualFold(date, "Pre-Init") {
continue
}
ts, err := time.ParseInLocation("01/02/2006 15:04:05", date+" "+tod, location)
if err != nil {
continue
}
sensorType := strings.TrimSpace(sensor)
sensorName := ""
if hashIdx := strings.Index(sensor, "#"); hashIdx != -1 {
sensorType = strings.TrimSpace(sensor[:hashIdx])
sensorName = strings.TrimSpace(sensor[hashIdx+1:])
}
events = append(events, models.Event{
ID: id,
Timestamp: ts,
Source: "SEL",
SensorType: sensorType,
SensorName: sensorName,
EventType: message,
Severity: mapH3CSeverity("", "", sensor+" "+message+" "+state),
Description: buildSELDescription(message, state),
RawData: line,
})
}
return events
}
func parseSELCSVFiles(files []parser.ExtractedFile) []models.Event {
events := make([]models.Event, 0)
for _, f := range files {
path := strings.ToLower(strings.TrimSpace(f.Path))
if !strings.HasSuffix(path, ".csv") {
continue
}
if !(strings.Contains(path, "/user/") || strings.HasPrefix(path, "user/")) {
continue
}
events = append(events, parseSELCSV(f.Content)...)
}
return events
}
func parseSELCSV(content []byte) []models.Event {
csvText := strings.ReplaceAll(string(content), "\x00", "")
reader := csv.NewReader(strings.NewReader(csvText))
reader.FieldsPerRecord = -1
reader.LazyQuotes = true
reader.TrimLeadingSpace = true
header, err := reader.Read()
if err != nil {
return nil
}
index := make(map[string]int, len(header))
for i, col := range header {
index[normalizeKey(col)] = i
}
required := []string{"descinfo"}
if !hasAllKeys(index, required...) {
return nil
}
if !hasAnyKey(index, "recordtimestamp", "eventoccurredtime") {
return nil
}
events := make([]models.Event, 0, 128)
for {
row, err := reader.Read()
if err == io.EOF {
break
}
if err != nil {
continue
}
ts := parseSELCSVTimestamp(
readCSVField(row, index, "eventoccurredtime"),
readCSVField(row, index, "recordtimestamp"),
)
if ts.IsZero() {
continue
}
desc := strings.TrimSpace(readCSVField(row, index, "descinfo"))
explanation := strings.TrimSpace(readCSVField(row, index, "explanation"))
if desc == "" && explanation == "" {
continue
}
events = append(events, models.Event{
ID: strconv.Itoa(len(events) + 1),
Timestamp: ts,
Source: "SEL",
SensorType: strings.TrimSpace(readCSVField(row, index, "sensortypestr")),
SensorName: strings.TrimSpace(readCSVField(row, index, "sensorname")),
EventType: strings.TrimSpace(readCSVField(row, index, "eventdir")),
Severity: mapH3CSeverity(readCSVField(row, index, "severitylevel"), readCSVField(row, index, "severitylevelid"), desc+" "+explanation),
Description: composeSELCSVDescription(desc, explanation),
RawData: strings.TrimSpace(readCSVField(row, index, "severitylevelid")),
})
}
return events
}
func parseSELCSVTimestamp(values ...string) time.Time {
location := parser.DefaultArchiveLocation()
for _, raw := range values {
clean := strings.TrimSpace(strings.ReplaceAll(raw, "\x00", ""))
if clean == "" || strings.EqualFold(clean, "Pre-Init") {
continue
}
if ts := parseH3CTimestamp(clean); !ts.IsZero() {
return ts
}
if ts, err := time.ParseInLocation("2006-01-02 15:04:05", clean, location); err == nil {
return ts
}
}
return time.Time{}
}
func composeSELCSVDescription(desc, explanation string) string {
desc = strings.TrimSpace(desc)
explanation = strings.TrimSpace(explanation)
if explanation == "" || explanation == desc {
return desc
}
if desc == "" {
return explanation
}
return desc + " | " + explanation
}
func readCSVField(row []string, index map[string]int, key string) string {
i, ok := index[normalizeKey(key)]
if !ok || i < 0 || i >= len(row) {
return ""
}
return strings.TrimSpace(strings.ReplaceAll(row[i], "\x00", ""))
}
func hasAnyKey(index map[string]int, keys ...string) bool {
for _, key := range keys {
if _, ok := index[normalizeKey(key)]; ok {
return true
}
}
return false
}
func hasAllKeys(index map[string]int, keys ...string) bool {
for _, key := range keys {
if _, ok := index[normalizeKey(key)]; !ok {
return false
}
}
return true
}
func parseH3CTimestamp(value string) time.Time {
value = strings.TrimSpace(value)
if value == "" {
return time.Time{}
}
layouts := []string{
"2006-01-02 15:04:05 MST-07:00",
"2006-01-02 15:04:05 -07:00",
time.RFC3339,
}
for _, layout := range layouts {
if ts, err := time.Parse(layout, value); err == nil {
return ts
}
}
cleaned := strings.ReplaceAll(value, "UTC", "")
cleaned = strings.Join(strings.Fields(cleaned), " ")
if ts, err := time.Parse("2006-01-02 15:04:05 -07:00", cleaned); err == nil {
return ts
}
return time.Time{}
}
func mapH3CSeverity(severity, level, text string) models.Severity {
all := strings.ToLower(strings.TrimSpace(severity + " " + level + " " + text))
if all == "" {
return models.SeverityInfo
}
if containsAny(all, "critical", "major", "fatal", "unrecoverable", "off-line", "offline", "redundancy lost", "failure", "fault", "error") {
return models.SeverityCritical
}
if containsAny(all, "warning", "warn", "minor", "non-critical", "lost", "degraded", "disabled", "abnormal") {
return models.SeverityWarning
}
return models.SeverityInfo
}
func buildSELDescription(message, state string) string {
message = strings.TrimSpace(message)
state = strings.TrimSpace(state)
if state == "" || strings.EqualFold(state, "Asserted") || strings.EqualFold(state, "Deasserted") {
return message
}
if message == "" {
return state
}
return message + " (" + state + ")"
}
func appendFirmwareUnique(result *models.AnalysisResult, seen map[string]struct{}, fw models.FirmwareInfo) {
fw.DeviceName = strings.TrimSpace(fw.DeviceName)
fw.Description = strings.TrimSpace(fw.Description)
fw.Version = strings.TrimSpace(fw.Version)
if fw.DeviceName == "" || fw.Version == "" {
return
}
key := strings.ToLower(fw.DeviceName + "|" + fw.Version + "|" + fw.Description)
if _, ok := seen[key]; ok {
return
}
seen[key] = struct{}{}
result.Hardware.Firmware = append(result.Hardware.Firmware, fw)
}
func parseLooseINI(content string) map[string]map[string]string {
sections := make(map[string]map[string]string)
current := ""
scanner := bufio.NewScanner(strings.NewReader(content))
for scanner.Scan() {
line := strings.TrimSpace(scanner.Text())
if line == "" || strings.HasPrefix(line, "#") || strings.HasPrefix(line, ";") {
continue
}
if strings.HasPrefix(line, "[") && strings.HasSuffix(line, "]") {
current = strings.TrimSpace(line[1 : len(line)-1])
if current != "" {
if _, ok := sections[current]; !ok {
sections[current] = make(map[string]string)
}
}
continue
}
if current == "" {
continue
}
sep := strings.IndexAny(line, "=:")
if sep <= 0 {
continue
}
key := strings.TrimSpace(line[:sep])
value := strings.TrimSpace(line[sep+1:])
if key == "" {
continue
}
sections[current][key] = value
}
return sections
}
func getSectionValue(section map[string]string, keys ...string) string {
for _, k := range keys {
normK := normalizeKey(k)
for rawKey, rawVal := range section {
if normalizeKey(rawKey) == normK {
val := strings.TrimSpace(rawVal)
if val != "" {
return val
}
}
}
}
return ""
}
func normalizeKey(s string) string {
s = strings.ToLower(strings.TrimSpace(s))
var b strings.Builder
b.Grow(len(s))
for _, r := range s {
if (r >= 'a' && r <= 'z') || (r >= '0' && r <= '9') {
b.WriteRune(r)
}
}
return b.String()
}
func sortedSectionNames(sections map[string]map[string]string) []string {
names := make([]string, 0, len(sections))
for name := range sections {
names = append(names, name)
}
sort.Strings(names)
return names
}
func isEmptyFRU(f models.FRUInfo) bool {
return strings.TrimSpace(f.Manufacturer) == "" &&
strings.TrimSpace(f.ProductName) == "" &&
strings.TrimSpace(f.SerialNumber) == "" &&
strings.TrimSpace(f.PartNumber) == "" &&
strings.TrimSpace(f.Version) == ""
}
func applyBoardFromFRU(board *models.BoardInfo, fru models.FRUInfo) {
setIfEmpty(&board.Manufacturer, fru.Manufacturer)
setIfEmpty(&board.ProductName, fru.ProductName)
setIfEmpty(&board.SerialNumber, fru.SerialNumber)
setIfEmpty(&board.PartNumber, fru.PartNumber)
setIfEmpty(&board.Version, fru.Version)
}
func setIfEmpty(dst *string, value string) {
if strings.TrimSpace(*dst) != "" {
return
}
*dst = strings.TrimSpace(value)
}
type xmlNode struct {
XMLName xml.Name
Text string `xml:",chardata"`
Nodes []xmlNode `xml:",any"`
}
func parseXMLRoot(content []byte) (xmlNode, bool) {
var root xmlNode
if err := xml.Unmarshal(content, &root); err != nil {
return xmlNode{}, false
}
return root, true
}
func xmlNodeFields(node xmlNode) map[string]string {
fields := make(map[string]string, len(node.Nodes))
for _, child := range node.Nodes {
fields[child.XMLName.Local] = strings.TrimSpace(child.Text)
}
return fields
}
var socketSuffixRE = regexp.MustCompile(`(?i)cpu(\d+)$`)
func parseSocketID(name string) int {
m := socketSuffixRE.FindStringSubmatch(strings.TrimSpace(name))
if len(m) != 2 {
return 0
}
v, _ := strconv.Atoi(m[1])
if v <= 0 {
return 0
}
return v
}
func parseSectionIndex(sectionName, expr string) int {
re, err := regexp.Compile(expr)
if err != nil {
return 0
}
match := re.FindStringSubmatch(sectionName)
if len(match) < 2 {
return 0
}
v, _ := strconv.Atoi(match[1])
if v <= 0 {
return 0
}
return v
}
func formatSlot(frontRear, slotNum string) string {
frontRear = strings.TrimSpace(frontRear)
slotNum = strings.TrimSpace(slotNum)
switch {
case frontRear == "" && slotNum == "":
return ""
case frontRear == "":
return slotNum
case slotNum == "":
return frontRear
default:
return frontRear + " slot " + slotNum
}
}
func parseMaybeInt(raw string) int {
raw = strings.TrimSpace(raw)
if raw == "" {
return 0
}
if strings.HasPrefix(strings.ToLower(raw), "0x") {
if v, err := strconv.ParseInt(raw[2:], 16, 64); err == nil {
return int(v)
}
}
if v, err := strconv.Atoi(raw); err == nil {
return v
}
if fv, err := strconv.ParseFloat(raw, 64); err == nil {
return int(fv)
}
return 0
}
var firstNumberRE = regexp.MustCompile(`[-+]?[0-9]+(?:\.[0-9]+)?`)
func parseMaybeIntLoose(raw string) int {
raw = strings.TrimSpace(raw)
if raw == "" {
return 0
}
if v := parseMaybeInt(raw); v > 0 {
return v
}
number := firstNumberRE.FindString(raw)
if number == "" {
return 0
}
if fv, err := strconv.ParseFloat(number, 64); err == nil {
return int(fv)
}
return 0
}
func parseMaybeFloat(raw string) (float64, bool) {
raw = strings.TrimSpace(raw)
if raw == "" || strings.EqualFold(raw, "na") {
return 0, false
}
if strings.HasPrefix(strings.ToLower(raw), "0x") {
return 0, false
}
v, err := strconv.ParseFloat(raw, 64)
if err != nil {
return 0, false
}
return v, true
}
func normalizePresenceStatus(raw string) string {
raw = strings.TrimSpace(raw)
if raw == "" {
return ""
}
if strings.Contains(strings.ToLower(raw), "presence") {
return "ok"
}
return strings.ToLower(raw)
}
func normalizeComponentStatus(raw string) string {
s := strings.ToLower(strings.TrimSpace(raw))
switch {
case s == "":
return ""
case containsAny(s, "ok", "normal", "presence", "present", "healthy", "running"):
return "ok"
case containsAny(s, "absent", "missing", "not present", "removed"):
return "absent"
case containsAny(s, "warn", "degrad", "minor"):
return "warning"
case containsAny(s, "critical", "major", "fault", "fail", "error"):
return "critical"
default:
return s
}
}
func inferStorageType(slot string) string {
return inferStorageTypeFromValues(slot, "", "", "")
}
func inferStorageTypeFromValues(slot, mediaType, protocol, model string) string {
lower := strings.ToLower(strings.TrimSpace(slot + " " + mediaType + " " + protocol + " " + model))
switch {
case strings.Contains(lower, "nvme"):
return "nvme"
case strings.Contains(lower, "ssd"):
return "ssd"
case strings.Contains(lower, "hdd"), strings.Contains(lower, "sas"), strings.Contains(lower, "sata"):
return "disk"
case strings.Contains(lower, "disk"), strings.Contains(lower, "front"), strings.Contains(lower, "rear"):
return "disk"
default:
return ""
}
}
func inferSensorType(unit, reading string) string {
lowerUnit := strings.ToLower(strings.TrimSpace(unit))
switch {
case strings.Contains(lowerUnit, "degrees"):
return "temperature"
case strings.Contains(lowerUnit, "volts"):
return "voltage"
case strings.Contains(lowerUnit, "amps"):
return "current"
case strings.Contains(lowerUnit, "watts"):
return "power"
case strings.Contains(lowerUnit, "rpm"):
return "fan"
case strings.Contains(lowerUnit, "discrete"):
return "discrete"
}
if strings.HasPrefix(strings.ToLower(strings.TrimSpace(reading)), "0x") {
return "discrete"
}
return "sensor"
}
func normalizeSensorStatus(status string) string {
status = strings.TrimSpace(status)
if status == "" {
return "unknown"
}
if strings.EqualFold(status, "na") {
return "unknown"
}
return strings.ToLower(status)
}
func isTruthy(raw string) bool {
raw = strings.ToLower(strings.TrimSpace(raw))
return raw == "yes" || raw == "y" || raw == "true" || raw == "1" || raw == "present"
}
func containsAny(s string, items ...string) bool {
for _, item := range items {
if strings.Contains(s, item) {
return true
}
}
return false
}
func normalizeStorageStatus(raw string, present bool) string {
s := strings.ToLower(strings.TrimSpace(raw))
switch {
case s == "" && present:
return "ok"
case s == "":
return "absent"
case containsAny(s, "ok", "healthy", "optimal", "normal"):
return "ok"
case containsAny(s, "missing", "absent", "removed", "not present"):
return "absent"
case containsAny(s, "predictive", "degrad", "warn"):
return "warning"
default:
return s
}
}
func normalizeVolumeStatus(raw string) string {
s := strings.ToLower(strings.TrimSpace(raw))
switch {
case s == "":
return ""
case containsAny(s, "ok", "healthy", "optimal", "normal"):
return "ok"
case containsAny(s, "degrad", "rebuild", "warn"):
return "warning"
case containsAny(s, "fail", "offline", "critical"):
return "critical"
default:
return s
}
}
func normalizeRAIDLevel(raw string) string {
s := strings.ToUpper(strings.TrimSpace(raw))
s = strings.ReplaceAll(s, " ", "")
if s == "" {
return ""
}
if strings.HasPrefix(s, "RAID") {
return s
}
if raidNumericRE.MatchString(s) {
return "RAID" + s
}
return s
}
var raidNumericRE = regexp.MustCompile(`^\d+(?:\+\d+)?$`)
var sizeValueRE = regexp.MustCompile(`(?i)^([0-9]+(?:\.[0-9]+)?)\s*([KMGTPE]?)(I?B)?$`)
func parseCapacityFields(bytesField, sizeField, sizeGBField string) (int64, int) {
if b := parseCapacityFromString(bytesField); b > 0 {
return b, int(b / 1_000_000_000)
}
if b := parseCapacityFromString(sizeField); b > 0 {
return b, int(b / 1_000_000_000)
}
sizeGB := parseMaybeInt(sizeGBField)
if sizeGB > 0 {
return int64(sizeGB) * 1_000_000_000, sizeGB
}
return 0, 0
}
func parseCapacityFromString(raw string) int64 {
clean := strings.ReplaceAll(strings.TrimSpace(raw), ",", "")
if clean == "" {
return 0
}
if strings.EqualFold(clean, "na") {
return 0
}
if v, err := strconv.ParseInt(clean, 10, 64); err == nil {
return v
}
m := sizeValueRE.FindStringSubmatch(clean)
if len(m) != 4 {
return 0
}
value, err := strconv.ParseFloat(m[1], 64)
if err != nil {
return 0
}
unit := strings.ToUpper(m[2])
suffix := strings.ToUpper(m[3])
base := float64(1000)
if strings.HasPrefix(suffix, "I") {
base = 1024
}
power := 0
switch unit {
case "K":
power = 1
case "M":
power = 2
case "G":
power = 3
case "T":
power = 4
case "P":
power = 5
case "E":
power = 6
}
mult := float64(1)
for i := 0; i < power; i++ {
mult *= base
}
return int64(value * mult)
}
func parseCapacityFromAny(v any) int64 {
switch t := v.(type) {
case nil:
return 0
case float64:
return int64(t)
case int:
return int64(t)
case int64:
return t
case json.Number:
if i, err := t.Int64(); err == nil {
return i
}
if f, err := t.Float64(); err == nil {
return int64(f)
}
return 0
case string:
return parseCapacityFromString(t)
case map[string]any:
return parseCapacityFromAny(lookupAnyCase(t, "value", "Value", "CapacityBytes", "SizeBytes"))
default:
return parseCapacityFromString(fmt.Sprintf("%v", t))
}
}
func parseLDCapacity(rec map[string]any) int64 {
base := parseCapacityFromAny(
lookupAnyCase(rec, "CapacityBytes", "SizeBytes", "Capacity", "Size", "LogicalCapacity", "LogicalCapacityBytes"),
)
if base > 0 {
return base
}
for rawKey, rawVal := range rec {
norm := normalizeKey(rawKey)
if strings.HasPrefix(norm, "logicalcapacity") || strings.HasPrefix(norm, "logicalcapicity") {
v := parseCapacityFromAny(rawVal)
if v <= 0 {
continue
}
if strings.Contains(norm, "512") {
return v * 512
}
return v
}
}
return 0
}
func parseColonKVLine(line string) (string, string, bool) {
idx := strings.Index(line, ":")
if idx <= 0 {
return "", "", false
}
key := strings.TrimSpace(line[:idx])
value := strings.TrimSpace(line[idx+1:])
if key == "" {
return "", "", false
}
return key, value, true
}
func parseFlexibleKVLine(line string) (string, string, bool) {
if key, val, ok := parseColonKVLine(line); ok {
return key, val, true
}
idx := strings.Index(line, "=")
if idx <= 0 {
return "", "", false
}
key := strings.TrimSpace(line[:idx])
value := strings.TrimSpace(line[idx+1:])
if key == "" {
return "", "", false
}
return key, value, true
}
func startsNewLogicalRecord(current map[string]string, normalizedKey string) bool {
switch normalizedKey {
case "ldid", "logicaldeviceid", "virtualdriveid":
return true
case "id":
return hasAnyNormalizedKey(current, "ldid", "logicaldeviceid", "virtualdriveid", "id")
case "name":
return hasAnyNormalizedKey(current, "name", "ldname", "logicaldevicename")
default:
return false
}
}
func hasAnyNormalizedKey(m map[string]string, keys ...string) bool {
for rawKey := range m {
n := normalizeKey(rawKey)
for _, key := range keys {
if n == key {
return true
}
}
}
return false
}
func parseKeyValueBlocks(content string) []map[string]string {
blocks := make([]map[string]string, 0)
current := make(map[string]string)
flush := func() {
if len(current) == 0 {
return
}
blocks = append(blocks, current)
current = make(map[string]string)
}
lines := strings.Split(content, "\n")
for _, rawLine := range lines {
line := strings.TrimSpace(rawLine)
if line == "" {
flush()
continue
}
if strings.HasPrefix(line, "[") && strings.HasSuffix(line, "]") {
flush()
continue
}
key, value, ok := parseFlexibleKVLine(line)
if !ok {
continue
}
nk := normalizeKey(key)
if nk == "serialnumber" && hasAnyNormalizedKey(current, "serialnumber") {
flush()
}
current[key] = value
}
flush()
return blocks
}
func findCPUIndex(items []models.CPU, target models.CPU) int {
targetSocket := target.Socket
targetPPIN := strings.ToLower(strings.TrimSpace(target.PPIN))
targetSerial := strings.ToLower(strings.TrimSpace(target.SerialNumber))
targetModel := strings.ToLower(strings.TrimSpace(target.Model))
for i := range items {
cpu := items[i]
if targetSocket > 0 && cpu.Socket > 0 && targetSocket == cpu.Socket {
return i
}
if targetSocket > 0 && cpu.Socket > 0 && targetSocket != cpu.Socket {
continue
}
ppin := strings.ToLower(strings.TrimSpace(cpu.PPIN))
if targetPPIN != "" && ppin != "" && targetPPIN == ppin {
return i
}
serial := strings.ToLower(strings.TrimSpace(cpu.SerialNumber))
if targetSerial != "" && serial != "" && targetSerial == serial {
return i
}
model := strings.ToLower(strings.TrimSpace(cpu.Model))
if targetSocket == 0 && cpu.Socket == 0 && targetModel != "" && model == targetModel {
return i
}
}
return -1
}
func mergeCPU(dst *models.CPU, src models.CPU) {
if dst.Socket == 0 && src.Socket > 0 {
dst.Socket = src.Socket
}
setStorageString(&dst.Model, src.Model)
setStorageString(&dst.Description, src.Description)
if dst.Cores == 0 && src.Cores > 0 {
dst.Cores = src.Cores
}
if dst.Threads == 0 && src.Threads > 0 {
dst.Threads = src.Threads
}
if dst.FrequencyMHz == 0 && src.FrequencyMHz > 0 {
dst.FrequencyMHz = src.FrequencyMHz
}
if dst.MaxFreqMHz == 0 && src.MaxFreqMHz > 0 {
dst.MaxFreqMHz = src.MaxFreqMHz
}
if dst.L1CacheKB == 0 && src.L1CacheKB > 0 {
dst.L1CacheKB = src.L1CacheKB
}
if dst.L2CacheKB == 0 && src.L2CacheKB > 0 {
dst.L2CacheKB = src.L2CacheKB
}
if dst.L3CacheKB == 0 && src.L3CacheKB > 0 {
dst.L3CacheKB = src.L3CacheKB
}
setStorageString(&dst.PPIN, src.PPIN)
setStorageString(&dst.SerialNumber, src.SerialNumber)
setStorageString(&dst.Status, src.Status)
}
func findMemoryIndex(items []models.MemoryDIMM, target models.MemoryDIMM) int {
targetSerial := strings.ToLower(strings.TrimSpace(target.SerialNumber))
targetSlot := strings.ToLower(strings.TrimSpace(target.Slot))
for i := range items {
serial := strings.ToLower(strings.TrimSpace(items[i].SerialNumber))
slot := strings.ToLower(strings.TrimSpace(items[i].Slot))
if targetSerial != "" && serial != "" && targetSerial == serial {
return i
}
if targetSerial == "" && targetSlot != "" && slot != "" && targetSlot == slot {
return i
}
}
return -1
}
func mergeMemoryDIMM(dst *models.MemoryDIMM, src models.MemoryDIMM) {
setStorageString(&dst.Slot, src.Slot)
setStorageString(&dst.Location, src.Location)
if src.Present {
dst.Present = true
}
if dst.SizeMB == 0 && src.SizeMB > 0 {
dst.SizeMB = src.SizeMB
}
setStorageString(&dst.Type, src.Type)
setStorageString(&dst.Technology, src.Technology)
if dst.MaxSpeedMHz == 0 && src.MaxSpeedMHz > 0 {
dst.MaxSpeedMHz = src.MaxSpeedMHz
}
if dst.CurrentSpeedMHz == 0 && src.CurrentSpeedMHz > 0 {
dst.CurrentSpeedMHz = src.CurrentSpeedMHz
}
setStorageString(&dst.Manufacturer, src.Manufacturer)
setStorageString(&dst.SerialNumber, src.SerialNumber)
setStorageString(&dst.PartNumber, src.PartNumber)
setStorageString(&dst.Status, src.Status)
if dst.Ranks == 0 && src.Ranks > 0 {
dst.Ranks = src.Ranks
}
}
func appendUniqueStorages(dst *[]models.Storage, additions []models.Storage) {
for _, add := range additions {
if isStorageEmpty(add) {
continue
}
idx := findStorageIndex(*dst, add)
if idx < 0 {
*dst = append(*dst, add)
continue
}
mergeStorage(&(*dst)[idx], add)
}
}
func dedupeStorage(items []models.Storage) []models.Storage {
out := make([]models.Storage, 0, len(items))
appendUniqueStorages(&out, items)
return out
}
func findStorageIndex(items []models.Storage, target models.Storage) int {
targetSerial := strings.ToLower(strings.TrimSpace(target.SerialNumber))
targetSlot := strings.ToLower(strings.TrimSpace(target.Slot))
for i := range items {
serial := strings.ToLower(strings.TrimSpace(items[i].SerialNumber))
slot := strings.ToLower(strings.TrimSpace(items[i].Slot))
if targetSerial != "" && serial != "" && targetSerial == serial {
return i
}
if targetSerial == "" && targetSlot != "" && slot != "" && targetSlot == slot {
return i
}
}
return -1
}
func mergeStorage(dst *models.Storage, src models.Storage) {
setStorageString(&dst.Slot, src.Slot)
if mergedType := strings.TrimSpace(pickStorageType(dst.Type, src.Type)); mergedType != "" {
dst.Type = mergedType
}
setStorageString(&dst.Model, src.Model)
if dst.SizeGB == 0 && src.SizeGB > 0 {
dst.SizeGB = src.SizeGB
}
setStorageString(&dst.SerialNumber, src.SerialNumber)
setStorageString(&dst.Manufacturer, src.Manufacturer)
setStorageString(&dst.Firmware, src.Firmware)
setStorageString(&dst.Interface, src.Interface)
if src.Present {
dst.Present = true
}
setStorageString(&dst.Location, src.Location)
setStorageString(&dst.Status, normalizeStorageStatus(src.Status, src.Present || dst.Present))
dst.Details = mergeH3CDetails(dst.Details, src.Details)
}
func setStorageString(dst *string, value string) {
value = strings.TrimSpace(value)
if value == "" {
return
}
if strings.TrimSpace(*dst) == "" {
*dst = value
return
}
if strings.EqualFold(*dst, "unknown") || strings.EqualFold(*dst, "n/a") || strings.EqualFold(*dst, "absent") {
*dst = value
}
}
func pickStorageType(current, next string) string {
if storageTypeRank(next) > storageTypeRank(current) {
return next
}
if strings.TrimSpace(current) == "" {
return next
}
return current
}
func storageTypeRank(t string) int {
switch strings.ToLower(strings.TrimSpace(t)) {
case "nvme":
return 4
case "ssd":
return 3
case "hdd", "disk":
return 2
case "unknown":
return 0
default:
if strings.TrimSpace(t) == "" {
return 0
}
return 1
}
}
func isStorageEmpty(s models.Storage) bool {
return strings.TrimSpace(s.Slot) == "" &&
strings.TrimSpace(s.SerialNumber) == "" &&
strings.TrimSpace(s.Model) == ""
}
func appendUniqueNetworkAdapters(dst *[]models.NetworkAdapter, additions []models.NetworkAdapter) {
for _, add := range additions {
if isNetworkAdapterEmpty(add) {
continue
}
idx := findNetworkAdapterIndex(*dst, add)
if idx < 0 {
*dst = append(*dst, add)
continue
}
mergeNetworkAdapter(&(*dst)[idx], add)
}
}
func isNetworkAdapterEmpty(n models.NetworkAdapter) bool {
return strings.TrimSpace(n.Slot) == "" &&
len(n.MACAddresses) == 0 &&
strings.TrimSpace(n.Model) == ""
}
func findNetworkAdapterIndex(items []models.NetworkAdapter, target models.NetworkAdapter) int {
targetSlot := strings.ToLower(strings.TrimSpace(target.Slot))
for i := range items {
if hasSharedMAC(items[i].MACAddresses, target.MACAddresses) {
return i
}
slot := strings.ToLower(strings.TrimSpace(items[i].Slot))
if targetSlot != "" && slot != "" && targetSlot == slot {
return i
}
}
return -1
}
func hasSharedMAC(a, b []string) bool {
if len(a) == 0 || len(b) == 0 {
return false
}
set := make(map[string]struct{}, len(a))
for _, item := range a {
norm := strings.ToLower(strings.TrimSpace(item))
if norm != "" {
set[norm] = struct{}{}
}
}
for _, item := range b {
norm := strings.ToLower(strings.TrimSpace(item))
if norm == "" {
continue
}
if _, ok := set[norm]; ok {
return true
}
}
return false
}
func mergeNetworkAdapter(dst *models.NetworkAdapter, src models.NetworkAdapter) {
setStorageString(&dst.Slot, src.Slot)
setStorageString(&dst.Location, src.Location)
if src.Present {
dst.Present = true
}
setStorageString(&dst.Model, src.Model)
dst.Description = mergeTextUnique(dst.Description, src.Description)
setStorageString(&dst.Vendor, src.Vendor)
if dst.VendorID == 0 && src.VendorID != 0 {
dst.VendorID = src.VendorID
}
if dst.DeviceID == 0 && src.DeviceID != 0 {
dst.DeviceID = src.DeviceID
}
setStorageString(&dst.SerialNumber, src.SerialNumber)
setStorageString(&dst.PartNumber, src.PartNumber)
setStorageString(&dst.Firmware, src.Firmware)
dst.PortCount = maxInt(dst.PortCount, src.PortCount)
setStorageString(&dst.PortType, src.PortType)
for _, mac := range src.MACAddresses {
appendStringUnique(&dst.MACAddresses, mac)
}
dst.PortCount = maxInt(dst.PortCount, len(dst.MACAddresses))
setStorageString(&dst.Status, src.Status)
}
func appendUniqueNICs(dst *[]models.NIC, additions []models.NIC) {
for _, add := range additions {
if isNICEmpty(add) {
continue
}
idx := findNICIndex(*dst, add)
if idx < 0 {
*dst = append(*dst, add)
continue
}
mergeNIC(&(*dst)[idx], add)
}
}
func isNICEmpty(n models.NIC) bool {
return strings.TrimSpace(n.Name) == "" &&
strings.TrimSpace(n.Model) == "" &&
strings.TrimSpace(n.MACAddress) == ""
}
func findNICIndex(items []models.NIC, target models.NIC) int {
targetMAC := strings.ToLower(strings.TrimSpace(target.MACAddress))
targetName := strings.ToLower(strings.TrimSpace(target.Name))
for i := range items {
mac := strings.ToLower(strings.TrimSpace(items[i].MACAddress))
if targetMAC != "" && mac != "" && targetMAC == mac {
return i
}
name := strings.ToLower(strings.TrimSpace(items[i].Name))
if targetMAC == "" && targetName != "" && name == targetName {
return i
}
}
return -1
}
func mergeNIC(dst *models.NIC, src models.NIC) {
setStorageString(&dst.Name, src.Name)
setStorageString(&dst.Model, src.Model)
dst.Description = mergeTextUnique(dst.Description, src.Description)
setStorageString(&dst.MACAddress, src.MACAddress)
if dst.SpeedMbps == 0 && src.SpeedMbps > 0 {
dst.SpeedMbps = src.SpeedMbps
}
setStorageString(&dst.SerialNumber, src.SerialNumber)
}
func mergeTextUnique(dst, src string) string {
dst = strings.TrimSpace(dst)
src = strings.TrimSpace(src)
switch {
case dst == "":
return src
case src == "":
return dst
case strings.EqualFold(dst, src):
return dst
case strings.Contains(strings.ToLower(dst), strings.ToLower(src)):
return dst
case strings.Contains(strings.ToLower(src), strings.ToLower(dst)):
return src
default:
return dst + "; " + src
}
}
func appendUniquePSUs(dst *[]models.PSU, additions []models.PSU) {
for _, add := range additions {
if isPSUEmpty(add) {
continue
}
idx := findPSUIndex(*dst, add)
if idx < 0 {
*dst = append(*dst, add)
continue
}
mergePSU(&(*dst)[idx], add)
}
}
func isPSUEmpty(p models.PSU) bool {
return strings.TrimSpace(p.Slot) == "" &&
strings.TrimSpace(p.SerialNumber) == "" &&
strings.TrimSpace(p.Model) == ""
}
func findPSUIndex(items []models.PSU, target models.PSU) int {
targetSerial := strings.ToLower(strings.TrimSpace(target.SerialNumber))
targetSlot := strings.ToLower(strings.TrimSpace(target.Slot))
for i := range items {
serial := strings.ToLower(strings.TrimSpace(items[i].SerialNumber))
slot := strings.ToLower(strings.TrimSpace(items[i].Slot))
if targetSerial != "" && serial != "" && targetSerial == serial {
return i
}
if targetSerial == "" && targetSlot != "" && slot != "" && targetSlot == slot {
return i
}
}
return -1
}
func mergePSU(dst *models.PSU, src models.PSU) {
setStorageString(&dst.Slot, src.Slot)
if src.Present {
dst.Present = true
}
setStorageString(&dst.Model, src.Model)
setStorageString(&dst.Description, src.Description)
setStorageString(&dst.Vendor, src.Vendor)
if dst.WattageW == 0 && src.WattageW > 0 {
dst.WattageW = src.WattageW
}
setStorageString(&dst.SerialNumber, src.SerialNumber)
setStorageString(&dst.PartNumber, src.PartNumber)
setStorageString(&dst.Firmware, src.Firmware)
setStorageString(&dst.Status, src.Status)
dst.Details = mergeH3CDetails(dst.Details, src.Details)
}
func mergeH3CDetails(primary, secondary map[string]any) map[string]any {
if len(secondary) == 0 {
return primary
}
if primary == nil {
primary = make(map[string]any, len(secondary))
}
for key, value := range secondary {
if _, ok := primary[key]; !ok {
primary[key] = value
}
}
return primary
}
func dedupeVolumes(items []models.StorageVolume) []models.StorageVolume {
out := make([]models.StorageVolume, 0, len(items))
for _, item := range items {
if isVolumeEmpty(item) {
continue
}
idx := findVolumeIndex(out, item)
if idx < 0 {
out = append(out, item)
continue
}
mergeVolume(&out[idx], item)
}
return out
}
func findVolumeIndex(items []models.StorageVolume, target models.StorageVolume) int {
targetID := strings.ToLower(strings.TrimSpace(target.ID))
targetName := strings.ToLower(strings.TrimSpace(target.Name))
targetController := strings.ToLower(strings.TrimSpace(target.Controller))
for i := range items {
id := strings.ToLower(strings.TrimSpace(items[i].ID))
name := strings.ToLower(strings.TrimSpace(items[i].Name))
controller := strings.ToLower(strings.TrimSpace(items[i].Controller))
if targetID != "" && id != "" && targetID == id {
if !controllersCompatible(targetController, controller) {
continue
}
return i
}
if targetName != "" && name == targetName {
if controllersCompatible(targetController, controller) {
return i
}
}
if targetID == "" && targetName != "" && name == targetName && controller == targetController {
return i
}
}
return -1
}
func controllersCompatible(a, b string) bool {
a = strings.ToLower(strings.TrimSpace(a))
b = strings.ToLower(strings.TrimSpace(b))
if a == "" || b == "" {
return true
}
if a == b {
return true
}
if strings.Contains(a, b) || strings.Contains(b, a) {
return true
}
return false
}
func mergeVolume(dst *models.StorageVolume, src models.StorageVolume) {
setStorageString(&dst.ID, src.ID)
setStorageString(&dst.Name, src.Name)
setStorageString(&dst.Controller, src.Controller)
setStorageString(&dst.RAIDLevel, src.RAIDLevel)
if dst.SizeGB == 0 && src.SizeGB > 0 {
dst.SizeGB = src.SizeGB
}
if dst.CapacityBytes == 0 && src.CapacityBytes > 0 {
dst.CapacityBytes = src.CapacityBytes
}
setStorageString(&dst.Status, src.Status)
if src.Bootable {
dst.Bootable = true
}
if src.Encrypted {
dst.Encrypted = true
}
}
func isVolumeEmpty(v models.StorageVolume) bool {
return strings.TrimSpace(v.ID) == "" &&
strings.TrimSpace(v.Name) == "" &&
strings.TrimSpace(v.RAIDLevel) == "" &&
v.CapacityBytes == 0 &&
v.SizeGB == 0
}
func lookupAnyCase(m map[string]any, keys ...string) any {
if len(m) == 0 {
return nil
}
for _, key := range keys {
norm := normalizeKey(key)
for rawKey, value := range m {
if normalizeKey(rawKey) == norm {
return value
}
}
}
return nil
}
func lookupAnyByPrefix(m map[string]any, prefixes ...string) any {
if len(m) == 0 {
return nil
}
normPrefixes := make([]string, 0, len(prefixes))
for _, prefix := range prefixes {
n := normalizeKey(prefix)
if n != "" {
normPrefixes = append(normPrefixes, n)
}
}
for rawKey, value := range m {
normKey := normalizeKey(rawKey)
for _, prefix := range normPrefixes {
if strings.HasPrefix(normKey, prefix) {
return value
}
}
}
return nil
}
func toAnyMap(v any) map[string]any {
if v == nil {
return nil
}
if m, ok := v.(map[string]any); ok {
return m
}
return nil
}
func toAnySlice(v any) []any {
if v == nil {
return nil
}
switch t := v.(type) {
case []any:
return t
case []map[string]any:
out := make([]any, 0, len(t))
for _, item := range t {
out = append(out, item)
}
return out
default:
return nil
}
}
func toStringAny(v any) string {
switch t := v.(type) {
case nil:
return ""
case string:
return strings.TrimSpace(t)
case fmt.Stringer:
return strings.TrimSpace(t.String())
case float64:
return strconv.FormatInt(int64(t), 10)
case int:
return strconv.Itoa(t)
case int64:
return strconv.FormatInt(t, 10)
case bool:
if t {
return "true"
}
return "false"
case json.Number:
return t.String()
default:
return strings.TrimSpace(fmt.Sprintf("%v", t))
}
}
func toBoolAny(v any) (bool, bool) {
switch t := v.(type) {
case nil:
return false, false
case bool:
return t, true
case string:
return parseBoolString(t)
case float64:
return t != 0, true
case int:
return t != 0, true
case int64:
return t != 0, true
case json.Number:
if i, err := t.Int64(); err == nil {
return i != 0, true
}
if f, err := t.Float64(); err == nil {
return f != 0, true
}
return false, false
default:
return parseBoolString(fmt.Sprintf("%v", t))
}
}
func parseBoolString(raw string) (bool, bool) {
switch strings.ToLower(strings.TrimSpace(raw)) {
case "1", "true", "yes", "y", "enabled", "on":
return true, true
case "0", "false", "no", "n", "disabled", "off":
return false, true
default:
return false, false
}
}
func firstNonEmpty(values ...string) string {
for _, v := range values {
v = strings.TrimSpace(v)
if v != "" {
return v
}
}
return ""
}
func minInt(a, b int) int {
if a < b {
return a
}
return b
}
func maxInt(a, b int) int {
if a > b {
return a
}
return b
}