Files
logpile/internal/parser/vendors/inspur_legacy/parser.go
T
Mikhail ChusavitinandClaude Sonnet 5 3311bafd8e feat(inspur_legacy): new parser for pre-Kaytus Inspur onekeylog
Older AMI-BMC Inspur onekeylog archives (NF5466M5 / NF5280M5 generation)
opened to an empty result: they carry none of the files the inspur parser
keys on (no asset.json, devicefrusdr.log, selelist.csv or component.log).

New package internal/parser/vendors/inspur_legacy (vendor id inspur_legacy),
separate from inspur:
- binary IPMI FRU decode (FRU.bin) -> board identity
- Inspur_AssetInfoInventory.log -> CPU / memory / PCIe / PSU inventory
- events from Inspur_<model>_<serial>_IDL, sel.log, blackbox.log,
  MegaRAID raid0.log, and the flat AMI <severity>.log files
- no live sensors in this archive class -> recorded as a collection error
- BMC clock timestamps before 2010 dropped as un-set (1970 / ~2005 RTC)

Registry: add optional PrioritizedParser { DetectPriority() int } so a
confidence tie is broken by specificity. inspur_legacy returns 10 and also
declines (Detect 0) when modern Kaytus markers are present, so the two
Inspur parsers never fight over a newer dump.

Docs: ADL-065, 06-parsers.md, releases/v1.32.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017VL8wLGD6Lnp6hZCpqT6cZ
2026-09-02 12:51:53 +03:00

192 lines
6.0 KiB
Go

