package inspur import ( "regexp" "sort" "strconv" "strings" "git.mchus.pro/mchus/logpile/internal/models" "git.mchus.pro/mchus/logpile/internal/parser" ) // solSmartdDeviceRe matches smartd device info lines from SOLHostCapture.log. // Example: // // Device: /dev/sda [SAT], Micron_5400_MTFDDAK480TGA, S/N:2310400DC7E3, WWN:..., FW:D4CM003, 480 GB var solSmartdDeviceRe = regexp.MustCompile( `Device: /dev/\S+ \[SAT\], (.+?), S/N:(\S+),.*?FW:(\S+), ([\d.]+) (GB|TB)`, ) type solSmartdDevice struct { Model string Serial string Firmware string SizeGB int } // parseSOLSmartdDevices extracts unique disk entries from SOLHostCapture.log content. // Deduplicates by serial number (case-insensitive); preserves first-seen order. func parseSOLSmartdDevices(content []byte) []solSmartdDevice { seen := make(map[string]struct{}) var out []solSmartdDevice for _, line := range strings.Split(string(content), "\n") { m := solSmartdDeviceRe.FindStringSubmatch(line) if m == nil { continue } serial := strings.TrimSpace(m[2]) if serial == "" { continue } key := strings.ToLower(serial) if _, ok := seen[key]; ok { continue } seen[key] = struct{}{} sizeGB := parseSolSizeGB(m[4], m[5]) out = append(out, solSmartdDevice{ Model: strings.TrimSpace(m[1]), Serial: serial, Firmware: strings.TrimSpace(m[3]), SizeGB: sizeGB, }) } return out } // parseSolSizeGB converts smartd size string ("480", "3.84") + unit ("GB", "TB") to integer GB. // Uses decimal TB (1 TB = 1000 GB) matching disk manufacturer conventions. func parseSolSizeGB(value, unit string) int { f, err := strconv.ParseFloat(value, 64) if err != nil || f <= 0 { return 0 } if strings.EqualFold(unit, "TB") { f *= 1000 } return int(f + 0.5) } // enrichStorageFromSOLSmartd enriches the storage inventory using disk info from // SOLHostCapture.log (smartd startup messages). Both the log/ and runningdata/ copies // are processed; serials are deduplicated across both files. // // Enrichment priority: // 1. Exact model match to existing entries that are missing a serial. // 2. Positional assignment to present placeholder slots (no model, no serial). // 3. New entries added for any remaining devices. func enrichStorageFromSOLSmartd(files []parser.ExtractedFile, hw *models.HardwareConfig) { if hw == nil { return } solFiles := parser.FindFileByPattern(files, "SOLHostCapture.log") if len(solFiles) == 0 { return } // Collect unique devices from all SOL log copies. seenSerial := make(map[string]struct{}) var devices []solSmartdDevice for _, f := range solFiles { for _, d := range parseSOLSmartdDevices(f.Content) { key := strings.ToLower(d.Serial) if _, ok := seenSerial[key]; ok { continue } seenSerial[key] = struct{}{} devices = append(devices, d) } } if len(devices) == 0 { return } // Skip devices whose serial already appears in the storage inventory. existingSerials := make(map[string]struct{}, len(hw.Storage)) for _, dev := range hw.Storage { sn := strings.ToLower(strings.TrimSpace(dev.SerialNumber)) if sn != "" { existingSerials[sn] = struct{}{} } } var newDevices []solSmartdDevice for _, d := range devices { if _, ok := existingSerials[strings.ToLower(d.Serial)]; !ok { newDevices = append(newDevices, d) } } if len(newDevices) == 0 { return } // Pass 1: enrich existing entries that match by model (first-match wins per device). remaining := solEnrichByModel(hw, newDevices) if len(remaining) == 0 { return } // Pass 2: assign to present placeholder slots (present=true, no model, no serial). remaining = solEnrichByPlaceholder(hw, remaining) if len(remaining) == 0 { return } // Pass 3: add as new storage entries without a slot assignment. for _, d := range remaining { hw.Storage = append(hw.Storage, solMakeStorage(d)) } } // solEnrichByModel fills SerialNumber (and optionally Firmware/SizeGB) on existing storage // entries whose model matches the smartd model exactly. Returns unmatched devices. func solEnrichByModel(hw *models.HardwareConfig, devices []solSmartdDevice) []solSmartdDevice { var unmatched []solSmartdDevice for _, d := range devices { matched := false for i := range hw.Storage { if strings.TrimSpace(hw.Storage[i].SerialNumber) != "" { continue } if !strings.EqualFold(strings.TrimSpace(hw.Storage[i].Model), d.Model) { continue } hw.Storage[i].SerialNumber = d.Serial if strings.TrimSpace(hw.Storage[i].Firmware) == "" { hw.Storage[i].Firmware = d.Firmware } if hw.Storage[i].SizeGB == 0 { hw.Storage[i].SizeGB = d.SizeGB } matched = true break } if !matched { unmatched = append(unmatched, d) } } return unmatched } // solEnrichByPlaceholder assigns smartd devices to present storage entries that have // neither a model nor a serial number, sorted by slot name. Returns unmatched devices. func solEnrichByPlaceholder(hw *models.HardwareConfig, devices []solSmartdDevice) []solSmartdDevice { type slot struct { index int name string } var placeholders []slot for i := range hw.Storage { if !hw.Storage[i].Present { continue } if strings.TrimSpace(hw.Storage[i].SerialNumber) != "" { continue } if strings.TrimSpace(hw.Storage[i].Model) != "" { continue } placeholders = append(placeholders, slot{index: i, name: hw.Storage[i].Slot}) } sort.Slice(placeholders, func(i, j int) bool { return placeholders[i].name < placeholders[j].name }) pi := 0 var unmatched []solSmartdDevice for _, d := range devices { if pi >= len(placeholders) { unmatched = append(unmatched, d) continue } idx := placeholders[pi].index pi++ hw.Storage[idx].SerialNumber = d.Serial hw.Storage[idx].Model = d.Model hw.Storage[idx].Firmware = d.Firmware if hw.Storage[idx].SizeGB == 0 { hw.Storage[idx].SizeGB = d.SizeGB } hw.Storage[idx].Type = solStorageType(d.Model) if hw.Storage[idx].Manufacturer == "" { hw.Storage[idx].Manufacturer = extractStorageManufacturer(d.Model) } if hw.Storage[idx].Interface == "" { hw.Storage[idx].Interface = "SATA" } } return unmatched } func solMakeStorage(d solSmartdDevice) models.Storage { return models.Storage{ Model: d.Model, SerialNumber: d.Serial, Firmware: d.Firmware, SizeGB: d.SizeGB, Type: solStorageType(d.Model), Manufacturer: extractStorageManufacturer(d.Model), Interface: "SATA", Present: true, } } // solStorageType infers SSD vs HDD from the model string. // Micron SSD models start with "MTFDD"; Intel SSDs contain "SSD". func solStorageType(model string) string { upper := strings.ToUpper(model) if strings.Contains(upper, "SSD") || strings.HasPrefix(upper, "MTFDD") || strings.HasPrefix(upper, "MICRON_5") { return "SSD" } return "HDD" }