@@ -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"
2526var 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
0 commit comments