package inspur import ( "encoding/json" "fmt" "regexp" "strings" "git.mchus.pro/mchus/logpile/internal/models" "git.mchus.pro/mchus/logpile/internal/parser/vendors/pciids" ) // 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) _, pciDeviceName := pciids.DeviceInfo(pcie.VendorID, pcie.DeviceID) // Build BDF string in canonical form (bb:dd.f) bdf := formatBDF(pcie.BusNum, pcie.DevNum, pcie.FuncNum) partNumber := strings.TrimSpace(pcie.PartNum) if partNumber == "" { partNumber = sanitizePCIeDeviceName(pcie.DeviceName) } if partNumber == "" { partNumber = normalizeModelLabel(pciDeviceName) } if isGenericPCIeClass(deviceClass) { if resolved := normalizeModelLabel(pciDeviceName); resolved != "" { deviceClass = resolved } } manufacturer := strings.TrimSpace(pcie.VendorName) if manufacturer == "" { manufacturer = normalizeModelLabel(pciids.VendorName(pcie.VendorID)) } device := models.PCIeDevice{ Slot: pcie.Location, VendorID: pcie.VendorID, DeviceID: pcie.DeviceID, BDF: bdf, DeviceClass: deviceClass, Manufacturer: manufacturer, LinkWidth: pcie.CurrentLinkWidth, LinkSpeed: currentSpeed, MaxLinkWidth: pcie.MaxLinkWidth, MaxLinkSpeed: maxSpeed, PartNumber: partNumber, SerialNumber: strings.TrimSpace(pcie.SerialNum), } devices = append(devices, device) } return devices } var rawHexDeviceNameRegex = regexp.MustCompile(`(?i)^0x[0-9a-f]+$`) func sanitizePCIeDeviceName(name string) string { name = strings.TrimSpace(name) if name == "" { return "" } if strings.EqualFold(name, "N/A") { return "" } if rawHexDeviceNameRegex.MatchString(name) { return "" } return name } // MergePCIeDevices enriches base devices (from asset.json) with detailed RESTful PCIe data. // Matching is done by BDF first, then by slot fallback. func MergePCIeDevices(base []models.PCIeDevice, rest []models.PCIeDevice) []models.PCIeDevice { if len(rest) == 0 { return base } if len(base) == 0 { return append([]models.PCIeDevice(nil), rest...) } type ref struct { index int } byBDF := make(map[string]ref, len(base)) bySlot := make(map[string]ref, len(base)) for i := range base { bdf := normalizePCIeBDF(base[i].BDF) if bdf != "" { byBDF[bdf] = ref{index: i} } slot := strings.ToLower(strings.TrimSpace(base[i].Slot)) if slot != "" { bySlot[slot] = ref{index: i} } } for _, detailed := range rest { idx := -1 if bdf := normalizePCIeBDF(detailed.BDF); bdf != "" { if found, ok := byBDF[bdf]; ok { idx = found.index } } if idx == -1 { slot := strings.ToLower(strings.TrimSpace(detailed.Slot)) if slot != "" { if found, ok := bySlot[slot]; ok { idx = found.index } } } if idx == -1 { base = append(base, detailed) newIdx := len(base) - 1 if bdf := normalizePCIeBDF(detailed.BDF); bdf != "" { byBDF[bdf] = ref{index: newIdx} } if slot := strings.ToLower(strings.TrimSpace(detailed.Slot)); slot != "" { bySlot[slot] = ref{index: newIdx} } continue } enrichPCIeDevice(&base[idx], detailed) } return base } func enrichPCIeDevice(dst *models.PCIeDevice, src models.PCIeDevice) { if dst == nil { return } if strings.TrimSpace(dst.Slot) == "" { dst.Slot = src.Slot } if strings.TrimSpace(dst.BDF) == "" { dst.BDF = src.BDF } if dst.VendorID == 0 { dst.VendorID = src.VendorID } if dst.DeviceID == 0 { dst.DeviceID = src.DeviceID } if strings.TrimSpace(dst.Manufacturer) == "" { dst.Manufacturer = src.Manufacturer } if strings.TrimSpace(dst.SerialNumber) == "" { dst.SerialNumber = src.SerialNumber } if strings.TrimSpace(dst.PartNumber) == "" { dst.PartNumber = src.PartNumber } if strings.TrimSpace(dst.LinkSpeed) == "" || strings.EqualFold(strings.TrimSpace(dst.LinkSpeed), "unknown") { dst.LinkSpeed = src.LinkSpeed } if strings.TrimSpace(dst.MaxLinkSpeed) == "" || strings.EqualFold(strings.TrimSpace(dst.MaxLinkSpeed), "unknown") { dst.MaxLinkSpeed = src.MaxLinkSpeed } if dst.LinkWidth == 0 { dst.LinkWidth = src.LinkWidth } if dst.MaxLinkWidth == 0 { dst.MaxLinkWidth = src.MaxLinkWidth } if isGenericPCIeClass(dst.DeviceClass) && !isGenericPCIeClass(src.DeviceClass) { dst.DeviceClass = src.DeviceClass } } func normalizePCIeBDF(bdf string) string { bdf = strings.TrimSpace(strings.ToLower(bdf)) if bdf == "" { return "" } if strings.Contains(bdf, "/") { parts := strings.Split(bdf, "/") if len(parts) == 4 { return fmt.Sprintf("%s:%s.%s", parts[1], parts[2], parts[3]) } } return bdf } func isGenericPCIeClass(class string) bool { switch strings.ToLower(strings.TrimSpace(class)) { case "", "unknown", "other", "bridge", "network", "storage", "sas", "sata", "display", "vga", "3d controller", "serial bus": return true default: return false } } // 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 }