Fixes #20. This onekeylog variant has no devicefrusdr.log at all: FRU/sensors come from raw ipmitool text output, PCIe/GPU presence has a dedicated structural snapshot, SEL lives at a different path, and BMC component failures are logged separately from SEL/IDL. - Fall back to component/fru.txt (same FRU block format as devicefrusdr.log) and component/sensor.txt / sdr.txt (ipmitool sensor list / sdr elist) when devicefrusdr.log is absent. - Parse log/bmc/diagnose/OtrdDiagnoseComponent.json's PCIe Device Info array for GPU/PCIe presence and link state, independent of SEL/IDL alarm history; flag devices running below their negotiated max link speed/width as degraded with a Warning event. - Fall back to log/sel.csv (same format as selelist.csv) when selelist.csv is absent. - Parse log/bmc/commer-comp/{commerslot,commerhmc,commerswvr,commerswcpld} logs (including rotated *.tar.gz.N parts) into failure events, filtering known-noisy lines. - Collapse SEL events duplicated across sources by (timestamp, event_type, description). - Surface a CollectionError when FRU/sensors are still empty after all fallbacks, instead of silently returning an empty inventory. - Fix ParseFRU: a later placeholder "Product Serial : 0" / "Product Part Number : NULL" line in the same FRU block (e.g. SCM_FRU) was overwriting an already-parsed real Board Serial/Part Number. Verified against dump_23DB01633_20260727-1359.tar.gz (HGX B200, KR9288-X3): fru 0→21, sensors 0→303, 8 GPUs present at Gen5 x16 in slots 100-107. Deferred (not covered by this change): BIOS-change-settings context and BIOS POST codes from the same layout — see bible-local/10-decisions.md ADL-048. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
212 lines
5.8 KiB
Go
212 lines
5.8 KiB
Go
package inspur
|
|
|
|
import (
|
|
"encoding/csv"
|
|
"strings"
|
|
"time"
|
|
|
|
"git.mchus.pro/mchus/logpile/internal/models"
|
|
"git.mchus.pro/mchus/logpile/internal/parser"
|
|
)
|
|
|
|
// 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 {
|
|
return ParseSELListWithLocation(content, parser.DefaultArchiveLocation())
|
|
}
|
|
|
|
// ParseSELListWithLocation parses selelist.csv using provided source timezone
|
|
// for timestamps that don't contain an explicit offset.
|
|
func ParseSELListWithLocation(content []byte, location *time.Location) []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, location)
|
|
|
|
// 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, location *time.Location) time.Time {
|
|
// Combine date and time: MM/DD/YYYY HH:MM:SS
|
|
timestampStr := dateStr + " " + timeStr
|
|
|
|
if location == nil {
|
|
location = parser.DefaultArchiveLocation()
|
|
}
|
|
|
|
// Try parsing with MM/DD/YYYY format
|
|
t, err := time.ParseInLocation("01/02/2006 15:04:05", timestampStr, location)
|
|
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
|
|
}
|
|
|
|
// dedupSELEvents collapses events reported more than once with the same
|
|
// (timestamp, event_type, description) triple. Unlike ParseIDLLog's
|
|
// dedup (which must keep recurring alarms with distinct timestamps), this
|
|
// only removes true duplicates: the same SEL entry surfacing through more
|
|
// than one source file for the exact same moment.
|
|
func dedupSELEvents(events []models.Event) []models.Event {
|
|
if len(events) == 0 {
|
|
return events
|
|
}
|
|
seen := make(map[string]struct{}, len(events))
|
|
out := make([]models.Event, 0, len(events))
|
|
for _, e := range events {
|
|
if e.Source != "SEL" {
|
|
out = append(out, e)
|
|
continue
|
|
}
|
|
key := e.Timestamp.String() + "|" + e.EventType + "|" + e.Description
|
|
if _, ok := seen[key]; ok {
|
|
continue
|
|
}
|
|
seen[key] = struct{}{}
|
|
out = append(out, e)
|
|
}
|
|
return out
|
|
}
|
|
|
|
// buildSELDescription builds human-readable description
|
|
func buildSELDescription(eventDesc, status string) string {
|
|
if status == "Asserted" || status == "Deasserted" {
|
|
return eventDesc
|
|
}
|
|
return eventDesc + " (" + status + ")"
|
|
}
|