package inspurlegacy import ( "regexp" "strconv" "strings" "git.mchus.pro/mchus/logpile/internal/models" ) // ParseAssetInfoInventory reads Inspur_AssetInfoInventory.log and fills CPU, // memory, PCIe and power-supply inventory. The file is organised in bracketed // sections ("[CPU]", "[Memory]", "[PCIE]", "[PSU]"); each data line is // ": k:v, k:v, ...". func ParseAssetInfoInventory(content []byte, hw *models.HardwareConfig) { section := "" for _, rawLine := range strings.Split(string(content), "\n") { line := strings.TrimSpace(rawLine) if line == "" { continue } if strings.HasPrefix(line, "[") && strings.HasSuffix(line, "]") { section = strings.ToUpper(strings.Trim(line, "[]")) continue } key, rest, ok := strings.Cut(line, ":") if !ok { continue } key = strings.TrimSpace(key) kv := parseAssetKVList(rest) switch section { case "CPU": if cpu, ok := assetCPU(key, kv); ok { hw.CPUs = append(hw.CPUs, cpu) } case "MEMORY": if dimm, ok := assetDIMM(key, kv); ok { hw.Memory = append(hw.Memory, dimm) } case "PCIE": if dev, ok := assetPCIe(kv); ok { hw.PCIeDevices = append(hw.PCIeDevices, dev) } case "PSU": if psu, ok := assetPSU(key, kv); ok { hw.PowerSupply = append(hw.PowerSupply, psu) } } } } // parseAssetKVList splits "k:v, k:v" into a map. Values may contain parenthesised // text and colons (e.g. "Model:Intel(R) Xeon(R) ... @ 2.90GHz"); only the first // colon of each comma-separated segment is treated as the separator, and commas // inside parentheses do not split. func parseAssetKVList(s string) map[string]string { out := make(map[string]string) for _, seg := range splitTopLevelComma(s) { seg = strings.TrimSpace(seg) if seg == "" { continue } // Most pairs use "k:v"; the PCIe bus address uses "B.D.F=x.y.z". sep := ":" if !strings.Contains(seg, ":") && strings.Contains(seg, "=") { sep = "=" } k, v, ok := strings.Cut(seg, sep) if !ok { continue } out[strings.TrimSpace(k)] = strings.TrimSpace(v) } return out } func splitTopLevelComma(s string) []string { var out []string depth := 0 start := 0 for i, r := range s { switch r { case '(', '[': depth++ case ')', ']': if depth > 0 { depth-- } case ',': if depth == 0 { out = append(out, s[start:i]) start = i + 1 } } } out = append(out, s[start:]) return out } var ( parenValueRe = regexp.MustCompile(`^([^(]*)\(([^)]*)\)`) sizeGBRe = regexp.MustCompile(`(\d+)\s*GB`) freqMHzRe = regexp.MustCompile(`(\d+)\s*MHz`) wattRe = regexp.MustCompile(`(\d+)\s*W`) ghzRe = regexp.MustCompile(`@\s*([\d.]+)\s*GHz`) trailingIdx = regexp.MustCompile(`_(\d+)$`) ) // outerParen returns the text between the first "(" and the last ")" of v, so // "3(PCIE X24 SLOT0(J99) CPU0)" yields "PCIE X24 SLOT0(J99) CPU0". func outerParen(v string) string { open := strings.Index(v, "(") shut := strings.LastIndex(v, ")") if open < 0 || shut < 0 || shut <= open+1 { return "" } return strings.TrimSpace(v[open+1 : shut]) } // splitParenValue splits "26(DDR4)" into ("26", "DDR4"). func splitParenValue(v string) (before, inParen string) { if m := parenValueRe.FindStringSubmatch(v); m != nil { return strings.TrimSpace(m[1]), strings.TrimSpace(m[2]) } return strings.TrimSpace(v), "" } func atoiSafe(s string) int { n, _ := strconv.Atoi(strings.TrimSpace(s)) return n } // parseHexID parses "0x15B3" into 5555. func parseHexID(s string) int { s = strings.TrimSpace(s) s = strings.TrimPrefix(strings.TrimPrefix(s, "0x"), "0X") n, err := strconv.ParseInt(s, 16, 64) if err != nil { return 0 } return int(n) } func assetCPU(key string, kv map[string]string) (models.CPU, bool) { model := kv["Model"] if model == "" { return models.CPU{}, false } cpu := models.CPU{ Socket: socketFromKey(key), Model: model, Cores: atoiSafe(kv["Core"]), TDP: atoiSafe(strings.TrimSuffix(kv["TDP"], "W")), } if m := ghzRe.FindStringSubmatch(model); m != nil { if ghz, err := strconv.ParseFloat(m[1], 64); err == nil { cpu.FrequencyMHz = int(ghz * 1000) } } return cpu, true } func socketFromKey(key string) int { if m := trailingIdx.FindStringSubmatch(key); m != nil { return atoiSafe(m[1]) } return 0 } func assetDIMM(key string, kv map[string]string) (models.MemoryDIMM, bool) { sizeGB := 0 if m := sizeGBRe.FindStringSubmatch(kv["Size"]); m != nil { sizeGB = atoiSafe(m[1]) } if sizeGB == 0 { return models.MemoryDIMM{}, false } _, memType := splitParenValue(kv["Type"]) speed := 0 if m := freqMHzRe.FindStringSubmatch(kv["Freq"]); m != nil { speed = atoiSafe(m[1]) } // "MEM_0_CPU0_C0D0" -> slot "CPU0_C0D0", drop the "MEM__" prefix. slot := key if parts := strings.SplitN(key, "_", 3); len(parts) == 3 { slot = parts[2] } return models.MemoryDIMM{ Slot: slot, Location: key, Present: true, SizeMB: sizeGB * 1024, Type: memType, MaxSpeedMHz: speed, CurrentSpeedMHz: speed, Manufacturer: kv["Manufacturer"], PartNumber: kv["PN"], SerialNumber: kv["SN"], }, true } func assetPCIe(kv map[string]string) (models.PCIeDevice, bool) { if len(kv) == 0 { return models.PCIeDevice{}, false } _, class := splitParenValue(kv["Type"]) vendHex, vendName := splitParenValue(kv["VendorID"]) devHex, devName := splitParenValue(kv["DevieID"]) // "DevieID" spelled as in source if devName == "" { _, devName = splitParenValue(kv["DeviceID"]) devHex, _ = splitParenValue(kv["DeviceID"]) } widthNum, _ := splitParenValue(kv["Width"]) _, speedName := splitParenValue(kv["Speed"]) slotName := outerParen(kv["Slot"]) if slotName == "" { slotName = strings.TrimSpace(kv["Slot"]) } dev := models.PCIeDevice{ Slot: slotName, DeviceClass: class, Manufacturer: vendName, Model: devName, VendorID: parseHexID(vendHex), DeviceID: parseHexID(devHex), BDF: bdfFromDecimal(kv["B.D.F"]), LinkWidth: atoiSafe(widthNum), LinkSpeed: speedName, } if dev.DeviceClass == "" && dev.BDF == "" && dev.Model == "" { return models.PCIeDevice{}, false } return dev, true } // bdfFromDecimal converts the AssetInfo "bus.dev.func" decimal triple into the // canonical hex "0000:bb:dd.f" BDF. Domain is not reported and assumed 0. func bdfFromDecimal(s string) string { parts := strings.Split(strings.TrimSpace(s), ".") if len(parts) != 3 { return "" } bus := atoiSafe(parts[0]) dev := atoiSafe(parts[1]) fn := atoiSafe(parts[2]) return strings.ToLower( zeroPadHex(0, 4) + ":" + zeroPadHex(bus, 2) + ":" + zeroPadHex(dev, 2) + "." + strconv.Itoa(fn), ) } func zeroPadHex(n, width int) string { h := strconv.FormatInt(int64(n), 16) for len(h) < width { h = "0" + h } return h } func assetPSU(key string, kv map[string]string) (models.PSU, bool) { model := kv["Model"] vendor := kv["Manufacturer"] if model == "" && vendor == "" { return models.PSU{}, false } watt := 0 if m := wattRe.FindStringSubmatch(kv["RatedPower"]); m != nil { watt = atoiSafe(m[1]) } slot := strings.ReplaceAll(key, "_", "") return models.PSU{ Slot: slot, Present: true, Model: model, Vendor: vendor, SerialNumber: kv["SN"], PartNumber: kv["PN"], Firmware: kv["FW"], WattageW: watt, }, true }