Merge lp:~thumper/juju-core/ssh-run-on-remote into lp:~go-bot/juju-core/trunk

Proposed by Tim Penhey
Status: Merged
Approved by: Tim Penhey
Approved revision: no longer in the source branch.
Merged at revision: 2189
Proposed branch: lp:~thumper/juju-core/ssh-run-on-remote
Merge into: lp:~go-bot/juju-core/trunk
Diff against target: 205 lines (+196/-0)
2 files modified
utils/ssh/run.go (+80/-0)
utils/ssh/run_test.go (+116/-0)
To merge this branch: bzr merge lp:~thumper/juju-core/ssh-run-on-remote
Reviewer Review Type Date Requested Status
Juju Engineering Pending
Review via email: mp+201095@code.launchpad.net

Commit message

Add ssh utility function.

Adds a function that wraps ssh to execute a list of
commands on a remote machine.

The function takes a timeout parameter that kills the
connection if the command takes too long.

https://codereview.appspot.com/49940045/

Description of the change

Add ssh utility function.

Adds a function that wraps ssh to execute a list of
commands on a remote machine.

The function takes a timeout parameter that kills the
connection if the command takes too long.

https://codereview.appspot.com/49940045/

To post a comment you must log in.
Revision history for this message
Tim Penhey (thumper) wrote :

Reviewers: mp+201095_code.launchpad.net,

Message:
Please take a look.

Description:
Add ssh utility function.

Adds a function that wraps ssh to execute a list of
commands on a remote machine.

The function takes a timeout parameter that kills the
connection if the command takes too long.

https://code.launchpad.net/~thumper/juju-core/ssh-run-on-remote/+merge/201095

(do not edit description out of merge proposal)

Please review this at https://codereview.appspot.com/49940045/

Affected files (+204, -0 lines):
   A [revision details]
   A utils/ssh/run.go
   A utils/ssh/run_test.go

Revision history for this message
Andrew Wilkins (axwalk) wrote :

https://codereview.appspot.com/49940045/diff/1/utils/ssh/run.go
File utils/ssh/run.go (right):

https://codereview.appspot.com/49940045/diff/1/utils/ssh/run.go#newcode44
utils/ssh/run.go:44: input, err := command.StdinPipe()
command.Stdin = strings.NewReader(params.Command+"\n")

https://codereview.appspot.com/49940045/diff/1/utils/ssh/run.go#newcode51
utils/ssh/run.go:51: var commandDone = make(chan error)
Style nit, I think := is appropriate here.
Also, the channel doesn't need to be created until after Start returns
successfully.

https://codereview.appspot.com/49940045/diff/1/utils/ssh/run.go#newcode59
utils/ssh/run.go:59: close(commandDone)
Maybe a bit paranoid, but I think it's good defensive programming to
defer the close at the top.

https://codereview.appspot.com/49940045/diff/1/utils/ssh/run.go#newcode67
utils/ssh/run.go:67: status :=
ee.ProcessState.Sys().(syscall.WaitStatus)
This is not going to work on the Windows CLI, is it?

You can use ee.ProcessState.Exited(), but I don't think there's a
platform-independent way of getting the exit status. A helper may be in
order.

https://codereview.appspot.com/49940045/diff/1/utils/ssh/run_test.go
File utils/ssh/run_test.go (right):

https://codereview.appspot.com/49940045/diff/1/utils/ssh/run_test.go#newcode105
utils/ssh/run_test.go:105: while read line
cat /dev/stdin?

https://codereview.appspot.com/49940045/

Revision history for this message
Tim Penhey (thumper) wrote :

Please take a look.

https://codereview.appspot.com/49940045/diff/1/utils/ssh/run.go
File utils/ssh/run.go (right):

https://codereview.appspot.com/49940045/diff/1/utils/ssh/run.go#newcode44
utils/ssh/run.go:44: input, err := command.StdinPipe()
On 2014/01/10 01:23:58, axw wrote:
> command.Stdin = strings.NewReader(params.Command+"\n")

Done.

https://codereview.appspot.com/49940045/diff/1/utils/ssh/run.go#newcode51
utils/ssh/run.go:51: var commandDone = make(chan error)
On 2014/01/10 01:23:58, axw wrote:
> Style nit, I think := is appropriate here.
> Also, the channel doesn't need to be created until after Start returns
> successfully.

OK.

https://codereview.appspot.com/49940045/diff/1/utils/ssh/run.go#newcode59
utils/ssh/run.go:59: close(commandDone)
On 2014/01/10 01:23:58, axw wrote:
> Maybe a bit paranoid, but I think it's good defensive programming to
defer the
> close at the top.

