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:
co-authored by
Claude Sonnet 5
parent
ab8636da04
commit
2fa0f78f94
+236
-57
@@ -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",
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user