diff --git a/README.md b/README.md index 090979e..d1dcd96 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # GoSSH - Open Source Go Infrastucture Automation tool -![](https://github.com/Aponiatowski/GoSSH/workflows/GoSSH/badge.svg) [![Go Report Card](https://goreportcard.com/badge/github.com/APoniatowski/GoSSH)](https://goreportcard.com/report/github.com/APoniatowski/GoSSH) +![](https://github.com/Aponiatowski/GoSSH/workflows/GoSSH/badge.svg) [![Go Report Card](https://goreportcard.com/badge/github.com/APoniatowski/GoSSH)](https://goreportcard.com/report/github.com/APoniatowski/GoSSH) [![codebeat badge](https://codebeat.co/badges/e53dab58-a0df-4699-a4d6-cfe67fbd9b81)](https://codebeat.co/projects/jackfan.us.kg-aponiatowski-gossh-master) ## Project update: It is currently in an usable state, and can be used to execute commands in varied ways and performs well. :+1: @@ -10,12 +10,11 @@ written to a log file for review. If running a command was successful, why would The logs are also rotated by date, to avoid multiple logs, if time was added to the file name. The logging will be enchanced even further, as the project continues. -Currently working on adding the ability to run commands as sudo, and also add some security for connecting to known hosts (see issue board for clarification ( [Issue #7](https://github.com/APoniatowski/GoSSH/issues/7) ) +Sudo commands are possible now. Just make sure you add the username's password to the password field, and it will be used. -I have also removed some possible features, that I was planning on implementing. [^1] - -But rather chose to implement another feature for this release. [^2] +The known_hosts file is causing some issues (issue open for it). Trying to resolve that, before release +Also redoing the CLI, as I would like it to be more seamless and intuitive. I will also be adding bash completion later, once I finish the new CLI. * Windows (laptop): ##### 22 production servers (across 8 different countries): @@ -55,8 +54,3 @@ Options: ## Please feel free to test/use this and leave issues and comments in the issues tab. ## I will be actively working on this for the foreseeable future - - -[^1]: Creating a client side agent, this might possibly be added for v2.0.0 release. Not guaranteed though. - -[^2]: Running a bash script with little effort. Makes things simpler, than trying to cat | gossh all, or gosh all $(cat my-script.sh), etc. diff --git a/clioptions/clioptions.go b/clioptions/clioptions.go new file mode 100644 index 0000000..ee63132 --- /dev/null +++ b/clioptions/clioptions.go @@ -0,0 +1,34 @@ +package clioptions + +import ( + "bufio" + "os" + "strconv" + "strings" +) + +// GeneralCommandParse CLI parser for general commands to linux servers +func GeneralCommandParse(cmd []string) string { + command := strconv.Quote(strings.Join(cmd, " ")) + command = "sh -c " + command + " 2>&1" + return command +} + +//BashScriptParse bash script parser, to pass write the script on the server, run it and remove it. +// It also accepts args for the script. Dependency scripts will not work, as they are considered a separate script +func BashScriptParse(cmd string, cmdargs []string) string { + scriptargs := strings.Join(cmdargs, " ") + script, _ := os.Open(cmd) + defer script.Close() + scanner := bufio.NewScanner(script) + scanner.Split(bufio.ScanLines) + var lines []string + for scanner.Scan() { + lines = append(lines, scanner.Text()) + lines = append(lines, "\n") + } + parsedcmd := strconv.Quote(strings.Join(lines, "")) + parsedcmd = strings.Replace(parsedcmd, `$`, `\$`, -1) + parsedlines := "set +H;echo -e " + parsedcmd + " > /tmp/gossh-script.sh;bash /tmp/gossh-script.sh " + scriptargs + ";rm /tmp/gossh-script.sh;set -H" + return parsedlines +} diff --git a/config/config.yml b/config/config.yml index 751579e..15a32bb 100644 --- a/config/config.yml +++ b/config/config.yml @@ -1,18 +1,16 @@ -ServerGroup1: - Server11: - FQDN: hostname11.whatever.com - Username: user11 - Password: password11 - Key_Path: /path/to/key - Port: 22 - Need_Sudo: false +ServerGroup1: #group name, spacing does not matter + Server11: #server name, spacing does not matter + FQDN: hostname11.whatever.com # + Username: user11 ## + Password: password11 ### FQDN, is needed. Username defaults to root, + Key_Path: /path/to/key ## password or key needed, ports default to 22 + Port: 22 # Server12: FQDN: hostname12.whatever.com Username: user12 Password: password12 Key_Path: /path/to/key Port: 223 - Need_Sudo: true ServerGroup2: Server21: FQDN: hostname21.whatever.com @@ -20,11 +18,9 @@ ServerGroup2: Password: password21 Key_Path: /path/to/key Port: 2233 - Need_Sudo: false Server22: FQDN: hostname22.whatever.com Username: user22 Password: password22 Key_Path: /path/to/key Port: - Need_Sudo: true diff --git a/loggerlib/loggerlib.go b/loggerlib/loggerlib.go index 4803d8a..f6f24c3 100644 --- a/loggerlib/loggerlib.go +++ b/loggerlib/loggerlib.go @@ -8,7 +8,7 @@ import ( ) // OutputLogger All INFO/WARNING messages will be written in this function -func OutputLogger(servername string, output []byte) { +func OutputLogger(servername string, option string, output []byte) { currentDate := time.Now() dateFormatted := currentDate.Format("2006-01-02") path, _ := filepath.Abs("./logs/output/") @@ -19,7 +19,7 @@ func OutputLogger(servername string, output []byte) { log.Println(err) } defer okFile.Close() - logger := log.New(okFile, "[INFO: Succeeded] ", log.LstdFlags) + logger := log.New(okFile, option, log.LstdFlags) logger.Print(servername + ": " + string(output)) } else { log.Println(err) @@ -27,7 +27,7 @@ func OutputLogger(servername string, output []byte) { } // ErrorLogger All ERROR/FATAL messages will be written in this function -func ErrorLogger(servername string, output []byte) { +func ErrorLogger(servername string, option string, output []byte) { currentDate := time.Now() dateFormatted := currentDate.Format("2006-01-02") path, _ := filepath.Abs("./logs/errors/") @@ -38,7 +38,7 @@ func ErrorLogger(servername string, output []byte) { log.Println(err) } defer errFile.Close() - logger := log.New(errFile, "[INFO: Failed] ", log.LstdFlags) + logger := log.New(errFile, option, log.LstdFlags) logger.Print(servername + ": " + string(output)) } else { log.Println(err) diff --git a/main.go b/main.go index 43763c8..7738ae3 100644 --- a/main.go +++ b/main.go @@ -1,50 +1,146 @@ package main import ( - "fmt" "log" "os" - "strconv" - "strings" + "github.com/APoniatowski/GoSSH/clioptions" "github.com/APoniatowski/GoSSH/sshlib" "github.com/APoniatowski/GoSSH/yamlparser" + "github.com/urfave/cli" ) -// Error checking function -func generalError(e error) { - if e != nil { - log.Fatal(e) - } -} - // Main function to carry out operations func main() { var cmd []string - if len(os.Args) > 2 { - cmd = os.Args[2:] // will change this to 3 later, when I see a need to expand on more arguments, eg. running only 1 group, or x amount of servers - } else { - fmt.Println("No command was specified, please specify a command.") - os.Exit(1) + + app := cli.NewApp() + app.Name = "GoSSH" + app.Version = "1.0.0" + app.Usage = "Open Source Go Infrastucture Automation Tool" + app.UsageText = "GoSSH [global options] command [subcommand] [script or arguments...]" + app.EnableBashCompletion = true + app.Commands = []cli.Command{ + { + Name: "sequential", + Aliases: []string{"s"}, + Usage: "Run the command sequentially on all servers in your config file", + Action: func(c *cli.Context) error { + yamlparser.Rollcall() + cmd = os.Args[2:] + command := clioptions.GeneralCommandParse(cmd) + sshlib.RunSequentially(&yamlparser.Config, &command) + return nil + }, + Subcommands: []cli.Command{ + { + Name: "run", + Usage: "Run a bash script on the defined servers", + Action: func(c *cli.Context) error { + yamlparser.Rollcall() + cmd := os.Args[3] + cmdargs := os.Args[4:] + command := clioptions.BashScriptParse(cmd, cmdargs) + sshlib.RunAllServers(&yamlparser.Config, &command) + return nil + }, + }, + //-----------------placeholder-------------------- + // { + // Name: "remove", + // Usage: "remove an existing template", + // Action: func(c *cli.Context) error { + // fmt.Println("removed task template: ", c.Args().First()) + // return nil + // }, + // }, + //-----------------placeholder-------------------- + }, + }, + { + Name: "groups", + Aliases: []string{"g"}, + Usage: "Run the command on all servers per group concurrently in your config file", + Action: func(c *cli.Context) error { + yamlparser.Rollcall() + cmd = os.Args[2:] + command := clioptions.GeneralCommandParse(cmd) + sshlib.RunGroups(&yamlparser.Config, &command) + return nil + }, + Subcommands: []cli.Command{ + { + Name: "run", + Usage: "Run a bash script on the defined servers", + Action: func(c *cli.Context) error { + yamlparser.Rollcall() + cmd := os.Args[3] + cmdargs := os.Args[4:] + command := clioptions.BashScriptParse(cmd, cmdargs) + sshlib.RunAllServers(&yamlparser.Config, &command) + return nil + }, + }, + //-----------------placeholder-------------------- + // { + // Name: "remove", + // Usage: "remove an existing template", + // Action: func(c *cli.Context) error { + // fmt.Println("removed task template: ", c.Args().First()) + // return nil + // }, + // }, + //-----------------placeholder-------------------- + }, + }, + { + Name: "all", + Aliases: []string{"a"}, + Usage: "Run the command on all servers concurrently in your config file", + Action: func(c *cli.Context) error { + yamlparser.Rollcall() + cmd = os.Args[2:] + command := clioptions.GeneralCommandParse(cmd) + sshlib.RunAllServers(&yamlparser.Config, &command) + return nil + }, + Subcommands: []cli.Command{ + { + Name: "run", + Usage: "Run a bash script on the defined servers", + Action: func(c *cli.Context) error { + yamlparser.Rollcall() + cmd := os.Args[3] + cmdargs := os.Args[4:] + command := clioptions.BashScriptParse(cmd, cmdargs) + sshlib.RunAllServers(&yamlparser.Config, &command) + return nil + }, + }, + //-----------------placeholder-------------------- + // { + // Name: "remove", + // Usage: "remove an existing template", + // Action: func(c *cli.Context) error { + // fmt.Println("removed task template: ", c.Args().First()) + // return nil + // }, + // }, + //-----------------placeholder-------------------- + }, + }, } - command := strconv.Quote(strings.Join(cmd, " ")) - command = "sh -c " + command + " 2>&1" - yamlparser.Rollcall() + // app.Flags = []cli.Flag{ + // cli.StringFlag{ + // Name: "lang, l", + // Value: "english", + // Usage: "language for the greeting", + // }, + // } - switch options := os.Args[1]; options { - case "seq": - sshlib.RunSequentially(&yamlparser.Config, &command) - case "groups": - sshlib.RunGroups(&yamlparser.Config, &command) - case "all": - sshlib.RunAllServers(&yamlparser.Config, &command) - default: - fmt.Println("Usage: gossh [option] [command]") - fmt.Println("Options:") - fmt.Println(" seq - Run the command sequentially on all servers in your config file") - fmt.Println(" groups - Run the command on all servers per group concurrently in your config file") - fmt.Println(" all - Run the command on all servers concurrently in your config file") + err := app.Run(os.Args) + if err != nil { + log.Fatal(err) } - } diff --git a/sshlib/sshlib.go b/sshlib/sshlib.go index e9950d2..99c3ed2 100644 --- a/sshlib/sshlib.go +++ b/sshlib/sshlib.go @@ -69,10 +69,10 @@ func executeCommand(servername string, cmd string, password string, connection * _, err = session.Output(cmd) if err != nil { validator = "NOK\n" - loggerlib.ErrorLogger(servername, terminaloutput) + loggerlib.ErrorLogger(servername, "[INFO: Failed] ", terminaloutput) } else { validator = "OK\n" - loggerlib.OutputLogger(servername, terminaloutput) + loggerlib.OutputLogger(servername, "[INFO: Success] ", terminaloutput) } return validator } @@ -104,7 +104,7 @@ func connectAndRun(command *string, servername string, fqdn string, username str ssh.KeyAlgoECDSA521, ssh.KeyAlgoED25519, }, - Timeout: 5 * time.Second, + Timeout: 15 * time.Second, } connection, err := ssh.Dial("tcp", fqdn+":"+port, sshConfig) loggerlib.GeneralError(err) @@ -139,7 +139,7 @@ func connectAndRunSeq(command *string, servername string, fqdn string, username ssh.KeyAlgoECDSA521, ssh.KeyAlgoED25519, }, - Timeout: 5 * time.Second, + Timeout: 15 * time.Second, } connection, err := ssh.Dial("tcp", fqdn+":"+port, sshConfig) loggerlib.GeneralError(err) diff --git a/yamlparser/yamlparser.go b/yamlparser/yamlparser.go index bb14e5d..d30af34 100644 --- a/yamlparser/yamlparser.go +++ b/yamlparser/yamlparser.go @@ -48,7 +48,7 @@ func Rollcall() { fmt.Printf("\n") fmt.Printf("Total groups of servers: %d\n", Grouptotal) fmt.Printf("Total number of logical cores: %v\n", runtime.NumCPU()) - fmt.Printf("=========================================\n") + fmt.Printf("======================================================\n") } // ParseServersList server list parser, parses it to a map of structs in main function