package nvidia import ( "bufio" "fmt" "regexp" "strconv" "strings" "git.mchus.pro/mchus/logpile/internal/models" "git.mchus.pro/mchus/logpile/internal/parser" ) var ( nvflashAdapterRegex = regexp.MustCompile(`^Adapter:\s+.+\(([\da-fA-F]+),([\da-fA-F]+),([\da-fA-F]+),([\da-fA-F]+)\)\s+S:([0-9A-Fa-f]{2}),B:([0-9A-Fa-f]{2}),D:([0-9A-Fa-f]{2}),F:([0-9A-Fa-f])`) gpuPCIIDRegex = regexp.MustCompile(`^GPU_SXM(\d+)_PCIID:\s*(\S+)$`) nvsPCIIDRegex = regexp.MustCompile(`^NVSWITCH_NVSWITCH(\d+)_PCIID:\s*(\S+)$`) ) var nvswitchProjectToPartNumber = map[string]string{ "5612-0002": "965-25612-0002-000", } type nvflashDeviceRecord struct { BDF string VendorID int DeviceID int SSVendorID int SSDeviceID int Version string BoardID string HierarchyID string ChipSKU string Project string } // ParseNVFlashVerboseLog parses inventory/nvflash_verbose.log and applies firmware versions // to already discovered devices using PCI BDF with optional ID checks. func ParseNVFlashVerboseLog(content []byte, result *models.AnalysisResult) error { if result == nil || result.Hardware == nil { return nil } records := parseNVFlashRecords(content) if len(records) == 0 { return nil } for i := range result.Hardware.GPUs { gpu := &result.Hardware.GPUs[i] bdf := normalizePCIBDF(gpu.BDF) if bdf == "" { continue } rec, ok := records[bdf] if !ok { continue } if gpu.DeviceID != 0 && rec.DeviceID != 0 && gpu.DeviceID != rec.DeviceID { continue } if gpu.VendorID != 0 && rec.VendorID != 0 && gpu.VendorID != rec.VendorID { continue } if strings.TrimSpace(rec.Version) != "" { gpu.Firmware = strings.TrimSpace(rec.Version) } } for i := range result.Hardware.PCIeDevices { dev := &result.Hardware.PCIeDevices[i] bdf := normalizePCIBDF(dev.BDF) if bdf == "" { continue } rec, ok := records[bdf] if !ok { continue } if dev.DeviceID != 0 && rec.DeviceID != 0 && dev.DeviceID != rec.DeviceID { continue } if dev.VendorID != 0 && rec.VendorID != 0 && dev.VendorID != rec.VendorID { continue } if strings.EqualFold(strings.TrimSpace(dev.DeviceClass), "NVSwitch") || strings.HasPrefix(strings.ToUpper(strings.TrimSpace(dev.Slot)), "NVSWITCH") { if mappedPN := mapNVSwitchPartNumberByProject(rec.Project); mappedPN != "" { dev.PartNumber = mappedPN } } if strings.TrimSpace(rec.Version) != "" && strings.TrimSpace(dev.PartNumber) == "" { // Fallback for non-NVSwitch devices where part number is unknown. dev.PartNumber = strings.TrimSpace(rec.Version) } } appendNVFlashFirmwareEntries(result, records) return nil } // ApplyInventoryPCIIDs enriches devices with PCI BDFs from inventory/inventory.log. func ApplyInventoryPCIIDs(content []byte, result *models.AnalysisResult) error { if result == nil || result.Hardware == nil { return nil } slotToBDF := parseInventoryPCIIDs(content) if len(slotToBDF) == 0 { return nil } for i := range result.Hardware.GPUs { gpu := &result.Hardware.GPUs[i] if strings.TrimSpace(gpu.BDF) != "" { continue } if bdf := slotToBDF[strings.TrimSpace(gpu.Slot)]; bdf != "" { gpu.BDF = bdf } } for i := range result.Hardware.PCIeDevices { dev := &result.Hardware.PCIeDevices[i] if strings.TrimSpace(dev.BDF) != "" { continue } if bdf := slotToBDF[normalizeNVSwitchSlot(strings.TrimSpace(dev.Slot))]; bdf != "" { dev.BDF = bdf } } return nil } func parseNVFlashRecords(content []byte) map[string]nvflashDeviceRecord { scanner := bufio.NewScanner(strings.NewReader(string(content))) records := make(map[string]nvflashDeviceRecord) var current *nvflashDeviceRecord commit := func() { if current == nil { return } if current.BDF == "" || strings.TrimSpace(current.Version) == "" { return } records[current.BDF] = *current } for scanner.Scan() { line := strings.TrimSpace(scanner.Text()) if line == "" { continue } if m := nvflashAdapterRegex.FindStringSubmatch(line); len(m) == 9 { commit() vendorID, _ := parseHexInt(m[1]) deviceID, _ := parseHexInt(m[2]) ssVendorID, _ := parseHexInt(m[3]) ssDeviceID, _ := parseHexInt(m[4]) current = &nvflashDeviceRecord{ BDF: fmt.Sprintf("0000:%s:%s.%s", strings.ToLower(m[6]), strings.ToLower(m[7]), strings.ToLower(m[8])), VendorID: vendorID, DeviceID: deviceID, SSVendorID: ssVendorID, SSDeviceID: ssDeviceID, } continue } if current == nil { continue } if !strings.Contains(line, ":") { continue } parts := strings.SplitN(line, ":", 2) key := strings.TrimSpace(parts[0]) val := strings.TrimSpace(parts[1]) if key == "" || val == "" { continue } switch key { case "Version": current.Version = val case "Board ID": current.BoardID = strings.ToLower(strings.TrimPrefix(val, "0x")) case "Vendor ID": if v, err := parseHexInt(val); err == nil { current.VendorID = v } case "Device ID": if v, err := parseHexInt(val); err == nil { current.DeviceID = v } case "Hierarchy ID": current.HierarchyID = val case "Chip SKU": current.ChipSKU = val case "Project": current.Project = val } } commit() return records } func parseInventoryPCIIDs(content []byte) map[string]string { scanner := bufio.NewScanner(strings.NewReader(string(content))) slotToBDF := make(map[string]string) for scanner.Scan() { line := strings.TrimSpace(scanner.Text()) if line == "" { continue } if m := gpuPCIIDRegex.FindStringSubmatch(line); len(m) == 3 { slotToBDF["GPUSXM"+m[1]] = normalizePCIBDF(m[2]) continue } if m := nvsPCIIDRegex.FindStringSubmatch(line); len(m) == 3 { slotToBDF["NVSWITCH"+m[1]] = normalizePCIBDF(m[2]) } } return slotToBDF } func normalizePCIBDF(v string) string { s := strings.TrimSpace(strings.ToLower(v)) if s == "" { return "" } // bus:device.func -> 0000:bus:device.func short := regexp.MustCompile(`^([0-9a-f]{2}:[0-9a-f]{2}\.[0-7])$`) if m := short.FindStringSubmatch(s); len(m) == 2 { return "0000:" + m[1] } full := regexp.MustCompile(`^([0-9a-f]{4}:[0-9a-f]{2}:[0-9a-f]{2}\.[0-7])$`) if m := full.FindStringSubmatch(s); len(m) == 2 { return m[1] } return s } func parseHexInt(v string) (int, error) { s := strings.TrimSpace(strings.ToLower(v)) s = strings.TrimPrefix(s, "0x") if s == "" { return 0, fmt.Errorf("empty hex value") } n, err := strconv.ParseInt(s, 16, 32) if err != nil { return 0, err } return int(n), nil } func findNVFlashVerboseLog(files []parser.ExtractedFile) *parser.ExtractedFile { for _, f := range files { path := strings.ToLower(f.Path) if strings.Contains(path, "inventory/nvflash_verbose.log") || strings.Contains(path, "inventory\\nvflash_verbose.log") { return &f } } return nil } func findInventoryInfoLog(files []parser.ExtractedFile) *parser.ExtractedFile { for _, f := range files { path := strings.ToLower(f.Path) if strings.Contains(path, "inventory/inventory.log") || strings.Contains(path, "inventory\\inventory.log") { return &f } } return nil } func appendNVFlashFirmwareEntries(result *models.AnalysisResult, records map[string]nvflashDeviceRecord) { if result == nil || result.Hardware == nil { return } if result.Hardware.Firmware == nil { result.Hardware.Firmware = make([]models.FirmwareInfo, 0) } seen := make(map[string]struct{}) for _, fw := range result.Hardware.Firmware { key := strings.ToLower(strings.TrimSpace(fw.DeviceName)) + "|" + strings.TrimSpace(fw.Version) seen[key] = struct{}{} } for _, gpu := range result.Hardware.GPUs { version := strings.TrimSpace(gpu.Firmware) if version == "" { continue } model := strings.TrimSpace(gpu.PartNumber) if model == "" { model = strings.TrimSpace(gpu.Model) } if model == "" { model = strings.TrimSpace(gpu.Slot) } deviceName := fmt.Sprintf("GPU %s (%s)", strings.TrimSpace(gpu.Slot), model) key := strings.ToLower(deviceName) + "|" + version if _, ok := seen[key]; ok { continue } seen[key] = struct{}{} result.Hardware.Firmware = append(result.Hardware.Firmware, models.FirmwareInfo{ DeviceName: deviceName, Version: version, }) } for _, dev := range result.Hardware.PCIeDevices { bdf := normalizePCIBDF(dev.BDF) rec, ok := records[bdf] if !ok { continue } version := strings.TrimSpace(rec.Version) if version == "" { continue } slot := strings.TrimSpace(dev.Slot) deviceClass := strings.TrimSpace(dev.DeviceClass) if strings.EqualFold(deviceClass, "NVSwitch") || strings.HasPrefix(strings.ToUpper(slot), "NVSWITCH") { model := slot if pn := strings.TrimSpace(dev.PartNumber); pn != "" { model = pn } deviceName := fmt.Sprintf("NVSwitch %s (%s)", slot, model) key := strings.ToLower(deviceName) + "|" + version if _, ok := seen[key]; ok { continue } seen[key] = struct{}{} result.Hardware.Firmware = append(result.Hardware.Firmware, models.FirmwareInfo{ DeviceName: deviceName, Version: version, }) } } } func mapNVSwitchPartNumberByProject(project string) string { key := strings.TrimSpace(strings.ToLower(project)) if key == "" { return "" } return strings.TrimSpace(nvswitchProjectToPartNumber[key]) }