feat: improve inspur parsing and pci.ids integration
This commit is contained in:
+559
@@ -0,0 +1,559 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user