Skip to content

Commit

Permalink
Minimum viable product
Browse files Browse the repository at this point in the history
Is able to take 2 flags:
 -command       'some command string' (mandatory)
 -identity-file <id_rsa file>

Still to do:
 - read hosts from a file
 - flag to select which subset of hosts to run command on

Signed-off-by: Blaine Gardner <[email protected]>
  • Loading branch information
BlaineEXE committed Sep 16, 2018
1 parent 47fd3e8 commit 2fb0bd3
Show file tree
Hide file tree
Showing 4 changed files with 219 additions and 0 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
vendor/
25 changes: 25 additions & 0 deletions Gopkg.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

30 changes: 30 additions & 0 deletions Gopkg.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# Gopkg.toml example
#
# Refer to https://golang.github.io/dep/docs/Gopkg.toml.html
# for detailed Gopkg.toml documentation.
#
# required = ["github.com/user/thing/cmd/thing"]
# ignored = ["github.com/user/project/pkgX", "bitbucket.org/user/project/pkgA/pkgY"]
#
# [[constraint]]
# name = "github.com/user/project"
# version = "1.0.0"
#
# [[constraint]]
# name = "github.com/user/project2"
# branch = "dev"
# source = "github.com/myfork/project2"
#
# [[override]]
# name = "github.com/x/y"
# version = "2.4.0"
#
# [prune]
# non-go = false
# go-tests = true
# unused-packages = true


[prune]
go-tests = true
unused-packages = true
163 changes: 163 additions & 0 deletions cmd/octopus/octopus.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
// Package octopus is a commandline tool for running the same command on multiple remote hosts in
// parallel.
package main

import (
"bytes"
"flag"
"fmt"
"io/ioutil"
"log"
"os"
"strings"

"golang.org/x/crypto/ssh"
)

const (
noHostNameText = "! could not get hostname !"
)

// Each of octopus's tentacles is a remote connection to a host executing the command
type tentacle struct {
host string
hostname string
stdout *bytes.Buffer
err error
}

func main() {
identityFile := flag.String("identity-file", "~/.ssh/id_rsa",
"identity file used to authenticate to remote hosts")
command := flag.String("command", "", "(required) command to execute on remote hosts")
flag.Parse()

if strings.Trim(*command, " \t") == "" {
fmt.Printf("ERROR! '-command' must be specified\n\n")
flag.PrintDefaults()
os.Exit(1)
}

key, err := ioutil.ReadFile(*identityFile)
if err != nil {
log.Fatalf("unable to read private key: %v", err)
}

signer, err := ssh.ParsePrivateKey(key)
if err != nil {
log.Fatalf("unable to parse private key: %v", err)
}

config := &ssh.ClientConfig{
User: "root",
Auth: []ssh.AuthMethod{
ssh.PublicKeys(signer),
},
HostKeyCallback: ssh.InsecureIgnoreHostKey(),
}

hosts := []string{"10.86.1.87", "10.86.1.103"}
tentacles := make(chan tentacle, len(hosts))

for i := 0; i < len(hosts); i++ {
go runCommand(hosts[i], *command, config, tentacles)
}

numErrors := 0

for range hosts {
t := <-tentacles
err := t.print()
if err != nil {
numErrors++
}
}

os.Exit(numErrors)
}

func runCommand(host, command string, config *ssh.ClientConfig, out chan<- tentacle) {
t := tentacle{
host: host,
hostname: "",
err: fmt.Errorf("run command failed"),
}
defer func() { out <- t }()

client, err := ssh.Dial("tcp", fmt.Sprintf("%s:22", t.host), config)
if err != nil {
t.err = fmt.Errorf("%v: %v", t.err, err)
return
// log.Fatal("Failed to dial: ", err)
}

hn := make(chan tentacle)
go getHostname(client, hn)

session, err := client.NewSession()
if err != nil {
t.err = fmt.Errorf("%v: %v", t.err, err)
// log.Fatal("Failed to create session: ", err)
}
defer session.Close()

t.stdout = new(bytes.Buffer)
stderr := new(bytes.Buffer)
session.Stdout = t.stdout
session.Stderr = stderr
err = session.Run(command)

if err != nil {
t.err = fmt.Errorf("%v: %v\n\n%s\n\n%s", t.err, err, strings.TrimRight(stderr.String(), "\n"), "")
} else {
t.err = nil
}

tn := <-hn
t.hostname = tn.hostname
return
}

func getHostname(client *ssh.Client, out chan<- tentacle) {
defer close(out)

t := tentacle{
hostname: noHostNameText,
}
defer func() { out <- t }()

session, err := client.NewSession()
if err != nil {
t.err = fmt.Errorf("%v: %v", t.err, err)
return
}
defer session.Close()

t.stdout = new(bytes.Buffer)
stderr := new(bytes.Buffer)
session.Stdout = t.stdout
session.Stderr = stderr
err = session.Run("hostname")
if err == nil {
t.hostname = strings.TrimRight(t.stdout.String(), "\n")
}

return
}

func (t *tentacle) print() error {
fmt.Println("-----")
fmt.Println(t.hostname)
if t.hostname == noHostNameText {
fmt.Println(t.host)
}
fmt.Printf("-----\n\n")
o := strings.TrimRight(t.stdout.String(), "\n")
if o != "" {
fmt.Printf("%s\n\n", o)
}
if t.err != nil {
fmt.Printf("Error: %v", t.err)
}
return t.err
}

0 comments on commit 2fb0bd3

Please sign in to comment.