├── .dockerignore ├── .gitignore ├── Dockerfile ├── Makefile ├── README.md ├── docker-compose.yml ├── godo.example.yml ├── images └── gif-example.gif ├── main.go └── src ├── cli └── cli.go ├── config └── config.go ├── exec └── exec.go ├── log └── log.go └── ssh └── ssh.go /.dockerignore: -------------------------------------------------------------------------------- 1 | .git -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | config.yml 2 | gin-bin 3 | godo 4 | build 5 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.4 2 | 3 | MAINTAINER Alessandro Nadalin "alessandro.nadalin@gmail.com" 4 | 5 | RUN go get github.com/codegangsta/gin 6 | RUN go get github.com/stretchr/testify/assert 7 | RUN go get golang.org/x/tools/cmd/godoc 8 | RUN go get golang.org/x/crypto/ssh 9 | RUN go get github.com/coreos/fleet/ssh 10 | RUN go get github.com/mgutz/ansi 11 | RUN go get github.com/kvz/logstreamer 12 | RUN go get github.com/codegangsta/cli 13 | RUN go get gopkg.in/yaml.v2 14 | RUN go get github.com/mitchellh/gox 15 | RUN gox -build-toolchain 16 | 17 | COPY . /go/src/github.com/namshi/godo/ 18 | WORKDIR /go/src/github.com/namshi/godo/ 19 | 20 | CMD godo gox --output=build/{{.OS}}_{{.Arch}}/{{.Dir}} -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | init: clean 2 | @echo 'Creating new builds...' 3 | @read -p "Enter version:" version; \ 4 | docker-compose run godo gox --output=build/{{.Dir}}-$$version-{{.OS}}_{{.Arch}} 5 | 6 | clean: 7 | @echo 'Removing old builds...' 8 | docker-compose run godo rm -rf build 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # godo 2 | 3 | Local and remote execution level 9000: **go** and **do** 4 | stuff. 5 | 6 | ![example](https://raw.githubusercontent.com/namshi/godo/master/images/gif-example.gif) 7 | 8 | `godo` is a very simple yet powerful tool that 9 | let's you specify a list of repetitive / useful 10 | commands you run on remote hosts or even locally, 11 | and run them with ease, without having to remember 12 | them or, even worse, login on each server and 13 | execute them manually. 14 | 15 | ## Installation 16 | 17 | Grab the [latest release](https://github.com/namshi/godo/releases) 18 | and simply run the executable: 19 | 20 | ``` 21 | ~/projects/namshi/godo (master ✘)✭ ᐅ ./godo 22 | NAME: 23 | godo - Stop SSHing into your server and run the same old commands. Automate. Automate. Automate. 24 | 25 | USAGE: 26 | godo [global options] command [command options] [arguments...] 27 | 28 | VERSION: 29 | X.Y.Z 30 | 31 | AUTHOR(S): 32 | 33 | COMMANDS: 34 | help, h Shows a list of commands or help for one command 35 | uptime Retrieves uptime info for all machines 36 | slow-queries mysql-log -n 10 37 | nginx-logs sudo tail -10f /var/log/nginx/access.log 38 | 39 | GLOBAL OPTIONS: 40 | --help, -h show help 41 | --version, -v print the version 42 | 43 | ~/projects/namshi/godo (master ✘)✭ ᐅ ./godo uptime 44 | Executing 'uptime' 45 | Command: 'uptime' 46 | Executing on servers web, db 47 | 48 | ... 49 | ... 50 | ``` 51 | 52 | ## Usage 53 | 54 | Create a `godo.yml` file and put it in your home directory: 55 | 56 | ``` yaml 57 | servers: 58 | web: 59 | address: "xxx.xxx.xx.xxx:22" 60 | user: "me" 61 | db: 62 | address: "xxx.xxx.xx.xxx:22" 63 | tunnel: "tunnel.yourcompany.com:22" 64 | user: "me" 65 | groups: 66 | all: [web, db] 67 | commands: 68 | uptime: 69 | target: all 70 | exec: "uptime" 71 | description: "Retrieves uptime info for all machines" 72 | slow-queries: 73 | target: db 74 | exec: "mysql-log -n 10" 75 | nginx-logs: 76 | target: web 77 | exec: "sudo tail -10f /var/log/nginx/access.log" 78 | my-uptime: 79 | target: local 80 | exec: "uptime" 81 | description: "Retrieves uptime info for the current machine" 82 | hostfile: "/home/YOU/.ssh/known_hosts" 83 | timeout: 2 84 | ``` 85 | 86 | There are a few sections to keep in mind: 87 | 88 | * `servers`: this is a dictionary of servers on which you can run commands on 89 | * `groups`: a list of grouped servers (ie. you might want to group by role, AWS zone, etc) 90 | * `commands`: the commands are the actual remote commands you would be executing on the servers, 91 | they have a target (which can be a server or a group), the command that you would execute (`exec`) 92 | and an optional description (which is printed when you do `godo help` or `godo`) 93 | * `hostfile`: you can omit it, it's used not to always ask you to trust SSH host 94 | 95 | Godo will try to read the `godo.yml` configuration file 96 | from 3 different directories: 97 | 98 | * your home 99 | * the current directory 100 | * the directory from which the godo executable runs 101 | 102 | but you can also specify the path to a different 103 | configuration file with the `-c` or `--config` flags: 104 | 105 | ``` 106 | godo -c ./../my-config.yml mysql-log 107 | ``` 108 | 109 | Sometimes, though, you might want to run a command 110 | that you usually execute on some servers on a 111 | different server, and you can do it by simply 112 | specifying it from the command line: 113 | 114 | ``` 115 | godo uptime @ db 116 | 117 | # or 118 | 119 | godo uptime@db 120 | ``` 121 | 122 | Godo provides some special groups: 123 | 124 | * `all`, represents all servers, so you can 125 | always run something like `godo uptime @ all` 126 | * `local`, which references the current machine, 127 | so you will be running the command locally (`godo uptime @ local`) 128 | 129 | ## Additional documentation 130 | 131 | You can run the docs through `godoc -http=:6060 -path=.`. 132 | 133 | ## Gotchas 134 | 135 | Currently all servers need to be in your `known_hosts` file (ie. you 136 | have to have SSHed into them at least once before using them with godo). 137 | 138 | ## Compiling 139 | 140 | Alternatively, you can run and compile godo on 141 | your own machine with a simple `go build -o godo main.go`. 142 | 143 | At the same time, we provide a simple docker container 144 | to run and compile it so that you don't have to 145 | go to crazy if you don't have Go running on your 146 | system: 147 | 148 | ``` 149 | git clone https://github.com/namshi/godo.git 150 | 151 | cd godo 152 | 153 | docker-compose run godo gox --output=build/{{.OS}}_{{.Arch}}/{{.Dir}} 154 | 155 | ./godo 156 | ``` 157 | 158 | The above command will compile the `godo` executables 159 | (for various platforms) in the `build` folder. 160 | 161 | We use a simple makefile to create new releases and 162 | yoou can probably do the same: just run `make` in the 163 | root of the repo and check the `build` folder. This 164 | requires that `docker` and `docker-compose` are installed 165 | on your system. 166 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | godo: 2 | build: . 3 | command: gin main.go 4 | volumes: 5 | - .:/go/src/github.com/namshi/godo/ 6 | -------------------------------------------------------------------------------- /godo.example.yml: -------------------------------------------------------------------------------- 1 | servers: 2 | web: 3 | address: "xxx.xxx.xx.xxx:22" 4 | user: "me" 5 | db: 6 | address: "xxx.xxx.xx.xxx:22" 7 | tunnel: "tunnel.yourcompany.com:22" 8 | user: "me" 9 | groups: 10 | all: [web, db] 11 | commands: 12 | uptime: 13 | target: all 14 | exec: "uptime" 15 | description: "Retrieves uptime info for all machines" 16 | slow-queries: 17 | target: db 18 | exec: "mysql-log -n 10" 19 | nginx-logs: 20 | target: web 21 | exec: "sudo tail -10f /var/log/nginx/access.log" 22 | hostfile: "/home/YOU/.ssh/known_hosts" 23 | timeout: 2 -------------------------------------------------------------------------------- /images/gif-example.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/namshi/godo/0c688bdbaccacabea5621ab1ac43a15bc0416ab8/images/gif-example.gif -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/namshi/godo/src/cli" 5 | ) 6 | 7 | // Start the fun! 8 | func main() { 9 | cli.Run() 10 | } 11 | -------------------------------------------------------------------------------- /src/cli/cli.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "sort" 7 | "strings" 8 | 9 | "github.com/codegangsta/cli" 10 | "github.com/namshi/godo/src/config" 11 | "github.com/namshi/godo/src/exec" 12 | "github.com/namshi/godo/src/log" 13 | ) 14 | 15 | const defaultConfigFile = "./godo.yml" 16 | 17 | // Creates a new cli app. 18 | func newApp() *cli.App { 19 | app := cli.NewApp() 20 | 21 | app.Name = "godo" 22 | app.Usage = "Stop SSHing into your server and run the same old commands. Automate. Automate. Automate." 23 | app.Version = "v1.3.0" 24 | 25 | return app 26 | } 27 | 28 | // Parses the arguments that we got 29 | // from the command line. 30 | // 31 | // Arguments have the format command[@target], 32 | // so something like "uptime" or "uptime @ load-balancer". 33 | func parseArgs(c *cli.Context) (string, string) { 34 | cmds := strings.Split(strings.Replace(strings.Join(c.Args(), ""), " ", "", -1), "@") 35 | 36 | if len(cmds) == 2 { 37 | return cmds[0], cmds[1] 38 | } 39 | 40 | return cmds[0], "" 41 | } 42 | 43 | // Registers all available commands 44 | // on the app. 45 | func addCommands(app *cli.App) { 46 | app.Action = func(c *cli.Context) { 47 | configFile := defaultConfigFile 48 | 49 | if c.String("config") != configFile { 50 | configFile = c.String("config") 51 | } 52 | 53 | cfg := config.Parse(configFile) 54 | cmd, target := parseArgs(c) 55 | 56 | if command, ok := cfg.Commands[cmd]; ok { 57 | log.Info("Executing '%s'", cmd) 58 | 59 | if target == "" { 60 | target = command.Target 61 | } 62 | 63 | runCommand(command, cfg, target) 64 | } else { 65 | printAvailableCommands(app, cfg.Commands, c) 66 | } 67 | } 68 | } 69 | 70 | // Helper function that prints all available 71 | // commands. 72 | // 73 | // @todo we should register commands before this, 74 | // but somehow if I do it the cli app executes a 75 | // random command 76 | func printAvailableCommands(app *cli.App, commands map[string]config.Command, c *cli.Context) { 77 | // golang's map iteration is random but we want commands to be printed in alphabetical order 78 | // @see http://nathanleclaire.com/blog/2014/04/27/a-surprising-feature-of-golang-that-colored-me-impressed/ 79 | var names []string 80 | for name := range commands { 81 | names = append(names, name) 82 | } 83 | sort.Strings(names) 84 | 85 | for _, name := range names { 86 | command := commands[name] 87 | description := command.Exec 88 | 89 | if command.Description != "" { 90 | description = command.Description 91 | } 92 | 93 | cmd := cli.Command{ 94 | Name: name, 95 | Usage: description, 96 | } 97 | 98 | app.Commands = append(app.Commands, cmd) 99 | } 100 | 101 | app.Command("help").Run(c) 102 | } 103 | 104 | // Adds servers to the list of targets 105 | // by checking if the specified 106 | // target was a group: if it was, add 107 | // all servers in that group. 108 | func addTargetFromGroups(targets map[string]config.Server, target string, cfg config.Config) { 109 | if group, ok := cfg.Groups[target]; ok { 110 | for _, server := range group { 111 | targets[server] = cfg.Servers[server] 112 | } 113 | } 114 | } 115 | 116 | // Adds a server to the list of targets if 117 | // the specified target was a single server. 118 | func addTargetFromServer(targets map[string]config.Server, target string, cfg config.Config) { 119 | if _, ok := cfg.Servers[target]; ok { 120 | targets[target] = cfg.Servers[target] 121 | } 122 | } 123 | 124 | // Returns a list of targets on which we 125 | // should execute the given command, based 126 | // on the target specified by the user. 127 | // 128 | // If the user specifies a target, we use that 129 | // one; if there is no user-specified target 130 | // we simply look at the configuration of the 131 | // command. 132 | // 133 | // A target can be a server, group of servers 134 | // or a special alias. 135 | // 136 | // The supported aliases are 137 | // - all: will execute the command on all servers 138 | // - local: instead of executing the command remotely 139 | // it will execute it on the current machine 140 | func getTargets(command config.Command, cfg config.Config, target string) map[string]config.Server { 141 | targets := make(map[string]config.Server) 142 | 143 | for _, target = range strings.Split(target, ",") { 144 | if target == "all" { 145 | targets = cfg.Servers 146 | } else if target == "local" { 147 | targets["local"] = config.Server{} 148 | } else { 149 | addTargetFromGroups(targets, target, cfg) 150 | addTargetFromServer(targets, target, cfg) 151 | } 152 | } 153 | 154 | return targets 155 | } 156 | 157 | // Runs one of the commands stored in the config 158 | // file. 159 | func runCommand(command config.Command, cfg config.Config, target string) { 160 | log.Info("\nCommand: '%s'", command.Exec) 161 | targets := getTargets(command, cfg, target) 162 | targetNames := []string{} 163 | 164 | for serverName, _ := range targets { 165 | targetNames = append(targetNames, serverName) 166 | } 167 | 168 | if len(targets) > 0 { 169 | log.Info("\nExecuting on server '%s'", strings.Join(targetNames, ", ")) 170 | fmt.Println() 171 | fmt.Println() 172 | exec.ExecuteCommands(command.Exec, targets, cfg) 173 | } else { 174 | log.Err("\nNo target server / group with the name '%s' could be found, maybe a typo?", target) 175 | printAvailableTargets(cfg) 176 | } 177 | } 178 | 179 | // Outputs the available targets, 180 | // extracted from the config file. 181 | func printAvailableTargets(cfg config.Config) { 182 | log.Err("\n\nAvailable groups are:") 183 | 184 | for groupName, group := range cfg.Groups { 185 | log.Err("\n * %s (%s)", groupName, strings.Join(group, ", ")) 186 | } 187 | 188 | log.Err("\n\nAvailable servers are:") 189 | 190 | for serverName := range cfg.Servers { 191 | log.Err("\n * %s", serverName) 192 | } 193 | } 194 | 195 | // Adds global flags to the CLI app. 196 | func addFlags(app *cli.App) { 197 | app.Flags = []cli.Flag{ 198 | cli.StringFlag{ 199 | Name: "config, c", 200 | Value: defaultConfigFile, 201 | Usage: "configuration file to be used for running godo", 202 | EnvVar: "GODO_CONFIG", 203 | }, 204 | } 205 | } 206 | 207 | // Runs the cli app! 208 | func Run() { 209 | app := newApp() 210 | addCommands(app) 211 | addFlags(app) 212 | 213 | app.Run(os.Args) 214 | } 215 | -------------------------------------------------------------------------------- /src/config/config.go: -------------------------------------------------------------------------------- 1 | // This package is responsible for parsing 2 | // a configuration file and generate 3 | // Go structures. 4 | package config 5 | 6 | import ( 7 | yaml "gopkg.in/yaml.v2" 8 | "io/ioutil" 9 | "log" 10 | "os" 11 | "os/user" 12 | "path" 13 | "strings" 14 | ) 15 | 16 | // Represents a server on which 17 | // we can connect to. 18 | type Server struct { 19 | // The IP address of the server 20 | Address string 21 | // The user that will login 22 | // into the server 23 | User string 24 | // An optional host that will 25 | // be used as SSH tunnel to the 26 | // current server 27 | Tunnel string 28 | } 29 | 30 | // Represents a command 31 | // to run on a server or 32 | // group of servers. 33 | type Command struct { 34 | // The target on which we are going 35 | // to execute the current command: can 36 | // be a server or a group 37 | Target string 38 | // A string representing the command 39 | // to execute 40 | Exec string 41 | // A description of the command, 42 | // used in the CLI 43 | Description string 44 | } 45 | 46 | // Represents the whole configuration 47 | // file. 48 | type Config struct { 49 | // The file to use to verify 50 | // known hosts 51 | Hostfile string 52 | // Timeout of the SSH connections 53 | Timeout int 54 | // List of servers 55 | Servers map[string]Server 56 | // List of commands 57 | Commands map[string]Command 58 | // List of groups 59 | Groups map[string][]string 60 | // The raw string containing the 61 | // whole configuration, in YAML 62 | // format 63 | Raw string 64 | } 65 | 66 | // Tries to read the contents of the file 67 | // trying to locate it in different directories. 68 | // ie. ~/godo.yml | ./godo.yml 69 | func getFileContents(file string) []byte { 70 | user, _ := user.Current() 71 | wd, _ := os.Getwd() 72 | homePath := path.Join(user.HomeDir, file) 73 | wdPath := path.Join(wd, file) 74 | paths := []string{wdPath, homePath, file} 75 | 76 | for _, path := range paths { 77 | content, err := ioutil.ReadFile(path) 78 | 79 | if err == nil { 80 | return content 81 | } 82 | } 83 | 84 | log.Fatalf("Unable to read config file (tried %s)", strings.Join(paths, ", ")) 85 | return []byte{} 86 | } 87 | 88 | // Validates the parsed configuration, 89 | // providing default values if anything 90 | // is missing. 91 | func validate(c *Config) { 92 | if c.Hostfile == "" { 93 | c.Hostfile = path.Join(os.TempDir(), "known_hosts_godo") 94 | } 95 | 96 | if c.Timeout == 0 { 97 | c.Timeout = 30 98 | } 99 | } 100 | 101 | // Parses a configuration file and returns 102 | // a Go structure that represents it. 103 | func Parse(file string) Config { 104 | content := getFileContents(file) 105 | c := Config{} 106 | 107 | err := yaml.Unmarshal([]byte(content), &c) 108 | 109 | if err != nil { 110 | panic(err) 111 | } 112 | 113 | c.Raw = string(content) 114 | validate(&c) 115 | 116 | return c 117 | } 118 | -------------------------------------------------------------------------------- /src/exec/exec.go: -------------------------------------------------------------------------------- 1 | // This package is used to "phisically" 2 | // execute remote commands and handle their 3 | // output on the current session. 4 | package exec 5 | 6 | import ( 7 | goexec "os/exec" 8 | "strings" 9 | "sync" 10 | "time" 11 | 12 | "github.com/namshi/godo/src/config" 13 | "github.com/namshi/godo/src/log" 14 | "github.com/namshi/godo/src/ssh" 15 | ) 16 | 17 | // Executes the command on the given 18 | // servers. 19 | // 20 | // The main goal of this method is to 21 | // figure out whether this command needs 22 | // to be executed locally or remotely, 23 | // and go ahead with the proper execution 24 | // strategy. 25 | func ExecuteCommands(command string, servers map[string]config.Server, cfg config.Config) { 26 | var wg sync.WaitGroup 27 | wg.Add(len(servers)) 28 | 29 | for server, serverConfig := range servers { 30 | if server == "local" { 31 | go executeLocalCommand(command, wg.Done) 32 | } else { 33 | go executeRemoteCommand(command, server, serverConfig, cfg, wg.Done) 34 | } 35 | } 36 | 37 | wg.Wait() 38 | } 39 | 40 | // Parses a command in format that 41 | // is suitable for exec.Command(). 42 | // 43 | // In practice, ls -la /tmp becomes 44 | // "ls" and ["-la", "/tmp"]. 45 | func parseLocalCommand(command string) (string, []string) { 46 | args := strings.Fields(command) 47 | 48 | return args[0], args[1:len(args)] 49 | } 50 | 51 | // Executes a command locally. 52 | func executeLocalCommand(command string, done func()) { 53 | c, args := parseLocalCommand(command) 54 | cmd := goexec.Command(c, args...) 55 | stdout, stderr := log.GetRemoteLoggers("local") 56 | cmd.Stdout = stdout 57 | cmd.Stderr = stderr 58 | 59 | err := cmd.Start() 60 | 61 | // failed to spawn new process 62 | if err != nil { 63 | log.Err(err.Error()) 64 | } 65 | 66 | // Failed to execute? 67 | err = cmd.Wait() 68 | if err != nil { 69 | log.Err(err.Error()) 70 | } 71 | 72 | defer done() 73 | } 74 | 75 | // Executes the given command through SSH, 76 | // connecting with the given config. 77 | func executeRemoteCommand(command string, server string, serverConfig config.Server, cfg config.Config, done func()) { 78 | c := &ssh.Config{Address: serverConfig.Address, Alias: server, Tunnel: serverConfig.Tunnel, User: serverConfig.User, Hostfile: cfg.Hostfile} 79 | c.Timeout = time.Duration(cfg.Timeout) * time.Second 80 | session, err := ssh.NewSession(c, server) 81 | stdout, stderr := log.GetRemoteLoggers(server) 82 | 83 | if err != nil { 84 | stderr.Write([]byte(err.Error())) 85 | done() 86 | return 87 | } 88 | 89 | session.Stdout = stdout 90 | session.Stderr = stderr 91 | 92 | err = session.Run(command) 93 | 94 | if err != nil { 95 | stderr.Write([]byte(err.Error())) 96 | } 97 | 98 | defer done() 99 | } 100 | -------------------------------------------------------------------------------- /src/log/log.go: -------------------------------------------------------------------------------- 1 | // This package adds some utility 2 | // functions in order to handle 3 | // logging in a fancy way. 4 | package log 5 | 6 | import ( 7 | "fmt" 8 | golog "log" 9 | "os" 10 | 11 | "github.com/kvz/logstreamer" 12 | "github.com/mgutz/ansi" 13 | ) 14 | 15 | // Returns a pair of loggers that will be 16 | // used to log the output of remote commands 17 | // executed via SSH. 18 | // 19 | // The first logger is used for the stdout, the 20 | // second one is for the stderr. 21 | func GetRemoteLoggers(server string) (*logstreamer.Logstreamer, *logstreamer.Logstreamer) { 22 | stdLogger := golog.New(os.Stdout, ansi.Color(" "+server+": ", "green"), 0) 23 | errLogger := golog.New(os.Stdout, ansi.Color(" "+server+": ", "red"), 0) 24 | 25 | logStreamerOut := logstreamer.NewLogstreamer(stdLogger, " ", false) 26 | logStreamerErr := logstreamer.NewLogstreamer(errLogger, " ", false) 27 | 28 | return logStreamerOut, logStreamerErr 29 | } 30 | 31 | // Prints an error message 32 | func Err(message string, args ...string) { 33 | list := []interface{}{} 34 | 35 | for _, val := range args { 36 | list = append(list, val) 37 | } 38 | 39 | fmt.Printf(ansi.Color(message, "red+h"), list...) 40 | } 41 | 42 | // Prints an info message 43 | func Info(message string, args ...string) { 44 | list := []interface{}{} 45 | 46 | for _, val := range args { 47 | val = ansi.Color(val, "blue+h") 48 | list = append(list, val) 49 | } 50 | 51 | fmt.Printf(message, list...) 52 | } 53 | -------------------------------------------------------------------------------- /src/ssh/ssh.go: -------------------------------------------------------------------------------- 1 | // Package used to be able 2 | // to remotely execute commands 3 | // via SSH. 4 | // 5 | // Most of this stuff relies on 6 | // CoreOS's SSH package (https://github.com/coreos/fleet/tree/master/ssh). 7 | package ssh 8 | 9 | import ( 10 | gossh "github.com/coreos/fleet/Godeps/_workspace/src/golang.org/x/crypto/ssh" 11 | "time" 12 | 13 | ssh "github.com/coreos/fleet/ssh" 14 | ) 15 | 16 | // A simple configuration to connect 17 | // to an SSH host. 18 | // If a Tunnel is provided, it will be 19 | // used as bastion host to connect to 20 | // the "real" address. 21 | type Config struct { 22 | Hostfile string 23 | Address string 24 | Tunnel string 25 | User string 26 | Alias string 27 | Timeout time.Duration 28 | } 29 | 30 | // Creates a new SSH session 31 | // and attaches a PTY to it. 32 | func NewSession(config *Config, server string) (*gossh.Session, error) { 33 | client, err := createClient(config, server) 34 | 35 | if err != nil { 36 | return nil, err 37 | } 38 | 39 | session, err := client.NewSession() 40 | 41 | if err != nil { 42 | return nil, err 43 | } 44 | 45 | modes := gossh.TerminalModes{ 46 | gossh.ECHO: 0, // disable echoing 47 | gossh.TTY_OP_ISPEED: 14400, // input speed = 14.4kbaud 48 | gossh.TTY_OP_OSPEED: 14400, // output speed = 14.4kbaud 49 | } 50 | session.RequestPty("xterm", 80, 40, modes) 51 | 52 | return session, nil 53 | } 54 | 55 | // Creates a new SSH client based on the 56 | // given configuration. 57 | func createClient(config *Config, server string) (*ssh.SSHForwardingClient, error) { 58 | hostfile := ssh.NewHostKeyFile(config.Hostfile) 59 | checker := ssh.NewHostKeyChecker(hostfile) 60 | 61 | if config.Tunnel != "" { 62 | return ssh.NewTunnelledSSHClient(config.User, config.Tunnel, config.Address, checker, true, config.Timeout) 63 | } 64 | 65 | return ssh.NewSSHClient(config.User, config.Address, checker, true, config.Timeout) 66 | } 67 | --------------------------------------------------------------------------------