Skip to content

Commit 8f235bd

Browse files
authored
Add option to record and restore system configuration through config command (#577)
* record Signed-off-by: Harper, Jason M <[email protected]> * restore Signed-off-by: Harper, Jason M <[email protected]> * clean output Signed-off-by: Harper, Jason M <[email protected]> * progress indicator Signed-off-by: Harper, Jason M <[email protected]> * human readable Signed-off-by: Harper, Jason M <[email protected]> * sequential execution Signed-off-by: Harper, Jason M <[email protected]> * runscripts for uncore freq Signed-off-by: Harper, Jason M <[email protected]> * fix spinner output Signed-off-by: Harper, Jason M <[email protected]> * remove remnants of set go routines Signed-off-by: Harper, Jason M <[email protected]> * fix set core frequency Signed-off-by: Harper, Jason M <[email protected]> * set bucket frequencies Signed-off-by: Harper, Jason M <[email protected]> * go 1.25.5 Signed-off-by: Harper, Jason M <[email protected]> * modernize Signed-off-by: Harper, Jason M <[email protected]> * handle new core frequency format Signed-off-by: Harper, Jason M <[email protected]> * readme Signed-off-by: Harper, Jason M <[email protected]> * add test Signed-off-by: Harper, Jason M <[email protected]> * spelling Signed-off-by: Harper, Jason M <[email protected]> * improve script name Signed-off-by: Harper, Jason M <[email protected]> * flag name Signed-off-by: Harper, Jason M <[email protected]> --------- Signed-off-by: Harper, Jason M <[email protected]>
1 parent 9693ca7 commit 8f235bd

File tree

14 files changed

+2138
-636
lines changed

14 files changed

+2138
-636
lines changed

README.md

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,38 @@ $ ./perfspect config --cores 24 --llc 2.0 --uncore-max 1.8
129129
...
130130
</pre>
131131

132+
##### Recording Configuration
133+
The current configuration can, optionally, be saved to a file using the `--record` flag. This creates a human-readable configuration file that can be used to restore settings later.
134+
135+
Example:
136+
<pre>
137+
$ ./perfspect config --tdp 300 --record
138+
Configuration recorded to: perfspect_2025-12-01_14-30-45/gnr_config.txt
139+
</pre>
140+
141+
##### Restoring Configuration
142+
The `config restore` subcommand restores configuration from a previously recorded file. This is useful for reverting changes or applying a known-good configuration across multiple systems.
143+
144+
Example:
145+
<pre>
146+
$ ./perfspect config restore perfspect_2025-12-01_14-30-45/gnr_config.txt
147+
Configuration settings to restore from perfspect_2025-12-01_14-30-45/gnr_config.txt:
148+
Cores per Socket : 86
149+
L3 Cache : 336
150+
Package Power / TDP : 350
151+
...
152+
Apply these configuration changes? [y/N]: y
153+
...
154+
</pre>
155+
156+
Use the `--yes` flag to skip the confirmation prompt:
157+
<pre>
158+
$ ./perfspect config restore perfspect_2025-12-01_14-30-45/gnr_config.txt --yes
159+
</pre>
160+
161+
> [!TIP]
162+
> The restore command works with remote targets too. Use `--target` or `--targets` to restore configuration on remote systems.
163+
132164
### Common Command Options
133165

134166
#### Local vs. Remote Targets

builder/build.Dockerfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ ARG TAG=
1414
FROM ${REGISTRY}${PREFIX}perfspect-tools:${TAG} AS tools
1515

1616
# STAGE 2 - image contains perfspect's Go components build environment
17-
FROM golang:1.25.4@sha256:6ca9eb0b32a4bd4e8c98a4a2edf2d7c96f3ea6db6eb4fc254eef6c067cf73bb4
17+
FROM golang:1.25.5@sha256:20b91eda7a9627c127c0225b0d4e8ec927b476fa4130c6760928b849d769c149
1818
# copy the tools binaries and source from the previous stage
1919
RUN mkdir /prebuilt
2020
RUN mkdir /prebuilt/tools

cmd/config/config.go

Lines changed: 122 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import (
1414
"perfspect/internal/report"
1515
"perfspect/internal/script"
1616
"perfspect/internal/target"
17+
"perfspect/internal/util"
1718
"slices"
1819
"strings"
1920

@@ -25,6 +26,8 @@ const cmdName = "config"
2526
var examples = []string{
2627
fmt.Sprintf(" Set core count on local host: $ %s %s --cores 32", common.AppName, cmdName),
2728
fmt.Sprintf(" Set multiple config items on local host: $ %s %s --core-max 3.0 --uncore-max 2.1 --tdp 120", common.AppName, cmdName),
29+
fmt.Sprintf(" Record config to file before changes: $ %s %s --c6 disable --epb 0 --record", common.AppName, cmdName),
30+
fmt.Sprintf(" Restore config from file: $ %s %s restore gnr_config.txt", common.AppName, cmdName),
2831
fmt.Sprintf(" Set core count on remote target: $ %s %s --cores 32 --target 192.168.1.1 --user fred --key fred_key", common.AppName, cmdName),
2932
fmt.Sprintf(" View current config on remote target: $ %s %s --target 192.168.1.1 --user fred --key fred_key", common.AppName, cmdName),
3033
fmt.Sprintf(" Set governor on remote targets: $ %s %s --gov performance --targets targets.yaml", common.AppName, cmdName),
@@ -52,6 +55,22 @@ func runCmd(cmd *cobra.Command, args []string) error {
5255
// appContext is the application context that holds common data and resources.
5356
appContext := cmd.Parent().Context().Value(common.AppContext{}).(common.AppContext)
5457
localTempDir := appContext.LocalTempDir
58+
outputDir := appContext.OutputDir
59+
60+
flagRecord := cmd.Flags().Lookup(flagRecordName).Value.String() == "true"
61+
flagNoSummary := cmd.Flags().Lookup(flagNoSummaryName).Value.String() == "true"
62+
63+
// create output directory if we are recording the configuration
64+
if flagRecord {
65+
err := util.CreateDirectoryIfNotExists(outputDir, 0755) // #nosec G301
66+
if err != nil {
67+
err = fmt.Errorf("failed to create output directory: %w", err)
68+
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
69+
slog.Error(err.Error())
70+
cmd.SilenceUsage = true
71+
return err
72+
}
73+
}
5574
// get the targets
5675
myTargets, targetErrs, err := common.GetTargets(cmd, true, true, localTempDir)
5776
if err != nil {
@@ -91,14 +110,40 @@ func runCmd(cmd *cobra.Command, args []string) error {
91110
cmd.SilenceUsage = true
92111
return err
93112
}
94-
// print config prior to changes, optionally
95-
if !cmd.Flags().Lookup(flagNoSummaryName).Changed {
96-
if err := printConfig(myTargets, localTempDir); err != nil {
113+
// collect and print and/or record the configuration before making changes
114+
if !flagNoSummary || flagRecord {
115+
config, err := getConfig(myTargets, localTempDir)
116+
if err != nil {
97117
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
98118
slog.Error(err.Error())
99119
cmd.SilenceUsage = true
100120
return err
101121
}
122+
reports, err := processConfig(config)
123+
if err != nil {
124+
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
125+
slog.Error(err.Error())
126+
cmd.SilenceUsage = true
127+
return err
128+
}
129+
filesWritten, err := printConfig(reports, !flagNoSummary, flagRecord, outputDir)
130+
if err != nil {
131+
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
132+
slog.Error(err.Error())
133+
cmd.SilenceUsage = true
134+
return err
135+
}
136+
if len(filesWritten) > 0 {
137+
message := "Configuration"
138+
if len(filesWritten) > 1 {
139+
message = "Configurations"
140+
}
141+
fmt.Printf("%s recorded:\n", message)
142+
for _, fileWritten := range filesWritten {
143+
fmt.Printf(" %s\n", fileWritten)
144+
}
145+
fmt.Println()
146+
}
102147
}
103148
// if no changes were requested, print a message and return
104149
var changeRequested bool
@@ -138,9 +183,24 @@ func runCmd(cmd *cobra.Command, args []string) error {
138183
}
139184
multiSpinner.Finish()
140185
fmt.Println() // blank line
141-
// print config after making changes
142-
if !cmd.Flags().Lookup(flagNoSummaryName).Changed {
143-
if err := printConfig(myTargets, localTempDir); err != nil {
186+
// collect and print the configuration before making changes
187+
if !flagNoSummary {
188+
config, err := getConfig(myTargets, localTempDir)
189+
if err != nil {
190+
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
191+
slog.Error(err.Error())
192+
cmd.SilenceUsage = true
193+
return err
194+
}
195+
reports, err := processConfig(config)
196+
if err != nil {
197+
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
198+
slog.Error(err.Error())
199+
cmd.SilenceUsage = true
200+
return err
201+
}
202+
_, err = printConfig(reports, !flagNoSummary, false, outputDir) // print, don't record
203+
if err != nil {
144204
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
145205
slog.Error(err.Error())
146206
cmd.SilenceUsage = true
@@ -176,58 +236,53 @@ func setOnTarget(cmd *cobra.Command, myTarget target.Target, flagGroups []flagGr
176236
channelError <- nil
177237
return
178238
}
179-
channelSetComplete := make(chan setOutput)
180-
var successMessages []string
181-
var errorMessages []string
239+
var statusMessages []string
182240
_ = statusUpdate(myTarget.GetName(), "updating configuration")
183241
for _, group := range flagGroups {
184242
for _, flag := range group.flags {
185243
if flag.HasSetFunc() && cmd.Flags().Lookup(flag.GetName()).Changed {
186-
successMessages = append(successMessages, fmt.Sprintf("set %s to %s", flag.GetName(), flag.GetValueAsString()))
187-
errorMessages = append(errorMessages, fmt.Sprintf("failed to set %s to %s", flag.GetName(), flag.GetValueAsString()))
244+
successMessage := fmt.Sprintf("set %s to %s", flag.GetName(), flag.GetValueAsString())
245+
errorMessage := fmt.Sprintf("failed to set %s to %s", flag.GetName(), flag.GetValueAsString())
246+
var setErr error
188247
switch flag.GetType() {
189248
case "int":
190249
if flag.intSetFunc != nil {
191250
value, _ := cmd.Flags().GetInt(flag.GetName())
192-
go flag.intSetFunc(value, myTarget, localTempDir, channelSetComplete, len(successMessages)-1)
251+
setErr = flag.intSetFunc(value, myTarget, localTempDir)
193252
}
194253
case "float64":
195254
if flag.floatSetFunc != nil {
196255
value, _ := cmd.Flags().GetFloat64(flag.GetName())
197-
go flag.floatSetFunc(value, myTarget, localTempDir, channelSetComplete, len(successMessages)-1)
256+
setErr = flag.floatSetFunc(value, myTarget, localTempDir)
198257
}
199258
case "string":
200259
if flag.stringSetFunc != nil {
201260
value, _ := cmd.Flags().GetString(flag.GetName())
202-
go flag.stringSetFunc(value, myTarget, localTempDir, channelSetComplete, len(successMessages)-1)
261+
setErr = flag.stringSetFunc(value, myTarget, localTempDir)
203262
}
204263
case "bool":
205264
if flag.boolSetFunc != nil {
206265
value, _ := cmd.Flags().GetBool(flag.GetName())
207-
go flag.boolSetFunc(value, myTarget, localTempDir, channelSetComplete, len(successMessages)-1)
266+
setErr = flag.boolSetFunc(value, myTarget, localTempDir)
208267
}
209268
}
269+
if setErr != nil {
270+
slog.Error(setErr.Error())
271+
statusMessages = append(statusMessages, errorMessage)
272+
} else {
273+
statusMessages = append(statusMessages, successMessage)
274+
}
210275
}
211276
}
212277
}
213-
// wait for all set goroutines to finish
214-
statusMessages := []string{}
215-
for range successMessages {
216-
out := <-channelSetComplete
217-
if out.err != nil {
218-
slog.Error(out.err.Error())
219-
statusMessages = append(statusMessages, errorMessages[out.goRoutineID])
220-
} else {
221-
statusMessages = append(statusMessages, successMessages[out.goRoutineID])
222-
}
223-
}
224278
statusMessage := fmt.Sprintf("configuration update complete: %s", strings.Join(statusMessages, ", "))
225279
slog.Info(statusMessage, slog.String("target", myTarget.GetName()))
226280
_ = statusUpdate(myTarget.GetName(), statusMessage)
227281
channelError <- nil
228282
}
229283

230-
func printConfig(myTargets []target.Target, localTempDir string) (err error) {
284+
// getConfig collects the configuration data from the target(s)
285+
func getConfig(myTargets []target.Target, localTempDir string) ([]common.TargetScriptOutputs, error) {
231286
scriptNames := report.GetScriptNamesForTable(report.ConfigurationTableName)
232287
var scriptsToRun []script.ScriptDefinition
233288
for _, scriptName := range scriptNames {
@@ -239,10 +294,10 @@ func printConfig(myTargets []target.Target, localTempDir string) (err error) {
239294
channelTargetScriptOutputs := make(chan common.TargetScriptOutputs)
240295
channelError := make(chan error)
241296
for _, myTarget := range myTargets {
242-
err = multiSpinner.AddSpinner(myTarget.GetName())
297+
err := multiSpinner.AddSpinner(myTarget.GetName())
243298
if err != nil {
244299
err = fmt.Errorf("failed to add spinner: %v", err)
245-
return
300+
return nil, err
246301
}
247302
// run the selected scripts on the target
248303
go collectOnTarget(myTarget, scriptsToRun, localTempDir, channelTargetScriptOutputs, channelError, multiSpinner.Status)
@@ -269,28 +324,55 @@ func printConfig(myTargets []target.Target, localTempDir string) (err error) {
269324
}
270325
}
271326
multiSpinner.Finish()
272-
// process and print the table for each target
273-
for _, targetScriptOutputs := range orderedTargetScriptOutputs {
327+
return orderedTargetScriptOutputs, nil
328+
}
329+
330+
// processConfig processes the collected configuration data and creates text reports
331+
func processConfig(targetScriptOutputs []common.TargetScriptOutputs) (map[string][]byte, error) {
332+
reports := make(map[string][]byte)
333+
var err error
334+
for _, targetScriptOutput := range targetScriptOutputs {
274335
// process the tables, i.e., get field values from raw script output
275336
tableNames := []string{report.ConfigurationTableName}
276337
var tableValues []report.TableValues
277-
if tableValues, err = report.ProcessTables(tableNames, targetScriptOutputs.ScriptOutputs); err != nil {
338+
if tableValues, err = report.ProcessTables(tableNames, targetScriptOutput.ScriptOutputs); err != nil {
278339
err = fmt.Errorf("failed to process collected data: %v", err)
279-
return
340+
return nil, err
280341
}
281342
// create the report for this single table
282343
var reportBytes []byte
283-
if reportBytes, err = report.Create("txt", tableValues, targetScriptOutputs.TargetName); err != nil {
344+
if reportBytes, err = report.Create("txt", tableValues, targetScriptOutput.TargetName); err != nil {
284345
err = fmt.Errorf("failed to create report: %v", err)
285-
return
346+
return nil, err
286347
}
287-
// print the report
288-
if len(orderedTargetScriptOutputs) > 1 {
289-
fmt.Printf("%s\n", targetScriptOutputs.TargetName)
348+
// append the report to the list
349+
reports[targetScriptOutput.TargetName] = reportBytes
350+
}
351+
return reports, nil
352+
}
353+
354+
// printConfig prints and/or saves the configuration reports
355+
func printConfig(reports map[string][]byte, toStdout bool, toFile bool, outputDir string) ([]string, error) {
356+
filesWritten := []string{}
357+
for targetName, reportBytes := range reports {
358+
if toStdout {
359+
// print the report to stdout
360+
if len(reports) > 1 {
361+
fmt.Printf("%s\n", targetName)
362+
}
363+
fmt.Print(string(reportBytes))
364+
}
365+
if toFile {
366+
outputFilePath := fmt.Sprintf("%s/%s_config.txt", outputDir, targetName)
367+
err := os.WriteFile(outputFilePath, reportBytes, 0644) // #nosec G306
368+
if err != nil {
369+
err = fmt.Errorf("failed to write configuration report to file: %v", err)
370+
return filesWritten, err
371+
}
372+
filesWritten = append(filesWritten, outputFilePath)
290373
}
291-
fmt.Print(string(reportBytes))
292374
}
293-
return
375+
return filesWritten, nil
294376
}
295377

296378
// collectOnTarget runs the scripts on the target and sends the results to the appropriate channels

cmd/config/flag.go

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -10,20 +10,20 @@ import (
1010
"github.com/spf13/pflag"
1111
)
1212

13-
// setOutput is a struct that holds the output of a flagDefinition set function
14-
type setOutput struct {
15-
goRoutineID int
16-
err error
17-
}
13+
type IntSetFunc func(int, target.Target, string) error
14+
type FloatSetFunc func(float64, target.Target, string) error
15+
type StringSetFunc func(string, target.Target, string) error
16+
type BoolSetFunc func(bool, target.Target, string) error
17+
type ValidationFunc func(cmd *cobra.Command) bool
1818

1919
// flagDefinition is a struct that defines a command line flag.
2020
type flagDefinition struct {
2121
pflag *pflag.Flag
22-
intSetFunc func(int, target.Target, string, chan setOutput, int)
23-
floatSetFunc func(float64, target.Target, string, chan setOutput, int)
24-
stringSetFunc func(string, target.Target, string, chan setOutput, int)
25-
boolSetFunc func(bool, target.Target, string, chan setOutput, int)
26-
validationFunc func(cmd *cobra.Command) bool
22+
intSetFunc IntSetFunc
23+
floatSetFunc FloatSetFunc
24+
stringSetFunc StringSetFunc
25+
boolSetFunc BoolSetFunc
26+
validationFunc ValidationFunc
2727
validationDescription string
2828
}
2929

@@ -48,7 +48,7 @@ func (f *flagDefinition) GetValueAsString() string {
4848
}
4949

5050
// newIntFlag creates a new int flag and adds it to the command.
51-
func newIntFlag(cmd *cobra.Command, name string, defaultValue int, setFunc func(int, target.Target, string, chan setOutput, int), help string, validationDescription string, validationFunc func(cmd *cobra.Command) bool) flagDefinition {
51+
func newIntFlag(cmd *cobra.Command, name string, defaultValue int, setFunc IntSetFunc, help string, validationDescription string, validationFunc ValidationFunc) flagDefinition {
5252
cmd.Flags().Int(name, defaultValue, help)
5353
pFlag := cmd.Flags().Lookup(name)
5454
return flagDefinition{
@@ -60,7 +60,7 @@ func newIntFlag(cmd *cobra.Command, name string, defaultValue int, setFunc func(
6060
}
6161

6262
// newFloat64Flag creates a new float64 flag and adds it to the command.
63-
func newFloat64Flag(cmd *cobra.Command, name string, defaultValue float64, setFunc func(float64, target.Target, string, chan setOutput, int), help string, validationDescription string, validationFunc func(cmd *cobra.Command) bool) flagDefinition {
63+
func newFloat64Flag(cmd *cobra.Command, name string, defaultValue float64, setFunc FloatSetFunc, help string, validationDescription string, validationFunc ValidationFunc) flagDefinition {
6464
cmd.Flags().Float64(name, defaultValue, help)
6565
pFlag := cmd.Flags().Lookup(name)
6666
return flagDefinition{
@@ -72,7 +72,7 @@ func newFloat64Flag(cmd *cobra.Command, name string, defaultValue float64, setFu
7272
}
7373

7474
// newStringFlag creates a new string flag and adds it to the command.
75-
func newStringFlag(cmd *cobra.Command, name string, defaultValue string, setFunc func(string, target.Target, string, chan setOutput, int), help string, validationDescription string, validationFunc func(cmd *cobra.Command) bool) flagDefinition {
75+
func newStringFlag(cmd *cobra.Command, name string, defaultValue string, setFunc StringSetFunc, help string, validationDescription string, validationFunc ValidationFunc) flagDefinition {
7676
cmd.Flags().String(name, defaultValue, help)
7777
pFlag := cmd.Flags().Lookup(name)
7878
return flagDefinition{
@@ -84,7 +84,7 @@ func newStringFlag(cmd *cobra.Command, name string, defaultValue string, setFunc
8484
}
8585

8686
// newBoolFlag creates a new boolean flag and adds it to the command.
87-
func newBoolFlag(cmd *cobra.Command, name string, defaultValue bool, setFunc func(bool, target.Target, string, chan setOutput, int), help string, validationDescription string, validationFunc func(cmd *cobra.Command) bool) flagDefinition {
87+
func newBoolFlag(cmd *cobra.Command, name string, defaultValue bool, setFunc BoolSetFunc, help string, validationDescription string, validationFunc ValidationFunc) flagDefinition {
8888
cmd.Flags().Bool(name, defaultValue, help)
8989
pFlag := cmd.Flags().Lookup(name)
9090
return flagDefinition{

0 commit comments

Comments
 (0)