From 4ecf762f5712f228e08320fc120fe82e6040abb5 Mon Sep 17 00:00:00 2001 From: "Harper, Jason M" Date: Tue, 16 Dec 2025 11:32:00 -0800 Subject: [PATCH] more descriptive representation of installed memory in summary table Signed-off-by: Harper, Jason M --- cmd/report/dimm.go | 101 +++++-------------------------- cmd/report/report_tables.go | 56 ++++++++--------- cmd/report/system.go | 2 +- internal/common/table_defs.go | 7 ++- internal/common/table_helpers.go | 69 +++++++++++++++++++++ 5 files changed, 120 insertions(+), 115 deletions(-) diff --git a/cmd/report/dimm.go b/cmd/report/dimm.go index 87fa821a..c9140e70 100644 --- a/cmd/report/dimm.go +++ b/cmd/report/dimm.go @@ -13,85 +13,16 @@ import ( "strings" ) -const ( - BankLocatorIdx = iota - LocatorIdx - ManufacturerIdx - PartIdx - SerialIdx - SizeIdx - TypeIdx - DetailIdx - SpeedIdx - RankIdx - ConfiguredSpeedIdx - DerivedSocketIdx - DerivedChannelIdx - DerivedSlotIdx -) - -func dimmInfoFromDmiDecode(dmiDecodeOutput string) [][]string { - return common.ValsArrayFromDmiDecodeRegexSubmatch( - dmiDecodeOutput, - "17", - `^Bank Locator:\s*(.+?)$`, - `^Locator:\s*(.+?)$`, - `^Manufacturer:\s*(.+?)$`, - `^Part Number:\s*(.+?)\s*$`, - `^Serial Number:\s*(.+?)\s*$`, - `^Size:\s*(.+?)$`, - `^Type:\s*(.+?)$`, - `^Type Detail:\s*(.+?)$`, - `^Speed:\s*(.+?)$`, - `^Rank:\s*(.+?)$`, - `^Configured.*Speed:\s*(.+?)$`, - ) -} - -func installedMemoryFromOutput(outputs map[string]script.ScriptOutput) string { - dimmInfo := dimmInfoFromDmiDecode(outputs[script.DmidecodeScriptName].Stdout) - dimmTypeCount := make(map[string]int) - for _, dimm := range dimmInfo { - dimmKey := dimm[TypeIdx] + ":" + dimm[SizeIdx] + ":" + dimm[SpeedIdx] + ":" + dimm[ConfiguredSpeedIdx] - if count, ok := dimmTypeCount[dimmKey]; ok { - dimmTypeCount[dimmKey] = count + 1 - } else { - dimmTypeCount[dimmKey] = 1 - } - } - var summaries []string - re := regexp.MustCompile(`(\d+)\s*(\w*)`) - for dimmKey, count := range dimmTypeCount { - fields := strings.Split(dimmKey, ":") - match := re.FindStringSubmatch(fields[1]) // size field - if match != nil { - size, err := strconv.Atoi(match[1]) - if err != nil { - slog.Warn("Don't recognize DIMM size format.", slog.String("field", fields[1])) - return "" - } - sum := count * size - unit := match[2] - dimmType := fields[0] - speed := strings.ReplaceAll(fields[2], " ", "") - configuredSpeed := strings.ReplaceAll(fields[3], " ", "") - summary := fmt.Sprintf("%d%s (%dx%d%s %s %s [%s])", sum, unit, count, size, unit, dimmType, speed, configuredSpeed) - summaries = append(summaries, summary) - } - } - return strings.Join(summaries, "; ") -} - func populatedChannelsFromOutput(outputs map[string]script.ScriptOutput) string { channelsMap := make(map[string]bool) - dimmInfo := dimmInfoFromDmiDecode(outputs[script.DmidecodeScriptName].Stdout) + dimmInfo := common.DimmInfoFromDmiDecode(outputs[script.DmidecodeScriptName].Stdout) derivedDimmFields := derivedDimmsFieldFromOutput(outputs) if len(derivedDimmFields) != len(dimmInfo) { slog.Warn("derivedDimmFields and dimmInfo have different lengths", slog.Int("derivedDimmFields", len(derivedDimmFields)), slog.Int("dimmInfo", len(dimmInfo))) return "" } for i, dimm := range dimmInfo { - if !strings.Contains(dimm[SizeIdx], "No") { + if !strings.Contains(dimm[common.SizeIdx], "No") { channelsMap[derivedDimmFields[i].socket+","+derivedDimmFields[i].channel] = true } } @@ -109,7 +40,7 @@ type derivedFields struct { // derivedDimmsFieldFromOutput returns a slice of derived fields from the output of a script. func derivedDimmsFieldFromOutput(outputs map[string]script.ScriptOutput) []derivedFields { - dimmInfo := dimmInfoFromDmiDecode(outputs[script.DmidecodeScriptName].Stdout) + dimmInfo := common.DimmInfoFromDmiDecode(outputs[script.DmidecodeScriptName].Stdout) var derivedFields []derivedFields var err error channels := channelsFromOutput(outputs) @@ -159,11 +90,11 @@ func deriveDIMMInfoDell(dimms [][]string, channelsPerSocket int) ([]derivedField derivedFields := make([]derivedFields, len(dimms)) re := regexp.MustCompile(`([ABCD])([1-9]\d*)`) for i, dimm := range dimms { - if !strings.Contains(dimm[BankLocatorIdx], "Not Specified") { + if !strings.Contains(dimm[common.BankLocatorIdx], "Not Specified") { err := fmt.Errorf("doesn't conform to expected Dell Bank Locator format") return nil, err } - match := re.FindStringSubmatch(dimm[LocatorIdx]) + match := re.FindStringSubmatch(dimm[common.LocatorIdx]) if match == nil { err := fmt.Errorf("doesn't conform to expected Dell Locator format") return nil, err @@ -223,8 +154,8 @@ func deriveDIMMInfoEC2(dimms [][]string, channelsPerSocket int) ([]derivedFields c6ilocRe := regexp.MustCompile(`CPU(\d+)\s+Channel(\d+)\s+DIMM(\d+)`) for i, dimm := range dimms { // try c5.metal format - bankLocMatch := c5bankLocRe.FindStringSubmatch(dimm[BankLocatorIdx]) - locMatch := c5locRe.FindStringSubmatch(dimm[LocatorIdx]) + bankLocMatch := c5bankLocRe.FindStringSubmatch(dimm[common.BankLocatorIdx]) + locMatch := c5locRe.FindStringSubmatch(dimm[common.LocatorIdx]) if locMatch != nil && bankLocMatch != nil { var socket, channel, slot int socket, _ = strconv.Atoi(bankLocMatch[1]) @@ -244,8 +175,8 @@ func deriveDIMMInfoEC2(dimms [][]string, channelsPerSocket int) ([]derivedFields continue } // try c6i.metal format - bankLocMatch = c6ibankLocRe.FindStringSubmatch(dimm[BankLocatorIdx]) - locMatch = c6ilocRe.FindStringSubmatch(dimm[LocatorIdx]) + bankLocMatch = c6ibankLocRe.FindStringSubmatch(dimm[common.BankLocatorIdx]) + locMatch = c6ilocRe.FindStringSubmatch(dimm[common.LocatorIdx]) if locMatch != nil && bankLocMatch != nil { var socket, channel, slot int socket, _ = strconv.Atoi(locMatch[1]) @@ -271,13 +202,13 @@ func deriveDIMMInfoHPE(dimms [][]string, numSockets int, channelsPerSocket int) slotsPerChannel := len(dimms) / (numSockets * channelsPerSocket) re := regexp.MustCompile(`PROC ([1-9]\d*) DIMM ([1-9]\d*)`) for i, dimm := range dimms { - if !strings.Contains(dimm[BankLocatorIdx], "Not Specified") { - err := fmt.Errorf("doesn't conform to expected HPE Bank Locator format: %s", dimm[BankLocatorIdx]) + if !strings.Contains(dimm[common.BankLocatorIdx], "Not Specified") { + err := fmt.Errorf("doesn't conform to expected HPE Bank Locator format: %s", dimm[common.BankLocatorIdx]) return nil, err } - match := re.FindStringSubmatch(dimm[LocatorIdx]) + match := re.FindStringSubmatch(dimm[common.LocatorIdx]) if match == nil { - err := fmt.Errorf("doesn't conform to expected HPE Locator format: %s", dimm[LocatorIdx]) + err := fmt.Errorf("doesn't conform to expected HPE Locator format: %s", dimm[common.LocatorIdx]) return nil, err } socket, err := strconv.Atoi(match[1]) @@ -625,18 +556,18 @@ func deriveDIMMInfoOther(dimms [][]string, channelsPerSocket int) ([]derivedFiel err := fmt.Errorf("no DIMMs") return nil, err } - if len(dimms[0]) <= max(BankLocatorIdx, LocatorIdx) { + if len(dimms[0]) <= max(common.BankLocatorIdx, common.LocatorIdx) { err := fmt.Errorf("DIMM data has insufficient fields") return nil, err } - dimmType, reBankLoc, reLoc := getDIMMParseInfo(dimms[0][BankLocatorIdx], dimms[0][LocatorIdx]) + dimmType, reBankLoc, reLoc := getDIMMParseInfo(dimms[0][common.BankLocatorIdx], dimms[0][common.LocatorIdx]) if dimmType == DIMMTypeUNKNOWN { err := fmt.Errorf("unknown DIMM identification format") return nil, err } for i, dimm := range dimms { var socket, slot int - socket, slot, err := getDIMMSocketSlot(dimmType, reBankLoc, reLoc, dimm[BankLocatorIdx], dimm[LocatorIdx]) + socket, slot, err := getDIMMSocketSlot(dimmType, reBankLoc, reLoc, dimm[common.BankLocatorIdx], dimm[common.LocatorIdx]) if err != nil { slog.Info("Couldn't extract socket and slot from DIMM info", slog.String("error", err.Error())) return nil, nil diff --git a/cmd/report/report_tables.go b/cmd/report/report_tables.go index 8db5fdcd..9da0c5f6 100644 --- a/cmd/report/report_tables.go +++ b/cmd/report/report_tables.go @@ -994,7 +994,7 @@ func sstTFLPTableValues(outputs map[string]script.ScriptOutput) []table.Field { func memoryTableValues(outputs map[string]script.ScriptOutput) []table.Field { return []table.Field{ - {Name: "Installed Memory", Values: []string{installedMemoryFromOutput(outputs)}}, + {Name: "Installed Memory", Values: []string{common.InstalledMemoryFromOutput(outputs)}}, {Name: "MemTotal", Values: []string{common.ValFromRegexSubmatch(outputs[script.MeminfoScriptName].Stdout, `^MemTotal:\s*(.+?)$`)}}, {Name: "MemFree", Values: []string{common.ValFromRegexSubmatch(outputs[script.MeminfoScriptName].Stdout, `^MemFree:\s*(.+?)$`)}}, {Name: "MemAvailable", Values: []string{common.ValFromRegexSubmatch(outputs[script.MeminfoScriptName].Stdout, `^MemAvailable:\s*(.+?)$`)}}, @@ -1670,7 +1670,7 @@ func systemSummaryTableValues(outputs map[string]script.ScriptOutput) []table.Fi {Name: "Prefetchers", Values: []string{common.PrefetchersSummaryFromOutput(outputs)}}, {Name: "PPINs", Values: []string{ppinsFromOutput(outputs)}}, {Name: "Accelerators Available [used]", Values: []string{acceleratorSummaryFromOutput(outputs)}}, - {Name: "Installed Memory", Values: []string{installedMemoryFromOutput(outputs)}}, + {Name: "Installed Memory", Values: []string{common.InstalledMemoryFromOutput(outputs)}}, {Name: "Hugepagesize", Values: []string{common.ValFromRegexSubmatch(outputs[script.MeminfoScriptName].Stdout, `^Hugepagesize:\s*(.+?)$`)}}, {Name: "Transparent Huge Pages", Values: []string{common.ValFromRegexSubmatch(outputs[script.TransparentHugePagesScriptName].Stdout, `.*\[(.*)\].*`)}}, {Name: "Automatic NUMA Balancing", Values: []string{numaBalancingFromOutput(outputs)}}, @@ -1691,57 +1691,57 @@ func systemSummaryTableValues(outputs map[string]script.ScriptOutput) []table.Fi } } func dimmDetails(dimm []string) (details string) { - if strings.Contains(dimm[SizeIdx], "No") { + if strings.Contains(dimm[common.SizeIdx], "No") { details = "No Module Installed" } else { // Intel PMEM modules may have serial number appended to end of part number... // strip that off so it doesn't mess with color selection later - partNumber := dimm[PartIdx] - if strings.Contains(dimm[DetailIdx], "Synchronous Non-Volatile") && - dimm[ManufacturerIdx] == "Intel" && - strings.HasSuffix(dimm[PartIdx], dimm[SerialIdx]) { - partNumber = dimm[PartIdx][:len(dimm[PartIdx])-len(dimm[SerialIdx])] + partNumber := dimm[common.PartIdx] + if strings.Contains(dimm[common.DetailIdx], "Synchronous Non-Volatile") && + dimm[common.ManufacturerIdx] == "Intel" && + strings.HasSuffix(dimm[common.PartIdx], dimm[common.SerialIdx]) { + partNumber = dimm[common.PartIdx][:len(dimm[common.PartIdx])-len(dimm[common.SerialIdx])] } // example: "64GB DDR5 R2 Synchronous Registered (Buffered) Micron Technology MTC78ASF4G72PZ-2G6E1 6400 MT/s [6000 MT/s]" details = fmt.Sprintf("%s %s %s R%s %s %s %s [%s]", - strings.ReplaceAll(dimm[SizeIdx], " ", ""), - dimm[TypeIdx], - dimm[DetailIdx], - dimm[RankIdx], - dimm[ManufacturerIdx], + strings.ReplaceAll(dimm[common.SizeIdx], " ", ""), + dimm[common.TypeIdx], + dimm[common.DetailIdx], + dimm[common.RankIdx], + dimm[common.ManufacturerIdx], partNumber, - strings.ReplaceAll(dimm[SpeedIdx], " ", ""), - strings.ReplaceAll(dimm[ConfiguredSpeedIdx], " ", "")) + strings.ReplaceAll(dimm[common.SpeedIdx], " ", ""), + strings.ReplaceAll(dimm[common.ConfiguredSpeedIdx], " ", "")) } return } func dimmTableHTMLRenderer(tableValues table.TableValues, targetName string) string { - if len(tableValues.Fields) <= max(DerivedSocketIdx, DerivedChannelIdx, DerivedSlotIdx) || - len(tableValues.Fields[DerivedSocketIdx].Values) == 0 || - len(tableValues.Fields[DerivedChannelIdx].Values) == 0 || - len(tableValues.Fields[DerivedSlotIdx].Values) == 0 || - tableValues.Fields[DerivedSocketIdx].Values[0] == "" || - tableValues.Fields[DerivedChannelIdx].Values[0] == "" || - tableValues.Fields[DerivedSlotIdx].Values[0] == "" { + if len(tableValues.Fields) <= max(common.DerivedSocketIdx, common.DerivedChannelIdx, common.DerivedSlotIdx) || + len(tableValues.Fields[common.DerivedSocketIdx].Values) == 0 || + len(tableValues.Fields[common.DerivedChannelIdx].Values) == 0 || + len(tableValues.Fields[common.DerivedSlotIdx].Values) == 0 || + tableValues.Fields[common.DerivedSocketIdx].Values[0] == "" || + tableValues.Fields[common.DerivedChannelIdx].Values[0] == "" || + tableValues.Fields[common.DerivedSlotIdx].Values[0] == "" { return report.DefaultHTMLTableRendererFunc(tableValues) } htmlColors := []string{"lightgreen", "orange", "aqua", "lime", "yellow", "beige", "magenta", "violet", "salmon", "pink"} var slotColorIndices = make(map[string]int) // socket -> channel -> slot -> dimm details var dimms = map[string]map[string]map[string]string{} - for dimmIdx := range tableValues.Fields[DerivedSocketIdx].Values { - if _, ok := dimms[tableValues.Fields[DerivedSocketIdx].Values[dimmIdx]]; !ok { - dimms[tableValues.Fields[DerivedSocketIdx].Values[dimmIdx]] = make(map[string]map[string]string) + for dimmIdx := range tableValues.Fields[common.DerivedSocketIdx].Values { + if _, ok := dimms[tableValues.Fields[common.DerivedSocketIdx].Values[dimmIdx]]; !ok { + dimms[tableValues.Fields[common.DerivedSocketIdx].Values[dimmIdx]] = make(map[string]map[string]string) } - if _, ok := dimms[tableValues.Fields[DerivedSocketIdx].Values[dimmIdx]][tableValues.Fields[DerivedChannelIdx].Values[dimmIdx]]; !ok { - dimms[tableValues.Fields[DerivedSocketIdx].Values[dimmIdx]][tableValues.Fields[DerivedChannelIdx].Values[dimmIdx]] = make(map[string]string) + if _, ok := dimms[tableValues.Fields[common.DerivedSocketIdx].Values[dimmIdx]][tableValues.Fields[common.DerivedChannelIdx].Values[dimmIdx]]; !ok { + dimms[tableValues.Fields[common.DerivedSocketIdx].Values[dimmIdx]][tableValues.Fields[common.DerivedChannelIdx].Values[dimmIdx]] = make(map[string]string) } dimmValues := []string{} for _, field := range tableValues.Fields { dimmValues = append(dimmValues, field.Values[dimmIdx]) } - dimms[tableValues.Fields[DerivedSocketIdx].Values[dimmIdx]][tableValues.Fields[DerivedChannelIdx].Values[dimmIdx]][tableValues.Fields[DerivedSlotIdx].Values[dimmIdx]] = dimmDetails(dimmValues) + dimms[tableValues.Fields[common.DerivedSocketIdx].Values[dimmIdx]][tableValues.Fields[common.DerivedChannelIdx].Values[dimmIdx]][tableValues.Fields[common.DerivedSlotIdx].Values[dimmIdx]] = dimmDetails(dimmValues) } var socketTableHeaders = []string{"Socket", ""} diff --git a/cmd/report/system.go b/cmd/report/system.go index 814a6683..20695c99 100644 --- a/cmd/report/system.go +++ b/cmd/report/system.go @@ -65,7 +65,7 @@ func systemSummaryFromOutput(outputs map[string]script.ScriptOutput) string { turboOnOff = "?" } // memory - installedMem = installedMemoryFromOutput(outputs) + installedMem = common.InstalledMemoryFromOutput(outputs) // BIOS biosVersion = common.ValFromRegexSubmatch(outputs[script.DmidecodeScriptName].Stdout, `^Version:\s*(.+?)$`) // microcode diff --git a/internal/common/table_defs.go b/internal/common/table_defs.go index fd84fbce..7bd1c93e 100644 --- a/internal/common/table_defs.go +++ b/internal/common/table_defs.go @@ -38,11 +38,16 @@ var TableDefinitions = map[string]table.TableDefinition{ script.ArmImplementerScriptName, script.ArmPartScriptName, script.ArmDmidecodePartScriptName, + script.DmidecodeScriptName, }, FieldsFunc: briefSummaryTableValues}, } func briefSummaryTableValues(outputs map[string]script.ScriptOutput) []table.Field { + memory := InstalledMemoryFromOutput(outputs) // Dmidecode, try this first + if memory == "" { + memory = ValFromRegexSubmatch(outputs[script.MeminfoScriptName].Stdout, `^MemTotal:\s*(.+?)$`) // Meminfo as fallback + } return []table.Field{ {Name: "Host Name", Values: []string{strings.TrimSpace(outputs[script.HostnameScriptName].Stdout)}}, // Hostname {Name: "Time", Values: []string{strings.TrimSpace(outputs[script.DateScriptName].Stdout)}}, // Date @@ -61,7 +66,7 @@ func briefSummaryTableValues(outputs map[string]script.ScriptOutput) []table.Fie {Name: "All-core Maximum Frequency", Values: []string{AllCoreMaxFrequencyFromOutput(outputs)}, Description: "The highest speed all cores can reach simultaneously with Turbo Boost."}, // Lscpu, LspciBits, LspciDevices, SpecCoreFrequencies {Name: "Energy Performance Bias", Values: []string{EPBFromOutput(outputs)}}, // EpbSource, EpbBIOS, EpbOS {Name: "Efficiency Latency Control", Values: []string{ELCSummaryFromOutput(outputs)}}, // Elc - {Name: "MemTotal", Values: []string{ValFromRegexSubmatch(outputs[script.MeminfoScriptName].Stdout, `^MemTotal:\s*(.+?)$`)}}, // Meminfo + {Name: "Memory", Values: []string{memory}}, // Dmidecode,Meminfo {Name: "NIC", Values: []string{NICSummaryFromOutput(outputs)}}, // Lshw, NicInfo {Name: "Disk", Values: []string{DiskSummaryFromOutput(outputs)}}, // DiskInfo, Hdparm {Name: "OS", Values: []string{OperatingSystemFromOutput(outputs)}}, // EtcRelease diff --git a/internal/common/table_helpers.go b/internal/common/table_helpers.go index 399959e4..85f9c6f9 100644 --- a/internal/common/table_helpers.go +++ b/internal/common/table_helpers.go @@ -289,3 +289,72 @@ func TDPFromOutput(outputs map[string]script.ScriptOutput) string { } return fmt.Sprint(msr/8) + "W" } + +const ( + BankLocatorIdx = iota + LocatorIdx + ManufacturerIdx + PartIdx + SerialIdx + SizeIdx + TypeIdx + DetailIdx + SpeedIdx + RankIdx + ConfiguredSpeedIdx + DerivedSocketIdx + DerivedChannelIdx + DerivedSlotIdx +) + +func DimmInfoFromDmiDecode(dmiDecodeOutput string) [][]string { + return ValsArrayFromDmiDecodeRegexSubmatch( + dmiDecodeOutput, + "17", + `^Bank Locator:\s*(.+?)$`, + `^Locator:\s*(.+?)$`, + `^Manufacturer:\s*(.+?)$`, + `^Part Number:\s*(.+?)\s*$`, + `^Serial Number:\s*(.+?)\s*$`, + `^Size:\s*(.+?)$`, + `^Type:\s*(.+?)$`, + `^Type Detail:\s*(.+?)$`, + `^Speed:\s*(.+?)$`, + `^Rank:\s*(.+?)$`, + `^Configured.*Speed:\s*(.+?)$`, + ) +} + +func InstalledMemoryFromOutput(outputs map[string]script.ScriptOutput) string { + dimmInfo := DimmInfoFromDmiDecode(outputs[script.DmidecodeScriptName].Stdout) + dimmTypeCount := make(map[string]int) + for _, dimm := range dimmInfo { + dimmKey := dimm[TypeIdx] + ":" + dimm[SizeIdx] + ":" + dimm[SpeedIdx] + ":" + dimm[ConfiguredSpeedIdx] + if count, ok := dimmTypeCount[dimmKey]; ok { + dimmTypeCount[dimmKey] = count + 1 + } else { + dimmTypeCount[dimmKey] = 1 + } + } + var summaries []string + re := regexp.MustCompile(`(\d+)\s*(\w*)`) + for dimmKey, count := range dimmTypeCount { + fields := strings.Split(dimmKey, ":") + match := re.FindStringSubmatch(fields[1]) // size field + if match != nil { + size, err := strconv.Atoi(match[1]) + if err != nil { + slog.Warn("Don't recognize DIMM size format.", slog.String("field", fields[1])) + return "" + } + sum := count * size + unit := match[2] + dimmType := fields[0] + speed := strings.ReplaceAll(fields[2], " ", "") + configuredSpeed := strings.ReplaceAll(fields[3], " ", "") + summary := fmt.Sprintf("%d%s (%dx%d%s %s %s [%s])", sum, unit, count, size, unit, dimmType, speed, configuredSpeed) + summaries = append(summaries, summary) + } + } + return strings.Join(summaries, "; ") +}