Files
logpile/internal/parser/vendors/inspur_legacy/asset_inventory.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

285 lines
7.2 KiB
Go

package inspurlegacy
import (
"regexp"
"strconv"
"strings"
"git.mchus.pro/mchus/logpile/internal/models"
)
// ParseAssetInfoInventory reads Inspur_AssetInfoInventory.log and fills CPU,
// memory, PCIe and power-supply inventory. The file is organised in bracketed
// sections ("[CPU]", "[Memory]", "[PCIE]", "[PSU]"); each data line is
// "<KEY>: k:v, k:v, ...".
func ParseAssetInfoInventory(content []byte, hw *models.HardwareConfig) {
section := ""
for _, rawLine := range strings.Split(string(content), "\n") {
line := strings.TrimSpace(rawLine)
if line == "" {
continue
}
if strings.HasPrefix(line, "[") && strings.HasSuffix(line, "]") {
section = strings.ToUpper(strings.Trim(line, "[]"))
continue
}
key, rest, ok := strings.Cut(line, ":")
if !ok {
continue
}
key = strings.TrimSpace(key)
kv := parseAssetKVList(rest)
switch section {
case "CPU":
if cpu, ok := assetCPU(key, kv); ok {
hw.CPUs = append(hw.CPUs, cpu)
}
case "MEMORY":
if dimm, ok := assetDIMM(key, kv); ok {
hw.Memory = append(hw.Memory, dimm)
}
case "PCIE":
if dev, ok := assetPCIe(kv); ok {
hw.PCIeDevices = append(hw.PCIeDevices, dev)
}
case "PSU":
if psu, ok := assetPSU(key, kv); ok {
hw.PowerSupply = append(hw.PowerSupply, psu)
}
}
}
}
// parseAssetKVList splits "k:v, k:v" into a map. Values may contain parenthesised
// text and colons (e.g. "Model:Intel(R) Xeon(R) ... @ 2.90GHz"); only the first
// colon of each comma-separated segment is treated as the separator, and commas
// inside parentheses do not split.
func parseAssetKVList(s string) map[string]string {
out := make(map[string]string)
for _, seg := range splitTopLevelComma(s) {
seg = strings.TrimSpace(seg)
if seg == "" {
continue
}
// Most pairs use "k:v"; the PCIe bus address uses "B.D.F=x.y.z".
sep := ":"
if !strings.Contains(seg, ":") && strings.Contains(seg, "=") {
sep = "="
}
k, v, ok := strings.Cut(seg, sep)
if !ok {
continue
}
out[strings.TrimSpace(k)] = strings.TrimSpace(v)
}
return out
}
func splitTopLevelComma(s string) []string {
var out []string
depth := 0
start := 0
for i, r := range s {
switch r {
case '(', '[':
depth++
case ')', ']':
if depth > 0 {
depth--
}
case ',':
if depth == 0 {
out = append(out, s[start:i])
start = i + 1
}
}
}
out = append(out, s[start:])
return out
}
var (
parenValueRe = regexp.MustCompile(`^([^(]*)\(([^)]*)\)`)
sizeGBRe = regexp.MustCompile(`(\d+)\s*GB`)
freqMHzRe = regexp.MustCompile(`(\d+)\s*MHz`)
wattRe = regexp.MustCompile(`(\d+)\s*W`)
ghzRe = regexp.MustCompile(`@\s*([\d.]+)\s*GHz`)
trailingIdx = regexp.MustCompile(`_(\d+)$`)
)
// outerParen returns the text between the first "(" and the last ")" of v, so
// "3(PCIE X24 SLOT0(J99) CPU0)" yields "PCIE X24 SLOT0(J99) CPU0".
func outerParen(v string) string {
open := strings.Index(v, "(")
shut := strings.LastIndex(v, ")")
if open < 0 || shut < 0 || shut <= open+1 {
return ""
}
return strings.TrimSpace(v[open+1 : shut])
}
// splitParenValue splits "26(DDR4)" into ("26", "DDR4").
func splitParenValue(v string) (before, inParen string) {
if m := parenValueRe.FindStringSubmatch(v); m != nil {
return strings.TrimSpace(m[1]), strings.TrimSpace(m[2])
}
return strings.TrimSpace(v), ""
}
func atoiSafe(s string) int {
n, _ := strconv.Atoi(strings.TrimSpace(s))
return n
}
// parseHexID parses "0x15B3" into 5555.
func parseHexID(s string) int {
s = strings.TrimSpace(s)
s = strings.TrimPrefix(strings.TrimPrefix(s, "0x"), "0X")
n, err := strconv.ParseInt(s, 16, 64)
if err != nil {
return 0
}
return int(n)
}
func assetCPU(key string, kv map[string]string) (models.CPU, bool) {
model := kv["Model"]
if model == "" {
return models.CPU{}, false
}
cpu := models.CPU{
Socket: socketFromKey(key),
Model: model,
Cores: atoiSafe(kv["Core"]),
TDP: atoiSafe(strings.TrimSuffix(kv["TDP"], "W")),
}
if m := ghzRe.FindStringSubmatch(model); m != nil {
if ghz, err := strconv.ParseFloat(m[1], 64); err == nil {
cpu.FrequencyMHz = int(ghz * 1000)
}
}
return cpu, true
}
func socketFromKey(key string) int {
if m := trailingIdx.FindStringSubmatch(key); m != nil {
return atoiSafe(m[1])
}
return 0
}
func assetDIMM(key string, kv map[string]string) (models.MemoryDIMM, bool) {
sizeGB := 0
if m := sizeGBRe.FindStringSubmatch(kv["Size"]); m != nil {
sizeGB = atoiSafe(m[1])
}
if sizeGB == 0 {
return models.MemoryDIMM{}, false
}
_, memType := splitParenValue(kv["Type"])
speed := 0
if m := freqMHzRe.FindStringSubmatch(kv["Freq"]); m != nil {
speed = atoiSafe(m[1])
}
// "MEM_0_CPU0_C0D0" -> slot "CPU0_C0D0", drop the "MEM_<idx>_" prefix.
slot := key
if parts := strings.SplitN(key, "_", 3); len(parts) == 3 {
slot = parts[2]
}
return models.MemoryDIMM{
Slot: slot,
Location: key,
Present: true,
SizeMB: sizeGB * 1024,
Type: memType,
MaxSpeedMHz: speed,
CurrentSpeedMHz: speed,
Manufacturer: kv["Manufacturer"],
PartNumber: kv["PN"],
SerialNumber: kv["SN"],
}, true
}
func assetPCIe(kv map[string]string) (models.PCIeDevice, bool) {
if len(kv) == 0 {
return models.PCIeDevice{}, false
}
_, class := splitParenValue(kv["Type"])
vendHex, vendName := splitParenValue(kv["VendorID"])
devHex, devName := splitParenValue(kv["DevieID"]) // "DevieID" spelled as in source
if devName == "" {
_, devName = splitParenValue(kv["DeviceID"])
devHex, _ = splitParenValue(kv["DeviceID"])
}
widthNum, _ := splitParenValue(kv["Width"])
_, speedName := splitParenValue(kv["Speed"])
slotName := outerParen(kv["Slot"])
if slotName == "" {
slotName = strings.TrimSpace(kv["Slot"])
}
dev := models.PCIeDevice{
Slot: slotName,
DeviceClass: class,
Manufacturer: vendName,
Model: devName,
VendorID: parseHexID(vendHex),
DeviceID: parseHexID(devHex),
BDF: bdfFromDecimal(kv["B.D.F"]),
LinkWidth: atoiSafe(widthNum),
LinkSpeed: speedName,
}
if dev.DeviceClass == "" && dev.BDF == "" && dev.Model == "" {
return models.PCIeDevice{}, false
}
return dev, true
}
// bdfFromDecimal converts the AssetInfo "bus.dev.func" decimal triple into the
// canonical hex "0000:bb:dd.f" BDF. Domain is not reported and assumed 0.
func bdfFromDecimal(s string) string {
parts := strings.Split(strings.TrimSpace(s), ".")
if len(parts) != 3 {
return ""
}
bus := atoiSafe(parts[0])
dev := atoiSafe(parts[1])
fn := atoiSafe(parts[2])
return strings.ToLower(
zeroPadHex(0, 4) + ":" + zeroPadHex(bus, 2) + ":" + zeroPadHex(dev, 2) + "." + strconv.Itoa(fn),
)
}
func zeroPadHex(n, width int) string {
h := strconv.FormatInt(int64(n), 16)
for len(h) < width {
h = "0" + h
}
return h
}
func assetPSU(key string, kv map[string]string) (models.PSU, bool) {
model := kv["Model"]
vendor := kv["Manufacturer"]
if model == "" && vendor == "" {
return models.PSU{}, false
}
watt := 0
if m := wattRe.FindStringSubmatch(kv["RatedPower"]); m != nil {
watt = atoiSafe(m[1])
}
slot := strings.ReplaceAll(key, "_", "")
return models.PSU{
Slot: slot,
Present: true,
Model: model,
Vendor: vendor,
SerialNumber: kv["SN"],
PartNumber: kv["PN"],
Firmware: kv["FW"],
WattageW: watt,
}, true
}