Extract firmware from: - asset.json: BIOS, ME, BKC, CPU Microcode, HDD/SSD/NVMe - component.log: PSU firmware, Network adapter firmware Deduplicate entries to avoid showing same firmware twice. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
411 lines
11 KiB
Go
411 lines
11 KiB
Go
package inspur
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"strings"
|
|
|
|
"git.mchus.pro/mchus/logpile/internal/models"
|
|
"git.mchus.pro/mchus/logpile/internal/parser/vendors/pciids"
|
|
)
|
|
|
|
// 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 {
|
|
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
|
|
func ParseAssetJSON(content []byte) (*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
|
|
seenMicrocode := make(map[string]bool)
|
|
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,
|
|
})
|
|
|
|
// Add CPU microcode to firmware list (deduplicated)
|
|
if cpu.MicroCodeVer != "" && !seenMicrocode[cpu.MicroCodeVer] {
|
|
config.Firmware = append(config.Firmware, models.FirmwareInfo{
|
|
DeviceName: fmt.Sprintf("CPU%d Microcode", i),
|
|
Version: cpu.MicroCodeVer,
|
|
})
|
|
seenMicrocode[cpu.MicroCodeVer] = true
|
|
}
|
|
}
|
|
|
|
// 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
|
|
seenHDDFW := make(map[string]bool)
|
|
for _, hdd := range asset.HddInfo {
|
|
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
|
|
modelName := strings.TrimSpace(hdd.ModelName)
|
|
manufacturer := resolveManufacturer(hdd.Manufacturer, modelName)
|
|
|
|
config.Storage = append(config.Storage, models.Storage{
|
|
Slot: hdd.LocationString,
|
|
Type: storageType,
|
|
Model: modelName,
|
|
SizeGB: hdd.Capacity,
|
|
SerialNumber: hdd.SerialNumber,
|
|
Manufacturer: manufacturer,
|
|
Firmware: hdd.FirmwareVersion,
|
|
Interface: diskInterfaceToString(hdd.DiskInterfaceType),
|
|
})
|
|
|
|
// Add HDD firmware to firmware list (deduplicated by model+version)
|
|
if hdd.FirmwareVersion != "" {
|
|
fwKey := modelName + ":" + hdd.FirmwareVersion
|
|
if !seenHDDFW[fwKey] {
|
|
slot := hdd.LocationString
|
|
if slot == "" {
|
|
slot = fmt.Sprintf("%s %dGB", storageType, hdd.Capacity)
|
|
}
|
|
config.Firmware = append(config.Firmware, models.FirmwareInfo{
|
|
DeviceName: fmt.Sprintf("%s (%s)", modelName, slot),
|
|
Version: hdd.FirmwareVersion,
|
|
})
|
|
seenHDDFW[fwKey] = true
|
|
}
|
|
}
|
|
}
|
|
|
|
// 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 = 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
|
|
}
|
|
|
|
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 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 ""
|
|
}
|