5 Commits

13 changed files with 286 additions and 27 deletions

View File

@ -0,0 +1,3 @@
kind: Added
body: 'CLI: Exec subcommand `hosts`. See documentation for more details.'
time: 2025-07-15T20:23:03.647128713-05:00

View File

@ -0,0 +1,3 @@
kind: Added
body: 'CLI: added `exec hosts` subcommand `list`'
time: 2025-07-23T22:03:40.24191927-05:00

View File

@ -21,7 +21,7 @@ var (
)
func init() {
execCmd.AddCommand(hostExecCommand)
execCmd.AddCommand(hostExecCommand, hostsExecCommand)
}

91
cmd/hosts.go Normal file
View File

@ -0,0 +1,91 @@
package cmd
import (
"maps"
"slices"
"git.andrewnw.xyz/CyberShell/backy/pkg/backy"
"git.andrewnw.xyz/CyberShell/backy/pkg/logging"
"github.com/spf13/cobra"
)
var (
hostsExecCommand = &cobra.Command{
Use: "hosts [--command=command1 --command=command2 ... | -c command1 -c command2 ...]",
Short: "Runs command defined in config file on the hosts in order specified.",
Long: "Hosts executes specified commands on all the hosts defined in config file.\nUse the --commands or -c flag to choose the commands.",
Run: Hosts,
}
hostsListExecCommand = &cobra.Command{
Use: "list list1 list2 ...",
Short: "Runs lists in order specified defined in config file on all hosts.",
Long: "Lists executes specified lists on all the hosts defined in hosts config.\nPass the names of lists as arguments after command.",
Run: HostsList,
}
)
func init() {
hostsExecCommand.AddCommand(hostsListExecCommand)
parseS3Config()
}
// cli input should be hosts and commands. Hosts are defined in config files.
// commands can be passed by the following mutually exclusive options:
// 1. as a list of commands defined in the config file
// 2. stdin (on command line) (TODO)
func Hosts(cmd *cobra.Command, args []string) {
backyConfOpts := backy.NewConfigOptions(configFile,
backy.SetLogFile(logFile),
backy.EnableCommandStdOut(cmdStdOut),
backy.SetHostsConfigFile(hostsConfigFile))
backyConfOpts.InitConfig()
backyConfOpts.ParseConfigurationFile()
for _, h := range backyConfOpts.Hosts {
hostsList = append(hostsList, h.Host)
}
if cmdList == nil {
logging.ExitWithMSG("error: commands must be specified", 1, &backyConfOpts.Logger)
}
for _, c := range cmdList {
_, cmdFound := backyConfOpts.Cmds[c]
if !cmdFound {
logging.ExitWithMSG("cmd "+c+" not found", 1, &backyConfOpts.Logger)
}
}
backyConfOpts.ExecCmdsOnHosts(cmdList, hostsList)
}
func HostsList(cmd *cobra.Command, args []string) {
backyConfOpts := backy.NewConfigOptions(configFile,
backy.SetLogFile(logFile),
backy.EnableCommandStdOut(cmdStdOut),
backy.SetHostsConfigFile(hostsConfigFile))
backyConfOpts.InitConfig()
backyConfOpts.ParseConfigurationFile()
if len(args) == 0 {
logging.ExitWithMSG("error: no lists specified", 1, &backyConfOpts.Logger)
}
for _, l := range args {
_, listFound := backyConfOpts.CmdConfigLists[l]
if !listFound {
logging.ExitWithMSG("list "+l+" not found", 1, &backyConfOpts.Logger)
}
}
maps.DeleteFunc(backyConfOpts.CmdConfigLists, func(k string, v *backy.CmdList) bool {
return !slices.Contains(args, k)
})
backyConfOpts.ExecuteListOnHosts(args)
}

View File

@ -36,13 +36,13 @@ func Execute() {
}
func init() {
rootCmd.PersistentFlags().StringVar(&logFile, "log-file", "", "log file to write to")
rootCmd.PersistentFlags().StringVar(&logFile, "logFile", "", "log file to write to")
rootCmd.PersistentFlags().BoolVar(&cmdStdOut, "cmdStdOut", false, "Pass to print command output to stdout")
rootCmd.PersistentFlags().StringVarP(&configFile, "config", "f", "", "config file to read from")
rootCmd.PersistentFlags().StringVar(&hostsConfigFile, "hostsConfig", "", "yaml hosts file to read from")
rootCmd.PersistentFlags().BoolVarP(&verbose, "verbose", "v", false, "Sets verbose level")
rootCmd.PersistentFlags().StringVar(&s3Endpoint, "s3-endpoint", "", "Sets the S3 endpoint used for config file fetching. Overrides S3_ENDPOINT env variable.")
rootCmd.PersistentFlags().StringVar(&s3Endpoint, "s3Endpoint", "", "Sets the S3 endpoint used for config file fetching. Overrides S3_ENDPOINT env variable.")
rootCmd.AddCommand(backupCmd, execCmd, cronCmd, versionCmd, listCmd)
}

