Files
logpile/internal/parser/vendors/inspur/component.go
Mikhail Chusavitin 4f7b5b826a fix(inspur): fix PSU section regex when PCIE section precedes Network
The PSU regex used "RESTful Network" as its end anchor, but in standard
Inspur component.log layout the PCIE Device section sits between PSU and
Network Adapter. The lazy [\s\S]*? captured across the PCIE error block,
producing invalid JSON and silently dropping all PSU data.

Changed anchor to RESTful (?:PCIE|Network) — matches whichever section
immediately follows PSU in a given archive.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-21 14:21:13 +03:00

1242 lines
34 KiB
Go

package inspur
import (
"encoding/json"
"fmt"
"regexp"
"strings"
"time"
"git.mchus.pro/mchus/logpile/internal/models"
"git.mchus.pro/mchus/logpile/internal/parser/vendors/pciids"
)
// ParseComponentLog parses component.log file and extracts detailed hardware info
func ParseComponentLog(content []byte, hw *models.HardwareConfig) {
if hw == nil {
return
}
text := string(content)
// Parse RESTful CPU info — fallback when asset.json is absent
if len(hw.CPUs) == 0 {
parseCPUInfo(text, hw)
}
// Parse RESTful Memory info (detailed memory data)
parseMemoryInfo(text, hw)
// Parse RESTful PSU info
parsePSUInfo(text, hw)
// Parse RESTful HDD info
parseHDDInfo(text, hw)
// Parse RESTful diskbackplane info
parseDiskBackplaneInfo(text, hw)
// Parse RESTful Network Adapter info
parseNetworkAdapterInfo(text, hw)
// Extract firmware from all components
extractComponentFirmware(text, hw)
}
// ParseComponentLogEvents extracts events from component.log (memory errors, etc.)
func ParseComponentLogEvents(content []byte) []models.Event {
var events []models.Event
text := string(content)
// Parse RESTful Memory info for Warning/Error status
memEvents := parseMemoryEvents(text)
events = append(events, memEvents...)
events = append(events, parseFanEvents(text)...)
return events
}
// ParseComponentLogSensors extracts sensor readings from component.log JSON sections.
func ParseComponentLogSensors(content []byte) []models.SensorReading {
text := string(content)
var out []models.SensorReading
out = append(out, parseFanSensors(text)...)
out = append(out, parseDiskBackplaneSensors(text)...)
out = append(out, parsePSUSummarySensors(text)...)
return out
}
// CPURESTInfo represents the RESTful CPU info structure in component.log
type CPURESTInfo struct {
Processors []struct {
ProcID int `json:"proc_id"`
CPUID string `json:"PROC_ID"` // uppercase key — prevents case-insensitive collision with proc_id
Manufacturer string `json:"Manufacturer"`
MaxSpeedMHz int `json:"MaxSpeedMHz"`
ConfigStatus int `json:"configStatus"`
ProcName string `json:"proc_name"`
ProcStatus int `json:"proc_status"`
ProcSpeed int `json:"proc_speed"`
CoreCount int `json:"proc_core_count"`
ThreadCount int `json:"proc_thread_count"`
TDP int `json:"proc_tdp"`
L1Cache int `json:"proc_l1cache_size"`
L2Cache int `json:"proc_l2cache_size"`
L3Cache int `json:"proc_l3cache_size"`
MicroCode string `json:"micro_code"`
PPIN string `json:"ppin"`
Status string `json:"status"`
} `json:"processors"`
}
func parseCPUInfo(text string, hw *models.HardwareConfig) {
re := regexp.MustCompile(`RESTful CPU info:\s*(\{[\s\S]*?\})\s*RESTful Memory`)
match := re.FindStringSubmatch(text)
if match == nil {
return
}
jsonStr := strings.ReplaceAll(match[1], "\n", "")
var cpuInfo CPURESTInfo
if err := json.Unmarshal([]byte(jsonStr), &cpuInfo); err != nil {
return
}
for _, proc := range cpuInfo.Processors {
if proc.ProcStatus != 1 && proc.ConfigStatus != 1 {
continue
}
hw.CPUs = append(hw.CPUs, models.CPU{
Socket: proc.ProcID,
Model: strings.TrimSpace(proc.ProcName),
Cores: proc.CoreCount,
Threads: proc.ThreadCount,
FrequencyMHz: proc.ProcSpeed,
MaxFreqMHz: proc.MaxSpeedMHz,
L1CacheKB: proc.L1Cache,
L2CacheKB: proc.L2Cache,
L3CacheKB: proc.L3Cache,
TDP: proc.TDP,
PPIN: proc.PPIN,
})
if proc.MicroCode != "" {
hw.Firmware = append(hw.Firmware, models.FirmwareInfo{
DeviceName: fmt.Sprintf("CPU%d Microcode", proc.ProcID),
Version: proc.MicroCode,
})
}
}
}
// MemoryRESTInfo represents the RESTful Memory info structure
type MemoryRESTInfo struct {
MemModules []struct {
MemModID int `json:"mem_mod_id"`
ConfigStatus int `json:"config_status"`
MemModSlot string `json:"mem_mod_slot"`
MemModStatus int `json:"mem_mod_status"`
MemModSize int `json:"mem_mod_size"`
MemModType string `json:"mem_mod_type"`
MemModTechnology string `json:"mem_mod_technology"`
MemModFrequency int `json:"mem_mod_frequency"`
MemModCurrentFreq int `json:"mem_mod_current_frequency"`
MemModVendor string `json:"mem_mod_vendor"`
MemModPartNum string `json:"mem_mod_part_num"`
MemModSerial string `json:"mem_mod_serial_num"`
MemModRanks int `json:"mem_mod_ranks"`
Status string `json:"status"`
} `json:"mem_modules"`
TotalMemoryCount int `json:"total_memory_count"`
PresentMemoryCount int `json:"present_memory_count"`
MemTotalMemSize int `json:"mem_total_mem_size"`
}
func parseMemoryInfo(text string, hw *models.HardwareConfig) {
// Find RESTful Memory info section
re := regexp.MustCompile(`RESTful Memory info:\s*(\{[\s\S]*?\})\s*RESTful HDD`)
match := re.FindStringSubmatch(text)
if match == nil {
return
}
jsonStr := match[1]
jsonStr = strings.ReplaceAll(jsonStr, "\n", "")
var memInfo MemoryRESTInfo
if err := json.Unmarshal([]byte(jsonStr), &memInfo); err != nil {
return
}
var merged []models.MemoryDIMM
seen := make(map[string]int)
for _, existing := range hw.Memory {
key := inspurMemoryKey(existing)
if key == "" {
continue
}
seen[key] = len(merged)
merged = append(merged, existing)
}
for _, mem := range memInfo.MemModules {
item := models.MemoryDIMM{
Slot: mem.MemModSlot,
Location: mem.MemModSlot,
// status=1 with a known serial/part is definitely present even if BMC reports size=0
Present: mem.MemModStatus == 1 && (mem.MemModSize > 0 || strings.TrimSpace(mem.MemModSerial) != "" || strings.TrimSpace(mem.MemModPartNum) != ""),
SizeMB: mem.MemModSize * 1024, // Convert GB to MB
Type: mem.MemModType,
Technology: strings.TrimSpace(mem.MemModTechnology),
MaxSpeedMHz: mem.MemModFrequency,
CurrentSpeedMHz: mem.MemModCurrentFreq,
Manufacturer: mem.MemModVendor,
SerialNumber: mem.MemModSerial,
PartNumber: strings.TrimSpace(mem.MemModPartNum),
Status: mem.Status,
Ranks: mem.MemModRanks,
}
key := inspurMemoryKey(item)
if idx, ok := seen[key]; ok {
mergeInspurMemoryDIMM(&merged[idx], item)
continue
}
if key != "" {
seen[key] = len(merged)
}
merged = append(merged, item)
}
// If a present DIMM has size=0 (BMC firmware glitch), infer size from
// another present DIMM with the same part number in the same batch.
partSize := make(map[string]int)
for _, m := range merged {
if m.Present && m.SizeMB > 0 && strings.TrimSpace(m.PartNumber) != "" {
partSize[strings.TrimSpace(m.PartNumber)] = m.SizeMB
}
}
for i := range merged {
if merged[i].Present && merged[i].SizeMB == 0 {
if pn := strings.TrimSpace(merged[i].PartNumber); pn != "" {
if sz, ok := partSize[pn]; ok {
merged[i].SizeMB = sz
}
}
}
}
hw.Memory = merged
}
// PSURESTInfo represents the RESTful PSU info structure
type PSURESTInfo struct {
PowerSupplies []struct {
ID int `json:"id"`
Present int `json:"present"`
VendorID string `json:"vendor_id"`
Model string `json:"model"`
SerialNum string `json:"serial_num"`
PartNum string `json:"part_num"`
FwVer string `json:"fw_ver"`
InputType string `json:"input_type"`
Status string `json:"status"`
RatedPower int `json:"rated_power"`
PSInPower int `json:"ps_in_power"`
PSOutPower int `json:"ps_out_power"`
PSInVolt float64 `json:"ps_in_volt"`
PSOutVolt float64 `json:"ps_out_volt"`
PSUMaxTemp int `json:"psu_max_temperature"`
} `json:"power_supplies"`
PresentPowerReading int `json:"present_power_reading"`
}
func parsePSUInfo(text string, hw *models.HardwareConfig) {
// Find RESTful PSU info section
re := regexp.MustCompile(`RESTful PSU info:\s*(\{[\s\S]*?\})\s*RESTful (?:PCIE|Network)`)
match := re.FindStringSubmatch(text)
if match == nil {
return
}
jsonStr := match[1]
jsonStr = strings.ReplaceAll(jsonStr, "\n", "")
var psuInfo PSURESTInfo
if err := json.Unmarshal([]byte(jsonStr), &psuInfo); err != nil {
return
}
var merged []models.PSU
seen := make(map[string]int)
for _, existing := range hw.PowerSupply {
key := inspurPSUKey(existing)
if key == "" {
continue
}
seen[key] = len(merged)
merged = append(merged, existing)
}
for _, psu := range psuInfo.PowerSupplies {
item := models.PSU{
Slot: fmt.Sprintf("PSU%d", psu.ID),
Present: psu.Present == 1,
Model: strings.TrimSpace(psu.Model),
Vendor: strings.TrimSpace(psu.VendorID),
WattageW: psu.RatedPower,
SerialNumber: strings.TrimSpace(psu.SerialNum),
PartNumber: strings.TrimSpace(psu.PartNum),
Firmware: psu.FwVer,
Status: psu.Status,
InputType: psu.InputType,
InputPowerW: psu.PSInPower,
OutputPowerW: psu.PSOutPower,
InputVoltage: psu.PSInVolt,
OutputVoltage: psu.PSOutVolt,
TemperatureC: psu.PSUMaxTemp,
}
key := inspurPSUKey(item)
if idx, ok := seen[key]; ok {
mergeInspurPSU(&merged[idx], item)
continue
}
if key != "" {
seen[key] = len(merged)
}
merged = append(merged, item)
}
hw.PowerSupply = merged
}
// HDDRESTInfo represents the RESTful HDD info structure
type HDDRESTInfo []struct {
ID int `json:"id"`
Present int `json:"present"`
Enable int `json:"enable"`
SN string `json:"SN"`
Model string `json:"model"`
Capacity int `json:"capacity"`
Manufacture string `json:"manufacture"`
Firmware string `json:"firmware"`
LocationString string `json:"locationstring"`
CapableSpeed int `json:"capablespeed"`
}
func parseHDDInfo(text string, hw *models.HardwareConfig) {
// Find RESTful HDD info section
re := regexp.MustCompile(`RESTful HDD info:\s*(\[[\s\S]*?\])\s*RESTful PSU`)
match := re.FindStringSubmatch(text)
if match == nil {
return
}
jsonStr := match[1]
jsonStr = strings.ReplaceAll(jsonStr, "\n", "")
var hddInfo HDDRESTInfo
if err := json.Unmarshal([]byte(jsonStr), &hddInfo); err != nil {
return
}
// Update storage with detailed info (merge with existing data from asset.json)
hddMap := make(map[string]struct {
SN string
Model string
Firmware string
Mfr string
})
for _, hdd := range hddInfo {
if hdd.Present == 1 {
slot := strings.TrimSpace(hdd.LocationString)
if slot == "" {
slot = fmt.Sprintf("HDD%d", hdd.ID)
}
hddMap[slot] = struct {
SN string
Model string
Firmware string
Mfr string
}{
SN: normalizeRedisValue(hdd.SN),
Model: strings.TrimSpace(hdd.Model),
Firmware: normalizeRedisValue(hdd.Firmware),
Mfr: strings.TrimSpace(hdd.Manufacture),
}
}
}
// Merge into existing inventory first (asset/other sections).
for i := range hw.Storage {
slot := strings.TrimSpace(hw.Storage[i].Slot)
if slot == "" {
continue
}
detail, ok := hddMap[slot]
if !ok {
continue
}
if normalizeRedisValue(hw.Storage[i].SerialNumber) == "" {
hw.Storage[i].SerialNumber = detail.SN
}
if hw.Storage[i].Model == "" {
hw.Storage[i].Model = detail.Model
}
if normalizeRedisValue(hw.Storage[i].Firmware) == "" {
hw.Storage[i].Firmware = detail.Firmware
}
if hw.Storage[i].Manufacturer == "" {
hw.Storage[i].Manufacturer = detail.Mfr
}
hw.Storage[i].Present = true
}
// If storage is empty, populate from HDD info
if len(hw.Storage) == 0 {
for _, hdd := range hddInfo {
if hdd.Present != 1 {
continue
}
storType := "HDD"
model := strings.TrimSpace(hdd.Model)
if strings.Contains(strings.ToUpper(model), "SSD") || strings.Contains(model, "MZ7") {
storType = "SSD"
}
iface := "SATA"
if hdd.CapableSpeed == 12 {
iface = "SAS"
}
slot := strings.TrimSpace(hdd.LocationString)
if slot == "" {
slot = fmt.Sprintf("HDD%d", hdd.ID)
}
hw.Storage = append(hw.Storage, models.Storage{
Slot: slot,
Type: storType,
Model: model,
SizeGB: hdd.Capacity,
SerialNumber: normalizeRedisValue(hdd.SN),
Manufacturer: extractStorageManufacturer(model),
Firmware: normalizeRedisValue(hdd.Firmware),
Interface: iface,
Present: true,
})
}
}
}
// FanRESTInfo represents the RESTful fan info structure.
type FanRESTInfo struct {
Fans []struct {
ID int `json:"id"`
FanName string `json:"fan_name"`
Present string `json:"present"`
Status string `json:"status"`
StatusStr string `json:"status_str"`
SpeedRPM int `json:"speed_rpm"`
SpeedPercent int `json:"speed_percent"`
MaxSpeedRPM int `json:"max_speed_rpm"`
FanModel string `json:"fan_model"`
} `json:"fans"`
FansPower int `json:"fans_power"`
}
// NetworkAdapterRESTInfo represents the RESTful Network Adapter info structure
type NetworkAdapterRESTInfo struct {
SysAdapters []struct {
ID int `json:"id"`
Name string `json:"name"`
Location string `json:"Location"`
Present int `json:"present"`
Slot int `json:"slot"`
VendorID int `json:"vendor_id"`
DeviceID int `json:"device_id"`
Vendor string `json:"vendor"`
Model string `json:"model"`
FwVer string `json:"fw_ver"`
Status string `json:"status"`
SN string `json:"sn"`
PN string `json:"pn"`
PortNum int `json:"port_num"`
PortType string `json:"port_type"`
Ports []struct {
ID int `json:"id"`
MacAddr string `json:"mac_addr"`
} `json:"ports"`
} `json:"sys_adapters"`
}
func parseNetworkAdapterInfo(text string, hw *models.HardwareConfig) {
// Find RESTful Network Adapter info section
re := regexp.MustCompile(`RESTful Network Adapter info:\s*(\{[\s\S]*?\})\s*RESTful fan`)
match := re.FindStringSubmatch(text)
if match == nil {
return
}
jsonStr := match[1]
jsonStr = strings.ReplaceAll(jsonStr, "\n", "")
var netInfo NetworkAdapterRESTInfo
if err := json.Unmarshal([]byte(jsonStr), &netInfo); err != nil {
return
}
var merged []models.NetworkAdapter
seen := make(map[string]int)
for _, existing := range hw.NetworkAdapters {
key := inspurNICKey(existing)
if key == "" {
continue
}
seen[key] = len(merged)
merged = append(merged, existing)
}
for _, adapter := range netInfo.SysAdapters {
var macs []string
for _, port := range adapter.Ports {
if port.MacAddr != "" {
macs = append(macs, port.MacAddr)
}
}
model := normalizeModelLabel(adapter.Model)
if model == "" || looksLikeRawDeviceID(model) {
if resolved := normalizeModelLabel(pciids.DeviceName(adapter.VendorID, adapter.DeviceID)); resolved != "" {
model = resolved
}
}
vendor := normalizeModelLabel(adapter.Vendor)
if vendor == "" {
vendor = normalizeModelLabel(pciids.VendorName(adapter.VendorID))
}
item := models.NetworkAdapter{
Slot: fmt.Sprintf("Slot %d", adapter.Slot),
Location: adapter.Location,
Present: adapter.Present == 1,
Model: model,
Vendor: vendor,
VendorID: adapter.VendorID,
DeviceID: adapter.DeviceID,
SerialNumber: normalizeRedisValue(adapter.SN),
PartNumber: normalizeRedisValue(adapter.PN),
Firmware: normalizeRedisValue(adapter.FwVer),
PortCount: adapter.PortNum,
PortType: adapter.PortType,
MACAddresses: macs,
Status: adapter.Status,
}
key := inspurNICKey(item)
if idx, ok := seen[key]; ok {
mergeInspurNIC(&merged[idx], item)
continue
}
if slotIdx := inspurFindNICBySlot(merged, item.Slot); slotIdx >= 0 {
mergeInspurNIC(&merged[slotIdx], item)
if key != "" {
seen[key] = slotIdx
}
continue
}
if key != "" {
seen[key] = len(merged)
}
merged = append(merged, item)
}
hw.NetworkAdapters = merged
}
func inspurMemoryKey(item models.MemoryDIMM) string {
return strings.ToLower(strings.TrimSpace(inspurFirstNonEmpty(item.SerialNumber, item.Slot, item.Location)))
}
func mergeInspurMemoryDIMM(dst *models.MemoryDIMM, src models.MemoryDIMM) {
if dst == nil {
return
}
if strings.TrimSpace(dst.Slot) == "" {
dst.Slot = src.Slot
}
if strings.TrimSpace(dst.Location) == "" {
dst.Location = src.Location
}
dst.Present = dst.Present || src.Present
if dst.SizeMB == 0 {
dst.SizeMB = src.SizeMB
}
if strings.TrimSpace(dst.Type) == "" {
dst.Type = src.Type
}
if strings.TrimSpace(dst.Technology) == "" {
dst.Technology = src.Technology
}
if dst.MaxSpeedMHz == 0 {
dst.MaxSpeedMHz = src.MaxSpeedMHz
}
if dst.CurrentSpeedMHz == 0 {
dst.CurrentSpeedMHz = src.CurrentSpeedMHz
}
if strings.TrimSpace(dst.Manufacturer) == "" {
dst.Manufacturer = src.Manufacturer
}
if strings.TrimSpace(dst.SerialNumber) == "" {
dst.SerialNumber = src.SerialNumber
}
if strings.TrimSpace(dst.PartNumber) == "" {
dst.PartNumber = src.PartNumber
}
if strings.TrimSpace(dst.Status) == "" {
dst.Status = src.Status
}
if dst.Ranks == 0 {
dst.Ranks = src.Ranks
}
}
func inspurPSUKey(item models.PSU) string {
return strings.ToLower(strings.TrimSpace(inspurFirstNonEmpty(item.SerialNumber, item.Slot, item.Model)))
}
func mergeInspurPSU(dst *models.PSU, src models.PSU) {
if dst == nil {
return
}
if strings.TrimSpace(dst.Slot) == "" {
dst.Slot = src.Slot
}
dst.Present = dst.Present || src.Present
if strings.TrimSpace(dst.Model) == "" {
dst.Model = src.Model
}
if strings.TrimSpace(dst.Vendor) == "" {
dst.Vendor = src.Vendor
}
if dst.WattageW == 0 {
dst.WattageW = src.WattageW
}
if strings.TrimSpace(dst.SerialNumber) == "" {
dst.SerialNumber = src.SerialNumber
}
if strings.TrimSpace(dst.PartNumber) == "" {
dst.PartNumber = src.PartNumber
}
if strings.TrimSpace(dst.Firmware) == "" {
dst.Firmware = src.Firmware
}
if strings.TrimSpace(dst.Status) == "" {
dst.Status = src.Status
}
if strings.TrimSpace(dst.InputType) == "" {
dst.InputType = src.InputType
}
if dst.InputPowerW == 0 {
dst.InputPowerW = src.InputPowerW
}
if dst.OutputPowerW == 0 {
dst.OutputPowerW = src.OutputPowerW
}
if dst.InputVoltage == 0 {
dst.InputVoltage = src.InputVoltage
}
if dst.OutputVoltage == 0 {
dst.OutputVoltage = src.OutputVoltage
}
if dst.TemperatureC == 0 {
dst.TemperatureC = src.TemperatureC
}
}
func inspurNICKey(item models.NetworkAdapter) string {
return strings.ToLower(strings.TrimSpace(inspurFirstNonEmpty(item.SerialNumber, strings.Join(item.MACAddresses, ","), item.Slot, item.Location)))
}
func mergeInspurNIC(dst *models.NetworkAdapter, src models.NetworkAdapter) {
if dst == nil {
return
}
if strings.TrimSpace(dst.Slot) == "" {
dst.Slot = src.Slot
}
if strings.TrimSpace(dst.Location) == "" {
dst.Location = src.Location
}
dst.Present = dst.Present || src.Present
if strings.TrimSpace(dst.BDF) == "" {
dst.BDF = src.BDF
}
if strings.TrimSpace(dst.Model) == "" {
dst.Model = src.Model
}
if strings.TrimSpace(dst.Description) == "" {
dst.Description = src.Description
}
if strings.TrimSpace(dst.Vendor) == "" {
dst.Vendor = src.Vendor
}
if dst.VendorID == 0 {
dst.VendorID = src.VendorID
}
if dst.DeviceID == 0 {
dst.DeviceID = src.DeviceID
}
if strings.TrimSpace(dst.SerialNumber) == "" {
dst.SerialNumber = src.SerialNumber
}
if strings.TrimSpace(dst.PartNumber) == "" {
dst.PartNumber = src.PartNumber
}
if strings.TrimSpace(dst.Firmware) == "" {
dst.Firmware = src.Firmware
}
if dst.PortCount == 0 {
dst.PortCount = src.PortCount
}
if strings.TrimSpace(dst.PortType) == "" {
dst.PortType = src.PortType
}
if dst.LinkWidth == 0 {
dst.LinkWidth = src.LinkWidth
}
if strings.TrimSpace(dst.LinkSpeed) == "" {
dst.LinkSpeed = src.LinkSpeed
}
if dst.MaxLinkWidth == 0 {
dst.MaxLinkWidth = src.MaxLinkWidth
}
if strings.TrimSpace(dst.MaxLinkSpeed) == "" {
dst.MaxLinkSpeed = src.MaxLinkSpeed
}
if dst.NUMANode == 0 {
dst.NUMANode = src.NUMANode
}
if strings.TrimSpace(dst.Status) == "" {
dst.Status = src.Status
}
for _, mac := range src.MACAddresses {
mac = strings.TrimSpace(mac)
if mac == "" {
continue
}
found := false
for _, existing := range dst.MACAddresses {
if strings.EqualFold(strings.TrimSpace(existing), mac) {
found = true
break
}
}
if !found {
dst.MACAddresses = append(dst.MACAddresses, mac)
}
}
}
func inspurFindNICBySlot(items []models.NetworkAdapter, slot string) int {
slot = strings.ToLower(strings.TrimSpace(slot))
if slot == "" {
return -1
}
for i := range items {
if strings.ToLower(strings.TrimSpace(items[i].Slot)) == slot {
return i
}
}
return -1
}
func inspurFirstNonEmpty(values ...string) string {
for _, value := range values {
if strings.TrimSpace(value) != "" {
return strings.TrimSpace(value)
}
}
return ""
}
func parseFanSensors(text string) []models.SensorReading {
re := regexp.MustCompile(`RESTful fan info:\s*(\{[\s\S]*?\})\s*RESTful diskbackplane`)
match := re.FindStringSubmatch(text)
if match == nil {
return nil
}
jsonStr := strings.ReplaceAll(match[1], "\n", "")
var fanInfo FanRESTInfo
if err := json.Unmarshal([]byte(jsonStr), &fanInfo); err != nil {
return nil
}
out := make([]models.SensorReading, 0, len(fanInfo.Fans)+1)
for _, fan := range fanInfo.Fans {
name := strings.TrimSpace(fan.FanName)
if name == "" {
name = fmt.Sprintf("FAN%d", fan.ID)
}
status := normalizeComponentStatus(fan.StatusStr, fan.Status, fan.Present)
raw := fmt.Sprintf("rpm=%d pct=%d model=%s max_rpm=%d", fan.SpeedRPM, fan.SpeedPercent, fan.FanModel, fan.MaxSpeedRPM)
out = append(out, models.SensorReading{
Name: name,
Type: "fan_speed",
Value: float64(fan.SpeedRPM),
Unit: "RPM",
RawValue: raw,
Status: status,
})
}
if fanInfo.FansPower > 0 {
out = append(out, models.SensorReading{
Name: "Fans_Power",
Type: "power",
Value: float64(fanInfo.FansPower),
Unit: "W",
RawValue: fmt.Sprintf("%d", fanInfo.FansPower),
Status: "OK",
})
}
return out
}
func parseFanEvents(text string) []models.Event {
re := regexp.MustCompile(`RESTful fan info:\s*(\{[\s\S]*?\})\s*RESTful diskbackplane`)
match := re.FindStringSubmatch(text)
if match == nil {
return nil
}
jsonStr := strings.ReplaceAll(match[1], "\n", "")
var fanInfo FanRESTInfo
if err := json.Unmarshal([]byte(jsonStr), &fanInfo); err != nil {
return nil
}
var events []models.Event
for _, fan := range fanInfo.Fans {
status := normalizeComponentStatus(fan.StatusStr, fan.Status, fan.Present)
if isHealthyComponentStatus(status) {
continue
}
name := strings.TrimSpace(fan.FanName)
if name == "" {
name = fmt.Sprintf("FAN%d", fan.ID)
}
severity := models.SeverityWarning
lowStatus := strings.ToLower(status)
if strings.Contains(lowStatus, "critical") || strings.Contains(lowStatus, "fail") || strings.Contains(lowStatus, "error") {
severity = models.SeverityCritical
}
events = append(events, models.Event{
ID: fmt.Sprintf("fan_%d_status", fan.ID),
Timestamp: time.Now(),
Source: "Fan",
SensorType: "fan",
SensorName: name,
EventType: "Fan Status",
Severity: severity,
Description: fmt.Sprintf("%s reports %s", name, status),
RawData: fmt.Sprintf("rpm=%d pct=%d model=%s", fan.SpeedRPM, fan.SpeedPercent, fan.FanModel),
})
}
return events
}
func parseDiskBackplaneSensors(text string) []models.SensorReading {
re := regexp.MustCompile(`RESTful diskbackplane info:\s*(\[[\s\S]*?\])\s*BMC`)
match := re.FindStringSubmatch(text)
if match == nil {
return nil
}
jsonStr := strings.ReplaceAll(match[1], "\n", "")
var backplaneInfo DiskBackplaneRESTInfo
if err := json.Unmarshal([]byte(jsonStr), &backplaneInfo); err != nil {
return nil
}
out := make([]models.SensorReading, 0, len(backplaneInfo))
for _, bp := range backplaneInfo {
if bp.Present != 1 {
continue
}
name := fmt.Sprintf("Backplane%d_Temp", bp.BackplaneIndex)
status := "OK"
if bp.Temperature <= 0 {
status = "unknown"
}
raw := fmt.Sprintf("front=%d ports=%d drives=%d cpld=%s", bp.Front, bp.PortCount, bp.DriverCount, bp.CPLDVersion)
out = append(out, models.SensorReading{
Name: name,
Type: "temperature",
Value: float64(bp.Temperature),
Unit: "C",
RawValue: raw,
Status: status,
})
}
return out
}
func parsePSUSummarySensors(text string) []models.SensorReading {
re := regexp.MustCompile(`RESTful PSU info:\s*(\{[\s\S]*?\})\s*RESTful (?:PCIE|Network)`)
match := re.FindStringSubmatch(text)
if match == nil {
return nil
}
jsonStr := strings.ReplaceAll(match[1], "\n", "")
var psuInfo PSURESTInfo
if err := json.Unmarshal([]byte(jsonStr), &psuInfo); err != nil {
return nil
}
out := make([]models.SensorReading, 0, len(psuInfo.PowerSupplies)*3+1)
if psuInfo.PresentPowerReading > 0 {
out = append(out, models.SensorReading{
Name: "PSU_Present_Power_Reading",
Type: "power",
Value: float64(psuInfo.PresentPowerReading),
Unit: "W",
RawValue: fmt.Sprintf("%d", psuInfo.PresentPowerReading),
Status: "OK",
})
}
for _, psu := range psuInfo.PowerSupplies {
if psu.Present != 1 {
continue
}
status := normalizeComponentStatus(psu.Status)
out = append(out, models.SensorReading{
Name: fmt.Sprintf("PSU%d_InputPower", psu.ID),
Type: "power",
Value: float64(psu.PSInPower),
Unit: "W",
RawValue: fmt.Sprintf("%d", psu.PSInPower),
Status: status,
})
out = append(out, models.SensorReading{
Name: fmt.Sprintf("PSU%d_OutputPower", psu.ID),
Type: "power",
Value: float64(psu.PSOutPower),
Unit: "W",
RawValue: fmt.Sprintf("%d", psu.PSOutPower),
Status: status,
})
out = append(out, models.SensorReading{
Name: fmt.Sprintf("PSU%d_Temp", psu.ID),
Type: "temperature",
Value: float64(psu.PSUMaxTemp),
Unit: "C",
RawValue: fmt.Sprintf("%d", psu.PSUMaxTemp),
Status: status,
})
}
return out
}
func normalizeComponentStatus(values ...string) string {
for _, v := range values {
s := strings.TrimSpace(v)
if s == "" {
continue
}
return s
}
return "unknown"
}
func isHealthyComponentStatus(status string) bool {
switch strings.ToLower(strings.TrimSpace(status)) {
case "", "ok", "normal", "present", "enabled":
return true
default:
return false
}
}
var rawDeviceIDLikeRegex = regexp.MustCompile(`(?i)^(?:0x)?[0-9a-f]{3,4}$`)
func looksLikeRawDeviceID(v string) bool {
v = strings.TrimSpace(v)
if v == "" {
return true
}
return rawDeviceIDLikeRegex.MatchString(v)
}
func parseMemoryEvents(text string) []models.Event {
var events []models.Event
// Find RESTful Memory info section
re := regexp.MustCompile(`RESTful Memory info:\s*(\{[\s\S]*?\})\s*RESTful HDD`)
match := re.FindStringSubmatch(text)
if match == nil {
return events
}
jsonStr := match[1]
jsonStr = strings.ReplaceAll(jsonStr, "\n", "")
var memInfo MemoryRESTInfo
if err := json.Unmarshal([]byte(jsonStr), &memInfo); err != nil {
return events
}
// Generate events for memory modules with Warning or Error status
for _, mem := range memInfo.MemModules {
if mem.Status == "Warning" || mem.Status == "Error" || mem.Status == "Critical" {
severity := models.SeverityWarning
if mem.Status == "Error" || mem.Status == "Critical" {
severity = models.SeverityCritical
}
description := fmt.Sprintf("Memory module %s: %s", mem.MemModSlot, mem.Status)
if mem.MemModSize == 0 {
description = fmt.Sprintf("Memory module %s not detected (capacity 0GB)", mem.MemModSlot)
}
events = append(events, models.Event{
ID: fmt.Sprintf("mem_%d", mem.MemModID),
Timestamp: time.Now(),
Source: "Memory",
SensorType: "memory",
SensorName: mem.MemModSlot,
EventType: "Memory Status",
Severity: severity,
Description: description,
RawData: fmt.Sprintf("Slot: %s, Vendor: %s, P/N: %s, S/N: %s", mem.MemModSlot, mem.MemModVendor, mem.MemModPartNum, mem.MemModSerial),
})
}
}
return events
}
// extractComponentFirmware extracts firmware versions from all component data
func extractComponentFirmware(text string, hw *models.HardwareConfig) {
// Create a map to track existing firmware entries (avoid duplicates)
existingFW := make(map[string]bool)
for _, fw := range hw.Firmware {
existingFW[fw.DeviceName] = true
}
// HDD firmware is already extracted from asset.json with better names
// Skip extracting from component.log to avoid duplicates
// Extract PSU firmware from RESTful PSU info
rePSU := regexp.MustCompile(`RESTful PSU info:\s*(\{[\s\S]*?\})\s*RESTful (?:PCIE|Network)`)
if match := rePSU.FindStringSubmatch(text); match != nil {
jsonStr := strings.ReplaceAll(match[1], "\n", "")
var psuInfo PSURESTInfo
if err := json.Unmarshal([]byte(jsonStr), &psuInfo); err == nil {
for _, psu := range psuInfo.PowerSupplies {
if psu.Present == 1 && psu.FwVer != "" {
fwName := fmt.Sprintf("PSU%d (%s)", psu.ID, psu.Model)
if !existingFW[fwName] {
hw.Firmware = append(hw.Firmware, models.FirmwareInfo{
DeviceName: fwName,
Version: psu.FwVer,
})
existingFW[fwName] = true
}
}
}
}
}
// Extract Network Adapter firmware from RESTful Network Adapter info
reNet := regexp.MustCompile(`RESTful Network Adapter info:\s*(\{[\s\S]*?\})\s*RESTful fan`)
if match := reNet.FindStringSubmatch(text); match != nil {
jsonStr := strings.ReplaceAll(match[1], "\n", "")
var netInfo NetworkAdapterRESTInfo
if err := json.Unmarshal([]byte(jsonStr), &netInfo); err == nil {
for _, adapter := range netInfo.SysAdapters {
if adapter.Present == 1 && adapter.FwVer != "" && adapter.FwVer != "NA" {
fwName := fmt.Sprintf("NIC %s (%s)", adapter.Location, adapter.Model)
if !existingFW[fwName] {
hw.Firmware = append(hw.Firmware, models.FirmwareInfo{
DeviceName: fwName,
Version: adapter.FwVer,
})
existingFW[fwName] = true
}
}
}
}
}
// Extract BMC, CPLD and VR firmware from RESTful version info section.
// The JSON is a flat array: [{"id":N,"dev_name":"...","dev_version":"..."}, ...]
reVer := regexp.MustCompile(`RESTful version info:\s*(\[[\s\S]*?\])\s*RESTful`)
if match := reVer.FindStringSubmatch(text); match != nil {
type verEntry struct {
DevName string `json:"dev_name"`
DevVersion string `json:"dev_version"`
}
var entries []verEntry
if err := json.Unmarshal([]byte(match[1]), &entries); err == nil {
for _, e := range entries {
name := normalizeVersionInfoName(e.DevName)
if name == "" {
continue
}
version := strings.TrimSpace(e.DevVersion)
if version == "" {
continue
}
if existingFW[name] {
continue
}
hw.Firmware = append(hw.Firmware, models.FirmwareInfo{
DeviceName: name,
Version: version,
})
existingFW[name] = true
}
}
}
}
// normalizeVersionInfoName converts RESTful version info dev_name to a clean label.
// Returns "" for entries that should be skipped (inactive BMC, PSU slots).
func normalizeVersionInfoName(name string) string {
name = strings.TrimSpace(name)
if name == "" {
return ""
}
// Skip PSU_N entries — firmware already extracted from PSU info section.
if regexp.MustCompile(`(?i)^PSU_\d+$`).MatchString(name) {
return ""
}
// Skip the inactive BMC partition.
if strings.HasPrefix(strings.ToLower(name), "inactivate(") {
return ""
}
// Active BMC: "Activate(BMC1)" → "BMC"
if strings.HasPrefix(strings.ToLower(name), "activate(") {
return "BMC"
}
// Strip trailing "Version" suffix (case-insensitive), e.g. "MainBoard0CPLDVersion" → "MainBoard0CPLD"
if strings.HasSuffix(strings.ToLower(name), "version") {
name = name[:len(name)-len("version")]
}
return strings.TrimSpace(name)
}
// DiskBackplaneRESTInfo represents the RESTful diskbackplane info structure
type DiskBackplaneRESTInfo []struct {
PortCount int `json:"port_count"`
DriverCount int `json:"driver_count"`
Front int `json:"front"`
BackplaneIndex int `json:"backplane_index"`
Present int `json:"present"`
CPLDVersion string `json:"cpld_version"`
Temperature int `json:"temperature"`
}
func parseDiskBackplaneInfo(text string, hw *models.HardwareConfig) {
// Find RESTful diskbackplane info section
re := regexp.MustCompile(`RESTful diskbackplane info:\s*(\[[\s\S]*?\])\s*BMC`)
match := re.FindStringSubmatch(text)
if match == nil {
return
}
jsonStr := match[1]
jsonStr = strings.ReplaceAll(jsonStr, "\n", "")
var backplaneInfo DiskBackplaneRESTInfo
if err := json.Unmarshal([]byte(jsonStr), &backplaneInfo); err != nil {
return
}
presentByBackplane := make(map[int]int)
totalPresent := 0
for _, bp := range backplaneInfo {
if bp.Present != 1 {
continue
}
if bp.DriverCount <= 0 {
continue
}
limit := bp.DriverCount
if bp.PortCount > 0 && limit > bp.PortCount {
limit = bp.PortCount
}
presentByBackplane[bp.BackplaneIndex] = limit
totalPresent += limit
}
if totalPresent == 0 {
return
}
existingPresent := countPresentStorage(hw.Storage)
remaining := totalPresent - existingPresent
if remaining <= 0 {
return
}
for _, bp := range backplaneInfo {
if bp.Present != 1 || remaining <= 0 {
continue
}
driveCount := presentByBackplane[bp.BackplaneIndex]
if driveCount <= 0 {
continue
}
location := "Rear"
if bp.Front == 1 {
location = "Front"
}
for i := 0; i < driveCount && remaining > 0; i++ {
slot := fmt.Sprintf("BP%d:%d", bp.BackplaneIndex, i)
if hasStorageSlot(hw.Storage, slot) {
continue
}
hw.Storage = append(hw.Storage, models.Storage{
Slot: slot,
Present: true,
Location: location,
BackplaneID: bp.BackplaneIndex,
Type: "HDD",
})
remaining--
}
}
}
func countPresentStorage(storage []models.Storage) int {
count := 0
for _, dev := range storage {
if dev.Present {
count++
continue
}
if strings.TrimSpace(dev.Slot) != "" && (normalizeRedisValue(dev.Model) != "" || normalizeRedisValue(dev.SerialNumber) != "" || dev.SizeGB > 0) {
count++
}
}
return count
}
func hasStorageSlot(storage []models.Storage, slot string) bool {
slot = strings.ToLower(strings.TrimSpace(slot))
if slot == "" {
return false
}
for _, dev := range storage {
if strings.ToLower(strings.TrimSpace(dev.Slot)) == slot {
return true
}
}
return false
}