package main import ( "bytes" "fmt" "io" "os" "path/filepath" "strings" "os/exec" "os/user" "git.andrewnw.xyz/CyberShell/backy/pkg/backy" "github.com/kevinburke/ssh_config" "github.com/spf13/viper" "golang.org/x/crypto/ssh" ) // const logFile = "backy.log" type backupCommand struct { // command to run Cmd string // host on which to run cmd Host string // Shell specifies which shell to run the command in, if any // Not applicable when host is not local Shell string // cmdArgs is an array that holds the arguments to cmd CmdArgs []string } // type config struct { // Commands struct { // backupCommand `mapstructure:",squash"` // } // } var testCmd = &backupCommand{ Cmd: "hostname", Shell: "bash", CmdArgs: []string{}, } var remoteHost backy.Host func main() { var test = true if test { readConfigFile() } else { remoteHost.User = "root" testCmd.runCmd() // shutdownEmailSvr.runCmd() // backupEmailSvrRestic.runCmd() // backupEmailSvrRsync.runCmd() // startEmailSvr.runCmd() } } func (command *backupCommand) runCmd() { 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 != "local" { remoteHost.Port = 22 remoteHost.Host = command.Host sshc, err := connectToSSHHost(&remoteHost) if err != nil { panic(fmt.Errorf("ssh dial: %w", err)) } defer sshc.Close() s, err := sshc.NewSession() if err != nil { panic(fmt.Errorf("new ssh session: %w", err)) } defer s.Close() cmd := command.Cmd for _, a := range command.CmdArgs { cmd += " " + a } var stdoutBuf, stderrBuf bytes.Buffer s.Stdout = io.MultiWriter(os.Stdout, &stdoutBuf) s.Stderr = io.MultiWriter(os.Stderr, &stderrBuf) err = s.Run(cmd) if err != nil { panic(fmt.Errorf("error when running cmd " + cmd + "\n Error: " + err.Error())) } } else { // shell := "/bin/bash" var err error if command.Shell != "" { cmdArgsStr = fmt.Sprintf("%s %s", command.Cmd, cmdArgsStr) localCMD := exec.Command(command.Shell, "-c", cmdArgsStr) var stdoutBuf, stderrBuf bytes.Buffer localCMD.Stdout = io.MultiWriter(os.Stdout, &stdoutBuf) localCMD.Stderr = io.MultiWriter(os.Stderr, &stderrBuf) err = localCMD.Run() if err != nil { panic(fmt.Errorf("error when running cmd: %s: %w", command.Cmd, err)) } return } localCMD := exec.Command(command.Cmd, command.CmdArgs...) var stdoutBuf, stderrBuf bytes.Buffer localCMD.Stdout = io.MultiWriter(os.Stdout, &stdoutBuf) localCMD.Stderr = io.MultiWriter(os.Stderr, &stderrBuf) err = localCMD.Run() if err != nil { panic(fmt.Errorf("error when running cmd: %s: %w", command.Cmd, err)) } // fmt.Printf("%s\n", string(output)) } } func connectToSSHHost(remoteConfig *backy.Host) (*ssh.Client, error) { var sshc *ssh.Client var connectErr error f, _ := os.Open(filepath.Join(os.Getenv("HOME"), ".ssh", "config")) cfg, _ := ssh_config.Decode(f) for _, host := range cfg.Hosts { if host.Matches(remoteConfig.Host) { var identityFile string if remoteConfig.PrivateKeyPath == "" { identityFile, _ = cfg.Get(remoteConfig.Host, "IdentityFile") usr, _ := user.Current() dir := usr.HomeDir if identityFile == "~" { // In case of "~", which won't be caught by the "else if" identityFile = dir } else if strings.HasPrefix(identityFile, "~/") { // Use strings.HasPrefix so we don't match paths like // "/something/~/something/" identityFile = filepath.Join(dir, identityFile[2:]) } remoteConfig.PrivateKeyPath = filepath.Join(identityFile) } remoteConfig.HostName, _ = cfg.Get(remoteConfig.Host, "HostName") if remoteConfig.HostName == "" { remoteConfig.HostName = remoteConfig.Host } port, _ := cfg.Get(remoteConfig.Host, "Port") if port == "" { port = "22" } privateKey, err := os.ReadFile(remoteConfig.PrivateKeyPath) remoteConfig.HostName = remoteConfig.HostName + ":" + port if err != nil { panic(fmt.Errorf("read private key: %w", err)) } signer, err := ssh.ParsePrivateKey(privateKey) if err != nil { panic(fmt.Errorf("parse private key: %w", err)) } sshConfig := &ssh.ClientConfig{ User: remoteHost.User, Auth: []ssh.AuthMethod{ssh.PublicKeys(signer)}, HostKeyCallback: ssh.InsecureIgnoreHostKey(), } sshc, connectErr = ssh.Dial("tcp", remoteConfig.HostName, sshConfig) break } } return sshc, connectErr } func readConfigFile() { viper.SetConfigName("backy") // name of config file (without extension) viper.SetConfigType("yaml") // REQUIRED if the config file does not have the extension in the name viper.AddConfigPath("$HOME/.config/backy") // call multiple times to add many search paths viper.AddConfigPath(".") // optionally look for config in the working directory err := viper.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 err := viper.ReadInConfig(); err != nil { // if _, ok := err.(viper.ConfigFileNotFoundError); ok { // // Config file not found; ignore error if desired // } else { // // Config file was found but another error was produced // } // } // Config file found and successfully parsed // viper.WriteConfig() // writes current config to predefined path set by 'viper.AddConfigPath()' and 'viper.SetConfigName' // err = viper.Unmarshal(&C) // if err != nil { // panic(fmt.Errorf("unable to decode into struct, %v", err)) // } commandsMap := viper.GetStringMapString("commands") var cmdNames []string for k := range commandsMap { cmdNames = append(cmdNames, k) } var cmdStructs []backupCommand for _, cmdName := range cmdNames { var backupCmdStruct backupCommand println(cmdName) subCmd := viper.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") if hostSet { println("Host:") println(host) backupCmdStruct.Host = host } else { println("Host is not set") } if cmdSet { println("Cmd:") println(cmd) backupCmdStruct.Cmd = cmd } else { println("Cmd is not set") } if cmdArgsSet { println("CmdArgs:") for _, arg := range cmdArgs { println(arg) } backupCmdStruct.CmdArgs = cmdArgs } else { println("CmdArgs is not set") } cmdStructs = append(cmdStructs, backupCmdStruct) for _, cmd := range cmdStructs { fmt.Printf("Cmd is %s\n", cmd.Cmd) fmt.Printf("Host is %s\n", cmd.Host) } } cmdOrder := viper.GetStringMapStringSlice("cmd-order") for cmd, orderSlice := range cmdOrder { println("Cmd name: ", cmd) println("Cmd order: ") for _, v := range orderSlice { println(v) } } } func getNestedConfig(nestedConfig, key string) string { return fmt.Sprintf("%s.%s", nestedConfig, key) }