View File

@ -26,8 +26,9 @@ Flags:
--cmdStdOut Pass to print command output to stdout
-f, --config string config file to read from
-h, --help help for backy
--log-file string log file to write to
--s3-endpoint string Sets the S3 endpoint used for config file fetching. Overrides S3_ENDPOINT env variable.
--hostsConfig string yaml hosts file to read from
--logFile string log file to write to
--s3Endpoint string Sets the S3 endpoint used for config file fetching. Overrides S3_ENDPOINT env variable.
-v, --verbose Sets verbose level
Use "backy [command] --help" for more information about a command.
@ -51,8 +52,9 @@ Flags:
Global Flags:
--cmdStdOut Pass to print command output to stdout
-f, --config string config file to read from
--log-file string log file to write to
--s3-endpoint string Sets the S3 endpoint used for config file fetching. Overrides S3_ENDPOINT env variable.
--hostsConfig string yaml hosts file to read from
--logFile string log file to write to
--s3Endpoint string Sets the S3 endpoint used for config file fetching. Overrides S3_ENDPOINT env variable.
-v, --verbose Sets verbose level
```
@ -70,8 +72,9 @@ Flags:
Global Flags:
--cmdStdOut Pass to print command output to stdout
-f, --config string config file to read from
--log-file string log file to write to
--s3-endpoint string Sets the S3 endpoint used for config file fetching. Overrides S3_ENDPOINT env variable.
--hostsConfig string yaml hosts file to read from
--logFile string log file to write to
--s3Endpoint string Sets the S3 endpoint used for config file fetching. Overrides S3_ENDPOINT env variable.
-v, --verbose Sets verbose level
```
@ -86,6 +89,7 @@ Usage:
Available Commands:
host Runs command defined in config file on the hosts in order specified.
hosts Runs command defined in config file on the hosts in order specified.
Flags:
-h, --help help for exec
@ -93,8 +97,9 @@ Flags:
Global Flags:
--cmdStdOut Pass to print command output to stdout
-f, --config string config file to read from
--log-file string log file to write to
--s3-endpoint string Sets the S3 endpoint used for config file fetching. Overrides S3_ENDPOINT env variable.
--hostsConfig string yaml hosts file to read from
--logFile string log file to write to
--s3Endpoint string Sets the S3 endpoint used for config file fetching. Overrides S3_ENDPOINT env variable.
-v, --verbose Sets verbose level
Use "backy exec [command] --help" for more information about a command.
@ -117,8 +122,9 @@ Flags:
Global Flags:
--cmdStdOut Pass to print command output to stdout
-f, --config string config file to read from
--log-file string log file to write to
--s3-endpoint string Sets the S3 endpoint used for config file fetching. Overrides S3_ENDPOINT env variable.
--hostsConfig string yaml hosts file to read from
--logFile string log file to write to
--s3Endpoint string Sets the S3 endpoint used for config file fetching. Overrides S3_ENDPOINT env variable.
-v, --verbose Sets verbose level
```
@ -138,8 +144,9 @@ Flags:
Global Flags:
--cmdStdOut Pass to print command output to stdout
-f, --config string config file to read from
--log-file string log file to write to
--s3-endpoint string Sets the S3 endpoint used for config file fetching. Overrides S3_ENDPOINT env variable.
--hostsConfig string yaml hosts file to read from
--logFile string log file to write to
--s3Endpoint string Sets the S3 endpoint used for config file fetching. Overrides S3_ENDPOINT env variable.
-v, --verbose Sets verbose level
```
@ -161,8 +168,9 @@ Flags:
Global Flags:
--cmdStdOut Pass to print command output to stdout
-f, --config string config file to read from
--log-file string log file to write to
--s3-endpoint string Sets the S3 endpoint used for config file fetching. Overrides S3_ENDPOINT env variable.
--hostsConfig string yaml hosts file to read from
--logFile string log file to write to
--s3Endpoint string Sets the S3 endpoint used for config file fetching. Overrides S3_ENDPOINT env variable.
-v, --verbose Sets verbose level
Use "backy list [command] --help" for more information about a command.
@ -181,8 +189,9 @@ Flags:
Global Flags:
--cmdStdOut Pass to print command output to stdout
-f, --config string config file to read from
--log-file string log file to write to
--s3-endpoint string Sets the S3 endpoint used for config file fetching. Overrides S3_ENDPOINT env variable.
--hostsConfig string yaml hosts file to read from
--logFile string log file to write to
--s3Endpoint string Sets the S3 endpoint used for config file fetching. Overrides S3_ENDPOINT env variable.
-v, --verbose Sets verbose level
```
## list lists
@ -199,7 +208,8 @@ Flags:
Global Flags:
--cmdStdOut Pass to print command output to stdout
-f, --config string config file to read from
--log-file string log file to write to
--s3-endpoint string Sets the S3 endpoint used for config file fetching. Overrides S3_ENDPOINT env variable.
--hostsConfig string yaml hosts file to read from
--logFile string log file to write to
--s3Endpoint string Sets the S3 endpoint used for config file fetching. Overrides S3_ENDPOINT env variable.
-v, --verbose Sets verbose level
```

