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:
+169
@@ -0,0 +1,169 @@
|
||||
// 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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user