Progress on sub-commands, added unmarshalling

This commit is contained in:
Andrew Woodlee 2023-01-09 22:18:56 -06:00
parent ae87ccb4b5
commit 15a7ca6c3d
4 changed files with 190 additions and 116 deletions

28
cmd/backup.go Normal file
View File

@ -0,0 +1,28 @@
package cmd
import (
"git.andrewnw.xyz/CyberShell/backy/pkg/backy"
"github.com/spf13/cobra"
)
var (
backupCmd = &cobra.Command{
Use: "backup [--commands==list1,list2]",
Short: "Runs commands defined in config file.",
Long: `Backup executes commands defined in config file,
use the -cmds flag to execute the specified commands.`,
}
)
var CmdList *[]string
func init() {
cobra.OnInitialize(initConfig)
backupCmd.Flags().StringSliceVarP(CmdList, "commands", "cmds", nil, "Accepts a comma-separated list of command lists to execute.")
}
func backup() {
backyConfig := backy.NewOpts(cfgFile)
backyConfig.GetConfig()
}

View File

@ -32,7 +32,7 @@ func init() {
rootCmd.PersistentFlags().StringVarP(&cfgFile, "config", "c", "", "config file to read from") rootCmd.PersistentFlags().StringVarP(&cfgFile, "config", "c", "", "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.PersistentFlags().Bool("viper", true, "use Viper for configuration")
} }
func initConfig() { func initConfig() {

View File

@ -7,15 +7,19 @@ import (
"io" "io"
"os" "os"
"os/exec" "os/exec"
"strings"
"time"
"github.com/rs/zerolog/log" "github.com/rs/zerolog"
"github.com/spf13/viper" "github.com/spf13/viper"
"gopkg.in/natefinch/lumberjack.v2"
) )
// Host defines a host to which to connect // Host defines a host to which to connect
// If not provided, the values will be looked up in the default ssh config files // If not provided, the values will be looked up in the default ssh config files
type Host struct { type Host struct {
ConfigFilePath string ConfigFilePath string
UseConfigFile bool
Empty bool Empty bool
Host string Host string
HostName string HostName string
@ -26,44 +30,55 @@ type Host struct {
} }
type Command struct { type Command struct {
Remote bool Remote bool `yaml:"remote,omitempty"`
// command to run // command to run
Cmd string Cmd string `yaml:"cmd"`
// host on which to run cmd // host on which to run cmd
Host string 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 Shell string `yaml:"shell,omitempty"`
RemoteHost Host RemoteHost Host `yaml:"-"`
// cmdArgs is an array that holds the arguments to cmd // cmdArgs is an array that holds the arguments to cmd
CmdArgs []string CmdArgs []string `yaml:"cmdArgs,omitempty"`
/*
Dir specifies a directory in which to run the command.
Ignored if Host is set.
*/
Dir *string `yaml:"dir,omitempty"`
}
type BackyGlobalOpts struct {
} }
type 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 Cmds map[string]Command `yaml:"commands"`
/* /*
CmdLists holds the lists of commands to be run in order CmdLists holds the lists of commands to be run in order.
key is the command list name Key is the command list name.
*/ */
CmdLists map[string][]string CmdLists map[string][]string `yaml:"cmd-lists"`
/* /*
Hosts holds the Host config Hosts holds the Host config.
key is the host key is the host.
*/ */
Hosts map[string]Host Hosts map[string]Host `yaml:"hosts"`
Logger zerolog.Logger
} }
// BackupConfig is a configuration struct that is used to define backups // BackupConfig is a configuration struct that is used to define backups
@ -79,19 +94,19 @@ type BackupConfig struct {
* Runs a backup configuration * Runs a backup configuration
*/ */
func (command Command) RunCmd() { func (command Command) RunCmd(log *zerolog.Logger) {
var cmdArgsStr string var cmdArgsStr string
for _, v := range command.CmdArgs { for _, v := range command.CmdArgs {
cmdArgsStr += fmt.Sprintf(" %s", v) cmdArgsStr += fmt.Sprintf(" %s", v)
} }
fmt.Printf("\n\nRunning command: " + command.Cmd + " " + cmdArgsStr + " on host " + command.Host + "...\n\n") fmt.Printf("\n\nRunning command: " + command.Cmd + " " + cmdArgsStr + " on host " + *command.Host + "...\n\n")
if command.Host != "" { if command.Host != nil {
command.RemoteHost.Host = command.Host command.RemoteHost.Host = *command.Host
command.RemoteHost.Port = 22 command.RemoteHost.Port = 22
sshc, err := command.RemoteHost.ConnectToSSHHost() sshc, err := command.RemoteHost.ConnectToSSHHost(log)
if err != nil { if err != nil {
panic(fmt.Errorf("ssh dial: %w", err)) panic(fmt.Errorf("ssh dial: %w", err))
} }
@ -123,6 +138,9 @@ func (command Command) RunCmd() {
if command.Shell != "" { if command.Shell != "" {
cmdArgsStr = fmt.Sprintf("%s %s", command.Cmd, cmdArgsStr) cmdArgsStr = fmt.Sprintf("%s %s", command.Cmd, cmdArgsStr)
localCMD := exec.Command(command.Shell, "-c", cmdArgsStr) localCMD := exec.Command(command.Shell, "-c", cmdArgsStr)
if command.Dir != nil {
localCMD.Dir = *command.Dir
}
var stdoutBuf, stderrBuf bytes.Buffer var stdoutBuf, stderrBuf bytes.Buffer
localCMD.Stdout = io.MultiWriter(os.Stdout, &stdoutBuf) localCMD.Stdout = io.MultiWriter(os.Stdout, &stdoutBuf)
@ -137,7 +155,9 @@ func (command Command) RunCmd() {
return return
} }
localCMD := exec.Command(command.Cmd, command.CmdArgs...) localCMD := exec.Command(command.Cmd, command.CmdArgs...)
if command.Dir != nil {
localCMD.Dir = *command.Dir
}
var stdoutBuf, stderrBuf bytes.Buffer var stdoutBuf, stderrBuf bytes.Buffer
localCMD.Stdout = io.MultiWriter(os.Stdout, &stdoutBuf) localCMD.Stdout = io.MultiWriter(os.Stdout, &stdoutBuf)
localCMD.Stderr = io.MultiWriter(os.Stderr, &stderrBuf) localCMD.Stderr = io.MultiWriter(os.Stderr, &stderrBuf)
@ -154,17 +174,49 @@ func (config *BackyConfigFile) RunBackyConfig() {
for _, list := range config.CmdLists { for _, list := range config.CmdLists {
for _, cmd := range list { for _, cmd := range list {
cmdToRun := config.Cmds[cmd] cmdToRun := config.Cmds[cmd]
cmdToRun.RunCmd() cmdToRun.RunCmd(&config.Logger)
} }
} }
} }
type BackyConfigOpts struct {
// Holds config file
ConfigFile *BackyConfigFile
// Holds config file
ConfigFilePath string
// Global log level
BackyLogLvl *string
}
type BackyOptionFunc func(*BackyConfigOpts)
func (c *BackyConfigOpts) LogLvl(level string) BackyOptionFunc {
return func(bco *BackyConfigOpts) {
c.BackyLogLvl = &level
}
}
func (c *BackyConfigOpts) GetConfig() {
c.ConfigFile = ReadAndParseConfigFile(c.ConfigFilePath)
}
func New() BackupConfig { func New() BackupConfig {
return BackupConfig{} return BackupConfig{}
} }
// NewConfig initializes new config that holds information func NewOpts(configFilePath string, opts ...BackyOptionFunc) *BackyConfigOpts {
// from the config file b := &BackyConfigOpts{}
b.ConfigFilePath = configFilePath
for _, opt := range opts {
opt(b)
}
return b
}
/*
* NewConfig initializes new config that holds information
* from the config file
*/
func NewConfig() *BackyConfigFile { func NewConfig() *BackyConfigFile {
return &BackyConfigFile{ return &BackyConfigFile{
Cmds: make(map[string]Command), Cmds: make(map[string]Command),
@ -173,21 +225,89 @@ func NewConfig() *BackyConfigFile {
} }
} }
func ReadAndParseConfigFile() *BackyConfigFile { func ReadAndParseConfigFile(configFile string) *BackyConfigFile {
backyConfigFile := NewConfig() backyConfigFile := NewConfig()
backyViper := viper.New() backyViper := viper.New()
if configFile != "" {
backyViper.SetConfigFile(configFile)
} else {
backyViper.SetConfigName("backy") // name of config file (without extension) backyViper.SetConfigName("backy") // name of config file (without extension)
backyViper.SetConfigType("yaml") // REQUIRED if the config file does not have the extension in the name backyViper.SetConfigType("yaml") // REQUIRED if the config file does not have the extension in the name
backyViper.AddConfigPath(".") // optionally look for config in the working directory backyViper.AddConfigPath(".") // optionally look for config in the working directory
backyViper.AddConfigPath("$HOME/.config/backy") // call multiple times to add many search paths backyViper.AddConfigPath("$HOME/.config/backy") // call multiple times to add many search paths
}
err := backyViper.ReadInConfig() // Find and read the config file err := backyViper.ReadInConfig() // Find and read the config file
if err != nil { // Handle errors reading the config file if err != nil { // Handle errors reading the config file
panic(fmt.Errorf("fatal error config file: %w", err)) panic(fmt.Errorf("fatal error finding config file: %w", err))
} }
backyLoggingOpts := backyViper.Sub("logging")
verbose := backyLoggingOpts.GetBool("verbose")
logFile := backyLoggingOpts.GetString("file")
if verbose {
zerolog.Level.String(zerolog.DebugLevel)
}
output := zerolog.ConsoleWriter{Out: os.Stdout, TimeFormat: time.RFC1123}
output.FormatLevel = func(i interface{}) string {
return strings.ToUpper(fmt.Sprintf("| %-6s|", i))
}
output.FormatMessage = func(i interface{}) string {
return fmt.Sprintf("%s", i)
}
output.FormatFieldName = func(i interface{}) string {
return fmt.Sprintf("%s: ", i)
}
output.FormatFieldValue = func(i interface{}) string {
return strings.ToUpper(fmt.Sprintf("%s", i))
}
fileLogger := &lumberjack.Logger{
MaxSize: 500, // megabytes
MaxBackups: 3,
MaxAge: 28, //days
Compress: true, // disabled by default
}
if strings.Trim(logFile, " ") != "" {
fileLogger.Filename = logFile
} else {
fileLogger.Filename = "./backy.log"
}
// UNIX Time is faster and smaller than most timestamps
zerolog.TimeFieldFormat = zerolog.TimeFormatUnix
// zerolog.TimeFieldFormat = time.RFC1123
writers := zerolog.MultiLevelWriter(os.Stdout, fileLogger)
log := zerolog.New(writers).With().Timestamp().Logger()
backyConfigFile.Logger = log
commandsMap := backyViper.GetStringMapString("commands") commandsMap := backyViper.GetStringMapString("commands")
commandsMapViper := backyViper.Sub("commands")
unmarshalErr := commandsMapViper.Unmarshal(&backyConfigFile.Cmds)
if unmarshalErr != nil {
panic(fmt.Errorf("error unmarshalling cmds struct: %w", unmarshalErr))
} else {
for cmdName, cmdConf := range backyConfigFile.Cmds {
fmt.Printf("\nCommand Name: %s\n", cmdName)
fmt.Printf("Shell: %v\n", cmdConf.Shell)
fmt.Printf("Command: %s\n", cmdConf.Cmd)
if len(cmdConf.CmdArgs) > 0 {
fmt.Println("\nCmd Args:")
for _, args := range cmdConf.CmdArgs {
fmt.Printf("%s\n", args)
}
}
if cmdConf.Host != nil {
fmt.Printf("Host: %s\n", *backyConfigFile.Cmds[cmdName].Host)
}
}
os.Exit(0)
}
var cmdNames []string var cmdNames []string
for k := range commandsMap { for k := range commandsMap {
cmdNames = append(cmdNames, k) cmdNames = append(cmdNames, k)
@ -196,63 +316,30 @@ func ReadAndParseConfigFile() *BackyConfigFile {
for _, cmdName := range cmdNames { for _, cmdName := range cmdNames {
var backupCmdStruct Command var backupCmdStruct Command
println(cmdName)
subCmd := backyViper.Sub(getNestedConfig("commands", cmdName)) subCmd := backyViper.Sub(getNestedConfig("commands", cmdName))
hostSet := subCmd.IsSet("host") hostSet := subCmd.IsSet("host")
host := subCmd.GetString("host") host := subCmd.GetString("host")
cmdSet := subCmd.IsSet("cmd")
cmd := subCmd.GetString("cmd")
cmdArgsSet := subCmd.IsSet("cmdargs")
cmdArgs := subCmd.GetStringSlice("cmdargs")
shellSet := subCmd.IsSet("shell")
shell := subCmd.GetString("shell")
if hostSet { if hostSet {
println("Host:") log.Debug().Timestamp().Str(cmdName, "host is set").Str("host", host).Send()
println(host) backupCmdStruct.Host = &host
backupCmdStruct.Host = host
if backyViper.IsSet(getNestedConfig("hosts", host)) { if backyViper.IsSet(getNestedConfig("hosts", host)) {
hostconfig := backyViper.Sub(getNestedConfig("hosts", host)) hostconfig := backyViper.Sub(getNestedConfig("hosts", host))
hostConfigsMap[host] = hostconfig hostConfigsMap[host] = hostconfig
} }
} else { } else {
println("Host is not set") log.Debug().Timestamp().Str(cmdName, "host is not set").Send()
} }
if cmdSet {
println("Cmd:") // backyConfigFile.Cmds[cmdName] = backupCmdStruct
println(cmd)
backupCmdStruct.Cmd = cmd
} else {
println("Cmd is not set")
}
if shellSet {
println("Shell:")
println(shell)
backupCmdStruct.Shell = shell
} else {
println("Shell is not set")
}
if cmdArgsSet {
println("CmdArgs:")
for _, arg := range cmdArgs {
println(arg)
}
backupCmdStruct.CmdArgs = cmdArgs
} else {
println("CmdArgs are not set")
}
backyConfigFile.Cmds[cmdName] = backupCmdStruct
} }
cmdListCfg := backyViper.GetStringMapStringSlice("cmd-lists") cmdListCfg := backyViper.GetStringMapStringSlice("cmd-lists")
var cmdNotFoundSliceErr []error var cmdNotFoundSliceErr []error
for cmdListName, cmdList := range cmdListCfg { for cmdListName, cmdList := range cmdListCfg {
println("Cmd list: ", cmdListName)
for _, cmdInList := range cmdList { for _, cmdInList := range cmdList {
println("Command in list: " + cmdInList)
_, cmdNameFound := backyConfigFile.Cmds[cmdInList] _, cmdNameFound := backyConfigFile.Cmds[cmdInList]
if !backyViper.IsSet(getNestedConfig("commands", cmdInList)) && !cmdNameFound { if !backyViper.IsSet(getNestedConfig("commands", cmdInList)) && !cmdNameFound {
cmdNotFoundStr := fmt.Sprintf("command definition %s is not in config file\n", cmdInList) cmdNotFoundStr := fmt.Sprintf("command definition %s is not in config file\n", cmdInList)

View File

@ -2,18 +2,15 @@ package backy
import ( import (
"errors" "errors"
"fmt"
"os" "os"
"os/user" "os/user"
"path/filepath" "path/filepath"
"strings" "strings"
"time"
"github.com/kevinburke/ssh_config" "github.com/kevinburke/ssh_config"
"github.com/rs/zerolog" "github.com/rs/zerolog"
"golang.org/x/crypto/ssh" "golang.org/x/crypto/ssh"
"golang.org/x/crypto/ssh/knownhosts" "golang.org/x/crypto/ssh/knownhosts"
"gopkg.in/natefinch/lumberjack.v2"
) )
type SshConfig struct { type SshConfig struct {
@ -53,45 +50,7 @@ func (config SshConfig) GetSSHConfig() (SshConfig, error) {
return config, nil return config, nil
} }
func (remoteConfig *Host) ConnectToSSHHost() (*ssh.Client, error) { func (remoteConfig *Host) ConnectToSSHHost(log *zerolog.Logger) (*ssh.Client, error) {
output := zerolog.ConsoleWriter{Out: os.Stdout, TimeFormat: time.RFC3339}
output.FormatLevel = func(i interface{}) string {
return strings.ToUpper(fmt.Sprintf("| %-6s|", i))
}
output.FormatMessage = func(i interface{}) string {
return fmt.Sprintf("%s", i)
}
output.FormatFieldName = func(i interface{}) string {
return fmt.Sprintf("%s: ", i)
}
output.FormatFieldValue = func(i interface{}) string {
return strings.ToUpper(fmt.Sprintf("%s", i))
}
fileLogger := &lumberjack.Logger{
Filename: "./backy.log",
MaxSize: 500, // megabytes
MaxBackups: 3,
MaxAge: 28, //days
Compress: true, // disabled by default
}
// fileOutput := zerolog.ConsoleWriter{Out: fileLogger, TimeFormat: time.RFC3339}
// fileOutput.FormatLevel = func(i interface{}) string {
// return strings.ToUpper(fmt.Sprintf("| %-6s|", i))
// }
// fileOutput.FormatMessage = func(i interface{}) string {
// return fmt.Sprintf("%s", i)
// }
// fileOutput.FormatFieldName = func(i interface{}) string {
// return fmt.Sprintf("%s: ", i)
// }
// fileOutput.FormatFieldValue = func(i interface{}) string {
// return strings.ToUpper(fmt.Sprintf("%s", i))
// }
zerolog.TimeFieldFormat = time.RFC1123
writers := zerolog.MultiLevelWriter(os.Stdout, fileLogger)
log := zerolog.New(writers).With().Timestamp().Logger()
var sshClient *ssh.Client var sshClient *ssh.Client
var connectErr error var connectErr error