feat: improve inspur parsing and pci.ids integration

This commit is contained in:
2026-02-17 18:09:36 +03:00
parent b33cca5fcc
commit 758fa66282
26 changed files with 43567 additions and 247 deletions

View File

@@ -3,12 +3,15 @@ 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 {
@@ -207,8 +210,8 @@ func ParseAssetJSON(content []byte) (*models.HardwareConfig, error) {
VendorID: pcie.VendorId,
DeviceID: pcie.DeviceId,
BDF: formatBDF(pcie.BusNumber, pcie.DeviceNumber, pcie.FunctionNumber),
LinkWidth: pcie.NegotiatedLinkWidth,
LinkSpeed: pcieLinkSpeedToString(pcie.CurrentLinkSpeed),
LinkWidth: pcie.NegotiatedLinkWidth,
LinkSpeed: pcieLinkSpeedToString(pcie.CurrentLinkSpeed),
MaxLinkWidth: pcie.MaxLinkWidth,
MaxLinkSpeed: pcieLinkSpeedToString(pcie.MaxLinkSpeed),
DeviceClass: pcieClassToString(pcie.ClassCode, pcie.SubClassCode),
@@ -225,25 +228,22 @@ func ParseAssetJSON(content []byte) (*models.HardwareConfig, error) {
}
// Use device name from PCI IDs database if available
if deviceName != "" {
device.DeviceClass = deviceName
device.DeviceClass = normalizeModelLabel(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)
}
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),
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),
}
@@ -260,6 +260,45 @@ func ParseAssetJSON(content []byte) (*models.HardwareConfig, error) {
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:

View File

@@ -0,0 +1,48 @@
package inspur
import "testing"
func TestParseAssetJSON_NVIDIAGPUModelFromPCIIDs(t *testing.T) {
raw := []byte(`{
"VersionInfo": [],
"CpuInfo": [],
"MemInfo": {"MemCommonInfo": [], "DimmInfo": []},
"HddInfo": [],
"PcieInfo": [{
"VendorId": 4318,
"DeviceId": 9019,
"BusNumber": 12,
"DeviceNumber": 0,
"FunctionNumber": 0,
"MaxLinkWidth": 16,
"MaxLinkSpeed": 5,
"NegotiatedLinkWidth": 16,
"CurrentLinkSpeed": 5,
"ClassCode": 3,
"SubClassCode": 2,
"PcieSlot": 11,
"LocString": "#CPU0_PCIE2",
"PartNumber": null,
"SerialNumber": null,
"Mac": []
}]
}`)
hw, err := ParseAssetJSON(raw)
if err != nil {
t.Fatalf("ParseAssetJSON failed: %v", err)
}
if len(hw.GPUs) != 1 {
t.Fatalf("expected 1 GPU, got %d", len(hw.GPUs))
}
if hw.GPUs[0].Model != "GH100 [H200 NVL]" {
t.Fatalf("expected model GH100 [H200 NVL], got %q", hw.GPUs[0].Model)
}
}
func TestNormalizeGPUModel_FallbackToDeviceIDForUnknownNVIDIA(t *testing.T) {
got := normalizeGPUModel(0x10de, 0xbeef, "0xBEEF\t", 3, 2)
if got != "0xBEEF" {
t.Fatalf("expected 0xBEEF, got %q", got)
}
}

View File

@@ -8,6 +8,7 @@ import (
"time"
"git.mchus.pro/mchus/logpile/internal/models"
"git.mchus.pro/mchus/logpile/internal/parser/vendors/pciids"
)
// ParseComponentLog parses component.log file and extracts detailed hardware info
@@ -52,20 +53,20 @@ func ParseComponentLogEvents(content []byte) []models.Event {
// 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"`
MemModStatus int `json:"mem_mod_status"`
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"`
MemModID int `json:"mem_mod_id"`
ConfigStatus int `json:"config_status"`
MemModSlot string `json:"mem_mod_slot"`
MemModStatus int `json:"mem_mod_status"`
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"`
@@ -112,21 +113,21 @@ func parseMemoryInfo(text string, hw *models.HardwareConfig) {
// 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"`
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"`
}
@@ -304,17 +305,28 @@ func parseNetworkAdapterInfo(text string, hw *models.HardwareConfig) {
}
}
model := normalizeModelLabel(adapter.Model)
if model == "" || looksLikeRawDeviceID(model) {
if resolved := normalizeModelLabel(pciids.DeviceName(adapter.VendorID, adapter.DeviceID)); resolved != "" {
model = resolved
}
}
vendor := normalizeModelLabel(adapter.Vendor)
if vendor == "" {
vendor = normalizeModelLabel(pciids.VendorName(adapter.VendorID))
}
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),
Model: model,
Vendor: vendor,
VendorID: adapter.VendorID,
DeviceID: adapter.DeviceID,
SerialNumber: strings.TrimSpace(adapter.SN),
PartNumber: strings.TrimSpace(adapter.PN),
Firmware: adapter.FwVer,
SerialNumber: normalizeRedisValue(adapter.SN),
PartNumber: normalizeRedisValue(adapter.PN),
Firmware: normalizeRedisValue(adapter.FwVer),
PortCount: adapter.PortNum,
PortType: adapter.PortType,
MACAddresses: macs,
@@ -323,6 +335,16 @@ func parseNetworkAdapterInfo(text string, hw *models.HardwareConfig) {
}
}
var rawDeviceIDLikeRegex = regexp.MustCompile(`(?i)^(?:0x)?[0-9a-f]{3,4}$`)
func looksLikeRawDeviceID(v string) bool {
v = strings.TrimSpace(v)
if v == "" {
return true
}
return rawDeviceIDLikeRegex.MatchString(v)
}
func parseMemoryEvents(text string) []models.Event {
var events []models.Event

View File

@@ -0,0 +1,52 @@
package inspur
import (
"strings"
"testing"
"git.mchus.pro/mchus/logpile/internal/models"
)
func TestParseNetworkAdapterInfo_ResolvesModelFromPCIIDsForRawHexModel(t *testing.T) {
text := `RESTful Network Adapter info:
{
"sys_adapters": [
{
"id": 1,
"name": "NIC1",
"Location": "#CPU0_PCIE4",
"present": 1,
"slot": 4,
"vendor_id": 32902,
"device_id": 5409,
"vendor": "",
"model": "0x1521",
"fw_ver": "",
"status": "OK",
"sn": "",
"pn": "",
"port_num": 4,
"port_type": "Base-T",
"ports": []
}
]
}
RESTful fan`
hw := &models.HardwareConfig{}
parseNetworkAdapterInfo(text, hw)
if len(hw.NetworkAdapters) != 1 {
t.Fatalf("expected 1 network adapter, got %d", len(hw.NetworkAdapters))
}
got := hw.NetworkAdapters[0]
if got.Model == "" {
t.Fatalf("expected NIC model resolved from pci.ids, got empty")
}
if !strings.Contains(strings.ToUpper(got.Model), "I350") {
t.Fatalf("expected I350 in model, got %q", got.Model)
}
if got.Vendor == "" {
t.Fatalf("expected NIC vendor resolved from pci.ids")
}
}

View File

@@ -15,7 +15,7 @@ import (
// parserVersion - version of this parser module
// IMPORTANT: Increment this version when making changes to parser logic!
const parserVersion = "1.1.0"
const parserVersion = "1.2.1"
func init() {
parser.Register(&Parser{})
@@ -125,6 +125,12 @@ func (p *Parser) Parse(files []parser.ExtractedFile) (*models.AnalysisResult, er
result.Events = append(result.Events, componentEvents...)
}
// Enrich runtime component data from Redis snapshot (serials, FW, telemetry),
// when text logs miss these fields.
if f := parser.FindFileByName(files, "redis-dump.rdb"); f != nil && result.Hardware != nil {
enrichFromRedisDump(f.Content, result.Hardware)
}
// Parse IDL-like logs (plain and structured JSON logs with embedded IDL messages)
idlFiles := parser.FindFileByPattern(files, "/idl.log", "idl_json.log", "run_json.log")
for _, f := range idlFiles {
@@ -199,14 +205,9 @@ func (p *Parser) parseDeviceFruSDR(content []byte, result *models.AnalysisResult
// 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
// Merge PCIe data: asset.json is the base inventory, RESTful data enriches names/links/serials.
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
result.Hardware.PCIeDevices = MergePCIeDevices(result.Hardware.PCIeDevices, pcieDevicesFromREST)
}
// Parse GPU devices and add temperature data from sensors

View File

@@ -3,36 +3,38 @@ 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"`
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
@@ -73,9 +75,27 @@ func ParsePCIeDevices(content []byte) []models.PCIeDevice {
// 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
bdf := fmt.Sprintf("%04x/%02x/%02x/%02x", 0, pcie.BusNum, pcie.DevNum, pcie.FuncNum)
// 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,
@@ -83,12 +103,12 @@ func ParsePCIeDevices(content []byte) []models.PCIeDevice {
DeviceID: pcie.DeviceID,
BDF: bdf,
DeviceClass: deviceClass,
Manufacturer: pcie.VendorName,
Manufacturer: manufacturer,
LinkWidth: pcie.CurrentLinkWidth,
LinkSpeed: currentSpeed,
MaxLinkWidth: pcie.MaxLinkWidth,
MaxLinkSpeed: maxSpeed,
PartNumber: strings.TrimSpace(pcie.PartNum),
PartNumber: partNumber,
SerialNumber: strings.TrimSpace(pcie.SerialNum),
}
@@ -98,6 +118,149 @@ func ParsePCIeDevices(content []byte) []models.PCIeDevice {
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:

View File

@@ -0,0 +1,77 @@
package inspur
import (
"strings"
"testing"
"git.mchus.pro/mchus/logpile/internal/models"
)
func TestParsePCIeDevices_UsesDeviceNameAsModelWhenPartNumberMissing(t *testing.T) {
content := []byte(`RESTful PCIE Device info:
[{"id":1,"present":1,"vendor_id":32902,"vendor_name":"Intel","device_id":5409,"device_name":"I350T4V2","bus_num":69,"dev_num":0,"func_num":0,"max_link_width":4,"max_link_speed":2,"current_link_width":4,"current_link_speed":2,"location":"#CPU0_PCIE4","dev_type":2,"dev_subtype":0,"part_num":"","serial_num":"","fw_ver":""}]
BMC sdr Info:`)
devices := ParsePCIeDevices(content)
if len(devices) != 1 {
t.Fatalf("expected 1 device, got %d", len(devices))
}
if devices[0].PartNumber != "I350T4V2" {
t.Fatalf("expected part/model I350T4V2, got %q", devices[0].PartNumber)
}
if devices[0].BDF != "45:00.0" {
t.Fatalf("expected BDF 45:00.0, got %q", devices[0].BDF)
}
}
func TestMergePCIeDevices_EnrichesGenericAssetEntry(t *testing.T) {
base := []models.PCIeDevice{
{
Slot: "#CPU1_PCIE9",
BDF: "98:00.0",
VendorID: 0x9005,
DeviceID: 0x028f,
DeviceClass: "SAS",
Manufacturer: "Adaptec / Microsemi",
},
}
rest := []models.PCIeDevice{
{
Slot: "#CPU1_PCIE9",
BDF: "98:00.0",
VendorID: 0x9005,
DeviceID: 0x028f,
DeviceClass: "Storage Controller",
Manufacturer: "Microchip",
PartNumber: "PM8222-SHBA",
},
}
got := MergePCIeDevices(base, rest)
if len(got) != 1 {
t.Fatalf("expected 1 merged device, got %d", len(got))
}
if got[0].PartNumber != "PM8222-SHBA" {
t.Fatalf("expected merged part number PM8222-SHBA, got %q", got[0].PartNumber)
}
}
func TestParsePCIeDevices_ResolvesModelFromPCIIDsWhenDeviceNameIsRawHex(t *testing.T) {
content := []byte(`RESTful PCIE Device info:
[{"id":5,"present":1,"vendor_id":36869,"vendor_name":"","device_id":655,"device_name":"0x028F","bus_num":152,"dev_num":0,"func_num":0,"max_link_width":8,"max_link_speed":3,"current_link_width":8,"current_link_speed":3,"location":"#CPU1_PCIE9","dev_type":1,"dev_subtype":7,"part_num":"","serial_num":"","fw_ver":""}]
BMC sdr Info:`)
devices := ParsePCIeDevices(content)
if len(devices) != 1 {
t.Fatalf("expected 1 device, got %d", len(devices))
}
if devices[0].PartNumber == "" {
t.Fatalf("expected part number resolved from pci.ids, got empty")
}
if strings.HasPrefix(strings.ToLower(strings.TrimSpace(devices[0].PartNumber)), "0x") {
t.Fatalf("expected resolved name instead of raw hex, got %q", devices[0].PartNumber)
}
if devices[0].Manufacturer == "" {
t.Fatalf("expected manufacturer resolved from pci.ids")
}
}

View File

@@ -0,0 +1,559 @@
package inspur
import (
"encoding/hex"
"regexp"
"sort"
"strconv"
"strings"
"unicode"
"git.mchus.pro/mchus/logpile/internal/models"
)
var (
reRedisGPUKey = regexp.MustCompile(`GPUInfo:REDIS_GPUINFO_T([0-9]+):([A-Za-z0-9_]+)`)
reRedisNICKey = regexp.MustCompile(`RedisNicInfo:redis_nic_info_t:stNicDeviceInfo([0-9]+):([A-Za-z0-9_]+)`)
reRedisRAIDSerial = regexp.MustCompile(`RAIDMSCCInfo:redis_pcie_mscc_raid_info_t([0-9]+):RAIDInfo:SerialNum`)
reRedisPCIESNPN = regexp.MustCompile(`AssetInfoPCIE:SNPN([0-9]+):(SN|PN)`)
)
type redisGPUSnapshot struct {
ByIndex map[int]map[string]string
}
type redisNICSnapshot struct {
ByIndex map[int]map[string]string
}
type redisPCIESerialSnapshot struct {
ByPart map[string]string
}
func enrichFromRedisDump(content []byte, hw *models.HardwareConfig) {
if hw == nil || len(content) == 0 {
return
}
gpuSnap := parseRedisGPUSnapshot(content)
nicSnap := parseRedisNICSnapshot(content)
raidSerials := parseRedisRAIDSerials(content)
pcieSnap := parseRedisPCIESerialSnapshot(content)
applyRedisGPUEnrichment(hw, gpuSnap)
applyRedisNICEnrichment(hw, nicSnap)
applyRedisPCIESNPNEnrichment(hw, pcieSnap)
applyRedisPCIeEnrichment(hw, raidSerials)
}
func parseRedisRAIDSerials(content []byte) []string {
matches := reRedisRAIDSerial.FindAllSubmatchIndex(content, -1)
if len(matches) == 0 {
return nil
}
seen := make(map[string]bool, len(matches))
serials := make([]string, 0, len(matches))
for _, m := range matches {
if len(m) < 4 {
continue
}
value := normalizeRedisValue(extractRedisCandidateValue(content, m[1]))
if value == "" || seen[value] {
continue
}
seen[value] = true
serials = append(serials, value)
}
return serials
}
func parseRedisPCIESerialSnapshot(content []byte) redisPCIESerialSnapshot {
type rec struct {
PN string
SN string
}
tmp := make(map[int]rec)
matches := reRedisPCIESNPN.FindAllSubmatchIndex(content, -1)
for _, m := range matches {
if len(m) < 6 {
continue
}
idxStr := string(content[m[2]:m[3]])
field := string(content[m[4]:m[5]])
idx, err := strconv.Atoi(idxStr)
if err != nil {
continue
}
value := normalizeRedisValue(extractRedisCandidateValue(content, m[1]))
if value == "" {
continue
}
r := tmp[idx]
if field == "PN" {
r.PN = value
} else if field == "SN" {
r.SN = value
}
tmp[idx] = r
}
out := redisPCIESerialSnapshot{ByPart: make(map[string]string)}
for _, r := range tmp {
pn := normalizeRedisValue(r.PN)
sn := normalizeRedisValue(r.SN)
if pn == "" || sn == "" {
continue
}
out.ByPart[strings.ToLower(strings.TrimSpace(pn))] = sn
}
return out
}
func parseRedisGPUSnapshot(content []byte) redisGPUSnapshot {
snap := redisGPUSnapshot{ByIndex: make(map[int]map[string]string)}
matches := reRedisGPUKey.FindAllSubmatchIndex(content, -1)
for _, m := range matches {
if len(m) < 6 {
continue
}
idxStr := string(content[m[2]:m[3]])
field := string(content[m[4]:m[5]])
idx, err := strconv.Atoi(idxStr)
if err != nil {
continue
}
value := extractRedisInlineValue(content, m[1])
if value == "" {
continue
}
byField, ok := snap.ByIndex[idx]
if !ok {
byField = make(map[string]string)
snap.ByIndex[idx] = byField
}
byField[field] = value
}
return snap
}
func parseRedisNICSnapshot(content []byte) redisNICSnapshot {
snap := redisNICSnapshot{ByIndex: make(map[int]map[string]string)}
matches := reRedisNICKey.FindAllSubmatchIndex(content, -1)
for _, m := range matches {
if len(m) < 6 {
continue
}
idxStr := string(content[m[2]:m[3]])
field := string(content[m[4]:m[5]])
idx, err := strconv.Atoi(idxStr)
if err != nil {
continue
}
value := extractRedisInlineValue(content, m[1])
if value == "" {
continue
}
byField, ok := snap.ByIndex[idx]
if !ok {
byField = make(map[string]string)
snap.ByIndex[idx] = byField
}
byField[field] = value
}
return snap
}
func extractRedisInlineValue(content []byte, start int) string {
if start < 0 || start >= len(content) {
return ""
}
i := start
for i < len(content) && content[i] <= 0x20 {
i++
}
if i >= len(content) {
return ""
}
j := i
for j < len(content) {
c := content[j]
if c == 0 || c < 0x20 || c > 0x7e {
break
}
j++
}
if j <= i {
return ""
}
raw := strings.TrimSpace(string(content[i:j]))
if raw == "" {
return ""
}
decoded := maybeDecodeHexString(raw)
if decoded != "" {
return decoded
}
return raw
}
func extractRedisCandidateValue(content []byte, start int) string {
// Fast-path for simple inline string values.
if v := extractRedisInlineValue(content, start); normalizeRedisValue(v) != "" {
return v
}
if start < 0 || start >= len(content) {
return ""
}
end := start + 256
if end > len(content) {
end = len(content)
}
window := content[start:end]
for _, token := range splitAlphaNumTokens(window) {
if len(token) < 6 {
continue
}
lower := strings.ToLower(token)
if strings.Contains(lower, "redis") || strings.Contains(lower, "sensor") || strings.Contains(lower, "fullsdr") {
continue
}
if decoded := maybeDecodeHexString(token); normalizeRedisValue(decoded) != "" {
return decoded
}
if normalizeRedisValue(token) != "" {
return token
}
}
return ""
}
func splitAlphaNumTokens(b []byte) []string {
var out []string
start := -1
for i := 0; i < len(b); i++ {
c := rune(b[i])
if unicode.IsLetter(c) || unicode.IsDigit(c) {
if start == -1 {
start = i
}
continue
}
if start != -1 {
out = append(out, string(b[start:i]))
start = -1
}
}
if start != -1 {
out = append(out, string(b[start:]))
}
return out
}
func maybeDecodeHexString(s string) string {
if len(s) < 8 || len(s)%2 != 0 {
return ""
}
for _, c := range s {
if (c < '0' || c > '9') && (c < 'a' || c > 'f') && (c < 'A' || c > 'F') {
return ""
}
}
b, err := hex.DecodeString(s)
if err != nil {
return ""
}
decoded := strings.TrimSpace(strings.TrimRight(string(b), "\x00"))
if decoded == "" {
return ""
}
for _, c := range decoded {
if c < 0x20 || c > 0x7e {
return ""
}
}
return decoded
}
func applyRedisGPUEnrichment(hw *models.HardwareConfig, snap redisGPUSnapshot) {
if len(hw.GPUs) == 0 || len(snap.ByIndex) == 0 {
return
}
type redisGPU struct {
Index int
Data map[string]string
}
redisGPUs := make([]redisGPU, 0, len(snap.ByIndex))
for idx, data := range snap.ByIndex {
if data == nil {
continue
}
if data["NV_GPU_SerialNumber"] == "" && data["NV_GPU_FWVersion"] == "" && data["NV_GPU_UUID"] == "" {
continue
}
redisGPUs = append(redisGPUs, redisGPU{Index: idx, Data: data})
}
if len(redisGPUs) == 0 {
return
}
sort.Slice(redisGPUs, func(i, j int) bool { return redisGPUs[i].Index < redisGPUs[j].Index })
target := make([]*models.GPU, 0, len(hw.GPUs))
for i := range hw.GPUs {
gpu := &hw.GPUs[i]
if isNVIDIAGPU(gpu) {
target = append(target, gpu)
}
}
if len(target) == 0 || len(target) != len(redisGPUs) {
return
}
sort.Slice(target, func(i, j int) bool {
left := strings.TrimSpace(target[i].BDF)
right := strings.TrimSpace(target[j].BDF)
if left != "" && right != "" {
return left < right
}
return strings.TrimSpace(target[i].Slot) < strings.TrimSpace(target[j].Slot)
})
for i := range target {
applyRedisGPUFields(target[i], redisGPUs[i].Data)
}
}
func isNVIDIAGPU(gpu *models.GPU) bool {
if gpu == nil {
return false
}
if gpu.VendorID == 0x10de {
return true
}
man := strings.ToLower(strings.TrimSpace(gpu.Manufacturer))
return strings.Contains(man, "nvidia")
}
func applyRedisGPUFields(gpu *models.GPU, fields map[string]string) {
if gpu == nil || fields == nil {
return
}
if serial := normalizeRedisValue(fields["NV_GPU_SerialNumber"]); serial != "" && isMissingGPUField(gpu.SerialNumber) {
gpu.SerialNumber = serial
}
if fw := normalizeRedisValue(fields["NV_GPU_FWVersion"]); fw != "" && isMissingGPUField(gpu.Firmware) {
gpu.Firmware = fw
}
if uuid := normalizeRedisValue(fields["NV_GPU_UUID"]); uuid != "" && isMissingGPUField(gpu.UUID) {
gpu.UUID = uuid
}
if part := normalizeRedisValue(fields["NVGPUPartNumber"]); part != "" && isMissingGPUField(gpu.PartNumber) {
gpu.PartNumber = part
}
if model := normalizeRedisValue(fields["NVGPUMarketingName"]); model != "" && isGenericGPUModel(gpu.Model) {
gpu.Model = model
}
if gpu.ClockSpeed == 0 {
if mhz, ok := parseIntField(fields["OperatingSpeedMHz"]); ok {
gpu.ClockSpeed = mhz
}
}
if gpu.Power == 0 {
if pwr, ok := parseIntField(fields["GPUTotalPower"]); ok {
gpu.Power = pwr
}
}
if gpu.Temperature == 0 {
if temp, ok := parseIntField(fields["Temp"]); ok {
gpu.Temperature = temp
}
}
if gpu.MemTemperature == 0 {
if temp, ok := parseIntField(fields["MemTemp"]); ok {
gpu.MemTemperature = temp
}
}
}
func parseIntField(v string) (int, bool) {
v = normalizeRedisValue(v)
if v == "" {
return 0, false
}
n, err := strconv.Atoi(v)
if err != nil {
return 0, false
}
return n, true
}
func normalizeRedisValue(v string) string {
v = strings.TrimSpace(v)
if v == "" {
return ""
}
l := strings.ToLower(v)
if l == "n/a" || l == "na" || l == "null" || l == "unknown" {
return ""
}
return v
}
func isMissingGPUField(v string) bool {
return normalizeRedisValue(v) == ""
}
func isGenericGPUModel(model string) bool {
m := strings.ToLower(strings.TrimSpace(model))
switch m {
case "", "unknown", "display", "display controller", "3d controller", "vga", "gpu":
return true
default:
return false
}
}
func applyRedisNICEnrichment(hw *models.HardwareConfig, snap redisNICSnapshot) {
if len(hw.NetworkAdapters) == 0 || len(snap.ByIndex) == 0 {
return
}
type redisNIC struct {
Index int
Data map[string]string
}
redisNICs := make([]redisNIC, 0, len(snap.ByIndex))
for idx, data := range snap.ByIndex {
if data == nil {
continue
}
if normalizeRedisValue(data["FWVersion"]) == "" {
continue
}
redisNICs = append(redisNICs, redisNIC{Index: idx, Data: data})
}
if len(redisNICs) == 0 {
return
}
sort.Slice(redisNICs, func(i, j int) bool { return redisNICs[i].Index < redisNICs[j].Index })
target := make([]*models.NetworkAdapter, 0, len(hw.NetworkAdapters))
for i := range hw.NetworkAdapters {
nic := &hw.NetworkAdapters[i]
if nic.Present {
target = append(target, nic)
}
}
if len(target) == 0 {
return
}
sort.Slice(target, func(i, j int) bool {
left := strings.TrimSpace(target[i].Location)
right := strings.TrimSpace(target[j].Location)
if left != "" && right != "" {
return left < right
}
return strings.TrimSpace(target[i].Slot) < strings.TrimSpace(target[j].Slot)
})
limit := len(target)
if len(redisNICs) < limit {
limit = len(redisNICs)
}
for i := 0; i < limit; i++ {
nic := target[i]
data := redisNICs[i].Data
if fw := normalizeRedisValue(data["FWVersion"]); fw != "" && normalizeRedisValue(nic.Firmware) == "" {
nic.Firmware = fw
}
if serial := normalizeRedisValue(data["SerialNum"]); serial != "" && normalizeRedisValue(nic.SerialNumber) == "" {
nic.SerialNumber = serial
}
if part := normalizeRedisValue(data["PartNum"]); part != "" && normalizeRedisValue(nic.PartNumber) == "" {
nic.PartNumber = part
}
}
}
func applyRedisPCIeEnrichment(hw *models.HardwareConfig, raidSerials []string) {
if hw == nil || len(hw.PCIeDevices) == 0 || len(raidSerials) == 0 {
return
}
target := make([]*models.PCIeDevice, 0, len(hw.PCIeDevices))
for i := range hw.PCIeDevices {
dev := &hw.PCIeDevices[i]
if normalizeRedisValue(dev.SerialNumber) != "" {
continue
}
class := strings.ToLower(strings.TrimSpace(dev.DeviceClass))
part := strings.ToLower(strings.TrimSpace(dev.PartNumber))
if strings.Contains(class, "raid") || strings.Contains(class, "sas") || strings.Contains(class, "storage") ||
strings.Contains(part, "raid") || strings.Contains(part, "sas") || strings.Contains(part, "hba") {
target = append(target, dev)
}
}
if len(target) == 0 {
return
}
sort.Slice(target, func(i, j int) bool {
left := strings.TrimSpace(target[i].BDF)
right := strings.TrimSpace(target[j].BDF)
if left != "" && right != "" {
return left < right
}
return strings.TrimSpace(target[i].Slot) < strings.TrimSpace(target[j].Slot)
})
limit := len(target)
if len(raidSerials) < limit {
limit = len(raidSerials)
}
for i := 0; i < limit; i++ {
target[i].SerialNumber = raidSerials[i]
}
}
func applyRedisPCIESNPNEnrichment(hw *models.HardwareConfig, snap redisPCIESerialSnapshot) {
if hw == nil || len(hw.PCIeDevices) == 0 || len(snap.ByPart) == 0 {
return
}
for i := range hw.PCIeDevices {
dev := &hw.PCIeDevices[i]
if normalizeRedisValue(dev.SerialNumber) != "" {
continue
}
part := strings.ToLower(strings.TrimSpace(dev.PartNumber))
if part == "" {
continue
}
if serial := normalizeRedisValue(snap.ByPart[part]); serial != "" {
dev.SerialNumber = serial
}
}
}

View File

@@ -0,0 +1,144 @@
package inspur
import (
"testing"
"git.mchus.pro/mchus/logpile/internal/models"
)
func TestExtractRedisInlineValue_DecodesHexEncodedString(t *testing.T) {
data := []byte("RedisNicInfo:redis_nic_info_t:stNicDeviceInfo0:FWVersion 32362e34332e32353636000000000000\x00tail")
key := []byte("RedisNicInfo:redis_nic_info_t:stNicDeviceInfo0:FWVersion")
pos := indexBytes(data, key)
if pos < 0 {
t.Fatal("key not found")
}
got := extractRedisInlineValue(data, pos+len(key))
if got != "26.43.2566" {
t.Fatalf("expected decoded fw 26.43.2566, got %q", got)
}
}
func TestApplyRedisGPUEnrichment_FillsSerialFirmwareUUID(t *testing.T) {
hw := &models.HardwareConfig{
GPUs: []models.GPU{
{Slot: "#CPU0_PCIE2", BDF: "0c:00.0", VendorID: 0x10de, Model: "3D Controller"},
{Slot: "#CPU0_PCIE1", BDF: "58:00.0", VendorID: 0x10de, Model: "3D Controller"},
},
}
snap := redisGPUSnapshot{
ByIndex: map[int]map[string]string{
1: {
"NV_GPU_SerialNumber": "1321125009572",
"NV_GPU_FWVersion": "96.00.B7.00.02",
"NV_GPU_UUID": "GPU-AAA",
},
2: {
"NV_GPU_SerialNumber": "1321125010420",
"NV_GPU_FWVersion": "96.00.B7.00.02",
"NV_GPU_UUID": "GPU-BBB",
},
},
}
applyRedisGPUEnrichment(hw, snap)
if hw.GPUs[0].SerialNumber != "1321125009572" || hw.GPUs[0].Firmware != "96.00.B7.00.02" || hw.GPUs[0].UUID != "GPU-AAA" {
t.Fatalf("unexpected gpu0 enrichment: %+v", hw.GPUs[0])
}
if hw.GPUs[1].SerialNumber != "1321125010420" || hw.GPUs[1].Firmware != "96.00.B7.00.02" || hw.GPUs[1].UUID != "GPU-BBB" {
t.Fatalf("unexpected gpu1 enrichment: %+v", hw.GPUs[1])
}
}
func TestApplyRedisGPUEnrichment_SkipsOnCountMismatch(t *testing.T) {
hw := &models.HardwareConfig{
GPUs: []models.GPU{
{Slot: "#CPU0_PCIE2", BDF: "0c:00.0", VendorID: 0x10de, Model: "3D Controller"},
},
}
snap := redisGPUSnapshot{
ByIndex: map[int]map[string]string{
1: {"NV_GPU_SerialNumber": "1321125009572"},
2: {"NV_GPU_SerialNumber": "1321125010420"},
},
}
applyRedisGPUEnrichment(hw, snap)
if hw.GPUs[0].SerialNumber != "" {
t.Fatalf("expected no enrichment on count mismatch, got %q", hw.GPUs[0].SerialNumber)
}
}
func TestParseRedisRAIDSerials_DecodesHexSerial(t *testing.T) {
raw := []byte("RAIDMSCCInfo:redis_pcie_mscc_raid_info_t0:RAIDInfo:SerialNum\x80%@`5341523531314532 \x00tail")
got := parseRedisRAIDSerials(raw)
if len(got) != 1 {
t.Fatalf("expected 1 raid serial, got %d", len(got))
}
if got[0] != "SAR511E2" {
t.Fatalf("expected decoded serial SAR511E2, got %q", got[0])
}
}
func TestApplyRedisPCIeEnrichment_FillsStorageControllerSerial(t *testing.T) {
hw := &models.HardwareConfig{
PCIeDevices: []models.PCIeDevice{
{Slot: "#CPU1_PCIE9", BDF: "98:00.0", DeviceClass: "Smart Storage PQI SAS", PartNumber: "PM8222-SHBA"},
{Slot: "#CPU0_PCIE3", BDF: "32:00.0", DeviceClass: "Fibre Channel", PartNumber: "LPE32002"},
},
}
applyRedisPCIeEnrichment(hw, []string{"SAR511E2"})
if hw.PCIeDevices[0].SerialNumber != "SAR511E2" {
t.Fatalf("expected PM8222 serial SAR511E2, got %q", hw.PCIeDevices[0].SerialNumber)
}
if hw.PCIeDevices[1].SerialNumber != "" {
t.Fatalf("expected non-storage device serial untouched, got %q", hw.PCIeDevices[1].SerialNumber)
}
}
func TestParseRedisPCIESerialSnapshot_MapsPNToSN(t *testing.T) {
raw := []byte("" +
"AssetInfoPCIE:SNPN9:PN PM8222-SHBA\x00" +
"AssetInfoPCIE:SNPN9:SN SAR511E2\x00")
snap := parseRedisPCIESerialSnapshot(raw)
got := snap.ByPart["pm8222-shba"]
if got != "SAR511E2" {
t.Fatalf("expected SN SAR511E2 for PM8222-SHBA, got %q", got)
}
}
func TestApplyRedisPCIESNPNEnrichment_FillsByPartNumber(t *testing.T) {
hw := &models.HardwareConfig{
PCIeDevices: []models.PCIeDevice{
{Slot: "#CPU1_PCIE9", PartNumber: "PM8222-SHBA"},
},
}
snap := redisPCIESerialSnapshot{ByPart: map[string]string{"pm8222-shba": "SAR511E2"}}
applyRedisPCIESNPNEnrichment(hw, snap)
if hw.PCIeDevices[0].SerialNumber != "SAR511E2" {
t.Fatalf("expected serial SAR511E2, got %q", hw.PCIeDevices[0].SerialNumber)
}
}
func indexBytes(haystack, needle []byte) int {
for i := 0; i+len(needle) <= len(haystack); i++ {
match := true
for j := 0; j < len(needle); j++ {
if haystack[i+j] != needle[j] {
match = false
break
}
}
if match {
return i
}
}
return -1
}

41507
internal/parser/vendors/pciids/pci.ids vendored Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,12 +1,27 @@
package pciids
import (
"bufio"
_ "embed"
"fmt"
"os"
"strconv"
"strings"
"sync"
)
var (
//go:embed pci.ids
embeddedPCIIDs string
loadOnce sync.Once
vendors map[int]string
devices map[string]string
)
// VendorName returns vendor name by PCI Vendor ID
func VendorName(vendorID int) string {
loadPCIIDs()
if name, ok := vendors[vendorID]; ok {
return name
}
@@ -15,6 +30,7 @@ func VendorName(vendorID int) string {
// DeviceName returns device name by Vendor ID and Device ID
func DeviceName(vendorID, deviceID int) string {
loadPCIIDs()
key := fmt.Sprintf("%04x:%04x", vendorID, deviceID)
if name, ok := devices[key]; ok {
return name
@@ -46,7 +62,6 @@ func VendorNameFromString(s string) string {
} else if c >= 'a' && c <= 'f' {
id = id*16 + int(c-'a'+10)
} else {
// Not a valid hex string, return original
return ""
}
}
@@ -54,124 +69,99 @@ func VendorNameFromString(s string) string {
return VendorName(id)
}
// Common PCI Vendor IDs
// Source: https://pci-ids.ucw.cz/
var vendors = map[int]string{
// Storage controllers and SSDs
0x1E0F: "KIOXIA",
0x144D: "Samsung Electronics",
0x1C5C: "SK Hynix",
0x15B7: "SanDisk (Western Digital)",
0x1179: "Toshiba",
0x8086: "Intel",
0x1344: "Micron Technology",
0x126F: "Silicon Motion",
0x1987: "Phison Electronics",
0x1CC1: "ADATA Technology",
0x2646: "Kingston Technology",
0x1E95: "Solid State Storage Technology",
0x025E: "Solidigm",
0x1D97: "Shenzhen Longsys Electronics",
0x1E4B: "MAXIO Technology",
func loadPCIIDs() {
loadOnce.Do(func() {
vendors = make(map[int]string)
devices = make(map[string]string)
// Network adapters
0x15B3: "Mellanox Technologies",
0x14E4: "Broadcom",
0x10EC: "Realtek Semiconductor",
0x1077: "QLogic",
0x19A2: "Emulex",
0x1137: "Cisco Systems",
0x1924: "Solarflare Communications",
0x177D: "Cavium",
0x1D6A: "Aquantia",
0x1FC9: "Tehuti Networks",
0x18D4: "Chelsio Communications",
parsePCIIDs(strings.NewReader(embeddedPCIIDs), vendors, devices)
// GPU / Graphics
0x10DE: "NVIDIA",
0x1002: "AMD/ATI",
0x102B: "Matrox Electronics",
0x1A03: "ASPEED Technology",
// Storage controllers (RAID/HBA)
0x1000: "LSI Logic / Broadcom",
0x9005: "Adaptec / Microsemi",
0x1028: "Dell",
0x103C: "Hewlett-Packard",
0x17D3: "Areca Technology",
0x1CC4: "Union Memory",
// Server vendors
0x1014: "IBM",
0x15D9: "Supermicro",
0x8088: "Inspur",
// Other common
0x1022: "AMD",
0x1106: "VIA Technologies",
0x10B5: "PLX Technology",
0x1B21: "ASMedia Technology",
0x1B4B: "Marvell Technology",
0x197B: "JMicron Technology",
for _, path := range candidatePCIIDsPaths() {
f, err := os.Open(path)
if err != nil {
continue
}
parsePCIIDs(f, vendors, devices)
_ = f.Close()
}
})
}
// Device IDs (vendor:device -> name)
var devices = map[string]string{
// NVIDIA GPUs (0x10DE)
"10de:26b9": "L40S 48GB",
"10de:26b1": "L40 48GB",
"10de:2684": "RTX 4090",
"10de:2704": "RTX 4080",
"10de:2782": "RTX 4070 Ti",
"10de:2786": "RTX 4070",
"10de:27b8": "RTX 4060 Ti",
"10de:2882": "RTX 4060",
"10de:2204": "RTX 3090",
"10de:2208": "RTX 3080 Ti",
"10de:2206": "RTX 3080",
"10de:2484": "RTX 3070",
"10de:2503": "RTX 3060",
"10de:20b0": "A100 80GB",
"10de:20b2": "A100 40GB",
"10de:20f1": "A10",
"10de:2236": "A10G",
"10de:25b6": "A16",
"10de:20b5": "A30",
"10de:20b7": "A30X",
"10de:1db4": "V100 32GB",
"10de:1db1": "V100 16GB",
"10de:1e04": "RTX 2080 Ti",
"10de:1e07": "RTX 2080",
"10de:1f02": "RTX 2070",
"10de:26ba": "L40S-PCIE-48G",
"10de:2330": "H100 80GB PCIe",
"10de:2331": "H100 80GB SXM5",
"10de:2322": "H100 NVL",
"10de:2324": "H200",
func candidatePCIIDsPaths() []string {
paths := []string{
"pci.ids",
"/usr/share/hwdata/pci.ids",
"/usr/share/misc/pci.ids",
"/opt/homebrew/share/pciids/pci.ids",
}
// AMD GPUs (0x1002)
"1002:744c": "Instinct MI250X",
"1002:7408": "Instinct MI100",
"1002:73a5": "RX 6950 XT",
"1002:73bf": "RX 6900 XT",
"1002:73df": "RX 6700 XT",
"1002:7480": "RX 7900 XTX",
"1002:7483": "RX 7900 XT",
// ASPEED (0x1A03) - BMC VGA
"1a03:2000": "AST2500 VGA",
"1a03:1150": "AST2600 VGA",
// Intel GPUs
"8086:56c0": "Data Center GPU Flex 170",
"8086:56c1": "Data Center GPU Flex 140",
// Mellanox/NVIDIA NICs (0x15B3)
"15b3:1017": "ConnectX-5 100GbE",
"15b3:1019": "ConnectX-5 Ex",
"15b3:101b": "ConnectX-6",
"15b3:101d": "ConnectX-6 Dx",
"15b3:101f": "ConnectX-6 Lx",
"15b3:1021": "ConnectX-7",
"15b3:a2d6": "ConnectX-4 Lx",
// Env paths have highest priority, so they are applied last.
if env := strings.TrimSpace(os.Getenv("LOGPILE_PCI_IDS_PATH")); env != "" {
for _, p := range strings.Split(env, string(os.PathListSeparator)) {
p = strings.TrimSpace(p)
if p != "" {
paths = append(paths, p)
}
}
}
return paths
}
func parsePCIIDs(r interface{ Read([]byte) (int, error) }, outVendors map[int]string, outDevices map[string]string) {
scanner := bufio.NewScanner(r)
currentVendor := -1
for scanner.Scan() {
line := scanner.Text()
if line == "" || strings.HasPrefix(line, "#") {
continue
}
// Subdevice line (tab-tab) - ignored for now
if strings.HasPrefix(line, "\t\t") {
continue
}
// Device line
if strings.HasPrefix(line, "\t") {
if currentVendor < 0 {
continue
}
trimmed := strings.TrimLeft(line, "\t")
fields := strings.Fields(trimmed)
if len(fields) < 2 {
continue
}
deviceID, err := strconv.ParseInt(fields[0], 16, 32)
if err != nil {
continue
}
name := strings.TrimSpace(trimmed[len(fields[0]):])
if name == "" {
continue
}
key := fmt.Sprintf("%04x:%04x", currentVendor, int(deviceID))
outDevices[key] = name
continue
}
// Vendor line
fields := strings.Fields(line)
if len(fields) < 2 {
currentVendor = -1
continue
}
vendorID, err := strconv.ParseInt(fields[0], 16, 32)
if err != nil {
currentVendor = -1
continue
}
name := strings.TrimSpace(line[len(fields[0]):])
if name == "" {
currentVendor = -1
continue
}
currentVendor = int(vendorID)
outVendors[currentVendor] = name
}
}

View File

@@ -0,0 +1,38 @@
package pciids
import (
"os"
"path/filepath"
"sync"
"testing"
)
func TestExternalPCIIDsLookup(t *testing.T) {
dir := t.TempDir()
idsPath := filepath.Join(dir, "pci.ids")
content := "" +
"# sample\n" +
"10de NVIDIA Corporation\n" +
"\t233b NVIDIA H200 SXM\n" +
"8086 Intel Corporation\n" +
"\t1521 I350 Gigabit Network Connection\n"
if err := os.WriteFile(idsPath, []byte(content), 0o644); err != nil {
t.Fatalf("write pci.ids: %v", err)
}
t.Setenv("LOGPILE_PCI_IDS_PATH", idsPath)
loadOnce = sync.Once{}
vendors = nil
devices = nil
if got := DeviceName(0x10de, 0x233b); got != "NVIDIA H200 SXM" {
t.Fatalf("expected external device name, got %q", got)
}
if got := VendorName(0x10de); got != "NVIDIA Corporation" {
t.Fatalf("expected external vendor name, got %q", got)
}
if got := DeviceName(0x8086, 0x1521); got != "I350 Gigabit Network Connection" {
t.Fatalf("expected external intel device name, got %q", got)
}
}