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 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 } // 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 } // Replace memory data with detailed info from component.log hw.Memory = nil for _, mem := range memInfo.MemModules { hw.Memory = append(hw.Memory, models.MemoryDIMM{ Slot: mem.MemModSlot, Location: mem.MemModSlot, Present: mem.MemModStatus == 1 && mem.MemModSize > 0, 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, }) } } // 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 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 } // Clear existing PSU data and populate with RESTful data hw.PowerSupply = nil for _, psu := range psuInfo.PowerSupplies { hw.PowerSupply = append(hw.PowerSupply, 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, }) } } // 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 } hw.NetworkAdapters = nil 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)) } hw.NetworkAdapters = append(hw.NetworkAdapters, 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, }) } } 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 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 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 } } } } } } // 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 }