diff --git a/cmd/backup.go b/cmd/backup.go new file mode 100644 index 0000000..ee81884 --- /dev/null +++ b/cmd/backup.go @@ -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() +} diff --git a/cmd/root.go b/cmd/root.go index f0a2e99..fec7602 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -32,7 +32,7 @@ func init() { rootCmd.PersistentFlags().StringVarP(&cfgFile, "config", "c", "", "config file to read from") rootCmd.PersistentFlags().BoolVarP(&verbose, "verbose", "v", false, "Sets verbose level") - rootCmd.PersistentFlags().Bool("viper", true, "use Viper for configuration") + } func initConfig() { diff --git a/pkg/backy/backy.go b/pkg/backy/backy.go index 1904284..533a72e 100644 --- a/pkg/backy/backy.go +++ b/pkg/backy/backy.go @@ -7,15 +7,19 @@ import ( "io" "os" "os/exec" + "strings" + "time" - "github.com/rs/zerolog/log" + "github.com/rs/zerolog" "github.com/spf13/viper" + "gopkg.in/natefinch/lumberjack.v2" ) // Host defines a host to which to connect // If not provided, the values will be looked up in the default ssh config files type Host struct { ConfigFilePath string + UseConfigFile bool Empty bool Host string HostName string @@ -26,44 +30,55 @@ type Host struct { } type Command struct { - Remote bool + Remote bool `yaml:"remote,omitempty"` // command to run - Cmd string + Cmd string `yaml:"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 - Not applicable when host is defined + Shell specifies which shell to run the command in, if any. + 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 []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 { /* - Cmds holds the commands for a list - key is the name of the command + Cmds holds the commands for a list. + 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 - key is the command list name + CmdLists holds the lists of commands to be run in order. + Key is the command list name. */ - CmdLists map[string][]string + CmdLists map[string][]string `yaml:"cmd-lists"` /* - Hosts holds the Host config - key is the host + Hosts holds the Host config. + 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 @@ -79,19 +94,19 @@ type BackupConfig struct { * Runs a backup configuration */ -func (command Command) RunCmd() { +func (command Command) RunCmd(log *zerolog.Logger) { var cmdArgsStr string for _, v := range command.CmdArgs { cmdArgsStr += fmt.Sprintf(" %s", v) } - fmt.Printf("\n\nRunning command: " + command.Cmd + " " + cmdArgsStr + " on host " + command.Host + "...\n\n") - if command.Host != "" { + fmt.Printf("\n\nRunning command: " + command.Cmd + " " + cmdArgsStr + " on host " + *command.Host + "...\n\n") + if command.Host != nil { - command.RemoteHost.Host = command.Host + command.RemoteHost.Host = *command.Host command.RemoteHost.Port = 22 - sshc, err := command.RemoteHost.ConnectToSSHHost() + sshc, err := command.RemoteHost.ConnectToSSHHost(log) if err != nil { panic(fmt.Errorf("ssh dial: %w", err)) } @@ -123,6 +138,9 @@ func (command Command) RunCmd() { if command.Shell != "" { cmdArgsStr = fmt.Sprintf("%s %s", command.Cmd, cmdArgsStr) localCMD := exec.Command(command.Shell, "-c", cmdArgsStr) + if command.Dir != nil { + localCMD.Dir = *command.Dir + } var stdoutBuf, stderrBuf bytes.Buffer localCMD.Stdout = io.MultiWriter(os.Stdout, &stdoutBuf) @@ -137,7 +155,9 @@ func (command Command) RunCmd() { return } localCMD := exec.Command(command.Cmd, command.CmdArgs...) - + if command.Dir != nil { + localCMD.Dir = *command.Dir + } var stdoutBuf, stderrBuf bytes.Buffer localCMD.Stdout = io.MultiWriter(os.Stdout, &stdoutBuf) localCMD.Stderr = io.MultiWriter(os.Stderr, &stderrBuf) @@ -154,17 +174,49 @@ func (config *BackyConfigFile) RunBackyConfig() { for _, list := range config.CmdLists { for _, cmd := range list { 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 { return BackupConfig{} } -// NewConfig initializes new config that holds information -// from the config file +func NewOpts(configFilePath string, opts ...BackyOptionFunc) *BackyConfigOpts { + 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 { return &BackyConfigFile{ Cmds: make(map[string]Command), @@ -173,21 +225,89 @@ func NewConfig() *BackyConfigFile { } } -func ReadAndParseConfigFile() *BackyConfigFile { +func ReadAndParseConfigFile(configFile string) *BackyConfigFile { backyConfigFile := NewConfig() backyViper := viper.New() - 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.AddConfigPath(".") // optionally look for config in the working directory - backyViper.AddConfigPath("$HOME/.config/backy") // call multiple times to add many search paths - err := backyViper.ReadInConfig() // Find and read the config file - if err != nil { // Handle errors reading the config file - panic(fmt.Errorf("fatal error config file: %w", err)) + + if configFile != "" { + backyViper.SetConfigFile(configFile) + } else { + 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.AddConfigPath(".") // optionally look for config in the working directory + backyViper.AddConfigPath("$HOME/.config/backy") // call multiple times to add many search paths + } + err := backyViper.ReadInConfig() // Find and read the config file + if err != nil { // Handle errors reading the config file + 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") + 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 for k := range commandsMap { cmdNames = append(cmdNames, k) @@ -196,63 +316,30 @@ func ReadAndParseConfigFile() *BackyConfigFile { for _, cmdName := range cmdNames { var backupCmdStruct Command - println(cmdName) subCmd := backyViper.Sub(getNestedConfig("commands", cmdName)) hostSet := subCmd.IsSet("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 { - println("Host:") - println(host) - backupCmdStruct.Host = host + log.Debug().Timestamp().Str(cmdName, "host is set").Str("host", host).Send() + backupCmdStruct.Host = &host if backyViper.IsSet(getNestedConfig("hosts", host)) { hostconfig := backyViper.Sub(getNestedConfig("hosts", host)) hostConfigsMap[host] = hostconfig } } else { - println("Host is not set") - } - if cmdSet { - println("Cmd:") - println(cmd) - backupCmdStruct.Cmd = cmd - } else { - println("Cmd is not set") + log.Debug().Timestamp().Str(cmdName, "host is not set").Send() } - 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 + + // backyConfigFile.Cmds[cmdName] = backupCmdStruct } cmdListCfg := backyViper.GetStringMapStringSlice("cmd-lists") var cmdNotFoundSliceErr []error for cmdListName, cmdList := range cmdListCfg { - println("Cmd list: ", cmdListName) for _, cmdInList := range cmdList { - println("Command in list: " + cmdInList) _, cmdNameFound := backyConfigFile.Cmds[cmdInList] if !backyViper.IsSet(getNestedConfig("commands", cmdInList)) && !cmdNameFound { cmdNotFoundStr := fmt.Sprintf("command definition %s is not in config file\n", cmdInList) diff --git a/pkg/backy/ssh.go b/pkg/backy/ssh.go index 137ecdd..c4bad4f 100644 --- a/pkg/backy/ssh.go +++ b/pkg/backy/ssh.go @@ -2,18 +2,15 @@ package backy import ( "errors" - "fmt" "os" "os/user" "path/filepath" "strings" - "time" "github.com/kevinburke/ssh_config" "github.com/rs/zerolog" "golang.org/x/crypto/ssh" "golang.org/x/crypto/ssh/knownhosts" - "gopkg.in/natefinch/lumberjack.v2" ) type SshConfig struct { @@ -53,45 +50,7 @@ func (config SshConfig) GetSSHConfig() (SshConfig, error) { return config, nil } -func (remoteConfig *Host) ConnectToSSHHost() (*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() +func (remoteConfig *Host) ConnectToSSHHost(log *zerolog.Logger) (*ssh.Client, error) { var sshClient *ssh.Client var connectErr error