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
This commit is contained in:
Mikhail Chusavitin
2026-09-01 11:58:55 +03:00
co-authored by Claude Sonnet 5
parent ab8636da04
commit 2fa0f78f94
9 changed files with 780 additions and 99 deletions
+236 -57
View File
@@ -4,6 +4,8 @@ import (
"encoding/json"
"fmt"
"regexp"
"sort"
"strconv"
"strings"
"time"
@@ -504,60 +506,59 @@ type FanRESTInfo struct {
MaxSpeedRPM int `json:"max_speed_rpm"`
FanModel string `json:"fan_model"`
} `json:"fans"`
FansPower int `json:"fans_power"`
// fans_power is reported as a float ("12.000000") on some BMC firmware;
// an int type here makes json.Unmarshal fail and drops every fan reading.
FansPower float64 `json:"fans_power"`
}
// NetworkAdapterRESTInfo represents the RESTful Network Adapter info structure
type NetworkAdapterRESTInfo struct {
SysAdapters []struct {
ID int `json:"id"`
Name string `json:"name"`
Location string `json:"Location"`
Present int `json:"present"`
Slot int `json:"slot"`
VendorID int `json:"vendor_id"`
DeviceID int `json:"device_id"`
Vendor string `json:"vendor"`
Model string `json:"model"`
FwVer string `json:"fw_ver"`
Status string `json:"status"`
SN string `json:"sn"`
PN string `json:"pn"`
PortNum int `json:"port_num"`
PortType string `json:"port_type"`
Ports []struct {
ID int `json:"id"`
MacAddr string `json:"mac_addr"`
} `json:"ports"`
} `json:"sys_adapters"`
SysAdapters []sysAdapter `json:"sys_adapters"`
}
type sysAdapter struct {
ID int `json:"id"`
Name string `json:"name"`
Location string `json:"Location"`
Present int `json:"present"`
Slot int `json:"slot"`
PcieBus int `json:"pcie_bus"`
PcieDev int `json:"pcie_dev"`
PcieFunc int `json:"pcie_func"`
VendorID int `json:"vendor_id"`
DeviceID int `json:"device_id"`
Vendor string `json:"vendor"`
Model string `json:"model"`
FwVer string `json:"fw_ver"`
Status string `json:"status"`
SN string `json:"sn"`
PN string `json:"pn"`
PortNum int `json:"port_num"`
PortType string `json:"port_type"`
Ports []struct {
ID int `json:"id"`
MacAddr string `json:"mac_addr"`
} `json:"ports"`
}
func parseNetworkAdapterInfo(text string, hw *models.HardwareConfig) {
// Find RESTful Network Adapter info section
re := regexp.MustCompile(`RESTful Network Adapter info:\s*(\{[\s\S]*?\})\s*RESTful fan`)
match := re.FindStringSubmatch(text)
if match == nil {
return
}
jsonStr := match[1]
jsonStr = strings.ReplaceAll(jsonStr, "\n", "")
var netInfo NetworkAdapterRESTInfo
if err := json.Unmarshal([]byte(jsonStr), &netInfo); err != nil {
if err := json.Unmarshal([]byte(strings.ReplaceAll(match[1], "\n", "")), &netInfo); err != nil {
return
}
var merged []models.NetworkAdapter
seen := make(map[string]int)
for _, existing := range hw.NetworkAdapters {
key := inspurNICKey(existing)
if key == "" {
continue
}
seen[key] = len(merged)
merged = append(merged, existing)
}
// BIOS "PCIE_n_INFO" lines enumerate the real BDF of every PCI function, which
// is the identity the live-CD export and the Reanimator contract key NICs by.
biosFuncs := parseBIOSPCIeFunctions(text)
merged := append([]models.NetworkAdapter(nil), hw.NetworkAdapters...)
for _, adapter := range netInfo.SysAdapters {
var macs []string
for _, port := range adapter.Ports {
@@ -577,8 +578,7 @@ func parseNetworkAdapterInfo(text string, hw *models.HardwareConfig) {
vendor = normalizeModelLabel(pciids.VendorName(adapter.VendorID))
}
item := models.NetworkAdapter{
Slot: fmt.Sprintf("Slot %d", adapter.Slot),
base := models.NetworkAdapter{
Location: adapter.Location,
Present: adapter.Present == 1,
Model: model,
@@ -590,29 +590,212 @@ func parseNetworkAdapterInfo(text string, hw *models.HardwareConfig) {
Firmware: normalizeRedisValue(adapter.FwVer),
PortCount: adapter.PortNum,
PortType: adapter.PortType,
MACAddresses: macs,
Status: adapter.Status,
}
key := inspurNICKey(item)
if idx, ok := seen[key]; ok {
mergeInspurNIC(&merged[idx], item)
// If another source (asset.json PcieInfo) already recorded this card's PCI
// functions, enrich those in place — don't add a competing set of records.
if enrichExistingPCIeNICByBus(hw, adapter.PcieBus, base, macs) {
continue
}
if slotIdx := inspurFindNICBySlot(merged, item.Slot); slotIdx >= 0 {
mergeInspurNIC(&merged[slotIdx], item)
if key != "" {
seen[key] = slotIdx
funcs := selectBIOSPCIeFunctions(biosFuncs, adapter.PcieBus, adapter.VendorID, adapter.DeviceID)
if len(funcs) == 0 && adapter.PcieBus > 0 {
// No BIOS table: fall back to consecutive functions from the port count.
for i := range max(1, len(macs)) {
funcs = append(funcs, pcieFunction{Dev: adapter.PcieDev, Func: adapter.PcieFunc + i})
}
}
if len(funcs) > 0 {
for i, fn := range funcs {
item := base
item.BDF = fullBDF(adapter.PcieBus, fn.Dev, fn.Func)
item.Slot = item.BDF
item.MACAddresses = macsForFunction(macs, len(funcs), i)
upsertInspurNIC(&merged, item)
}
continue
}
if key != "" {
seen[key] = len(merged)
}
merged = append(merged, item)
// No PCI location at all: keep the card-level record (label-only slot).
item := base
item.Slot = fmt.Sprintf("Slot %d", adapter.Slot)
item.MACAddresses = macs
upsertInspurNIC(&merged, item)
}
hw.NetworkAdapters = merged
}
// macsForFunction distributes a card's port MAC list across its PCI functions:
// one MAC per function when the counts line up, otherwise every MAC on the first.
func macsForFunction(macs []string, funcCount, idx int) []string {
if len(macs) == funcCount {
return []string{macs[idx]}
}
if idx == 0 {
return append([]string(nil), macs...)
}
return nil
}
func fullBDF(bus, dev, fn int) string {
return fmt.Sprintf("0000:%02x:%02x.%x", bus, dev, fn)
}
type pcieFunction struct {
Dev int
Func int
VendorID int
DeviceID int
}
var biosPCIeInfoRe = regexp.MustCompile(
`PCIE_\d+_INFO:\s*Device B\.D\.F=0x([0-9a-fA-F]+)\.0x([0-9a-fA-F]+)\.0x([0-9a-fA-F]+),.*?VendorID:0x([0-9a-fA-F]+),\s*Devi[c]?eID:0x([0-9a-fA-F]+)`,
)
// parseBIOSPCIeFunctions reads the BIOS "PCIE_n_INFO" transcript into a
// bus-number -> functions map. Bus/dev/func/IDs are printed in hex.
func parseBIOSPCIeFunctions(text string) map[int][]pcieFunction {
out := make(map[int][]pcieFunction)
for _, m := range biosPCIeInfoRe.FindAllStringSubmatch(text, -1) {
bus := parseHexInt(m[1])
fn := pcieFunction{
Dev: parseHexInt(m[2]),
Func: parseHexInt(m[3]),
VendorID: parseHexInt(m[4]),
DeviceID: parseHexInt(m[5]),
}
out[bus] = append(out[bus], fn)
}
return out
}
// selectBIOSPCIeFunctions returns the functions on the adapter's bus whose PCI
// IDs match the adapter, sorted by (dev, func).
func selectBIOSPCIeFunctions(byBus map[int][]pcieFunction, bus, vendorID, deviceID int) []pcieFunction {
if bus <= 0 {
return nil
}
var out []pcieFunction
for _, fn := range byBus[bus] {
if vendorID != 0 && fn.VendorID != 0 && fn.VendorID != vendorID {
continue
}
if deviceID != 0 && fn.DeviceID != 0 && fn.DeviceID != deviceID {
continue
}
out = append(out, fn)
}
sort.Slice(out, func(i, j int) bool {
if out[i].Dev != out[j].Dev {
return out[i].Dev < out[j].Dev
}
return out[i].Func < out[j].Func
})
return out
}
func parseHexInt(s string) int {
n, err := strconv.ParseInt(strings.TrimSpace(s), 16, 0)
if err != nil {
return 0
}
return int(n)
}
// enrichExistingPCIeNICByBus fills status/model/firmware and per-port MACs on
// PCIe-device records that another source already created for this card's bus.
// Returns true when at least one record was matched.
func enrichExistingPCIeNICByBus(hw *models.HardwareConfig, bus int, info models.NetworkAdapter, macs []string) bool {
if bus <= 0 || hw == nil {
return false
}
matched := false
macIdx := 0
for i := range hw.PCIeDevices {
dev := &hw.PCIeDevices[i]
if bdfBusNumber(dev.BDF) != bus {
continue
}
matched = true
if strings.TrimSpace(dev.Model) == "" || looksLikeRawDeviceID(dev.Model) {
dev.Model = info.Model
}
if strings.TrimSpace(dev.Manufacturer) == "" {
dev.Manufacturer = info.Vendor
}
if strings.TrimSpace(dev.Firmware) == "" {
dev.Firmware = info.Firmware
}
if strings.TrimSpace(dev.SerialNumber) == "" {
dev.SerialNumber = info.SerialNumber
}
if len(dev.MACAddresses) == 0 && macIdx < len(macs) {
dev.MACAddresses = []string{macs[macIdx]}
}
macIdx++
}
return matched
}
// bdfBusNumber extracts the hex bus field from a BDF ("0000:65:00.1" or
// "65:00.1") as a decimal int, or -1 if it does not look like a BDF.
func bdfBusNumber(bdf string) int {
parts := strings.Split(strings.TrimSpace(bdf), ":")
if len(parts) < 2 {
return -1
}
n, err := strconv.ParseInt(parts[len(parts)-2], 16, 0)
if err != nil {
return -1
}
return int(n)
}
// upsertInspurNIC adds item to list, merging into an existing entry that shares
// its BDF or a MAC address.
func upsertInspurNIC(list *[]models.NetworkAdapter, item models.NetworkAdapter) {
items := *list
if bdf := strings.ToLower(strings.TrimSpace(item.BDF)); bdf != "" {
for i := range items {
if strings.ToLower(strings.TrimSpace(items[i].BDF)) == bdf {
mergeInspurNIC(&items[i], item)
return
}
}
}
for _, mac := range item.MACAddresses {
if idx := inspurFindNICByMAC(items, mac); idx >= 0 {
mergeInspurNIC(&items[idx], item)
return
}
}
if item.BDF == "" {
if idx := inspurFindNICBySlot(items, item.Slot); idx >= 0 {
mergeInspurNIC(&items[idx], item)
return
}
}
*list = append(items, item)
}
func inspurFindNICByMAC(items []models.NetworkAdapter, mac string) int {
mac = strings.ToLower(strings.TrimSpace(mac))
if mac == "" {
return -1
}
for i := range items {
for _, existing := range items[i].MACAddresses {
if strings.ToLower(strings.TrimSpace(existing)) == mac {
return i
}
}
}
return -1
}
func inspurMemoryKey(item models.MemoryDIMM) string {
return strings.ToLower(strings.TrimSpace(inspurFirstNonEmpty(item.SerialNumber, item.Slot, item.Location)))
}
@@ -713,10 +896,6 @@ func mergeInspurPSU(dst *models.PSU, src models.PSU) {
}
}
func inspurNICKey(item models.NetworkAdapter) string {
return strings.ToLower(strings.TrimSpace(inspurFirstNonEmpty(item.SerialNumber, strings.Join(item.MACAddresses, ","), item.Slot, item.Location)))
}
func mergeInspurNIC(dst *models.NetworkAdapter, src models.NetworkAdapter) {
if dst == nil {
return
@@ -854,9 +1033,9 @@ func parseFanSensors(text string) []models.SensorReading {
out = append(out, models.SensorReading{
Name: "Fans_Power",
Type: "power",
Value: float64(fanInfo.FansPower),
Value: fanInfo.FansPower,
Unit: "W",
RawValue: fmt.Sprintf("%d", fanInfo.FansPower),
RawValue: fmt.Sprintf("%g", fanInfo.FansPower),
Status: "OK",
})
}