// 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 = "1.0.0" parserVersionG6 = "1.0.0" ) 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) 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) 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(¤t.IPv4, trimmed, netCfgInet4OldRE) appendMatchesUnique(¤t.IPv4, trimmed, netCfgInet4NewRE) appendMatchesUnique(¤t.IPv6, strings.ToLower(trimmed), netCfgInet6OldRE) appendMatchesUnique(¤t.IPv6, strings.ToLower(trimmed), netCfgInet6NewRE) if currentName != "" { appendStringUnique(¤t.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)) } 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) } 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 }