View File

@ -11,6 +11,7 @@ import (
"io"
"os"
"os/exec"
"slices"
"strings"
"text/template"
@ -326,6 +327,71 @@ func cmdListWorker(msgTemps *msgTemplates, jobs <-chan *CmdList, results chan<-
results <- "done"
}
}
func cmdListWorkerWithHosts(msgTemps *msgTemplates, jobs <-chan *CmdList, hosts <-chan *Host, results chan<- string, opts *ConfigOpts) {
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 host := range hosts {
for _, cmd := range list.Order {
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 notifyError(logger zerolog.Logger, templates *msgTemplates, list *CmdList, cmdsRan []string, outStructArr []outStruct, err error, cmd *Command) {
errStruct := map[string]interface{}{
@ -397,6 +463,57 @@ func (opts *ConfigOpts) RunListConfig(cron string) {
opts.closeHostConnections()
}
func (opts *ConfigOpts) ExecuteListOnHosts(lists []string) {
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)
}
}
configListsLen := len(opts.CmdConfigLists)
listChan := make(chan *CmdList, configListsLen)
hostChan := make(chan *Host, len(opts.Hosts))
results := make(chan string, configListsLen)
// Start workers
for w := 1; w <= configListsLen; w++ {
go cmdListWorkerWithHosts(mTemps, listChan, hostChan, results, opts)
}
// Enqueue jobs
for listName, cmdConfig := range opts.CmdConfigLists {
if cmdConfig.Name == "" {
cmdConfig.Name = listName
}
listChan <- cmdConfig
}
for _, h := range opts.Hosts {
if h.isProxyHost {
continue
}
hostChan <- h
// for _, proxyHost := range h.ProxyHost {
// if proxyHost.isProxyHost {
// continue
// }
// hostChan <- proxyHost
// }
}
close(listChan)
close(hostChan)
// Process results
for a := 1; a <= configListsLen; a++ {
<-results
}
opts.closeHostConnections()
}
func (opts *ConfigOpts) ExecuteCmds() {
for _, cmd := range opts.executeCmds {
cmdToRun := opts.Cmds[cmd]

10
pkg/backy/planForHosts.md Normal file
View File

@ -0,0 +1,10 @@
# Running commands on hosts
I want all commands in a list to be able to be run on all hosts. The underlying solution will be using a function to run the list on a host, and therefore change the host on the commands. This can be done in several ways:
1. The commands can have a `Hosts` field that will be a []string. This array can be populated several ways:
- From the config file
- using CLI options and commands
The commands can be run in succession on all hosts using functions
2. The existing `Host` field can be modified in a function. The commands need to be added to a `[]*Command` slice so that all hosts can be run.

View File

@ -22,7 +22,6 @@ commands:
outputToLog: true
Args:
- "-v"
host: email-svr
cmdLists:
TestHooks:

View File

@ -1,4 +1,4 @@
FROM debian:buster
FROM ubuntu:latest
# Install SSH server
RUN apt-get update && \
@ -19,7 +19,7 @@ EXPOSE 22
RUN mkdir /var/run/sshd
RUN chmod 0755 /var/run/sshd
# RUN apt-get update && apt-get install -y
RUN apt-get update && apt-get install -y curl
# Start SSH service
CMD ["/usr/sbin/sshd", "-D"]

View File

@ -1,3 +1,4 @@
cd ~/Projects/backy/tests/docker
docker container rm -f ssh_server_container
docker build -t ssh_server_image .
docker run -d -p 2222:22 --name ssh_server_container ssh_server_image

View File

@ -2,5 +2,5 @@ hosts:
docker:
port: 2222
Hostname: localhost
user: backy
user: root
IdentityFile: ./docker/backytest

View File

@ -38,6 +38,24 @@ commands:
packageManager: apt
packageOperation: checkVersion
installJq:
type: package
shell: bash
packages:
- name: "jq"
packageManager: apt
packageOperation: install
output:
toLog: true
testJq:
cmd: jq
shell: bash
Args:
- '--version'
output:
toLog: true
checkDockerVersionWithInvalidRegex:
type: package
shell: zsh
@ -48,7 +66,7 @@ commands:
version: '5:28\.0\K.4-1~([A-Za-z0-9]+(\.[A-Za-z0-9]+)+)*'
packageManager: apt
packageOperation: checkVersion
# host: mail.andrewnw.com
cmdLists:
packageCommands:
@ -59,6 +77,13 @@ cmdLists:
- checkDockerPartialVersionWithRegex
- checkDockerVersionWithInvalidRegex
- checkDockerNoVersion
aptCommands:
output:
onFailure: true
order:
- installJq
logging:
verbose: true
verbose: true
cmd-std-out: true