started refactoring into executors using interfaces
This commit is contained in:
@ -11,8 +11,8 @@ import (
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
"slices"
|
||||
"strings"
|
||||
"sync"
|
||||
"text/template"
|
||||
|
||||
"embed"
|
||||
@ -27,6 +27,124 @@ var requiredKeys = []string{"commands"}
|
||||
|
||||
var Sprintf = fmt.Sprintf
|
||||
|
||||
type CommandExecutor interface {
|
||||
Run(cmd *Command, opts *ConfigOpts, logger zerolog.Logger) ([]string, error)
|
||||
}
|
||||
|
||||
type OutputHandler interface {
|
||||
CollectOutput(buf *bytes.Buffer, commandName string, logger zerolog.Logger, wantOutput bool) []string
|
||||
}
|
||||
|
||||
type EnvInjector interface {
|
||||
Inject(cmd *Command, opts *ConfigOpts)
|
||||
}
|
||||
|
||||
type PackageCommandExecutor struct{}
|
||||
|
||||
func (e *PackageCommandExecutor) Run(cmd *Command, opts *ConfigOpts, logger zerolog.Logger) ([]string, error) {
|
||||
var (
|
||||
ArgsStr string
|
||||
cmdOutBuf bytes.Buffer
|
||||
outputArr []string
|
||||
cmdOutWriters io.Writer
|
||||
)
|
||||
|
||||
for _, v := range cmd.Args {
|
||||
ArgsStr += fmt.Sprintf(" %s", v)
|
||||
}
|
||||
|
||||
// Example: Check version operation
|
||||
if cmd.PackageOperation == PackageOperationCheckVersion {
|
||||
logger.Info().Msg("Checking package versions")
|
||||
|
||||
logger.Info().Msg("")
|
||||
for _, p := range cmd.Packages {
|
||||
logger.Info().Str("package", p.Name).Msg("Checking installed and remote package versions")
|
||||
}
|
||||
opts.Logger.Info().Msg("")
|
||||
|
||||
// Execute the package version command
|
||||
execCmd := exec.Command(cmd.Cmd, cmd.Args...)
|
||||
cmdOutWriters = io.MultiWriter(&cmdOutBuf)
|
||||
|
||||
if IsCmdStdOutEnabled() {
|
||||
cmdOutWriters = io.MultiWriter(os.Stdout, &cmdOutBuf)
|
||||
}
|
||||
execCmd.Stdout = cmdOutWriters
|
||||
execCmd.Stderr = cmdOutWriters
|
||||
|
||||
if err := execCmd.Run(); err != nil {
|
||||
return nil, fmt.Errorf("error running command %s: %w", ArgsStr, err)
|
||||
}
|
||||
|
||||
return parsePackageVersion(cmdOutBuf.String(), logger, cmd, cmdOutBuf)
|
||||
}
|
||||
|
||||
// Other package operations (install, upgrade, etc.) can be handled here
|
||||
|
||||
// Default: run as a shell command
|
||||
execCmd := exec.Command(cmd.Cmd, cmd.Args...)
|
||||
execCmd.Stdout = &cmdOutBuf
|
||||
execCmd.Stderr = &cmdOutBuf
|
||||
err := execCmd.Run()
|
||||
outputArr = logCommandOutput(cmd, cmdOutBuf, logger, outputArr)
|
||||
if err != nil {
|
||||
logger.Error().Err(fmt.Errorf("error running package command %s: %w", cmd.Name, err)).Send()
|
||||
return outputArr, err
|
||||
}
|
||||
return outputArr, nil
|
||||
}
|
||||
|
||||
type LocalCommandExecutor struct{}
|
||||
|
||||
func (e *LocalCommandExecutor) Run(cmd *Command, opts *ConfigOpts, logger zerolog.Logger) ([]string, error) {
|
||||
var (
|
||||
ArgsStr string
|
||||
cmdOutBuf bytes.Buffer
|
||||
outputArr []string
|
||||
)
|
||||
|
||||
for _, v := range cmd.Args {
|
||||
ArgsStr += fmt.Sprintf(" %s", v)
|
||||
}
|
||||
|
||||
// Build the command
|
||||
var localCMD *exec.Cmd
|
||||
if cmd.Shell != "" {
|
||||
logger.Info().Str("Command", fmt.Sprintf("Running command %s on local machine in %s", cmd.Name, cmd.Shell)).Send()
|
||||
ArgsStr = fmt.Sprintf("%s %s", cmd.Cmd, ArgsStr)
|
||||
localCMD = exec.Command(cmd.Shell, "-c", ArgsStr)
|
||||
} else {
|
||||
localCMD = exec.Command(cmd.Cmd, cmd.Args...)
|
||||
}
|
||||
|
||||
// Set working directory
|
||||
if cmd.Dir != nil {
|
||||
localCMD.Dir = *cmd.Dir
|
||||
}
|
||||
|
||||
// Inject environment variables (extract this to an EnvInjector if desired)
|
||||
// injectEnvIntoLocalCMD(...)
|
||||
|
||||
// Set output writers
|
||||
cmdOutWriters := io.MultiWriter(&cmdOutBuf)
|
||||
if IsCmdStdOutEnabled() {
|
||||
cmdOutWriters = io.MultiWriter(os.Stdout, &cmdOutBuf)
|
||||
}
|
||||
localCMD.Stdout = cmdOutWriters
|
||||
localCMD.Stderr = cmdOutWriters
|
||||
|
||||
// Run the command
|
||||
err := localCMD.Run()
|
||||
outputArr = logCommandOutput(cmd, cmdOutBuf, logger, outputArr)
|
||||
if err != nil {
|
||||
logger.Error().Err(fmt.Errorf("error when running cmd %s: %w", cmd.Name, err)).Send()
|
||||
return outputArr, err
|
||||
}
|
||||
|
||||
return outputArr, nil
|
||||
}
|
||||
|
||||
// RunCmd runs a Command.
|
||||
// The environment of local commands will be the machine's environment plus any extra
|
||||
// variables specified in the Env file or Environment.
|
||||
@ -70,29 +188,10 @@ func (command *Command) RunCmd(cmdCtxLogger zerolog.Logger, opts *ConfigOpts) ([
|
||||
}
|
||||
} else {
|
||||
|
||||
// Handle package operations
|
||||
if command.Type == PackageCommandType && command.PackageOperation == PackageOperationCheckVersion {
|
||||
opts.Logger.Info().Msg("")
|
||||
for _, p := range command.Packages {
|
||||
cmdCtxLogger.Info().Str("package", p.Name).Msg("Checking installed and remote package versions")
|
||||
}
|
||||
opts.Logger.Info().Msg("")
|
||||
|
||||
// Execute the package version command
|
||||
cmd := exec.Command(command.Cmd, command.Args...)
|
||||
cmdOutWriters = io.MultiWriter(&cmdOutBuf)
|
||||
|
||||
if IsCmdStdOutEnabled() {
|
||||
cmdOutWriters = io.MultiWriter(os.Stdout, &cmdOutBuf)
|
||||
}
|
||||
cmd.Stdout = cmdOutWriters
|
||||
cmd.Stderr = cmdOutWriters
|
||||
|
||||
if err := cmd.Run(); err != nil {
|
||||
return nil, fmt.Errorf("error running command %s: %w", ArgsStr, err)
|
||||
}
|
||||
|
||||
return parsePackageVersion(cmdOutBuf.String(), cmdCtxLogger, command, cmdOutBuf)
|
||||
switch command.Type {
|
||||
case PackageCommandType:
|
||||
var executor PackageCommandExecutor
|
||||
return executor.Run(command, opts, cmdCtxLogger)
|
||||
}
|
||||
|
||||
var localCMD *exec.Cmd
|
||||
@ -393,6 +492,163 @@ func cmdListWorkerWithHosts(msgTemps *msgTemplates, jobs <-chan *CmdList, hosts
|
||||
}
|
||||
}
|
||||
|
||||
// func cmdListWorkerExecuteCommandsInParallel(msgTemps *msgTemplates, jobs <-chan *CmdList, hosts <-chan *Host, results chan<- string, opts *ConfigOpts) {
|
||||
// opts.Logger.Info().Msg("Running commands in parallel")
|
||||
// for list := range jobs {
|
||||
// fieldsMap := map[string]interface{}{"list": list.Name}
|
||||
// var cmdLogger zerolog.Logger
|
||||
// var commandExecuted *Command
|
||||
// var cmdsRan []string
|
||||
// var outStructArr []outStruct
|
||||
// var hasError bool // Tracks if any command in the list failed
|
||||
|
||||
// for _, cmd := range list.Order {
|
||||
// for host := range hosts {
|
||||
// cmdToRun := opts.Cmds[cmd]
|
||||
// if cmdToRun.Host != host.Host {
|
||||
// cmdToRun.Host = host.Host
|
||||
// cmdToRun.RemoteHost = host
|
||||
// }
|
||||
// commandExecuted = cmdToRun
|
||||
// currentCmd := cmdToRun.Name
|
||||
// fieldsMap["cmd"] = currentCmd
|
||||
// cmdLogger = cmdToRun.GenerateLogger(opts)
|
||||
// cmdLogger.Info().Fields(fieldsMap).Send()
|
||||
|
||||
// outputArr, runErr := cmdToRun.RunCmd(cmdLogger, opts)
|
||||
// cmdsRan = append(cmdsRan, cmd)
|
||||
|
||||
// if runErr != nil {
|
||||
|
||||
// cmdLogger.Err(runErr).Send()
|
||||
|
||||
// cmdToRun.ExecuteHooks("error", opts)
|
||||
|
||||
// // Notify failure
|
||||
// if list.NotifyConfig != nil {
|
||||
// notifyError(cmdLogger, msgTemps, list, cmdsRan, outStructArr, runErr, cmdToRun)
|
||||
// }
|
||||
|
||||
// // Execute error hooks for the failed command
|
||||
// hasError = true
|
||||
// break
|
||||
// }
|
||||
|
||||
// if list.GetCommandOutputInNotificationsOnSuccess || cmdToRun.Output.InList {
|
||||
// outStructArr = append(outStructArr, outStruct{
|
||||
// CmdName: currentCmd,
|
||||
// CmdExecuted: currentCmd,
|
||||
// Output: outputArr,
|
||||
// })
|
||||
// }
|
||||
// }
|
||||
|
||||
// if !hasError && list.NotifyConfig != nil && list.Notify.OnFailure {
|
||||
// notifySuccess(cmdLogger, msgTemps, list, cmdsRan, outStructArr)
|
||||
// }
|
||||
|
||||
// if !hasError {
|
||||
// commandExecuted.ExecuteHooks("success", opts)
|
||||
// }
|
||||
|
||||
// commandExecuted.ExecuteHooks("final", opts)
|
||||
|
||||
// }
|
||||
// results <- "done"
|
||||
// }
|
||||
// }
|
||||
|
||||
func cmdListWorkerExecuteCommandsInParallel(msgTemps *msgTemplates, jobs <-chan *CmdList, hosts <-chan *Host, results chan<- string, opts *ConfigOpts) {
|
||||
opts.Logger.Info().Msg("Running commands in parallel")
|
||||
for list := range jobs {
|
||||
fieldsMap := map[string]interface{}{"list": list.Name}
|
||||
var cmdLogger zerolog.Logger
|
||||
var commandExecuted *Command
|
||||
var cmdsRan []string
|
||||
var outStructArr []outStruct
|
||||
var hasError bool // Tracks if any command in the list failed
|
||||
|
||||
var wg sync.WaitGroup
|
||||
hostList := []*Host{}
|
||||
for host := range hosts {
|
||||
hostList = append(hostList, host)
|
||||
}
|
||||
println("Total hosts to run commands on:", len(hostList))
|
||||
println("Total commands to run:", len(list.Order))
|
||||
|
||||
for _, cmd := range list.Order {
|
||||
cmdsRan = append(cmdsRan, cmd)
|
||||
println("Running cmd:", cmd, "on", len(hostList), "hosts")
|
||||
outputChan := make(chan outStruct, len(hostList))
|
||||
errorChan := make(chan error, len(hostList))
|
||||
// cmdToRun := opts.Cmds[cmd]
|
||||
origCmd := opts.Cmds[cmd]
|
||||
|
||||
for _, host := range hostList {
|
||||
wg.Add(1)
|
||||
cmdToRun := *origCmd // shallow copy
|
||||
commandExecuted = origCmd
|
||||
if cmdToRun.Host != host.Host {
|
||||
cmdToRun.Host = host.Host
|
||||
cmdToRun.RemoteHost = host
|
||||
}
|
||||
cmdLogger = cmdToRun.GenerateLogger(opts)
|
||||
cmdLogger.Info().Fields(fieldsMap).Send()
|
||||
print("Running cmd on: ", host.Host, "\n")
|
||||
|
||||
go func(cmd string, host *Host) {
|
||||
defer wg.Done()
|
||||
currentCmd := cmdToRun.Name
|
||||
fieldsMap["cmd"] = currentCmd
|
||||
|
||||
outputArr, runErr := cmdToRun.RunCmd(cmdLogger, opts)
|
||||
if runErr != nil {
|
||||
cmdLogger.Err(runErr).Send()
|
||||
cmdToRun.ExecuteHooks("error", opts)
|
||||
errorChan <- runErr
|
||||
return
|
||||
}
|
||||
|
||||
if list.GetCommandOutputInNotificationsOnSuccess || cmdToRun.Output.InList {
|
||||
outputChan <- outStruct{
|
||||
CmdName: currentCmd,
|
||||
CmdExecuted: currentCmd,
|
||||
Output: outputArr,
|
||||
}
|
||||
}
|
||||
}(cmd, host)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
close(outputChan)
|
||||
close(errorChan)
|
||||
|
||||
for out := range outputChan {
|
||||
outStructArr = append(outStructArr, out)
|
||||
}
|
||||
if len(errorChan) > 0 {
|
||||
hasError = true
|
||||
runErr := <-errorChan
|
||||
if list.NotifyConfig != nil {
|
||||
notifyError(cmdLogger, msgTemps, list, cmdsRan, outStructArr, runErr, commandExecuted)
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
if !hasError && list.NotifyConfig != nil && list.Notify.OnFailure {
|
||||
notifySuccess(cmdLogger, msgTemps, list, cmdsRan, outStructArr)
|
||||
}
|
||||
|
||||
if !hasError {
|
||||
commandExecuted.ExecuteHooks("success", opts)
|
||||
}
|
||||
|
||||
}
|
||||
commandExecuted.ExecuteHooks("final", opts)
|
||||
results <- "done"
|
||||
}
|
||||
}
|
||||
|
||||
func notifyError(logger zerolog.Logger, templates *msgTemplates, list *CmdList, cmdsRan []string, outStructArr []outStruct, err error, cmd *Command) {
|
||||
errStruct := map[string]interface{}{
|
||||
"listName": list.Name,
|
||||
@ -463,17 +719,17 @@ func (opts *ConfigOpts) RunListConfig(cron string) {
|
||||
opts.closeHostConnections()
|
||||
}
|
||||
|
||||
func (opts *ConfigOpts) ExecuteListOnHosts(lists []string) {
|
||||
func (opts *ConfigOpts) ExecuteListOnHosts(lists []string, parallel bool) {
|
||||
|
||||
mTemps := &msgTemplates{
|
||||
err: template.Must(template.New("error.txt").ParseFS(templates, "templates/error.txt")),
|
||||
success: template.Must(template.New("success.txt").ParseFS(templates, "templates/success.txt")),
|
||||
}
|
||||
for _, l := range opts.CmdConfigLists {
|
||||
if !slices.Contains(lists, l.Name) {
|
||||
delete(opts.CmdConfigLists, l.Name)
|
||||
}
|
||||
}
|
||||
// for _, l := range opts.CmdConfigLists {
|
||||
// if !slices.Contains(lists, l.Name) {
|
||||
// delete(opts.CmdConfigLists, l.Name)
|
||||
// }
|
||||
// }
|
||||
configListsLen := len(opts.CmdConfigLists)
|
||||
listChan := make(chan *CmdList, configListsLen)
|
||||
hostChan := make(chan *Host, len(opts.Hosts))
|
||||
@ -481,7 +737,11 @@ func (opts *ConfigOpts) ExecuteListOnHosts(lists []string) {
|
||||
|
||||
// Start workers
|
||||
for w := 1; w <= configListsLen; w++ {
|
||||
go cmdListWorkerWithHosts(mTemps, listChan, hostChan, results, opts)
|
||||
if parallel {
|
||||
go cmdListWorkerExecuteCommandsInParallel(mTemps, listChan, hostChan, results, opts)
|
||||
} else {
|
||||
go cmdListWorkerWithHosts(mTemps, listChan, hostChan, results, opts)
|
||||
}
|
||||
}
|
||||
|
||||
// Enqueue jobs
|
||||
@ -645,6 +905,33 @@ func (opts *ConfigOpts) ExecCmdsOnHosts(cmdList []string, hostsList []string) {
|
||||
}
|
||||
}
|
||||
|
||||
func (opts *ConfigOpts) ExecCmdsOnHostsInParallel(cmdList []string, hostsList []string) {
|
||||
opts.Logger.Info().Msg("Executing commands in parallel on hosts")
|
||||
// Iterate over hosts and exec commands
|
||||
for _, c := range cmdList {
|
||||
for _, h := range hostsList {
|
||||
host := opts.Hosts[h]
|
||||
cmd := opts.Cmds[c]
|
||||
cmd.RemoteHost = host
|
||||
cmd.Host = h
|
||||
if IsHostLocal(h) {
|
||||
_, err := cmd.RunCmd(cmd.GenerateLogger(opts), opts)
|
||||
if err != nil {
|
||||
opts.Logger.Err(err).Str("host", h).Str("cmd", c).Send()
|
||||
}
|
||||
} else {
|
||||
|
||||
cmd.Host = host.Host
|
||||
opts.Logger.Info().Str("host", h).Str("cmd", c).Send()
|
||||
_, err := cmd.RunCmdOnHost(cmd.GenerateLogger(opts), opts)
|
||||
if err != nil {
|
||||
opts.Logger.Err(err).Str("host", h).Str("cmd", c).Send()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func logCommandOutput(command *Command, cmdOutBuf bytes.Buffer, cmdCtxLogger zerolog.Logger, outputArr []string) []string {
|
||||
|
||||
outScanner := bufio.NewScanner(&cmdOutBuf)
|
||||
|
@ -1,83 +1,83 @@
|
||||
package backy
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"testing"
|
||||
// import (
|
||||
// "context"
|
||||
// "fmt"
|
||||
// "io"
|
||||
// "log"
|
||||
// "testing"
|
||||
|
||||
"git.andrewnw.xyz/CyberShell/backy/pkg/pkgman"
|
||||
packagemanagercommon "git.andrewnw.xyz/CyberShell/backy/pkg/pkgman/common"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
// "git.andrewnw.xyz/CyberShell/backy/pkg/pkgman"
|
||||
// packagemanagercommon "git.andrewnw.xyz/CyberShell/backy/pkg/pkgman/common"
|
||||
// "github.com/stretchr/testify/assert"
|
||||
// "github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/testcontainers/testcontainers-go"
|
||||
)
|
||||
// "github.com/testcontainers/testcontainers-go"
|
||||
// )
|
||||
|
||||
// TestConfigOptions tests the configuration options for the backy package.
|
||||
func Test_ErrorHook(t *testing.T) {
|
||||
// // TestConfigOptions tests the configuration options for the backy package.
|
||||
// func Test_ErrorHook(t *testing.T) {
|
||||
|
||||
configFile := "../../tests/ErrorHook.yml"
|
||||
logFile := "ErrorHook.log"
|
||||
backyConfigOptions := NewConfigOptions(configFile, SetLogFile(logFile))
|
||||
backyConfigOptions.InitConfig()
|
||||
backyConfigOptions.ParseConfigurationFile()
|
||||
backyConfigOptions.RunListConfig("")
|
||||
// configFile := "../../tests/ErrorHook.yml"
|
||||
// logFile := "ErrorHook.log"
|
||||
// backyConfigOptions := NewConfigOptions(configFile, SetLogFile(logFile))
|
||||
// backyConfigOptions.InitConfig()
|
||||
// backyConfigOptions.ParseConfigurationFile()
|
||||
// backyConfigOptions.RunListConfig("")
|
||||
|
||||
}
|
||||
// }
|
||||
|
||||
func TestSettingCommandInfoPackageCommandDnf(t *testing.T) {
|
||||
// func TestSettingCommandInfoPackageCommandDnf(t *testing.T) {
|
||||
|
||||
packagecommand := &Command{
|
||||
Type: PackageCommandType,
|
||||
PackageManager: "dnf",
|
||||
Shell: "zsh",
|
||||
PackageOperation: PackageOperationCheckVersion,
|
||||
Packages: []packagemanagercommon.Package{{Name: "docker-ce"}},
|
||||
}
|
||||
dnfPackage, _ := pkgman.PackageManagerFactory("dnf", pkgman.WithoutAuth())
|
||||
// packagecommand := &Command{
|
||||
// Type: PackageCommandType,
|
||||
// PackageManager: "dnf",
|
||||
// Shell: "zsh",
|
||||
// PackageOperation: PackageOperationCheckVersion,
|
||||
// Packages: []packagemanagercommon.Package{{Name: "docker-ce"}},
|
||||
// }
|
||||
// dnfPackage, _ := pkgman.PackageManagerFactory("dnf", pkgman.WithoutAuth())
|
||||
|
||||
packagecommand.pkgMan = dnfPackage
|
||||
PackageCommand := getCommandTypeAndSetCommandInfo(packagecommand)
|
||||
// packagecommand.pkgMan = dnfPackage
|
||||
// PackageCommand := getCommandTypeAndSetCommandInfo(packagecommand)
|
||||
|
||||
assert.Equal(t, "dnf", PackageCommand.Cmd)
|
||||
// assert.Equal(t, "dnf", PackageCommand.Cmd)
|
||||
|
||||
}
|
||||
// }
|
||||
|
||||
func TestWithDockerFile(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
// func TestWithDockerFile(t *testing.T) {
|
||||
// ctx := context.Background()
|
||||
|
||||
docker, err := testcontainers.Run(ctx, "",
|
||||
testcontainers.WithDockerfile(testcontainers.FromDockerfile{
|
||||
Context: "../../tests/docker",
|
||||
Dockerfile: "Dockerfile",
|
||||
KeepImage: false,
|
||||
// BuildOptionsModifier: func(buildOptions *types.ImageBuildOptions) {
|
||||
// buildOptions.Target = "target2"
|
||||
// },
|
||||
}),
|
||||
)
|
||||
// docker.
|
||||
// docker, err := testcontainers.Run(ctx, "",
|
||||
// testcontainers.WithDockerfile(testcontainers.FromDockerfile{
|
||||
// Context: "../../tests/docker",
|
||||
// Dockerfile: "Dockerfile",
|
||||
// KeepImage: false,
|
||||
// // BuildOptionsModifier: func(buildOptions *types.ImageBuildOptions) {
|
||||
// // buildOptions.Target = "target2"
|
||||
// // },
|
||||
// }),
|
||||
// )
|
||||
// // docker.
|
||||
|
||||
if err != nil {
|
||||
log.Printf("failed to start container: %v", err)
|
||||
return
|
||||
}
|
||||
// if err != nil {
|
||||
// log.Printf("failed to start container: %v", err)
|
||||
// return
|
||||
// }
|
||||
|
||||
r, err := docker.Logs(ctx)
|
||||
if err != nil {
|
||||
log.Printf("failed to get logs: %v", err)
|
||||
return
|
||||
}
|
||||
// r, err := docker.Logs(ctx)
|
||||
// if err != nil {
|
||||
// log.Printf("failed to get logs: %v", err)
|
||||
// return
|
||||
// }
|
||||
|
||||
logs, err := io.ReadAll(r)
|
||||
if err != nil {
|
||||
log.Printf("failed to read logs: %v", err)
|
||||
return
|
||||
}
|
||||
// logs, err := io.ReadAll(r)
|
||||
// if err != nil {
|
||||
// log.Printf("failed to read logs: %v", err)
|
||||
// return
|
||||
// }
|
||||
|
||||
fmt.Println(string(logs))
|
||||
// fmt.Println(string(logs))
|
||||
|
||||
require.NoError(t, err)
|
||||
}
|
||||
// require.NoError(t, err)
|
||||
// }
|
||||
|
@ -3,31 +3,51 @@ package backy
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"time"
|
||||
)
|
||||
|
||||
type MetricFile struct {
|
||||
Filename string `json:"filename"`
|
||||
CommandMetrics map[string]*Metrics `json:"commandMetrics"`
|
||||
ListMetrics map[string]*Metrics `json:"listMetrics"`
|
||||
}
|
||||
|
||||
type Metrics struct {
|
||||
SuccessfulExecutions uint64 `json:"successful_executions"`
|
||||
FailedExecutions uint64 `json:"failed_executions"`
|
||||
TotalExecutions uint64 `json:"total_executions"`
|
||||
ExecutionTime float64 `json:"execution_time"` // in seconds
|
||||
AverageExecutionTime float64 `json:"average_execution_time"` // in seconds
|
||||
SuccessRate float64 `json:"success_rate"` // percentage of successful executions
|
||||
FailureRate float64 `json:"failure_rate"` // percentage of failed executions
|
||||
DateStartedLast string `json:"dateStartedLast"`
|
||||
DateLastFinished string `json:"dateLastFinished"`
|
||||
DateLastFinishedSuccessfully string `json:"dateLastFinishedSuccessfully"`
|
||||
SuccessfulExecutions uint64 `json:"successfulExecutions"`
|
||||
FailedExecutions uint64 `json:"failedExecutions"`
|
||||
TotalExecutions uint64 `json:"totalExecutions"`
|
||||
TotalExecutionTime float64 `json:"lastExecutionTime"` // in seconds
|
||||
AverageExecutionTime float64 `json:"totalExecutionTime"` // in seconds
|
||||
SuccessRate float64 `json:"successRate"` // percentage of successful executions
|
||||
FailureRate float64 `json:"failureRate"` // percentage of failed executions
|
||||
}
|
||||
|
||||
func NewMetrics() *Metrics {
|
||||
return &Metrics{
|
||||
DateStartedLast: time.Now().Format(time.RFC3339),
|
||||
SuccessfulExecutions: 0,
|
||||
FailedExecutions: 0,
|
||||
TotalExecutions: 0,
|
||||
ExecutionTime: 0.0,
|
||||
TotalExecutionTime: 0.0,
|
||||
AverageExecutionTime: 0.0,
|
||||
SuccessRate: 0.0,
|
||||
FailureRate: 0.0,
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Metrics) Update(success bool, executionTime float64) {
|
||||
func NewMetricsFromFile(filename string) *MetricFile {
|
||||
return &MetricFile{
|
||||
Filename: filename,
|
||||
CommandMetrics: make(map[string]*Metrics),
|
||||
ListMetrics: make(map[string]*Metrics),
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func (m *Metrics) Update(success bool, executionTime float64, dateLastFinished time.Time) {
|
||||
m.TotalExecutions++
|
||||
if success {
|
||||
m.SuccessfulExecutions++
|
||||
@ -35,8 +55,10 @@ func (m *Metrics) Update(success bool, executionTime float64) {
|
||||
m.FailedExecutions++
|
||||
}
|
||||
|
||||
m.ExecutionTime += executionTime
|
||||
m.AverageExecutionTime = m.ExecutionTime / float64(m.TotalExecutions)
|
||||
m.DateLastFinished = dateLastFinished.Format(time.RFC3339)
|
||||
|
||||
m.TotalExecutionTime += executionTime
|
||||
m.AverageExecutionTime = m.TotalExecutionTime / float64(m.TotalExecutions)
|
||||
|
||||
if m.TotalExecutions > 0 {
|
||||
m.SuccessRate = float64(m.SuccessfulExecutions) / float64(m.TotalExecutions) * 100
|
||||
@ -44,14 +66,23 @@ func (m *Metrics) Update(success bool, executionTime float64) {
|
||||
}
|
||||
}
|
||||
|
||||
func SaveToFile(metrics *Metrics, filename string) error {
|
||||
data, err := json.MarshalIndent(metrics, "", " ")
|
||||
func (metricFile *MetricFile) SaveToFile() error {
|
||||
data, err := json.MarshalIndent(metricFile, "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return os.WriteFile(filename, data, 0644)
|
||||
return os.WriteFile(metricFile.Filename, data, 0644)
|
||||
}
|
||||
|
||||
func LoadFromFile(filename string) (*Metrics, error) {
|
||||
return nil, nil
|
||||
func LoadMetricsFromFile(filename string) (*MetricFile, error) {
|
||||
jsonData, err := os.ReadFile(filename)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var metrics MetricFile
|
||||
err = json.Unmarshal(jsonData, &metrics)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &metrics, nil
|
||||
}
|
||||
|
@ -1,7 +1,67 @@
|
||||
package backy
|
||||
|
||||
import "testing"
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestAddingMetricsForCommand(t *testing.T) {
|
||||
|
||||
// Create a new MetricFile
|
||||
metricFile := NewMetricsFromFile("test_metrics.json")
|
||||
|
||||
metricFile, err := LoadMetricsFromFile(metricFile.Filename)
|
||||
if err != nil {
|
||||
t.Errorf("Failed to load metrics from file: %v", err)
|
||||
}
|
||||
|
||||
// Add metrics for a command
|
||||
commandName := "test_command"
|
||||
if _, exists := metricFile.CommandMetrics[commandName]; !exists {
|
||||
metricFile.CommandMetrics[commandName] = NewMetrics()
|
||||
}
|
||||
|
||||
// Update the metrics for the command
|
||||
executionTime := 1.8 // Example execution time in seconds
|
||||
success := true // Example success status
|
||||
metricFile.CommandMetrics[commandName].Update(success, executionTime, time.Now())
|
||||
|
||||
// Check if the metrics were updated correctly
|
||||
if metricFile.CommandMetrics[commandName].SuccessfulExecutions > 50 {
|
||||
t.Errorf("Expected 1 successful execution, got %d", metricFile.CommandMetrics[commandName].SuccessfulExecutions)
|
||||
}
|
||||
if metricFile.CommandMetrics[commandName].TotalExecutions > 50 {
|
||||
t.Errorf("Expected 1 total execution, got %d", metricFile.CommandMetrics[commandName].TotalExecutions)
|
||||
}
|
||||
// if metricFile.CommandMetrics[commandName].TotalExecutionTime != executionTime {
|
||||
// t.Errorf("Expected execution time %f, got %f", executionTime, metricFile.CommandMetrics[commandName].TotalExecutionTime)
|
||||
// }
|
||||
|
||||
err = metricFile.SaveToFile()
|
||||
if err != nil {
|
||||
t.Errorf("Failed to save metrics to file: %v", err)
|
||||
}
|
||||
|
||||
listName := "test_list"
|
||||
if _, exists := metricFile.ListMetrics[listName]; !exists {
|
||||
metricFile.ListMetrics[listName] = NewMetrics()
|
||||
}
|
||||
// Update the metrics for the list
|
||||
metricFile.ListMetrics[listName].Update(success, executionTime, time.Now())
|
||||
if metricFile.ListMetrics[listName].SuccessfulExecutions > 50 {
|
||||
t.Errorf("Expected 1 successful execution for list, got %d", metricFile.ListMetrics[listName].SuccessfulExecutions)
|
||||
}
|
||||
if metricFile.ListMetrics[listName].TotalExecutions > 50 {
|
||||
t.Errorf("Expected 1 total execution for list, got %d", metricFile.ListMetrics[listName].TotalExecutions)
|
||||
}
|
||||
// if metricFile.ListMetrics[listName].TotalExecutionTime > executionTime {
|
||||
// t.Errorf("Expected execution time %f for list, got %f", executionTime, metricFile.ListMetrics[listName].TotalExecutionTime)
|
||||
// }
|
||||
|
||||
// Save the metrics to a file
|
||||
err = metricFile.SaveToFile()
|
||||
if err != nil {
|
||||
t.Errorf("Failed to save metrics to file: %v", err)
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -27,6 +27,10 @@ import (
|
||||
var PrivateKeyExtraInfoErr = errors.New("Private key may be encrypted. \nIf encrypted, make sure the password is specified correctly in the correct section. This may be done in one of two ways: \n Using external directives - see docs \n privatekeypassword: password (not recommended). \n ")
|
||||
var TS = strings.TrimSpace
|
||||
|
||||
type RemoteHostCommandExecutor interface {
|
||||
RunCmdOnHost(command *Command, commandSession *ssh.Session, cmdCtxLogger zerolog.Logger, cmdOutBuf bytes.Buffer) ([]string, error)
|
||||
}
|
||||
|
||||
// ConnectToHost connects to a host by looking up the config values in the file ~/.ssh/config
|
||||
// It uses any set values and looks up an unset values in the config files
|
||||
// remoteHost is modified directly. The *ssh.Client is returned as part of remoteHost,
|
||||
@ -483,26 +487,8 @@ func (command *Command) RunCmdOnHost(cmdCtxLogger zerolog.Logger, opts *ConfigOp
|
||||
case ScriptFileCommandType:
|
||||
return command.runScriptFile(commandSession, cmdCtxLogger, &cmdOutBuf)
|
||||
case PackageCommandType:
|
||||
if command.PackageOperation == PackageOperationCheckVersion {
|
||||
commandSession.Stderr = nil
|
||||
// Execute the package version command remotely
|
||||
// Parse the output of package version command
|
||||
// Compare versions
|
||||
// Check if a specific version is specified
|
||||
commandSession.Stdout = nil
|
||||
return checkPackageVersion(cmdCtxLogger, command, commandSession, cmdOutBuf)
|
||||
} else {
|
||||
if command.Shell != "" {
|
||||
ArgsStr = fmt.Sprintf("%s -c '%s %s'", command.Shell, command.Cmd, ArgsStr)
|
||||
} else {
|
||||
ArgsStr = fmt.Sprintf("%s %s", command.Cmd, ArgsStr)
|
||||
}
|
||||
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)
|
||||
}
|
||||
}
|
||||
var remoteHostPackageExecutor RemoteHostPackageExecutor
|
||||
return remoteHostPackageExecutor.RunCmdOnHost(command, commandSession, cmdCtxLogger, cmdOutBuf)
|
||||
default:
|
||||
if command.Shell != "" {
|
||||
ArgsStr = fmt.Sprintf("%s -c '%s %s'", command.Shell, command.Cmd, ArgsStr)
|
||||
@ -539,7 +525,6 @@ func (command *Command) RunCmdOnHost(cmdCtxLogger zerolog.Logger, opts *ConfigOp
|
||||
}
|
||||
|
||||
defer rmFileFunc()
|
||||
// commandSession.Stdin = command.stdin
|
||||
}
|
||||
if err := commandSession.Run(ArgsStr); err != nil {
|
||||
return collectOutput(&cmdOutBuf, command.Name, cmdCtxLogger, command.Output.ToLog), fmt.Errorf("error running command: %w", err)
|
||||
@ -823,3 +808,34 @@ func IsHostLocal(host string) bool {
|
||||
host = strings.ToLower(host)
|
||||
return host == "127.0.0.1" || host == "localhost" || host == ""
|
||||
}
|
||||
|
||||
type RemoteHostPackageExecutor struct{}
|
||||
|
||||
func (r RemoteHostPackageExecutor) RunCmdOnHost(command *Command, commandSession *ssh.Session, cmdCtxLogger zerolog.Logger, cmdOutBuf bytes.Buffer) ([]string, error) {
|
||||
var ArgsStr string
|
||||
// Prepare command arguments
|
||||
for _, v := range command.Args {
|
||||
ArgsStr += fmt.Sprintf(" %s", v)
|
||||
}
|
||||
|
||||
if command.PackageOperation == PackageOperationCheckVersion {
|
||||
commandSession.Stderr = nil
|
||||
// Execute the package version command remotely
|
||||
// Parse the output of package version command
|
||||
// Compare versions
|
||||
// Check if a specific version is specified
|
||||
commandSession.Stdout = nil
|
||||
return checkPackageVersion(cmdCtxLogger, command, commandSession, cmdOutBuf)
|
||||
}
|
||||
if command.Shell != "" {
|
||||
ArgsStr = fmt.Sprintf("%s -c '%s %s'", command.Shell, command.Cmd, ArgsStr)
|
||||
} else {
|
||||
ArgsStr = fmt.Sprintf("%s %s", command.Cmd, ArgsStr)
|
||||
}
|
||||
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, cmdCtxLogger, command.Output.ToLog), nil
|
||||
}
|
||||
|
@ -12,7 +12,7 @@ type RemoteFetcher interface {
|
||||
|
||||
// Parse decodes the raw data into a Go structure (e.g., Commands, CommandLists)
|
||||
// Takes the raw data as input and populates the target interface
|
||||
Parse(data []byte, target interface{}) error
|
||||
Parse(data []byte, target any) error
|
||||
|
||||
// Hash returns the hash of the configuration data
|
||||
Hash(data []byte) string
|
||||
|
Reference in New Issue
Block a user