sure, although this is paranoia as there is no other return options.

https://codereview.appspot.com/49940045/diff/1/utils/ssh/run.go#newcode67
utils/ssh/run.go:67: status :=
ee.ProcessState.Sys().(syscall.WaitStatus)
On 2014/01/10 01:23:58, axw wrote:
> This is not going to work on the Windows CLI, is it?

> You can use ee.ProcessState.Exited(), but I don't think there's a
> platform-independent way of getting the exit status. A helper may be
in order.

Perhaps, but the CLI isn't doing this, the apiserver is.

https://codereview.appspot.com/49940045/

Revision history for this message
Andrew Wilkins (axwalk) wrote :

LGTM

https://codereview.appspot.com/49940045/diff/1/utils/ssh/run.go
File utils/ssh/run.go (right):

https://codereview.appspot.com/49940045/diff/1/utils/ssh/run.go#newcode67
utils/ssh/run.go:67: status :=
ee.ProcessState.Sys().(syscall.WaitStatus)
On 2014/01/10 01:57:15, thumper wrote:
> On 2014/01/10 01:23:58, axw wrote:
> > This is not going to work on the Windows CLI, is it?
> >
> > You can use ee.ProcessState.Exited(), but I don't think there's a
> > platform-independent way of getting the exit status. A helper may be
in order.

> Perhaps, but the CLI isn't doing this, the apiserver is.

Ah yes, I misunderstood the point of this. Never mind then.

https://codereview.appspot.com/49940045/

Revision history for this message
Go Bot (go-bot) wrote :
Download full text (19.7 KiB)

The attempt to merge lp:~thumper/juju-core/ssh-run-on-remote into lp:juju-core failed. Below is the output from the failed tests.

