Files
Mikhail Chusavitin 9505303d1d fix(inspur): show microcode version for every CPU, not just the first
Dedup by version caused CPU1 Microcode to be omitted when both CPUs run
the same version, leaving the firmware column blank for the second socket.
Each CPU gets its own firmware entry keyed by index.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-21 14:15:28 +03:00

491 lines
14 KiB
Go

package inspur
import (
"encoding/json"
"fmt"
"regexp"
"strings"
"git.mchus.pro/mchus/logpile/internal/models"
"git.mchus.pro/mchus/logpile/internal/parser/vendors/pciids"
)
var rawHexPCIDeviceRegex = regexp.MustCompile(`(?i)^0x[0-9a-f]+$`)
// AssetJSON represents the structure of Inspur asset.json file
type AssetJSON struct {
VersionInfo []struct {
DeviceID int `json:"DeviceId"`
DeviceName string `json:"DeviceName"`
DeviceRevision string `json:"DeviceRevision"`
BuildTime string `json:"BuildTime"`
} `json:"VersionInfo"`
CpuInfo []struct {
ProcessorName string `json:"ProcessorName"`
ProcessorID string `json:"ProcessorId"`
MicroCodeVer string `json:"MicroCodeVer"`
CurrentSpeed int `json:"CurrentSpeed"`
Core int `json:"Core"`
ThreadCount int `json:"ThreadCount"`
L1Cache int `json:"L1Cache"`
L2Cache int `json:"L2Cache"`
L3Cache int `json:"L3Cache"`
CpuTdp int `json:"CpuTdp"`
PPIN string `json:"PPIN"`
TurboEnableMaxSpeed int `json:"TurboEnableMaxSpeed"`
TurboCloseMaxSpeed int `json:"TurboCloseMaxSpeed"`
UPIBandwidth string `json:"UPIBandwidth"`
} `json:"CpuInfo"`
MemInfo struct {
MemCommonInfo []struct {
Manufacturer string `json:"Manufacturer"`
MaxSpeed int `json:"MaxSpeed"`
CurrentSpeed int `json:"CurrentSpeed"`
MemoryType int `json:"MemoryType"`
Rank int `json:"Rank"`
DataWidth int `json:"DataWidth"`
ConfiguredVoltage int `json:"ConfiguredVoltage"`
PhysicalSize int `json:"PhysicalSize"`
} `json:"MemCommonInfo"`
DimmInfo []struct {
SerialNumber string `json:"SerialNumber"`
PartNumber string `json:"PartNumber"`
AssetTag string `json:"AssetTag"`
} `json:"DimmInfo"`
} `json:"MemInfo"`
HddInfo []struct {
PresentBitmap []int `json:"PresentBitmap"`
SerialNumber string `json:"SerialNumber"`
Manufacturer string `json:"Manufacturer"`
ModelName string `json:"ModelName"`
FirmwareVersion string `json:"FirmwareVersion"`
Capacity int `json:"Capacity"`
Location int `json:"Location"`
DiskInterfaceType int `json:"DiskInterfaceType"`
MediaType int `json:"MediaType"`
LocationString string `json:"LocationString"`
BlockSizeBytes int `json:"BlockSizeBytes"`
CapableSpeedGbs string `json:"CapableSpeedGbs"`
NegotiatedSpeedGbs string `json:"NegotiatedSpeedGbs"`
PcieSlot int `json:"PcieSlot"`
} `json:"HddInfo"`
PcieInfo []struct {
VendorId int `json:"VendorId"`
DeviceId int `json:"DeviceId"`
BusNumber int `json:"BusNumber"`
DeviceNumber int `json:"DeviceNumber"`
FunctionNumber int `json:"FunctionNumber"`
MaxLinkWidth int `json:"MaxLinkWidth"`
MaxLinkSpeed int `json:"MaxLinkSpeed"`
NegotiatedLinkWidth int `json:"NegotiatedLinkWidth"`
CurrentLinkSpeed int `json:"CurrentLinkSpeed"`
ClassCode int `json:"ClassCode"`
SubClassCode int `json:"SubClassCode"`
PcieSlot int `json:"PcieSlot"`
LocString string `json:"LocString"`
PartNumber *string `json:"PartNumber"`
SerialNumber *string `json:"SerialNumber"`
Mac []string `json:"Mac"`
} `json:"PcieInfo"`
}
// ParseAssetJSON parses Inspur asset.json content.
// - pcieSlotDeviceNames: optional map from integer PCIe slot ID to device name string,
// sourced from devicefrusdr.log PCIe REST section. Fills missing NVMe model names.
// - pcieSlotSerials: optional map from integer PCIe slot ID to serial number string,
// sourced from audit.log SN-changed events. Fills missing NVMe serial numbers.
func ParseAssetJSON(content []byte, pcieSlotDeviceNames map[int]string, pcieSlotSerials map[int]string) (*models.HardwareConfig, error) {
var asset AssetJSON
if err := json.Unmarshal(content, &asset); err != nil {
return nil, err
}
config := &models.HardwareConfig{}
// Parse version info
for _, v := range asset.VersionInfo {
config.Firmware = append(config.Firmware, models.FirmwareInfo{
DeviceName: v.DeviceName,
Version: v.DeviceRevision,
BuildTime: v.BuildTime,
})
}
// Parse CPU info
for i, cpu := range asset.CpuInfo {
config.CPUs = append(config.CPUs, models.CPU{
Socket: i,
Model: strings.TrimSpace(cpu.ProcessorName),
Cores: cpu.Core,
Threads: cpu.ThreadCount,
FrequencyMHz: cpu.CurrentSpeed,
MaxFreqMHz: cpu.TurboEnableMaxSpeed,
L1CacheKB: cpu.L1Cache,
L2CacheKB: cpu.L2Cache,
L3CacheKB: cpu.L3Cache,
TDP: cpu.CpuTdp,
PPIN: cpu.PPIN,
})
if cpu.MicroCodeVer != "" {
config.Firmware = append(config.Firmware, models.FirmwareInfo{
DeviceName: fmt.Sprintf("CPU%d Microcode", i),
Version: cpu.MicroCodeVer,
})
}
}
// 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: 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,
})
}
}
// Parse storage info
for _, hdd := range asset.HddInfo {
slot := normalizeAssetHDDSlot(hdd.LocationString, hdd.Location, hdd.DiskInterfaceType)
modelName := strings.TrimSpace(hdd.ModelName)
serial := normalizeRedisValue(hdd.SerialNumber)
present := bitmapHasAnyValue(hdd.PresentBitmap)
if !present && (slot != "" || modelName != "" || serial != "" || hdd.Capacity > 0) {
present = true
}
if !present && slot == "" && modelName == "" && serial == "" && hdd.Capacity == 0 {
continue
}
// Enrich model name from PCIe device name (supplied from devicefrusdr.log).
// BMC does not populate HddInfo.ModelName for NVMe drives, but the PCIe REST
// section in devicefrusdr.log carries the drive model as device_name.
if modelName == "" && hdd.PcieSlot > 0 && len(pcieSlotDeviceNames) > 0 {
if devName, ok := pcieSlotDeviceNames[hdd.PcieSlot]; ok && devName != "" {
modelName = devName
}
}
// Enrich serial number from audit.log SN-changed events (supplied via pcieSlotSerials).
// BMC asset.json does not carry NVMe serial numbers; audit.log logs every SN change.
if serial == "" && hdd.PcieSlot > 0 && len(pcieSlotSerials) > 0 {
if sn, ok := pcieSlotSerials[hdd.PcieSlot]; ok && sn != "" {
serial = sn
}
}
storageType := "HDD"
if hdd.DiskInterfaceType == 5 {
storageType = "NVMe"
} else if hdd.MediaType == 1 {
storageType = "SSD"
}
// Resolve manufacturer: try vendor ID first, then model name extraction
manufacturer := resolveManufacturer(hdd.Manufacturer, modelName)
config.Storage = append(config.Storage, models.Storage{
Slot: slot,
Type: storageType,
Model: modelName,
SizeGB: hdd.Capacity,
SerialNumber: serial,
Manufacturer: manufacturer,
Firmware: hdd.FirmwareVersion,
Interface: diskInterfaceToString(hdd.DiskInterfaceType),
Present: present,
})
// Disk firmware is already stored in Storage.Firmware — do not duplicate in Hardware.Firmware.
}
// Parse PCIe info
for _, pcie := range asset.PcieInfo {
vendor, deviceName := pciids.DeviceInfo(pcie.VendorId, pcie.DeviceId)
device := models.PCIeDevice{
Slot: pcie.LocString,
VendorID: pcie.VendorId,
DeviceID: pcie.DeviceId,
BDF: formatBDF(pcie.BusNumber, pcie.DeviceNumber, pcie.FunctionNumber),
LinkWidth: pcie.NegotiatedLinkWidth,
LinkSpeed: pcieLinkSpeedToString(pcie.CurrentLinkSpeed),
MaxLinkWidth: pcie.MaxLinkWidth,
MaxLinkSpeed: pcieLinkSpeedToString(pcie.MaxLinkSpeed),
DeviceClass: pcieClassToString(pcie.ClassCode, pcie.SubClassCode),
Manufacturer: vendor,
}
if pcie.PartNumber != nil {
device.PartNumber = strings.TrimSpace(*pcie.PartNumber)
}
if pcie.SerialNumber != nil {
device.SerialNumber = strings.TrimSpace(*pcie.SerialNumber)
}
if len(pcie.Mac) > 0 {
device.MACAddresses = pcie.Mac
}
// Use device name from PCI IDs database if available
if deviceName != "" {
device.DeviceClass = normalizeModelLabel(deviceName)
}
config.PCIeDevices = append(config.PCIeDevices, device)
// Extract GPUs (class 3 = display controller)
if pcie.ClassCode == 3 {
gpuModel := normalizeGPUModel(pcie.VendorId, pcie.DeviceId, deviceName, 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),
CurrentLinkWidth: pcie.NegotiatedLinkWidth,
CurrentLinkSpeed: pcieLinkSpeedToString(pcie.CurrentLinkSpeed),
MaxLinkWidth: pcie.MaxLinkWidth,
MaxLinkSpeed: pcieLinkSpeedToString(pcie.MaxLinkSpeed),
}
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
}
func normalizeModelLabel(v string) string {
v = strings.TrimSpace(v)
if v == "" {
return ""
}
return strings.Join(strings.Fields(v), " ")
}
func normalizeGPUModel(vendorID, deviceID int, model string, classCode, subClass int) string {
model = normalizeModelLabel(model)
if model == "" || rawHexPCIDeviceRegex.MatchString(model) || isGenericGPUModelLabel(model) {
if pciModel := normalizeModelLabel(pciids.DeviceName(vendorID, deviceID)); pciModel != "" {
model = pciModel
}
}
if model == "" || isGenericGPUModelLabel(model) {
model = pcieClassToString(classCode, subClass)
}
// Last fallback for unknown NVIDIA display devices: expose PCI DeviceID
// instead of generic "3D Controller".
if (model == "" || strings.EqualFold(model, "3D Controller")) && vendorID == 0x10de && deviceID > 0 {
return fmt.Sprintf("0x%04X", deviceID)
}
return model
}
func isGenericGPUModelLabel(model string) bool {
switch strings.ToLower(strings.TrimSpace(model)) {
case "", "gpu", "display", "display controller", "vga", "3d controller", "other", "unknown":
return true
default:
return false
}
}
func memoryTypeToString(memType int) string {
switch memType {
case 26:
return "DDR4"
case 34:
return "DDR5"
default:
return "Unknown"
}
}
func diskInterfaceToString(ifType int) string {
switch ifType {
case 4:
return "SATA"
case 5:
return "NVMe"
case 6:
return "SAS"
default:
return "Unknown"
}
}
func normalizeAssetHDDSlot(locationString string, location int, diskInterfaceType int) string {
slot := strings.TrimSpace(locationString)
if slot != "" {
return slot
}
if location < 0 {
return ""
}
if diskInterfaceType == 5 {
return fmt.Sprintf("OB%02d", location+1)
}
return fmt.Sprintf("%d", location)
}
func bitmapHasAnyValue(values []int) bool {
for _, v := range values {
if v != 0 {
return true
}
}
return false
}
func pcieLinkSpeedToString(speed int) string {
switch speed {
case 1:
return "2.5 GT/s"
case 2:
return "5.0 GT/s"
case 3:
return "8.0 GT/s"
case 4:
return "16.0 GT/s"
case 5:
return "32.0 GT/s"
default:
return "Unknown"
}
}
func pcieClassToString(classCode, subClass int) string {
switch classCode {
case 1:
switch subClass {
case 0:
return "SCSI"
case 1:
return "IDE"
case 4:
return "RAID"
case 6:
return "SATA"
case 7:
return "SAS"
case 8:
return "NVMe"
default:
return "Storage"
}
case 2:
return "Network"
case 3:
switch subClass {
case 0:
return "VGA"
case 2:
return "3D Controller"
default:
return "Display"
}
case 4:
return "Multimedia"
case 6:
return "Bridge"
case 12:
return "Serial Bus"
default:
return "Other"
}
}
func formatBDF(bus, dev, fun int) string {
return fmt.Sprintf("%02x:%02x.%x", bus, dev, fun)
}
// resolveManufacturer resolves manufacturer name from various sources
func resolveManufacturer(rawManufacturer, modelName string) string {
raw := strings.TrimSpace(rawManufacturer)
// If it looks like a vendor ID (hex), try to resolve it
if raw != "" {
if name := pciids.VendorNameFromString(raw); name != "" {
return name
}
// If not a vendor ID but looks like a real name (has letters), use it
hasLetter := false
for _, c := range raw {
if (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') {
hasLetter = true
break
}
}
if hasLetter && len(raw) > 2 {
return raw
}
}
// Try to extract from model name
return extractStorageManufacturer(modelName)
}
// extractStorageManufacturer tries to extract manufacturer from model name
func extractStorageManufacturer(model string) string {
modelUpper := strings.ToUpper(model)
knownVendors := []struct {
prefix string
name string
}{
{"SAMSUNG", "Samsung"},
{"KIOXIA", "KIOXIA"},
{"TOSHIBA", "Toshiba"},
{"WDC", "Western Digital"},
{"WD", "Western Digital"},
{"SEAGATE", "Seagate"},
{"HGST", "HGST"},
{"INTEL", "Intel"},
{"MICRON", "Micron"},
{"KINGSTON", "Kingston"},
{"CRUCIAL", "Crucial"},
{"SK HYNIX", "SK Hynix"},
{"SKHYNIX", "SK Hynix"},
{"SANDISK", "SanDisk"},
{"LITEON", "Lite-On"},
{"PLEXTOR", "Plextor"},
{"ADATA", "ADATA"},
{"TRANSCEND", "Transcend"},
{"CORSAIR", "Corsair"},
{"SOLIDIGM", "Solidigm"},
}
for _, v := range knownVendors {
if strings.HasPrefix(modelUpper, v.prefix) {
return v.name
}
}
return ""
}