Files
Mikhail ChusavitinandClaude Sonnet 5 2fa0f78f94 fix(inspur): read RESTful FRU info and float fans_power from component.log
Diffing an NF5280M6 BMC dump against its BEE-SP live-CD bundle found two
blind spots in the combined-component.log onekeylog layout (no
devicefrusdr.log / asset.json):

- board manufacturer/product/part/uuid empty and stats.fru 0: the
  "RESTful FRU info:" JSON block was never parsed. New component_fru.go
  (ParseComponentLogFRU) flattens it to []models.FRUInfo, prefers the
  product-area system serial over the board PCB serial, and sets
  BoardInfo.UUID from system_uuid. Wired as a fallback only when
  result.FRU is still empty.
- zero fan sensors: FanRESTInfo.FansPower was int but this firmware
  writes "fans_power": 12.000000, so json.Unmarshal of the whole fan
  block failed. Changed to float64.

Also included: SOL smartd SCSI/SAS device-line parsing and diagnose.go
gofmt from concurrent work on the same live-CD-diff task. See ADL-064.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011LffAvostt3uMkiUbVUiyM
2026-09-01 11:58:55 +03:00

115 lines
3.8 KiB
Go

package inspur
import (
"encoding/json"
"regexp"
"strings"
"git.mchus.pro/mchus/logpile/internal/models"
)
// componentLogFRURegex isolates the JSON array that follows "RESTful FRU info:"
// in a combined component.log. FRU areas carry no nested arrays, so the lazy
// match stops at the array's own closing bracket.
var componentLogFRURegex = regexp.MustCompile(`RESTful FRU info:\s*(\[[\s\S]*?\])`)
type componentFRUArea struct {
Version int `json:"version"`
Type string `json:"type"`
Date string `json:"date"`
Manufacturer string `json:"manufacturer"`
ProductName string `json:"product_name"`
SerialNumber string `json:"serial_number"`
PartNumber string `json:"part_number"`
AssetTag string `json:"asset_tag"`
}
type componentFRUDevice struct {
Device struct {
ID int `json:"id"`
Name string `json:"name"`
SystemUUID string `json:"system_uuid"`
} `json:"device"`
Chassis componentFRUArea `json:"chassis"`
Board componentFRUArea `json:"board"`
Product componentFRUArea `json:"product"`
}
// ParseComponentLogFRU parses the "RESTful FRU info:" JSON block from a combined
// component.log. Some onekeylog dumps ship this block instead of a
// devicefrusdr.log / asset.json, so it is the only source of board manufacturer,
// product name, part number and system UUID for that dump class. It also sets
// hw.BoardInfo.UUID directly, which extractBoardInfo does not populate.
func ParseComponentLogFRU(content []byte, hw *models.HardwareConfig) []models.FRUInfo {
m := componentLogFRURegex.FindSubmatch(content)
if m == nil {
return nil
}
var devices []componentFRUDevice
if err := json.Unmarshal(m[1], &devices); err != nil {
return nil
}
var out []models.FRUInfo
for _, d := range devices {
fru := componentFRUToModel(d)
if fru.Manufacturer == "" && fru.ProductName == "" && fru.SerialNumber == "" && fru.PartNumber == "" {
continue
}
out = append(out, fru)
if hw != nil && hw.BoardInfo.UUID == "" && isSystemFRUDevice(d.Device.Name) {
if u := fruAreaValue(d.Device.SystemUUID); u != "" {
hw.BoardInfo.UUID = u
}
}
}
return out
}
func isSystemFRUDevice(name string) bool {
n := strings.ToUpper(strings.TrimSpace(name))
return n == "BMC_FRU" || n == "MB_FRU" || strings.HasPrefix(n, "MAINBOARD")
}
// componentFRUToModel flattens a FRU device into one models.FRUInfo. The product
// area (system serial / asset tag the operator knows, e.g. "24C319579") is
// preferred over the board area (physical PCB serial) for identity, matching the
// BEE-SP live-CD inventory and the Reanimator board contract.
func componentFRUToModel(d componentFRUDevice) models.FRUInfo {
desc := strings.TrimSpace(d.Device.Name)
if desc == "" {
desc = "FRU Device"
}
// extractBoardInfo only lifts manufacturer from an entry it recognises as the
// main board; tag the system FRU so it does.
if isSystemFRUDevice(d.Device.Name) {
desc += " (builtin board)"
}
return models.FRUInfo{
Description: desc,
ChassisType: fruAreaValue(d.Chassis.Type),
Manufacturer: fruAreaValue(d.Product.Manufacturer, d.Board.Manufacturer),
ProductName: fruAreaValue(d.Product.ProductName, d.Board.ProductName),
SerialNumber: fruAreaValue(d.Product.SerialNumber, d.Board.SerialNumber),
PartNumber: fruAreaValue(d.Product.PartNumber, d.Board.PartNumber, d.Chassis.PartNumber),
MfgDate: fruAreaValue(d.Board.Date),
AssetTag: fruAreaValue(d.Product.AssetTag),
}
}
// fruAreaValue returns the first argument that is a real value. iBMC uses "0",
// "NULL" and whitespace-padded blanks as placeholders in unused FRU areas.
func fruAreaValue(values ...string) string {
for _, v := range values {
t := strings.TrimSpace(v)
if t == "" || t == "0" || strings.EqualFold(t, "null") || strings.EqualFold(t, "n/a") {
continue
}
return t
}
return ""
}