package nvidia_bug_report import ( "bufio" "regexp" "strconv" "strings" "time" "git.mchus.pro/mchus/logpile/internal/models" ) // parseGPUInfo extracts GPU information from the bug report func parseGPUInfo(content string, result *models.AnalysisResult) { scanner := bufio.NewScanner(strings.NewReader(content)) var currentGPU *models.GPU inGPUInfo := false for scanner.Scan() { line := scanner.Text() // Look for GPU information section markers (but skip ls listings) if strings.Contains(line, "/proc/driver/nvidia") && strings.Contains(line, "/gpus/") && strings.Contains(line, "/information") && !strings.Contains(line, "ls:") { // Extract PCI address re := regexp.MustCompile(`/gpus/([\da-f]{4}:[\da-f]{2}:[\da-f]{2}\.[\da-f])`) matches := re.FindStringSubmatch(line) if len(matches) > 1 { pciAddr := matches[1] // Save previous GPU if exists if currentGPU != nil { result.Hardware.GPUs = append(result.Hardware.GPUs, *currentGPU) } // Start new GPU entry currentGPU = &models.GPU{ BDF: pciAddr, Manufacturer: "NVIDIA", } inGPUInfo = true continue } } // End of GPU info section (separator line or new section, but not ls lines) if inGPUInfo && (strings.HasPrefix(line, "___") || (strings.HasPrefix(line, "***") && !strings.Contains(line, "ls:"))) { inGPUInfo = false continue } // Parse GPU fields within information section if inGPUInfo && currentGPU != nil && strings.Contains(line, ":") { // Split on first colon and trim whitespace/tabs parts := strings.SplitN(line, ":", 2) if len(parts) != 2 { continue } field := strings.TrimSpace(parts[0]) value := strings.TrimSpace(parts[1]) if value == "" { continue } switch field { case "Model": currentGPU.Model = value case "IRQ": if irq, err := strconv.Atoi(value); err == nil { currentGPU.IRQ = irq } case "GPU UUID": currentGPU.UUID = value case "Video BIOS": currentGPU.VideoBIOS = value case "Bus Type": currentGPU.BusType = value case "DMA Size": currentGPU.DMASize = value case "DMA Mask": currentGPU.DMAMask = value case "Bus Location": // BDF already set from path, but verify consistency if currentGPU.BDF != value { // Use the value from the information section as it's more explicit currentGPU.BDF = value } case "Device Minor": if minor, err := strconv.Atoi(value); err == nil { currentGPU.DeviceMinor = minor } case "GPU Excluded": // Store as status if "Yes" if strings.ToLower(value) == "yes" { currentGPU.Status = "Excluded" } } } } // Save last GPU if exists if currentGPU != nil { result.Hardware.GPUs = append(result.Hardware.GPUs, *currentGPU) } applyGPUSerialNumbers(content, result.Hardware.GPUs) // Create event for GPU summary if len(result.Hardware.GPUs) > 0 { result.Events = append(result.Events, models.Event{ Timestamp: time.Now(), Source: "NVIDIA Driver", EventType: "GPU Detection", Description: "NVIDIA GPUs detected", Severity: models.SeverityInfo, RawData: formatGPUSummary(result.Hardware.GPUs), }) } } // parseDriverVersion extracts NVIDIA driver version func parseDriverVersion(content string, result *models.AnalysisResult) { scanner := bufio.NewScanner(strings.NewReader(content)) for scanner.Scan() { line := scanner.Text() // Look for NVRM version line if strings.Contains(line, "NVRM version:") { // Extract version info parts := strings.Split(line, "NVRM version:") if len(parts) > 1 { version := strings.TrimSpace(parts[1]) result.Events = append(result.Events, models.Event{ Timestamp: time.Now(), Source: "NVIDIA Driver", EventType: "Driver Version", Description: "NVIDIA driver version detected", Severity: models.SeverityInfo, RawData: version, }) break } } } } // formatGPUSummary creates a summary string for GPUs func formatGPUSummary(gpus []models.GPU) string { if len(gpus) == 0 { return "" } var summary strings.Builder for i, gpu := range gpus { if i > 0 { summary.WriteString("; ") } summary.WriteString(gpu.BDF) if gpu.Model != "" { summary.WriteString(" (") summary.WriteString(gpu.Model) summary.WriteString(")") } } return summary.String() } func applyGPUSerialNumbers(content string, gpus []models.GPU) { if len(gpus) == 0 { return } serialByBDF := parseGPUSerialsFromNvidiaSMI(content) if len(serialByBDF) == 0 { serialByBDF = parseGPUSerialsFromSummary(content) } if len(serialByBDF) == 0 { return } for i := range gpus { bdf := normalizeGPUAddress(gpus[i].BDF) if bdf == "" { continue } if serial, ok := serialByBDF[bdf]; ok && serial != "" { gpus[i].SerialNumber = serial } } } func parseGPUSerialsFromNvidiaSMI(content string) map[string]string { scanner := bufio.NewScanner(strings.NewReader(content)) reGPU := regexp.MustCompile(`^GPU\s+([0-9A-F]{8}:[0-9A-F]{2}:[0-9A-F]{2}\.[0-9A-F])$`) serialByBDF := make(map[string]string) currentBDF := "" for scanner.Scan() { line := strings.TrimSpace(scanner.Text()) if line == "" { continue } if matches := reGPU.FindStringSubmatch(line); len(matches) == 2 { currentBDF = normalizeGPUAddress(matches[1]) continue } if currentBDF == "" { continue } if strings.HasPrefix(line, "Serial Number") { parts := strings.SplitN(line, ":", 2) if len(parts) != 2 { continue } serial := strings.TrimSpace(parts[1]) if serial != "" && !strings.EqualFold(serial, "N/A") { serialByBDF[currentBDF] = serial } } } return serialByBDF } func parseGPUSerialsFromSummary(content string) map[string]string { scanner := bufio.NewScanner(strings.NewReader(content)) serialByBDF := make(map[string]string) inGPUDetails := false for scanner.Scan() { line := scanner.Text() trimmed := strings.TrimSpace(line) if strings.HasPrefix(trimmed, "NVIDIA GPU Details") { inGPUDetails = true } if !inGPUDetails { continue } if strings.HasPrefix(trimmed, "NVIDIA Switch Details") { break } parts := strings.Split(line, "|") if len(parts) < 2 { continue } payload := strings.TrimSpace(parts[len(parts)-1]) if payload == "" { continue } fields := strings.Split(payload, ",") if len(fields) < 6 { continue } bdf := normalizeGPUAddress(strings.TrimSpace(fields[4])) serial := strings.TrimSpace(fields[5]) if bdf == "" || serial == "" || strings.EqualFold(serial, "N/A") { continue } serialByBDF[bdf] = serial } return serialByBDF } func normalizeGPUAddress(addr string) string { addr = strings.TrimSpace(addr) if addr == "" { return "" } parts := strings.Split(addr, ":") if len(parts) != 3 { return strings.ToLower(addr) } domain := parts[0] bus := parts[1] devFn := parts[2] devFnParts := strings.Split(devFn, ".") if len(devFnParts) != 2 { return strings.ToLower(addr) } device := devFnParts[0] fn := devFnParts[1] if len(domain) == 8 { domain = domain[4:] } return strings.ToLower(domain + ":" + bus + ":" + device + "." + fn) }