package nvidia import ( "encoding/json" "regexp" "strconv" "strings" "git.mchus.pro/mchus/logpile/internal/models" "git.mchus.pro/mchus/logpile/internal/parser" ) var ( gpuNameWithSerialRegex = regexp.MustCompile(`^SXM(\d+)_SN_(.+)$`) gpuNameSlotOnlyRegex = regexp.MustCompile(`^SXM(\d+)$`) skuCodeRegex = regexp.MustCompile(`^(G\d{3})[.-](\d{4})`) skuCodeInsideRegex = regexp.MustCompile(`(?:^|[^A-Z0-9])(?:\d)?(G\d{3})[.-](\d{4})(?:[^A-Z0-9]|$)`) inforomPathRegex = regexp.MustCompile(`(?i)(?:^|[\\/])(checkinforom|inforom)[\\/](SXM(\d+))(?:_SN_([^\\/]+))?[\\/]fieldiag\.jso$`) inforomProductPNRegex = regexp.MustCompile(`"product_part_num"\s*:\s*"([^"]+)"`) inforomSerialRegex = regexp.MustCompile(`"serial_number"\s*:\s*"([^"]+)"`) ) type testSpecData struct { Actions []struct { VirtualID string `json:"virtual_id"` Args struct { SKUToFile map[string]string `json:"sku_to_sku_json_file_map"` ModsMapping map[string]json.RawMessage `json:"mods_mapping"` } `json:"args"` } `json:"actions"` } type inventoryFieldDiagSummary struct { ModsRuns []struct { ModsHeader []struct { GPUName string `json:"GpuName"` BoardInfo string `json:"BoardInfo"` } `json:"ModsHeader"` } `json:"ModsRuns"` } var hardcodedSKUToFileMap = map[string]string{ "G520-0200": "sku_hgx-h100-8-gpu_80g_aircooled_field.json", "G520-0201": "sku_hgx-h100-8-gpu_80g_aircooled_field.json", "G520-0202": "sku_hgx-h100-8-gpu_80g_tpol_field.json", "G520-0203": "sku_hgx-h100-8-gpu_80g_tpol_field.json", "G520-0205": "sku_hgx-h800-8-gpu_80g_aircooled_field.json", "G520-0207": "sku_hgx-h800-8-gpu_80g_tpol_field.json", "G520-0221": "sku_hgx-h100-8-gpu_96g_aircooled_field.json", "G520-0236": "sku_hgx-h20-8-gpu_96g_aircooled_field.json", "G520-0238": "sku_hgx-h20-8-gpu_96g_tpol_field.json", "G520-0266": "sku_hgx-h20-8-gpu_141g_aircooled_field.json", "G520-0280": "sku_hgx-h200-8-gpu_141g_aircooled_field.json", "G520-0282": "sku_hgx-h200-8-gpu_141g_tpol_field.json", "G520-0292": "sku_hgx-h100-8-gpu_sku_292_field.json", } // ApplyGPUModelsFromSKU updates GPU model names using SKU mapping from testspec.json. // Mapping source: // - inventory/fieldiag_summary.json: GPUName -> BoardInfo(SKU) // - hardcoded SKU mapping // - testspec.json: SKU -> sku_hgx-... filename (fallback for unknown hardcoded SKU) // - inforom/*/fieldiag.jso: product_part_num (full P/N with embedded SKU) // - testspec.json gpu_fieldiag.mods_mapping: DeviceID -> GPU generation (last fallback for description) func ApplyGPUModelsFromSKU(files []parser.ExtractedFile, result *models.AnalysisResult) { if result == nil || result.Hardware == nil || len(result.Hardware.GPUs) == 0 { return } skuToFile := parseSKUToFileMap(files) generationByDeviceID := parseGenerationByDeviceID(files) serialToSKU, slotToSKU, serialToPartNumber, slotToPartNumber := parseGPUSKUMapping(files) for i := range result.Hardware.GPUs { gpu := &result.Hardware.GPUs[i] slot := strings.TrimSpace(gpu.Slot) serial := strings.TrimSpace(gpu.SerialNumber) if gpu.PartNumber == "" && serial != "" { if pn := strings.TrimSpace(serialToPartNumber[serial]); pn != "" { gpu.PartNumber = pn } } if gpu.PartNumber == "" { if pn := strings.TrimSpace(slotToPartNumber[slot]); pn != "" { gpu.PartNumber = pn } } if partNumber := strings.TrimSpace(gpu.PartNumber); partNumber != "" { gpu.Model = partNumber } sku := extractSKUFromPartNumber(gpu.PartNumber) if sku == "" && serial != "" { sku = serialToSKU[serial] } if sku == "" { sku = slotToSKU[slot] } if sku != "" { if desc := resolveDescriptionFromSKU(sku, skuToFile); desc != "" { gpu.Description = desc continue } } if gen := resolveGenerationDescription(gpu.DeviceID, generationByDeviceID); gen != "" { gpu.Description = gen } } } func parseSKUToFileMap(files []parser.ExtractedFile) map[string]string { result := make(map[string]string, len(hardcodedSKUToFileMap)) for sku, file := range hardcodedSKUToFileMap { result[normalizeSKUCode(sku)] = strings.TrimSpace(file) } specFile := parser.FindFileByName(files, "testspec.json") if specFile == nil { return result } var spec testSpecData if err := json.Unmarshal(specFile.Content, &spec); err != nil { return result } for _, action := range spec.Actions { for sku, file := range action.Args.SKUToFile { normSKU := normalizeSKUCode(sku) if normSKU == "" { continue } // Priority: hardcoded mapping wins, testspec extends unknown SKU list. if _, exists := result[normSKU]; !exists { result[normSKU] = strings.TrimSpace(file) } } } return result } func parseGenerationByDeviceID(files []parser.ExtractedFile) map[string]string { specFile := parser.FindFileByName(files, "testspec.json") if specFile == nil { return nil } var spec testSpecData if err := json.Unmarshal(specFile.Content, &spec); err != nil { return nil } familyToGeneration := make(map[string]string) deviceToGeneration := make(map[string]string) for _, action := range spec.Actions { if strings.TrimSpace(strings.ToLower(action.VirtualID)) != "gpu_fieldiag" { continue } for key, raw := range action.Args.ModsMapping { if strings.HasPrefix(key, "#mods.") { family := strings.TrimSpace(strings.TrimPrefix(key, "#mods.")) if family == "" { continue } var generation string if err := json.Unmarshal(raw, &generation); err == nil { generation = strings.TrimSpace(generation) if generation != "" { familyToGeneration[family] = generation } } } } for key, raw := range action.Args.ModsMapping { family := strings.TrimSpace(key) if family == "" || strings.HasPrefix(family, "#") { continue } generation := strings.TrimSpace(familyToGeneration[family]) if generation == "" { continue } var deviceIDs []string if err := json.Unmarshal(raw, &deviceIDs); err != nil { continue } for _, id := range deviceIDs { norm := normalizeDeviceIDHex(id) if norm != "" { deviceToGeneration[norm] = generation } } } } return deviceToGeneration } func parseGPUSKUMapping(files []parser.ExtractedFile) (map[string]string, map[string]string, map[string]string, map[string]string) { serialToSKU := make(map[string]string) slotToSKU := make(map[string]string) serialToPartNumber := make(map[string]string) slotToPartNumber := make(map[string]string) // 1) inventory/fieldiag_summary.json mapping (GPUName/BoardInfo). var summaryFile *parser.ExtractedFile for _, f := range files { path := strings.ToLower(f.Path) if strings.Contains(path, "inventory/fieldiag_summary.json") || strings.Contains(path, "inventory\\fieldiag_summary.json") { summaryFile = &f break } } if summaryFile == nil { // Continue: inforom may still contain usable part numbers. } else { var summaries []inventoryFieldDiagSummary if err := json.Unmarshal(summaryFile.Content, &summaries); err == nil { for _, summary := range summaries { addSummaryMapping(summary, serialToSKU, slotToSKU) } } else { var summary inventoryFieldDiagSummary if err := json.Unmarshal(summaryFile.Content, &summary); err == nil { addSummaryMapping(summary, serialToSKU, slotToSKU) } } } // 2) inforom/checkinforom fieldiag.jso mapping (full product_part_num). for _, f := range files { path := strings.TrimSpace(f.Path) m := inforomPathRegex.FindStringSubmatch(path) if len(m) == 0 { continue } slot := "GPU" + strings.ToUpper(strings.TrimSpace(m[2])) // SXM7 -> GPUSXM7 serialFromPath := strings.TrimSpace(m[4]) productPNMatch := inforomProductPNRegex.FindSubmatch(f.Content) if len(productPNMatch) == 2 { partNumber := strings.TrimSpace(string(productPNMatch[1])) if partNumber != "" { slotToPartNumber[slot] = partNumber if serialFromPath != "" { serialToPartNumber[serialFromPath] = partNumber } if sku := extractSKUFromPartNumber(partNumber); sku != "" { slotToSKU[slot] = sku if serialFromPath != "" { serialToSKU[serialFromPath] = sku } } } } serialMatch := inforomSerialRegex.FindSubmatch(f.Content) if len(serialMatch) == 2 { serial := strings.TrimSpace(string(serialMatch[1])) if serial != "" { if sku := slotToSKU[slot]; sku != "" { serialToSKU[serial] = sku } if pn := slotToPartNumber[slot]; pn != "" { serialToPartNumber[serial] = pn } } } } return serialToSKU, slotToSKU, serialToPartNumber, slotToPartNumber } func addSummaryMapping(summary inventoryFieldDiagSummary, serialToSKU map[string]string, slotToSKU map[string]string) { for _, run := range summary.ModsRuns { for _, h := range run.ModsHeader { sku := normalizeSKUCode(h.BoardInfo) if sku == "" { continue } gpuName := strings.TrimSpace(h.GPUName) if matches := gpuNameWithSerialRegex.FindStringSubmatch(gpuName); len(matches) == 3 { slotToSKU["GPUSXM"+matches[1]] = sku serialToSKU[strings.TrimSpace(matches[2])] = sku continue } if matches := gpuNameSlotOnlyRegex.FindStringSubmatch(gpuName); len(matches) == 2 { slotToSKU["GPUSXM"+matches[1]] = sku } } } } func resolveDescriptionFromSKU(sku string, skuToFile map[string]string) string { file := strings.ToLower(strings.TrimSpace(skuToFile[normalizeSKUCode(sku)])) if file == "" { return "" } return skuFilenameToDescription(file) } func normalizeSKUCode(v string) string { s := strings.TrimSpace(strings.ToUpper(v)) if s == "" { return "" } if m := skuCodeRegex.FindStringSubmatch(s); len(m) == 3 { return m[1] + "-" + m[2] } return s } func extractSKUFromPartNumber(partNumber string) string { s := strings.TrimSpace(strings.ToUpper(partNumber)) if s == "" { return "" } if m := skuCodeInsideRegex.FindStringSubmatch(s); len(m) == 3 { return m[1] + "-" + m[2] } return "" } func skuFilenameToDescription(file string) string { s := strings.TrimSpace(strings.ToLower(file)) if s == "" { return "" } s = strings.TrimSuffix(s, ".json") s = strings.TrimSuffix(s, "_field") s = strings.TrimPrefix(s, "sku_") s = strings.ReplaceAll(s, "-", " ") s = strings.ReplaceAll(s, "_", " ") s = strings.Join(strings.Fields(s), " ") return strings.TrimSpace(s) } func resolveGenerationDescription(deviceID int, deviceToGeneration map[string]string) string { if deviceID <= 0 || len(deviceToGeneration) == 0 { return "" } return strings.TrimSpace(deviceToGeneration[normalizeDeviceIDHex(strconv.FormatInt(int64(deviceID), 16))]) } func normalizeDeviceIDHex(v string) string { s := strings.TrimSpace(strings.ToLower(v)) s = strings.TrimPrefix(s, "0x") if s == "" { return "" } n, err := strconv.ParseUint(s, 16, 32) if err != nil { return "" } return "0x" + strings.ToLower(strconv.FormatUint(n, 16)) }