package nvidia import ( "bufio" "regexp" "strings" "git.mchus.pro/mchus/logpile/internal/models" "git.mchus.pro/mchus/logpile/internal/parser" ) var ( // Regex to extract devname mappings from fieldiag command line // Example: "devname=0000:ba:00.0,SXM5_SN_1653925027099" devnameRegex = regexp.MustCompile(`devname=([\da-fA-F:\.]+),(\w+)`) // Regex to capture BDF from commands like: // "$ lspci -vvvs 0000:05:00.0" or "$ lspci -vvs 0000:05:00.0" lspciBDFRegex = regexp.MustCompile(`^\$\s+lspci\s+-[^\s]*\s+([0-9a-fA-F]{4}:[0-9a-fA-F]{2}:[0-9a-fA-F]{2}\.[0-7])\s*$`) // Example: "Capabilities: [2f0 v1] Device Serial Number 99-d3-61-c8-ac-2d-b0-48" deviceSerialRegex = regexp.MustCompile(`Device Serial Number\s+([0-9a-fA-F\-:]+)`) ) // ParseInventoryLog parses inventory/output.log to extract GPU serial numbers // from fieldiag devname parameters (e.g., "SXM5_SN_1653925027099") func ParseInventoryLog(content []byte, result *models.AnalysisResult) error { if result.Hardware == nil || len(result.Hardware.GPUs) == 0 { // No GPUs to update return nil } scanner := bufio.NewScanner(strings.NewReader(string(content))) // First pass: build mapping of PCI BDF -> Slot name and serial number from fieldiag command line pciToSlot := make(map[string]string) pciToSerial := make(map[string]string) for scanner.Scan() { line := scanner.Text() // Look for fieldiag command with devname parameters if strings.Contains(line, "devname=") && strings.Contains(line, "fieldiag") { matches := devnameRegex.FindAllStringSubmatch(line, -1) for _, match := range matches { if len(match) == 3 { pciBDF := match[1] slotName := match[2] // Extract slot number and serial from name like "SXM5_SN_1653925027099" if strings.HasPrefix(slotName, "SXM") { parts := strings.Split(slotName, "_") if len(parts) >= 1 { // Convert "SXM5" to "GPUSXM5" slot := "GPU" + parts[0] pciToSlot[pciBDF] = slot } // Extract serial number from "SXM5_SN_1653925027099" if len(parts) == 3 && parts[1] == "SN" { serial := parts[2] pciToSerial[pciBDF] = serial } } } } } } // Second pass: assign serial numbers to GPUs based on slot mapping for i := range result.Hardware.GPUs { slot := result.Hardware.GPUs[i].Slot // Find the PCI BDF for this slot var foundSerial string for pciBDF, mappedSlot := range pciToSlot { if mappedSlot == slot { // Found matching slot, get serial number if serial, ok := pciToSerial[pciBDF]; ok { foundSerial = serial break } } } if foundSerial != "" { result.Hardware.GPUs[i].SerialNumber = foundSerial } } // Third pass: parse lspci "Device Serial Number" by BDF (useful for NVSwitch serials). bdfToDeviceSerial := make(map[string]string) currentBDF := "" scanner = bufio.NewScanner(strings.NewReader(string(content))) for scanner.Scan() { line := strings.TrimSpace(scanner.Text()) if line == "" { continue } if m := lspciBDFRegex.FindStringSubmatch(line); len(m) == 2 { currentBDF = strings.ToLower(strings.TrimSpace(m[1])) continue } if currentBDF == "" { continue } if m := deviceSerialRegex.FindStringSubmatch(line); len(m) == 2 { serial := strings.TrimSpace(m[1]) if serial != "" { bdfToDeviceSerial[currentBDF] = serial } currentBDF = "" } } // Apply to PCIe devices first (includes NVSwitch). for i := range result.Hardware.PCIeDevices { dev := &result.Hardware.PCIeDevices[i] if strings.TrimSpace(dev.SerialNumber) != "" { continue } bdf := strings.ToLower(strings.TrimSpace(dev.BDF)) if bdf == "" { continue } if serial := bdfToDeviceSerial[bdf]; serial != "" { dev.SerialNumber = serial } } // Apply to GPUs only if GPU serial is still empty (do not overwrite prod serial from devname). for i := range result.Hardware.GPUs { gpu := &result.Hardware.GPUs[i] if strings.TrimSpace(gpu.SerialNumber) != "" { continue } bdf := strings.ToLower(strings.TrimSpace(gpu.BDF)) if bdf == "" { continue } if serial := bdfToDeviceSerial[bdf]; serial != "" { gpu.SerialNumber = serial } } return scanner.Err() } // findInventoryOutputLog finds the inventory/output.log file func findInventoryOutputLog(files []parser.ExtractedFile) *parser.ExtractedFile { for _, f := range files { // Look for inventory/output.log path := strings.ToLower(f.Path) if strings.Contains(path, "inventory/output.log") || strings.Contains(path, "inventory\\output.log") { return &f } } return nil }