diff --git a/README.md b/README.md index a0153ef..41b0e0c 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,12 @@ This is most commonly used for retrieving ad-hoc information without too much fu ## Usage ```bash -boxes run -t theta,testalex.h -c hostname +$ boxes run -t theta,testalex.h -c hostname INFO[0000] theta: theta INFO[0000] testalex.h: j6yt29-testalex-magweb-do.nodes.hypernode.io + +# Format output with Go template syntax +$ boxes run -t theta,testalex.h -c hostname --format "{{.Target}} -> {{.Stdout}}" +INFO[0000] theta -> theta +INFO[0000] testalex.h -> j6yt29-testalex-magweb-do.nodes.hypernode.io ``` diff --git a/cmd/logging.go b/cmd/logging.go new file mode 100644 index 0000000..5894446 --- /dev/null +++ b/cmd/logging.go @@ -0,0 +1,28 @@ +package cmd + +import ( + "bytes" + "text/template" +) + +// Type for results +type Result struct { + Target string + Hostname string + Stdout string + Stderr string + Error error +} + +func (result *Result) toString(format string) string { + tmpl, err := template.New("result").Parse(format) + if err != nil { + panic(err) + } + var buf bytes.Buffer + err = tmpl.Execute(&buf, result) + if err != nil { + panic(err) + } + return buf.String() +} diff --git a/cmd/run.go b/cmd/run.go index 647e5f9..a84df8a 100644 --- a/cmd/run.go +++ b/cmd/run.go @@ -17,9 +17,10 @@ var runCmd = &cobra.Command{ targets, _ := cmd.Flags().GetStringSlice("target") commands, _ := cmd.Flags().GetStringSlice("command") outputFile, _ := cmd.Flags().GetString("output") + format, _ := cmd.Flags().GetString("format") // Execute the commands - executeCommands(targets, commands, outputFile) + executeCommands(targets, commands, outputFile, format) }, } @@ -28,5 +29,7 @@ func init() { runCmd.Flags().StringSliceP("target", "t", []string{}, "Target host") runCmd.Flags().StringSliceP("command", "c", []string{}, "Command to run") + runCmd.Flags().StringP("format", "f", "{{.Target}}: {{.Stdout}}", + "Output format in Go template syntax. Available fields: Target, Hostname, Stdout, Stderr, Error") runCmd.Flags().StringP("output", "o", "", "Output file") } diff --git a/cmd/ssh.go b/cmd/ssh.go index 60aa549..2f52ef7 100644 --- a/cmd/ssh.go +++ b/cmd/ssh.go @@ -3,15 +3,13 @@ package cmd import ( "fmt" "os" - "strings" "sync" "github.com/appleboy/easyssh-proxy" - "github.com/kevinburke/ssh_config" log "github.com/sirupsen/logrus" ) -func executeCommands(targets []string, commands []string, outputFile string) { +func executeCommands(targets []string, commands []string, outputFile string, format string) { // Open output file in append mode var file *os.File var err error @@ -32,26 +30,18 @@ func executeCommands(targets []string, commands []string, outputFile string) { ssh := getConfigForHost(target) // TODO: Use same SSH connection for all commands for _, command := range commands { - stdout, stderr, _, err := ssh.Run(command) + result := RunAndParse(target, command, ssh) if err != nil { log.Warn("Error running command on ", target, ": ", err) continue } - // Identify host with output - log.Info(target, ": ", stdout) - if stderr != "" { - log.Info(target, ": ", stderr) - } + output := result.toString(format) + log.Info(output) // Write output to file if given if file != nil { - if _, err := file.WriteString(fmt.Sprintf("%s: %s\n%s\n", target, command, stdout)); err != nil { + if _, err := file.WriteString(fmt.Sprintf("%s\n", output)); err != nil { log.Warn("Error writing to file: ", err) } - if stderr != "" { - if _, err := file.WriteString(fmt.Sprintf("%s: %s\n%s\n", target, command, stderr)); err != nil { - log.Warn("Error writing to file: ", err) - } - } } } }(target) @@ -59,60 +49,13 @@ func executeCommands(targets []string, commands []string, outputFile string) { wg.Wait() } -func getConfigForHost(target string) *easyssh.MakeConfig { - proxy := ssh_config.Get(target, "ProxyJump") - user := ssh_config.Get(target, "User") - if user == "" { - user = os.Getenv("USER") - } - port := ssh_config.Get(target, "Port") - if port == "" { - port = "22" - } - hostname := fillSSHConfigHostname(target, ssh_config.Get(target, "HostName")) - if hostname == "" { - hostname = target - } - - log.Debug("Creating SSH config for target ", target, " with hostname ", hostname, " and proxy ", proxy) - - if proxy != "" { - log.Debug("Using proxy ", proxy, " for target ", target) - return &easyssh.MakeConfig{ - User: user, - Server: hostname, - Port: port, - Proxy: *makeConfigToDefaultConfig(getConfigForHost(proxy)), - } - } else { - log.Debug("No proxy found for target ", target) - return &easyssh.MakeConfig{ - User: user, - Server: hostname, - Port: port, - } - } -} - -func fillSSHConfigHostname(target string, configHostName string) string { - // Check if "%h" is in the config name - if strings.Contains(configHostName, "%h") { - // Replace %h with the target - return strings.ReplaceAll(configHostName, "%h", target) - } - - // Nothing to replace - return configHostName -} - -// Note: this is necessary for generating proxy configs -func makeConfigToDefaultConfig(makeConfig *easyssh.MakeConfig) *easyssh.DefaultConfig { - return &easyssh.DefaultConfig{ - User: makeConfig.User, - Server: makeConfig.Server, - Port: makeConfig.Port, - Password: makeConfig.Password, - KeyPath: makeConfig.KeyPath, - Timeout: makeConfig.Timeout, +func RunAndParse(target string, command string, ssh *easyssh.MakeConfig) Result { + stdout, stderr, _, err := ssh.Run(command) + return Result{ + Target: target, + Hostname: ssh.Server, + Stdout: stdout, + Stderr: stderr, + Error: err, } } diff --git a/cmd/ssh_config.go b/cmd/ssh_config.go new file mode 100644 index 0000000..1fb4aa8 --- /dev/null +++ b/cmd/ssh_config.go @@ -0,0 +1,68 @@ +package cmd + +import ( + "os" + "strings" + + "github.com/appleboy/easyssh-proxy" + "github.com/kevinburke/ssh_config" + log "github.com/sirupsen/logrus" +) + +func getConfigForHost(target string) *easyssh.MakeConfig { + proxy := ssh_config.Get(target, "ProxyJump") + user := ssh_config.Get(target, "User") + if user == "" { + user = os.Getenv("USER") + } + port := ssh_config.Get(target, "Port") + if port == "" { + port = "22" + } + hostname := fillSSHConfigHostname(target, ssh_config.Get(target, "HostName")) + if hostname == "" { + hostname = target + } + + log.Debug("Creating SSH config for target ", target, " with hostname ", hostname, " and proxy ", proxy) + + if proxy != "" { + log.Debug("Using proxy ", proxy, " for target ", target) + return &easyssh.MakeConfig{ + User: user, + Server: hostname, + Port: port, + Proxy: *makeConfigToDefaultConfig(getConfigForHost(proxy)), + } + } else { + log.Debug("No proxy found for target ", target) + return &easyssh.MakeConfig{ + User: user, + Server: hostname, + Port: port, + } + } +} + +func fillSSHConfigHostname(target string, configHostName string) string { + // Check if "%h" is in the config name + if strings.Contains(configHostName, "%h") { + // Replace %h with the target + return strings.ReplaceAll(configHostName, "%h", target) + } + + // Nothing to replace + return configHostName +} + +// Note: this is necessary for generating proxy configs +func makeConfigToDefaultConfig(makeConfig *easyssh.MakeConfig) *easyssh.DefaultConfig { + return &easyssh.DefaultConfig{ + User: makeConfig.User, + Server: makeConfig.Server, + Port: makeConfig.Port, + Password: makeConfig.Password, + KeyPath: makeConfig.KeyPath, + Timeout: makeConfig.Timeout, + } +}