v1.2.0: Enhanced Inspur/Kaytus parser with GPU, PCIe, and storage support
Major improvements: - Add CSV SEL event parser for Kaytus firmware format - Add PCIe device parser with link speed/width detection - Add GPU temperature and PCIe link monitoring - Add disk backplane parser for storage bay information - Fix memory module detection (only show installed DIMMs) Parser enhancements: - Parse RESTful PCIe Device info (max/current link width/speed) - Parse GPU sensor data (core and memory temperatures) - Parse diskbackplane info (slot count, installed drives) - Parse SEL events from CSV format (selelist.csv) - Fix memory Present status logic (check mem_mod_status) Web interface improvements: - Add PCIe link degradation highlighting (red when current < max) - Add storage table with Present status and location - Update memory specification to show only installed modules with frequency - Sort events from newest to oldest - Filter out N/A serial numbers from display Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 4.5
parent
c7422e95aa
commit
21f4e5a67e
+174
@@ -0,0 +1,174 @@
|
||||
package inspur
|
||||
|
||||
import (
|
||||
"encoding/csv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"git.mchus.pro/mchus/logpile/internal/models"
|
||||
)
|
||||
|
||||
// ParseSELList parses selelist.csv file with SEL events
|
||||
// Format: ID, Date (MM/DD/YYYY), Time (HH:MM:SS), Sensor, Event, Status
|
||||
// Example: 1,04/18/2025,09:31:18,Event Logging Disabled SEL_Status,Log area reset/cleared,Asserted
|
||||
func ParseSELList(content []byte) []models.Event {
|
||||
var events []models.Event
|
||||
|
||||
text := string(content)
|
||||
lines := strings.Split(text, "\n")
|
||||
|
||||
// Skip header line(s) if present
|
||||
startIdx := 0
|
||||
for i, line := range lines {
|
||||
if strings.Contains(strings.ToLower(line), "sel elist") {
|
||||
startIdx = i + 1
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Parse CSV data
|
||||
for i := startIdx; i < len(lines); i++ {
|
||||
line := strings.TrimSpace(lines[i])
|
||||
if line == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
// Parse CSV line
|
||||
r := csv.NewReader(strings.NewReader(line))
|
||||
records, err := r.Read()
|
||||
if err != nil || len(records) < 6 {
|
||||
continue
|
||||
}
|
||||
|
||||
eventID := strings.TrimSpace(records[0])
|
||||
dateStr := strings.TrimSpace(records[1])
|
||||
timeStr := strings.TrimSpace(records[2])
|
||||
sensorStr := strings.TrimSpace(records[3])
|
||||
eventDesc := strings.TrimSpace(records[4])
|
||||
status := strings.TrimSpace(records[5])
|
||||
|
||||
// Parse timestamp: MM/DD/YYYY HH:MM:SS
|
||||
timestamp := parseSELTimestamp(dateStr, timeStr)
|
||||
|
||||
// Extract sensor type and name
|
||||
sensorType, sensorName := parseSensorInfo(sensorStr)
|
||||
|
||||
// Determine severity
|
||||
severity := determineSELSeverity(sensorStr, eventDesc, status)
|
||||
|
||||
// Build full description
|
||||
description := buildSELDescription(eventDesc, status)
|
||||
|
||||
events = append(events, models.Event{
|
||||
ID: eventID,
|
||||
Timestamp: timestamp,
|
||||
Source: "SEL",
|
||||
SensorType: sensorType,
|
||||
SensorName: sensorName,
|
||||
EventType: eventDesc,
|
||||
Severity: severity,
|
||||
Description: description,
|
||||
RawData: line,
|
||||
})
|
||||
}
|
||||
|
||||
return events
|
||||
}
|
||||
|
||||
// parseSELTimestamp parses MM/DD/YYYY and HH:MM:SS into time.Time
|
||||
func parseSELTimestamp(dateStr, timeStr string) time.Time {
|
||||
// Combine date and time: MM/DD/YYYY HH:MM:SS
|
||||
timestampStr := dateStr + " " + timeStr
|
||||
|
||||
// Try parsing with MM/DD/YYYY format
|
||||
t, err := time.Parse("01/02/2006 15:04:05", timestampStr)
|
||||
if err != nil {
|
||||
// Fallback to current time
|
||||
return time.Now()
|
||||
}
|
||||
|
||||
return t
|
||||
}
|
||||
|
||||
// parseSensorInfo extracts sensor type and name from sensor string
|
||||
// Example: "Event Logging Disabled SEL_Status" -> ("sel", "SEL_Status")
|
||||
// Example: "Power Supply PSU0_Status" -> ("power_supply", "PSU0_Status")
|
||||
func parseSensorInfo(sensorStr string) (sensorType, sensorName string) {
|
||||
parts := strings.Fields(sensorStr)
|
||||
if len(parts) == 0 {
|
||||
return "unknown", sensorStr
|
||||
}
|
||||
|
||||
// Last part is usually the sensor name
|
||||
sensorName = parts[len(parts)-1]
|
||||
|
||||
// First parts form the sensor type
|
||||
if len(parts) > 1 {
|
||||
sensorType = strings.ToLower(strings.Join(parts[:len(parts)-1], "_"))
|
||||
} else {
|
||||
sensorType = "system"
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// determineSELSeverity determines event severity based on sensor and event description
|
||||
func determineSELSeverity(sensorStr, eventDesc, status string) models.Severity {
|
||||
lowerSensor := strings.ToLower(sensorStr)
|
||||
lowerEvent := strings.ToLower(eventDesc)
|
||||
lowerStatus := strings.ToLower(status)
|
||||
|
||||
// Critical indicators
|
||||
criticalKeywords := []string{
|
||||
"critical", "failure", "fault", "error",
|
||||
"ac lost", "predictive failure", "redundancy lost",
|
||||
"going high", "going low", "transition to critical",
|
||||
}
|
||||
|
||||
for _, keyword := range criticalKeywords {
|
||||
if strings.Contains(lowerSensor, keyword) ||
|
||||
strings.Contains(lowerEvent, keyword) ||
|
||||
strings.Contains(lowerStatus, keyword) {
|
||||
return models.SeverityCritical
|
||||
}
|
||||
}
|
||||
|
||||
// Warning indicators
|
||||
warningKeywords := []string{
|
||||
"warning", "disabled", "non-recoverable",
|
||||
"device removed", "device absent",
|
||||
}
|
||||
|
||||
for _, keyword := range warningKeywords {
|
||||
if strings.Contains(lowerSensor, keyword) ||
|
||||
strings.Contains(lowerEvent, keyword) ||
|
||||
strings.Contains(lowerStatus, keyword) {
|
||||
return models.SeverityWarning
|
||||
}
|
||||
}
|
||||
|
||||
// Info indicators (normal operations)
|
||||
infoKeywords := []string{
|
||||
"presence detected", "device present", "asserted",
|
||||
"initiated by", "state asserted", "s0/g0: working",
|
||||
"power button pressed",
|
||||
}
|
||||
|
||||
for _, keyword := range infoKeywords {
|
||||
if strings.Contains(lowerEvent, keyword) ||
|
||||
strings.Contains(lowerStatus, keyword) {
|
||||
return models.SeverityInfo
|
||||
}
|
||||
}
|
||||
|
||||
// Default to info
|
||||
return models.SeverityInfo
|
||||
}
|
||||
|
||||
// buildSELDescription builds human-readable description
|
||||
func buildSELDescription(eventDesc, status string) string {
|
||||
if status == "Asserted" || status == "Deasserted" {
|
||||
return eventDesc
|
||||
}
|
||||
return eventDesc + " (" + status + ")"
|
||||
}
|
||||
Reference in New Issue
Block a user