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>
170 lines
4.3 KiB
Go
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
|
|
}
|
|
}
|