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
269 lines
7.1 KiB
Go
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)
|
|
}
|
|
}
|