Files
logpile/internal/parser/vendors/inspur/component.go
Mikhail Chusavitin 9df29b1be9 fix: dedup GPUs across multiple chassis PCIeDevice trees in Redfish collector
Supermicro HGX exposes each GPU under both Chassis/1/PCIeDevices and a
dedicated Chassis/HGX_GPU_SXM_N/PCIeDevices. gpuDocDedupKey was keying
by @odata.id path, so identical GPUs with the same serial were not
deduplicated across sources. Now stable identifiers (serial → BDF →
slot+model) take priority over path.

Also includes Inspur parser improvements: NVMe model/serial enrichment
from devicefrusdr.log and audit.log, RAID drive slot normalization to
BP notation, PSU slot normalization, BMC/CPLD/VR firmware from RESTful
version info section, and parser version bump to 1.8.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-06 14:44:36 +03:00

887 lines
25 KiB
Go

package inspur
import (
"encoding/json"
"fmt"
"regexp"
"strings"
"time"
"git.mchus.pro/mchus/logpile/internal/models"
"git.mchus.pro/mchus/logpile/internal/parser/vendors/pciids"
)
// ParseComponentLog parses component.log file and extracts detailed hardware info
func ParseComponentLog(content []byte, hw *models.HardwareConfig) {
if hw == nil {
return
}
text := string(content)
// Parse RESTful Memory info (detailed memory data)
parseMemoryInfo(text, hw)
// Parse RESTful PSU info
parsePSUInfo(text, hw)
// Parse RESTful HDD info
parseHDDInfo(text, hw)
// Parse RESTful diskbackplane info
parseDiskBackplaneInfo(text, hw)
// Parse RESTful Network Adapter info
parseNetworkAdapterInfo(text, hw)
// Extract firmware from all components
extractComponentFirmware(text, hw)
}
// ParseComponentLogEvents extracts events from component.log (memory errors, etc.)
func ParseComponentLogEvents(content []byte) []models.Event {
var events []models.Event
text := string(content)
// Parse RESTful Memory info for Warning/Error status
memEvents := parseMemoryEvents(text)
events = append(events, memEvents...)
events = append(events, parseFanEvents(text)...)
return events
}
// ParseComponentLogSensors extracts sensor readings from component.log JSON sections.
func ParseComponentLogSensors(content []byte) []models.SensorReading {
text := string(content)
var out []models.SensorReading
out = append(out, parseFanSensors(text)...)
out = append(out, parseDiskBackplaneSensors(text)...)
out = append(out, parsePSUSummarySensors(text)...)
return out
}
// MemoryRESTInfo represents the RESTful Memory info structure
type MemoryRESTInfo struct {
MemModules []struct {
MemModID int `json:"mem_mod_id"`
ConfigStatus int `json:"config_status"`
MemModSlot string `json:"mem_mod_slot"`
MemModStatus int `json:"mem_mod_status"`
MemModSize int `json:"mem_mod_size"`
MemModType string `json:"mem_mod_type"`
MemModTechnology string `json:"mem_mod_technology"`
MemModFrequency int `json:"mem_mod_frequency"`
MemModCurrentFreq int `json:"mem_mod_current_frequency"`
MemModVendor string `json:"mem_mod_vendor"`
MemModPartNum string `json:"mem_mod_part_num"`
MemModSerial string `json:"mem_mod_serial_num"`
MemModRanks int `json:"mem_mod_ranks"`
Status string `json:"status"`
} `json:"mem_modules"`
TotalMemoryCount int `json:"total_memory_count"`
PresentMemoryCount int `json:"present_memory_count"`
MemTotalMemSize int `json:"mem_total_mem_size"`
}
func parseMemoryInfo(text string, hw *models.HardwareConfig) {
// Find RESTful Memory info section
re := regexp.MustCompile(`RESTful Memory info:\s*(\{[\s\S]*?\})\s*RESTful HDD`)
match := re.FindStringSubmatch(text)
if match == nil {
return
}
jsonStr := match[1]
jsonStr = strings.ReplaceAll(jsonStr, "\n", "")
var memInfo MemoryRESTInfo
if err := json.Unmarshal([]byte(jsonStr), &memInfo); err != nil {
return
}
// Replace memory data with detailed info from component.log
hw.Memory = nil
for _, mem := range memInfo.MemModules {
hw.Memory = append(hw.Memory, models.MemoryDIMM{
Slot: mem.MemModSlot,
Location: mem.MemModSlot,
Present: mem.MemModStatus == 1 && mem.MemModSize > 0,
SizeMB: mem.MemModSize * 1024, // Convert GB to MB
Type: mem.MemModType,
Technology: strings.TrimSpace(mem.MemModTechnology),
MaxSpeedMHz: mem.MemModFrequency,
CurrentSpeedMHz: mem.MemModCurrentFreq,
Manufacturer: mem.MemModVendor,
SerialNumber: mem.MemModSerial,
PartNumber: strings.TrimSpace(mem.MemModPartNum),
Status: mem.Status,
Ranks: mem.MemModRanks,
})
}
}
// PSURESTInfo represents the RESTful PSU info structure
type PSURESTInfo struct {
PowerSupplies []struct {
ID int `json:"id"`
Present int `json:"present"`
VendorID string `json:"vendor_id"`
Model string `json:"model"`
SerialNum string `json:"serial_num"`
PartNum string `json:"part_num"`
FwVer string `json:"fw_ver"`
InputType string `json:"input_type"`
Status string `json:"status"`
RatedPower int `json:"rated_power"`
PSInPower int `json:"ps_in_power"`
PSOutPower int `json:"ps_out_power"`
PSInVolt float64 `json:"ps_in_volt"`
PSOutVolt float64 `json:"ps_out_volt"`
PSUMaxTemp int `json:"psu_max_temperature"`
} `json:"power_supplies"`
PresentPowerReading int `json:"present_power_reading"`
}
func parsePSUInfo(text string, hw *models.HardwareConfig) {
// Find RESTful PSU info section
re := regexp.MustCompile(`RESTful PSU info:\s*(\{[\s\S]*?\})\s*RESTful Network`)
match := re.FindStringSubmatch(text)
if match == nil {
return
}
jsonStr := match[1]
jsonStr = strings.ReplaceAll(jsonStr, "\n", "")
var psuInfo PSURESTInfo
if err := json.Unmarshal([]byte(jsonStr), &psuInfo); err != nil {
return
}
// Clear existing PSU data and populate with RESTful data
hw.PowerSupply = nil
for _, psu := range psuInfo.PowerSupplies {
hw.PowerSupply = append(hw.PowerSupply, models.PSU{
Slot: fmt.Sprintf("PSU%d", psu.ID),
Present: psu.Present == 1,
Model: strings.TrimSpace(psu.Model),
Vendor: strings.TrimSpace(psu.VendorID),
WattageW: psu.RatedPower,
SerialNumber: strings.TrimSpace(psu.SerialNum),
PartNumber: strings.TrimSpace(psu.PartNum),
Firmware: psu.FwVer,
Status: psu.Status,
InputType: psu.InputType,
InputPowerW: psu.PSInPower,
OutputPowerW: psu.PSOutPower,
InputVoltage: psu.PSInVolt,
OutputVoltage: psu.PSOutVolt,
TemperatureC: psu.PSUMaxTemp,
})
}
}
// HDDRESTInfo represents the RESTful HDD info structure
type HDDRESTInfo []struct {
ID int `json:"id"`
Present int `json:"present"`
Enable int `json:"enable"`
SN string `json:"SN"`
Model string `json:"model"`
Capacity int `json:"capacity"`
Manufacture string `json:"manufacture"`
Firmware string `json:"firmware"`
LocationString string `json:"locationstring"`
CapableSpeed int `json:"capablespeed"`
}
func parseHDDInfo(text string, hw *models.HardwareConfig) {
// Find RESTful HDD info section
re := regexp.MustCompile(`RESTful HDD info:\s*(\[[\s\S]*?\])\s*RESTful PSU`)
match := re.FindStringSubmatch(text)
if match == nil {
return
}
jsonStr := match[1]
jsonStr = strings.ReplaceAll(jsonStr, "\n", "")
var hddInfo HDDRESTInfo
if err := json.Unmarshal([]byte(jsonStr), &hddInfo); err != nil {
return
}
// Update storage with detailed info (merge with existing data from asset.json)
hddMap := make(map[string]struct {
SN string
Model string
Firmware string
Mfr string
})
for _, hdd := range hddInfo {
if hdd.Present == 1 {
slot := strings.TrimSpace(hdd.LocationString)
if slot == "" {
slot = fmt.Sprintf("HDD%d", hdd.ID)
}
hddMap[slot] = struct {
SN string
Model string
Firmware string
Mfr string
}{
SN: normalizeRedisValue(hdd.SN),
Model: strings.TrimSpace(hdd.Model),
Firmware: normalizeRedisValue(hdd.Firmware),
Mfr: strings.TrimSpace(hdd.Manufacture),
}
}
}
// Merge into existing inventory first (asset/other sections).
for i := range hw.Storage {
slot := strings.TrimSpace(hw.Storage[i].Slot)
if slot == "" {
continue
}
detail, ok := hddMap[slot]
if !ok {
continue
}
if normalizeRedisValue(hw.Storage[i].SerialNumber) == "" {
hw.Storage[i].SerialNumber = detail.SN
}
if hw.Storage[i].Model == "" {
hw.Storage[i].Model = detail.Model
}
if normalizeRedisValue(hw.Storage[i].Firmware) == "" {
hw.Storage[i].Firmware = detail.Firmware
}
if hw.Storage[i].Manufacturer == "" {
hw.Storage[i].Manufacturer = detail.Mfr
}
hw.Storage[i].Present = true
}
// If storage is empty, populate from HDD info
if len(hw.Storage) == 0 {
for _, hdd := range hddInfo {
if hdd.Present != 1 {
continue
}
storType := "HDD"
model := strings.TrimSpace(hdd.Model)
if strings.Contains(strings.ToUpper(model), "SSD") || strings.Contains(model, "MZ7") {
storType = "SSD"
}
iface := "SATA"
if hdd.CapableSpeed == 12 {
iface = "SAS"
}
slot := strings.TrimSpace(hdd.LocationString)
if slot == "" {
slot = fmt.Sprintf("HDD%d", hdd.ID)
}
hw.Storage = append(hw.Storage, models.Storage{
Slot: slot,
Type: storType,
Model: model,
SizeGB: hdd.Capacity,
SerialNumber: normalizeRedisValue(hdd.SN),
Manufacturer: extractStorageManufacturer(model),
Firmware: normalizeRedisValue(hdd.Firmware),
Interface: iface,
Present: true,
})
}
}
}
// FanRESTInfo represents the RESTful fan info structure.
type FanRESTInfo struct {
Fans []struct {
ID int `json:"id"`
FanName string `json:"fan_name"`
Present string `json:"present"`
Status string `json:"status"`
StatusStr string `json:"status_str"`
SpeedRPM int `json:"speed_rpm"`
SpeedPercent int `json:"speed_percent"`
MaxSpeedRPM int `json:"max_speed_rpm"`
FanModel string `json:"fan_model"`
} `json:"fans"`
FansPower int `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"`
}
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 {
return
}
hw.NetworkAdapters = nil
for _, adapter := range netInfo.SysAdapters {
var macs []string
for _, port := range adapter.Ports {
if port.MacAddr != "" {
macs = append(macs, port.MacAddr)
}
}
model := normalizeModelLabel(adapter.Model)
if model == "" || looksLikeRawDeviceID(model) {
if resolved := normalizeModelLabel(pciids.DeviceName(adapter.VendorID, adapter.DeviceID)); resolved != "" {
model = resolved
}
}
vendor := normalizeModelLabel(adapter.Vendor)
if vendor == "" {
vendor = normalizeModelLabel(pciids.VendorName(adapter.VendorID))
}
hw.NetworkAdapters = append(hw.NetworkAdapters, models.NetworkAdapter{
Slot: fmt.Sprintf("Slot %d", adapter.Slot),
Location: adapter.Location,
Present: adapter.Present == 1,
Model: model,
Vendor: vendor,
VendorID: adapter.VendorID,
DeviceID: adapter.DeviceID,
SerialNumber: normalizeRedisValue(adapter.SN),
PartNumber: normalizeRedisValue(adapter.PN),
Firmware: normalizeRedisValue(adapter.FwVer),
PortCount: adapter.PortNum,
PortType: adapter.PortType,
MACAddresses: macs,
Status: adapter.Status,
})
}
}
func parseFanSensors(text string) []models.SensorReading {
re := regexp.MustCompile(`RESTful fan info:\s*(\{[\s\S]*?\})\s*RESTful diskbackplane`)
match := re.FindStringSubmatch(text)
if match == nil {
return nil
}
jsonStr := strings.ReplaceAll(match[1], "\n", "")
var fanInfo FanRESTInfo
if err := json.Unmarshal([]byte(jsonStr), &fanInfo); err != nil {
return nil
}
out := make([]models.SensorReading, 0, len(fanInfo.Fans)+1)
for _, fan := range fanInfo.Fans {
name := strings.TrimSpace(fan.FanName)
if name == "" {
name = fmt.Sprintf("FAN%d", fan.ID)
}
status := normalizeComponentStatus(fan.StatusStr, fan.Status, fan.Present)
raw := fmt.Sprintf("rpm=%d pct=%d model=%s max_rpm=%d", fan.SpeedRPM, fan.SpeedPercent, fan.FanModel, fan.MaxSpeedRPM)
out = append(out, models.SensorReading{
Name: name,
Type: "fan_speed",
Value: float64(fan.SpeedRPM),
Unit: "RPM",
RawValue: raw,
Status: status,
})
}
if fanInfo.FansPower > 0 {
out = append(out, models.SensorReading{
Name: "Fans_Power",
Type: "power",
Value: float64(fanInfo.FansPower),
Unit: "W",
RawValue: fmt.Sprintf("%d", fanInfo.FansPower),
Status: "OK",
})
}
return out
}
func parseFanEvents(text string) []models.Event {
re := regexp.MustCompile(`RESTful fan info:\s*(\{[\s\S]*?\})\s*RESTful diskbackplane`)
match := re.FindStringSubmatch(text)
if match == nil {
return nil
}
jsonStr := strings.ReplaceAll(match[1], "\n", "")
var fanInfo FanRESTInfo
if err := json.Unmarshal([]byte(jsonStr), &fanInfo); err != nil {
return nil
}
var events []models.Event
for _, fan := range fanInfo.Fans {
status := normalizeComponentStatus(fan.StatusStr, fan.Status, fan.Present)
if isHealthyComponentStatus(status) {
continue
}
name := strings.TrimSpace(fan.FanName)
if name == "" {
name = fmt.Sprintf("FAN%d", fan.ID)
}
severity := models.SeverityWarning
lowStatus := strings.ToLower(status)
if strings.Contains(lowStatus, "critical") || strings.Contains(lowStatus, "fail") || strings.Contains(lowStatus, "error") {
severity = models.SeverityCritical
}
events = append(events, models.Event{
ID: fmt.Sprintf("fan_%d_status", fan.ID),
Timestamp: time.Now(),
Source: "Fan",
SensorType: "fan",
SensorName: name,
EventType: "Fan Status",
Severity: severity,
Description: fmt.Sprintf("%s reports %s", name, status),
RawData: fmt.Sprintf("rpm=%d pct=%d model=%s", fan.SpeedRPM, fan.SpeedPercent, fan.FanModel),
})
}
return events
}
func parseDiskBackplaneSensors(text string) []models.SensorReading {
re := regexp.MustCompile(`RESTful diskbackplane info:\s*(\[[\s\S]*?\])\s*BMC`)
match := re.FindStringSubmatch(text)
if match == nil {
return nil
}
jsonStr := strings.ReplaceAll(match[1], "\n", "")
var backplaneInfo DiskBackplaneRESTInfo
if err := json.Unmarshal([]byte(jsonStr), &backplaneInfo); err != nil {
return nil
}
out := make([]models.SensorReading, 0, len(backplaneInfo))
for _, bp := range backplaneInfo {
if bp.Present != 1 {
continue
}
name := fmt.Sprintf("Backplane%d_Temp", bp.BackplaneIndex)
status := "OK"
if bp.Temperature <= 0 {
status = "unknown"
}
raw := fmt.Sprintf("front=%d ports=%d drives=%d cpld=%s", bp.Front, bp.PortCount, bp.DriverCount, bp.CPLDVersion)
out = append(out, models.SensorReading{
Name: name,
Type: "temperature",
Value: float64(bp.Temperature),
Unit: "C",
RawValue: raw,
Status: status,
})
}
return out
}
func parsePSUSummarySensors(text string) []models.SensorReading {
re := regexp.MustCompile(`RESTful PSU info:\s*(\{[\s\S]*?\})\s*RESTful Network`)
match := re.FindStringSubmatch(text)
if match == nil {
return nil
}
jsonStr := strings.ReplaceAll(match[1], "\n", "")
var psuInfo PSURESTInfo
if err := json.Unmarshal([]byte(jsonStr), &psuInfo); err != nil {
return nil
}
out := make([]models.SensorReading, 0, len(psuInfo.PowerSupplies)*3+1)
if psuInfo.PresentPowerReading > 0 {
out = append(out, models.SensorReading{
Name: "PSU_Present_Power_Reading",
Type: "power",
Value: float64(psuInfo.PresentPowerReading),
Unit: "W",
RawValue: fmt.Sprintf("%d", psuInfo.PresentPowerReading),
Status: "OK",
})
}
for _, psu := range psuInfo.PowerSupplies {
if psu.Present != 1 {
continue
}
status := normalizeComponentStatus(psu.Status)
out = append(out, models.SensorReading{
Name: fmt.Sprintf("PSU%d_InputPower", psu.ID),
Type: "power",
Value: float64(psu.PSInPower),
Unit: "W",
RawValue: fmt.Sprintf("%d", psu.PSInPower),
Status: status,
})
out = append(out, models.SensorReading{
Name: fmt.Sprintf("PSU%d_OutputPower", psu.ID),
Type: "power",
Value: float64(psu.PSOutPower),
Unit: "W",
RawValue: fmt.Sprintf("%d", psu.PSOutPower),
Status: status,
})
out = append(out, models.SensorReading{
Name: fmt.Sprintf("PSU%d_Temp", psu.ID),
Type: "temperature",
Value: float64(psu.PSUMaxTemp),
Unit: "C",
RawValue: fmt.Sprintf("%d", psu.PSUMaxTemp),
Status: status,
})
}
return out
}
func normalizeComponentStatus(values ...string) string {
for _, v := range values {
s := strings.TrimSpace(v)
if s == "" {
continue
}
return s
}
return "unknown"
}
func isHealthyComponentStatus(status string) bool {
switch strings.ToLower(strings.TrimSpace(status)) {
case "", "ok", "normal", "present", "enabled":
return true
default:
return false
}
}
var rawDeviceIDLikeRegex = regexp.MustCompile(`(?i)^(?:0x)?[0-9a-f]{3,4}$`)
func looksLikeRawDeviceID(v string) bool {
v = strings.TrimSpace(v)
if v == "" {
return true
}
return rawDeviceIDLikeRegex.MatchString(v)
}
func parseMemoryEvents(text string) []models.Event {
var events []models.Event
// Find RESTful Memory info section
re := regexp.MustCompile(`RESTful Memory info:\s*(\{[\s\S]*?\})\s*RESTful HDD`)
match := re.FindStringSubmatch(text)
if match == nil {
return events
}
jsonStr := match[1]
jsonStr = strings.ReplaceAll(jsonStr, "\n", "")
var memInfo MemoryRESTInfo
if err := json.Unmarshal([]byte(jsonStr), &memInfo); err != nil {
return events
}
// Generate events for memory modules with Warning or Error status
for _, mem := range memInfo.MemModules {
if mem.Status == "Warning" || mem.Status == "Error" || mem.Status == "Critical" {
severity := models.SeverityWarning
if mem.Status == "Error" || mem.Status == "Critical" {
severity = models.SeverityCritical
}
description := fmt.Sprintf("Memory module %s: %s", mem.MemModSlot, mem.Status)
if mem.MemModSize == 0 {
description = fmt.Sprintf("Memory module %s not detected (capacity 0GB)", mem.MemModSlot)
}
events = append(events, models.Event{
ID: fmt.Sprintf("mem_%d", mem.MemModID),
Timestamp: time.Now(),
Source: "Memory",
SensorType: "memory",
SensorName: mem.MemModSlot,
EventType: "Memory Status",
Severity: severity,
Description: description,
RawData: fmt.Sprintf("Slot: %s, Vendor: %s, P/N: %s, S/N: %s", mem.MemModSlot, mem.MemModVendor, mem.MemModPartNum, mem.MemModSerial),
})
}
}
return events
}
// extractComponentFirmware extracts firmware versions from all component data
func extractComponentFirmware(text string, hw *models.HardwareConfig) {
// Create a map to track existing firmware entries (avoid duplicates)
existingFW := make(map[string]bool)
for _, fw := range hw.Firmware {
existingFW[fw.DeviceName] = true
}
// HDD firmware is already extracted from asset.json with better names
// Skip extracting from component.log to avoid duplicates
// Extract PSU firmware from RESTful PSU info
rePSU := regexp.MustCompile(`RESTful PSU info:\s*(\{[\s\S]*?\})\s*RESTful Network`)
if match := rePSU.FindStringSubmatch(text); match != nil {
jsonStr := strings.ReplaceAll(match[1], "\n", "")
var psuInfo PSURESTInfo
if err := json.Unmarshal([]byte(jsonStr), &psuInfo); err == nil {
for _, psu := range psuInfo.PowerSupplies {
if psu.Present == 1 && psu.FwVer != "" {
fwName := fmt.Sprintf("PSU%d (%s)", psu.ID, psu.Model)
if !existingFW[fwName] {
hw.Firmware = append(hw.Firmware, models.FirmwareInfo{
DeviceName: fwName,
Version: psu.FwVer,
})
existingFW[fwName] = true
}
}
}
}
}
// Extract Network Adapter firmware from RESTful Network Adapter info
reNet := regexp.MustCompile(`RESTful Network Adapter info:\s*(\{[\s\S]*?\})\s*RESTful fan`)
if match := reNet.FindStringSubmatch(text); match != nil {
jsonStr := strings.ReplaceAll(match[1], "\n", "")
var netInfo NetworkAdapterRESTInfo
if err := json.Unmarshal([]byte(jsonStr), &netInfo); err == nil {
for _, adapter := range netInfo.SysAdapters {
if adapter.Present == 1 && adapter.FwVer != "" && adapter.FwVer != "NA" {
fwName := fmt.Sprintf("NIC %s (%s)", adapter.Location, adapter.Model)
if !existingFW[fwName] {
hw.Firmware = append(hw.Firmware, models.FirmwareInfo{
DeviceName: fwName,
Version: adapter.FwVer,
})
existingFW[fwName] = true
}
}
}
}
}
// Extract BMC, CPLD and VR firmware from RESTful version info section.
// The JSON is a flat array: [{"id":N,"dev_name":"...","dev_version":"..."}, ...]
reVer := regexp.MustCompile(`RESTful version info:\s*(\[[\s\S]*?\])\s*RESTful`)
if match := reVer.FindStringSubmatch(text); match != nil {
type verEntry struct {
DevName string `json:"dev_name"`
DevVersion string `json:"dev_version"`
}
var entries []verEntry
if err := json.Unmarshal([]byte(match[1]), &entries); err == nil {
for _, e := range entries {
name := normalizeVersionInfoName(e.DevName)
if name == "" {
continue
}
version := strings.TrimSpace(e.DevVersion)
if version == "" {
continue
}
if existingFW[name] {
continue
}
hw.Firmware = append(hw.Firmware, models.FirmwareInfo{
DeviceName: name,
Version: version,
})
existingFW[name] = true
}
}
}
}
// normalizeVersionInfoName converts RESTful version info dev_name to a clean label.
// Returns "" for entries that should be skipped (inactive BMC, PSU slots).
func normalizeVersionInfoName(name string) string {
name = strings.TrimSpace(name)
if name == "" {
return ""
}
// Skip PSU_N entries — firmware already extracted from PSU info section.
if regexp.MustCompile(`(?i)^PSU_\d+$`).MatchString(name) {
return ""
}
// Skip the inactive BMC partition.
if strings.HasPrefix(strings.ToLower(name), "inactivate(") {
return ""
}
// Active BMC: "Activate(BMC1)" → "BMC"
if strings.HasPrefix(strings.ToLower(name), "activate(") {
return "BMC"
}
// Strip trailing "Version" suffix (case-insensitive), e.g. "MainBoard0CPLDVersion" → "MainBoard0CPLD"
if strings.HasSuffix(strings.ToLower(name), "version") {
name = name[:len(name)-len("version")]
}
return strings.TrimSpace(name)
}
// DiskBackplaneRESTInfo represents the RESTful diskbackplane info structure
type DiskBackplaneRESTInfo []struct {
PortCount int `json:"port_count"`
DriverCount int `json:"driver_count"`
Front int `json:"front"`
BackplaneIndex int `json:"backplane_index"`
Present int `json:"present"`
CPLDVersion string `json:"cpld_version"`
Temperature int `json:"temperature"`
}
func parseDiskBackplaneInfo(text string, hw *models.HardwareConfig) {
// Find RESTful diskbackplane info section
re := regexp.MustCompile(`RESTful diskbackplane info:\s*(\[[\s\S]*?\])\s*BMC`)
match := re.FindStringSubmatch(text)
if match == nil {
return
}
jsonStr := match[1]
jsonStr = strings.ReplaceAll(jsonStr, "\n", "")
var backplaneInfo DiskBackplaneRESTInfo
if err := json.Unmarshal([]byte(jsonStr), &backplaneInfo); err != nil {
return
}
presentByBackplane := make(map[int]int)
totalPresent := 0
for _, bp := range backplaneInfo {
if bp.Present != 1 {
continue
}
if bp.DriverCount <= 0 {
continue
}
limit := bp.DriverCount
if bp.PortCount > 0 && limit > bp.PortCount {
limit = bp.PortCount
}
presentByBackplane[bp.BackplaneIndex] = limit
totalPresent += limit
}
if totalPresent == 0 {
return
}
existingPresent := countPresentStorage(hw.Storage)
remaining := totalPresent - existingPresent
if remaining <= 0 {
return
}
for _, bp := range backplaneInfo {
if bp.Present != 1 || remaining <= 0 {
continue
}
driveCount := presentByBackplane[bp.BackplaneIndex]
if driveCount <= 0 {
continue
}
location := "Rear"
if bp.Front == 1 {
location = "Front"
}
for i := 0; i < driveCount && remaining > 0; i++ {
slot := fmt.Sprintf("BP%d:%d", bp.BackplaneIndex, i)
if hasStorageSlot(hw.Storage, slot) {
continue
}
hw.Storage = append(hw.Storage, models.Storage{
Slot: slot,
Present: true,
Location: location,
BackplaneID: bp.BackplaneIndex,
Type: "HDD",
})
remaining--
}
}
}
func countPresentStorage(storage []models.Storage) int {
count := 0
for _, dev := range storage {
if dev.Present {
count++
continue
}
if strings.TrimSpace(dev.Slot) != "" && (normalizeRedisValue(dev.Model) != "" || normalizeRedisValue(dev.SerialNumber) != "" || dev.SizeGB > 0) {
count++
}
}
return count
}
func hasStorageSlot(storage []models.Storage, slot string) bool {
slot = strings.ToLower(strings.TrimSpace(slot))
if slot == "" {
return false
}
for _, dev := range storage {
if strings.ToLower(strings.TrimSpace(dev.Slot)) == slot {
return true
}
}
return false
}