diff --git a/.changes/v0.12.1.md b/.changes/v0.12.1.md new file mode 100644 index 0000000..57166a3 --- /dev/null +++ b/.changes/v0.12.1.md @@ -0,0 +1,8 @@ +## v0.12.1 - 2026-03-23 +### Added +* Command output respects `command.[name].output.toLog` when standard output is enabled +### Changed +* SSH: revert scriptFile to before 0.11.3 +* Output: tabs print as 4 spaces instead of '\t' +### Fixed +* Internal: output buffer will be copied to temporary buffer diff --git a/.vscode/settings.json b/.vscode/settings.json index 08c6817..fbb5086 100755 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -10,5 +10,14 @@ "nikoksr", "Strs" ], - "CodeGPT.apiKey": "CodeGPT Plus Beta" + "CodeGPT.apiKey": "CodeGPT Plus Beta", + "yaml.schemas": { + "file:///c%3A/Users/anw12/.vscode/extensions/continue.continue-1.2.14-win32-x64/config-yaml-schema.json": [ + ".continue/**/*.yaml" + ], + "file:///c%3A/Users/anw12/.vscode/extensions/continue.continue-1.2.16-win32-x64/config-yaml-schema.json": [ + ".continue/**/*.yaml" + ], + "file:///c:/Users/anw12/.vscode/extensions/continue.continue-1.2.16-win32-x64/config-yaml-schema.json": "vscode-local:/c%3A/Users/anw12/.continue/config.yaml" + } } \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index ce728b6..6d76078 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,15 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html), and is generated by [Changie](https://github.com/miniscruff/changie). +## v0.12.1 - 2026-03-23 +### Added +* Command output respects `command.[name].output.toLog` when standard output is enabled +### Changed +* SSH: revert scriptFile to before 0.11.3 +* Output: tabs print as 4 spaces instead of '\t' +### Fixed +* Internal: output buffer will be copied to temporary buffer + ## v0.12.0 - 2026-02-11 ### Changed * internal logic handling for cron webserver diff --git a/cmd/validate.go b/cmd/validate.go new file mode 100644 index 0000000..0f33310 --- /dev/null +++ b/cmd/validate.go @@ -0,0 +1,20 @@ +package cmd + +import "testing" + +// validate checks commands passed in from the command line + +/* +*args: +* -t - type of cmd +* -c + + */ + +func TestUserCmd(t *testing.T) { + + /* + + */ + +} diff --git a/cmd/version.go b/cmd/version.go index a0f9154..64e8e58 100755 --- a/cmd/version.go +++ b/cmd/version.go @@ -7,7 +7,7 @@ import ( "github.com/spf13/cobra" ) -const versionStr = "0.12.0" +const versionStr = "0.12.1" var ( versionCmd = &cobra.Command{ diff --git a/localDeploy b/localDeploy new file mode 100755 index 0000000..2b5632e --- /dev/null +++ b/localDeploy @@ -0,0 +1,5 @@ +#!/bin/bash +go install . +systemctl --user stop backy +cp ~/go/bin/backy ~/prodConfigs/backups/backy/backy +systemctl --user start backy \ No newline at end of file diff --git a/pkg/backy/backy.go b/pkg/backy/backy.go index 8b9fd79..87b1092 100755 --- a/pkg/backy/backy.go +++ b/pkg/backy/backy.go @@ -14,7 +14,6 @@ import ( "strings" "sync" "text/template" - "time" "embed" @@ -152,28 +151,7 @@ func (e *LocalCommandExecutor) Run(cmd *Command, opts *ConfigOpts, logger zerolo // caller is responsible for closing the returned *os.File when non-nil. func makeCmdOutWriters(buf *bytes.Buffer, outputFile string) (io.Writer, *os.File, error) { writers := io.MultiWriter(buf) - if IsCmdStdOutEnabled() { - console := zerolog.ConsoleWriter{Out: os.Stdout, TimeFormat: time.RFC1123} - console.FormatLevel = func(i interface{}) string { - return strings.ToUpper(fmt.Sprintf("| %-6s|", i)) - } - console.FormatMessage = func(i any) string { - if i == nil { - return "" - } - return fmt.Sprintf("MSG: %s", i) - } - console.FormatFieldName = func(i interface{}) string { - return fmt.Sprintf("%s: ", i) - } - console.FormatFieldValue = func(i interface{}) string { - return fmt.Sprintf("%s", i) - // return strings.ToUpper(fmt.Sprintf("%s", i)) - } - - writers = io.MultiWriter(console, writers) - } if outputFile != "" { fileLogger := &lumberjack.Logger{ @@ -202,7 +180,7 @@ func (opts *ConfigOpts) ensureRemoteHost(localCmd *Command, host string) { } } // fallback: create a minimal Host so RunCmdOnHost sees a non-nil RemoteHost. - // This uses host as the address/alias; further fields (user/key) will use defaults. + // This uses host as the address/alias; further fields (user/key) will use be looked up as needed. localCmd.RemoteHost = &Host{Host: host} } @@ -227,11 +205,13 @@ func (opts *ConfigOpts) ExecCommandOnHostsParallel(cmdName string) ([]CmdResult, // shallow copy to avoid races local := *cmdObj local.Host = h - opts.Logger.Debug().Str("host", h).Msg("executing command in parallel on host") + opts.Logger.Info().Str("host", h).Msg("executing command in parallel on host") + local.cmdLoggers.global = opts.Logger var err error if IsHostLocal(h) { - _, err := local.RunCmd(local.GenerateLogger(opts), opts) + local.GenerateLogger(opts) + _, err := local.RunCmd(local.cmdLoggers.cmdContxt, opts) resultsCh <- CmdResult{CmdName: cmdName, ListName: "", Error: err} return // _, err = local.RunCmd(local.GenerateLogger(opts), opts) @@ -240,9 +220,10 @@ func (opts *ConfigOpts) ExecCommandOnHostsParallel(cmdName string) ([]CmdResult, // ensure RemoteHost is populated before calling RunCmdOnHost opts.ensureRemoteHost(&local, h) - _, err = local.RunCmdOnHost(local.GenerateLogger(opts), opts) + local.GenerateLogger(opts) + _, err = local.RunCmdOnHost(local.cmdLoggers.cmdContxt, opts) - resultsCh <- CmdResult{CmdName: cmdName, ListName: "", Error: err} + resultsCh <- CmdResult{CmdName: cmdName, ListName: "", Error: err, HostName: h} }(host) } @@ -252,6 +233,9 @@ func (opts *ConfigOpts) ExecCommandOnHostsParallel(cmdName string) ([]CmdResult, var results []CmdResult for r := range resultsCh { results = append(results, r) + if r.Error != nil { + opts.Logger.Info().AnErr("error", r.Error).Str("cmd", r.CmdName).Send() + } } return results, nil } @@ -282,7 +266,11 @@ func (command *Command) RunCmd(cmdCtxLogger zerolog.Logger, opts *ConfigOpts) ([ cmdCtxLogger.Warn().Msg("both 'host' and 'hosts' are set; 'hosts' will be ignored") return nil, fmt.Errorf("both 'host' and 'hosts' are set; please set one or the other") } else if command.Hosts != nil { - opts.ExecCommandOnHostsParallel(command.Name) + _, err := opts.ExecCommandOnHostsParallel(command.Name) + if err != nil { + opts.Logger.Err(err).Send() + return nil, err + } return nil, nil } @@ -296,6 +284,7 @@ func (command *Command) RunCmd(cmdCtxLogger zerolog.Logger, opts *ConfigOpts) ([ if command.Type == UserCommandType { if command.UserOperation == "password" { + command.cmdLoggers.global.Info().Str("password", command.UserPassword).Msg("user password to be updated") cmdCtxLogger.Info().Str("password", command.UserPassword).Msg("user password to be updated") } } @@ -436,7 +425,7 @@ func (command *Command) RunCmd(cmdCtxLogger zerolog.Logger, opts *ConfigOpts) ([ localCMD := exec.Command(fmt.Sprintf("grep \"%s\" /etc/passwd | cut -d: -f6", command.Username)) userHome, err = localCMD.CombinedOutput() if err != nil { - return collectOutput(&cmdOutBuf, command.Name, cmdCtxLogger, command.Output.ToLog), fmt.Errorf("error finding user home from /etc/passwd: %v", err) + return collectOutput(&cmdOutBuf, command.Name, command.cmdLoggers.cmdContxt, command.cmdLoggers.global, command.Output.ToLog), fmt.Errorf("error finding user home from /etc/passwd: %v", err) } command.UserHome = strings.TrimSpace(string(userHome)) @@ -445,33 +434,33 @@ func (command *Command) RunCmd(cmdCtxLogger zerolog.Logger, opts *ConfigOpts) ([ if _, err := os.Stat(userSshDir); os.IsNotExist(err) { err := os.MkdirAll(userSshDir, 0700) if err != nil { - return collectOutput(&cmdOutBuf, command.Name, cmdCtxLogger, command.Output.ToLog), fmt.Errorf("error creating directory %s %v", userSshDir, err) + return collectOutput(&cmdOutBuf, command.Name, command.cmdLoggers.cmdContxt, command.cmdLoggers.global, command.Output.ToLog), fmt.Errorf("error creating directory %s %v", userSshDir, err) } } if _, err := os.Stat(fmt.Sprintf("%s/authorized_keys", userSshDir)); os.IsNotExist(err) { _, err := os.Create(fmt.Sprintf("%s/authorized_keys", userSshDir)) if err != nil { - return collectOutput(&cmdOutBuf, command.Name, cmdCtxLogger, command.Output.ToLog), fmt.Errorf("error creating file %s/authorized_keys: %v", userSshDir, err) + return collectOutput(&cmdOutBuf, command.Name, command.cmdLoggers.cmdContxt, command.cmdLoggers.global, command.Output.ToLog), fmt.Errorf("error creating file %s/authorized_keys: %v", userSshDir, err) } } authorizedKeysFile, err = os.OpenFile(fmt.Sprintf("%s/authorized_keys", userSshDir), 0700, os.ModeAppend) if err != nil { - return collectOutput(&cmdOutBuf, command.Name, cmdCtxLogger, command.Output.ToLog), fmt.Errorf("error opening file %s/authorized_keys: %v", userSshDir, err) + return collectOutput(&cmdOutBuf, command.Name, command.cmdLoggers.cmdContxt, command.cmdLoggers.global, command.Output.ToLog), fmt.Errorf("error opening file %s/authorized_keys: %v", userSshDir, err) } defer authorizedKeysFile.Close() for _, k := range command.UserSshPubKeys { buf := bytes.NewBufferString(k) cmdCtxLogger.Info().Str("key", k).Msg("adding SSH key") if _, err := authorizedKeysFile.ReadFrom(buf); err != nil { - return collectOutput(&cmdOutBuf, command.Name, cmdCtxLogger, command.Output.ToLog), fmt.Errorf("error adding to authorized keys: %v", err) + return collectOutput(&cmdOutBuf, command.Name, command.cmdLoggers.cmdContxt, command.cmdLoggers.global, command.Output.ToLog), fmt.Errorf("error adding to authorized keys: %v", err) } } localCMD = exec.Command(fmt.Sprintf("chown -R %s:%s %s", command.Username, command.Username, userHome)) _, err = localCMD.CombinedOutput() if err != nil { - return collectOutput(&cmdOutBuf, command.Name, cmdCtxLogger, command.Output.ToLog), err + return collectOutput(&cmdOutBuf, command.Name, command.cmdLoggers.cmdContxt, command.cmdLoggers.global, command.Output.ToLog), err } } @@ -495,10 +484,10 @@ func cmdListWorker(msgTemps *msgTemplates, jobs <-chan *CmdList, results chan<- commandExecuted = cmdToRun currentCmd := cmdToRun.Name fieldsMap["cmd"] = currentCmd - cmdLogger = cmdToRun.GenerateLogger(opts) - cmdLogger.Info().Fields(fieldsMap).Send() + cmdToRun.GenerateLogger(opts) + cmdToRun.cmdLoggers.cmdContxt.Info().Fields(fieldsMap).Send() - outputArr, runErr := cmdToRun.RunCmd(cmdLogger, opts) + outputArr, runErr := cmdToRun.RunCmd(cmdToRun.cmdLoggers.cmdContxt, opts) cmdsRan = append(cmdsRan, cmd) if runErr != nil { @@ -559,10 +548,10 @@ func cmdListWorkerWithHosts(msgTemps *msgTemplates, jobs <-chan *CmdList, hosts commandExecuted = cmdToRun currentCmd := cmdToRun.Name fieldsMap["cmd"] = currentCmd - cmdLogger = cmdToRun.GenerateLogger(opts) - cmdLogger.Info().Fields(fieldsMap).Send() + cmdToRun.GenerateLogger(opts) + cmdToRun.cmdLoggers.cmdContxt.Info().Fields(fieldsMap).Send() - outputArr, runErr := cmdToRun.RunCmd(cmdLogger, opts) + outputArr, runErr := cmdToRun.RunCmd(cmdToRun.cmdLoggers.cmdContxt, opts) cmdsRan = append(cmdsRan, cmd) if runErr != nil { @@ -705,8 +694,8 @@ func cmdListWorkerExecuteCommandsInParallel(msgTemps *msgTemplates, jobs <-chan cmdToRun.Host = host.Host cmdToRun.RemoteHost = host } - cmdLogger = cmdToRun.GenerateLogger(opts) - cmdLogger.Info().Fields(fieldsMap).Send() + cmdToRun.GenerateLogger(opts) + cmdToRun.cmdLoggers.cmdContxt.Info().Fields(fieldsMap).Send() print("Running cmd on: ", host.Host, "\n") go func(cmd string, host *Host) { @@ -714,7 +703,7 @@ func cmdListWorkerExecuteCommandsInParallel(msgTemps *msgTemplates, jobs <-chan currentCmd := cmdToRun.Name fieldsMap["cmd"] = currentCmd - outputArr, runErr := cmdToRun.RunCmd(cmdLogger, opts) + outputArr, runErr := cmdToRun.RunCmd(cmdToRun.cmdLoggers.cmdContxt, opts) if runErr != nil { cmdLogger.Err(runErr).Send() cmdToRun.ExecuteHooks("error", opts) @@ -890,8 +879,8 @@ func (opts *ConfigOpts) ExecuteListOnHosts(lists []string, parallel bool) { func (opts *ConfigOpts) ExecuteCmds() { for _, cmd := range opts.executeCmds { cmdToRun := opts.Cmds[cmd] - cmdLogger := cmdToRun.GenerateLogger(opts) - _, runErr := cmdToRun.RunCmd(cmdLogger, opts) + cmdToRun.GenerateLogger(opts) + _, runErr := cmdToRun.RunCmd(cmdToRun.cmdLoggers.cmdContxt, opts) if runErr != nil { opts.Logger.Err(runErr).Send() cmdToRun.ExecuteHooks("error", opts) @@ -979,30 +968,29 @@ func (cmd *Command) ExecuteHooks(hookType string, opts *ConfigOpts) { } } -func (cmd *Command) GenerateLogger(opts *ConfigOpts) zerolog.Logger { - cmdLogger := opts.Logger.With(). +func (cmd *Command) GenerateLogger(opts *ConfigOpts) { + cmd.cmdLoggers.cmdContxt = opts.Logger.With(). Str("Backy-cmd", cmd.Name).Str("Host", "local machine"). Logger() if !IsHostLocal(cmd.Host) { - cmdLogger = opts.Logger.With(). + cmd.cmdLoggers.cmdContxt = opts.Logger.With(). Str("Backy-cmd", cmd.Name).Str("Host", cmd.Host). Logger() } - return cmdLogger } func (cmd *Command) GenerateLoggerForCmd(logger zerolog.Logger) zerolog.Logger { - cmdLogger := logger.With(). + cmd.cmdLoggers.cmdContxt = logger.With(). Str("Backy-cmd", cmd.Name).Str("Host", "local machine"). Logger() if !IsHostLocal(cmd.Host) { - cmdLogger = logger.With(). + cmd.cmdLoggers.cmdContxt = logger.With(). Str("Backy-cmd", cmd.Name).Str("Host", cmd.Host). Logger() } - return cmdLogger + return cmd.cmdLoggers.cmdContxt } func (opts *ConfigOpts) ExecCmdsOnHosts(cmdList []string, hostsList []string) { @@ -1014,7 +1002,8 @@ func (opts *ConfigOpts) ExecCmdsOnHosts(cmdList []string, hostsList []string) { cmd.RemoteHost = host cmd.Host = h if IsHostLocal(h) { - _, err := cmd.RunCmd(cmd.GenerateLogger(opts), opts) + cmd.GenerateLogger(opts) + _, err := cmd.RunCmd(cmd.cmdLoggers.cmdContxt, opts) if err != nil { opts.Logger.Err(err).Str("host", h).Str("cmd", c).Send() } @@ -1022,7 +1011,8 @@ func (opts *ConfigOpts) ExecCmdsOnHosts(cmdList []string, hostsList []string) { cmd.Host = host.Host opts.Logger.Info().Str("host", h).Str("cmd", c).Send() - _, err := cmd.RunCmdOnHost(cmd.GenerateLogger(opts), opts) + cmd.GenerateLogger(opts) + _, err := cmd.RunCmdOnHost(cmd.cmdLoggers.cmdContxt, opts) if err != nil { opts.Logger.Err(err).Str("host", h).Str("cmd", c).Send() } @@ -1041,7 +1031,8 @@ func (opts *ConfigOpts) ExecCmdsOnHostsInParallel(cmdList []string, hostsList [] cmd.RemoteHost = host cmd.Host = h if IsHostLocal(h) { - _, err := cmd.RunCmd(cmd.GenerateLogger(opts), opts) + cmd.GenerateLogger(opts) + _, err := cmd.RunCmd(cmd.cmdLoggers.cmdContxt, opts) if err != nil { opts.Logger.Err(err).Str("host", h).Str("cmd", c).Send() } @@ -1049,7 +1040,8 @@ func (opts *ConfigOpts) ExecCmdsOnHostsInParallel(cmdList []string, hostsList [] cmd.Host = host.Host opts.Logger.Info().Str("host", h).Str("cmd", c).Send() - _, err := cmd.RunCmdOnHost(cmd.GenerateLogger(opts), opts) + cmd.GenerateLogger(opts) + _, err := cmd.RunCmdOnHost(cmd.cmdLoggers.cmdContxt, opts) if err != nil { opts.Logger.Err(err).Str("host", h).Str("cmd", c).Send() } diff --git a/pkg/backy/ssh.go b/pkg/backy/ssh.go index 0c0ed00..fce953f 100755 --- a/pkg/backy/ssh.go +++ b/pkg/backy/ssh.go @@ -472,7 +472,7 @@ func (command *Command) RunCmdOnHost(cmdCtxLogger zerolog.Logger, opts *ConfigOp defer commandSession.Close() // Set output writers - var file *os.File + // var file *os.File if !IsHostLocal(command.Host) && command.Output.File != "" { if filepath.IsAbs(command.Output.File) { fileName := filepath.Base(command.Output.File) @@ -483,19 +483,19 @@ func (command *Command) RunCmdOnHost(cmdCtxLogger zerolog.Logger, opts *ConfigOp } } - cmdOutWriters, file, err = makeCmdOutWriters(&cmdOutBuf, command.Output.File) + cmdOutWriters, _, err = makeCmdOutWriters(&cmdOutBuf, command.Output.File) if err != nil { return nil, fmt.Errorf("error creating command output writers: %w", err) } - defer func() { - if file != nil { - file.Close() - } - }() + // defer func() { + // if file != nil { + // file.Close() + // } + // }() // cmdOutWriters = logging.SetLoggingWriterForCommand(&cmdOutBuf, command.Output.File, IsCmdStdOutEnabled()) cmdCtxLogger = zerolog.New(cmdOutWriters).With().Timestamp().Logger() - cmdCtxLogger = command.GenerateLoggerForCmd(cmdCtxLogger) - + // cmdCtxLogger = command.GenerateLoggerForCmd(cmdCtxLogger) + command.cmdLoggers.cmdContxt = cmdCtxLogger // cmdCtxLogger.Info().Msgf("Executing %s", command.Cmd) commandSession.Stdout = cmdOutWriters commandSession.Stderr = cmdOutWriters @@ -513,13 +513,13 @@ func (command *Command) RunCmdOnHost(cmdCtxLogger zerolog.Logger, opts *ConfigOp // Handle command execution based on type switch command.Type { case ScriptCommandType: - return command.runScript(commandSession, cmdCtxLogger, &cmdOutBuf) + return command.runScript(commandSession, &cmdOutBuf) case RemoteScriptCommandType: - return command.runRemoteScript(commandSession, cmdCtxLogger, &cmdOutBuf) + return command.runRemoteScript(commandSession, &cmdOutBuf) case ScriptFileCommandType: - commandSession.Stdout = nil - commandSession.Stderr = nil - return command.runScriptFile(commandSession, cmdCtxLogger, opts.Logger, &cmdOutBuf) + // commandSession.Stdout = nil + // commandSession.Stderr = nil + return command.runScriptFile(commandSession, &cmdOutBuf) case PackageCommandType: var remoteHostPackageExecutor RemoteHostPackageExecutor return remoteHostPackageExecutor.RunCmdOnHost(command, commandSession, cmdCtxLogger, cmdOutBuf) @@ -541,18 +541,18 @@ func (command *Command) RunCmdOnHost(cmdCtxLogger zerolog.Logger, opts *ConfigOp userNamePass := fmt.Sprintf("%s:%s", command.Username, command.UserPassword) client, err := sftp.NewClient(command.RemoteHost.SshClient) if err != nil { - return collectOutput(&cmdOutBuf, command.Name, cmdCtxLogger, command.Output.ToLog), fmt.Errorf("error creating sftp client: %v", err) + return collectOutput(&cmdOutBuf, command.Name, command.cmdLoggers.cmdContxt, command.cmdLoggers.global, command.Output.ToLog), fmt.Errorf("error creating sftp client: %v", err) } uuidFile := uuid.New() passFilePath := fmt.Sprintf("/tmp/%s", uuidFile.String()) passFile, passFileErr := client.Create(passFilePath) if passFileErr != nil { - return collectOutput(&cmdOutBuf, command.Name, cmdCtxLogger, command.Output.ToLog), fmt.Errorf("error creating file /tmp/%s: %v", uuidFile.String(), passFileErr) + return collectOutput(&cmdOutBuf, command.Name, command.cmdLoggers.cmdContxt, command.cmdLoggers.global, command.Output.ToLog), fmt.Errorf("error creating file /tmp/%s: %v", uuidFile.String(), passFileErr) } _, err = passFile.Write([]byte(userNamePass)) if err != nil { - return collectOutput(&cmdOutBuf, command.Name, cmdCtxLogger, command.Output.ToLog), fmt.Errorf("error writing to file /tmp/%s: %v", uuidFile.String(), err) + return collectOutput(&cmdOutBuf, command.Name, command.cmdLoggers.cmdContxt, command.cmdLoggers.global, command.Output.ToLog), fmt.Errorf("error writing to file /tmp/%s: %v", uuidFile.String(), err) } ArgsStr = fmt.Sprintf("cat %s | chpasswd", passFilePath) @@ -566,7 +566,7 @@ func (command *Command) RunCmdOnHost(cmdCtxLogger zerolog.Logger, opts *ConfigOp defer rmFileFunc() } if err := commandSession.Run(command.ArgStr); err != nil { - return collectOutput(&cmdOutBuf, command.Name, cmdCtxLogger, command.Output.ToLog), fmt.Errorf("error running command: %w", err) + return collectOutput(&cmdOutBuf, command.Name, command.cmdLoggers.cmdContxt, command.cmdLoggers.global, command.Output.ToLog), fmt.Errorf("error running command: %w", err) } if command.Type == UserCommandType { @@ -587,41 +587,41 @@ func (command *Command) RunCmdOnHost(cmdCtxLogger zerolog.Logger, opts *ConfigOp commandSession, _ = command.RemoteHost.createSSHSession(opts) userHome, err = commandSession.CombinedOutput(fmt.Sprintf("grep \"%s\" /etc/passwd | cut -d: -f6", command.Username)) if err != nil { - return collectOutput(&cmdOutBuf, command.Name, cmdCtxLogger, command.Output.ToLog), fmt.Errorf("error finding user home from /etc/passwd: %v", err) + return collectOutput(&cmdOutBuf, command.Name, command.cmdLoggers.cmdContxt, command.cmdLoggers.global, command.Output.ToLog), fmt.Errorf("error finding user home from /etc/passwd: %v", err) } command.UserHome = strings.TrimSpace(string(userHome)) userSshDir := fmt.Sprintf("%s/.ssh", command.UserHome) client, err = sftp.NewClient(command.RemoteHost.SshClient) if err != nil { - return collectOutput(&cmdOutBuf, command.Name, cmdCtxLogger, command.Output.ToLog), fmt.Errorf("error creating sftp client: %v", err) + return collectOutput(&cmdOutBuf, command.Name, command.cmdLoggers.cmdContxt, command.cmdLoggers.global, command.Output.ToLog), fmt.Errorf("error creating sftp client: %v", err) } err = client.MkdirAll(userSshDir) if err != nil { - return collectOutput(&cmdOutBuf, command.Name, cmdCtxLogger, command.Output.ToLog), fmt.Errorf("error creating directory %s: %v", userSshDir, err) + return collectOutput(&cmdOutBuf, command.Name, command.cmdLoggers.cmdContxt, command.cmdLoggers.global, command.Output.ToLog), fmt.Errorf("error creating directory %s: %v", userSshDir, err) } _, err = client.Create(fmt.Sprintf("%s/authorized_keys", userSshDir)) if err != nil { - return collectOutput(&cmdOutBuf, command.Name, cmdCtxLogger, command.Output.ToLog), fmt.Errorf("error opening file %s/authorized_keys: %v", userSshDir, err) + return collectOutput(&cmdOutBuf, command.Name, command.cmdLoggers.cmdContxt, command.cmdLoggers.global, command.Output.ToLog), fmt.Errorf("error opening file %s/authorized_keys: %v", userSshDir, err) } f, err = client.OpenFile(fmt.Sprintf("%s/authorized_keys", userSshDir), os.O_APPEND|os.O_CREATE|os.O_WRONLY) if err != nil { - return collectOutput(&cmdOutBuf, command.Name, cmdCtxLogger, command.Output.ToLog), fmt.Errorf("error opening file %s/authorized_keys: %v", userSshDir, err) + return collectOutput(&cmdOutBuf, command.Name, command.cmdLoggers.cmdContxt, command.cmdLoggers.global, command.Output.ToLog), fmt.Errorf("error opening file %s/authorized_keys: %v", userSshDir, err) } defer f.Close() for _, k := range command.UserSshPubKeys { buf := bytes.NewBufferString(k) cmdCtxLogger.Info().Str("key", k).Msg("adding SSH key") if _, err := f.ReadFrom(buf); err != nil { - return collectOutput(&cmdOutBuf, command.Name, cmdCtxLogger, command.Output.ToLog), fmt.Errorf("error adding to authorized keys: %v", err) + return collectOutput(&cmdOutBuf, command.Name, command.cmdLoggers.cmdContxt, command.cmdLoggers.global, command.Output.ToLog), fmt.Errorf("error adding to authorized keys: %v", err) } } commandSession, _ = command.RemoteHost.createSSHSession(opts) _, err = commandSession.CombinedOutput(fmt.Sprintf("chown -R %s:%s %s", command.Username, command.Username, userHome)) if err != nil { - return collectOutput(&cmdOutBuf, command.Name, cmdCtxLogger, command.Output.ToLog), err + return collectOutput(&cmdOutBuf, command.Name, command.cmdLoggers.cmdContxt, command.cmdLoggers.global, command.Output.ToLog), err } } @@ -629,7 +629,7 @@ func (command *Command) RunCmdOnHost(cmdCtxLogger zerolog.Logger, opts *ConfigOp } } - return collectOutput(&cmdOutBuf, command.Name, cmdCtxLogger, command.Output.ToLog), nil + return collectOutput(&cmdOutBuf, command.Name, command.cmdLoggers.cmdContxt, command.cmdLoggers.global, command.Output.ToLog), nil } func checkPackageVersion(cmdCtxLogger zerolog.Logger, command *Command, commandSession *ssh.Session, cmdOutBuf bytes.Buffer) ([]string, error) { @@ -649,9 +649,9 @@ func checkPackageVersion(cmdCtxLogger zerolog.Logger, command *Command, commandS _, parseErr := parsePackageVersion(string(cmdOut), cmdCtxLogger, command, cmdOutBuf) if parseErr != nil { - return collectOutput(&cmdOutBuf, command.Name, cmdCtxLogger, command.Output.ToLog), fmt.Errorf("error: packages %v not listed: %w", command.Packages, err) + return collectOutput(&cmdOutBuf, command.Name, command.cmdLoggers.cmdContxt, command.cmdLoggers.global, command.Output.ToLog), fmt.Errorf("error: packages %v not listed: %w", command.Packages, err) } - return collectOutput(&cmdOutBuf, command.Name, cmdCtxLogger, command.Output.ToLog), fmt.Errorf("error running %s: %w", ArgsStr, err) + return collectOutput(&cmdOutBuf, command.Name, command.cmdLoggers.cmdContxt, command.cmdLoggers.global, command.Output.ToLog), fmt.Errorf("error running %s: %w", ArgsStr, err) } return parsePackageVersion(string(cmdOut), cmdCtxLogger, command, cmdOutBuf) @@ -666,7 +666,7 @@ func getCommandTypeAndSetCommandInfoLabel(commandType CommandType) string { } // runScript handles the execution of inline scripts. -func (command *Command) runScript(session *ssh.Session, cmdCtxLogger zerolog.Logger, outputBuf *bytes.Buffer) ([]string, error) { +func (command *Command) runScript(session *ssh.Session, outputBuf *bytes.Buffer) ([]string, error) { script, err := command.prepareScriptBuffer() if err != nil { return nil, err @@ -678,60 +678,29 @@ func (command *Command) runScript(session *ssh.Session, cmdCtxLogger zerolog.Log } if err := session.Wait(); err != nil { - return collectOutput(outputBuf, command.Name, cmdCtxLogger, true), fmt.Errorf("error waiting for shell: %w", err) + return collectOutput(outputBuf, command.Name, command.cmdLoggers.cmdContxt, command.cmdLoggers.global, command.Output.ToLog), fmt.Errorf("error waiting for command: %w", err) } - return collectOutput(outputBuf, command.Name, cmdCtxLogger, command.Output.ToLog), nil + return collectOutput(outputBuf, command.Name, command.cmdLoggers.cmdContxt, command.cmdLoggers.global, command.Output.ToLog), nil } // runScriptFile handles the execution of script files. -func (command *Command) runScriptFile(session *ssh.Session, cmdCtxLogger, globalLogger zerolog.Logger, outputBuf *bytes.Buffer) ([]string, error) { +func (command *Command) runScriptFile(session *ssh.Session, outputBuf *bytes.Buffer) ([]string, error) { script, err := command.prepareScriptFileBuffer() if err != nil { return nil, err } session.Stdin = script - // modes := ssh.TerminalModes{ - // ssh.ECHO: 0, - // ssh.ECHOCTL: 0, - // ssh.TTY_OP_ISPEED: 14400, - // ssh.TTY_OP_OSPEED: 14400, - // } - - // session.RequestPty("xterm", 80, 40, modes) - - stdout, stdOutErr := session.StdoutPipe() - if stdOutErr != nil { - return nil, fmt.Errorf("error getting stdout pipe: %w", stdOutErr) - } - if err := session.Shell(); err != nil { return nil, fmt.Errorf("error starting shell: %w", err) } - var LogOutputToFile bool - if command.Output.File != "" || command.Output.ToLog { - if command.Output.File != "" { - globalLogger.Info().Str("file", command.Output.File).Msg("Writing script output to file") - } - LogOutputToFile = true - } - stdOutput, stdoOutReadErr := io.ReadAll(stdout) if err := session.Wait(); err != nil { - stdOutBuff := bytes.NewBuffer(stdOutput) - // outputBuf.Write(stdOutBuff.Bytes()) - // Read output - return collectOutput(stdOutBuff, command.Name, cmdCtxLogger, LogOutputToFile), fmt.Errorf("error waiting for shell: %w", err) + return collectOutput(outputBuf, command.Name, command.cmdLoggers.cmdContxt, command.cmdLoggers.global, command.Output.ToLog), fmt.Errorf("error waiting for shell: %w", err) } - // Read output - if stdoOutReadErr != nil { - return collectOutput(outputBuf, command.Name, cmdCtxLogger, LogOutputToFile), fmt.Errorf("error reading stdout after shell error: %w", stdoOutReadErr) - } - stdOutBuff := bytes.NewBuffer(stdOutput) - - return collectOutput(stdOutBuff, command.Name, cmdCtxLogger, LogOutputToFile), nil + return collectOutput(outputBuf, command.Name, command.cmdLoggers.cmdContxt, command.cmdLoggers.global, command.Output.ToLog), nil } // prepareScriptBuffer prepares a buffer for inline scripts. @@ -794,7 +763,7 @@ func (command *Command) prepareScriptFileBuffer() (*bytes.Buffer, error) { } // runRemoteScript handles the execution of remote scripts -func (command *Command) runRemoteScript(session *ssh.Session, cmdCtxLogger zerolog.Logger, outputBuf *bytes.Buffer) ([]string, error) { +func (command *Command) runRemoteScript(session *ssh.Session, outputBuf *bytes.Buffer) ([]string, error) { script, err := command.Fetcher.Fetch(command.Cmd) if err != nil { return nil, err @@ -806,10 +775,10 @@ func (command *Command) runRemoteScript(session *ssh.Session, cmdCtxLogger zerol err = session.Run(command.Shell) if err != nil { - return collectOutput(outputBuf, command.Name, cmdCtxLogger, command.Output.ToLog), fmt.Errorf("error running remote script: %w", err) + return collectOutput(outputBuf, command.Name, command.cmdLoggers.cmdContxt, command.cmdLoggers.global, command.Output.ToLog), fmt.Errorf("error running remote script: %w", err) } - return collectOutput(outputBuf, command.Name, cmdCtxLogger, command.Output.ToLog), nil + return collectOutput(outputBuf, command.Name, command.cmdLoggers.cmdContxt, command.cmdLoggers.global, command.Output.ToLog), nil } // readFileToBuffer reads a file into a buffer. @@ -908,7 +877,7 @@ func (r RemoteHostPackageExecutor) RunCmdOnHost(command *Command, commandSession cmdCtxLogger.Debug().Str("cmd + args", ArgsStr).Send() // Run simple command if err := commandSession.Run(ArgsStr); err != nil { - return collectOutput(&cmdOutBuf, command.Name, cmdCtxLogger, command.Output.ToLog), fmt.Errorf("error running command: %w", err) + return collectOutput(&cmdOutBuf, command.Name, command.cmdLoggers.cmdContxt, command.cmdLoggers.global, command.Output.ToLog), fmt.Errorf("error running command: %w", err) } - return collectOutput(&cmdOutBuf, command.Name, cmdCtxLogger, command.Output.ToLog), nil + return collectOutput(&cmdOutBuf, command.Name, command.cmdLoggers.cmdContxt, command.cmdLoggers.global, command.Output.ToLog), nil } diff --git a/pkg/backy/types.go b/pkg/backy/types.go index 37a0f28..d431a13 100755 --- a/pkg/backy/types.go +++ b/pkg/backy/types.go @@ -58,6 +58,8 @@ type ( // See CommandType enum further down the page for acceptable values Type CommandType `yaml:"type,omitempty"` + RawOutput bytes.Buffer + Host string `yaml:"host,omitempty"` Hosts []string `yaml:"hosts,omitempty"` @@ -141,6 +143,11 @@ type ( // END USER CommandType FIELDS + cmdLoggers struct { + global zerolog.Logger + cmdContxt zerolog.Logger + } + // BEGIN FILE COMMAND FIELDS FileOperation string `yaml:"fileOperation,omitempty"` @@ -303,6 +310,7 @@ type ( } CmdResult struct { + HostName string CmdName string // Name of the command executed ListName string // Name of the command list Error error // Error encountered, if any diff --git a/pkg/backy/utils.go b/pkg/backy/utils.go index f4bac02..615b9f6 100755 --- a/pkg/backy/utils.go +++ b/pkg/backy/utils.go @@ -16,6 +16,7 @@ import ( "path/filepath" "regexp" "strings" + "time" "git.andrewnw.xyz/CyberShell/backy/pkg/logging" "git.andrewnw.xyz/CyberShell/backy/pkg/remotefetcher" @@ -222,17 +223,23 @@ func CheckConfigValues(config *koanf.Koanf, file string) { } // collectOutput collects output from a buffer and logs it. -func collectOutput(buf *bytes.Buffer, commandName string, logger zerolog.Logger, wantOutput bool) []string { +func collectOutput(buf *bytes.Buffer, commandName string, logger, globalLogger zerolog.Logger, wantOutput bool) []string { var outputArr []string - scanner := bufio.NewScanner(buf) + copyBuf := bytes.NewBuffer(buf.Bytes()) + scanner := bufio.NewScanner(copyBuf) for scanner.Scan() { line := scanner.Text() clean := sanitizeString(line) outputArr = append(outputArr, clean) if wantOutput { + time.Sleep(10 * time.Millisecond) logger.Info().Str("cmd", commandName).Str("output", clean).Send() + if IsCmdStdOutEnabled() { + globalLogger.Info().Str("cmd", commandName).Str("output", clean).Send() + } } } + buf.Reset() return outputArr } @@ -240,6 +247,7 @@ func collectOutput(buf *bytes.Buffer, commandName string, logger zerolog.Logger, // while preserving tabs. This helps remove color codes and other terminal control // characters from remote command output. func sanitizeString(s string) string { + // Remove common ANSI CSI sequences like "\x1b[31m" etc. var ansiCSI = regexp.MustCompile(`\x1b\[[0-9;?]*[ -/]*[@-~]`) s = ansiCSI.ReplaceAllString(s, "") @@ -255,7 +263,10 @@ func sanitizeString(s string) string { var b strings.Builder for _, r := range s { - if r == '\t' || (r >= 0x20 && r != 0x7f) { + if r == '\t' { + b.WriteString(" ") + } + if r >= 0x20 && r != 0x7f { b.WriteRune(r) } } @@ -410,7 +421,7 @@ func parsePackageVersion(output string, cmdCtxLogger zerolog.Logger, command *Co pkgVersionOnSystem, err := command.pkgMan.ParseRemotePackageManagerVersionOutput(output) if err != nil { cmdCtxLogger.Error().AnErr("Error parsing package version output", err).Send() - return collectOutput(&cmdOutBuf, command.Name, cmdCtxLogger, command.Output.ToLog), fmt.Errorf("error parsing package version output: %v", err) + return collectOutput(&cmdOutBuf, command.Name, cmdCtxLogger, command.cmdLoggers.global, command.Output.ToLog), fmt.Errorf("error parsing package version output: %v", err) } for _, p := range pkgVersionOnSystem { @@ -453,9 +464,9 @@ func parsePackageVersion(output string, cmdCtxLogger zerolog.Logger, command *Co } } if errs == nil { - return collectOutput(&cmdOutBuf, command.Name, cmdCtxLogger, command.Output.ToLog), nil + return collectOutput(&cmdOutBuf, command.Name, command.cmdLoggers.cmdContxt, command.cmdLoggers.global, command.Output.ToLog), nil } - return collectOutput(&cmdOutBuf, command.Name, cmdCtxLogger, command.Output.ToLog), fmt.Errorf("error parsing package version output: %v", errs) + return collectOutput(&cmdOutBuf, command.Name, command.cmdLoggers.cmdContxt, command.cmdLoggers.global, command.Output.ToLog), fmt.Errorf("error parsing package version output: %v", errs) } func getPackageIndexFromCommand(command *Command, name string) int { diff --git a/pkg/backy/validate.go b/pkg/backy/validate.go new file mode 100644 index 0000000..da176ea --- /dev/null +++ b/pkg/backy/validate.go @@ -0,0 +1,7 @@ +package backy + +func Validate(c Command) { + if c.Cmd != "" { + + } +}