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

269 lines
7.1 KiB
Go

package inspurlegacy
import (
"strings"
"time"
"git.mchus.pro/mchus/logpile/internal/models"
)
// binaryFRU holds the fields decoded from a binary IPMI FRU image (FRU.bin).
type binaryFRU struct {
ChassisType string
ChassisPart string
ChassisSerial string
BoardManufacturer string
BoardProduct string
BoardSerial string
BoardPart string
BoardMfgDate string
ProductManufacturer string
ProductName string
ProductPart string
ProductVersion string
ProductSerial string
ProductAssetTag string
}
// chassisTypeNames maps IPMI/SMBIOS chassis type codes to names. Only codes seen
// on Inspur rack servers are listed; unknown codes fall back to the raw number.
var chassisTypeNames = map[byte]string{
0x03: "Desktop",
0x11: "Main Server Chassis",
0x17: "Rack Mount Chassis",
0x1C: "Blade",
0x1D: "Blade Enclosure",
}
// fruEpoch is the IPMI FRU manufacturing-date epoch (1996-01-01 00:00 UTC).
var fruEpoch = time.Date(1996, 1, 1, 0, 0, 0, 0, time.UTC)
// DecodeBinaryFRU parses a binary IPMI FRU image. It returns false when the
// common header is missing or no area yields any field.
func DecodeBinaryFRU(data []byte) (binaryFRU, bool) {
var fru binaryFRU
if len(data) < 8 || data[0] != 0x01 {
return fru, false
}
// Common-header area offsets are stored in 8-byte units.
chassisOff := int(data[2]) * 8
boardOff := int(data[3]) * 8
productOff := int(data[4]) * 8
if chassisOff >= 3 {
decodeChassisArea(data, chassisOff, &fru)
}
if boardOff >= 3 {
decodeBoardArea(data, boardOff, &fru)
}
if productOff >= 3 {
decodeProductArea(data, productOff, &fru)
}
if fru == (binaryFRU{}) {
return fru, false
}
return fru, true
}
// areaEnd returns the exclusive end offset of an info area beginning at off,
// clamped to the buffer. ok is false when the area header is invalid.
func areaEnd(data []byte, off int) (end int, ok bool) {
if off < 0 || off+2 > len(data) || data[off] != 0x01 {
return 0, false
}
end = off + int(data[off+1])*8
if end <= off || end > len(data) {
end = len(data)
}
return end, true
}
// readField decodes one IPMI type/length-encoded field starting at pos.
// done is true at the 0xC1 end marker or when the buffer is exhausted.
func readField(data []byte, pos, end int) (value string, next int, done bool) {
if pos >= end || pos >= len(data) {
return "", pos, true
}
tl := data[pos]
// Note: 0xC1 is the IPMI "end of fields" sentinel, but this Inspur FRU also
// uses 0xC1 to encode legitimate single-character type-3 strings (the "0"
// placeholders). Terminate only on area end, zero padding or 0xFF instead.
if tl == 0x00 || tl == 0xFF {
return "", pos + 1, true
}
typ := tl >> 6
length := int(tl & 0x3F)
if pos+1+length > end || pos+1+length > len(data) {
return "", end, true
}
raw := data[pos+1 : pos+1+length]
next = pos + 1 + length
switch typ {
case 0x03, 0x00: // 8-bit ASCII+Latin1, or unspecified/binary treated as text
return strings.TrimSpace(string(raw)), next, false
case 0x02: // 6-bit packed ASCII
return decode6bitASCII(raw), next, false
case 0x01: // BCD plus
return decodeBCDPlus(raw), next, false
default:
return "", next, false
}
}
func decode6bitASCII(raw []byte) string {
var b strings.Builder
var acc uint32
var bits uint
for _, by := range raw {
acc |= uint32(by) << bits
bits += 8
for bits >= 6 {
b.WriteByte(byte(acc&0x3F) + 0x20)
acc >>= 6
bits -= 6
}
}
return strings.TrimSpace(b.String())
}
func decodeBCDPlus(raw []byte) string {
const digits = "0123456789 -. " // 0xA space, 0xB dash, 0xC dot
var b strings.Builder
for _, by := range raw {
hi, lo := by>>4, by&0x0F
if int(hi) < len(digits) {
b.WriteByte(digits[hi])
}
if int(lo) < len(digits) {
b.WriteByte(digits[lo])
}
}
return strings.TrimSpace(b.String())
}
// fieldList reads consecutive type/length fields from start up to the area end
// or the 0xC1 marker. Fixed-position fields map to slots by index; trailing
// custom fields are ignored.
func fieldList(data []byte, start, end int) []string {
var out []string
pos := start
for {
v, next, done := readField(data, pos, end)
if done {
break
}
out = append(out, v)
pos = next
if len(out) > 16 {
break
}
}
return out
}
func at(list []string, i int) string {
if i < len(list) {
return list[i]
}
return ""
}
func decodeChassisArea(data []byte, off int, fru *binaryFRU) {
end, ok := areaEnd(data, off)
if !ok || off+3 > len(data) {
return
}
if name, known := chassisTypeNames[data[off+2]]; known {
fru.ChassisType = name
}
f := fieldList(data, off+3, end)
fru.ChassisPart = at(f, 0)
fru.ChassisSerial = at(f, 1)
}
func decodeBoardArea(data []byte, off int, fru *binaryFRU) {
end, ok := areaEnd(data, off)
if !ok || off+6 > len(data) {
return
}
mins := int(data[off+3]) | int(data[off+4])<<8 | int(data[off+5])<<16
if mins > 0 {
fru.BoardMfgDate = fruEpoch.Add(time.Duration(mins) * time.Minute).Format("2006-01-02")
}
f := fieldList(data, off+6, end)
fru.BoardManufacturer = at(f, 0)
fru.BoardProduct = at(f, 1)
fru.BoardSerial = at(f, 2)
fru.BoardPart = at(f, 3)
}
func decodeProductArea(data []byte, off int, fru *binaryFRU) {
end, ok := areaEnd(data, off)
if !ok || off+3 > len(data) {
return
}
f := fieldList(data, off+3, end)
fru.ProductManufacturer = at(f, 0)
fru.ProductName = at(f, 1)
fru.ProductPart = at(f, 2)
fru.ProductVersion = at(f, 3)
fru.ProductSerial = at(f, 4)
fru.ProductAssetTag = at(f, 5)
}
// placeholder reports whether a decoded FRU value is an empty-ish placeholder
// ("", "0", "NULL", "N/A") that must not overwrite real data.
func placeholder(s string) bool {
switch strings.ToUpper(strings.TrimSpace(s)) {
case "", "0", "NULL", "N/A", "NONE", "TO BE FILLED BY O.E.M.":
return true
}
return false
}
func firstReal(vals ...string) string {
for _, v := range vals {
if !placeholder(v) {
return strings.TrimSpace(v)
}
}
return ""
}
// toFRUInfo renders the decoded image as a single builtin FRU record.
func (f binaryFRU) toFRUInfo() models.FRUInfo {
return models.FRUInfo{
Description: "Builtin FRU Device (ID 0)",
ChassisType: f.ChassisType,
Manufacturer: firstReal(f.ProductManufacturer, f.BoardManufacturer),
ProductName: firstReal(f.ProductName, f.BoardProduct),
SerialNumber: firstReal(f.ProductSerial, f.BoardSerial, f.ChassisSerial),
PartNumber: firstReal(f.ProductPart, f.BoardPart, f.ChassisPart),
Version: firstReal(f.ProductVersion),
AssetTag: firstReal(f.ProductAssetTag),
MfgDate: f.BoardMfgDate,
}
}
// applyToBoardInfo fills empty BoardInfo identity fields. The product area holds
// the operator-facing system serial; the board area holds the PCB serial and,
// on this hardware, the only real motherboard part number.
func (f binaryFRU) applyToBoardInfo(b *models.BoardInfo) {
if b.Manufacturer == "" {
b.Manufacturer = firstReal(f.ProductManufacturer, f.BoardManufacturer)
}
if b.ProductName == "" {
b.ProductName = firstReal(f.ProductName, f.BoardProduct)
}
if b.SerialNumber == "" {
b.SerialNumber = firstReal(f.ProductSerial, f.BoardSerial)
}
if b.PartNumber == "" {
b.PartNumber = firstReal(f.BoardPart, f.ProductPart)
}
}