Files
mchusandClaude Sonnet 5 74e6bf0578 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>
2026-07-29 10:05:58 +03:00

170 lines
4.3 KiB
Go

// Parses log/bmc/commer-comp/*, per-component BMC transcripts (slot presence,
// HGX management controller, switch VR/power, switch CPLD). Each rotated log
// (component.log plus component.tar.gz.0..9) uses the same
// "[timestamp][file, line][level] message" line format.
package inspur
import (
"archive/tar"
"bufio"
"bytes"
"compress/gzip"
"fmt"
"io"
"regexp"
"strings"
"time"
"git.mchus.pro/mchus/logpile/internal/models"
"git.mchus.pro/mchus/logpile/internal/parser"
)
var commerCompComponents = []string{"commerslot", "commerhmc", "commerswvr", "commerswcpld"}
var commerCompLineRegex = regexp.MustCompile(`^\[(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})\]\[[^\]]*\]\[(\w+)\]\s*(.+)$`)
var commerCompNoisePatterns = []*regexp.Regexp{
regexp.MustCompile(`(?i)invalid comp data`),
regexp.MustCompile(`(?i)error in update redis`),
regexp.MustCompile(`(?i)mutextimeout\s*=\s*0`),
}
var commerCompFailureRegex = regexp.MustCompile(`(?i)\b(fail|failed|failure|error)\b`)
// ParseCommerCompEvents extracts BMC component failure events from
// log/bmc/commer-comp/<component>/ logs, including rotated *.tar.gz.N parts.
func ParseCommerCompEvents(files []parser.ExtractedFile, location *time.Location) []models.Event {
var events []models.Event
for _, f := range files {
path := strings.ToLower(f.Path)
if !strings.Contains(path, "/commer-comp/") {
continue
}
component := commerCompComponentFromPath(path)
if component == "" {
continue
}
if strings.Contains(path, ".tar.gz") {
for _, part := range decodeNestedTarGz(f.Content) {
events = append(events, parseCommerCompLog(part, component, location)...)
}
continue
}
if !strings.HasSuffix(path, ".log") {
continue
}
events = append(events, parseCommerCompLog(f.Content, component, location)...)
}
return events
}
func commerCompComponentFromPath(path string) string {
for _, c := range commerCompComponents {
if strings.Contains(path, "/"+c+"/") {
return c
}
}
return ""
}
// decodeNestedTarGz unpacks a rotated log part (a full tar.gz archive stored
// as e.g. commerhmc.tar.gz.0) and returns the contents of every regular file
// inside it.
func decodeNestedTarGz(content []byte) [][]byte {
gz, err := gzip.NewReader(bytes.NewReader(content))
if err != nil {
return nil
}
defer gz.Close()
tr := tar.NewReader(gz)
var out [][]byte
for {
hdr, err := tr.Next()
if err == io.EOF {
break
}
if err != nil {
break
}
if hdr.Typeflag != tar.TypeReg {
continue
}
buf, err := io.ReadAll(tr)
if err != nil {
continue
}
out = append(out, buf)
}
return out
}
func parseCommerCompLog(content []byte, component string, location *time.Location) []models.Event {
var events []models.Event
scanner := bufio.NewScanner(bytes.NewReader(content))
scanner.Buffer(make([]byte, 0, 64*1024), 1024*1024)
for scanner.Scan() {
line := scanner.Text()
m := commerCompLineRegex.FindStringSubmatch(line)
if m == nil {
continue
}
msg := strings.TrimSpace(m[3])
if isCommerCompNoise(msg) || !commerCompFailureRegex.MatchString(msg) {
continue
}
ts, err := time.ParseInLocation("2006-01-02 15:04:05", m[1], location)
if err != nil {
continue
}
events = append(events, models.Event{
ID: fmt.Sprintf("commer_comp_%s_%d_%s", component, ts.Unix(), commerCompIDSuffix(msg)),
Timestamp: ts,
Source: fmt.Sprintf("BMC/%s", component),
SensorType: "bmc_component",
EventType: m[2],
Severity: commerCompSeverity(m[2]),
Description: msg,
RawData: line,
})
}
return events
}
func isCommerCompNoise(msg string) bool {
for _, re := range commerCompNoisePatterns {
if re.MatchString(msg) {
return true
}
}
return false
}
var commerCompNonAlnumRegex = regexp.MustCompile(`[^a-zA-Z0-9]+`)
// commerCompIDSuffix derives a short, stable suffix from the message so that
// two different failures logged in the same second get distinct event IDs.
func commerCompIDSuffix(msg string) string {
s := commerCompNonAlnumRegex.ReplaceAllString(strings.ToLower(msg), "_")
s = strings.Trim(s, "_")
if len(s) > 40 {
s = s[:40]
}
return s
}
func commerCompSeverity(level string) models.Severity {
switch strings.ToLower(level) {
case "critial", "critical", "error":
return models.SeverityCritical
case "warning", "warn":
return models.SeverityWarning
default:
return models.SeverityInfo
}
}