// Package inspurlegacy parses the legacy Inspur "onekeylog" BMC diagnostic
// archive produced by older AMI-based Inspur BMCs (e.g. NF5466M5 / NF5280M5
// generation). That format predates the Kaytus-style onekeylog handled by the
// internal/parser/vendors/inspur package: it has no asset.json, no
// devicefrusdr.log, no selelist.csv and no component.log. Instead hardware
// inventory lives in Inspur_AssetInfoInventory.log, FRU data in a binary
// FRU.bin, the event log in an Inspur_<model>_<serial>_IDL text file plus a
// pipe-delimited sel.log, and BMC syslog in flat <sev>.log files at the archive
// root.
//
// IMPORTANT: bump parserVersion on any behavior change (see module-versioning).
package inspurlegacy
import (
"strings"
"git.mchus.pro/mchus/logpile/internal/models"
"git.mchus.pro/mchus/logpile/internal/parser"
)
// parserVersion follows the N.M module-versioning scheme.
const parserVersion = "1.0"
func init() {
parser.Register(&Parser{})
}
// Parser implements parser.VendorParser for the legacy Inspur onekeylog format.
type Parser struct{}
// Name returns a human-readable parser name.
func (p *Parser) Name() string { return "Inspur Legacy onekeylog BMC Parser" }
// Vendor returns the parser identifier.
func (p *Parser) Vendor() string { return "inspur_legacy" }
// Version returns the parser module version.
func (p *Parser) Version() string { return parserVersion }
// DetectPriority makes this parser win a confidence tie against the broader
// Kaytus-style Inspur parser, which also matches anything under onekeylog/.
func (p *Parser) DetectPriority() int { return 10 }
// modernMarkers are files that only appear in the newer Kaytus-style onekeylog.
// Their presence means the other Inspur parser should handle the archive.
var modernMarkers = []string{
"asset.json",
"devicefrusdr.log",
"selelist.csv",
"component.log",
"onekeylog_dreport.log",
}
// Detect returns a confidence score for the legacy onekeylog format.
func (p *Parser) Detect(files []parser.ExtractedFile) int {
confidence := 0
for _, f := range files {
base := strings.ToLower(baseName(f.Path))
for _, m := range modernMarkers {
if base == m {
return 0
}
}
switch {
case base == "inspur_assetinfoinventory.log":
confidence += 40
case isLegacyIDLName(base):
confidence += 30
case base == "fru.bin":
confidence += 15
case base == "blackbox.log":
confidence += 10
case base == "eepromdata.log":
confidence += 10
case base == "sdr.dat":
confidence += 10
case base == "sadtadreg.dat":
confidence += 5
}
}
if confidence > 100 {
return 100
}
return confidence
}
// Parse extracts events, FRU and hardware inventory from a legacy onekeylog.
func (p *Parser) Parse(files []parser.ExtractedFile) (*models.AnalysisResult, error) {
result := &models.AnalysisResult{
Events: make([]models.Event, 0),
FRU: make([]models.FRUInfo, 0),
Sensors: make([]models.SensorReading, 0),
}
hw := &models.HardwareConfig{}
result.Hardware = hw
if f := parser.FindFileByName(files, "FRU.bin"); f != nil {
if fru, ok := DecodeBinaryFRU(f.Content); ok {
result.FRU = append(result.FRU, fru.toFRUInfo())
fru.applyToBoardInfo(&hw.BoardInfo)
}
}
if f := parser.FindFileByName(files, "Inspur_AssetInfoInventory.log"); f != nil {
ParseAssetInfoInventory(f.Content, hw)
}
// Event sources, most structured first.
for _, f := range findLegacyIDLFiles(files) {
result.Events = append(result.Events, ParseIDLEvents(f.Content)...)
}
if f := parser.FindFileByName(files, "sel.log"); f != nil {
result.Events = append(result.Events, ParseSELLog(f.Content)...)
}
if f := parser.FindFileByName(files, "blackbox.log"); f != nil {
result.Events = append(result.Events, ParseBlackbox(f.Content)...)
}
for _, f := range findRAIDLogFiles(files) {
result.Events = append(result.Events, ParseMegaRAIDLog(f.Content, baseName(f.Path))...)
}
for _, f := range findSyslogFiles(files) {
result.Events = append(result.Events, ParseAMISyslog(f.Content, baseName(f.Path))...)
}
result.Events = dedupeEvents(result.Events)
sortEvents(result.Events)
// This archive class carries no live sensor readings: SDR.dat holds only
// sensor definitions, and there is no "sensor list" capture. Record why the
// sensor set is empty instead of leaving it silently unexplained.
result.CollectionErrors = append(result.CollectionErrors, models.CollectionError{
Section: "sensors",
Message: "legacy onekeylog carries no live sensor readings (SDR.dat holds definitions only, no sensor-list capture)",
})
return result, nil
}
func baseName(path string) string {
path = strings.ReplaceAll(path, "\\", "/")
if i := strings.LastIndex(path, "/"); i >= 0 {
return path[i+1:]
}
return path
}
// isLegacyIDLName matches the "Inspur_<model>_<serial>_IDL" event-log filename
// (no extension), lower-cased.
func isLegacyIDLName(base string) bool {
return strings.HasPrefix(base, "inspur_") && strings.HasSuffix(base, "_idl")
}
func findLegacyIDLFiles(files []parser.ExtractedFile) []parser.ExtractedFile {
var out []parser.ExtractedFile
for _, f := range files {
if isLegacyIDLName(strings.ToLower(baseName(f.Path))) {
out = append(out, f)
}
}
return out
}
func findRAIDLogFiles(files []parser.ExtractedFile) []parser.ExtractedFile {
var out []parser.ExtractedFile
for _, f := range files {
base := strings.ToLower(baseName(f.Path))
if strings.HasPrefix(base, "raid") && strings.HasSuffix(base, ".log") {
out = append(out, f)
}
}
return out
}
// syslogFileNames are the flat AMI BMC severity logs at the archive root that
// carry fault-relevant records. debug.log and audit.log are intentionally left
// out: they are high-volume housekeeping noise, not diagnostics.
var syslogFileNames = map[string]struct{}{
"emerg.log": {}, "alert.log": {}, "crit.log": {}, "err.log": {}, "warning.log": {},
}
func findSyslogFiles(files []parser.ExtractedFile) []parser.ExtractedFile {
var out []parser.ExtractedFile
for _, f := range files {
if _, ok := syslogFileNames[strings.ToLower(baseName(f.Path))]; ok {
out = append(out, f)
}
}
return out
}