ok launchpad.net/juju-core/agent 1.553s
ok launchpad.net/juju-core/agent/tools 0.236s
ok launchpad.net/juju-core/bzr 7.916s
ok launchpad.net/juju-core/cert 3.278s
ok launchpad.net/juju-core/charm 0.606s
? launchpad.net/juju-core/charm/hooks [no test files]
ok launchpad.net/juju-core/cloudinit 0.067s
ok launchpad.net/juju-core/cloudinit/sshinit 1.109s
ok launchpad.net/juju-core/cmd 0.250s
ok launchpad.net/juju-core/cmd/charm-admin 0.818s
? launchpad.net/juju-core/cmd/charmd [no test files]
? launchpad.net/juju-core/cmd/charmload [no test files]
ok launchpad.net/juju-core/cmd/juju 181.973s
ok launchpad.net/juju-core/cmd/jujud 55.014s
ok launchpad.net/juju-core/cmd/plugins/juju-metadata 3.113s
? launchpad.net/juju-core/cmd/plugins/juju-restore [no test files]
ok launchpad.net/juju-core/constraints 0.028s
ok launchpad.net/juju-core/container 0.035s
ok launchpad.net/juju-core/container/factory 0.068s
ok launchpad.net/juju-core/container/kvm 0.304s
ok launchpad.net/juju-core/container/kvm/mock 0.037s
? launchpad.net/juju-core/container/kvm/testing [no test files]
ok launchpad.net/juju-core/container/lxc 0.421s
? launchpad.net/juju-core/container/lxc/mock [no test files]
? launchpad.net/juju-core/container/lxc/testing [no test files]
? launchpad.net/juju-core/container/testing [no test files]
ok launchpad.net/juju-core/downloader 5.266s
ok launchpad.net/juju-core/environs 3.257s
ok launchpad.net/juju-core/environs/bootstrap 4.600s
ok launchpad.net/juju-core/environs/cloudinit 0.827s
ok launchpad.net/juju-core/environs/config 1.118s
ok launchpad.net/juju-core/environs/configstore 0.043s
ok launchpad.net/juju-core/environs/filestorage 0.043s
ok launchpad.net/juju-core/environs/httpstorage 0.949s
ok launchpad.net/juju-core/environs/imagemetadata 0.499s
? launchpad.net/juju-core/environs/imagemetadata/testing [no test files]
ok launchpad.net/juju-core/environs/instances 0.059s
ok launchpad.net/juju-core/environs/jujutest 0.242s
ok launchpad.net/juju-core/environs/manual 11.156s
ok launchpad.net/juju-core/environs/simplestreams 0.366s
? launchpad.net/juju-core/environs/simplestreams/testing [no test files]
ok launchpad.net/juju-core/environs/sshstorage 1.332s
ok launchpad.net/juju-core/environs/storage 1.196s
ok launchpad.net/juju-core/environs/sync 30.786s
ok launchpad.net/juju-core/environs/testing 0.216s
ok launchpad.net/juju-core/environs/tools 6.757s
? launchpad.net/juju-core/environs/tools/testing [no test files]
ok launchpad.net/juju-core/errors 0.016s
ok launchpad.net/juju-core/instance 0.023s
? launchpad.net/juju-core/instance/testing [no test files]
ok launchpad.net/juju-core/juju 16.437s
ok launchpad.net/juju-core/juju/osenv 0.019s
? launchpad.net/juju-core/juju/testing [no test files]
ok launchpad.net/juju-core/log 0.016s
ok launchpad.net/juju-core/log/syslog 0.025s
ok launchpad.net/juju-core/names 0.029s
? launchpad.net/juju-core/provider [no test files]
? launchpad.net/juju-core/prov...

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== added file 'utils/ssh/run.go'
2--- utils/ssh/run.go 1970-01-01 00:00:00 +0000
3+++ utils/ssh/run.go 2014-01-10 01:56:08 +0000
4@@ -0,0 +1,80 @@
5+// Copyright 2013 Canonical Ltd.
6+// Licensed under the AGPLv3, see LICENCE file for details.
7+
8+package ssh
9+
10+import (
11+ "bytes"
12+ "fmt"
13+ "os/exec"
14+ "strings"
15+ "syscall"
16+ "time"
17+
18+ "launchpad.net/juju-core/cmd"
19+)
20+
21+// ExecParams are used for the parameters for ExecuteCommandOnMachine.
22+type ExecParams struct {
23+ IdentityFile string
24+ Host string
25+ Command string
26+ Timeout time.Duration
27+}
28+
29+// ExecuteCommandOnMachine will execute the command passed through on
30+// the host specified. This is done using ssh, and passing the commands
31+// through /bin/bash. If the command is not finished within the timeout
32+// specified, an error is returned. Any output captured during that time
33+// is also returned in the remote response.
34+func ExecuteCommandOnMachine(params ExecParams) (result cmd.RemoteResponse, err error) {
35+ // execute bash accepting commands on stdin
36+ if params.Host == "" {
37+ return result, fmt.Errorf("missing host address")
38+ }
39+ logger.Debugf("execute on %s", params.Host)
40+ options := []Option{NoPasswordAuthentication}
41+ if params.IdentityFile != "" {
42+ options = append(options, Option{"-i", params.IdentityFile})
43+ }
44+ command := Command(params.Host, []string{"/bin/bash", "-s"}, options...)
45+ // start a go routine to do the actual execution
46+ var stdout, stderr bytes.Buffer
47+ command.Stdout = &stdout
48+ command.Stderr = &stderr
49+ command.Stdin = strings.NewReader(params.Command + "\n")
50+
51+ if err = command.Start(); err != nil {
52+ return result, err
53+ }
54+ commandDone := make(chan error)
55+ go func() {
56+ defer close(commandDone)
57+ err := command.Wait()
58+ logger.Debugf("command.Wait finished: %v", err)
59+ commandDone <- err
60+ }()
61+
62+ select {
63+ case err = <-commandDone:
64+ logger.Debugf("select from commandDone channel: %v", err)
65+ // command finished and returned us the results
66+ if ee, ok := err.(*exec.ExitError); ok && err != nil {
67+ status := ee.ProcessState.Sys().(syscall.WaitStatus)
68+ if status.Exited() {
69+ // A non-zero return code isn't considered an error here.
70+ result.Code = status.ExitStatus()
71+ err = nil
72+ }
73+ }
74+
75+ case <-time.After(params.Timeout):
76+ logger.Infof("killing the command due to timeout")
77+ err = fmt.Errorf("command timed out")
78+ command.Process.Kill()
79+ }
80+ // In either case, gather as much as we have from stdout and stderr
81+ result.Stderr = stderr.Bytes()
82+ result.Stdout = stdout.Bytes()
83+ return result, err
84+}
85
86=== added file 'utils/ssh/run_test.go'
87--- utils/ssh/run_test.go 1970-01-01 00:00:00 +0000
88+++ utils/ssh/run_test.go 2014-01-10 01:56:08 +0000
89@@ -0,0 +1,116 @@
90+// Copyright 2013 Canonical Ltd.
91+// Licensed under the AGPLv3, see LICENCE file for details.
92+
93+package ssh_test
94+
95+import (
96+ "io/ioutil"
97+ "os"
98+ "path/filepath"
99+
100+ gc "launchpad.net/gocheck"
101+
102+ "launchpad.net/juju-core/testing"
103+ jc "launchpad.net/juju-core/testing/checkers"
104+ "launchpad.net/juju-core/testing/testbase"
105+ "launchpad.net/juju-core/utils/ssh"
106+)
107+
108+type ExecuteSSHCommandSuite struct {
109+ testbase.LoggingSuite
110+ testbin string
111+ fakessh string
112+}
113+
114+var _ = gc.Suite(&ExecuteSSHCommandSuite{})
115+
116+func (s *ExecuteSSHCommandSuite) SetUpTest(c *gc.C) {
117+ s.LoggingSuite.SetUpTest(c)
118+ s.testbin = c.MkDir()
119+ s.fakessh = filepath.Join(s.testbin, "ssh")
120+ newPath := s.testbin + ":" + os.Getenv("PATH")
121+ s.PatchEnvironment("PATH", newPath)
122+}
123+
124+func (s *ExecuteSSHCommandSuite) fakeSSH(c *gc.C, cmd string) {
125+ err := ioutil.WriteFile(s.fakessh, []byte(cmd), 0755)
126+ c.Assert(err, gc.IsNil)
127+}
128+
129+func (s *ExecuteSSHCommandSuite) TestCaptureOutput(c *gc.C) {
130+ s.fakeSSH(c, echoSSH)
131+
132+ response, err := ssh.ExecuteCommandOnMachine(ssh.ExecParams{
133+ Host: "hostname",
134+ Command: "sudo apt-get update\nsudo apt-get upgrade",
135+ Timeout: testing.ShortWait,
136+ })
137+
138+ c.Assert(err, gc.IsNil)
139+ c.Assert(response.Code, gc.Equals, 0)
140+ c.Assert(string(response.Stdout), gc.Equals, "sudo apt-get update\nsudo apt-get upgrade\n")
141+ c.Assert(string(response.Stderr), gc.Equals,
142+ "-o StrictHostKeyChecking no -o PasswordAuthentication no hostname -- /bin/bash -s\n")
143+}
144+
145+func (s *ExecuteSSHCommandSuite) TestIdentityFile(c *gc.C) {
146+ s.fakeSSH(c, echoSSH)
147+
148+ response, err := ssh.ExecuteCommandOnMachine(ssh.ExecParams{
149+ IdentityFile: "identity-file",
150+ Host: "hostname",
151+ Timeout: testing.ShortWait,
152+ })
153+
154+ c.Assert(err, gc.IsNil)
155+ c.Assert(string(response.Stderr), jc.Contains, " -i identity-file ")
156+}
157+
158+func (s *ExecuteSSHCommandSuite) TestTimoutCaptureOutput(c *gc.C) {
159+ s.fakeSSH(c, slowSSH)
160+
161+ response, err := ssh.ExecuteCommandOnMachine(ssh.ExecParams{
162+ IdentityFile: "identity-file",
163+ Host: "hostname",
164+ Command: "ignored",
165+ Timeout: testing.ShortWait,
166+ })
167+
168+ c.Check(err, gc.ErrorMatches, "command timed out")
169+ c.Assert(response.Code, gc.Equals, 0)
170+ c.Assert(string(response.Stdout), gc.Equals, "stdout\n")
171+ c.Assert(string(response.Stderr), gc.Equals, "stderr\n")
172+}
173+
174+func (s *ExecuteSSHCommandSuite) TestCapturesReturnCode(c *gc.C) {
175+ s.fakeSSH(c, passthroughSSH)
176+
177+ response, err := ssh.ExecuteCommandOnMachine(ssh.ExecParams{
178+ IdentityFile: "identity-file",
179+ Host: "hostname",
180+ Command: "echo stdout; exit 42",
181+ Timeout: testing.ShortWait,
182+ })
183+
184+ c.Check(err, gc.IsNil)
185+ c.Assert(response.Code, gc.Equals, 42)
186+ c.Assert(string(response.Stdout), gc.Equals, "stdout\n")
187+ c.Assert(string(response.Stderr), gc.Equals, "")
188+}
189+
190+// echoSSH outputs the command args to stderr, and copies stdin to stdout
191+var echoSSH = `#!/bin/bash
192+# Write the args to stderr
193+echo "$*" >&2
194+cat /dev/stdin
195+`
196+
197+// slowSSH sleeps for a while after outputting some text to stdout and stderr
198+var slowSSH = `#!/bin/bash
199+echo "stderr" >&2
200+echo "stdout"
201+sleep 5s
202+`
203+
204+// passthroughSSH creates an ssh that executes stdin.
205+var passthroughSSH = `#!/bin/bash -s`

Subscribers

People subscribed via source and target branches

to status/vote changes: