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
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
be37f1852b
commit
3311bafd8e
@@ -0,0 +1,284 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user