Add detailed hardware configuration view with sub-tabs
- Redesign config page with tabs: Spec, CPU, Memory, Power, Storage, GPU, Network, PCIe - Parse detailed memory info from component.log with all fields: Location, Present, Size, Type, Max/Current Speed, Manufacturer, Part Number, Status - Add GPU model extraction from PCIe devices - Add NetworkAdapter model with detailed fields from RESTful API - Update PSU model with power metrics (input/output power, voltage, temperature) - Memory modules with 0GB size (failed) highlighted in warning color - Add memory overview stats (total GB, installed count, active count) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
47
internal/parser/vendors/inspur/asset.go
vendored
47
internal/parser/vendors/inspur/asset.go
vendored
@@ -125,18 +125,24 @@ func ParseAssetJSON(content []byte) (*models.HardwareConfig, error) {
|
||||
})
|
||||
}
|
||||
|
||||
// Parse memory info
|
||||
// Memory info is parsed from component.log (RESTful Memory info) which has more details
|
||||
// Only use asset.json memory data as fallback if component.log is not available
|
||||
if len(asset.MemInfo.MemCommonInfo) > 0 {
|
||||
common := asset.MemInfo.MemCommonInfo[0]
|
||||
for i, dimm := range asset.MemInfo.DimmInfo {
|
||||
slot := fmt.Sprintf("DIMM%d", i)
|
||||
config.Memory = append(config.Memory, models.MemoryDIMM{
|
||||
Slot: i,
|
||||
SizeMB: common.PhysicalSize * 1024,
|
||||
Type: memoryTypeToString(common.MemoryType),
|
||||
SpeedMHz: common.CurrentSpeed,
|
||||
Manufacturer: common.Manufacturer,
|
||||
SerialNumber: dimm.SerialNumber,
|
||||
PartNumber: strings.TrimSpace(dimm.PartNumber),
|
||||
Slot: slot,
|
||||
Location: slot,
|
||||
Present: true,
|
||||
SizeMB: common.PhysicalSize * 1024,
|
||||
Type: memoryTypeToString(common.MemoryType),
|
||||
MaxSpeedMHz: common.MaxSpeed,
|
||||
CurrentSpeedMHz: common.CurrentSpeed,
|
||||
Manufacturer: common.Manufacturer,
|
||||
SerialNumber: dimm.SerialNumber,
|
||||
PartNumber: strings.TrimSpace(dimm.PartNumber),
|
||||
Ranks: common.Rank,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -195,6 +201,31 @@ func ParseAssetJSON(content []byte) (*models.HardwareConfig, error) {
|
||||
device.DeviceClass = deviceName
|
||||
}
|
||||
config.PCIeDevices = append(config.PCIeDevices, device)
|
||||
|
||||
// Extract GPUs (class 3 = display controller)
|
||||
if pcie.ClassCode == 3 {
|
||||
gpuModel := deviceName
|
||||
if gpuModel == "" {
|
||||
gpuModel = pcieClassToString(pcie.ClassCode, pcie.SubClassCode)
|
||||
}
|
||||
gpu := models.GPU{
|
||||
Slot: pcie.LocString,
|
||||
Model: gpuModel,
|
||||
Manufacturer: vendor,
|
||||
VendorID: pcie.VendorId,
|
||||
DeviceID: pcie.DeviceId,
|
||||
BDF: formatBDF(pcie.BusNumber, pcie.DeviceNumber, pcie.FunctionNumber),
|
||||
LinkWidth: pcie.NegotiatedLinkWidth,
|
||||
LinkSpeed: pcieLinkSpeedToString(pcie.CurrentLinkSpeed),
|
||||
}
|
||||
if pcie.PartNumber != nil {
|
||||
gpu.PartNumber = strings.TrimSpace(*pcie.PartNumber)
|
||||
}
|
||||
if pcie.SerialNumber != nil {
|
||||
gpu.SerialNumber = strings.TrimSpace(*pcie.SerialNumber)
|
||||
}
|
||||
config.GPUs = append(config.GPUs, gpu)
|
||||
}
|
||||
}
|
||||
|
||||
return config, nil
|
||||
|
||||
300
internal/parser/vendors/inspur/component.go
vendored
300
internal/parser/vendors/inspur/component.go
vendored
@@ -10,7 +10,7 @@ import (
|
||||
"git.mchus.pro/mchus/logpile/internal/models"
|
||||
)
|
||||
|
||||
// ParseComponentLog parses component.log file and extracts PSU and other info
|
||||
// ParseComponentLog parses component.log file and extracts detailed hardware info
|
||||
func ParseComponentLog(content []byte, hw *models.HardwareConfig) {
|
||||
if hw == nil {
|
||||
return
|
||||
@@ -18,8 +18,17 @@ func ParseComponentLog(content []byte, hw *models.HardwareConfig) {
|
||||
|
||||
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 Network Adapter info
|
||||
parseNetworkAdapterInfo(text, hw)
|
||||
}
|
||||
|
||||
// ParseComponentLogEvents extracts events from component.log (memory errors, etc.)
|
||||
@@ -34,33 +43,99 @@ func ParseComponentLogEvents(content []byte) []models.Event {
|
||||
return events
|
||||
}
|
||||
|
||||
// PSUInfo represents the RESTful PSU info structure
|
||||
type PSUInfo 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"`
|
||||
FwVer string `json:"fw_ver"`
|
||||
RatedPower int `json:"rated_power"`
|
||||
Status string `json:"status"`
|
||||
} `json:"power_supplies"`
|
||||
// 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"`
|
||||
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 parsePSUInfo(text string, hw *models.HardwareConfig) {
|
||||
// Find RESTful PSU info section
|
||||
re := regexp.MustCompile(`RESTful PSU info:\s*(\{[\s\S]*?\})\s*(?:RESTful|BMC|$)`)
|
||||
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]
|
||||
// Clean up the JSON (it might have newlines)
|
||||
jsonStr = strings.ReplaceAll(jsonStr, "\n", "")
|
||||
|
||||
var psuInfo PSUInfo
|
||||
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.ConfigStatus == 1,
|
||||
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
|
||||
}
|
||||
@@ -68,34 +143,177 @@ func parsePSUInfo(text string, hw *models.HardwareConfig) {
|
||||
// Clear existing PSU data and populate with RESTful data
|
||||
hw.PowerSupply = nil
|
||||
for _, psu := range psuInfo.PowerSupplies {
|
||||
if psu.Present != 1 {
|
||||
continue
|
||||
}
|
||||
hw.PowerSupply = append(hw.PowerSupply, models.PSU{
|
||||
Slot: formatPSUSlot(psu.ID),
|
||||
Model: psu.Model,
|
||||
WattageW: psu.RatedPower,
|
||||
SerialNumber: psu.SerialNum,
|
||||
Status: psu.Status,
|
||||
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,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func formatPSUSlot(id int) string {
|
||||
return fmt.Sprintf("PSU%d", id)
|
||||
// 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"`
|
||||
}
|
||||
|
||||
// MemoryInfo represents the RESTful Memory info structure
|
||||
type MemoryInfo struct {
|
||||
MemModules []struct {
|
||||
MemModID int `json:"mem_mod_id"`
|
||||
MemModSlot string `json:"mem_mod_slot"`
|
||||
MemModSize int `json:"mem_mod_size"`
|
||||
MemModVendor string `json:"mem_mod_vendor"`
|
||||
MemModPartNum string `json:"mem_mod_part_num"`
|
||||
MemModSerial string `json:"mem_mod_serial_num"`
|
||||
Status string `json:"status"`
|
||||
} `json:"mem_modules"`
|
||||
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 {
|
||||
hddMap[hdd.LocationString] = struct {
|
||||
SN string
|
||||
Model string
|
||||
Firmware string
|
||||
Mfr string
|
||||
}{
|
||||
SN: strings.TrimSpace(hdd.SN),
|
||||
Model: strings.TrimSpace(hdd.Model),
|
||||
Firmware: strings.TrimSpace(hdd.Firmware),
|
||||
Mfr: strings.TrimSpace(hdd.Manufacture),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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"
|
||||
}
|
||||
|
||||
hw.Storage = append(hw.Storage, models.Storage{
|
||||
Slot: hdd.LocationString,
|
||||
Type: storType,
|
||||
Model: model,
|
||||
SizeGB: hdd.Capacity,
|
||||
SerialNumber: strings.TrimSpace(hdd.SN),
|
||||
Manufacturer: extractStorageManufacturer(model),
|
||||
Firmware: strings.TrimSpace(hdd.Firmware),
|
||||
Interface: iface,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
|
||||
hw.NetworkAdapters = append(hw.NetworkAdapters, models.NetworkAdapter{
|
||||
Slot: fmt.Sprintf("Slot %d", adapter.Slot),
|
||||
Location: adapter.Location,
|
||||
Present: adapter.Present == 1,
|
||||
Model: strings.TrimSpace(adapter.Model),
|
||||
Vendor: strings.TrimSpace(adapter.Vendor),
|
||||
VendorID: adapter.VendorID,
|
||||
DeviceID: adapter.DeviceID,
|
||||
SerialNumber: strings.TrimSpace(adapter.SN),
|
||||
PartNumber: strings.TrimSpace(adapter.PN),
|
||||
Firmware: adapter.FwVer,
|
||||
PortCount: adapter.PortNum,
|
||||
PortType: adapter.PortType,
|
||||
MACAddresses: macs,
|
||||
Status: adapter.Status,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func parseMemoryEvents(text string) []models.Event {
|
||||
@@ -111,7 +329,7 @@ func parseMemoryEvents(text string) []models.Event {
|
||||
jsonStr := match[1]
|
||||
jsonStr = strings.ReplaceAll(jsonStr, "\n", "")
|
||||
|
||||
var memInfo MemoryInfo
|
||||
var memInfo MemoryRESTInfo
|
||||
if err := json.Unmarshal([]byte(jsonStr), &memInfo); err != nil {
|
||||
return events
|
||||
}
|
||||
@@ -131,7 +349,7 @@ func parseMemoryEvents(text string) []models.Event {
|
||||
|
||||
events = append(events, models.Event{
|
||||
ID: fmt.Sprintf("mem_%d", mem.MemModID),
|
||||
Timestamp: time.Now(), // No timestamp in source, use current
|
||||
Timestamp: time.Now(),
|
||||
Source: "Memory",
|
||||
SensorType: "memory",
|
||||
SensorName: mem.MemModSlot,
|
||||
|
||||
Reference in New Issue
Block a user