fix(parser): support Inspur HGX dump_<serial>_<timestamp>/ onekeylog layout
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>
This commit is contained in:
@@ -0,0 +1,104 @@
|
||||
// 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
|
||||
}
|
||||
Reference in New Issue
Block a user