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
=== added file 'utils/ssh/run.go'
--- utils/ssh/run.go 1970-01-01 00:00:00 +0000
+++ utils/ssh/run.go 2014-01-10 01:56:08 +0000
@@ -0,0 +1,80 @@
1// Copyright 2013 Canonical Ltd.
2// Licensed under the AGPLv3, see LICENCE file for details.
3
4package ssh
5
6import (
7 "bytes"
8 "fmt"
9 "os/exec"
10 "strings"
11 "syscall"
12 "time"
13
14 "launchpad.net/juju-core/cmd"
15)
16
17// ExecParams are used for the parameters for ExecuteCommandOnMachine.
18type ExecParams struct {
19 IdentityFile string
20 Host string
21 Command string
22 Timeout time.Duration
23}
24
25// ExecuteCommandOnMachine will execute the command passed through on
26// the host specified. This is done using ssh, and passing the commands
27// through /bin/bash. If the command is not finished within the timeout
28// specified, an error is returned. Any output captured during that time
29// is also returned in the remote response.
30func ExecuteCommandOnMachine(params ExecParams) (result cmd.RemoteResponse, err error) {
31 // execute bash accepting commands on stdin
32 if params.Host == "" {
33 return result, fmt.Errorf("missing host address")
34 }
35 logger.Debugf("execute on %s", params.Host)
36 options := []Option{NoPasswordAuthentication}
37 if params.IdentityFile != "" {
38 options = append(options, Option{"-i", params.IdentityFile})
39 }
40 command := Command(params.Host, []string{"/bin/bash", "-s"}, options...)
41 // start a go routine to do the actual execution
42 var stdout, stderr bytes.Buffer
43 command.Stdout = &stdout
44 command.Stderr = &stderr
45 command.Stdin = strings.NewReader(params.Command + "\n")
46
47 if err = command.Start(); err != nil {
48 return result, err
49 }
50 commandDone := make(chan error)
51 go func() {
52 defer close(commandDone)
53 err := command.Wait()
54 logger.Debugf("command.Wait finished: %v", err)
55 commandDone <- err
56 }()
57
58 select {
59 case err = <-commandDone:
60 logger.Debugf("select from commandDone channel: %v", err)
61 // command finished and returned us the results
62 if ee, ok := err.(*exec.ExitError); ok && err != nil {
63 status := ee.ProcessState.Sys().(syscall.WaitStatus)
64 if status.Exited() {
65 // A non-zero return code isn't considered an error here.
66 result.Code = status.ExitStatus()
67 err = nil
68 }
69 }
70
71 case <-time.After(params.Timeout):
72 logger.Infof("killing the command due to timeout")
73 err = fmt.Errorf("command timed out")
74 command.Process.Kill()
75 }
76 // In either case, gather as much as we have from stdout and stderr
77 result.Stderr = stderr.Bytes()
78 result.Stdout = stdout.Bytes()
79 return result, err
80}
081
=== added file 'utils/ssh/run_test.go'
--- utils/ssh/run_test.go 1970-01-01 00:00:00 +0000
+++ utils/ssh/run_test.go 2014-01-10 01:56:08 +0000
@@ -0,0 +1,116 @@
1// Copyright 2013 Canonical Ltd.
2// Licensed under the AGPLv3, see LICENCE file for details.
3
4package ssh_test
5
6import (
7 "io/ioutil"
8 "os"
9 "path/filepath"
10
11 gc "launchpad.net/gocheck"
12
13 "launchpad.net/juju-core/testing"
14 jc "launchpad.net/juju-core/testing/checkers"
15 "launchpad.net/juju-core/testing/testbase"
16 "launchpad.net/juju-core/utils/ssh"
17)
18
19type ExecuteSSHCommandSuite struct {
20 testbase.LoggingSuite
21 testbin string
22 fakessh string
23}
24
25var _ = gc.Suite(&ExecuteSSHCommandSuite{})
26
27func (s *ExecuteSSHCommandSuite) SetUpTest(c *gc.C) {
28 s.LoggingSuite.SetUpTest(c)
29 s.testbin = c.MkDir()
30 s.fakessh = filepath.Join(s.testbin, "ssh")
31 newPath := s.testbin + ":" + os.Getenv("PATH")
32 s.PatchEnvironment("PATH", newPath)
33}
34
35func (s *ExecuteSSHCommandSuite) fakeSSH(c *gc.C, cmd string) {
36 err := ioutil.WriteFile(s.fakessh, []byte(cmd), 0755)
37 c.Assert(err, gc.IsNil)
38}
39
40func (s *ExecuteSSHCommandSuite) TestCaptureOutput(c *gc.C) {
41 s.fakeSSH(c, echoSSH)
42
43 response, err := ssh.ExecuteCommandOnMachine(ssh.ExecParams{
44 Host: "hostname",
45 Command: "sudo apt-get update\nsudo apt-get upgrade",
46 Timeout: testing.ShortWait,
47 })
48
49 c.Assert(err, gc.IsNil)
50 c.Assert(response.Code, gc.Equals, 0)
51 c.Assert(string(response.Stdout), gc.Equals, "sudo apt-get update\nsudo apt-get upgrade\n")
52 c.Assert(string(response.Stderr), gc.Equals,
53 "-o StrictHostKeyChecking no -o PasswordAuthentication no hostname -- /bin/bash -s\n")
54}
55
56func (s *ExecuteSSHCommandSuite) TestIdentityFile(c *gc.C) {
57 s.fakeSSH(c, echoSSH)
58
59 response, err := ssh.ExecuteCommandOnMachine(ssh.ExecParams{
60 IdentityFile: "identity-file",
61 Host: "hostname",
62 Timeout: testing.ShortWait,
63 })
64
65 c.Assert(err, gc.IsNil)
66 c.Assert(string(response.Stderr), jc.Contains, " -i identity-file ")
67}
68
69func (s *ExecuteSSHCommandSuite) TestTimoutCaptureOutput(c *gc.C) {
70 s.fakeSSH(c, slowSSH)
71
72 response, err := ssh.ExecuteCommandOnMachine(ssh.ExecParams{
73 IdentityFile: "identity-file",
74 Host: "hostname",
75 Command: "ignored",
76 Timeout: testing.ShortWait,
77 })
78
79 c.Check(err, gc.ErrorMatches, "command timed out")
80 c.Assert(response.Code, gc.Equals, 0)
81 c.Assert(string(response.Stdout), gc.Equals, "stdout\n")
82 c.Assert(string(response.Stderr), gc.Equals, "stderr\n")
83}
84
85func (s *ExecuteSSHCommandSuite) TestCapturesReturnCode(c *gc.C) {
86 s.fakeSSH(c, passthroughSSH)
87
88 response, err := ssh.ExecuteCommandOnMachine(ssh.ExecParams{
89 IdentityFile: "identity-file",
90 Host: "hostname",
91 Command: "echo stdout; exit 42",
92 Timeout: testing.ShortWait,
93 })
94
95 c.Check(err, gc.IsNil)
96 c.Assert(response.Code, gc.Equals, 42)
97 c.Assert(string(response.Stdout), gc.Equals, "stdout\n")
98 c.Assert(string(response.Stderr), gc.Equals, "")
99}
100
101// echoSSH outputs the command args to stderr, and copies stdin to stdout
102var echoSSH = `#!/bin/bash
103# Write the args to stderr
104echo "$*" >&2
105cat /dev/stdin
106`
107
108// slowSSH sleeps for a while after outputting some text to stdout and stderr
109var slowSSH = `#!/bin/bash
110echo "stderr" >&2
111echo "stdout"
112sleep 5s
113`
114
115// passthroughSSH creates an ssh that executes stdin.
116var passthroughSSH = `#!/bin/bash -s`

Subscribers

People subscribed via source and target branches

to status/vote changes: