v1.2.0: Enhanced Inspur/Kaytus parser with GPU, PCIe, and storage support
Major improvements: - Add CSV SEL event parser for Kaytus firmware format - Add PCIe device parser with link speed/width detection - Add GPU temperature and PCIe link monitoring - Add disk backplane parser for storage bay information - Fix memory module detection (only show installed DIMMs) Parser enhancements: - Parse RESTful PCIe Device info (max/current link width/speed) - Parse GPU sensor data (core and memory temperatures) - Parse diskbackplane info (slot count, installed drives) - Parse SEL events from CSV format (selelist.csv) - Fix memory Present status logic (check mem_mod_status) Web interface improvements: - Add PCIe link degradation highlighting (red when current < max) - Add storage table with Present status and location - Update memory specification to show only installed modules with frequency - Sort events from newest to oldest - Filter out N/A serial numbers from display Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -21,7 +21,7 @@ var (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
port := flag.Int("port", 8080, "HTTP server port")
|
port := flag.Int("port", 8082, "HTTP server port")
|
||||||
file := flag.String("file", "", "Pre-load archive file")
|
file := flag.String("file", "", "Pre-load archive file")
|
||||||
showVersion := flag.Bool("version", false, "Show version")
|
showVersion := flag.Bool("version", false, "Show version")
|
||||||
noBrowser := flag.Bool("no-browser", false, "Don't open browser automatically")
|
noBrowser := flag.Bool("no-browser", false, "Don't open browser automatically")
|
||||||
|
|||||||
@@ -129,6 +129,9 @@ type Storage struct {
|
|||||||
Manufacturer string `json:"manufacturer,omitempty"`
|
Manufacturer string `json:"manufacturer,omitempty"`
|
||||||
Firmware string `json:"firmware,omitempty"`
|
Firmware string `json:"firmware,omitempty"`
|
||||||
Interface string `json:"interface,omitempty"`
|
Interface string `json:"interface,omitempty"`
|
||||||
|
Present bool `json:"present"`
|
||||||
|
Location string `json:"location,omitempty"` // Front/Rear
|
||||||
|
BackplaneID int `json:"backplane_id,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// PCIeDevice represents a PCIe device
|
// PCIeDevice represents a PCIe device
|
||||||
@@ -178,16 +181,26 @@ type PSU struct {
|
|||||||
|
|
||||||
// GPU represents a graphics processing unit
|
// GPU represents a graphics processing unit
|
||||||
type GPU struct {
|
type GPU struct {
|
||||||
Slot string `json:"slot"`
|
Slot string `json:"slot"`
|
||||||
Model string `json:"model"`
|
Location string `json:"location,omitempty"`
|
||||||
Manufacturer string `json:"manufacturer,omitempty"`
|
Model string `json:"model"`
|
||||||
VendorID int `json:"vendor_id,omitempty"`
|
Manufacturer string `json:"manufacturer,omitempty"`
|
||||||
DeviceID int `json:"device_id,omitempty"`
|
VendorID int `json:"vendor_id,omitempty"`
|
||||||
BDF string `json:"bdf,omitempty"`
|
DeviceID int `json:"device_id,omitempty"`
|
||||||
SerialNumber string `json:"serial_number,omitempty"`
|
BDF string `json:"bdf,omitempty"`
|
||||||
PartNumber string `json:"part_number,omitempty"`
|
SerialNumber string `json:"serial_number,omitempty"`
|
||||||
LinkWidth int `json:"link_width,omitempty"`
|
PartNumber string `json:"part_number,omitempty"`
|
||||||
LinkSpeed string `json:"link_speed,omitempty"`
|
Firmware string `json:"firmware,omitempty"`
|
||||||
|
Temperature int `json:"temperature,omitempty"` // GPU core temp
|
||||||
|
MemTemperature int `json:"mem_temperature,omitempty"` // GPU memory temp
|
||||||
|
Power int `json:"power,omitempty"` // Current power draw (W)
|
||||||
|
MaxPower int `json:"max_power,omitempty"` // TDP (W)
|
||||||
|
ClockSpeed int `json:"clock_speed,omitempty"` // Operating speed MHz
|
||||||
|
MaxLinkWidth int `json:"max_link_width,omitempty"`
|
||||||
|
MaxLinkSpeed string `json:"max_link_speed,omitempty"`
|
||||||
|
CurrentLinkWidth int `json:"current_link_width,omitempty"`
|
||||||
|
CurrentLinkSpeed string `json:"current_link_speed,omitempty"`
|
||||||
|
Status string `json:"status,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// NetworkAdapter represents a network adapter with detailed info
|
// NetworkAdapter represents a network adapter with detailed info
|
||||||
|
|||||||
10
internal/parser/vendors/inspur/asset.go
vendored
10
internal/parser/vendors/inspur/asset.go
vendored
@@ -207,8 +207,8 @@ func ParseAssetJSON(content []byte) (*models.HardwareConfig, error) {
|
|||||||
VendorID: pcie.VendorId,
|
VendorID: pcie.VendorId,
|
||||||
DeviceID: pcie.DeviceId,
|
DeviceID: pcie.DeviceId,
|
||||||
BDF: formatBDF(pcie.BusNumber, pcie.DeviceNumber, pcie.FunctionNumber),
|
BDF: formatBDF(pcie.BusNumber, pcie.DeviceNumber, pcie.FunctionNumber),
|
||||||
LinkWidth: pcie.NegotiatedLinkWidth,
|
LinkWidth: pcie.NegotiatedLinkWidth,
|
||||||
LinkSpeed: pcieLinkSpeedToString(pcie.CurrentLinkSpeed),
|
LinkSpeed: pcieLinkSpeedToString(pcie.CurrentLinkSpeed),
|
||||||
MaxLinkWidth: pcie.MaxLinkWidth,
|
MaxLinkWidth: pcie.MaxLinkWidth,
|
||||||
MaxLinkSpeed: pcieLinkSpeedToString(pcie.MaxLinkSpeed),
|
MaxLinkSpeed: pcieLinkSpeedToString(pcie.MaxLinkSpeed),
|
||||||
DeviceClass: pcieClassToString(pcie.ClassCode, pcie.SubClassCode),
|
DeviceClass: pcieClassToString(pcie.ClassCode, pcie.SubClassCode),
|
||||||
@@ -242,8 +242,10 @@ func ParseAssetJSON(content []byte) (*models.HardwareConfig, error) {
|
|||||||
VendorID: pcie.VendorId,
|
VendorID: pcie.VendorId,
|
||||||
DeviceID: pcie.DeviceId,
|
DeviceID: pcie.DeviceId,
|
||||||
BDF: formatBDF(pcie.BusNumber, pcie.DeviceNumber, pcie.FunctionNumber),
|
BDF: formatBDF(pcie.BusNumber, pcie.DeviceNumber, pcie.FunctionNumber),
|
||||||
LinkWidth: pcie.NegotiatedLinkWidth,
|
CurrentLinkWidth: pcie.NegotiatedLinkWidth,
|
||||||
LinkSpeed: pcieLinkSpeedToString(pcie.CurrentLinkSpeed),
|
CurrentLinkSpeed: pcieLinkSpeedToString(pcie.CurrentLinkSpeed),
|
||||||
|
MaxLinkWidth: pcie.MaxLinkWidth,
|
||||||
|
MaxLinkSpeed: pcieLinkSpeedToString(pcie.MaxLinkSpeed),
|
||||||
}
|
}
|
||||||
if pcie.PartNumber != nil {
|
if pcie.PartNumber != nil {
|
||||||
gpu.PartNumber = strings.TrimSpace(*pcie.PartNumber)
|
gpu.PartNumber = strings.TrimSpace(*pcie.PartNumber)
|
||||||
|
|||||||
59
internal/parser/vendors/inspur/component.go
vendored
59
internal/parser/vendors/inspur/component.go
vendored
@@ -27,6 +27,9 @@ func ParseComponentLog(content []byte, hw *models.HardwareConfig) {
|
|||||||
// Parse RESTful HDD info
|
// Parse RESTful HDD info
|
||||||
parseHDDInfo(text, hw)
|
parseHDDInfo(text, hw)
|
||||||
|
|
||||||
|
// Parse RESTful diskbackplane info
|
||||||
|
parseDiskBackplaneInfo(text, hw)
|
||||||
|
|
||||||
// Parse RESTful Network Adapter info
|
// Parse RESTful Network Adapter info
|
||||||
parseNetworkAdapterInfo(text, hw)
|
parseNetworkAdapterInfo(text, hw)
|
||||||
|
|
||||||
@@ -52,6 +55,7 @@ type MemoryRESTInfo struct {
|
|||||||
MemModID int `json:"mem_mod_id"`
|
MemModID int `json:"mem_mod_id"`
|
||||||
ConfigStatus int `json:"config_status"`
|
ConfigStatus int `json:"config_status"`
|
||||||
MemModSlot string `json:"mem_mod_slot"`
|
MemModSlot string `json:"mem_mod_slot"`
|
||||||
|
MemModStatus int `json:"mem_mod_status"`
|
||||||
MemModSize int `json:"mem_mod_size"`
|
MemModSize int `json:"mem_mod_size"`
|
||||||
MemModType string `json:"mem_mod_type"`
|
MemModType string `json:"mem_mod_type"`
|
||||||
MemModTechnology string `json:"mem_mod_technology"`
|
MemModTechnology string `json:"mem_mod_technology"`
|
||||||
@@ -90,7 +94,7 @@ func parseMemoryInfo(text string, hw *models.HardwareConfig) {
|
|||||||
hw.Memory = append(hw.Memory, models.MemoryDIMM{
|
hw.Memory = append(hw.Memory, models.MemoryDIMM{
|
||||||
Slot: mem.MemModSlot,
|
Slot: mem.MemModSlot,
|
||||||
Location: mem.MemModSlot,
|
Location: mem.MemModSlot,
|
||||||
Present: mem.ConfigStatus == 1,
|
Present: mem.MemModStatus == 1 && mem.MemModSize > 0,
|
||||||
SizeMB: mem.MemModSize * 1024, // Convert GB to MB
|
SizeMB: mem.MemModSize * 1024, // Convert GB to MB
|
||||||
Type: mem.MemModType,
|
Type: mem.MemModType,
|
||||||
Technology: strings.TrimSpace(mem.MemModTechnology),
|
Technology: strings.TrimSpace(mem.MemModTechnology),
|
||||||
@@ -420,3 +424,56 @@ func extractComponentFirmware(text string, hw *models.HardwareConfig) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create storage entries based on backplane info
|
||||||
|
for _, bp := range backplaneInfo {
|
||||||
|
if bp.Present != 1 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
location := "Rear"
|
||||||
|
if bp.Front == 1 {
|
||||||
|
location = "Front"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create entries for each port (disk slot)
|
||||||
|
for i := 0; i < bp.PortCount; i++ {
|
||||||
|
isPresent := i < bp.DriverCount
|
||||||
|
|
||||||
|
hw.Storage = append(hw.Storage, models.Storage{
|
||||||
|
Slot: fmt.Sprintf("%d", i),
|
||||||
|
Present: isPresent,
|
||||||
|
Location: location,
|
||||||
|
BackplaneID: bp.BackplaneIndex,
|
||||||
|
Type: "HDD",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
86
internal/parser/vendors/inspur/parser.go
vendored
86
internal/parser/vendors/inspur/parser.go
vendored
@@ -6,6 +6,7 @@
|
|||||||
package inspur
|
package inspur
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"git.mchus.pro/mchus/logpile/internal/models"
|
"git.mchus.pro/mchus/logpile/internal/models"
|
||||||
@@ -91,12 +92,7 @@ func (p *Parser) Parse(files []parser.ExtractedFile) (*models.AnalysisResult, er
|
|||||||
Sensors: make([]models.SensorReading, 0),
|
Sensors: make([]models.SensorReading, 0),
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse devicefrusdr.log (contains SDR and FRU data)
|
// Parse asset.json first (base hardware info)
|
||||||
if f := parser.FindFileByName(files, "devicefrusdr.log"); f != nil {
|
|
||||||
p.parseDeviceFruSDR(f.Content, result)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse asset.json
|
|
||||||
if f := parser.FindFileByName(files, "asset.json"); f != nil {
|
if f := parser.FindFileByName(files, "asset.json"); f != nil {
|
||||||
if hw, err := ParseAssetJSON(f.Content); err == nil {
|
if hw, err := ParseAssetJSON(f.Content); err == nil {
|
||||||
result.Hardware = hw
|
result.Hardware = hw
|
||||||
@@ -107,6 +103,12 @@ func (p *Parser) Parse(files []parser.ExtractedFile) (*models.AnalysisResult, er
|
|||||||
if result.Hardware == nil {
|
if result.Hardware == nil {
|
||||||
result.Hardware = &models.HardwareConfig{}
|
result.Hardware = &models.HardwareConfig{}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Parse devicefrusdr.log (contains SDR, FRU, PCIe and additional data)
|
||||||
|
if f := parser.FindFileByName(files, "devicefrusdr.log"); f != nil {
|
||||||
|
p.parseDeviceFruSDR(f.Content, result)
|
||||||
|
}
|
||||||
|
|
||||||
extractBoardInfo(result.FRU, result.Hardware)
|
extractBoardInfo(result.FRU, result.Hardware)
|
||||||
|
|
||||||
// Extract PlatformId (server model) from ThermalConfig
|
// Extract PlatformId (server model) from ThermalConfig
|
||||||
@@ -129,6 +131,12 @@ func (p *Parser) Parse(files []parser.ExtractedFile) (*models.AnalysisResult, er
|
|||||||
result.Events = append(result.Events, idlEvents...)
|
result.Events = append(result.Events, idlEvents...)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Parse SEL list (selelist.csv)
|
||||||
|
if f := parser.FindFileByName(files, "selelist.csv"); f != nil {
|
||||||
|
selEvents := ParseSELList(f.Content)
|
||||||
|
result.Events = append(result.Events, selEvents...)
|
||||||
|
}
|
||||||
|
|
||||||
// Parse syslog files
|
// Parse syslog files
|
||||||
syslogFiles := parser.FindFileByPattern(files, "syslog/alert", "syslog/warning", "syslog/notice", "syslog/info")
|
syslogFiles := parser.FindFileByPattern(files, "syslog/alert", "syslog/warning", "syslog/notice", "syslog/info")
|
||||||
for _, f := range syslogFiles {
|
for _, f := range syslogFiles {
|
||||||
@@ -161,4 +169,70 @@ func (p *Parser) parseDeviceFruSDR(content []byte, result *models.AnalysisResult
|
|||||||
fruContent := lines[fruStart:]
|
fruContent := lines[fruStart:]
|
||||||
result.FRU = ParseFRU([]byte(fruContent))
|
result.FRU = ParseFRU([]byte(fruContent))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Parse PCIe devices from RESTful PCIE Device info
|
||||||
|
// This supplements data from asset.json with serial numbers, firmware, etc.
|
||||||
|
pcieDevicesFromREST := ParsePCIeDevices(content)
|
||||||
|
|
||||||
|
// Merge PCIe data: keep asset.json data but add RESTful data if available
|
||||||
|
if result.Hardware != nil {
|
||||||
|
// If asset.json didn't have PCIe devices, use RESTful data
|
||||||
|
if len(result.Hardware.PCIeDevices) == 0 && len(pcieDevicesFromREST) > 0 {
|
||||||
|
result.Hardware.PCIeDevices = pcieDevicesFromREST
|
||||||
|
}
|
||||||
|
// If we have both, merge them (RESTful data takes precedence for detailed info)
|
||||||
|
// For now, we keep asset.json data which has more details
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse GPU devices and add temperature data from sensors
|
||||||
|
if len(result.Sensors) > 0 && result.Hardware != nil {
|
||||||
|
// Use existing GPU data from asset.json and enrich with sensor data
|
||||||
|
for i := range result.Hardware.GPUs {
|
||||||
|
gpu := &result.Hardware.GPUs[i]
|
||||||
|
|
||||||
|
// Extract GPU number from slot name
|
||||||
|
slotNum := extractSlotNumberFromGPU(gpu.Slot)
|
||||||
|
|
||||||
|
// Find temperature sensors for this GPU
|
||||||
|
for _, sensor := range result.Sensors {
|
||||||
|
sensorName := strings.ToUpper(sensor.Name)
|
||||||
|
|
||||||
|
// Match GPU temperature sensor
|
||||||
|
if strings.Contains(sensorName, fmt.Sprintf("GPU%d_TEMP", slotNum)) && !strings.Contains(sensorName, "MEM") {
|
||||||
|
if sensor.RawValue != "" {
|
||||||
|
fmt.Sscanf(sensor.RawValue, "%d", &gpu.Temperature)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Match GPU memory temperature
|
||||||
|
if strings.Contains(sensorName, fmt.Sprintf("GPU%d_MEM_TEMP", slotNum)) {
|
||||||
|
if sensor.RawValue != "" {
|
||||||
|
fmt.Sscanf(sensor.RawValue, "%d", &gpu.MemTemperature)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Match PCIe slot temperature as fallback
|
||||||
|
if strings.Contains(sensorName, fmt.Sprintf("PCIE%d_GPU_TLM_T", slotNum)) && gpu.Temperature == 0 {
|
||||||
|
if sensor.RawValue != "" {
|
||||||
|
fmt.Sscanf(sensor.RawValue, "%d", &gpu.Temperature)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// extractSlotNumberFromGPU extracts slot number from GPU slot string
|
||||||
|
func extractSlotNumberFromGPU(slot string) int {
|
||||||
|
parts := strings.Split(slot, "_")
|
||||||
|
for _, part := range parts {
|
||||||
|
if strings.HasPrefix(part, "PCIE") {
|
||||||
|
var num int
|
||||||
|
fmt.Sscanf(part, "PCIE%d", &num)
|
||||||
|
if num > 0 {
|
||||||
|
return num
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0
|
||||||
}
|
}
|
||||||
|
|||||||
214
internal/parser/vendors/inspur/pcie.go
vendored
Normal file
214
internal/parser/vendors/inspur/pcie.go
vendored
Normal file
@@ -0,0 +1,214 @@
|
|||||||
|
package inspur
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"git.mchus.pro/mchus/logpile/internal/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
// PCIeRESTInfo represents the RESTful PCIE Device info structure
|
||||||
|
type PCIeRESTInfo []struct {
|
||||||
|
ID int `json:"id"`
|
||||||
|
Present int `json:"present"`
|
||||||
|
Enable int `json:"enable"`
|
||||||
|
Status int `json:"status"`
|
||||||
|
VendorID int `json:"vendor_id"`
|
||||||
|
VendorName string `json:"vendor_name"`
|
||||||
|
DeviceID int `json:"device_id"`
|
||||||
|
DeviceName string `json:"device_name"`
|
||||||
|
BusNum int `json:"bus_num"`
|
||||||
|
DevNum int `json:"dev_num"`
|
||||||
|
FuncNum int `json:"func_num"`
|
||||||
|
MaxLinkWidth int `json:"max_link_width"`
|
||||||
|
MaxLinkSpeed int `json:"max_link_speed"`
|
||||||
|
CurrentLinkWidth int `json:"current_link_width"`
|
||||||
|
CurrentLinkSpeed int `json:"current_link_speed"`
|
||||||
|
Slot int `json:"slot"`
|
||||||
|
Location string `json:"location"`
|
||||||
|
DeviceLocator string `json:"DeviceLocator"`
|
||||||
|
DevType int `json:"dev_type"`
|
||||||
|
DevSubtype int `json:"dev_subtype"`
|
||||||
|
PartNum string `json:"part_num"`
|
||||||
|
SerialNum string `json:"serial_num"`
|
||||||
|
FwVer string `json:"fw_ver"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParsePCIeDevices parses RESTful PCIE Device info from devicefrusdr.log
|
||||||
|
func ParsePCIeDevices(content []byte) []models.PCIeDevice {
|
||||||
|
text := string(content)
|
||||||
|
|
||||||
|
// Find RESTful PCIE Device info section
|
||||||
|
startMarker := "RESTful PCIE Device info:"
|
||||||
|
endMarker := "BMC sdr Info:"
|
||||||
|
|
||||||
|
startIdx := strings.Index(text, startMarker)
|
||||||
|
if startIdx == -1 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
endIdx := strings.Index(text[startIdx:], endMarker)
|
||||||
|
if endIdx == -1 {
|
||||||
|
endIdx = len(text) - startIdx
|
||||||
|
}
|
||||||
|
|
||||||
|
jsonText := text[startIdx+len(startMarker) : startIdx+endIdx]
|
||||||
|
jsonText = strings.TrimSpace(jsonText)
|
||||||
|
|
||||||
|
var pcieInfo PCIeRESTInfo
|
||||||
|
if err := json.Unmarshal([]byte(jsonText), &pcieInfo); err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var devices []models.PCIeDevice
|
||||||
|
for _, pcie := range pcieInfo {
|
||||||
|
if pcie.Present != 1 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert PCIe speed to GEN notation
|
||||||
|
maxSpeed := fmt.Sprintf("GEN%d", pcie.MaxLinkSpeed)
|
||||||
|
currentSpeed := fmt.Sprintf("GEN%d", pcie.CurrentLinkSpeed)
|
||||||
|
|
||||||
|
// Determine device class based on dev_type
|
||||||
|
deviceClass := determineDeviceClass(pcie.DevType, pcie.DevSubtype, pcie.DeviceName)
|
||||||
|
|
||||||
|
// Build BDF string
|
||||||
|
bdf := fmt.Sprintf("%04x/%02x/%02x/%02x", 0, pcie.BusNum, pcie.DevNum, pcie.FuncNum)
|
||||||
|
|
||||||
|
device := models.PCIeDevice{
|
||||||
|
Slot: pcie.Location,
|
||||||
|
VendorID: pcie.VendorID,
|
||||||
|
DeviceID: pcie.DeviceID,
|
||||||
|
BDF: bdf,
|
||||||
|
DeviceClass: deviceClass,
|
||||||
|
Manufacturer: pcie.VendorName,
|
||||||
|
LinkWidth: pcie.CurrentLinkWidth,
|
||||||
|
LinkSpeed: currentSpeed,
|
||||||
|
MaxLinkWidth: pcie.MaxLinkWidth,
|
||||||
|
MaxLinkSpeed: maxSpeed,
|
||||||
|
PartNumber: strings.TrimSpace(pcie.PartNum),
|
||||||
|
SerialNumber: strings.TrimSpace(pcie.SerialNum),
|
||||||
|
}
|
||||||
|
|
||||||
|
devices = append(devices, device)
|
||||||
|
}
|
||||||
|
|
||||||
|
return devices
|
||||||
|
}
|
||||||
|
|
||||||
|
// determineDeviceClass maps device type to human-readable class
|
||||||
|
func determineDeviceClass(devType, devSubtype int, deviceName string) string {
|
||||||
|
// dev_type mapping:
|
||||||
|
// 1 = Mass Storage Controller
|
||||||
|
// 2 = Network Controller
|
||||||
|
// 3 = Display Controller (GPU)
|
||||||
|
// 4 = Multimedia Controller
|
||||||
|
|
||||||
|
switch devType {
|
||||||
|
case 1:
|
||||||
|
if devSubtype == 4 {
|
||||||
|
return "RAID Controller"
|
||||||
|
}
|
||||||
|
return "Storage Controller"
|
||||||
|
case 2:
|
||||||
|
return "Network Controller"
|
||||||
|
case 3:
|
||||||
|
// GPU
|
||||||
|
if strings.Contains(strings.ToUpper(deviceName), "H100") {
|
||||||
|
return "GPU (H100)"
|
||||||
|
}
|
||||||
|
if strings.Contains(strings.ToUpper(deviceName), "A100") {
|
||||||
|
return "GPU (A100)"
|
||||||
|
}
|
||||||
|
if strings.Contains(strings.ToUpper(deviceName), "NVIDIA") {
|
||||||
|
return "GPU"
|
||||||
|
}
|
||||||
|
return "Display Controller"
|
||||||
|
case 4:
|
||||||
|
return "Multimedia Controller"
|
||||||
|
default:
|
||||||
|
return "Unknown"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParseGPUs extracts GPU data from PCIe devices and sensors
|
||||||
|
func ParseGPUs(pcieDevices []models.PCIeDevice, sensors []models.SensorReading) []models.GPU {
|
||||||
|
var gpus []models.GPU
|
||||||
|
|
||||||
|
// Find GPU devices
|
||||||
|
for _, pcie := range pcieDevices {
|
||||||
|
if !strings.Contains(strings.ToLower(pcie.DeviceClass), "gpu") &&
|
||||||
|
!strings.Contains(strings.ToLower(pcie.DeviceClass), "display") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skip integrated graphics (ASPEED, etc.)
|
||||||
|
if strings.Contains(pcie.Manufacturer, "ASPEED") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
gpu := models.GPU{
|
||||||
|
Slot: pcie.Slot,
|
||||||
|
Location: pcie.Slot,
|
||||||
|
Model: pcie.DeviceClass,
|
||||||
|
Manufacturer: pcie.Manufacturer,
|
||||||
|
SerialNumber: pcie.SerialNumber,
|
||||||
|
MaxLinkWidth: pcie.MaxLinkWidth,
|
||||||
|
MaxLinkSpeed: pcie.MaxLinkSpeed,
|
||||||
|
CurrentLinkWidth: pcie.LinkWidth,
|
||||||
|
CurrentLinkSpeed: pcie.LinkSpeed,
|
||||||
|
Status: "OK",
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract GPU number from slot name (e.g., "PCIE7" -> 7)
|
||||||
|
slotNum := extractSlotNumber(pcie.Slot)
|
||||||
|
|
||||||
|
// Find temperature sensors for this GPU
|
||||||
|
for _, sensor := range sensors {
|
||||||
|
sensorName := strings.ToUpper(sensor.Name)
|
||||||
|
|
||||||
|
// Match GPU temperature sensor (e.g., "GPU7_Temp")
|
||||||
|
if strings.Contains(sensorName, fmt.Sprintf("GPU%d_TEMP", slotNum)) {
|
||||||
|
if sensor.RawValue != "" {
|
||||||
|
fmt.Sscanf(sensor.RawValue, "%d", &gpu.Temperature)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Match GPU memory temperature (e.g., "GPU7_Mem_Temp")
|
||||||
|
if strings.Contains(sensorName, fmt.Sprintf("GPU%d_MEM_TEMP", slotNum)) {
|
||||||
|
if sensor.RawValue != "" {
|
||||||
|
fmt.Sscanf(sensor.RawValue, "%d", &gpu.MemTemperature)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Match PCIe slot temperature (e.g., "PCIE7_GPU_TLM_T")
|
||||||
|
if strings.Contains(sensorName, fmt.Sprintf("PCIE%d_GPU_TLM_T", slotNum)) {
|
||||||
|
if sensor.RawValue != "" && gpu.Temperature == 0 {
|
||||||
|
fmt.Sscanf(sensor.RawValue, "%d", &gpu.Temperature)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
gpus = append(gpus, gpu)
|
||||||
|
}
|
||||||
|
|
||||||
|
return gpus
|
||||||
|
}
|
||||||
|
|
||||||
|
// extractSlotNumber extracts slot number from location string
|
||||||
|
// e.g., "CPU0_PE3_AC_PCIE7" -> 7
|
||||||
|
func extractSlotNumber(location string) int {
|
||||||
|
parts := strings.Split(location, "_")
|
||||||
|
for _, part := range parts {
|
||||||
|
if strings.HasPrefix(part, "PCIE") || strings.HasPrefix(part, "#CPU") {
|
||||||
|
var num int
|
||||||
|
fmt.Sscanf(part, "PCIE%d", &num)
|
||||||
|
if num > 0 {
|
||||||
|
return num
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
1
internal/parser/vendors/inspur/sdr.go
vendored
1
internal/parser/vendors/inspur/sdr.go
vendored
@@ -46,6 +46,7 @@ func ParseSDR(content []byte) []models.SensorReading {
|
|||||||
if v, err := strconv.ParseFloat(vm[1], 64); err == nil {
|
if v, err := strconv.ParseFloat(vm[1], 64); err == nil {
|
||||||
reading.Value = v
|
reading.Value = v
|
||||||
reading.Unit = strings.TrimSpace(vm[2])
|
reading.Unit = strings.TrimSpace(vm[2])
|
||||||
|
reading.RawValue = valueStr // Keep original string for reference
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if strings.HasPrefix(valueStr, "0x") {
|
} else if strings.HasPrefix(valueStr, "0x") {
|
||||||
|
|||||||
174
internal/parser/vendors/inspur/sel.go
vendored
Normal file
174
internal/parser/vendors/inspur/sel.go
vendored
Normal file
@@ -0,0 +1,174 @@
|
|||||||
|
package inspur
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/csv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"git.mchus.pro/mchus/logpile/internal/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ParseSELList parses selelist.csv file with SEL events
|
||||||
|
// Format: ID, Date (MM/DD/YYYY), Time (HH:MM:SS), Sensor, Event, Status
|
||||||
|
// Example: 1,04/18/2025,09:31:18,Event Logging Disabled SEL_Status,Log area reset/cleared,Asserted
|
||||||
|
func ParseSELList(content []byte) []models.Event {
|
||||||
|
var events []models.Event
|
||||||
|
|
||||||
|
text := string(content)
|
||||||
|
lines := strings.Split(text, "\n")
|
||||||
|
|
||||||
|
// Skip header line(s) if present
|
||||||
|
startIdx := 0
|
||||||
|
for i, line := range lines {
|
||||||
|
if strings.Contains(strings.ToLower(line), "sel elist") {
|
||||||
|
startIdx = i + 1
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse CSV data
|
||||||
|
for i := startIdx; i < len(lines); i++ {
|
||||||
|
line := strings.TrimSpace(lines[i])
|
||||||
|
if line == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse CSV line
|
||||||
|
r := csv.NewReader(strings.NewReader(line))
|
||||||
|
records, err := r.Read()
|
||||||
|
if err != nil || len(records) < 6 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
eventID := strings.TrimSpace(records[0])
|
||||||
|
dateStr := strings.TrimSpace(records[1])
|
||||||
|
timeStr := strings.TrimSpace(records[2])
|
||||||
|
sensorStr := strings.TrimSpace(records[3])
|
||||||
|
eventDesc := strings.TrimSpace(records[4])
|
||||||
|
status := strings.TrimSpace(records[5])
|
||||||
|
|
||||||
|
// Parse timestamp: MM/DD/YYYY HH:MM:SS
|
||||||
|
timestamp := parseSELTimestamp(dateStr, timeStr)
|
||||||
|
|
||||||
|
// Extract sensor type and name
|
||||||
|
sensorType, sensorName := parseSensorInfo(sensorStr)
|
||||||
|
|
||||||
|
// Determine severity
|
||||||
|
severity := determineSELSeverity(sensorStr, eventDesc, status)
|
||||||
|
|
||||||
|
// Build full description
|
||||||
|
description := buildSELDescription(eventDesc, status)
|
||||||
|
|
||||||
|
events = append(events, models.Event{
|
||||||
|
ID: eventID,
|
||||||
|
Timestamp: timestamp,
|
||||||
|
Source: "SEL",
|
||||||
|
SensorType: sensorType,
|
||||||
|
SensorName: sensorName,
|
||||||
|
EventType: eventDesc,
|
||||||
|
Severity: severity,
|
||||||
|
Description: description,
|
||||||
|
RawData: line,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return events
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseSELTimestamp parses MM/DD/YYYY and HH:MM:SS into time.Time
|
||||||
|
func parseSELTimestamp(dateStr, timeStr string) time.Time {
|
||||||
|
// Combine date and time: MM/DD/YYYY HH:MM:SS
|
||||||
|
timestampStr := dateStr + " " + timeStr
|
||||||
|
|
||||||
|
// Try parsing with MM/DD/YYYY format
|
||||||
|
t, err := time.Parse("01/02/2006 15:04:05", timestampStr)
|
||||||
|
if err != nil {
|
||||||
|
// Fallback to current time
|
||||||
|
return time.Now()
|
||||||
|
}
|
||||||
|
|
||||||
|
return t
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseSensorInfo extracts sensor type and name from sensor string
|
||||||
|
// Example: "Event Logging Disabled SEL_Status" -> ("sel", "SEL_Status")
|
||||||
|
// Example: "Power Supply PSU0_Status" -> ("power_supply", "PSU0_Status")
|
||||||
|
func parseSensorInfo(sensorStr string) (sensorType, sensorName string) {
|
||||||
|
parts := strings.Fields(sensorStr)
|
||||||
|
if len(parts) == 0 {
|
||||||
|
return "unknown", sensorStr
|
||||||
|
}
|
||||||
|
|
||||||
|
// Last part is usually the sensor name
|
||||||
|
sensorName = parts[len(parts)-1]
|
||||||
|
|
||||||
|
// First parts form the sensor type
|
||||||
|
if len(parts) > 1 {
|
||||||
|
sensorType = strings.ToLower(strings.Join(parts[:len(parts)-1], "_"))
|
||||||
|
} else {
|
||||||
|
sensorType = "system"
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// determineSELSeverity determines event severity based on sensor and event description
|
||||||
|
func determineSELSeverity(sensorStr, eventDesc, status string) models.Severity {
|
||||||
|
lowerSensor := strings.ToLower(sensorStr)
|
||||||
|
lowerEvent := strings.ToLower(eventDesc)
|
||||||
|
lowerStatus := strings.ToLower(status)
|
||||||
|
|
||||||
|
// Critical indicators
|
||||||
|
criticalKeywords := []string{
|
||||||
|
"critical", "failure", "fault", "error",
|
||||||
|
"ac lost", "predictive failure", "redundancy lost",
|
||||||
|
"going high", "going low", "transition to critical",
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, keyword := range criticalKeywords {
|
||||||
|
if strings.Contains(lowerSensor, keyword) ||
|
||||||
|
strings.Contains(lowerEvent, keyword) ||
|
||||||
|
strings.Contains(lowerStatus, keyword) {
|
||||||
|
return models.SeverityCritical
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Warning indicators
|
||||||
|
warningKeywords := []string{
|
||||||
|
"warning", "disabled", "non-recoverable",
|
||||||
|
"device removed", "device absent",
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, keyword := range warningKeywords {
|
||||||
|
if strings.Contains(lowerSensor, keyword) ||
|
||||||
|
strings.Contains(lowerEvent, keyword) ||
|
||||||
|
strings.Contains(lowerStatus, keyword) {
|
||||||
|
return models.SeverityWarning
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Info indicators (normal operations)
|
||||||
|
infoKeywords := []string{
|
||||||
|
"presence detected", "device present", "asserted",
|
||||||
|
"initiated by", "state asserted", "s0/g0: working",
|
||||||
|
"power button pressed",
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, keyword := range infoKeywords {
|
||||||
|
if strings.Contains(lowerEvent, keyword) ||
|
||||||
|
strings.Contains(lowerStatus, keyword) {
|
||||||
|
return models.SeverityInfo
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default to info
|
||||||
|
return models.SeverityInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
// buildSELDescription builds human-readable description
|
||||||
|
func buildSELDescription(eventDesc, status string) string {
|
||||||
|
if status == "Asserted" || status == "Deasserted" {
|
||||||
|
return eventDesc
|
||||||
|
}
|
||||||
|
return eventDesc + " (" + status + ")"
|
||||||
|
}
|
||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
"html/template"
|
"html/template"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -86,7 +87,17 @@ func (s *Server) handleGetEvents(w http.ResponseWriter, r *http.Request) {
|
|||||||
jsonResponse(w, []interface{}{})
|
jsonResponse(w, []interface{}{})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
jsonResponse(w, result.Events)
|
|
||||||
|
// Sort events by timestamp (newest first)
|
||||||
|
events := make([]models.Event, len(result.Events))
|
||||||
|
copy(events, result.Events)
|
||||||
|
|
||||||
|
// Sort in descending order using sort.Slice (newest first)
|
||||||
|
sort.Slice(events, func(i, j int) bool {
|
||||||
|
return events[i].Timestamp.After(events[j].Timestamp)
|
||||||
|
})
|
||||||
|
|
||||||
|
jsonResponse(w, events)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) handleGetSensors(w http.ResponseWriter, r *http.Request) {
|
func (s *Server) handleGetSensors(w http.ResponseWriter, r *http.Request) {
|
||||||
@@ -145,10 +156,20 @@ func buildSpecification(result *models.AnalysisResult) []SpecLine {
|
|||||||
spec = append(spec, SpecLine{Category: "Процессор", Name: name, Quantity: count})
|
spec = append(spec, SpecLine{Category: "Процессор", Name: name, Quantity: count})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Memory - group by size and type
|
// Memory - group by size, type and frequency (only installed modules)
|
||||||
memGroups := make(map[string]int)
|
memGroups := make(map[string]int)
|
||||||
for _, mem := range hw.Memory {
|
for _, mem := range hw.Memory {
|
||||||
key := fmt.Sprintf("%s %dGB", mem.Type, mem.SizeMB/1024)
|
// Skip empty slots (not present or 0 size)
|
||||||
|
if !mem.Present || mem.SizeMB == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// Include frequency if available
|
||||||
|
key := ""
|
||||||
|
if mem.CurrentSpeedMHz > 0 {
|
||||||
|
key = fmt.Sprintf("%s %dGB %dMHz", mem.Type, mem.SizeMB/1024, mem.CurrentSpeedMHz)
|
||||||
|
} else {
|
||||||
|
key = fmt.Sprintf("%s %dGB", mem.Type, mem.SizeMB/1024)
|
||||||
|
}
|
||||||
memGroups[key]++
|
memGroups[key]++
|
||||||
}
|
}
|
||||||
for key, count := range memGroups {
|
for key, count := range memGroups {
|
||||||
|
|||||||
@@ -698,3 +698,14 @@ footer {
|
|||||||
padding: 0.25rem 0.5rem;
|
padding: 0.25rem 0.5rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* PCIe degraded link highlighting */
|
||||||
|
.pcie-degraded {
|
||||||
|
color: #dc3545;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pcie-max {
|
||||||
|
color: #6c757d;
|
||||||
|
font-size: 0.9em;
|
||||||
|
}
|
||||||
|
|||||||
@@ -326,20 +326,24 @@ function renderConfig(data) {
|
|||||||
if (storNVMe > 0) typesSummary.push(`${storNVMe} NVMe`);
|
if (storNVMe > 0) typesSummary.push(`${storNVMe} NVMe`);
|
||||||
html += `<h3>Накопители</h3>
|
html += `<h3>Накопители</h3>
|
||||||
<div class="section-overview">
|
<div class="section-overview">
|
||||||
<div class="stat-box"><span class="stat-value">${storTotal}</span><span class="stat-label">Всего</span></div>
|
<div class="stat-box"><span class="stat-value">${storTotal}</span><span class="stat-label">Всего слотов</span></div>
|
||||||
<div class="stat-box"><span class="stat-value">${totalTB} TB</span><span class="stat-label">Объём</span></div>
|
<div class="stat-box"><span class="stat-value">${config.storage.filter(s => s.present).length}</span><span class="stat-label">Установлено</span></div>
|
||||||
|
<div class="stat-box"><span class="stat-value">${totalTB > 0 ? totalTB + ' TB' : '-'}</span><span class="stat-label">Объём</span></div>
|
||||||
<div class="stat-box model-box"><span class="stat-value">${typesSummary.join(', ') || '-'}</span><span class="stat-label">По типам</span></div>
|
<div class="stat-box model-box"><span class="stat-value">${typesSummary.join(', ') || '-'}</span><span class="stat-label">По типам</span></div>
|
||||||
</div>
|
</div>
|
||||||
<table class="config-table"><thead><tr><th>Слот</th><th>Тип</th><th>Интерфейс</th><th>Модель</th><th>Производитель</th><th>Размер</th><th>Серийный номер</th></tr></thead><tbody>`;
|
<table class="config-table"><thead><tr><th>NO.</th><th>Статус</th><th>Расположение</th><th>Backplane ID</th><th>Тип</th><th>Модель</th><th>Размер</th><th>Серийный номер</th></tr></thead><tbody>`;
|
||||||
config.storage.forEach(s => {
|
config.storage.forEach(s => {
|
||||||
|
const presentIcon = s.present ? '<span style="color: #27ae60;">●</span>' : '<span style="color: #95a5a6;">○</span>';
|
||||||
|
const presentText = s.present ? 'Present' : 'Empty';
|
||||||
html += `<tr>
|
html += `<tr>
|
||||||
<td>${escapeHtml(s.slot || '-')}</td>
|
<td>${escapeHtml(s.slot || '-')}</td>
|
||||||
|
<td>${presentIcon} ${presentText}</td>
|
||||||
|
<td>${escapeHtml(s.location || '-')}</td>
|
||||||
|
<td>${s.backplane_id !== undefined ? s.backplane_id : '-'}</td>
|
||||||
<td>${escapeHtml(s.type || '-')}</td>
|
<td>${escapeHtml(s.type || '-')}</td>
|
||||||
<td>${escapeHtml(s.interface || '-')}</td>
|
|
||||||
<td>${escapeHtml(s.model || '-')}</td>
|
<td>${escapeHtml(s.model || '-')}</td>
|
||||||
<td>${escapeHtml(s.manufacturer || '-')}</td>
|
<td>${s.size_gb > 0 ? s.size_gb + ' GB' : '-'}</td>
|
||||||
<td>${s.size_gb} GB</td>
|
<td>${s.serial_number ? '<code>' + escapeHtml(s.serial_number) + '</code>' : '-'}</td>
|
||||||
<td><code>${escapeHtml(s.serial_number || '-')}</code></td>
|
|
||||||
</tr>`;
|
</tr>`;
|
||||||
});
|
});
|
||||||
html += '</tbody></table>';
|
html += '</tbody></table>';
|
||||||
@@ -362,12 +366,18 @@ function renderConfig(data) {
|
|||||||
</div>
|
</div>
|
||||||
<table class="config-table"><thead><tr><th>Слот</th><th>Модель</th><th>Производитель</th><th>BDF</th><th>PCIe</th><th>Серийный номер</th></tr></thead><tbody>`;
|
<table class="config-table"><thead><tr><th>Слот</th><th>Модель</th><th>Производитель</th><th>BDF</th><th>PCIe</th><th>Серийный номер</th></tr></thead><tbody>`;
|
||||||
config.gpus.forEach(gpu => {
|
config.gpus.forEach(gpu => {
|
||||||
|
const pcieLink = formatPCIeLink(
|
||||||
|
gpu.current_link_width || gpu.link_width,
|
||||||
|
gpu.current_link_speed || gpu.link_speed,
|
||||||
|
gpu.max_link_width,
|
||||||
|
gpu.max_link_speed
|
||||||
|
);
|
||||||
html += `<tr>
|
html += `<tr>
|
||||||
<td>${escapeHtml(gpu.slot || '-')}</td>
|
<td>${escapeHtml(gpu.slot || '-')}</td>
|
||||||
<td>${escapeHtml(gpu.model || '-')}</td>
|
<td>${escapeHtml(gpu.model || '-')}</td>
|
||||||
<td>${escapeHtml(gpu.manufacturer || '-')}</td>
|
<td>${escapeHtml(gpu.manufacturer || '-')}</td>
|
||||||
<td><code>${escapeHtml(gpu.bdf || '-')}</code></td>
|
<td><code>${escapeHtml(gpu.bdf || '-')}</code></td>
|
||||||
<td>x${gpu.link_width || '-'} ${escapeHtml(gpu.link_speed || '-')}</td>
|
<td>${pcieLink}</td>
|
||||||
<td><code>${escapeHtml(gpu.serial_number || '-')}</code></td>
|
<td><code>${escapeHtml(gpu.serial_number || '-')}</code></td>
|
||||||
</tr>`;
|
</tr>`;
|
||||||
});
|
});
|
||||||
@@ -416,7 +426,12 @@ function renderConfig(data) {
|
|||||||
if (config.pcie_devices && config.pcie_devices.length > 0) {
|
if (config.pcie_devices && config.pcie_devices.length > 0) {
|
||||||
html += '<h3>PCIe устройства</h3><table class="config-table"><thead><tr><th>Слот</th><th>BDF</th><th>Тип</th><th>Производитель</th><th>Vendor:Device ID</th><th>PCIe Link</th></tr></thead><tbody>';
|
html += '<h3>PCIe устройства</h3><table class="config-table"><thead><tr><th>Слот</th><th>BDF</th><th>Тип</th><th>Производитель</th><th>Vendor:Device ID</th><th>PCIe Link</th></tr></thead><tbody>';
|
||||||
config.pcie_devices.forEach(p => {
|
config.pcie_devices.forEach(p => {
|
||||||
const pcieLink = formatPCIeLink(p.link_width, p.link_speed);
|
const pcieLink = formatPCIeLink(
|
||||||
|
p.link_width,
|
||||||
|
p.link_speed,
|
||||||
|
p.max_link_width,
|
||||||
|
p.max_link_speed
|
||||||
|
);
|
||||||
html += `<tr>
|
html += `<tr>
|
||||||
<td>${escapeHtml(p.slot || '-')}</td>
|
<td>${escapeHtml(p.slot || '-')}</td>
|
||||||
<td><code>${escapeHtml(p.bdf || '-')}</code></td>
|
<td><code>${escapeHtml(p.bdf || '-')}</code></td>
|
||||||
@@ -592,7 +607,8 @@ function renderSerials(serials) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
serials.forEach(item => {
|
serials.forEach(item => {
|
||||||
if (!item.serial_number) return;
|
// Skip items without serial number or with N/A
|
||||||
|
if (!item.serial_number || item.serial_number === 'N/A') return;
|
||||||
const row = document.createElement('tr');
|
const row = document.createElement('tr');
|
||||||
row.innerHTML = `
|
row.innerHTML = `
|
||||||
<td><span class="category-badge ${item.category.toLowerCase()}">${categoryNames[item.category] || item.category}</span></td>
|
<td><span class="category-badge ${item.category.toLowerCase()}">${categoryNames[item.category] || item.category}</span></td>
|
||||||
@@ -711,23 +727,59 @@ function escapeHtml(text) {
|
|||||||
return div.innerHTML;
|
return div.innerHTML;
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatPCIeLink(width, speed) {
|
function formatPCIeLink(currentWidth, currentSpeed, maxWidth, maxSpeed) {
|
||||||
if (!width && !speed) return '-';
|
// Helper to convert speed to generation
|
||||||
|
function speedToGen(speed) {
|
||||||
// Convert GT/s to PCIe generation
|
if (!speed) return '';
|
||||||
let gen = '';
|
|
||||||
if (speed) {
|
|
||||||
const gtMatch = speed.match(/(\d+\.?\d*)\s*GT/i);
|
const gtMatch = speed.match(/(\d+\.?\d*)\s*GT/i);
|
||||||
if (gtMatch) {
|
if (gtMatch) {
|
||||||
const gts = parseFloat(gtMatch[1]);
|
const gts = parseFloat(gtMatch[1]);
|
||||||
if (gts >= 32) gen = 'Gen5';
|
if (gts >= 32) return 'Gen5';
|
||||||
else if (gts >= 16) gen = 'Gen4';
|
if (gts >= 16) return 'Gen4';
|
||||||
else if (gts >= 8) gen = 'Gen3';
|
if (gts >= 8) return 'Gen3';
|
||||||
else if (gts >= 5) gen = 'Gen2';
|
if (gts >= 5) return 'Gen2';
|
||||||
else if (gts >= 2.5) gen = 'Gen1';
|
if (gts >= 2.5) return 'Gen1';
|
||||||
}
|
}
|
||||||
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
const widthStr = width ? `x${width}` : '';
|
// Helper to extract GT/s value for comparison
|
||||||
return gen ? `${widthStr} PCIe ${gen}` : `${widthStr} ${speed || ''}`;
|
function extractGTs(speed) {
|
||||||
|
if (!speed) return 0;
|
||||||
|
const gtMatch = speed.match(/(\d+\.?\d*)\s*GT/i);
|
||||||
|
return gtMatch ? parseFloat(gtMatch[1]) : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If no data, return dash
|
||||||
|
if (!currentWidth && !currentSpeed) return '-';
|
||||||
|
|
||||||
|
const curGen = speedToGen(currentSpeed);
|
||||||
|
const maxGen = speedToGen(maxSpeed);
|
||||||
|
|
||||||
|
// Check if current is lower than max
|
||||||
|
const widthDegraded = maxWidth && currentWidth && currentWidth < maxWidth;
|
||||||
|
const speedDegraded = maxSpeed && currentSpeed && extractGTs(currentSpeed) < extractGTs(maxSpeed);
|
||||||
|
|
||||||
|
// Build current link string
|
||||||
|
const curWidthStr = currentWidth ? `x${currentWidth}` : '';
|
||||||
|
const curLinkStr = curGen ? `${curWidthStr} ${curGen}` : `${curWidthStr} ${currentSpeed || ''}`;
|
||||||
|
|
||||||
|
// Build max link string (if available)
|
||||||
|
let maxLinkStr = '';
|
||||||
|
if (maxWidth || maxSpeed) {
|
||||||
|
const maxWidthStr = maxWidth ? `x${maxWidth}` : '';
|
||||||
|
maxLinkStr = maxGen ? `${maxWidthStr} ${maxGen}` : `${maxWidthStr} ${maxSpeed || ''}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply degraded class if needed
|
||||||
|
const degradedClass = (widthDegraded || speedDegraded) ? ' class="pcie-degraded"' : '';
|
||||||
|
|
||||||
|
// Format output: show "current" or "current / max" if max differs
|
||||||
|
if (maxLinkStr && (widthDegraded || speedDegraded)) {
|
||||||
|
return `<span${degradedClass}>${curLinkStr}</span> <span class="pcie-max">/ ${maxLinkStr}</span>`;
|
||||||
|
} else if (maxLinkStr && maxLinkStr !== curLinkStr) {
|
||||||
|
return `${curLinkStr} <span class="pcie-max">/ ${maxLinkStr}</span>`;
|
||||||
|
} else {
|
||||||
|
return curLinkStr;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user