Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,38 @@ $ ./perfspect config --cores 24 --llc 2.0 --uncore-max 1.8
...
</pre>

##### Recording Configuration
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.

Example:
<pre>
$ ./perfspect config --tdp 300 --record
Configuration recorded to: perfspect_2025-12-01_14-30-45/gnr_config.txt
</pre>

##### Restoring Configuration
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.

Example:
<pre>
$ ./perfspect config restore perfspect_2025-12-01_14-30-45/gnr_config.txt
Configuration settings to restore from perfspect_2025-12-01_14-30-45/gnr_config.txt:
Cores per Socket : 86
L3 Cache : 336
Package Power / TDP : 350
...
Apply these configuration changes? [y/N]: y
...
</pre>

Use the `--yes` flag to skip the confirmation prompt:
<pre>
$ ./perfspect config restore perfspect_2025-12-01_14-30-45/gnr_config.txt --yes
</pre>

> [!TIP]
> The restore command works with remote targets too. Use `--target` or `--targets` to restore configuration on remote systems.

### Common Command Options

#### Local vs. Remote Targets
Expand Down
2 changes: 1 addition & 1 deletion builder/build.Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ ARG TAG=
FROM ${REGISTRY}${PREFIX}perfspect-tools:${TAG} AS tools

# STAGE 2 - image contains perfspect's Go components build environment
FROM golang:1.25.4@sha256:6ca9eb0b32a4bd4e8c98a4a2edf2d7c96f3ea6db6eb4fc254eef6c067cf73bb4
FROM golang:1.25.5@sha256:20b91eda7a9627c127c0225b0d4e8ec927b476fa4130c6760928b849d769c149
# copy the tools binaries and source from the previous stage
RUN mkdir /prebuilt
RUN mkdir /prebuilt/tools
Expand Down
162 changes: 122 additions & 40 deletions cmd/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import (
"perfspect/internal/report"
"perfspect/internal/script"
"perfspect/internal/target"
"perfspect/internal/util"
"slices"
"strings"

Expand All @@ -25,6 +26,8 @@ const cmdName = "config"
var examples = []string{
fmt.Sprintf(" Set core count on local host: $ %s %s --cores 32", common.AppName, cmdName),
fmt.Sprintf(" Set multiple config items on local host: $ %s %s --core-max 3.0 --uncore-max 2.1 --tdp 120", common.AppName, cmdName),
fmt.Sprintf(" Record config to file before changes: $ %s %s --c6 disable --epb 0 --record", common.AppName, cmdName),
fmt.Sprintf(" Restore config from file: $ %s %s restore gnr_config.txt", common.AppName, cmdName),
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),
fmt.Sprintf(" View current config on remote target: $ %s %s --target 192.168.1.1 --user fred --key fred_key", common.AppName, cmdName),
fmt.Sprintf(" Set governor on remote targets: $ %s %s --gov performance --targets targets.yaml", common.AppName, cmdName),
Expand Down Expand Up @@ -52,6 +55,22 @@ func runCmd(cmd *cobra.Command, args []string) error {
// appContext is the application context that holds common data and resources.
appContext := cmd.Parent().Context().Value(common.AppContext{}).(common.AppContext)
localTempDir := appContext.LocalTempDir
outputDir := appContext.OutputDir

flagRecord := cmd.Flags().Lookup(flagRecordName).Value.String() == "true"
flagNoSummary := cmd.Flags().Lookup(flagNoSummaryName).Value.String() == "true"

// create output directory if we are recording the configuration
if flagRecord {
err := util.CreateDirectoryIfNotExists(outputDir, 0755) // #nosec G301
if err != nil {
err = fmt.Errorf("failed to create output directory: %w", err)
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
slog.Error(err.Error())
cmd.SilenceUsage = true
return err
}
}
// get the targets
myTargets, targetErrs, err := common.GetTargets(cmd, true, true, localTempDir)
if err != nil {
Expand Down Expand Up @@ -91,14 +110,40 @@ func runCmd(cmd *cobra.Command, args []string) error {
cmd.SilenceUsage = true
return err
}
// print config prior to changes, optionally
if !cmd.Flags().Lookup(flagNoSummaryName).Changed {
if err := printConfig(myTargets, localTempDir); err != nil {
// collect and print and/or record the configuration before making changes
if !flagNoSummary || flagRecord {
config, err := getConfig(myTargets, localTempDir)
if err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
slog.Error(err.Error())
cmd.SilenceUsage = true
return err
}
reports, err := processConfig(config)
if err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
slog.Error(err.Error())
cmd.SilenceUsage = true
return err
}
filesWritten, err := printConfig(reports, !flagNoSummary, flagRecord, outputDir)
if err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
slog.Error(err.Error())
cmd.SilenceUsage = true
return err
}
if len(filesWritten) > 0 {
message := "Configuration"
if len(filesWritten) > 1 {
message = "Configurations"
}
fmt.Printf("%s recorded:\n", message)
for _, fileWritten := range filesWritten {
fmt.Printf(" %s\n", fileWritten)
}
fmt.Println()
}
}
// if no changes were requested, print a message and return
var changeRequested bool
Expand Down Expand Up @@ -138,9 +183,24 @@ func runCmd(cmd *cobra.Command, args []string) error {
}
multiSpinner.Finish()
fmt.Println() // blank line
// print config after making changes
if !cmd.Flags().Lookup(flagNoSummaryName).Changed {
if err := printConfig(myTargets, localTempDir); err != nil {
// collect and print the configuration before making changes
if !flagNoSummary {
config, err := getConfig(myTargets, localTempDir)
if err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
slog.Error(err.Error())
cmd.SilenceUsage = true
return err
}
reports, err := processConfig(config)
if err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
slog.Error(err.Error())
cmd.SilenceUsage = true
return err
}
_, err = printConfig(reports, !flagNoSummary, false, outputDir) // print, don't record
if err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
slog.Error(err.Error())
cmd.SilenceUsage = true
Expand Down Expand Up @@ -176,58 +236,53 @@ func setOnTarget(cmd *cobra.Command, myTarget target.Target, flagGroups []flagGr
channelError <- nil
return
}
channelSetComplete := make(chan setOutput)
var successMessages []string
var errorMessages []string
var statusMessages []string
_ = statusUpdate(myTarget.GetName(), "updating configuration")
for _, group := range flagGroups {
for _, flag := range group.flags {
if flag.HasSetFunc() && cmd.Flags().Lookup(flag.GetName()).Changed {
successMessages = append(successMessages, fmt.Sprintf("set %s to %s", flag.GetName(), flag.GetValueAsString()))
errorMessages = append(errorMessages, fmt.Sprintf("failed to set %s to %s", flag.GetName(), flag.GetValueAsString()))
successMessage := fmt.Sprintf("set %s to %s", flag.GetName(), flag.GetValueAsString())
errorMessage := fmt.Sprintf("failed to set %s to %s", flag.GetName(), flag.GetValueAsString())
var setErr error
switch flag.GetType() {
case "int":
if flag.intSetFunc != nil {
value, _ := cmd.Flags().GetInt(flag.GetName())
go flag.intSetFunc(value, myTarget, localTempDir, channelSetComplete, len(successMessages)-1)
setErr = flag.intSetFunc(value, myTarget, localTempDir)
}
case "float64":
if flag.floatSetFunc != nil {
value, _ := cmd.Flags().GetFloat64(flag.GetName())
go flag.floatSetFunc(value, myTarget, localTempDir, channelSetComplete, len(successMessages)-1)
setErr = flag.floatSetFunc(value, myTarget, localTempDir)
}
case "string":
if flag.stringSetFunc != nil {
value, _ := cmd.Flags().GetString(flag.GetName())
go flag.stringSetFunc(value, myTarget, localTempDir, channelSetComplete, len(successMessages)-1)
setErr = flag.stringSetFunc(value, myTarget, localTempDir)
}
case "bool":
if flag.boolSetFunc != nil {
value, _ := cmd.Flags().GetBool(flag.GetName())
go flag.boolSetFunc(value, myTarget, localTempDir, channelSetComplete, len(successMessages)-1)
setErr = flag.boolSetFunc(value, myTarget, localTempDir)
}
}
if setErr != nil {
slog.Error(setErr.Error())
statusMessages = append(statusMessages, errorMessage)
} else {
statusMessages = append(statusMessages, successMessage)
}
}
}
}
// wait for all set goroutines to finish
statusMessages := []string{}
for range successMessages {
out := <-channelSetComplete
if out.err != nil {
slog.Error(out.err.Error())
statusMessages = append(statusMessages, errorMessages[out.goRoutineID])
} else {
statusMessages = append(statusMessages, successMessages[out.goRoutineID])
}
}
statusMessage := fmt.Sprintf("configuration update complete: %s", strings.Join(statusMessages, ", "))
slog.Info(statusMessage, slog.String("target", myTarget.GetName()))
_ = statusUpdate(myTarget.GetName(), statusMessage)
channelError <- nil
}

func printConfig(myTargets []target.Target, localTempDir string) (err error) {
// getConfig collects the configuration data from the target(s)
func getConfig(myTargets []target.Target, localTempDir string) ([]common.TargetScriptOutputs, error) {
scriptNames := report.GetScriptNamesForTable(report.ConfigurationTableName)
var scriptsToRun []script.ScriptDefinition
for _, scriptName := range scriptNames {
Expand All @@ -239,10 +294,10 @@ func printConfig(myTargets []target.Target, localTempDir string) (err error) {
channelTargetScriptOutputs := make(chan common.TargetScriptOutputs)
channelError := make(chan error)
for _, myTarget := range myTargets {
err = multiSpinner.AddSpinner(myTarget.GetName())
err := multiSpinner.AddSpinner(myTarget.GetName())
if err != nil {
err = fmt.Errorf("failed to add spinner: %v", err)
return
return nil, err
}
// run the selected scripts on the target
go collectOnTarget(myTarget, scriptsToRun, localTempDir, channelTargetScriptOutputs, channelError, multiSpinner.Status)
Expand All @@ -269,28 +324,55 @@ func printConfig(myTargets []target.Target, localTempDir string) (err error) {
}
}
multiSpinner.Finish()
// process and print the table for each target
for _, targetScriptOutputs := range orderedTargetScriptOutputs {
return orderedTargetScriptOutputs, nil
}

// processConfig processes the collected configuration data and creates text reports
func processConfig(targetScriptOutputs []common.TargetScriptOutputs) (map[string][]byte, error) {
reports := make(map[string][]byte)
var err error
for _, targetScriptOutput := range targetScriptOutputs {
// process the tables, i.e., get field values from raw script output
tableNames := []string{report.ConfigurationTableName}
var tableValues []report.TableValues
if tableValues, err = report.ProcessTables(tableNames, targetScriptOutputs.ScriptOutputs); err != nil {
if tableValues, err = report.ProcessTables(tableNames, targetScriptOutput.ScriptOutputs); err != nil {
err = fmt.Errorf("failed to process collected data: %v", err)
return
return nil, err
}
// create the report for this single table
var reportBytes []byte
if reportBytes, err = report.Create("txt", tableValues, targetScriptOutputs.TargetName); err != nil {
if reportBytes, err = report.Create("txt", tableValues, targetScriptOutput.TargetName); err != nil {
err = fmt.Errorf("failed to create report: %v", err)
return
return nil, err
}
// print the report
if len(orderedTargetScriptOutputs) > 1 {
fmt.Printf("%s\n", targetScriptOutputs.TargetName)
// append the report to the list
reports[targetScriptOutput.TargetName] = reportBytes
}
return reports, nil
}

// printConfig prints and/or saves the configuration reports
func printConfig(reports map[string][]byte, toStdout bool, toFile bool, outputDir string) ([]string, error) {
filesWritten := []string{}
for targetName, reportBytes := range reports {
if toStdout {
// print the report to stdout
if len(reports) > 1 {
fmt.Printf("%s\n", targetName)
}
fmt.Print(string(reportBytes))
}
if toFile {
outputFilePath := fmt.Sprintf("%s/%s_config.txt", outputDir, targetName)
err := os.WriteFile(outputFilePath, reportBytes, 0644) // #nosec G306
if err != nil {
err = fmt.Errorf("failed to write configuration report to file: %v", err)
return filesWritten, err
}
filesWritten = append(filesWritten, outputFilePath)
}
fmt.Print(string(reportBytes))
}
return
return filesWritten, nil
}

// collectOnTarget runs the scripts on the target and sends the results to the appropriate channels
Expand Down
28 changes: 14 additions & 14 deletions cmd/config/flag.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,20 +10,20 @@ import (
"github.com/spf13/pflag"
)

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

// flagDefinition is a struct that defines a command line flag.
type flagDefinition struct {
pflag *pflag.Flag
intSetFunc func(int, target.Target, string, chan setOutput, int)
floatSetFunc func(float64, target.Target, string, chan setOutput, int)
stringSetFunc func(string, target.Target, string, chan setOutput, int)
boolSetFunc func(bool, target.Target, string, chan setOutput, int)
validationFunc func(cmd *cobra.Command) bool
intSetFunc IntSetFunc
floatSetFunc FloatSetFunc
stringSetFunc StringSetFunc
boolSetFunc BoolSetFunc
validationFunc ValidationFunc
validationDescription string
}

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

// newIntFlag creates a new int flag and adds it to the command.
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 {
func newIntFlag(cmd *cobra.Command, name string, defaultValue int, setFunc IntSetFunc, help string, validationDescription string, validationFunc ValidationFunc) flagDefinition {
cmd.Flags().Int(name, defaultValue, help)
pFlag := cmd.Flags().Lookup(name)
return flagDefinition{
Expand All @@ -60,7 +60,7 @@ func newIntFlag(cmd *cobra.Command, name string, defaultValue int, setFunc func(
}

// newFloat64Flag creates a new float64 flag and adds it to the command.
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 {
func newFloat64Flag(cmd *cobra.Command, name string, defaultValue float64, setFunc FloatSetFunc, help string, validationDescription string, validationFunc ValidationFunc) flagDefinition {
cmd.Flags().Float64(name, defaultValue, help)
pFlag := cmd.Flags().Lookup(name)
return flagDefinition{
Expand All @@ -72,7 +72,7 @@ func newFloat64Flag(cmd *cobra.Command, name string, defaultValue float64, setFu
}

// newStringFlag creates a new string flag and adds it to the command.
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 {
func newStringFlag(cmd *cobra.Command, name string, defaultValue string, setFunc StringSetFunc, help string, validationDescription string, validationFunc ValidationFunc) flagDefinition {
cmd.Flags().String(name, defaultValue, help)
pFlag := cmd.Flags().Lookup(name)
return flagDefinition{
Expand All @@ -84,7 +84,7 @@ func newStringFlag(cmd *cobra.Command, name string, defaultValue string, setFunc
}

// newBoolFlag creates a new boolean flag and adds it to the command.
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 {
func newBoolFlag(cmd *cobra.Command, name string, defaultValue bool, setFunc BoolSetFunc, help string, validationDescription string, validationFunc ValidationFunc) flagDefinition {
cmd.Flags().Bool(name, defaultValue, help)
pFlag := cmd.Flags().Lookup(name)
return flagDefinition{
Expand Down
Loading
Loading