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
+268
@@ -0,0 +1,268 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user