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
285 lines
7.2 KiB
Go
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
|
|
}
|