Add LOGPile BMC diagnostic log analyzer
Features: - Modular parser architecture for vendor-specific formats - Inspur/Kaytus parser supporting asset.json, devicefrusdr.log, component.log, idl.log, and syslog files - PCI Vendor/Device ID lookup for hardware identification - Web interface with tabs: Events, Sensors, Config, Serials, Firmware - Server specification summary with component grouping - Export to CSV, JSON, TXT formats - BMC alarm parsing from IDL logs (memory errors, PSU events, etc.) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
352
internal/parser/vendors/inspur/asset.go
vendored
Normal file
352
internal/parser/vendors/inspur/asset.go
vendored
Normal file
@@ -0,0 +1,352 @@
|
||||
package inspur
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"git.mchus.pro/mchus/logpile/internal/models"
|
||||
"git.mchus.pro/mchus/logpile/internal/parser/vendors/pciids"
|
||||
)
|
||||
|
||||
// AssetJSON represents the structure of Inspur asset.json file
|
||||
type AssetJSON struct {
|
||||
VersionInfo []struct {
|
||||
DeviceID int `json:"DeviceId"`
|
||||
DeviceName string `json:"DeviceName"`
|
||||
DeviceRevision string `json:"DeviceRevision"`
|
||||
BuildTime string `json:"BuildTime"`
|
||||
} `json:"VersionInfo"`
|
||||
|
||||
CpuInfo []struct {
|
||||
ProcessorName string `json:"ProcessorName"`
|
||||
ProcessorID string `json:"ProcessorId"`
|
||||
MicroCodeVer string `json:"MicroCodeVer"`
|
||||
CurrentSpeed int `json:"CurrentSpeed"`
|
||||
Core int `json:"Core"`
|
||||
ThreadCount int `json:"ThreadCount"`
|
||||
L1Cache int `json:"L1Cache"`
|
||||
L2Cache int `json:"L2Cache"`
|
||||
L3Cache int `json:"L3Cache"`
|
||||
CpuTdp int `json:"CpuTdp"`
|
||||
PPIN string `json:"PPIN"`
|
||||
TurboEnableMaxSpeed int `json:"TurboEnableMaxSpeed"`
|
||||
TurboCloseMaxSpeed int `json:"TurboCloseMaxSpeed"`
|
||||
UPIBandwidth string `json:"UPIBandwidth"`
|
||||
} `json:"CpuInfo"`
|
||||
|
||||
MemInfo struct {
|
||||
MemCommonInfo []struct {
|
||||
Manufacturer string `json:"Manufacturer"`
|
||||
MaxSpeed int `json:"MaxSpeed"`
|
||||
CurrentSpeed int `json:"CurrentSpeed"`
|
||||
MemoryType int `json:"MemoryType"`
|
||||
Rank int `json:"Rank"`
|
||||
DataWidth int `json:"DataWidth"`
|
||||
ConfiguredVoltage int `json:"ConfiguredVoltage"`
|
||||
PhysicalSize int `json:"PhysicalSize"`
|
||||
} `json:"MemCommonInfo"`
|
||||
|
||||
DimmInfo []struct {
|
||||
SerialNumber string `json:"SerialNumber"`
|
||||
PartNumber string `json:"PartNumber"`
|
||||
AssetTag string `json:"AssetTag"`
|
||||
} `json:"DimmInfo"`
|
||||
} `json:"MemInfo"`
|
||||
|
||||
HddInfo []struct {
|
||||
SerialNumber string `json:"SerialNumber"`
|
||||
Manufacturer string `json:"Manufacturer"`
|
||||
ModelName string `json:"ModelName"`
|
||||
FirmwareVersion string `json:"FirmwareVersion"`
|
||||
Capacity int `json:"Capacity"`
|
||||
Location int `json:"Location"`
|
||||
DiskInterfaceType int `json:"DiskInterfaceType"`
|
||||
MediaType int `json:"MediaType"`
|
||||
LocationString string `json:"LocationString"`
|
||||
BlockSizeBytes int `json:"BlockSizeBytes"`
|
||||
CapableSpeedGbs string `json:"CapableSpeedGbs"`
|
||||
NegotiatedSpeedGbs string `json:"NegotiatedSpeedGbs"`
|
||||
PcieSlot int `json:"PcieSlot"`
|
||||
} `json:"HddInfo"`
|
||||
|
||||
PcieInfo []struct {
|
||||
VendorId int `json:"VendorId"`
|
||||
DeviceId int `json:"DeviceId"`
|
||||
BusNumber int `json:"BusNumber"`
|
||||
DeviceNumber int `json:"DeviceNumber"`
|
||||
FunctionNumber int `json:"FunctionNumber"`
|
||||
MaxLinkWidth int `json:"MaxLinkWidth"`
|
||||
MaxLinkSpeed int `json:"MaxLinkSpeed"`
|
||||
NegotiatedLinkWidth int `json:"NegotiatedLinkWidth"`
|
||||
CurrentLinkSpeed int `json:"CurrentLinkSpeed"`
|
||||
ClassCode int `json:"ClassCode"`
|
||||
SubClassCode int `json:"SubClassCode"`
|
||||
PcieSlot int `json:"PcieSlot"`
|
||||
LocString string `json:"LocString"`
|
||||
PartNumber *string `json:"PartNumber"`
|
||||
SerialNumber *string `json:"SerialNumber"`
|
||||
Mac []string `json:"Mac"`
|
||||
} `json:"PcieInfo"`
|
||||
}
|
||||
|
||||
// ParseAssetJSON parses Inspur asset.json content
|
||||
func ParseAssetJSON(content []byte) (*models.HardwareConfig, error) {
|
||||
var asset AssetJSON
|
||||
if err := json.Unmarshal(content, &asset); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
config := &models.HardwareConfig{}
|
||||
|
||||
// Parse version info
|
||||
for _, v := range asset.VersionInfo {
|
||||
config.Firmware = append(config.Firmware, models.FirmwareInfo{
|
||||
DeviceName: v.DeviceName,
|
||||
Version: v.DeviceRevision,
|
||||
BuildTime: v.BuildTime,
|
||||
})
|
||||
}
|
||||
|
||||
// Parse CPU info
|
||||
for i, cpu := range asset.CpuInfo {
|
||||
config.CPUs = append(config.CPUs, models.CPU{
|
||||
Socket: i,
|
||||
Model: strings.TrimSpace(cpu.ProcessorName),
|
||||
Cores: cpu.Core,
|
||||
Threads: cpu.ThreadCount,
|
||||
FrequencyMHz: cpu.CurrentSpeed,
|
||||
MaxFreqMHz: cpu.TurboEnableMaxSpeed,
|
||||
L1CacheKB: cpu.L1Cache,
|
||||
L2CacheKB: cpu.L2Cache,
|
||||
L3CacheKB: cpu.L3Cache,
|
||||
TDP: cpu.CpuTdp,
|
||||
PPIN: cpu.PPIN,
|
||||
})
|
||||
}
|
||||
|
||||
// Parse memory info
|
||||
if len(asset.MemInfo.MemCommonInfo) > 0 {
|
||||
common := asset.MemInfo.MemCommonInfo[0]
|
||||
for i, dimm := range asset.MemInfo.DimmInfo {
|
||||
config.Memory = append(config.Memory, models.MemoryDIMM{
|
||||
Slot: i,
|
||||
SizeMB: common.PhysicalSize * 1024,
|
||||
Type: memoryTypeToString(common.MemoryType),
|
||||
SpeedMHz: common.CurrentSpeed,
|
||||
Manufacturer: common.Manufacturer,
|
||||
SerialNumber: dimm.SerialNumber,
|
||||
PartNumber: strings.TrimSpace(dimm.PartNumber),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Parse storage info
|
||||
for _, hdd := range asset.HddInfo {
|
||||
storageType := "HDD"
|
||||
if hdd.DiskInterfaceType == 5 {
|
||||
storageType = "NVMe"
|
||||
} else if hdd.MediaType == 1 {
|
||||
storageType = "SSD"
|
||||
}
|
||||
|
||||
// Resolve manufacturer: try vendor ID first, then model name extraction
|
||||
modelName := strings.TrimSpace(hdd.ModelName)
|
||||
manufacturer := resolveManufacturer(hdd.Manufacturer, modelName)
|
||||
|
||||
config.Storage = append(config.Storage, models.Storage{
|
||||
Slot: hdd.LocationString,
|
||||
Type: storageType,
|
||||
Model: modelName,
|
||||
SizeGB: hdd.Capacity,
|
||||
SerialNumber: hdd.SerialNumber,
|
||||
Manufacturer: manufacturer,
|
||||
Firmware: hdd.FirmwareVersion,
|
||||
Interface: diskInterfaceToString(hdd.DiskInterfaceType),
|
||||
})
|
||||
}
|
||||
|
||||
// Parse PCIe info
|
||||
for _, pcie := range asset.PcieInfo {
|
||||
vendor, deviceName := pciids.DeviceInfo(pcie.VendorId, pcie.DeviceId)
|
||||
device := models.PCIeDevice{
|
||||
Slot: pcie.LocString,
|
||||
VendorID: pcie.VendorId,
|
||||
DeviceID: pcie.DeviceId,
|
||||
BDF: formatBDF(pcie.BusNumber, pcie.DeviceNumber, pcie.FunctionNumber),
|
||||
LinkWidth: pcie.NegotiatedLinkWidth,
|
||||
LinkSpeed: pcieLinkSpeedToString(pcie.CurrentLinkSpeed),
|
||||
MaxLinkWidth: pcie.MaxLinkWidth,
|
||||
MaxLinkSpeed: pcieLinkSpeedToString(pcie.MaxLinkSpeed),
|
||||
DeviceClass: pcieClassToString(pcie.ClassCode, pcie.SubClassCode),
|
||||
Manufacturer: vendor,
|
||||
}
|
||||
if pcie.PartNumber != nil {
|
||||
device.PartNumber = strings.TrimSpace(*pcie.PartNumber)
|
||||
}
|
||||
if pcie.SerialNumber != nil {
|
||||
device.SerialNumber = strings.TrimSpace(*pcie.SerialNumber)
|
||||
}
|
||||
if len(pcie.Mac) > 0 {
|
||||
device.MACAddresses = pcie.Mac
|
||||
}
|
||||
// Use device name from PCI IDs database if available
|
||||
if deviceName != "" {
|
||||
device.DeviceClass = deviceName
|
||||
}
|
||||
config.PCIeDevices = append(config.PCIeDevices, device)
|
||||
}
|
||||
|
||||
return config, nil
|
||||
}
|
||||
|
||||
func memoryTypeToString(memType int) string {
|
||||
switch memType {
|
||||
case 26:
|
||||
return "DDR4"
|
||||
case 34:
|
||||
return "DDR5"
|
||||
default:
|
||||
return "Unknown"
|
||||
}
|
||||
}
|
||||
|
||||
func diskInterfaceToString(ifType int) string {
|
||||
switch ifType {
|
||||
case 4:
|
||||
return "SATA"
|
||||
case 5:
|
||||
return "NVMe"
|
||||
case 6:
|
||||
return "SAS"
|
||||
default:
|
||||
return "Unknown"
|
||||
}
|
||||
}
|
||||
|
||||
func pcieLinkSpeedToString(speed int) string {
|
||||
switch speed {
|
||||
case 1:
|
||||
return "2.5 GT/s"
|
||||
case 2:
|
||||
return "5.0 GT/s"
|
||||
case 3:
|
||||
return "8.0 GT/s"
|
||||
case 4:
|
||||
return "16.0 GT/s"
|
||||
case 5:
|
||||
return "32.0 GT/s"
|
||||
default:
|
||||
return "Unknown"
|
||||
}
|
||||
}
|
||||
|
||||
func pcieClassToString(classCode, subClass int) string {
|
||||
switch classCode {
|
||||
case 1:
|
||||
switch subClass {
|
||||
case 0:
|
||||
return "SCSI"
|
||||
case 1:
|
||||
return "IDE"
|
||||
case 4:
|
||||
return "RAID"
|
||||
case 6:
|
||||
return "SATA"
|
||||
case 7:
|
||||
return "SAS"
|
||||
case 8:
|
||||
return "NVMe"
|
||||
default:
|
||||
return "Storage"
|
||||
}
|
||||
case 2:
|
||||
return "Network"
|
||||
case 3:
|
||||
switch subClass {
|
||||
case 0:
|
||||
return "VGA"
|
||||
case 2:
|
||||
return "3D Controller"
|
||||
default:
|
||||
return "Display"
|
||||
}
|
||||
case 4:
|
||||
return "Multimedia"
|
||||
case 6:
|
||||
return "Bridge"
|
||||
case 12:
|
||||
return "Serial Bus"
|
||||
default:
|
||||
return "Other"
|
||||
}
|
||||
}
|
||||
|
||||
func formatBDF(bus, dev, fun int) string {
|
||||
return fmt.Sprintf("%02x:%02x.%x", bus, dev, fun)
|
||||
}
|
||||
|
||||
// resolveManufacturer resolves manufacturer name from various sources
|
||||
func resolveManufacturer(rawManufacturer, modelName string) string {
|
||||
raw := strings.TrimSpace(rawManufacturer)
|
||||
|
||||
// If it looks like a vendor ID (hex), try to resolve it
|
||||
if raw != "" {
|
||||
if name := pciids.VendorNameFromString(raw); name != "" {
|
||||
return name
|
||||
}
|
||||
// If not a vendor ID but looks like a real name (has letters), use it
|
||||
hasLetter := false
|
||||
for _, c := range raw {
|
||||
if (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') {
|
||||
hasLetter = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if hasLetter && len(raw) > 2 {
|
||||
return raw
|
||||
}
|
||||
}
|
||||
|
||||
// Try to extract from model name
|
||||
return extractStorageManufacturer(modelName)
|
||||
}
|
||||
|
||||
// extractStorageManufacturer tries to extract manufacturer from model name
|
||||
func extractStorageManufacturer(model string) string {
|
||||
modelUpper := strings.ToUpper(model)
|
||||
|
||||
knownVendors := []struct {
|
||||
prefix string
|
||||
name string
|
||||
}{
|
||||
{"SAMSUNG", "Samsung"},
|
||||
{"KIOXIA", "KIOXIA"},
|
||||
{"TOSHIBA", "Toshiba"},
|
||||
{"WDC", "Western Digital"},
|
||||
{"WD", "Western Digital"},
|
||||
{"SEAGATE", "Seagate"},
|
||||
{"HGST", "HGST"},
|
||||
{"INTEL", "Intel"},
|
||||
{"MICRON", "Micron"},
|
||||
{"KINGSTON", "Kingston"},
|
||||
{"CRUCIAL", "Crucial"},
|
||||
{"SK HYNIX", "SK Hynix"},
|
||||
{"SKHYNIX", "SK Hynix"},
|
||||
{"SANDISK", "SanDisk"},
|
||||
{"LITEON", "Lite-On"},
|
||||
{"PLEXTOR", "Plextor"},
|
||||
{"ADATA", "ADATA"},
|
||||
{"TRANSCEND", "Transcend"},
|
||||
{"CORSAIR", "Corsair"},
|
||||
{"SOLIDIGM", "Solidigm"},
|
||||
}
|
||||
|
||||
for _, v := range knownVendors {
|
||||
if strings.HasPrefix(modelUpper, v.prefix) {
|
||||
return v.name
|
||||
}
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
147
internal/parser/vendors/inspur/component.go
vendored
Normal file
147
internal/parser/vendors/inspur/component.go
vendored
Normal file
@@ -0,0 +1,147 @@
|
||||
package inspur
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"git.mchus.pro/mchus/logpile/internal/models"
|
||||
)
|
||||
|
||||
// ParseComponentLog parses component.log file and extracts PSU and other info
|
||||
func ParseComponentLog(content []byte, hw *models.HardwareConfig) {
|
||||
if hw == nil {
|
||||
return
|
||||
}
|
||||
|
||||
text := string(content)
|
||||
|
||||
// Parse RESTful PSU info
|
||||
parsePSUInfo(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...)
|
||||
|
||||
return events
|
||||
}
|
||||
|
||||
// PSUInfo represents the RESTful PSU info structure
|
||||
type PSUInfo 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"`
|
||||
FwVer string `json:"fw_ver"`
|
||||
RatedPower int `json:"rated_power"`
|
||||
Status string `json:"status"`
|
||||
} `json:"power_supplies"`
|
||||
}
|
||||
|
||||
func parsePSUInfo(text string, hw *models.HardwareConfig) {
|
||||
// Find RESTful PSU info section
|
||||
re := regexp.MustCompile(`RESTful PSU info:\s*(\{[\s\S]*?\})\s*(?:RESTful|BMC|$)`)
|
||||
match := re.FindStringSubmatch(text)
|
||||
if match == nil {
|
||||
return
|
||||
}
|
||||
|
||||
jsonStr := match[1]
|
||||
// Clean up the JSON (it might have newlines)
|
||||
jsonStr = strings.ReplaceAll(jsonStr, "\n", "")
|
||||
|
||||
var psuInfo PSUInfo
|
||||
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 {
|
||||
if psu.Present != 1 {
|
||||
continue
|
||||
}
|
||||
hw.PowerSupply = append(hw.PowerSupply, models.PSU{
|
||||
Slot: formatPSUSlot(psu.ID),
|
||||
Model: psu.Model,
|
||||
WattageW: psu.RatedPower,
|
||||
SerialNumber: psu.SerialNum,
|
||||
Status: psu.Status,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func formatPSUSlot(id int) string {
|
||||
return fmt.Sprintf("PSU%d", id)
|
||||
}
|
||||
|
||||
// MemoryInfo represents the RESTful Memory info structure
|
||||
type MemoryInfo struct {
|
||||
MemModules []struct {
|
||||
MemModID int `json:"mem_mod_id"`
|
||||
MemModSlot string `json:"mem_mod_slot"`
|
||||
MemModSize int `json:"mem_mod_size"`
|
||||
MemModVendor string `json:"mem_mod_vendor"`
|
||||
MemModPartNum string `json:"mem_mod_part_num"`
|
||||
MemModSerial string `json:"mem_mod_serial_num"`
|
||||
Status string `json:"status"`
|
||||
} `json:"mem_modules"`
|
||||
}
|
||||
|
||||
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 MemoryInfo
|
||||
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(), // No timestamp in source, use current
|
||||
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
|
||||
}
|
||||
97
internal/parser/vendors/inspur/fru.go
vendored
Normal file
97
internal/parser/vendors/inspur/fru.go
vendored
Normal file
@@ -0,0 +1,97 @@
|
||||
package inspur
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"git.mchus.pro/mchus/logpile/internal/models"
|
||||
)
|
||||
|
||||
var (
|
||||
fruDeviceRegex = regexp.MustCompile(`^FRU Device Description\s*:\s*(.+)$`)
|
||||
fruFieldRegex = regexp.MustCompile(`^\s+(.+?)\s*:\s*(.*)$`)
|
||||
)
|
||||
|
||||
// ParseFRU parses BMC FRU (Field Replaceable Unit) output
|
||||
func ParseFRU(content []byte) []models.FRUInfo {
|
||||
var fruList []models.FRUInfo
|
||||
var current *models.FRUInfo
|
||||
|
||||
scanner := bufio.NewScanner(strings.NewReader(string(content)))
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
|
||||
// Check for new FRU device
|
||||
if matches := fruDeviceRegex.FindStringSubmatch(line); matches != nil {
|
||||
if current != nil && current.Description != "" {
|
||||
fruList = append(fruList, *current)
|
||||
}
|
||||
current = &models.FRUInfo{
|
||||
Description: strings.TrimSpace(matches[1]),
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// Skip if no current FRU device
|
||||
if current == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// Skip "Device not present" entries
|
||||
if strings.Contains(line, "Device not present") {
|
||||
current = nil
|
||||
continue
|
||||
}
|
||||
|
||||
// Parse FRU fields
|
||||
if matches := fruFieldRegex.FindStringSubmatch(line); matches != nil {
|
||||
fieldName := strings.TrimSpace(matches[1])
|
||||
fieldValue := strings.TrimSpace(matches[2])
|
||||
|
||||
switch fieldName {
|
||||
case "Chassis Type":
|
||||
current.ChassisType = fieldValue
|
||||
case "Chassis Part Number":
|
||||
if fieldValue != "0" {
|
||||
current.PartNumber = fieldValue
|
||||
}
|
||||
case "Chassis Serial":
|
||||
if fieldValue != "0" {
|
||||
current.SerialNumber = fieldValue
|
||||
}
|
||||
case "Board Mfg Date":
|
||||
current.MfgDate = fieldValue
|
||||
case "Board Mfg", "Product Manufacturer":
|
||||
if fieldValue != "NULL" {
|
||||
current.Manufacturer = fieldValue
|
||||
}
|
||||
case "Board Product", "Product Name":
|
||||
if fieldValue != "NULL" {
|
||||
current.ProductName = fieldValue
|
||||
}
|
||||
case "Board Serial", "Product Serial":
|
||||
current.SerialNumber = fieldValue
|
||||
case "Board Part Number", "Product Part Number":
|
||||
if fieldValue != "0" {
|
||||
current.PartNumber = fieldValue
|
||||
}
|
||||
case "Product Version":
|
||||
if fieldValue != "0" {
|
||||
current.Version = fieldValue
|
||||
}
|
||||
case "Product Asset Tag":
|
||||
if fieldValue != "NULL" {
|
||||
current.AssetTag = fieldValue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Don't forget the last one
|
||||
if current != nil && current.Description != "" {
|
||||
fruList = append(fruList, *current)
|
||||
}
|
||||
|
||||
return fruList
|
||||
}
|
||||
123
internal/parser/vendors/inspur/idl.go
vendored
Normal file
123
internal/parser/vendors/inspur/idl.go
vendored
Normal file
@@ -0,0 +1,123 @@
|
||||
package inspur
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"git.mchus.pro/mchus/logpile/internal/models"
|
||||
)
|
||||
|
||||
// ParseIDLLog parses the IDL (Inspur Diagnostic Log) file for BMC alarms
|
||||
// Format: |timestamp|component|type|severity|eventID|description|
|
||||
func ParseIDLLog(content []byte) []models.Event {
|
||||
var events []models.Event
|
||||
|
||||
// Pattern to match CommerDiagnose log entries
|
||||
// Example: |2025-12-02T17:54:27+08:00|MEMORY|Assert|Warning|0C180401|CPU1_C4D0 Memory Device Disabled...|
|
||||
re := regexp.MustCompile(`\|(\d{4}-\d{2}-\d{2}T[\d:]+[+-]\d{2}:\d{2})\|([^|]+)\|([^|]+)\|([^|]+)\|([^|]+)\|([^|]+)\|`)
|
||||
|
||||
lines := strings.Split(string(content), "\n")
|
||||
seenEvents := make(map[string]bool) // Deduplicate events
|
||||
|
||||
for _, line := range lines {
|
||||
if !strings.Contains(line, "CommerDiagnose") {
|
||||
continue
|
||||
}
|
||||
|
||||
matches := re.FindStringSubmatch(line)
|
||||
if matches == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
timestamp := matches[1]
|
||||
component := matches[2]
|
||||
eventType := matches[3]
|
||||
severityStr := matches[4]
|
||||
eventID := matches[5]
|
||||
description := matches[6]
|
||||
|
||||
// Parse timestamp
|
||||
ts, err := time.Parse("2006-01-02T15:04:05-07:00", timestamp)
|
||||
if err != nil {
|
||||
ts = time.Now()
|
||||
}
|
||||
|
||||
// Map severity
|
||||
severity := mapIDLSeverity(severityStr)
|
||||
|
||||
// Clean up description
|
||||
description = cleanDescription(description)
|
||||
|
||||
// Create unique key for deduplication
|
||||
eventKey := eventID + "|" + description
|
||||
if seenEvents[eventKey] {
|
||||
continue
|
||||
}
|
||||
seenEvents[eventKey] = true
|
||||
|
||||
// Extract sensor name from description if available
|
||||
sensorName := extractSensorName(description, component)
|
||||
|
||||
events = append(events, models.Event{
|
||||
ID: eventID,
|
||||
Timestamp: ts,
|
||||
Source: component,
|
||||
SensorType: strings.ToLower(component),
|
||||
SensorName: sensorName,
|
||||
EventType: eventType,
|
||||
Severity: severity,
|
||||
Description: description,
|
||||
})
|
||||
}
|
||||
|
||||
return events
|
||||
}
|
||||
|
||||
func mapIDLSeverity(s string) models.Severity {
|
||||
switch strings.ToLower(s) {
|
||||
case "critical", "error":
|
||||
return models.SeverityCritical
|
||||
case "warning":
|
||||
return models.SeverityWarning
|
||||
default:
|
||||
return models.SeverityInfo
|
||||
}
|
||||
}
|
||||
|
||||
func cleanDescription(desc string) string {
|
||||
// Remove trailing " - Assert" or similar
|
||||
desc = strings.TrimSuffix(desc, " - Assert")
|
||||
desc = strings.TrimSuffix(desc, " - Deassert")
|
||||
desc = strings.TrimSpace(desc)
|
||||
return desc
|
||||
}
|
||||
|
||||
func extractSensorName(desc, component string) string {
|
||||
// Try to extract sensor/device name from description
|
||||
// For memory: CPU1_C4D0, CPU1_C4D1, etc.
|
||||
if component == "MEMORY" {
|
||||
re := regexp.MustCompile(`(CPU\d+_C\d+D\d+)`)
|
||||
if matches := re.FindStringSubmatch(desc); matches != nil {
|
||||
return matches[1]
|
||||
}
|
||||
}
|
||||
|
||||
// For PSU: PSU0, PSU1, etc.
|
||||
if component == "PSU" || component == "POWER" {
|
||||
re := regexp.MustCompile(`(PSU\d+)`)
|
||||
if matches := re.FindStringSubmatch(desc); matches != nil {
|
||||
return matches[1]
|
||||
}
|
||||
}
|
||||
|
||||
// For temperature sensors
|
||||
if component == "TEMPERATURE" || component == "THERMAL" {
|
||||
re := regexp.MustCompile(`(\w+_Temp|\w+_DTS)`)
|
||||
if matches := re.FindStringSubmatch(desc); matches != nil {
|
||||
return matches[1]
|
||||
}
|
||||
}
|
||||
|
||||
return component
|
||||
}
|
||||
143
internal/parser/vendors/inspur/parser.go
vendored
Normal file
143
internal/parser/vendors/inspur/parser.go
vendored
Normal file
@@ -0,0 +1,143 @@
|
||||
// Package inspur provides parser for Inspur/Kaytus BMC diagnostic archives
|
||||
// Tested with: Kaytus KR4268X2 (onekeylog format)
|
||||
package inspur
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"git.mchus.pro/mchus/logpile/internal/models"
|
||||
"git.mchus.pro/mchus/logpile/internal/parser"
|
||||
)
|
||||
|
||||
func init() {
|
||||
parser.Register(&Parser{})
|
||||
}
|
||||
|
||||
// Parser implements VendorParser for Inspur/Kaytus servers
|
||||
type Parser struct{}
|
||||
|
||||
// Name returns human-readable parser name
|
||||
func (p *Parser) Name() string {
|
||||
return "Inspur/Kaytus BMC Parser"
|
||||
}
|
||||
|
||||
// Vendor returns vendor identifier
|
||||
func (p *Parser) Vendor() string {
|
||||
return "inspur"
|
||||
}
|
||||
|
||||
// Detect checks if archive matches Inspur/Kaytus format
|
||||
// Returns confidence 0-100
|
||||
func (p *Parser) Detect(files []parser.ExtractedFile) int {
|
||||
confidence := 0
|
||||
|
||||
for _, f := range files {
|
||||
path := strings.ToLower(f.Path)
|
||||
|
||||
// Strong indicators for Inspur/Kaytus onekeylog format
|
||||
if strings.Contains(path, "onekeylog/") {
|
||||
confidence += 30
|
||||
}
|
||||
if strings.Contains(path, "devicefrusdr.log") {
|
||||
confidence += 25
|
||||
}
|
||||
if strings.Contains(path, "component/component.log") {
|
||||
confidence += 15
|
||||
}
|
||||
|
||||
// Check for asset.json with Inspur-specific structure
|
||||
if strings.HasSuffix(path, "asset.json") {
|
||||
if containsInspurMarkers(f.Content) {
|
||||
confidence += 20
|
||||
}
|
||||
}
|
||||
|
||||
// Cap at 100
|
||||
if confidence >= 100 {
|
||||
return 100
|
||||
}
|
||||
}
|
||||
|
||||
return confidence
|
||||
}
|
||||
|
||||
// containsInspurMarkers checks if content has Inspur-specific markers
|
||||
func containsInspurMarkers(content []byte) bool {
|
||||
s := string(content)
|
||||
// Check for typical Inspur asset.json structure
|
||||
return strings.Contains(s, "VersionInfo") &&
|
||||
strings.Contains(s, "CpuInfo") &&
|
||||
strings.Contains(s, "MemInfo")
|
||||
}
|
||||
|
||||
// Parse parses Inspur/Kaytus archive
|
||||
func (p *Parser) Parse(files []parser.ExtractedFile) (*models.AnalysisResult, error) {
|
||||
result := &models.AnalysisResult{
|
||||
Events: make([]models.Event, 0),
|
||||
FRU: make([]models.FRUInfo, 0),
|
||||
Sensors: make([]models.SensorReading, 0),
|
||||
}
|
||||
|
||||
// Parse devicefrusdr.log (contains SDR and FRU data)
|
||||
if f := parser.FindFileByName(files, "devicefrusdr.log"); f != nil {
|
||||
p.parseDeviceFruSDR(f.Content, result)
|
||||
}
|
||||
|
||||
// Parse asset.json
|
||||
if f := parser.FindFileByName(files, "asset.json"); f != nil {
|
||||
if hw, err := ParseAssetJSON(f.Content); err == nil {
|
||||
result.Hardware = hw
|
||||
}
|
||||
}
|
||||
|
||||
// Parse component.log for additional data (PSU, etc.)
|
||||
if f := parser.FindFileByName(files, "component.log"); f != nil {
|
||||
if result.Hardware == nil {
|
||||
result.Hardware = &models.HardwareConfig{}
|
||||
}
|
||||
ParseComponentLog(f.Content, result.Hardware)
|
||||
|
||||
// Extract events from component.log (memory errors, etc.)
|
||||
componentEvents := ParseComponentLogEvents(f.Content)
|
||||
result.Events = append(result.Events, componentEvents...)
|
||||
}
|
||||
|
||||
// Parse IDL log (BMC alarms/diagnose events)
|
||||
if f := parser.FindFileByName(files, "idl.log"); f != nil {
|
||||
idlEvents := ParseIDLLog(f.Content)
|
||||
result.Events = append(result.Events, idlEvents...)
|
||||
}
|
||||
|
||||
// Parse syslog files
|
||||
syslogFiles := parser.FindFileByPattern(files, "syslog/alert", "syslog/warning", "syslog/notice", "syslog/info")
|
||||
for _, f := range syslogFiles {
|
||||
events := ParseSyslog(f.Content, f.Path)
|
||||
result.Events = append(result.Events, events...)
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (p *Parser) parseDeviceFruSDR(content []byte, result *models.AnalysisResult) {
|
||||
lines := string(content)
|
||||
|
||||
// Find SDR section
|
||||
sdrStart := strings.Index(lines, "BMC sdr Info:")
|
||||
fruStart := strings.Index(lines, "BMC fru Info:")
|
||||
|
||||
if sdrStart != -1 {
|
||||
var sdrContent string
|
||||
if fruStart != -1 && fruStart > sdrStart {
|
||||
sdrContent = lines[sdrStart:fruStart]
|
||||
} else {
|
||||
sdrContent = lines[sdrStart:]
|
||||
}
|
||||
result.Sensors = ParseSDR([]byte(sdrContent))
|
||||
}
|
||||
|
||||
// Find FRU section
|
||||
if fruStart != -1 {
|
||||
fruContent := lines[fruStart:]
|
||||
result.FRU = ParseFRU([]byte(fruContent))
|
||||
}
|
||||
}
|
||||
89
internal/parser/vendors/inspur/sdr.go
vendored
Normal file
89
internal/parser/vendors/inspur/sdr.go
vendored
Normal file
@@ -0,0 +1,89 @@
|
||||
package inspur
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"git.mchus.pro/mchus/logpile/internal/models"
|
||||
)
|
||||
|
||||
// SDR sensor reading patterns
|
||||
var (
|
||||
sdrLineRegex = regexp.MustCompile(`^(\S+)\s+\|\s+(.+?)\s+\|\s+(\w+)$`)
|
||||
valueRegex = regexp.MustCompile(`^([\d.]+)\s+(.+)$`)
|
||||
)
|
||||
|
||||
// ParseSDR parses BMC SDR (Sensor Data Record) output
|
||||
func ParseSDR(content []byte) []models.SensorReading {
|
||||
var readings []models.SensorReading
|
||||
|
||||
scanner := bufio.NewScanner(strings.NewReader(string(content)))
|
||||
for scanner.Scan() {
|
||||
line := strings.TrimSpace(scanner.Text())
|
||||
if line == "" || strings.HasPrefix(line, "BMC sdr Info:") {
|
||||
continue
|
||||
}
|
||||
|
||||
matches := sdrLineRegex.FindStringSubmatch(line)
|
||||
if matches == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
name := strings.TrimSpace(matches[1])
|
||||
valueStr := strings.TrimSpace(matches[2])
|
||||
status := strings.TrimSpace(matches[3])
|
||||
|
||||
reading := models.SensorReading{
|
||||
Name: name,
|
||||
Status: status,
|
||||
}
|
||||
|
||||
// Parse value and unit
|
||||
if valueStr != "disabled" && valueStr != "no reading" && !strings.HasPrefix(valueStr, "0x") {
|
||||
if vm := valueRegex.FindStringSubmatch(valueStr); vm != nil {
|
||||
if v, err := strconv.ParseFloat(vm[1], 64); err == nil {
|
||||
reading.Value = v
|
||||
reading.Unit = strings.TrimSpace(vm[2])
|
||||
}
|
||||
}
|
||||
} else if strings.HasPrefix(valueStr, "0x") {
|
||||
reading.RawValue = valueStr
|
||||
}
|
||||
|
||||
// Determine sensor type
|
||||
reading.Type = determineSensorType(name)
|
||||
|
||||
readings = append(readings, reading)
|
||||
}
|
||||
|
||||
return readings
|
||||
}
|
||||
|
||||
func determineSensorType(name string) string {
|
||||
nameLower := strings.ToLower(name)
|
||||
|
||||
switch {
|
||||
case strings.Contains(nameLower, "temp"):
|
||||
return "temperature"
|
||||
case strings.Contains(nameLower, "fan") && strings.Contains(nameLower, "speed"):
|
||||
return "fan_speed"
|
||||
case strings.Contains(nameLower, "fan") && strings.Contains(nameLower, "status"):
|
||||
return "fan_status"
|
||||
case strings.HasSuffix(nameLower, "_vin") || strings.HasSuffix(nameLower, "_vout") ||
|
||||
strings.HasSuffix(nameLower, "_v") || strings.Contains(nameLower, "volt"):
|
||||
return "voltage"
|
||||
case strings.Contains(nameLower, "power") || strings.HasSuffix(nameLower, "_pin") ||
|
||||
strings.HasSuffix(nameLower, "_pout") || strings.HasSuffix(nameLower, "_pwr"):
|
||||
return "power"
|
||||
case strings.Contains(nameLower, "psu") && strings.Contains(nameLower, "status"):
|
||||
return "psu_status"
|
||||
case strings.Contains(nameLower, "cpu") && strings.Contains(nameLower, "status"):
|
||||
return "cpu_status"
|
||||
case strings.Contains(nameLower, "hdd") || strings.Contains(nameLower, "nvme"):
|
||||
return "storage_status"
|
||||
default:
|
||||
return "other"
|
||||
}
|
||||
}
|
||||
97
internal/parser/vendors/inspur/syslog.go
vendored
Normal file
97
internal/parser/vendors/inspur/syslog.go
vendored
Normal file
@@ -0,0 +1,97 @@
|
||||
package inspur
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"git.mchus.pro/mchus/logpile/internal/models"
|
||||
)
|
||||
|
||||
var (
|
||||
// Syslog format: <priority> timestamp hostname process: message
|
||||
syslogRegex = regexp.MustCompile(`^<(\d+)>\s*(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}[^\s]*)\s+(\S+)\s+(\S+):\s*(.*)$`)
|
||||
)
|
||||
|
||||
// ParseSyslog parses syslog format logs
|
||||
func ParseSyslog(content []byte, sourcePath string) []models.Event {
|
||||
var events []models.Event
|
||||
|
||||
// Determine severity from file path
|
||||
severity := determineSeverityFromPath(sourcePath)
|
||||
|
||||
scanner := bufio.NewScanner(strings.NewReader(string(content)))
|
||||
lineNum := 0
|
||||
|
||||
for scanner.Scan() {
|
||||
lineNum++
|
||||
line := strings.TrimSpace(scanner.Text())
|
||||
if line == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
matches := syslogRegex.FindStringSubmatch(line)
|
||||
if matches == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
timestamp, err := time.Parse(time.RFC3339, matches[2])
|
||||
if err != nil {
|
||||
// Try alternative format
|
||||
timestamp, err = time.Parse("2006-01-02T15:04:05.000000-07:00", matches[2])
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
event := models.Event{
|
||||
ID: generateEventID(sourcePath, lineNum),
|
||||
Timestamp: timestamp,
|
||||
Source: matches[4],
|
||||
SensorType: "syslog",
|
||||
SensorName: matches[3],
|
||||
Description: matches[5],
|
||||
Severity: severity,
|
||||
RawData: line,
|
||||
}
|
||||
|
||||
events = append(events, event)
|
||||
}
|
||||
|
||||
return events
|
||||
}
|
||||
|
||||
func determineSeverityFromPath(path string) models.Severity {
|
||||
pathLower := strings.ToLower(path)
|
||||
|
||||
switch {
|
||||
case strings.Contains(pathLower, "emerg") || strings.Contains(pathLower, "alert") ||
|
||||
strings.Contains(pathLower, "crit"):
|
||||
return models.SeverityCritical
|
||||
case strings.Contains(pathLower, "warn") || strings.Contains(pathLower, "error"):
|
||||
return models.SeverityWarning
|
||||
default:
|
||||
return models.SeverityInfo
|
||||
}
|
||||
}
|
||||
|
||||
func generateEventID(source string, lineNum int) string {
|
||||
parts := strings.Split(source, "/")
|
||||
filename := parts[len(parts)-1]
|
||||
return strings.TrimSuffix(filename, ".log") + "_" + itoa(lineNum)
|
||||
}
|
||||
|
||||
func itoa(i int) string {
|
||||
if i == 0 {
|
||||
return "0"
|
||||
}
|
||||
var b [20]byte
|
||||
pos := len(b)
|
||||
for i > 0 {
|
||||
pos--
|
||||
b[pos] = byte('0' + i%10)
|
||||
i /= 10
|
||||
}
|
||||
return string(b[pos:])
|
||||
}
|
||||
Reference in New Issue
Block a user