Files
logpile/internal/parser/vendors/inspur/sel.go
Mikhail Chusavitin 21f4e5a67e 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>
2026-01-30 12:30:18 +03:00

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 + ")"
}