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>
175 lines
4.5 KiB
Go
175 lines
4.5 KiB
Go
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 + ")"
|
|
}
|