├── .github └── dependabot.yml ├── .travis.yml ├── LICENSE ├── README.md ├── gcmd ├── README.md └── gcmd.go ├── go.mod ├── go.sum └── gssh.go /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: gomod 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | open-pull-requests-limit: 10 8 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | --- 2 | language: go 3 | 4 | go: 5 | - 1.x 6 | 7 | env: 8 | - GO111MODULE=on 9 | 10 | install: 11 | - go build 12 | 13 | script: 14 | - go test -v ./... 15 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2014 Square Inc. 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | #### gssh 2 | 3 | simple command line program to run commands on multiple hosts in parallel. 4 | 5 | ##### Usage 6 | 7 | ``` 8 | echo host1 > /tmp/hosts 9 | echo host2 >> /tmp/hosts 10 | gssh -f /tmp/hosts uname -a 11 | 12 | #host1:stdout:Linux host1 2.6.32-431.11.2.el6.x86_64 #1 SMP Tue Mar 25 19:59:55 UTC 2014 x86_64 x86_64 x86_64 GNU/Linux 13 | #host2:stdout:Linux host2 2.6.32-431.11.2.el6.x86_64 #1 SMP Tue Mar 25 19:59:55 UTC 2014 x86_64 x86_64 x86_64 GNU/Linux 14 | ``` 15 | 16 | ``` 17 | gssh -r host1..2 uname -a 18 | ``` 19 | 20 | ###### Overriding ssh options 21 | 22 | Example: 23 | 24 | ``` 25 | gssh -f file -- -o ConnectTimeout=30 -o BatchMode=yes id 26 | ``` 27 | 28 | gssh supports streaming output. Useful for tailing logs across multiple machines etc 29 | 30 | ``` 31 | gssh -f file -- tail -F /var/log/secure | grep -i Accepted 32 | ``` 33 | 34 | ##### Installation 35 | 36 | 1. Install go 37 | 2. `go get -u -v github.com/square/gssh` 38 | 39 | ##### Development 40 | * We use godep for vendoring and dependency management. 41 | * `godep restore # restore to last known good set` 42 | * Please run `gofmt` and `golint` before submitting PRs 43 | -------------------------------------------------------------------------------- /gcmd/README.md: -------------------------------------------------------------------------------- 1 | Usage 2 | ====== 3 | 4 | ```go 5 | import "github.com/square/gcmd" 6 | 7 | nodes = []string{"host1", "host2"} 8 | 9 | // __NODE__ is replaced by each node 10 | g := gcmd.New(nodes, "ssh", ,"__NODE__", "uname") 11 | 12 | // maximum number of commands to run in parallel 13 | g.Maxflight = 10 14 | g.Run() 15 | 16 | // you can override default stdout/stderr/exit 17 | // handlers 18 | g.StderrHandler = func(node string, o string) { 19 | fmt.Printf("%s:stderr:%s\n", node, string(o)) 20 | } 21 | ``` 22 | -------------------------------------------------------------------------------- /gcmd/gcmd.go: -------------------------------------------------------------------------------- 1 | package gcmd 2 | 3 | import ( 4 | "bufio" 5 | "fmt" 6 | "os/exec" 7 | "strings" 8 | "sync" 9 | ) 10 | 11 | type StdoutHandlerFunc func(node string, o string) 12 | type StderrHandlerFunc func(node string, e string) 13 | type ExitHandlerFunc func(node string, exit error) 14 | 15 | type Gcmd struct { 16 | Maxflight int 17 | StdoutHandler StdoutHandlerFunc 18 | StderrHandler StderrHandlerFunc 19 | ExitHandler ExitHandlerFunc 20 | command string 21 | command_args []string 22 | nodes []string 23 | remaining int 24 | } 25 | 26 | func New(nodes []string, command string, command_args ...string) *Gcmd { 27 | g := new(Gcmd) 28 | g.nodes = nodes 29 | g.command = command 30 | g.command_args = command_args 31 | // default handler functions 32 | g.StdoutHandler = func(node string, o string) { 33 | fmt.Printf("%s:stdout:%s\n", node, o) 34 | } 35 | 36 | g.StderrHandler = func(node string, o string) { 37 | fmt.Printf("%s:stderr:%s\n", node, o) 38 | } 39 | 40 | g.ExitHandler = func(node string, exit error) { 41 | if exit != nil { 42 | fmt.Printf("%s:failed:%s\n", node, exit.Error()) 43 | return 44 | } 45 | fmt.Printf("%s:success\n", node) 46 | } 47 | 48 | return g 49 | } 50 | 51 | // Run the command with maxflight number of parallel 52 | // processes and marker __NODE__ replaced with node 53 | // name 54 | func (g *Gcmd) Run() { 55 | 56 | maxflightChan := make(chan string, g.Maxflight) 57 | var wg sync.WaitGroup 58 | 59 | for g.remaining = len(g.nodes); g.remaining > 0; g.remaining-- { 60 | node := g.nodes[len(g.nodes)-g.remaining] 61 | maxflightChan <- node 62 | command_args := g.replaceMarker(node) 63 | 64 | // run each process in a goroutine 65 | wg.Add(1) 66 | go func(node string) { 67 | defer wg.Done() 68 | defer func() { 69 | <-maxflightChan 70 | }() 71 | 72 | cmd := exec.Command(g.command, command_args...) 73 | 74 | // setup stdout pipe 75 | stdout, err := cmd.StdoutPipe() 76 | if err != nil { 77 | g.ExitHandler(node, err) 78 | return 79 | } 80 | 81 | // setup stderr pipe 82 | stderr, err := cmd.StderrPipe() 83 | if err != nil { 84 | g.ExitHandler(node, err) 85 | return 86 | } 87 | 88 | // run the command 89 | if err = cmd.Start(); err != nil { 90 | g.ExitHandler(node, err) 91 | return 92 | } 93 | 94 | // read from stdout/stderr and invoke 95 | // user supplied handlers 96 | stdoutScanner := bufio.NewScanner(stdout) 97 | stderrScanner := bufio.NewScanner(stderr) 98 | 99 | wg.Add(1) 100 | go func() { 101 | defer wg.Done() 102 | for stdoutScanner.Scan() { 103 | g.StdoutHandler(node, stdoutScanner.Text()) 104 | } 105 | }() 106 | 107 | wg.Add(1) 108 | go func() { 109 | defer wg.Done() 110 | for stderrScanner.Scan() { 111 | g.StderrHandler(node, stderrScanner.Text()) 112 | } 113 | }() 114 | 115 | err = cmd.Wait() 116 | g.ExitHandler(node, err) 117 | }(node) 118 | } 119 | wg.Wait() 120 | } 121 | 122 | // unexported methods 123 | 124 | // TODO: make replace marker configurable 125 | func (g *Gcmd) replaceMarker(node string) []string { 126 | var command_args []string 127 | for _, arg := range g.command_args { 128 | command_args = append(command_args, 129 | strings.Replace(arg, "__NODE__", node, -1)) 130 | } 131 | return command_args 132 | } 133 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/square/gssh 2 | 3 | go 1.12 4 | 5 | require ( 6 | github.com/deckarep/golang-set v1.7.1 // indirect 7 | github.com/orcaman/concurrent-map v0.0.0-20190826125027-8c72a8bb44f6 // indirect 8 | github.com/square/erg v1.2.1 9 | github.com/square/grange v0.0.0-20200108221412-2a4cb7b5b334 // indirect 10 | vbom.ml/util v0.0.0-20180919145318-efcd4e0f9787 // indirect 11 | ) 12 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/deckarep/golang-set v0.0.0-20170202203032-fc8930a5e645/go.mod h1:93vsz/8Wt4joVM7c2AVqh+YRMiUSc14yDtF28KmMOgQ= 2 | github.com/deckarep/golang-set v1.7.1 h1:SCQV0S6gTtp6itiFrTqI+pfmJ4LN85S1YzhDf9rTHJQ= 3 | github.com/deckarep/golang-set v1.7.1/go.mod h1:93vsz/8Wt4joVM7c2AVqh+YRMiUSc14yDtF28KmMOgQ= 4 | github.com/orcaman/concurrent-map v0.0.0-20160823150647-8bf1e9bacbf6/go.mod h1:Lu3tH6HLW3feq74c2GC+jIMS/K2CFcDWnWD9XkenwhI= 5 | github.com/orcaman/concurrent-map v0.0.0-20190826125027-8c72a8bb44f6 h1:lNCW6THrCKBiJBpz8kbVGjC7MgdCGKwuvBgc7LoD6sw= 6 | github.com/orcaman/concurrent-map v0.0.0-20190826125027-8c72a8bb44f6/go.mod h1:Lu3tH6HLW3feq74c2GC+jIMS/K2CFcDWnWD9XkenwhI= 7 | github.com/square/erg v1.2.1 h1:UwFbfH62AydbD8aN+9x9EPu7c2+ScHXvQMypgjekHPs= 8 | github.com/square/erg v1.2.1/go.mod h1:VBs5t17gOUoQU5voWjNClyBEilVMdpRausThgaOe8vY= 9 | github.com/square/grange v0.0.0-20200108221412-2a4cb7b5b334 h1:vPQnobL6r0hUKCIZp9ItETiT8ub4U9kFpKFYsb8zO3I= 10 | github.com/square/grange v0.0.0-20200108221412-2a4cb7b5b334/go.mod h1:fdjNIx6dWGr/QZbOxOA0lFJfyB4CNpb3KuJ5uZr2gZE= 11 | github.com/xlab/handysort v0.0.0-20150421192137-fb3537ed64a1 h1:j2hhcujLRHAg872RWAV5yaUrEjHEObwDv3aImCaNLek= 12 | github.com/xlab/handysort v0.0.0-20150421192137-fb3537ed64a1/go.mod h1:QcJo0QPSfTONNIgpN5RA8prR7fF8nkF6cTWTcNerRO8= 13 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 14 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 15 | gopkg.in/yaml.v2 v2.2.7 h1:VUgggvou5XRW9mHwD/yXxIYSMtY0zoKQf/v226p2nyo= 16 | gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 17 | vbom.ml/util v0.0.0-20150502001426-d600ec780753/go.mod h1:so/NYdZXCz+E3ZpW0uAoCj6uzU2+8OWDFv/HxUSs7kI= 18 | vbom.ml/util v0.0.0-20180919145318-efcd4e0f9787 h1:O69FD9pJA4WUZlEwYatBEEkRWKQ5cKodWpdKTrCS/iQ= 19 | vbom.ml/util v0.0.0-20180919145318-efcd4e0f9787/go.mod h1:so/NYdZXCz+E3ZpW0uAoCj6uzU2+8OWDFv/HxUSs7kI= 20 | -------------------------------------------------------------------------------- /gssh.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2014 Square, Inc 2 | 3 | package main 4 | 5 | import ( 6 | "bufio" 7 | "flag" 8 | "fmt" 9 | "log" 10 | "os" 11 | "strconv" 12 | 13 | "github.com/square/erg" 14 | "github.com/square/gssh/gcmd" 15 | ) 16 | 17 | func main() { 18 | // options 19 | var maxflight, timeout int 20 | var file, rangeexp string 21 | var collapse bool 22 | 23 | flag.IntVar(&maxflight, "m", 50, 24 | "maximum number of parallel processes, default - 50") 25 | flag.IntVar(&maxflight, "maxflight", 50, 26 | "maximum number of parallel processes, default - 50") 27 | flag.IntVar(&timeout, "t", 10, "timeout in seconds for initial conn, default - 10s") 28 | flag.IntVar(&timeout, "timeout", 10, 29 | "timeout in seconds for initial conn, default - 10s") 30 | flag.StringVar(&file, "f", "", 31 | "file to read hostnames from default - stdin") 32 | flag.StringVar(&file, "file", "", 33 | "file to read hostnames from default - stdin") 34 | // TODO: should be able to use any grouping system 35 | // TODO: perhaps use a cfg file to determine grouping system 36 | flag.StringVar(&rangeexp, "r", "", 37 | "rangeexp to read nodes from") 38 | flag.BoolVar(&collapse, "c", false, 39 | "collapse similar output - needs -r - be careful about memory usage") 40 | flag.BoolVar(&collapse, "collapse", false, 41 | "collapse similar output - needs -r - be careful about memory usage") 42 | flag.Parse() 43 | 44 | var nodes []string 45 | var scanner *bufio.Scanner 46 | var e *erg.Erg 47 | 48 | // read list of nodes from grouping system 49 | // or file/stdin 50 | if rangeexp != "" || collapse { 51 | host := "range" 52 | port := 80 53 | 54 | if envHost := os.Getenv("RANGE_HOST"); len(envHost) > 0 { 55 | host = envHost 56 | } 57 | 58 | if envPort := os.Getenv("RANGE_PORT"); len(envPort) > 0 { 59 | x, err := strconv.Atoi(envPort) 60 | if err != nil { 61 | log.Fatal("Invalid port in RANGE_PORT: ", envPort) 62 | } 63 | port = x 64 | } 65 | 66 | if envSSL := os.Getenv("RANGE_SSL"); len(envSSL) > 0 { 67 | e = erg.NewWithSsl(host, port) 68 | } else { 69 | e = erg.New(host, port) 70 | } 71 | 72 | result, err := e.Expand(rangeexp) 73 | nodes = result 74 | 75 | if err != nil { 76 | log.Fatal("Unable to expand: ", rangeexp, ":", err.Error()) 77 | } 78 | 79 | } else { 80 | if file == "" { 81 | scanner = bufio.NewScanner(os.Stdin) 82 | } else { 83 | f, err := os.Open(file) 84 | if err != nil { 85 | log.Fatal("open:", file, err.Error()) 86 | } 87 | scanner = bufio.NewScanner(f) 88 | 89 | } 90 | 91 | for scanner.Scan() { 92 | nodes = append(nodes, scanner.Text()) 93 | } 94 | } 95 | 96 | timeout_arg := fmt.Sprintf("ConnectTimeout=%d", timeout) 97 | args := []string{"__NODE__", "-n", "-o", timeout_arg} // marker 98 | args = append(args, flag.Args()...) 99 | g := gcmd.New(nodes, "ssh", args...) 100 | g.Maxflight = maxflight 101 | 102 | // collapse output if asked to 103 | collapseStdout := map[string][]string{} 104 | collapseStderr := map[string][]string{} 105 | collapseExit := map[string][]string{} 106 | 107 | if collapse { 108 | g.StdoutHandler = func(node string, o string) { 109 | _, ok := collapseStdout[o] 110 | if !ok { 111 | collapseStdout[o] = make([]string, 0) 112 | } 113 | collapseStdout[o] = append(collapseStdout[o], node) 114 | } 115 | 116 | g.StderrHandler = func(node string, o string) { 117 | _, ok := collapseStderr[o] 118 | if !ok { 119 | collapseStderr[o] = make([]string, 0) 120 | } 121 | collapseStderr[o] = append(collapseStderr[o], node) 122 | } 123 | g.ExitHandler = func(node string, exit error) { 124 | o := "success" 125 | if exit != nil { 126 | o = exit.Error() 127 | } 128 | _, ok := collapseExit[o] 129 | if !ok { 130 | collapseExit[o] = make([]string, 0) 131 | } 132 | collapseExit[o] = append(collapseExit[o], node) 133 | } 134 | } 135 | 136 | g.Run() 137 | 138 | if collapse { 139 | for o, nodeArr := range collapseStdout { 140 | fmt.Println(e.Compress(nodeArr), "STDOUT", o) 141 | } 142 | for o, nodeArr := range collapseStderr { 143 | fmt.Println(e.Compress(nodeArr), "STDERR", o) 144 | } 145 | for o, nodeArr := range collapseExit { 146 | fmt.Println(e.Compress(nodeArr), "STATUS", o) 147 | } 148 | } 149 | } 150 | --------------------------------------------------------------------------------