Compare commits

..

No commits in common. "9fd60b6bf74ce6f56be5cb153d64ef917cce37d0" and "37c20aaafa69aa4c335686834cd601dfcf46bf4e" have entirely different histories.

24 changed files with 217 additions and 467 deletions

View File

@ -1,9 +0,0 @@
## 0.2.4 - 2023-02-18
### Added
* Notifications now display errors and the output of the failed command.
* CI configs for GitHub and Woodpecker
* Added `version` subcommand
### Changed
* Console logging can be disabled by setting `console-disabled` in the `logging` object
## Fixed
* If Host was not defined for an incomplete `hosts` object, any commands would fail as they could not look up the values in the SSH config files.

View File

@ -1,6 +0,0 @@
# Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html),
and is generated by [Changie](https://github.com/miniscruff/changie).

View File

@ -1,26 +0,0 @@
changesDir: .changes
unreleasedDir: unreleased
headerPath: header.tpl.md
changelogPath: CHANGELOG.md
versionExt: md
versionFormat: '## {{.Version}} - {{.Time.Format "2006-01-02"}}'
kindFormat: '### {{.Kind}}'
changeFormat: '* {{.Body}}'
kinds:
- label: Added
auto: minor
- label: Changed
auto: major
- label: Deprecated
auto: minor
- label: Removed
auto: major
- label: Fixed
auto: patch
- label: Security
auto: patch
newlines:
afterChangelogHeader: 1
beforeChangelogVersion: 1
endOfVersion: 1
envPrefix: CHANGIE_

View File

@ -1,43 +0,0 @@
name: goreleaser
on:
push:
# run only against tags
tags:
- '*'
permissions:
contents: write
packages: write
# issues: write
jobs:
goreleaser:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0
- run: git fetch --force --tags
- uses: actions/setup-go@v3
with:
go-version: '==1.19.5'
cache: true
# More assembly might be required: Docker logins, GPG, etc. It all depends
# on your needs.
- name: Get tag
id: tag
uses: dawidd6/action-get-tag@v1
with:
# Optionally strip `v` prefix
strip_v: true
- uses: goreleaser/goreleaser-action@v4
with:
distribution: goreleaser
version: latest
args: release --release-notes=".changes/${{steps.tag.outputs.tag}}.md" -f .goreleaser/github.yml --clean
env:
GITHUB_TOKEN: ${{ secrets.GORELEASER_TOKEN }}
# Your GoReleaser Pro key, if you are using the 'goreleaser-pro'
# distribution:
# GORELEASER_KEY: ${{ secrets.GORELEASER_KEY }}

View File

@ -1,3 +1,5 @@
# This is an example .goreleaser.yml file with some sensible defaults.
# Make sure to check the documentation at https://goreleaser.com
before: before:
hooks: hooks:
# You may remove this if you don't use go modules. # You may remove this if you don't use go modules.
@ -39,8 +41,8 @@ changelog:
- '^test:' - '^test:'
gitea_urls: gitea_urls:
api: https://git.vern.cc/api/v1 api: https://git.andrewnw.xyz/api/v1
download: https://git.vern.cc download: https://git.andrewnw.xyz
# The lines beneath this are called `modelines`. See `:help modeline` # The lines beneath this are called `modelines`. See `:help modeline`
# Feel free to remove those if you don't want/use them. # Feel free to remove those if you don't want/use them.
# yaml-language-server: $schema=https://goreleaser.com/static/schema.json # yaml-language-server: $schema=https://goreleaser.com/static/schema.json

View File

@ -1,42 +0,0 @@
before:
hooks:
# You may remove this if you don't use go modules.
- go mod tidy
builds:
- env:
- CGO_ENABLED=0
goos:
- freebsd
- linux
goarch:
- "386"
- amd64
- arm64
archives:
- format: tar.gz
# this name template makes the OS and Arch compatible with the results of uname.
name_template: >-
{{ .ProjectName }}_{{ .Version }}_
{{- title .Os }}_
{{- if eq .Arch "amd64" }}x86_64
{{- else if eq .Arch "386" }}i386
{{- else }}{{ .Arch }}{{ end }}
{{- if .Arm }}v{{ .Arm }}{{ end }}
# use zip for windows archives
format_overrides:
- goos: windows
format: zip
checksum:
name_template: 'checksums.txt'
snapshot:
name_template: "{{ incpatch .Version }}-next"
changelog:
skip: false
gitea_urls:
api: https://git.andrewnw.xyz/api/v1
download: https://git.andrewnw.xyz
# The lines beneath this are called `modelines`. See `:help modeline`
# Feel free to remove those if you don't want/use them.
# yaml-language-server: $schema=https://goreleaser.com/static/schema.json

View File

@ -1,41 +0,0 @@
# This is an example .goreleaser.yml file with some sensible defaults.
# Make sure to check the documentation at https://goreleaser.com
before:
hooks:
# You may remove this if you don't use go modules.
- go mod tidy
builds:
- env:
- CGO_ENABLED=0
goos:
- freebsd
- linux
goarch:
- "386"
- amd64
- arm64
archives:
- format: tar.gz
# this name template makes the OS and Arch compatible with the results of uname.
name_template: >-
{{ .ProjectName }}_{{ .Version }}_
{{- title .Os }}_
{{- if eq .Arch "amd64" }}x86_64
{{- else if eq .Arch "386" }}i386
{{- else }}{{ .Arch }}{{ end }}
{{- if .Arm }}v{{ .Arm }}{{ end }}
# use zip for windows archives
format_overrides:
- goos: windows
format: zip
checksum:
name_template: 'checksums.txt'
snapshot:
name_template: "{{ incpatch .Version }}-next"
changelog:
sort: asc
filters:
exclude:
- '^docs:'
- '^test:'

View File

@ -1,10 +0,0 @@
pipeline:
release:
image: goreleaser/goreleaser
commands:
- goreleaser release -f .goreleaser/vern.yml --release-notes=".changes/$(go run backy.go version).md"
secrets: [ gitea_token ]
when:
event: tag
branches: master

View File

@ -1,17 +0,0 @@
# Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html),
and is generated by [Changie](https://github.com/miniscruff/changie).
## 0.2.4 - 2023-02-18
### Added
* Notifications now display errors and the output of the failed command.
* CI configs for GitHub and Woodpecker
* Added `version` subcommand
### Changed
* Console logging can be disabled by setting `console-disabled` in the `logging` object
## Fixed
* If Host was not defined for an incomplete `hosts` object, any commands would fail as they could not look up the values in the SSH config files.

View File

@ -1,8 +1,5 @@
build: build:
go build go build
install: gorealeaser-build:
go install . goreleaser release --snapshot --rm-dist
goreleaser-snapshot:
goreleaser -f .goreleaser/gitea.yml release --snapshot --clean

View File

@ -12,10 +12,11 @@ import (
var ( var (
backupCmd = &cobra.Command{ backupCmd = &cobra.Command{
Use: "backup [--lists=list1,list2]", Use: "backup [--lists==list1,list2]",
Short: "Runs commands defined in config file.", Short: "Runs commands defined in config file.",
Long: "Backup executes commands defined in config file.\nUse the --lists flag to execute the specified commands. If not specified, all lists will be executed.", Long: `Backup executes commands defined in config file.
Run: Backup, Use the --lists flag to execute the specified commands.`,
Run: Backup,
} }
) )

View File

@ -8,9 +8,9 @@ import (
var ( var (
cronCmd = &cobra.Command{ cronCmd = &cobra.Command{
Use: "cron [flags]", Use: "cron command ...",
Short: "Runs command lists defined in config file.", Short: "Runs commands defined in config file.",
Long: `Cron starts a scheduler that executes command lists at the time defined in config file.`, Long: `Cron executes commands at the time defined in config file.`,
Run: cron, Run: cron,
} }
) )

View File

@ -29,5 +29,6 @@ func execute(cmd *cobra.Command, args []string) {
opts := backy.NewOpts(cfgFile, backy.AddCommands(args)) opts := backy.NewOpts(cfgFile, backy.AddCommands(args))
opts.InitConfig() opts.InitConfig()
// opts.InitMongo() // opts.InitMongo()
backy.ReadConfig(opts).ExecuteCmds(opts) backy.ReadConfig(opts).ExecuteCmds()
} }

View File

@ -36,5 +36,5 @@ func init() {
rootCmd.PersistentFlags().StringVarP(&cfgFile, "config", "f", "", "config file to read from") rootCmd.PersistentFlags().StringVarP(&cfgFile, "config", "f", "", "config file to read from")
rootCmd.PersistentFlags().BoolVarP(&verbose, "verbose", "v", false, "Sets verbose level") rootCmd.PersistentFlags().BoolVarP(&verbose, "verbose", "v", false, "Sets verbose level")
rootCmd.AddCommand(backupCmd, execCmd, cronCmd, versionCmd) rootCmd.AddCommand(backupCmd, execCmd, cronCmd)
} }

View File

@ -1,25 +0,0 @@
package cmd
import (
"fmt"
"os"
"github.com/spf13/cobra"
)
const versionStr = "0.2.4"
var (
versionCmd = &cobra.Command{
Use: "version",
Short: "Prints the version and exits.",
Run: version,
}
)
func version(cmd *cobra.Command, args []string) {
fmt.Printf("%s\n", versionStr)
os.Exit(0)
}

View File

@ -33,7 +33,6 @@ cmd-configs:
notifications: notifications:
- matrix - matrix
name: backup-some-server name: backup-some-server
cron: "0 0 1 * * *"
hostname: hostname:
name: hostname name: hostname
order: order:

View File

@ -29,10 +29,9 @@ var Sprintf = fmt.Sprintf
// The environment of local commands will be the machine's environment plus any extra // The environment of local commands will be the machine's environment plus any extra
// variables specified in the Env file or Environment. // variables specified in the Env file or Environment.
// Dir can also be specified for local commands. // Dir can also be specified for local commands.
func (command *Command) RunCmd(log *zerolog.Logger, hosts map[string]*Host) ([]string, error) { func (command *Command) RunCmd(log *zerolog.Logger, hosts map[string]*Host) error {
var ( var (
outputArr []string
ArgsStr string ArgsStr string
cmdOutBuf bytes.Buffer cmdOutBuf bytes.Buffer
cmdOutWriters io.Writer cmdOutWriters io.Writer
@ -42,23 +41,24 @@ func (command *Command) RunCmd(log *zerolog.Logger, hosts map[string]*Host) ([]s
env: command.Environment, env: command.Environment,
} }
) )
envVars.env = append(envVars.env, os.Environ()...)
for _, v := range command.Args { for _, v := range command.Args {
ArgsStr += fmt.Sprintf(" %s", v) ArgsStr += fmt.Sprintf(" %s", v)
} }
if command.Host != nil { if command.Host != nil {
log.Info().Str("Command", fmt.Sprintf("Running command %s %s on host %s", command.Cmd, ArgsStr, *command.Host)).Send() log.Info().Str("Command", fmt.Sprintf("Running command: %s %s on host %s", command.Cmd, ArgsStr, *command.Host)).Send()
err := command.RemoteHost.ConnectToSSHHost(log, hosts) err := command.RemoteHost.ConnectToSSHHost(log, hosts)
if err != nil { if err != nil {
return nil, err return err
} }
defer command.RemoteHost.SshClient.Close() defer command.RemoteHost.SshClient.Close()
commandSession, err := command.RemoteHost.SshClient.NewSession() commandSession, err := command.RemoteHost.SshClient.NewSession()
if err != nil { if err != nil {
log.Err(fmt.Errorf("new ssh session: %w", err)).Send() log.Err(fmt.Errorf("new ssh session: %w", err)).Send()
return nil, err return err
} }
defer commandSession.Close() defer commandSession.Close()
@ -81,15 +81,12 @@ func (command *Command) RunCmd(log *zerolog.Logger, hosts map[string]*Host) ([]s
outMap := make(map[string]interface{}) outMap := make(map[string]interface{})
outMap["cmd"] = cmd outMap["cmd"] = cmd
outMap["output"] = outScanner.Text() outMap["output"] = outScanner.Text()
if str, ok := outMap["output"].(string); ok {
outputArr = append(outputArr, str)
}
log.Info().Fields(outMap).Send() log.Info().Fields(outMap).Send()
} }
if err != nil { if err != nil {
log.Error().Err(fmt.Errorf("error when running cmd: %s: %w", command.Cmd, err)).Send() log.Error().Err(fmt.Errorf("error when running cmd: %s: %w", command.Cmd, err)).Send()
return outputArr, err return err
} }
} else { } else {
cmdExists := command.checkCmdExists() cmdExists := command.checkCmdExists()
@ -99,7 +96,7 @@ func (command *Command) RunCmd(log *zerolog.Logger, hosts map[string]*Host) ([]s
var err error var err error
if command.Shell != "" { if command.Shell != "" {
log.Info().Str("Command", fmt.Sprintf("Running command %s %s on local machine in %s", command.Cmd, ArgsStr, command.Shell)).Send() log.Info().Str("Command", fmt.Sprintf("Running command: %s %s on local machine in %s", command.Cmd, ArgsStr, command.Shell)).Send()
ArgsStr = fmt.Sprintf("%s %s", command.Cmd, ArgsStr) ArgsStr = fmt.Sprintf("%s %s", command.Cmd, ArgsStr)
localCMD := exec.Command(command.Shell, "-c", ArgsStr) localCMD := exec.Command(command.Shell, "-c", ArgsStr)
if command.Dir != nil { if command.Dir != nil {
@ -121,29 +118,23 @@ func (command *Command) RunCmd(log *zerolog.Logger, hosts map[string]*Host) ([]s
outMap := make(map[string]interface{}) outMap := make(map[string]interface{})
outMap["cmd"] = command.Cmd outMap["cmd"] = command.Cmd
outMap["output"] = outScanner.Text() outMap["output"] = outScanner.Text()
if str, ok := outMap["output"].(string); ok {
outputArr = append(outputArr, str)
}
log.Info().Fields(outMap).Send() log.Info().Fields(outMap).Send()
} }
if err != nil { if err != nil {
log.Error().Err(fmt.Errorf("error when running cmd %s: %w", command.Cmd, err)).Send() log.Error().Err(fmt.Errorf("error when running cmd: %s: %w", command.Cmd, err)).Send()
return outputArr, err return err
} }
return outputArr, nil return nil
} }
log.Info().Str("Command", fmt.Sprintf("Running command %s %s on local machine", command.Cmd, ArgsStr)).Send() log.Info().Str("Command", fmt.Sprintf("Running command: %s %s on local machine", command.Cmd, ArgsStr)).Send()
localCMD := exec.Command(command.Cmd, command.Args...) localCMD := exec.Command(command.Cmd, command.Args...)
if command.Dir != nil { if command.Dir != nil {
localCMD.Dir = *command.Dir localCMD.Dir = *command.Dir
} }
// fmt.Printf("%v\n", envVars.env)
injectEnvIntoLocalCMD(envVars, localCMD, log) injectEnvIntoLocalCMD(envVars, localCMD, log)
cmdOutWriters = io.MultiWriter(&cmdOutBuf) cmdOutWriters = io.MultiWriter(&cmdOutBuf)
// fmt.Printf("%v\n", localCMD.Environ())
if IsCmdStdOutEnabled() { if IsCmdStdOutEnabled() {
cmdOutWriters = io.MultiWriter(os.Stdout, &cmdOutBuf) cmdOutWriters = io.MultiWriter(os.Stdout, &cmdOutBuf)
@ -156,21 +147,17 @@ func (command *Command) RunCmd(log *zerolog.Logger, hosts map[string]*Host) ([]s
outMap := make(map[string]interface{}) outMap := make(map[string]interface{})
outMap["cmd"] = command.Cmd outMap["cmd"] = command.Cmd
outMap["output"] = outScanner.Text() outMap["output"] = outScanner.Text()
if str, ok := outMap["output"].(string); ok {
outputArr = append(outputArr, str)
}
log.Info().Fields(outMap).Send() log.Info().Fields(outMap).Send()
} }
if err != nil { if err != nil {
log.Error().Err(fmt.Errorf("error when running cmd %s: %w", command.Cmd, err)).Send() log.Error().Err(fmt.Errorf("error when running cmd: %s: %w", command.Cmd, err)).Send()
return outputArr, err return err
} }
} }
return outputArr, nil return nil
} }
func cmdListWorker(msgTemps *msgTemplates, jobs <-chan *CmdList, config *BackyConfigFile, results chan<- string) { func cmdListWorker(id int, jobs <-chan *CmdList, config *BackyConfigFile, results chan<- string) {
for list := range jobs { for list := range jobs {
var currentCmd string var currentCmd string
fieldsMap := make(map[string]interface{}) fieldsMap := make(map[string]interface{})
@ -186,7 +173,7 @@ func cmdListWorker(msgTemps *msgTemplates, jobs <-chan *CmdList, config *BackyCo
cmdLogger := config.Logger.With(). cmdLogger := config.Logger.With().
Str("backy-cmd", cmd). Str("backy-cmd", cmd).
Logger() Logger()
outputArr, runOutErr := cmdToRun.RunCmd(&cmdLogger, config.Hosts) runOutErr := cmdToRun.RunCmd(&cmdLogger, config.Hosts)
count++ count++
if runOutErr != nil { if runOutErr != nil {
var errMsg bytes.Buffer var errMsg bytes.Buffer
@ -196,8 +183,8 @@ func cmdListWorker(msgTemps *msgTemplates, jobs <-chan *CmdList, config *BackyCo
errStruct["Command"] = currentCmd errStruct["Command"] = currentCmd
errStruct["Err"] = runOutErr errStruct["Err"] = runOutErr
errStruct["CmdsRan"] = cmdsRan errStruct["CmdsRan"] = cmdsRan
errStruct["Output"] = outputArr t := template.Must(template.New("error.txt").ParseFS(templates, "templates/error.txt"))
tmpErr := msgTemps.err.Execute(&errMsg, errStruct) tmpErr := t.Execute(&errMsg, errStruct)
if tmpErr != nil { if tmpErr != nil {
config.Logger.Err(tmpErr).Send() config.Logger.Err(tmpErr).Send()
} }
@ -217,7 +204,8 @@ func cmdListWorker(msgTemps *msgTemplates, jobs <-chan *CmdList, config *BackyCo
successStruct := make(map[string]interface{}) successStruct := make(map[string]interface{})
successStruct["listName"] = list.Name successStruct["listName"] = list.Name
successStruct["CmdsRan"] = cmdsRan successStruct["CmdsRan"] = cmdsRan
tmpErr := msgTemps.success.Execute(&successMsg, successStruct) t := template.Must(template.New("success.txt").ParseFS(templates, "templates/success.txt"))
tmpErr := t.Execute(&successMsg, successStruct)
if tmpErr != nil { if tmpErr != nil {
config.Logger.Err(tmpErr).Send() config.Logger.Err(tmpErr).Send()
break break
@ -240,10 +228,6 @@ func cmdListWorker(msgTemps *msgTemplates, jobs <-chan *CmdList, config *BackyCo
// RunBackyConfig runs a command list from the BackyConfigFile. // RunBackyConfig runs a command list from the BackyConfigFile.
func (config *BackyConfigFile) RunBackyConfig(cron string) { func (config *BackyConfigFile) RunBackyConfig(cron 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")),
}
configListsLen := len(config.CmdConfigLists) configListsLen := len(config.CmdConfigLists)
listChan := make(chan *CmdList, configListsLen) listChan := make(chan *CmdList, configListsLen)
results := make(chan string) results := make(chan string)
@ -251,7 +235,8 @@ func (config *BackyConfigFile) RunBackyConfig(cron string) {
// This starts up 3 workers, initially blocked // This starts up 3 workers, initially blocked
// because there are no jobs yet. // because there are no jobs yet.
for w := 1; w <= configListsLen; w++ { for w := 1; w <= configListsLen; w++ {
go cmdListWorker(mTemps, listChan, config, results) go cmdListWorker(w, listChan, config, results)
} }
// Here we send 5 `jobs` and then `close` that // Here we send 5 `jobs` and then `close` that
@ -277,16 +262,11 @@ func (config *BackyConfigFile) RunBackyConfig(cron string) {
} }
func (config *BackyConfigFile) ExecuteCmds(opts *BackyConfigOpts) { func (config *BackyConfigFile) ExecuteCmds() {
for _, cmd := range opts.executeCmds { for _, cmd := range config.Cmds {
cmdToRun := config.Cmds[cmd] runErr := cmd.RunCmd(&config.Logger, config.Hosts)
cmdLogger := config.Logger.With().
Str("backy-cmd", cmd).
Logger()
_, runErr := cmdToRun.RunCmd(&cmdLogger, config.Hosts)
if runErr != nil { if runErr != nil {
config.Logger.Err(runErr).Send() config.Logger.Err(runErr).Send()
} }
} }
} }

View File

@ -71,11 +71,12 @@ func ReadConfig(opts *BackyConfigOpts) *BackyConfigFile {
os.Setenv("BACKY_LOGLEVEL", Sprintf("%v", globalLvl)) os.Setenv("BACKY_LOGLEVEL", Sprintf("%v", globalLvl))
} }
consoleLoggingDisabled := backyViper.GetBool(getLoggingKeyFromConfig("console-disabled")) consoleLoggingEnabled := backyViper.GetBool(getLoggingKeyFromConfig("console"))
os.Setenv("BACKY_CONSOLE_LOGGING", "enabled")
// Other qualifiers can go here as well // Other qualifiers can go here as well
if consoleLoggingDisabled { if consoleLoggingEnabled {
os.Setenv("BACKY_CONSOLE_LOGGING", "enabled")
} else {
os.Setenv("BACKY_CONSOLE_LOGGING", "") os.Setenv("BACKY_CONSOLE_LOGGING", "")
} }
@ -118,10 +119,7 @@ func ReadConfig(opts *BackyConfigOpts) *BackyConfigFile {
if unmarshalErr != nil { if unmarshalErr != nil {
panic(fmt.Errorf("error unmarshalling hosts struct: %w", unmarshalErr)) panic(fmt.Errorf("error unmarshalling hosts struct: %w", unmarshalErr))
} }
for hostConfigName, host := range backyConfigFile.Hosts { for _, host := range backyConfigFile.Hosts {
if host.Host == "" {
host.Host = hostConfigName
}
if host.ProxyJump != "" { if host.ProxyJump != "" {
proxyHosts := strings.Split(host.ProxyJump, ",") proxyHosts := strings.Split(host.ProxyJump, ",")
if len(proxyHosts) > 1 { if len(proxyHosts) > 1 {
@ -155,7 +153,6 @@ func ReadConfig(opts *BackyConfigOpts) *BackyConfigFile {
} }
} }
} }
cmdListCfg := backyViper.Sub("cmd-configs") cmdListCfg := backyViper.Sub("cmd-configs")
unmarshalErr = cmdListCfg.Unmarshal(&backyConfigFile.CmdConfigLists) unmarshalErr = cmdListCfg.Unmarshal(&backyConfigFile.CmdConfigLists)
if unmarshalErr != nil { if unmarshalErr != nil {

View File

@ -20,7 +20,7 @@ import (
"golang.org/x/crypto/ssh/knownhosts" "golang.org/x/crypto/ssh/knownhosts"
) )
var PrivateKeyExtraInfoErr = errors.New("Private key may be encrypted. \nIf encrypted, make sure the password is specified correctly in the correct section: \n privatekeypassword: password (not recommended) \n privatekeypassword: env:PR_KEY_PASS \n privatekeypassword: file:/path/to/password-file. \n ") var ErrPrivateKeyFileFailedToOpen = errors.New("Failed to open private key file. If encrypted, make sure the password is specified.")
var TS = strings.TrimSpace var TS = strings.TrimSpace
// ConnectToSSHHost connects to a host by looking up the config values in the directory ~/.ssh/config // ConnectToSSHHost connects to a host by looking up the config values in the directory ~/.ssh/config
@ -38,6 +38,16 @@ func (remoteConfig *Host) ConnectToSSHHost(log *zerolog.Logger, hosts map[string
if TS(remoteConfig.ConfigFilePath) == "" { if TS(remoteConfig.ConfigFilePath) == "" {
remoteConfig.useDefaultConfig = true remoteConfig.useDefaultConfig = true
} }
if remoteConfig.ProxyHost != nil {
for _, proxyHost := range remoteConfig.ProxyHost {
log.Info().Msgf("Proxy Host %s", proxyHost.Host)
err := proxyHost.GetProxyJumpConfig(hosts)
if err != nil {
return err
}
}
}
khPath, khPathErr := GetKnownHosts(remoteConfig.KnownHostsFile) khPath, khPathErr := GetKnownHosts(remoteConfig.KnownHostsFile)
if khPathErr != nil { if khPathErr != nil {
@ -67,20 +77,6 @@ func (remoteConfig *Host) ConnectToSSHHost(log *zerolog.Logger, hosts map[string
if decodeErr != nil { if decodeErr != nil {
return decodeErr return decodeErr
} }
err := remoteConfig.GetProxyJumpFromConfig(hosts)
if err != nil {
return err
}
if remoteConfig.ProxyHost != nil {
for _, proxyHost := range remoteConfig.ProxyHost {
log.Info().Msgf("Proxy Host %s", proxyHost.Host)
err := proxyHost.GetProxyJumpConfig(hosts)
if err != nil {
return err
}
}
}
remoteConfig.ClientConfig.Timeout = time.Second * 30 remoteConfig.ClientConfig.Timeout = time.Second * 30
remoteConfig.GetPrivateKeyFileFromConfig() remoteConfig.GetPrivateKeyFileFromConfig()
remoteConfig.GetPort() remoteConfig.GetPort()
@ -90,7 +86,7 @@ func (remoteConfig *Host) ConnectToSSHHost(log *zerolog.Logger, hosts map[string
if remoteConfig.HostName == "" { if remoteConfig.HostName == "" {
return errors.New("No hostname found or specified") return errors.New("No hostname found or specified")
} }
err = remoteConfig.GetAuthMethods() err := remoteConfig.GetAuthMethods()
if err != nil { if err != nil {
return err return err
} }
@ -150,13 +146,13 @@ func (remoteHost *Host) GetAuthMethods() error {
if remoteHost.PrivateKeyPassword == "" { if remoteHost.PrivateKeyPassword == "" {
signer, err = ssh.ParsePrivateKey(privateKey) signer, err = ssh.ParsePrivateKey(privateKey)
if err != nil { if err != nil {
return errors.Errorf("Failed to open private key file %s: %v \n\n %v", remoteHost.PrivateKeyPath, err, PrivateKeyExtraInfoErr) return ErrPrivateKeyFileFailedToOpen
} }
remoteHost.ClientConfig.Auth = []ssh.AuthMethod{ssh.PublicKeys(signer)} remoteHost.ClientConfig.Auth = []ssh.AuthMethod{ssh.PublicKeys(signer)}
} else { } else {
signer, err = ssh.ParsePrivateKeyWithPassphrase(privateKey, []byte(remoteHost.PrivateKeyPassword)) signer, err = ssh.ParsePrivateKeyWithPassphrase(privateKey, []byte(remoteHost.PrivateKeyPassword))
if err != nil { if err != nil {
return errors.Errorf("Failed to open private key file %s: %v \n\n %v", remoteHost.PrivateKeyPath, err, PrivateKeyExtraInfoErr) return err
} }
remoteHost.ClientConfig.Auth = []ssh.AuthMethod{ssh.PublicKeys(signer)} remoteHost.ClientConfig.Auth = []ssh.AuthMethod{ssh.PublicKeys(signer)}
} }
@ -197,7 +193,7 @@ func (remoteHost *Host) GetPrivateKeyFileFromConfig() {
// GetPort checks if the port from the config file is 0 // GetPort checks if the port from the config file is 0
// If it is the port is searched in the SSH config file(s) // If it is the port is searched in the SSH config file(s)
func (remoteHost *Host) GetPort() { func (remoteHost *Host) GetPort() {
port := fmt.Sprintf("%d", remoteHost.Port) port := fmt.Sprintf("%v", remoteHost.Port)
// port specifed? // port specifed?
if port == "0" { if port == "0" {
port, _ = remoteHost.SSHConfigFile.SshConfigFile.Get(remoteHost.Host, "Port") port, _ = remoteHost.SSHConfigFile.SshConfigFile.Get(remoteHost.Host, "Port")
@ -208,16 +204,12 @@ func (remoteHost *Host) GetPort() {
} }
} }
} }
portNum, _ := strconv.ParseUint(port, 10, 16) portNum, _ := strconv.ParseUint(port, 10, 32)
remoteHost.Port = uint16(portNum) remoteHost.Port = uint16(portNum)
} }
func (remoteHost *Host) CombineHostNameWithPort() { func (remoteHost *Host) CombineHostNameWithPort() {
port := fmt.Sprintf(":%d", remoteHost.Port) remoteHost.HostName = fmt.Sprintf("%s:%v", remoteHost.HostName, remoteHost.Port)
if strings.HasSuffix(remoteHost.HostName, port) {
return
}
remoteHost.HostName = fmt.Sprintf("%s:%d", remoteHost.HostName, remoteHost.Port)
} }
func (remoteHost *Host) GetHostName() { func (remoteHost *Host) GetHostName() {
@ -274,7 +266,7 @@ func GetPrivateKeyPassword(key string) (string, error) {
privKeyPassFilePath, _ = resolveDir(privKeyPassFilePath) privKeyPassFilePath, _ = resolveDir(privKeyPassFilePath)
keyFile, keyFileErr := os.Open(privKeyPassFilePath) keyFile, keyFileErr := os.Open(privKeyPassFilePath)
if keyFileErr != nil { if keyFileErr != nil {
return "", errors.Errorf("Private key password file %s failed to open. \n Make sure it is accessible and correct.", privKeyPassFilePath) return "", ErrPrivateKeyFileFailedToOpen
} }
passwordScanner := bufio.NewScanner(keyFile) passwordScanner := bufio.NewScanner(keyFile)
for passwordScanner.Scan() { for passwordScanner.Scan() {
@ -336,10 +328,8 @@ func (remoteConfig *Host) GetProxyJumpFromConfig(hosts map[string]*Host) error {
if proxyHostFound { if proxyHostFound {
remoteConfig.ProxyHost = append(remoteConfig.ProxyHost, proxyHost) remoteConfig.ProxyHost = append(remoteConfig.ProxyHost, proxyHost)
} else { } else {
if proxyJump != "" { newProxy := &Host{Host: proxyJump}
newProxy := &Host{Host: proxyJump} remoteConfig.ProxyHost = append(remoteConfig.ProxyHost, newProxy)
remoteConfig.ProxyHost = append(remoteConfig.ProxyHost, newProxy)
}
} }
} }
@ -350,6 +340,7 @@ func (remoteConfig *Host) GetProxyJumpConfig(hosts map[string]*Host) error {
remoteConfig.useDefaultConfig = true remoteConfig.useDefaultConfig = true
} }
// log.Info().Msgf("Proxy Host %s", remoteConfig.ProxyHost[0].Host)
khPath, khPathErr := GetKnownHosts(remoteConfig.KnownHostsFile) khPath, khPathErr := GetKnownHosts(remoteConfig.KnownHostsFile)
if khPathErr != nil { if khPathErr != nil {

View File

@ -1,12 +1,8 @@
Command list {{.listName }} failed on running {{.Command}}. Command list {{.listName }} failed on running {{.Command}}.
{{ if .Err }} The error was {{ .Err }}{{ end }} The error was {{ .Err }}
{{ if .Output }} The output was {{- range .Output}} {{.}} {{end}} {{end}}
{{ if .CmdsRan }}
The following commands ran: The following commands ran:
{{- range .CmdsRan}} {{- range .CmdsRan}}
- {{. -}} - {{. -}}
{{end}} {{end}}
{{ end }}

View File

@ -1,8 +1,10 @@
// types.go
// Copyright (C) Andrew Woodlee 2023
// License: Apache-2.0
package backy package backy
import ( import (
"bytes" "bytes"
"text/template"
"github.com/kevinburke/ssh_config" "github.com/kevinburke/ssh_config"
"github.com/nikoksr/notify" "github.com/nikoksr/notify"
@ -13,158 +15,158 @@ import (
"golang.org/x/crypto/ssh" "golang.org/x/crypto/ssh"
) )
type ( type CmdConfigSchema struct {
CmdConfigSchema struct { ID primitive.ObjectID `bson:"_id,omitempty"`
ID primitive.ObjectID `bson:"_id,omitempty"` CmdList []string `bson:"command-list,omitempty"`
CmdList []string `bson:"command-list,omitempty"` Name string `bson:"name,omitempty"`
Name string `bson:"name,omitempty"` }
} type CmdSchema struct {
ID primitive.ObjectID `bson:"_id,omitempty"`
Cmd string `bson:"cmd,omitempty"`
Args []string `bson:"args,omitempty"`
Host string `bson:"host,omitempty"`
Dir string `bson:"dir,omitempty"`
}
CmdSchema struct { type Schemas struct {
ID primitive.ObjectID `bson:"_id,omitempty"` CmdConfigSchema
Cmd string `bson:"cmd,omitempty"` CmdSchema
Args []string `bson:"args,omitempty"` }
Host string `bson:"host,omitempty"`
Dir string `bson:"dir,omitempty"`
}
Schemas struct { // Host defines a host to which to connect.
CmdConfigSchema // If not provided, the values will be looked up in the default ssh config files
CmdSchema type Host struct {
} ConfigFilePath string `yaml:"configfilepath,omitempty"`
Host string `yaml:"host,omitempty"`
HostName string `yaml:"hostname,omitempty"`
KnownHostsFile string `yaml:"knownhostsfile,omitempty"`
ClientConfig *ssh.ClientConfig
SSHConfigFile *sshConfigFile
SshClient *ssh.Client
Port uint16 `yaml:"port,omitempty"`
ProxyJump string `yaml:"proxyjump,omitempty"`
Password string `yaml:"password,omitempty"`
PrivateKeyPath string `yaml:"privatekeypath,omitempty"`
PrivateKeyPassword string `yaml:"privatekeypassword,omitempty"`
UseConfigFiles bool `yaml:"use_config_files,omitempty"`
useDefaultConfig bool
User string `yaml:"user,omitempty"`
// ProxyHost holds the configuration for a ProxyJump host
ProxyHost []*Host
}
// Host defines a host to which to connect. type sshConfigFile struct {
// If not provided, the values will be looked up in the default ssh config files SshConfigFile *ssh_config.Config
Host struct { DefaultUserSettings *ssh_config.UserSettings
ConfigFilePath string `yaml:"configfilepath,omitempty"` }
Host string `yaml:"host,omitempty"`
HostName string `yaml:"hostname,omitempty"`
KnownHostsFile string `yaml:"knownhostsfile,omitempty"`
ClientConfig *ssh.ClientConfig
SSHConfigFile *sshConfigFile
SshClient *ssh.Client
Port uint16 `yaml:"port,omitempty"`
ProxyJump string `yaml:"proxyjump,omitempty"`
Password string `yaml:"password,omitempty"`
PrivateKeyPath string `yaml:"privatekeypath,omitempty"`
PrivateKeyPassword string `yaml:"privatekeypassword,omitempty"`
useDefaultConfig bool
User string `yaml:"user,omitempty"`
// ProxyHost holds the configuration for a ProxyJump host
ProxyHost []*Host
// CertPath string `yaml:"cert_path,omitempty"`
}
sshConfigFile struct { type Command struct {
SshConfigFile *ssh_config.Config // Remote bool `yaml:"remote,omitempty"`
DefaultUserSettings *ssh_config.UserSettings
}
Command struct { Output BackyCommandOutput `yaml:"-"`
// command to run // command to run
Cmd string `yaml:"cmd"` Cmd string `yaml:"cmd"`
// host on which to run cmd // host on which to run cmd
Host *string `yaml:"host,omitempty"` Host *string `yaml:"host,omitempty"`
/* /*
Shell specifies which shell to run the command in, if any. Shell specifies which shell to run the command in, if any.
Not applicable when host is defined. Not applicable when host is defined.
*/ */
Shell string `yaml:"shell,omitempty"` Shell string `yaml:"shell,omitempty"`
RemoteHost *Host `yaml:"-"` RemoteHost *Host `yaml:"-"`
// Args is an array that holds the arguments to cmd // Args is an array that holds the arguments to cmd
Args []string `yaml:"args,omitempty"` Args []string `yaml:"Args,omitempty"`
/* /*
Dir specifies a directory in which to run the command. Dir specifies a directory in which to run the command.
Ignored if Host is set. Ignored if Host is set.
*/ */
Dir *string `yaml:"dir,omitempty"` Dir *string `yaml:"dir,omitempty"`
// Env points to a file containing env variables to be used with the command // Env points to a file containing env variables to be used with the command
Env string `yaml:"env,omitempty"` Env string `yaml:"env,omitempty"`
// Environment holds env variables to be used with the command // Environment holds env variables to be used with the command
Environment []string `yaml:"environment,omitempty"` Environment []string `yaml:"environment,omitempty"`
} }
BackyOptionFunc func(*BackyConfigOpts) type BackyOptionFunc func(*BackyConfigOpts)
CmdList struct { type CmdList struct {
Name string `yaml:"name,omitempty"` Name string `yaml:"name,omitempty"`
Cron string `yaml:"cron,omitempty"` Cron string `yaml:"cron,omitempty"`
Order []string `yaml:"order,omitempty"` Order []string `yaml:"order,omitempty"`
Notifications []string `yaml:"notifications,omitempty"` Notifications []string `yaml:"notifications,omitempty"`
NotifyConfig *notify.Notify NotifyConfig *notify.Notify
// NotificationsConfig map[string]*NotificationsConfig // NotificationsConfig map[string]*NotificationsConfig
// NotifyConfig map[string]*notify.Notify // NotifyConfig map[string]*notify.Notify
} }
BackyConfigFile struct { type BackyConfigFile struct {
// Cmds holds the commands for a list. // Cmds holds the commands for a list.
// Key is the name of the command, // Key is the name of the command,
Cmds map[string]*Command `yaml:"commands"` Cmds map[string]*Command `yaml:"commands"`
// CmdConfigLists holds the lists of commands to be run in order. // CmdConfigLists holds the lists of commands to be run in order.
// Key is the command list name. // Key is the command list name.
CmdConfigLists map[string]*CmdList `yaml:"cmd-configs"` CmdConfigLists map[string]*CmdList `yaml:"cmd-configs"`
// Hosts holds the Host config. // Hosts holds the Host config.
// key is the host. // key is the host.
Hosts map[string]*Host `yaml:"hosts"` Hosts map[string]*Host `yaml:"hosts"`
// Notifications holds the config for different notifications. // Notifications holds the config for different notifications.
Notifications map[string]*NotificationsConfig Notifications map[string]*NotificationsConfig
Logger zerolog.Logger Logger zerolog.Logger
} }
BackyConfigOpts struct { type BackyConfigOpts struct {
// Global log level // Global log level
BackyLogLvl *string BackyLogLvl *string
// Holds config file // Holds config file
ConfigFile *BackyConfigFile ConfigFile *BackyConfigFile
// Holds config file // Holds config file
ConfigFilePath string ConfigFilePath string
Schemas Schemas
DB *mongo.Database DB *mongo.Database
// use command lists using cron // use command lists using cron
useCron bool useCron bool
// Holds commands to execute for the exec command // Holds commands to execute for the exec command
executeCmds []string executeCmds []string
// Holds commands to execute for the exec command // Holds commands to execute for the exec command
executeLists []string executeLists []string
// Holds env vars from .env file // Holds env vars from .env file
backyEnv map[string]string backyEnv map[string]string
viper *viper.Viper viper *viper.Viper
} }
NotificationsConfig struct { type NotificationsConfig struct {
Config *viper.Viper Config *viper.Viper
Enabled bool Enabled bool
} }
CmdOutput struct { type CmdOutput struct {
Err error Err error
Output bytes.Buffer Output bytes.Buffer
} }
environmentVars struct { type BackyCommandOutput interface {
file string Error() error
env []string GetOutput() CmdOutput
} }
msgTemplates struct { type environmentVars struct {
success *template.Template file string
err *template.Template env []string
} }
)

View File

@ -44,12 +44,13 @@ func injectEnvIntoSSH(envVarsToInject environmentVars, process *ssh.Session, log
} }
errEnvFile: errEnvFile:
// fmt.Printf("%v", envVarsToInject.env) if len(envVarsToInject.env) > 0 {
for _, envVal := range envVarsToInject.env { for _, envVal := range envVarsToInject.env {
// don't append env Vars for Backy // don't append env Vars for Backy
if strings.Contains(envVal, "=") { if strings.Contains(envVal, "=") && !strings.HasPrefix(envVal, "BACKY_") {
envVarArr := strings.Split(envVal, "=") envVarArr := strings.Split(envVal, "=")
process.Setenv(envVarArr[0], envVarArr[1]) process.Setenv(envVarArr[0], envVarArr[1])
}
} }
} }
} }
@ -74,13 +75,14 @@ func injectEnvIntoLocalCMD(envVarsToInject environmentVars, process *exec.Cmd, l
} }
errEnvFile: errEnvFile:
if len(envVarsToInject.env) > 0 {
for _, envVal := range envVarsToInject.env { for _, envVal := range envVarsToInject.env {
if strings.Contains(envVal, "=") { if strings.Contains(envVal, "=") {
process.Env = append(process.Env, envVal) process.Env = append(process.Env, envVal)
}
} }
} }
process.Env = append(process.Env, os.Environ()...) envVarsToInject.env = append(envVarsToInject.env, os.Environ()...)
} }
func (cmd *Command) checkCmdExists() bool { func (cmd *Command) checkCmdExists() bool {
@ -102,8 +104,9 @@ func CheckConfigValues(config *viper.Viper) {
for _, key := range requiredKeys { for _, key := range requiredKeys {
isKeySet := config.IsSet(key) isKeySet := config.IsSet(key)
if !isKeySet { if !isKeySet {
logging.ExitWithMSG(Sprintf("Config key %s is not defined in %s. Please make sure this value is set and has the appropriate keys set.", key, config.ConfigFileUsed()), 1, nil) logging.ExitWithMSG(Sprintf("Config key %s is not defined in %s", key, config.ConfigFileUsed()), 1, nil)
} }
} }
} }
@ -114,6 +117,8 @@ func testFile(c string) error {
if errors.Is(fileOpenErr, os.ErrNotExist) { if errors.Is(fileOpenErr, os.ErrNotExist) {
return fileOpenErr return fileOpenErr
} }
fmt.Printf("%s\t\t%v", c, fileOpenErr)
} }
return nil return nil

8
release Executable file → Normal file
View File

@ -1,6 +1,4 @@
#!/bin/bash #!/bin/bash
export GORELEASER_CURRENT_TAG="$(go run backy.go version)" git tag "$(svu next)"
git tag "$(go run backy.go version)" git push --tags
git push all goreleaser --rm-dist
git push all --tags
goreleaser release -f .goreleaser/gitea.yml --clean --release-notes=".changes/$(go run backy.go version).md"