package inspur import ( "encoding/hex" "regexp" "sort" "strconv" "strings" "unicode" "git.mchus.pro/mchus/logpile/internal/models" ) var ( reRedisGPUKey = regexp.MustCompile(`GPUInfo:REDIS_GPUINFO_T([0-9]+):([A-Za-z0-9_]+)`) reRedisNICKey = regexp.MustCompile(`RedisNicInfo:redis_nic_info_t:stNicDeviceInfo([0-9]+):([A-Za-z0-9_]+)`) reRedisRAIDSerial = regexp.MustCompile(`RAIDMSCCInfo:redis_pcie_mscc_raid_info_t([0-9]+):RAIDInfo:SerialNum`) reRedisPCIESNPN = regexp.MustCompile(`AssetInfoPCIE:SNPN([0-9]+):(SN|PN)`) ) type redisGPUSnapshot struct { ByIndex map[int]map[string]string } type redisNICSnapshot struct { ByIndex map[int]map[string]string } type redisPCIESerialSnapshot struct { ByPart map[string]string } func enrichFromRedisDump(content []byte, hw *models.HardwareConfig) { if hw == nil || len(content) == 0 { return } gpuSnap := parseRedisGPUSnapshot(content) nicSnap := parseRedisNICSnapshot(content) raidSerials := parseRedisRAIDSerials(content) pcieSnap := parseRedisPCIESerialSnapshot(content) applyRedisGPUEnrichment(hw, gpuSnap) applyRedisNICEnrichment(hw, nicSnap) applyRedisPCIESNPNEnrichment(hw, pcieSnap) applyRedisPCIeEnrichment(hw, raidSerials) } func parseRedisRAIDSerials(content []byte) []string { matches := reRedisRAIDSerial.FindAllSubmatchIndex(content, -1) if len(matches) == 0 { return nil } seen := make(map[string]bool, len(matches)) serials := make([]string, 0, len(matches)) for _, m := range matches { if len(m) < 4 { continue } value := normalizeRedisValue(extractRedisCandidateValue(content, m[1])) if value == "" || seen[value] { continue } seen[value] = true serials = append(serials, value) } return serials } func parseRedisPCIESerialSnapshot(content []byte) redisPCIESerialSnapshot { type rec struct { PN string SN string } tmp := make(map[int]rec) matches := reRedisPCIESNPN.FindAllSubmatchIndex(content, -1) for _, m := range matches { if len(m) < 6 { continue } idxStr := string(content[m[2]:m[3]]) field := string(content[m[4]:m[5]]) idx, err := strconv.Atoi(idxStr) if err != nil { continue } value := normalizeRedisValue(extractRedisCandidateValue(content, m[1])) if value == "" { continue } r := tmp[idx] if field == "PN" { r.PN = value } else if field == "SN" { r.SN = value } tmp[idx] = r } out := redisPCIESerialSnapshot{ByPart: make(map[string]string)} for _, r := range tmp { pn := normalizeRedisValue(r.PN) sn := normalizeRedisValue(r.SN) if pn == "" || sn == "" { continue } out.ByPart[strings.ToLower(strings.TrimSpace(pn))] = sn } return out } func parseRedisGPUSnapshot(content []byte) redisGPUSnapshot { snap := redisGPUSnapshot{ByIndex: make(map[int]map[string]string)} matches := reRedisGPUKey.FindAllSubmatchIndex(content, -1) for _, m := range matches { if len(m) < 6 { continue } idxStr := string(content[m[2]:m[3]]) field := string(content[m[4]:m[5]]) idx, err := strconv.Atoi(idxStr) if err != nil { continue } value := extractRedisInlineValue(content, m[1]) if value == "" { continue } byField, ok := snap.ByIndex[idx] if !ok { byField = make(map[string]string) snap.ByIndex[idx] = byField } byField[field] = value } return snap } func parseRedisNICSnapshot(content []byte) redisNICSnapshot { snap := redisNICSnapshot{ByIndex: make(map[int]map[string]string)} matches := reRedisNICKey.FindAllSubmatchIndex(content, -1) for _, m := range matches { if len(m) < 6 { continue } idxStr := string(content[m[2]:m[3]]) field := string(content[m[4]:m[5]]) idx, err := strconv.Atoi(idxStr) if err != nil { continue } value := extractRedisInlineValue(content, m[1]) if value == "" { continue } byField, ok := snap.ByIndex[idx] if !ok { byField = make(map[string]string) snap.ByIndex[idx] = byField } byField[field] = value } return snap } func extractRedisInlineValue(content []byte, start int) string { if start < 0 || start >= len(content) { return "" } i := start for i < len(content) && content[i] <= 0x20 { i++ } if i >= len(content) { return "" } j := i for j < len(content) { c := content[j] if c == 0 || c < 0x20 || c > 0x7e { break } j++ } if j <= i { return "" } raw := strings.TrimSpace(string(content[i:j])) if raw == "" { return "" } decoded := maybeDecodeHexString(raw) if decoded != "" { return decoded } return raw } func extractRedisCandidateValue(content []byte, start int) string { // Fast-path for simple inline string values. if v := extractRedisInlineValue(content, start); normalizeRedisValue(v) != "" { return v } if start < 0 || start >= len(content) { return "" } end := start + 256 if end > len(content) { end = len(content) } window := content[start:end] for _, token := range splitAlphaNumTokens(window) { if len(token) < 6 { continue } lower := strings.ToLower(token) if strings.Contains(lower, "redis") || strings.Contains(lower, "sensor") || strings.Contains(lower, "fullsdr") { continue } if decoded := maybeDecodeHexString(token); normalizeRedisValue(decoded) != "" { return decoded } if normalizeRedisValue(token) != "" { return token } } return "" } func splitAlphaNumTokens(b []byte) []string { var out []string start := -1 for i := 0; i < len(b); i++ { c := rune(b[i]) if unicode.IsLetter(c) || unicode.IsDigit(c) { if start == -1 { start = i } continue } if start != -1 { out = append(out, string(b[start:i])) start = -1 } } if start != -1 { out = append(out, string(b[start:])) } return out } func maybeDecodeHexString(s string) string { if len(s) < 8 || len(s)%2 != 0 { return "" } for _, c := range s { if (c < '0' || c > '9') && (c < 'a' || c > 'f') && (c < 'A' || c > 'F') { return "" } } b, err := hex.DecodeString(s) if err != nil { return "" } decoded := strings.TrimSpace(strings.TrimRight(string(b), "\x00")) if decoded == "" { return "" } for _, c := range decoded { if c < 0x20 || c > 0x7e { return "" } } return decoded } func applyRedisGPUEnrichment(hw *models.HardwareConfig, snap redisGPUSnapshot) { if len(hw.GPUs) == 0 || len(snap.ByIndex) == 0 { return } type redisGPU struct { Index int Data map[string]string } redisGPUs := make([]redisGPU, 0, len(snap.ByIndex)) for idx, data := range snap.ByIndex { if data == nil { continue } if data["NV_GPU_SerialNumber"] == "" && data["NV_GPU_FWVersion"] == "" && data["NV_GPU_UUID"] == "" { continue } redisGPUs = append(redisGPUs, redisGPU{Index: idx, Data: data}) } if len(redisGPUs) == 0 { return } sort.Slice(redisGPUs, func(i, j int) bool { return redisGPUs[i].Index < redisGPUs[j].Index }) target := make([]*models.GPU, 0, len(hw.GPUs)) for i := range hw.GPUs { gpu := &hw.GPUs[i] if isNVIDIAGPU(gpu) { target = append(target, gpu) } } if len(target) == 0 || len(target) != len(redisGPUs) { return } sort.Slice(target, func(i, j int) bool { left := strings.TrimSpace(target[i].BDF) right := strings.TrimSpace(target[j].BDF) if left != "" && right != "" { return left < right } return strings.TrimSpace(target[i].Slot) < strings.TrimSpace(target[j].Slot) }) for i := range target { applyRedisGPUFields(target[i], redisGPUs[i].Data) } } func isNVIDIAGPU(gpu *models.GPU) bool { if gpu == nil { return false } if gpu.VendorID == 0x10de { return true } man := strings.ToLower(strings.TrimSpace(gpu.Manufacturer)) return strings.Contains(man, "nvidia") } func applyRedisGPUFields(gpu *models.GPU, fields map[string]string) { if gpu == nil || fields == nil { return } if serial := normalizeRedisValue(fields["NV_GPU_SerialNumber"]); serial != "" && isMissingGPUField(gpu.SerialNumber) { gpu.SerialNumber = serial } if fw := normalizeRedisValue(fields["NV_GPU_FWVersion"]); fw != "" && isMissingGPUField(gpu.Firmware) { gpu.Firmware = fw } if uuid := normalizeRedisValue(fields["NV_GPU_UUID"]); uuid != "" && isMissingGPUField(gpu.UUID) { gpu.UUID = uuid } if part := normalizeRedisValue(fields["NVGPUPartNumber"]); part != "" && isMissingGPUField(gpu.PartNumber) { gpu.PartNumber = part } if model := normalizeRedisValue(fields["NVGPUMarketingName"]); model != "" && isGenericGPUModel(gpu.Model) { gpu.Model = model } if gpu.ClockSpeed == 0 { if mhz, ok := parseIntField(fields["OperatingSpeedMHz"]); ok { gpu.ClockSpeed = mhz } } if gpu.Power == 0 { if pwr, ok := parseIntField(fields["GPUTotalPower"]); ok { gpu.Power = pwr } } if gpu.Temperature == 0 { if temp, ok := parseIntField(fields["Temp"]); ok { gpu.Temperature = temp } } if gpu.MemTemperature == 0 { if temp, ok := parseIntField(fields["MemTemp"]); ok { gpu.MemTemperature = temp } } } func parseIntField(v string) (int, bool) { v = normalizeRedisValue(v) if v == "" { return 0, false } n, err := strconv.Atoi(v) if err != nil { return 0, false } return n, true } func normalizeRedisValue(v string) string { v = strings.TrimSpace(v) if v == "" { return "" } l := strings.ToLower(v) if l == "n/a" || l == "na" || l == "null" || l == "unknown" { return "" } return v } func isMissingGPUField(v string) bool { return normalizeRedisValue(v) == "" } func isGenericGPUModel(model string) bool { m := strings.ToLower(strings.TrimSpace(model)) switch m { case "", "unknown", "display", "display controller", "3d controller", "vga", "gpu": return true default: return false } } func applyRedisNICEnrichment(hw *models.HardwareConfig, snap redisNICSnapshot) { if len(hw.NetworkAdapters) == 0 || len(snap.ByIndex) == 0 { return } type redisNIC struct { Index int Data map[string]string } redisNICs := make([]redisNIC, 0, len(snap.ByIndex)) for idx, data := range snap.ByIndex { if data == nil { continue } if normalizeRedisValue(data["FWVersion"]) == "" { continue } redisNICs = append(redisNICs, redisNIC{Index: idx, Data: data}) } if len(redisNICs) == 0 { return } sort.Slice(redisNICs, func(i, j int) bool { return redisNICs[i].Index < redisNICs[j].Index }) target := make([]*models.NetworkAdapter, 0, len(hw.NetworkAdapters)) for i := range hw.NetworkAdapters { nic := &hw.NetworkAdapters[i] if nic.Present { target = append(target, nic) } } if len(target) == 0 { return } sort.Slice(target, func(i, j int) bool { left := strings.TrimSpace(target[i].Location) right := strings.TrimSpace(target[j].Location) if left != "" && right != "" { return left < right } return strings.TrimSpace(target[i].Slot) < strings.TrimSpace(target[j].Slot) }) limit := len(target) if len(redisNICs) < limit { limit = len(redisNICs) } for i := 0; i < limit; i++ { nic := target[i] data := redisNICs[i].Data if fw := normalizeRedisValue(data["FWVersion"]); fw != "" && normalizeRedisValue(nic.Firmware) == "" { nic.Firmware = fw } if serial := normalizeRedisValue(data["SerialNum"]); serial != "" && normalizeRedisValue(nic.SerialNumber) == "" { nic.SerialNumber = serial } if part := normalizeRedisValue(data["PartNum"]); part != "" && normalizeRedisValue(nic.PartNumber) == "" { nic.PartNumber = part } } } func applyRedisPCIeEnrichment(hw *models.HardwareConfig, raidSerials []string) { if hw == nil || len(hw.PCIeDevices) == 0 || len(raidSerials) == 0 { return } target := make([]*models.PCIeDevice, 0, len(hw.PCIeDevices)) for i := range hw.PCIeDevices { dev := &hw.PCIeDevices[i] if normalizeRedisValue(dev.SerialNumber) != "" { continue } class := strings.ToLower(strings.TrimSpace(dev.DeviceClass)) part := strings.ToLower(strings.TrimSpace(dev.PartNumber)) if strings.Contains(class, "raid") || strings.Contains(class, "sas") || strings.Contains(class, "storage") || strings.Contains(part, "raid") || strings.Contains(part, "sas") || strings.Contains(part, "hba") { target = append(target, dev) } } if len(target) == 0 { return } sort.Slice(target, func(i, j int) bool { left := strings.TrimSpace(target[i].BDF) right := strings.TrimSpace(target[j].BDF) if left != "" && right != "" { return left < right } return strings.TrimSpace(target[i].Slot) < strings.TrimSpace(target[j].Slot) }) limit := len(target) if len(raidSerials) < limit { limit = len(raidSerials) } for i := 0; i < limit; i++ { target[i].SerialNumber = raidSerials[i] } } func applyRedisPCIESNPNEnrichment(hw *models.HardwareConfig, snap redisPCIESerialSnapshot) { if hw == nil || len(hw.PCIeDevices) == 0 || len(snap.ByPart) == 0 { return } for i := range hw.PCIeDevices { dev := &hw.PCIeDevices[i] if normalizeRedisValue(dev.SerialNumber) != "" { continue } part := strings.ToLower(strings.TrimSpace(dev.PartNumber)) if part == "" { continue } if serial := normalizeRedisValue(snap.ByPart[part]); serial != "" { dev.SerialNumber = serial } } }