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>
105 lines
2.7 KiB
Go
105 lines
2.7 KiB
Go
// Fallback inventory parsing for the per-file dump_<serial>_<timestamp>/ onekeylog
|
|
// layout, which has no combined devicefrusdr.log. Instead FRU and sensors live in
|
|
// separate ipmitool-output text files under component/.
|
|
package inspur
|
|
|
|
import (
|
|
"bufio"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"git.mchus.pro/mchus/logpile/internal/models"
|
|
)
|
|
|
|
// ParseSensorList parses component/sensor.txt ("ipmitool sensor list" output).
|
|
// Line format: Name | Value | Unit | Status | lnr | lcr | lnc | unc | ucr | unr
|
|
func ParseSensorList(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.Contains(line, "|") {
|
|
continue
|
|
}
|
|
fields := strings.Split(line, "|")
|
|
if len(fields) < 4 {
|
|
continue
|
|
}
|
|
name := strings.TrimSpace(fields[0])
|
|
valueStr := strings.TrimSpace(fields[1])
|
|
unit := strings.TrimSpace(fields[2])
|
|
status := strings.TrimSpace(fields[3])
|
|
if name == "" {
|
|
continue
|
|
}
|
|
|
|
reading := models.SensorReading{
|
|
Name: name,
|
|
Status: status,
|
|
Unit: unit,
|
|
Type: determineSensorType(name),
|
|
}
|
|
|
|
if valueStr != "" && valueStr != "na" {
|
|
if v, err := strconv.ParseFloat(valueStr, 64); err == nil {
|
|
reading.Value = v
|
|
reading.RawValue = valueStr + " " + unit
|
|
} else {
|
|
reading.RawValue = valueStr
|
|
}
|
|
}
|
|
|
|
readings = append(readings, reading)
|
|
}
|
|
|
|
return readings
|
|
}
|
|
|
|
// ParseSDRElist parses component/sdr.txt ("ipmitool sdr elist" output), used as a
|
|
// fallback when component/sensor.txt is unavailable.
|
|
// Line format: Name | ID | Status | Entity | Reading (e.g. "21 degrees C" or "no reading")
|
|
func ParseSDRElist(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.Contains(line, "|") {
|
|
continue
|
|
}
|
|
fields := strings.Split(line, "|")
|
|
if len(fields) < 3 {
|
|
continue
|
|
}
|
|
name := strings.TrimSpace(fields[0])
|
|
status := strings.TrimSpace(fields[2])
|
|
if name == "" {
|
|
continue
|
|
}
|
|
|
|
reading := models.SensorReading{
|
|
Name: name,
|
|
Status: status,
|
|
Type: determineSensorType(name),
|
|
}
|
|
|
|
if len(fields) >= 5 {
|
|
readingStr := strings.TrimSpace(fields[4])
|
|
if readingStr != "" && readingStr != "no reading" {
|
|
reading.RawValue = readingStr
|
|
if m := valueRegex.FindStringSubmatch(readingStr); m != nil {
|
|
if v, err := strconv.ParseFloat(m[1], 64); err == nil {
|
|
reading.Value = v
|
|
reading.Unit = strings.TrimSpace(m[2])
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
readings = append(readings, reading)
|
|
}
|
|
|
|
return readings
|
|
}
|