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().BoolVarP(&verbose, "verbose", "v", false, "Sets verbose level")
rootCmd.PersistentFlags().Bool("viper", true, "use Viper for configuration")
}
func initConfig() {

View File

@ -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")
log.Debug().Timestamp().Str(cmdName, "host is not set").Send()
}
if cmdSet {
println("Cmd:")
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
// 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)

View File

@ -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