Merge lp:~axwalk/juju-core/ssh-client-interface into lp:~go-bot/juju-core/trunk

Proposed by Andrew Wilkins
Status: Merged
Approved by: Andrew Wilkins
Approved revision: no longer in the source branch.
Merged at revision: 2203
Proposed branch: lp:~axwalk/juju-core/ssh-client-interface
Merge into: lp:~go-bot/juju-core/trunk
Diff against target: 1414 lines (+593/-222)
19 files modified
cloudinit/sshinit/configure.go (+10/-8)
cmd/juju/scp.go (+1/-6)
cmd/juju/scp_test.go (+7/-1)
cmd/juju/ssh.go (+3/-1)
cmd/juju/ssh_test.go (+5/-11)
environs/manual/fakessh.go (+7/-62)
environs/manual/init.go (+9/-4)
environs/manual/init_test.go (+14/-13)
environs/sshstorage/storage.go (+5/-5)
environs/sshstorage/storage_test.go (+25/-18)
environs/testing/bootstrap.go (+2/-1)
provider/common/bootstrap.go (+23/-11)
provider/common/bootstrap_test.go (+8/-7)
provider/null/environ.go (+1/-1)
utils/ssh/run.go (+4/-4)
utils/ssh/ssh.go (+202/-0)
utils/ssh/ssh_openssh.go (+104/-40)
utils/ssh/ssh_test.go (+82/-29)
utils/ssh/testing/fakessh.go (+81/-0)
To merge this branch: bzr merge lp:~axwalk/juju-core/ssh-client-interface
Reviewer Review Type Date Requested Status
Juju Engineering Pending
Review via email: mp+201334@code.launchpad.net

Commit message

Introduce utils/ssh.Client interface

This is a refactoring of utils/ssh to introduce
a Client interface which consists of Command and
Copy methods. The existing Command and ScpCommand
methods have been subsumed by the new OpenSSHClient
implementation.

All of the packages using ssh.Command/ScpCommand
have been updated to use the new client interface.
The options have also be changed to a struct which
is interpreted by the implementation. There is one
change in default options: password authentication
is now disabled by default, and must be explicitly
enabled.

There is a new option, which is identity files to
specify when authenticating. Clients are expected
to give these precedence over their client-specific
defaults (~/.ssh/id_rsa, etc.)

Parts of environs/manual/fake.ssh have been moved
to utils/ssh/testing.

https://codereview.appspot.com/49950044/

Description of the change

Introduce utils/ssh.Client interface

This is a refactoring of utils/ssh to introduce
a Client interface which consists of Command and
Copy methods. The existing Command and ScpCommand
methods have been subsumed by the new OpenSSHClient
implementation.

All of the packages using ssh.Command/ScpCommand
have been updated to use the new client interface.
The options have also be changed to a struct which
is interpreted by the implementation. There is one
change in default options: password authentication
is now disabled by default, and must be explicitly
enabled.

There is a new option, which is identity files to
specify when authenticating. Clients are expected
to give these precedence over their client-specific
defaults (~/.ssh/id_rsa, etc.)

Parts of environs/manual/fake.ssh have been moved
to utils/ssh/testing.

https://codereview.appspot.com/49950044/

To post a comment you must log in.
Revision history for this message
Andrew Wilkins (axwalk) wrote :

Reviewers: mp+201334_code.launchpad.net,

Message:
Please take a look.

Description:
Introduce utils/ssh.Client interface

This is a refactoring of utils/ssh to introduce
a Client interface which consists of Command and
Copy methods. The existing Command and ScpCommand
methods have been subsumed by the new OpenSSHClient
implementation.

All of the packages using ssh.Command/ScpCommand
have been updated to use the new client interface.
The options have also be changed to a struct which
is interpreted by the implementation. There is one
change in default options: password authentication
is now disabled by default, and must be explicitly
enabled.

There is a new option, which is identity files to
specify when authenticating. Clients are expected
to give these precedence over their client-specific
defaults (~/.ssh/id_rsa, etc.)

Parts of environs/manual/fake.ssh have been moved
to utils/ssh/testing.

https://code.launchpad.net/~axwalk/juju-core/ssh-client-interface/+merge/201334

(do not edit description out of merge proposal)

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

Affected files (+590, -221 lines):
   A [revision details]
   M cloudinit/sshinit/configure.go
   M cmd/juju/scp.go
   M cmd/juju/scp_test.go
   M cmd/juju/ssh.go
   M cmd/juju/ssh_test.go
   M environs/manual/fakessh.go
   M environs/manual/init.go
   M environs/manual/init_test.go
   M environs/sshstorage/storage.go
   M environs/sshstorage/storage_test.go
   M environs/testing/bootstrap.go
   M provider/common/bootstrap.go
   M provider/common/bootstrap_test.go
   M provider/null/environ.go
   M utils/ssh/run.go
   A utils/ssh/ssh.go
   M utils/ssh/ssh_openssh.go
   M utils/ssh/ssh_test.go
   A utils/ssh/testing/fakessh.go

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

https://codereview.appspot.com/49950044/diff/20001/cmd/juju/scp.go
File cmd/juju/scp.go (right):

https://codereview.appspot.com/49950044/diff/20001/cmd/juju/scp.go#newcode84
cmd/juju/scp.go:84: options.EnablePTY()
why does scp need this?

https://codereview.appspot.com/49950044/diff/20001/cmd/juju/ssh_test.go
File cmd/juju/ssh_test.go (right):

https://codereview.appspot.com/49950044/diff/20001/cmd/juju/ssh_test.go#newcode44
cmd/juju/ssh_test.go:44: s.PatchEnvironment("PATH",
s.bin+":"+os.Getenv("PATH"))
yay \o/

https://codereview.appspot.com/49950044/diff/20001/utils/ssh/ssh.go
File utils/ssh/ssh.go (right):

https://codereview.appspot.com/49950044/diff/20001/utils/ssh/ssh.go#newcode50
utils/ssh/ssh.go:50: o.identities = append([]string{}, identityFiles...)
why not just do
   o.identites = identityFiles?
it is a slice right?

https://codereview.appspot.com/49950044/diff/20001/utils/ssh/ssh_openssh.go
File utils/ssh/ssh_openssh.go (right):

https://codereview.appspot.com/49950044/diff/20001/utils/ssh/ssh_openssh.go#newcode15
utils/ssh/ssh_openssh.go:15: var opensshCommonOptions = []string{"-o",
"StrictHostKeyChecking no"}
Would love to kill this :-)

https://codereview.appspot.com/49950044/diff/20001/utils/ssh/testing/fakessh.go
File utils/ssh/testing/fakessh.go (right):

https://codereview.appspot.com/49950044/diff/20001/utils/ssh/testing/fakessh.go#newcode68
utils/ssh/testing/fakessh.go:68: case []string:
worth
   c.Check(output, gc.HasLen, 2)
?

https://codereview.appspot.com/49950044/

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

Please take a look.

https://codereview.appspot.com/49950044/diff/20001/cmd/juju/scp.go
File cmd/juju/scp.go (right):

https://codereview.appspot.com/49950044/diff/20001/cmd/juju/scp.go#newcode84
cmd/juju/scp.go:84: options.EnablePTY()
On 2014/01/14 02:20:29, thumper wrote:
> why does scp need this?

It doesn't. I don't know why I did that. Removed.

https://codereview.appspot.com/49950044/diff/20001/utils/ssh/ssh.go
File utils/ssh/ssh.go (right):

https://codereview.appspot.com/49950044/diff/20001/utils/ssh/ssh.go#newcode50
utils/ssh/ssh.go:50: o.identities = append([]string{}, identityFiles...)
On 2014/01/14 02:20:29, thumper wrote:
> why not just do
> o.identites = identityFiles?
> it is a slice right?

I want it to take a copy, making the value of o.identities immutable.
Otherwise we have to worry about things synchronisation, for one thing.

https://codereview.appspot.com/49950044/diff/20001/utils/ssh/ssh_openssh.go
File utils/ssh/ssh_openssh.go (right):

https://codereview.appspot.com/49950044/diff/20001/utils/ssh/ssh_openssh.go#newcode15
utils/ssh/ssh_openssh.go:15: var opensshCommonOptions = []string{"-o",
"StrictHostKeyChecking no"}
On 2014/01/14 02:20:29, thumper wrote:
> Would love to kill this :-)

Me too.

https://codereview.appspot.com/49950044/diff/20001/utils/ssh/testing/fakessh.go
File utils/ssh/testing/fakessh.go (right):

https://codereview.appspot.com/49950044/diff/20001/utils/ssh/testing/fakessh.go#newcode68
utils/ssh/testing/fakessh.go:68: case []string:
On 2014/01/14 02:20:29, thumper wrote:
> worth
> c.Check(output, gc.HasLen, 2)
> ?

Done - could help debug broken tests. I also added an assertion on
input's type.

https://codereview.appspot.com/49950044/

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

Please take a look.

https://codereview.appspot.com/49950044/diff/30001/provider/common/bootstrap.go
File provider/common/bootstrap.go (right):

https://codereview.appspot.com/49950044/diff/30001/provider/common/bootstrap.go#newcode317
provider/common/bootstrap.go:317: addr: addr,
On 2014/01/14 08:23:37, axw wrote:
> Need to initialise client field here.

Done.

https://codereview.appspot.com/49950044/

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

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'cloudinit/sshinit/configure.go'
2--- cloudinit/sshinit/configure.go 2014-01-07 13:01:01 +0000
3+++ cloudinit/sshinit/configure.go 2014-01-14 09:50:43 +0000
4@@ -4,7 +4,6 @@
5 package sshinit
6
7 import (
8- "encoding/base64"
9 "fmt"
10 "io"
11 "strings"
12@@ -22,6 +21,10 @@
13 // Host is the host to configure, in the format [user@]hostname.
14 Host string
15
16+ // Client is the SSH client to connect with.
17+ // If Client is nil, ssh.DefaultClient will be used.
18+ Client ssh.Client
19+
20 // Config is the cloudinit config to carry out.
21 Config *cloudinit.Config
22
23@@ -38,13 +41,12 @@
24 return err
25 }
26 logger.Debugf("running script on %s: %s", params.Host, script)
27- scriptBase64 := base64.StdEncoding.EncodeToString([]byte(script))
28- script = fmt.Sprintf(`F=$(mktemp); echo %s | base64 -d > $F; . $F`, scriptBase64)
29- cmd := ssh.Command(
30- params.Host,
31- []string{"sudo", "-n", fmt.Sprintf("bash -c '%s'", script)},
32- ssh.NoPasswordAuthentication,
33- )
34+ client := params.Client
35+ if client == nil {
36+ client = ssh.DefaultClient
37+ }
38+ cmd := ssh.Command(params.Host, []string{"sudo", "/bin/bash"}, nil)
39+ cmd.Stdin = strings.NewReader(script)
40 cmd.Stderr = params.Stderr
41 return cmd.Run()
42 }
43
44=== modified file 'cmd/juju/scp.go'
45--- cmd/juju/scp.go 2013-12-03 06:04:43 +0000
46+++ cmd/juju/scp.go 2014-01-14 09:50:43 +0000
47@@ -79,10 +79,5 @@
48 c.Args[i] = "ubuntu@" + host + ":" + v[1]
49 }
50 }
51-
52- cmd := ssh.ScpCommand(c.Args[0], c.Args[1], ssh.NoPasswordAuthentication)
53- cmd.Stdin = ctx.Stdin
54- cmd.Stdout = ctx.Stdout
55- cmd.Stderr = ctx.Stderr
56- return cmd.Run()
57+ return ssh.Copy(c.Args[0], c.Args[1], nil)
58 }
59
60=== modified file 'cmd/juju/scp_test.go'
61--- cmd/juju/scp_test.go 2013-12-03 14:19:47 +0000
62+++ cmd/juju/scp_test.go 2014-01-14 09:50:43 +0000
63@@ -6,7 +6,9 @@
64 import (
65 "bytes"
66 "fmt"
67+ "io/ioutil"
68 "net/url"
69+ "path/filepath"
70
71 gc "launchpad.net/gocheck"
72
73@@ -69,7 +71,11 @@
74 ctx := coretesting.Context(c)
75 code := cmd.Main(&SCPCommand{}, ctx, t.args)
76 c.Check(code, gc.Equals, 0)
77+ // we suppress stdout from scp
78 c.Check(ctx.Stderr.(*bytes.Buffer).String(), gc.Equals, "")
79- c.Check(ctx.Stdout.(*bytes.Buffer).String(), gc.Equals, t.result)
80+ c.Check(ctx.Stdout.(*bytes.Buffer).String(), gc.Equals, "")
81+ data, err := ioutil.ReadFile(filepath.Join(s.bin, "scp.args"))
82+ c.Assert(err, gc.IsNil)
83+ c.Assert(string(data), gc.Equals, t.result)
84 }
85 }
86
87=== modified file 'cmd/juju/ssh.go'
88--- cmd/juju/ssh.go 2013-12-17 18:21:26 +0000
89+++ cmd/juju/ssh.go 2014-01-14 09:50:43 +0000
90@@ -88,7 +88,9 @@
91 // it from the CLI for backwards compatibility.
92 args = args[1:]
93 }
94- cmd := ssh.Command("ubuntu@"+host, args, ssh.NoPasswordAuthentication, ssh.AllocateTTY)
95+ var options ssh.Options
96+ options.EnablePTY()
97+ cmd := ssh.Command("ubuntu@"+host, args, &options)
98 cmd.Stdin = ctx.Stdin
99 cmd.Stdout = ctx.Stdout
100 cmd.Stderr = ctx.Stderr
101
102=== modified file 'cmd/juju/ssh_test.go'
103--- cmd/juju/ssh_test.go 2013-12-03 14:19:47 +0000
104+++ cmd/juju/ssh_test.go 2014-01-14 09:50:43 +0000
105@@ -28,23 +28,22 @@
106
107 type SSHCommonSuite struct {
108 testing.JujuConnSuite
109- oldpath string
110+ bin string
111 }
112
113 // fakecommand outputs its arguments to stdout for verification
114 var fakecommand = `#!/bin/bash
115
116-echo $@
117+echo $@ | tee $0.args
118 `
119
120 func (s *SSHCommonSuite) SetUpTest(c *gc.C) {
121 s.JujuConnSuite.SetUpTest(c)
122
123- path := c.MkDir()
124- s.oldpath = os.Getenv("PATH")
125- os.Setenv("PATH", path+":"+s.oldpath)
126+ s.bin = c.MkDir()
127+ s.PatchEnvironment("PATH", s.bin+":"+os.Getenv("PATH"))
128 for _, name := range []string{"ssh", "scp"} {
129- f, err := os.OpenFile(filepath.Join(path, name), os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0777)
130+ f, err := os.OpenFile(filepath.Join(s.bin, name), os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0777)
131 c.Assert(err, gc.IsNil)
132 _, err = f.Write([]byte(fakecommand))
133 c.Assert(err, gc.IsNil)
134@@ -53,11 +52,6 @@
135 }
136 }
137
138-func (s *SSHCommonSuite) TearDownTest(c *gc.C) {
139- os.Setenv("PATH", s.oldpath)
140- s.JujuConnSuite.TearDownTest(c)
141-}
142-
143 const (
144 commonArgs = `-o StrictHostKeyChecking no -o PasswordAuthentication no `
145 sshArgs = commonArgs + `-t `
146
147=== modified file 'environs/manual/fakessh.go'
148--- environs/manual/fakessh.go 2014-01-06 07:17:46 +0000
149+++ environs/manual/fakessh.go 2014-01-14 09:50:43 +0000
150@@ -4,69 +4,14 @@
151 package manual
152
153 import (
154- "fmt"
155- "io/ioutil"
156- "os"
157- "path/filepath"
158 "strings"
159
160 gc "launchpad.net/gocheck"
161
162 "launchpad.net/juju-core/testing/testbase"
163+ sshtesting "launchpad.net/juju-core/utils/ssh/testing"
164 )
165
166-// sshscript should only print the result on the first execution,
167-// to handle the case where it's called multiple times. On
168-// subsequent executions, it should find the next 'ssh' in $PATH
169-// and exec that.
170-var sshscript = `#!/bin/bash --norc
171-if [ ! -e "$0.run" ]; then
172- touch "$0.run"
173- diff "$0.expected-input" -
174- exitcode=$?
175- if [ $exitcode -ne 0 ]; then
176- echo "ERROR: did not match expected input" >&2
177- exit $exitcode
178- fi
179- # stdout
180- %s
181- # stderr
182- %s
183- exit %d
184-else
185- export PATH=${PATH#*:}
186- exec ssh $*
187-fi`
188-
189-// installFakeSSH creates a fake "ssh" command in a new $PATH,
190-// updates $PATH, and returns a function to reset $PATH to
191-// its original value when called.
192-//
193-// output may be:
194-// - nil (no output)
195-// - a string (stdout)
196-// - a slice of strings, of length two (stdout, stderr)
197-func installFakeSSH(c *gc.C, input string, output interface{}, rc int) testbase.Restorer {
198- fakebin := c.MkDir()
199- ssh := filepath.Join(fakebin, "ssh")
200- sshexpectedinput := ssh + ".expected-input"
201- var stdout, stderr string
202- switch output := output.(type) {
203- case nil:
204- case string:
205- stdout = fmt.Sprintf("cat<<EOF\n%s\nEOF", output)
206- case []string:
207- stdout = fmt.Sprintf("cat<<EOF\n%s\nEOF", output[0])
208- stderr = fmt.Sprintf("cat>&2<<EOF\n%s\nEOF", output[1])
209- }
210- script := fmt.Sprintf(sshscript, stdout, stderr, rc)
211- err := ioutil.WriteFile(ssh, []byte(script), 0777)
212- c.Assert(err, gc.IsNil)
213- err = ioutil.WriteFile(sshexpectedinput, []byte(input), 0644)
214- c.Assert(err, gc.IsNil)
215- return testbase.PatchEnvironment("PATH", fakebin+":"+os.Getenv("PATH"))
216-}
217-
218 // installDetectionFakeSSH installs a fake SSH command, which will respond
219 // to the series/hardware detection script with the specified
220 // series/arch.
221@@ -83,10 +28,10 @@
222 "MemTotal: 4096 kB",
223 "processor: 0",
224 }, "\n")
225- return installFakeSSH(c, detectionScript, detectionoutput, 0)
226+ return sshtesting.InstallFakeSSH(c, detectionScript, detectionoutput, 0)
227 }
228
229-// FakeSSH wraps the invocation of installFakeSSH based on the parameters.
230+// FakeSSH wraps the invocation of InstallFakeSSH based on the parameters.
231 type fakeSSH struct {
232 Series string
233 Arch string
234@@ -121,11 +66,11 @@
235 // output and exit codes.
236 func (r fakeSSH) install(c *gc.C) testbase.Restorer {
237 var restore testbase.Restorer
238- add := func(input string, output interface{}, rc int) {
239- restore = restore.Add(installFakeSSH(c, input, output, rc))
240+ add := func(input, output interface{}, rc int) {
241+ restore = restore.Add(sshtesting.InstallFakeSSH(c, input, output, rc))
242 }
243 if !r.SkipProvisionAgent {
244- add("", nil, r.ProvisionAgentExitCode)
245+ add(nil, nil, r.ProvisionAgentExitCode)
246 }
247 if !r.SkipDetection {
248 restore.Add(installDetectionFakeSSH(c, r.Series, r.Arch))
249@@ -134,7 +79,7 @@
250 if r.Provisioned {
251 checkProvisionedOutput = "/etc/init/jujud-machine-0.conf"
252 }
253- add("", checkProvisionedOutput, r.CheckProvisionedExitCode)
254+ add(checkProvisionedScript, checkProvisionedOutput, r.CheckProvisionedExitCode)
255 if r.InitUbuntuUser {
256 add("", nil, 0)
257 }
258
259=== modified file 'environs/manual/init.go'
260--- environs/manual/init.go 2014-01-03 05:14:11 +0000
261+++ environs/manual/init.go 2014-01-14 09:50:43 +0000
262@@ -27,10 +27,11 @@
263 // exist on the host machine.
264 func checkProvisioned(host string) (bool, error) {
265 logger.Infof("Checking if %s is already provisioned", host)
266- cmd := ssh.Command("ubuntu@"+host, []string{"bash", "-c", utils.ShQuote(checkProvisionedScript)}, ssh.NoPasswordAuthentication)
267+ cmd := ssh.Command("ubuntu@"+host, []string{"/bin/bash"}, nil)
268 var stdout, stderr bytes.Buffer
269 cmd.Stdout = &stdout
270 cmd.Stderr = &stderr
271+ cmd.Stdin = strings.NewReader(checkProvisionedScript)
272 if err := cmd.Run(); err != nil {
273 if stderr.Len() != 0 {
274 err = fmt.Errorf("%v (%v)", err, strings.TrimSpace(stderr.String()))
275@@ -52,7 +53,7 @@
276 // by connecting to the machine and executing a bash script.
277 func DetectSeriesAndHardwareCharacteristics(host string) (hc instance.HardwareCharacteristics, series string, err error) {
278 logger.Infof("Detecting series and characteristics on %s", host)
279- cmd := ssh.Command("ubuntu@"+host, []string{"bash"}, ssh.NoPasswordAuthentication)
280+ cmd := ssh.Command("ubuntu@"+host, []string{"/bin/bash"}, nil)
281 var stdout, stderr bytes.Buffer
282 cmd.Stdout = &stdout
283 cmd.Stderr = &stderr
284@@ -160,8 +161,9 @@
285 //
286 // Note that we explicitly do not allocate a PTY, so we
287 // get a failure if sudo prompts.
288- cmd := ssh.Command("ubuntu@"+host, []string{"sudo", "-n", "true"}, ssh.NoPasswordAuthentication)
289+ cmd := ssh.Command("ubuntu@"+host, []string{"sudo", "-n", "true"}, nil)
290 if cmd.Run() == nil {
291+ logger.Infof("ubuntu user is already initialised")
292 return nil
293 }
294
295@@ -171,7 +173,10 @@
296 host = login + "@" + host
297 }
298 script := fmt.Sprintf(initUbuntuScript, utils.ShQuote(authorizedKeys))
299- cmd = ssh.Command(host, []string{"sudo", "bash -c " + utils.ShQuote(script)}, ssh.AllocateTTY)
300+ var options ssh.Options
301+ options.AllowPasswordAuthentication()
302+ options.EnablePTY()
303+ cmd = ssh.Command(host, []string{"sudo", "/bin/bash -c " + utils.ShQuote(script)}, &options)
304 var stderr bytes.Buffer
305 cmd.Stdin = stdin
306 cmd.Stdout = stdout // for sudo prompt
307
308=== modified file 'environs/manual/init_test.go'
309--- environs/manual/init_test.go 2014-01-03 05:14:11 +0000
310+++ environs/manual/init_test.go 2014-01-14 09:50:43 +0000
311@@ -10,6 +10,7 @@
312
313 jc "launchpad.net/juju-core/testing/checkers"
314 "launchpad.net/juju-core/testing/testbase"
315+ sshtesting "launchpad.net/juju-core/utils/ssh/testing"
316 )
317
318 type initialisationSuite struct {
319@@ -25,7 +26,7 @@
320 "MemTotal: 4096 kB",
321 "processor: 0",
322 }, "\n")
323- defer installFakeSSH(c, detectionScript, response, 0)()
324+ defer sshtesting.InstallFakeSSH(c, detectionScript, response, 0)()
325 _, series, err := DetectSeriesAndHardwareCharacteristics("whatever")
326 c.Assert(err, gc.IsNil)
327 c.Assert(series, gc.Equals, "edgy")
328@@ -40,11 +41,11 @@
329 }, "\n")
330 // if the script fails for whatever reason, then checkProvisioned
331 // will return an error. stderr will be included in the error message.
332- defer installFakeSSH(c, detectionScript, []string{scriptResponse, "oh noes"}, 33)()
333+ defer sshtesting.InstallFakeSSH(c, detectionScript, []string{scriptResponse, "oh noes"}, 33)()
334 hc, _, err := DetectSeriesAndHardwareCharacteristics("hostname")
335 c.Assert(err, gc.ErrorMatches, "exit status 33 \\(oh noes\\)")
336 // if the script doesn't fail, stderr is simply ignored.
337- defer installFakeSSH(c, detectionScript, []string{scriptResponse, "non-empty-stderr"}, 0)()
338+ defer sshtesting.InstallFakeSSH(c, detectionScript, []string{scriptResponse, "non-empty-stderr"}, 0)()
339 hc, _, err = DetectSeriesAndHardwareCharacteristics("hostname")
340 c.Assert(err, gc.IsNil)
341 c.Assert(hc.String(), gc.Equals, "arch=arm cpu-cores=1 mem=4M")
342@@ -105,7 +106,7 @@
343 for i, test := range tests {
344 c.Logf("test %d: %s", i, test.summary)
345 scriptResponse := strings.Join(test.scriptResponse, "\n")
346- defer installFakeSSH(c, detectionScript, scriptResponse, 0)()
347+ defer sshtesting.InstallFakeSSH(c, detectionScript, scriptResponse, 0)()
348 hc, _, err := DetectSeriesAndHardwareCharacteristics("hostname")
349 c.Assert(err, gc.IsNil)
350 c.Assert(hc.String(), gc.Equals, test.expectedHc)
351@@ -113,44 +114,44 @@
352 }
353
354 func (s *initialisationSuite) TestCheckProvisioned(c *gc.C) {
355- defer installFakeSSH(c, "", "", 0)()
356+ defer sshtesting.InstallFakeSSH(c, checkProvisionedScript, "", 0)()
357 provisioned, err := checkProvisioned("example.com")
358 c.Assert(err, gc.IsNil)
359 c.Assert(provisioned, jc.IsFalse)
360
361- defer installFakeSSH(c, "", "non-empty", 0)()
362+ defer sshtesting.InstallFakeSSH(c, checkProvisionedScript, "non-empty", 0)()
363 provisioned, err = checkProvisioned("example.com")
364 c.Assert(err, gc.IsNil)
365 c.Assert(provisioned, jc.IsTrue)
366
367 // stderr should not affect result.
368- defer installFakeSSH(c, "", []string{"", "non-empty-stderr"}, 0)()
369+ defer sshtesting.InstallFakeSSH(c, checkProvisionedScript, []string{"", "non-empty-stderr"}, 0)()
370 provisioned, err = checkProvisioned("example.com")
371 c.Assert(err, gc.IsNil)
372 c.Assert(provisioned, jc.IsFalse)
373
374 // if the script fails for whatever reason, then checkProvisioned
375 // will return an error. stderr will be included in the error message.
376- defer installFakeSSH(c, "", []string{"non-empty-stdout", "non-empty-stderr"}, 255)()
377+ defer sshtesting.InstallFakeSSH(c, checkProvisionedScript, []string{"non-empty-stdout", "non-empty-stderr"}, 255)()
378 _, err = checkProvisioned("example.com")
379 c.Assert(err, gc.ErrorMatches, "exit status 255 \\(non-empty-stderr\\)")
380 }
381
382 func (s *initialisationSuite) TestInitUbuntuUserNonExisting(c *gc.C) {
383- defer installFakeSSH(c, "", "", 0)() // successful creation of ubuntu user
384- defer installFakeSSH(c, "", "", 1)() // simulate failure of ubuntu@ login
385+ defer sshtesting.InstallFakeSSH(c, "", "", 0)() // successful creation of ubuntu user
386+ defer sshtesting.InstallFakeSSH(c, "", "", 1)() // simulate failure of ubuntu@ login
387 err := InitUbuntuUser("testhost", "testuser", "", nil, nil)
388 c.Assert(err, gc.IsNil)
389 }
390
391 func (s *initialisationSuite) TestInitUbuntuUserExisting(c *gc.C) {
392- defer installFakeSSH(c, "", nil, 0)()
393+ defer sshtesting.InstallFakeSSH(c, "", nil, 0)()
394 InitUbuntuUser("testhost", "testuser", "", nil, nil)
395 }
396
397 func (s *initialisationSuite) TestInitUbuntuUserError(c *gc.C) {
398- defer installFakeSSH(c, "", []string{"", "failed to create ubuntu user"}, 123)()
399- defer installFakeSSH(c, "", "", 1)() // simulate failure of ubuntu@ login
400+ defer sshtesting.InstallFakeSSH(c, "", []string{"", "failed to create ubuntu user"}, 123)()
401+ defer sshtesting.InstallFakeSSH(c, "", "", 1)() // simulate failure of ubuntu@ login
402 err := InitUbuntuUser("testhost", "testuser", "", nil, nil)
403 c.Assert(err, gc.ErrorMatches, "exit status 123 \\(failed to create ubuntu user\\)")
404 }
405
406=== modified file 'environs/sshstorage/storage.go'
407--- environs/sshstorage/storage.go 2014-01-03 05:14:11 +0000
408+++ environs/sshstorage/storage.go 2014-01-14 09:50:43 +0000
409@@ -11,7 +11,6 @@
410 "fmt"
411 "io"
412 "io/ioutil"
413- "os/exec"
414 "path"
415 "sort"
416 "strconv"
417@@ -37,14 +36,14 @@
418 remotepath string
419 tmpdir string
420
421- cmd *exec.Cmd
422+ cmd *ssh.Cmd
423 stdin io.WriteCloser
424 stdout io.ReadCloser
425 scanner *bufio.Scanner
426 }
427
428-var sshCommand = func(host string, command string) *exec.Cmd {
429- return ssh.Command(host, []string{command}, ssh.NoPasswordAuthentication)
430+var sshCommand = func(host string, command ...string) *ssh.Cmd {
431+ return ssh.Command(host, command, nil)
432 }
433
434 type flockmode string
435@@ -87,9 +86,10 @@
436 utils.ShQuote(params.TmpDir),
437 )
438
439- cmd := sshCommand(params.Host, fmt.Sprintf("sudo -n bash -c %s", utils.ShQuote(script)))
440+ cmd := sshCommand(params.Host, "sudo", "-n", "/bin/bash")
441 var stderr bytes.Buffer
442 cmd.Stderr = &stderr
443+ cmd.Stdin = strings.NewReader(script)
444 if err := cmd.Run(); err != nil {
445 err = fmt.Errorf("failed to create storage dir: %v (%v)", err, strings.TrimSpace(stderr.String()))
446 return nil, err
447
448=== modified file 'environs/sshstorage/storage_test.go'
449--- environs/sshstorage/storage_test.go 2014-01-03 05:14:11 +0000
450+++ environs/sshstorage/storage_test.go 2014-01-14 09:50:43 +0000
451@@ -13,6 +13,7 @@
452 "path"
453 "path/filepath"
454 "regexp"
455+ "strings"
456 "time"
457
458 gc "launchpad.net/gocheck"
459@@ -22,22 +23,23 @@
460 jc "launchpad.net/juju-core/testing/checkers"
461 "launchpad.net/juju-core/testing/testbase"
462 "launchpad.net/juju-core/utils"
463+ "launchpad.net/juju-core/utils/ssh"
464 )
465
466 type storageSuite struct {
467 testbase.LoggingSuite
468+ bin string
469 }
470
471 var _ = gc.Suite(&storageSuite{})
472
473-func sshCommandTesting(host string, command string) *exec.Cmd {
474- cmd := exec.Command("bash", "-c", command)
475- uid := fmt.Sprint(os.Getuid())
476- gid := fmt.Sprint(os.Getgid())
477- defer testbase.PatchEnvironment("SUDO_UID", uid)()
478- defer testbase.PatchEnvironment("SUDO_GID", gid)()
479- cmd.Env = os.Environ()
480- return cmd
481+func (s *storageSuite) sshCommand(c *gc.C, host string, command ...string) *ssh.Cmd {
482+ script := []byte("#!/bin/bash\n" + strings.Join(command, " "))
483+ err := ioutil.WriteFile(filepath.Join(s.bin, "ssh"), script, 0755)
484+ c.Assert(err, gc.IsNil)
485+ client, err := ssh.NewOpenSSHClient()
486+ c.Assert(err, gc.IsNil)
487+ return client.Command(host, command, nil)
488 }
489
490 func newSSHStorage(host, storageDir, tmpDir string) (*SSHStorage, error) {
491@@ -59,19 +61,24 @@
492 flockBin, err = exec.LookPath("flock")
493 c.Assert(err, gc.IsNil)
494
495- bin := c.MkDir()
496- restoreEnv := testbase.PatchEnvironment("PATH", bin+":"+os.Getenv("PATH"))
497+ s.bin = c.MkDir()
498+ restoreEnv := testbase.PatchEnvironment("PATH", s.bin+":"+os.Getenv("PATH"))
499 s.AddSuiteCleanup(func(*gc.C) { restoreEnv() })
500
501- // Create a "sudo" command which shifts away the "-n" and executes the remaining args.
502- err = ioutil.WriteFile(filepath.Join(bin, "sudo"), []byte("#!/bin/sh\nshift; exec \"$@\""), 0755)
503+ // Create a "sudo" command which shifts away the "-n", sets
504+ // SUDO_UID/SUDO_GID, and executes the remaining args.
505+ err = ioutil.WriteFile(filepath.Join(s.bin, "sudo"), []byte(
506+ "#!/bin/sh\nshift; export SUDO_UID=`id -u` SUDO_GID=`id -g`; exec \"$@\"",
507+ ), 0755)
508 c.Assert(err, gc.IsNil)
509- restoreSshCommand := testbase.PatchValue(&sshCommand, sshCommandTesting)
510+ restoreSshCommand := testbase.PatchValue(&sshCommand, func(host string, command ...string) *ssh.Cmd {
511+ return s.sshCommand(c, host, command...)
512+ })
513 s.AddSuiteCleanup(func(*gc.C) { restoreSshCommand() })
514
515 // Create a new "flock" which calls the original, but in non-blocking mode.
516 data := []byte(fmt.Sprintf("#!/bin/sh\nexec %s --nonblock \"$@\"", flockBin))
517- err = ioutil.WriteFile(filepath.Join(bin, "flock"), data, 0755)
518+ err = ioutil.WriteFile(filepath.Join(s.bin, "flock"), data, 0755)
519 c.Assert(err, gc.IsNil)
520 }
521
522@@ -167,18 +174,18 @@
523 // 3: second "install"
524 // 4: touch
525 var invocations int
526- badSshCommand := func(host string, command string) *exec.Cmd {
527+ badSshCommand := func(host string, command ...string) *ssh.Cmd {
528 invocations++
529 switch invocations {
530 case 1, 3:
531- return exec.Command("true")
532+ return s.sshCommand(c, host, "true")
533 case 2:
534 // Note: must close stdin before responding the first time, or
535 // the second command will race with closing stdin, and may
536 // flush first.
537- return exec.Command("bash", "-c", "head -n 1 > /dev/null; exec 0<&-; echo JUJU-RC: 0; echo blah blah; echo more")
538+ return s.sshCommand(c, host, "head -n 1 > /dev/null; exec 0<&-; echo JUJU-RC: 0; echo blah blah; echo more")
539 case 4:
540- return exec.Command("bash", "-c", `head -n 1 > /dev/null; echo "Hey it's JUJU-RC: , but not at the beginning of the line"; echo more`)
541+ return s.sshCommand(c, host, `head -n 1 > /dev/null; echo "Hey it's JUJU-RC: , but not at the beginning of the line"; echo more`)
542 default:
543 c.Errorf("unexpected invocation: #%d, %s", invocations, command)
544 return nil
545
546=== modified file 'environs/testing/bootstrap.go'
547--- environs/testing/bootstrap.go 2013-12-20 02:38:56 +0000
548+++ environs/testing/bootstrap.go 2014-01-14 09:50:43 +0000
549@@ -14,6 +14,7 @@
550 "launchpad.net/juju-core/instance"
551 "launchpad.net/juju-core/provider/common"
552 "launchpad.net/juju-core/testing/testbase"
553+ "launchpad.net/juju-core/utils/ssh"
554 )
555
556 var logger = loggo.GetLogger("juju.environs.testing")
557@@ -22,7 +23,7 @@
558 // do not attempt to SSH to non-existent machines. The result is a function
559 // that restores finishBootstrap.
560 func DisableFinishBootstrap() func() {
561- f := func(environs.BootstrapContext, instance.Instance, *cloudinit.MachineConfig) error {
562+ f := func(environs.BootstrapContext, ssh.Client, instance.Instance, *cloudinit.MachineConfig) error {
563 logger.Warningf("provider/common.FinishBootstrap is disabled")
564 return nil
565 }
566
567=== modified file 'provider/common/bootstrap.go'
568--- provider/common/bootstrap.go 2014-01-06 07:38:53 +0000
569+++ provider/common/bootstrap.go 2014-01-14 09:50:43 +0000
570@@ -39,6 +39,15 @@
571 var inst instance.Instance
572 defer func() { handleBootstrapError(err, ctx, inst, env) }()
573
574+ // Get the bootstrap SSH client. Do this early, so we know
575+ // not to bother with any of the below if we can't finish the job.
576+ client := ssh.DefaultClient
577+ if client == nil {
578+ // This should never happen: if we don't have OpenSSH, then
579+ // go.crypto/ssh should be used with an auto-generated key.
580+ return fmt.Errorf("no SSH client available")
581+ }
582+
583 // Create an empty bootstrap state file so we can get its URL.
584 // It will be updated with the instance id and hardware characteristics
585 // after the bootstrap instance is started.
586@@ -78,7 +87,7 @@
587 if err != nil {
588 return fmt.Errorf("cannot save state: %v", err)
589 }
590- return FinishBootstrap(ctx, inst, machineConfig)
591+ return FinishBootstrap(ctx, client, inst, machineConfig)
592 }
593
594 // GenerateSystemSSHKey creates a new key for the system identity. The
595@@ -143,7 +152,7 @@
596 // to the instance via SSH and carrying out the cloud-config.
597 //
598 // Note: FinishBootstrap is exposed so it can be replaced for testing.
599-var FinishBootstrap = func(ctx environs.BootstrapContext, inst instance.Instance, machineConfig *cloudinit.MachineConfig) error {
600+var FinishBootstrap = func(ctx environs.BootstrapContext, client ssh.Client, inst instance.Instance, machineConfig *cloudinit.MachineConfig) error {
601 interrupted := make(chan os.Signal, 1)
602 ctx.InterruptNotify(interrupted)
603 defer ctx.StopInterruptNotify(interrupted)
604@@ -168,7 +177,7 @@
605 // TODO: jam 2013-12-04 bug #1257649
606 // It would be nice if users had some controll over their bootstrap
607 // timeout, since it is unlikely to be a perfect match for all clouds.
608- addr, err := waitSSH(ctx, interrupted, checkNonceCommand, inst, DefaultBootstrapSSHTimeout())
609+ addr, err := waitSSH(ctx, interrupted, client, checkNonceCommand, inst, DefaultBootstrapSSHTimeout())
610 if err != nil {
611 return err
612 }
613@@ -184,6 +193,7 @@
614 }
615 return sshinit.Configure(sshinit.ConfigureParams{
616 Host: "ubuntu@" + addr,
617+ Client: client,
618 Config: cloudcfg,
619 Stderr: ctx.Stderr(),
620 })
621@@ -227,7 +237,8 @@
622 }
623
624 type hostChecker struct {
625- addr instance.Address
626+ addr instance.Address
627+ client ssh.Client
628
629 // checkDelay is the amount of time to wait between retries.
630 checkDelay time.Duration
631@@ -256,7 +267,7 @@
632 var lastErr error
633 for {
634 go func() {
635- done <- connectSSH(hc.addr.Value, hc.checkHostScript)
636+ done <- connectSSH(hc.client, hc.addr.Value, hc.checkHostScript)
637 }()
638 select {
639 case <-hc.closed:
640@@ -278,7 +289,7 @@
641
642 type parallelHostChecker struct {
643 *parallel.Try
644-
645+ client ssh.Client
646 stderr io.Writer
647
648 // active is a map of adresses to channels for addresses actively
649@@ -304,6 +315,7 @@
650 closed := make(chan struct{})
651 hc := &hostChecker{
652 addr: addr,
653+ client: p.client,
654 checkDelay: p.checkDelay,
655 checkHostScript: p.checkHostScript,
656 closed: closed,
657@@ -329,10 +341,9 @@
658
659 // connectSSH is called to connect to the specified host and
660 // execute the "checkHostScript" bash script on it.
661-var connectSSH = func(host, checkHostScript string) error {
662- cmd := ssh.Command("ubuntu@"+host, []string{
663- "/bin/bash", "-c", utils.ShQuote(checkHostScript),
664- }, ssh.NoPasswordAuthentication)
665+var connectSSH = func(client ssh.Client, host, checkHostScript string) error {
666+ cmd := client.Command("ubuntu@"+host, []string{"/bin/bash"}, nil)
667+ cmd.Stdin = strings.NewReader(checkHostScript)
668 output, err := cmd.CombinedOutput()
669 if err != nil && len(output) > 0 {
670 err = fmt.Errorf("%s", strings.TrimSpace(string(output)))
671@@ -349,7 +360,7 @@
672 // the presence of a file on the machine that contains the
673 // machine's nonce. The "checkHostScript" is a bash script
674 // that performs this file check.
675-func waitSSH(ctx environs.BootstrapContext, interrupted <-chan os.Signal, checkHostScript string, inst addresser, timeout SSHTimeoutOpts) (addr string, err error) {
676+func waitSSH(ctx environs.BootstrapContext, interrupted <-chan os.Signal, client ssh.Client, checkHostScript string, inst addresser, timeout SSHTimeoutOpts) (addr string, err error) {
677 globalTimeout := time.After(timeout.Timeout)
678 pollAddresses := time.NewTimer(0)
679
680@@ -358,6 +369,7 @@
681 // or the tomb is killed.
682 checker := parallelHostChecker{
683 Try: parallel.NewTry(0, nil),
684+ client: client,
685 stderr: ctx.Stderr(),
686 active: make(map[instance.Address]chan struct{}),
687 checkDelay: timeout.ConnectDelay,
688
689=== modified file 'provider/common/bootstrap_test.go'
690--- provider/common/bootstrap_test.go 2014-01-06 01:38:49 +0000
691+++ provider/common/bootstrap_test.go 2014-01-14 09:50:43 +0000
692@@ -25,6 +25,7 @@
693 jc "launchpad.net/juju-core/testing/checkers"
694 "launchpad.net/juju-core/testing/testbase"
695 "launchpad.net/juju-core/tools"
696+ "launchpad.net/juju-core/utils/ssh"
697 )
698
699 type BootstrapSuite struct {
700@@ -41,7 +42,7 @@
701 func (s *BootstrapSuite) SetUpTest(c *gc.C) {
702 s.LoggingSuite.SetUpTest(c)
703 s.ToolsFixture.SetUpTest(c)
704- s.PatchValue(common.ConnectSSH, func(host, checkHostScript string) error {
705+ s.PatchValue(common.ConnectSSH, func(_ ssh.Client, host, checkHostScript string) error {
706 return fmt.Errorf("mock connection failure to %s", host)
707 })
708 }
709@@ -268,7 +269,7 @@
710
711 func (s *BootstrapSuite) TestWaitSSHTimesOutWaitingForAddresses(c *gc.C) {
712 ctx, stderr := bootstrapContext(c)
713- _, err := common.WaitSSH(ctx, nil, "/bin/true", neverAddresses{}, testSSHTimeout)
714+ _, err := common.WaitSSH(ctx, nil, ssh.DefaultClient, "/bin/true", neverAddresses{}, testSSHTimeout)
715 c.Check(err, gc.ErrorMatches, `waited for `+testSSHTimeout.Timeout.String()+` without getting any addresses`)
716 c.Check(stderr.String(), gc.Matches, "Waiting for address\n")
717 }
718@@ -280,7 +281,7 @@
719 <-time.After(2 * time.Millisecond)
720 interrupted <- os.Interrupt
721 }()
722- _, err := common.WaitSSH(ctx, interrupted, "/bin/true", neverAddresses{}, testSSHTimeout)
723+ _, err := common.WaitSSH(ctx, interrupted, ssh.DefaultClient, "/bin/true", neverAddresses{}, testSSHTimeout)
724 c.Check(err, gc.ErrorMatches, "interrupted")
725 c.Check(stderr.String(), gc.Matches, "Waiting for address\n")
726 }
727@@ -295,7 +296,7 @@
728
729 func (s *BootstrapSuite) TestWaitSSHStopsOnBadError(c *gc.C) {
730 ctx, stderr := bootstrapContext(c)
731- _, err := common.WaitSSH(ctx, nil, "/bin/true", brokenAddresses{}, testSSHTimeout)
732+ _, err := common.WaitSSH(ctx, nil, ssh.DefaultClient, "/bin/true", brokenAddresses{}, testSSHTimeout)
733 c.Check(err, gc.ErrorMatches, "getting addresses: Addresses will never work")
734 c.Check(stderr.String(), gc.Equals, "Waiting for address\n")
735 }
736@@ -312,7 +313,7 @@
737 func (s *BootstrapSuite) TestWaitSSHTimesOutWaitingForDial(c *gc.C) {
738 ctx, stderr := bootstrapContext(c)
739 // 0.x.y.z addresses are always invalid
740- _, err := common.WaitSSH(ctx, nil, "/bin/true", &neverOpensPort{addr: "0.1.2.3"}, testSSHTimeout)
741+ _, err := common.WaitSSH(ctx, nil, ssh.DefaultClient, "/bin/true", &neverOpensPort{addr: "0.1.2.3"}, testSSHTimeout)
742 c.Check(err, gc.ErrorMatches,
743 `waited for `+testSSHTimeout.Timeout.String()+` without being able to connect: mock connection failure to 0.1.2.3`)
744 c.Check(stderr.String(), gc.Matches,
745@@ -342,7 +343,7 @@
746 timeout := testSSHTimeout
747 timeout.Timeout = 1 * time.Minute
748 interrupted := make(chan os.Signal, 1)
749- _, err := common.WaitSSH(ctx, interrupted, "", &interruptOnDial{name: "0.1.2.3", interrupted: interrupted}, timeout)
750+ _, err := common.WaitSSH(ctx, interrupted, ssh.DefaultClient, "", &interruptOnDial{name: "0.1.2.3", interrupted: interrupted}, timeout)
751 c.Check(err, gc.ErrorMatches, "interrupted")
752 // Exact timing is imprecise but it should have tried a few times before being killed
753 c.Check(stderr.String(), gc.Matches,
754@@ -371,7 +372,7 @@
755
756 func (s *BootstrapSuite) TestWaitSSHRefreshAddresses(c *gc.C) {
757 ctx, stderr := bootstrapContext(c)
758- _, err := common.WaitSSH(ctx, nil, "", &addressesChange{addrs: [][]string{
759+ _, err := common.WaitSSH(ctx, nil, ssh.DefaultClient, "", &addressesChange{addrs: [][]string{
760 nil,
761 nil,
762 []string{"0.1.2.3"},
763
764=== modified file 'provider/null/environ.go'
765--- provider/null/environ.go 2014-01-08 05:49:24 +0000
766+++ provider/null/environ.go 2014-01-14 09:50:43 +0000
767@@ -234,7 +234,7 @@
768 }
769
770 var runSSHCommand = func(host string, command []string) (stderr string, err error) {
771- cmd := ssh.Command(host, command, ssh.NoPasswordAuthentication)
772+ cmd := ssh.Command(host, command, nil)
773 var stderrBuf bytes.Buffer
774 cmd.Stderr = &stderrBuf
775 err = cmd.Run()
776
777=== modified file 'utils/ssh/run.go'
778--- utils/ssh/run.go 2014-01-10 01:57:51 +0000
779+++ utils/ssh/run.go 2014-01-14 09:50:43 +0000
780@@ -33,11 +33,11 @@
781 return result, fmt.Errorf("missing host address")
782 }
783 logger.Debugf("execute on %s", params.Host)
784- options := []Option{NoPasswordAuthentication}
785+ var options Options
786 if params.IdentityFile != "" {
787- options = append(options, Option{"-i", params.IdentityFile})
788+ options.SetIdentities(params.IdentityFile)
789 }
790- command := Command(params.Host, []string{"/bin/bash", "-s"}, options...)
791+ command := Command(params.Host, []string{"/bin/bash", "-s"}, &options)
792 // start a go routine to do the actual execution
793 var stdout, stderr bytes.Buffer
794 command.Stdout = &stdout
795@@ -71,7 +71,7 @@
796 case <-time.After(params.Timeout):
797 logger.Infof("killing the command due to timeout")
798 err = fmt.Errorf("command timed out")
799- command.Process.Kill()
800+ command.Kill()
801 }
802 // In either case, gather as much as we have from stdout and stderr
803 result.Stderr = stderr.Bytes()
804
805=== added file 'utils/ssh/ssh.go'
806--- utils/ssh/ssh.go 1970-01-01 00:00:00 +0000
807+++ utils/ssh/ssh.go 2014-01-14 09:50:43 +0000
808@@ -0,0 +1,202 @@
809+// Copyright 2013 Canonical Ltd.
810+// Licensed under the AGPLv3, see LICENCE file for details.
811+
812+// Package ssh contains utilities for dealing with SSH connections,
813+// key management, and so on. All SSH-based command executions in
814+// Juju should use the Command/ScpCommand functions in this package.
815+//
816+// TODO(axw) fallback to go.crypto/ssh if no native client is available.
817+package ssh
818+
819+import (
820+ "bytes"
821+ "errors"
822+ "io"
823+)
824+
825+// Options is a client-implementation independent SSH options set.
826+type Options struct {
827+ // no PTY forced by default
828+ allocatePTY bool
829+ // password authentication is disallowed by default
830+ passwordAuthAllowed bool
831+ // identities is a sequence of paths to private key/identity files
832+ // to use when attempting to login. A client implementaton may attempt
833+ // with additional identities, but must give preference to these
834+ identities []string
835+}
836+
837+// EnablePTY forces the allocation of a pseudo-TTY.
838+//
839+// Forcing a pseudo-TTY is required, for example, for sudo
840+// prompts on the target host.
841+func (o *Options) EnablePTY() {
842+ o.allocatePTY = true
843+}
844+
845+// AllowPasswordAuthentication allows the SSH
846+// client to prompt the user for a password.
847+//
848+// Password authentication is disallowed by default.
849+func (o *Options) AllowPasswordAuthentication() {
850+ o.passwordAuthAllowed = true
851+}
852+
853+// SetIdentities sets a sequence of paths to private key/identity files
854+// to use when attempting login. Client implementations may attempt to
855+// use additional identities, but must give preference to the ones
856+// specified here.
857+func (o *Options) SetIdentities(identityFiles ...string) {
858+ o.identities = append([]string{}, identityFiles...)
859+}
860+
861+// Client is an interface for SSH clients to implement
862+type Client interface {
863+ // Command returns a Command for executing a command
864+ // on the specified host. Each Command is executed
865+ // within its own SSH session.
866+ //
867+ // Host is specified in the format [user@]host.
868+ Command(host string, command []string, options *Options) *Cmd
869+
870+ // Copy copies a file between the local host and
871+ // target host. Paths are specified in the scp format,
872+ // [[user@]host:]path.
873+ Copy(source, dest string, options *Options) error
874+}
875+
876+// Cmd represents a command to be (or being) executed
877+// on a remote host.
878+type Cmd struct {
879+ Stdin io.Reader
880+ Stdout io.Writer
881+ Stderr io.Writer
882+ impl command
883+}
884+
885+// CombinedOutput runs the command, and returns the
886+// combined stdout/stderr output and result of
887+// executing the command.
888+func (c *Cmd) CombinedOutput() ([]byte, error) {
889+ if c.Stdout != nil {
890+ return nil, errors.New("ssh: Stdout already set")
891+ }
892+ if c.Stderr != nil {
893+ return nil, errors.New("ssh: Stderr already set")
894+ }
895+ var b bytes.Buffer
896+ c.Stdout = &b
897+ c.Stderr = &b
898+ err := c.Run()
899+ return b.Bytes(), err
900+}
901+
902+// Output runs the command, and returns the stdout
903+// output and result of executing the command.
904+func (c *Cmd) Output() ([]byte, error) {
905+ if c.Stdout != nil {
906+ return nil, errors.New("ssh: Stdout already set")
907+ }
908+ var b bytes.Buffer
909+ c.Stdout = &b
910+ err := c.Run()
911+ return b.Bytes(), err
912+}
913+
914+// Run runs the command, and returns the result as an error.
915+func (c *Cmd) Run() error {
916+ if err := c.Start(); err != nil {
917+ return err
918+ }
919+ return c.Wait()
920+}
921+
922+// Start starts the command running, but does not wait for
923+// it to complete. If the command could not be started, an
924+// error is returned.
925+func (c *Cmd) Start() error {
926+ c.impl.SetStdio(c.Stdin, c.Stdout, c.Stderr)
927+ return c.impl.Start()
928+}
929+
930+// Wait waits for the started command to complete,
931+// and returns the result as an error.
932+func (c *Cmd) Wait() error {
933+ return c.impl.Wait()
934+}
935+
936+// Kill kills the started command.
937+func (c *Cmd) Kill() error {
938+ return c.impl.Kill()
939+}
940+
941+// StdinPipe creates a pipe and connects it to
942+// the command's stdin. The read end of the pipe
943+// is assigned to c.Stdin.
944+func (c *Cmd) StdinPipe() (io.WriteCloser, error) {
945+ wc, r, err := c.impl.StdinPipe()
946+ if err != nil {
947+ return nil, err
948+ }
949+ c.Stdin = r
950+ return wc, nil
951+}
952+
953+// StdoutPipe creates a pipe and connects it to
954+// the command's stdout. The write end of the pipe
955+// is assigned to c.Stdout.
956+func (c *Cmd) StdoutPipe() (io.ReadCloser, error) {
957+ rc, w, err := c.impl.StdoutPipe()
958+ if err != nil {
959+ return nil, err
960+ }
961+ c.Stdout = w
962+ return rc, nil
963+}
964+
965+// StderrPipe creates a pipe and connects it to
966+// the command's stderr. The write end of the pipe
967+// is assigned to c.Stderr.
968+func (c *Cmd) StderrPipe() (io.ReadCloser, error) {
969+ rc, w, err := c.impl.StderrPipe()
970+ if err != nil {
971+ return nil, err
972+ }
973+ c.Stderr = w
974+ return rc, nil
975+}
976+
977+// command is an implementation-specific representation of a
978+// command prepared to execute against a specific host.
979+type command interface {
980+ Start() error
981+ Wait() error
982+ Kill() error
983+ SetStdio(stdin io.Reader, stdout, stderr io.Writer)
984+ StdinPipe() (io.WriteCloser, io.Reader, error)
985+ StdoutPipe() (io.ReadCloser, io.Writer, error)
986+ StderrPipe() (io.ReadCloser, io.Writer, error)
987+}
988+
989+// DefaultClient is the default SSH client for the process.
990+//
991+// There is currently only one client available: OpenSSH.
992+// If the OpenSSH client is not installed, then DefaultClient
993+// will be nil.
994+var DefaultClient Client
995+
996+func init() {
997+ if client, err := NewOpenSSHClient(); err == nil {
998+ DefaultClient = client
999+ }
1000+}
1001+
1002+// Command is a short-cut for DefaultClient.Command.
1003+func Command(host string, command []string, options *Options) *Cmd {
1004+ return DefaultClient.Command(host, command, options)
1005+}
1006+
1007+// Copy is a short-cut for DefaultClient.Copy.
1008+func Copy(source, dest string, options *Options) error {
1009+ return DefaultClient.Copy(source, dest, options)
1010+}
1011
1012=== renamed file 'utils/ssh/ssh.go' => 'utils/ssh/ssh_openssh.go'
1013--- utils/ssh/ssh.go 2013-12-17 08:43:06 +0000
1014+++ utils/ssh/ssh_openssh.go 2014-01-14 09:50:43 +0000
1015@@ -1,31 +1,18 @@
1016 // Copyright 2013 Canonical Ltd.
1017 // Licensed under the AGPLv3, see LICENCE file for details.
1018
1019-// Package ssh contains utilities for dealing with SSH connections,
1020-// key management, and so on. All SSH-based command executions in
1021-// Juju should use the Command/ScpCommand functions in this package.
1022-//
1023-// TODO(axw) use PuTTY/plink if it's available on Windows.
1024-// TODO(axw) fallback to go.crypto/ssh if no native client is available.
1025 package ssh
1026
1027 import (
1028+ "bytes"
1029+ "fmt"
1030+ "io"
1031 "os"
1032 "os/exec"
1033-)
1034-
1035-type Option []string
1036-
1037-var (
1038- commonOptions Option = []string{"-o", "StrictHostKeyChecking no"}
1039-
1040- // AllocateTTY forces pseudo-TTY allocation, which is required,
1041- // for example, for sudo password prompts on the target host.
1042- AllocateTTY Option = []string{"-t"}
1043-
1044- // NoPasswordAuthentication disallows password-based authentication.
1045- NoPasswordAuthentication Option = []string{"-o", "PasswordAuthentication no"}
1046-)
1047+ "strings"
1048+)
1049+
1050+var opensshCommonOptions = []string{"-o", "StrictHostKeyChecking no"}
1051
1052 // sshpassWrap wraps the command/args with sshpass if it is found in $PATH
1053 // and the SSHPASS environment variable is set. Otherwise, the original
1054@@ -39,34 +26,111 @@
1055 return cmd, args
1056 }
1057
1058-// Command initialises an os/exec.Cmd to execute the native ssh program.
1059-//
1060-// If the SSHPASS environment variable is set, and the sshpass program
1061-// is available in $PATH, then the ssh command will be run with "sshpass -e".
1062-func Command(host string, command []string, options ...Option) *exec.Cmd {
1063- args := append([]string{}, commonOptions...)
1064- for _, option := range options {
1065- args = append(args, option...)
1066- }
1067+// OpenSSHClient is an implementation of Client that
1068+// uses the ssh and scp executables found in $PATH.
1069+type OpenSSHClient struct{}
1070+
1071+// NewOpenSSHClient creates a new OpenSSHClient.
1072+// If the ssh and scp programs cannot be found
1073+// in $PATH, then an error is returned.
1074+func NewOpenSSHClient() (*OpenSSHClient, error) {
1075+ var c OpenSSHClient
1076+ if _, err := exec.LookPath("ssh"); err != nil {
1077+ return nil, err
1078+ }
1079+ if _, err := exec.LookPath("scp"); err != nil {
1080+ return nil, err
1081+ }
1082+ return &c, nil
1083+}
1084+
1085+func opensshOptions(options *Options) []string {
1086+ args := append([]string{}, opensshCommonOptions...)
1087+ if options == nil {
1088+ options = &Options{}
1089+ }
1090+ if !options.passwordAuthAllowed {
1091+ args = append(args, "-o", "PasswordAuthentication no")
1092+ }
1093+ if options.allocatePTY {
1094+ args = append(args, "-t")
1095+ }
1096+ for _, identity := range options.identities {
1097+ args = append(args, "-i", identity)
1098+ }
1099+ return args
1100+}
1101+
1102+// Command implements Client.Command.
1103+func (c *OpenSSHClient) Command(host string, command []string, options *Options) *Cmd {
1104+ args := opensshOptions(options)
1105 args = append(args, host)
1106 if len(command) > 0 {
1107 args = append(args, "--")
1108 args = append(args, command...)
1109 }
1110 bin, args := sshpassWrap("ssh", args)
1111- return exec.Command(bin, args...)
1112+ return &Cmd{impl: &opensshCmd{exec.Command(bin, args...)}}
1113 }
1114
1115-// ScpCommand initialises an os/exec.Cmd to execute the native scp program.
1116-//
1117-// If the SSHPASS environment variable is set, and the sshpass program
1118-// is available in $PATH, then the scp command will be run with "sshpass -e".
1119-func ScpCommand(source, destination string, options ...Option) *exec.Cmd {
1120- args := append([]string{}, commonOptions...)
1121- for _, option := range options {
1122- args = append(args, option...)
1123+// Copy implements Client.Copy.
1124+func (c *OpenSSHClient) Copy(source, dest string, userOptions *Options) error {
1125+ var options Options
1126+ if userOptions != nil {
1127+ options = *userOptions
1128+ options.allocatePTY = false // doesn't make sense for scp
1129 }
1130- args = append(args, source, destination)
1131+ args := opensshOptions(&options)
1132+ args = append(args, source, dest)
1133 bin, args := sshpassWrap("scp", args)
1134- return exec.Command(bin, args...)
1135+ cmd := exec.Command(bin, args...)
1136+ var stderr bytes.Buffer
1137+ cmd.Stderr = &stderr
1138+ if err := cmd.Run(); err != nil {
1139+ stderr := strings.TrimSpace(stderr.String())
1140+ if len(stderr) > 0 {
1141+ err = fmt.Errorf("%v (%v)", err, stderr)
1142+ }
1143+ return err
1144+ }
1145+ return nil
1146+}
1147+
1148+type opensshCmd struct {
1149+ *exec.Cmd
1150+}
1151+
1152+func (c *opensshCmd) SetStdio(stdin io.Reader, stdout, stderr io.Writer) {
1153+ c.Stdin, c.Stdout, c.Stderr = stdin, stdout, stderr
1154+}
1155+
1156+func (c *opensshCmd) StdinPipe() (io.WriteCloser, io.Reader, error) {
1157+ wc, err := c.Cmd.StdinPipe()
1158+ if err != nil {
1159+ return nil, nil, err
1160+ }
1161+ return wc, c.Stdin, nil
1162+}
1163+
1164+func (c *opensshCmd) StdoutPipe() (io.ReadCloser, io.Writer, error) {
1165+ rc, err := c.Cmd.StdoutPipe()
1166+ if err != nil {
1167+ return nil, nil, err
1168+ }
1169+ return rc, c.Stdout, nil
1170+}
1171+
1172+func (c *opensshCmd) StderrPipe() (io.ReadCloser, io.Writer, error) {
1173+ rc, err := c.Cmd.StderrPipe()
1174+ if err != nil {
1175+ return nil, nil, err
1176+ }
1177+ return rc, c.Stderr, nil
1178+}
1179+
1180+func (c *opensshCmd) Kill() error {
1181+ if c.Process == nil {
1182+ return fmt.Errorf("process has not been started")
1183+ }
1184+ return c.Process.Kill()
1185 }
1186
1187=== modified file 'utils/ssh/ssh_test.go'
1188--- utils/ssh/ssh_test.go 2013-12-17 08:43:06 +0000
1189+++ utils/ssh/ssh_test.go 2014-01-14 09:50:43 +0000
1190@@ -7,6 +7,7 @@
1191 "io/ioutil"
1192 "os"
1193 "path/filepath"
1194+ "strings"
1195
1196 gc "launchpad.net/gocheck"
1197
1198@@ -18,49 +19,101 @@
1199 testbase.LoggingSuite
1200 testbin string
1201 fakessh string
1202+ fakescp string
1203+ client ssh.Client
1204 }
1205
1206 var _ = gc.Suite(&SSHCommandSuite{})
1207
1208+const echoCommandScript = "#!/bin/sh\necho $0 \"$@\" | tee $0.args"
1209+
1210 func (s *SSHCommandSuite) SetUpTest(c *gc.C) {
1211 s.LoggingSuite.SetUpTest(c)
1212 s.testbin = c.MkDir()
1213 s.fakessh = filepath.Join(s.testbin, "ssh")
1214- err := ioutil.WriteFile(s.fakessh, nil, 0755)
1215- c.Assert(err, gc.IsNil)
1216- s.PatchEnvironment("PATH", s.testbin)
1217-}
1218-
1219-func (s *SSHCommandSuite) TestCommand(c *gc.C) {
1220- s.assertCommandArgs(c, "localhost", []string{"echo", "123"}, []string{
1221- "ssh", "-o", "StrictHostKeyChecking no", "localhost", "--", "echo", "123",
1222- })
1223-}
1224-
1225-func (s *SSHCommandSuite) assertCommandArgs(c *gc.C, hostname string, command []string, expected []string) {
1226- cmd := ssh.Command("localhost", []string{"echo", "123"})
1227- c.Assert(cmd, gc.NotNil)
1228- c.Assert(cmd.Args, gc.DeepEquals, expected)
1229+ s.fakescp = filepath.Join(s.testbin, "scp")
1230+ err := ioutil.WriteFile(s.fakessh, []byte(echoCommandScript), 0755)
1231+ c.Assert(err, gc.IsNil)
1232+ err = ioutil.WriteFile(s.fakescp, []byte(echoCommandScript), 0755)
1233+ c.Assert(err, gc.IsNil)
1234+ s.PatchEnvironment("PATH", s.testbin+":"+os.Getenv("PATH"))
1235+ s.client, err = ssh.NewOpenSSHClient()
1236+ c.Assert(err, gc.IsNil)
1237+}
1238+
1239+func (s *SSHCommandSuite) command(args ...string) *ssh.Cmd {
1240+ return s.commandOptions(args, nil)
1241+}
1242+
1243+func (s *SSHCommandSuite) commandOptions(args []string, opts *ssh.Options) *ssh.Cmd {
1244+ return s.client.Command("localhost", args, opts)
1245+}
1246+
1247+func (s *SSHCommandSuite) assertCommandArgs(c *gc.C, cmd *ssh.Cmd, expected string) {
1248+ out, err := cmd.Output()
1249+ c.Assert(err, gc.IsNil)
1250+ c.Assert(strings.TrimSpace(string(out)), gc.Equals, expected)
1251 }
1252
1253 func (s *SSHCommandSuite) TestCommandSSHPass(c *gc.C) {
1254- // First create a fake sshpass, but don't set SSHPASS
1255+ // First create a fake sshpass, but don't set $SSHPASS
1256 fakesshpass := filepath.Join(s.testbin, "sshpass")
1257- err := ioutil.WriteFile(fakesshpass, nil, 0755)
1258- s.assertCommandArgs(c, "localhost", []string{"echo", "123"}, []string{
1259- "ssh", "-o", "StrictHostKeyChecking no", "localhost", "--", "echo", "123",
1260- })
1261-
1262- // Now set SSHPASS.
1263+ err := ioutil.WriteFile(fakesshpass, []byte(echoCommandScript), 0755)
1264+ s.assertCommandArgs(c, s.command("echo", "123"),
1265+ s.fakessh+" -o StrictHostKeyChecking no -o PasswordAuthentication no localhost -- echo 123",
1266+ )
1267+ // Now set $SSHPASS.
1268 s.PatchEnvironment("SSHPASS", "anyoldthing")
1269- s.assertCommandArgs(c, "localhost", []string{"echo", "123"}, []string{
1270- fakesshpass, "-e", "ssh", "-o", "StrictHostKeyChecking no", "localhost", "--", "echo", "123",
1271- })
1272-
1273+ s.assertCommandArgs(c, s.command("echo", "123"),
1274+ fakesshpass+" -e ssh -o StrictHostKeyChecking no -o PasswordAuthentication no localhost -- echo 123",
1275+ )
1276 // Finally, remove sshpass from $PATH.
1277 err = os.Remove(fakesshpass)
1278 c.Assert(err, gc.IsNil)
1279- s.assertCommandArgs(c, "localhost", []string{"echo", "123"}, []string{
1280- "ssh", "-o", "StrictHostKeyChecking no", "localhost", "--", "echo", "123",
1281- })
1282+ s.assertCommandArgs(c, s.command("echo", "123"),
1283+ s.fakessh+" -o StrictHostKeyChecking no -o PasswordAuthentication no localhost -- echo 123",
1284+ )
1285+}
1286+
1287+func (s *SSHCommandSuite) TestCommand(c *gc.C) {
1288+ s.assertCommandArgs(c, s.command("echo", "123"),
1289+ s.fakessh+" -o StrictHostKeyChecking no -o PasswordAuthentication no localhost -- echo 123",
1290+ )
1291+}
1292+
1293+func (s *SSHCommandSuite) TestCommandEnablePTY(c *gc.C) {
1294+ var opts ssh.Options
1295+ opts.EnablePTY()
1296+ s.assertCommandArgs(c, s.commandOptions([]string{"echo", "123"}, &opts),
1297+ s.fakessh+" -o StrictHostKeyChecking no -o PasswordAuthentication no -t localhost -- echo 123",
1298+ )
1299+}
1300+
1301+func (s *SSHCommandSuite) TestCommandAllowPasswordAuthentication(c *gc.C) {
1302+ var opts ssh.Options
1303+ opts.AllowPasswordAuthentication()
1304+ s.assertCommandArgs(c, s.commandOptions([]string{"echo", "123"}, &opts),
1305+ s.fakessh+" -o StrictHostKeyChecking no localhost -- echo 123",
1306+ )
1307+}
1308+
1309+func (s *SSHCommandSuite) TestCommandIdentities(c *gc.C) {
1310+ var opts ssh.Options
1311+ opts.SetIdentities("x", "y")
1312+ s.assertCommandArgs(c, s.commandOptions([]string{"echo", "123"}, &opts),
1313+ s.fakessh+" -o StrictHostKeyChecking no -o PasswordAuthentication no -i x -i y localhost -- echo 123",
1314+ )
1315+}
1316+
1317+func (s *SSHCommandSuite) TestCopy(c *gc.C) {
1318+ var opts ssh.Options
1319+ opts.EnablePTY()
1320+ opts.AllowPasswordAuthentication()
1321+ opts.SetIdentities("x", "y")
1322+ err := s.client.Copy("/tmp/blah", "foo@bar.com:baz", &opts)
1323+ c.Assert(err, gc.IsNil)
1324+ out, err := ioutil.ReadFile(s.fakescp + ".args")
1325+ c.Assert(err, gc.IsNil)
1326+ // EnablePTY has no effect for Copy
1327+ c.Assert(string(out), gc.Equals, s.fakescp+" -o StrictHostKeyChecking no -i x -i y /tmp/blah foo@bar.com:baz\n")
1328 }
1329
1330=== added file 'utils/ssh/testing/fakessh.go'
1331--- utils/ssh/testing/fakessh.go 1970-01-01 00:00:00 +0000
1332+++ utils/ssh/testing/fakessh.go 2014-01-14 09:50:43 +0000
1333@@ -0,0 +1,81 @@
1334+// Copyright 2013 Canonical Ltd.
1335+// Licensed under the AGPLv3, see LICENCE file for details.
1336+
1337+package testing
1338+
1339+import (
1340+ "fmt"
1341+ "io/ioutil"
1342+ "os"
1343+ "path/filepath"
1344+
1345+ gc "launchpad.net/gocheck"
1346+
1347+ "launchpad.net/juju-core/testing/testbase"
1348+)
1349+
1350+// sshscript should only print the result on the first execution,
1351+// to handle the case where it's called multiple times. On
1352+// subsequent executions, it should find the next 'ssh' in $PATH
1353+// and exec that.
1354+var sshscript = `#!/bin/bash --norc
1355+if [ ! -e "$0.run" ]; then
1356+ touch "$0.run"
1357+ if [ -e "$0.expected-input" ]; then
1358+ diff "$0.expected-input" -
1359+ exitcode=$?
1360+ if [ $exitcode -ne 0 ]; then
1361+ echo "ERROR: did not match expected input" >&2
1362+ exit $exitcode
1363+ fi
1364+ else
1365+ head >/dev/null
1366+ fi
1367+ # stdout
1368+ %s
1369+ # stderr
1370+ %s
1371+ exit %d
1372+else
1373+ export PATH=${PATH#*:}
1374+ exec ssh $*
1375+fi`
1376+
1377+// InstallFakeSSH creates a fake "ssh" command in a new $PATH,
1378+// updates $PATH, and returns a function to reset $PATH to its
1379+// original value when called.
1380+//
1381+// input may be:
1382+// - nil (ignore input)
1383+// - a string (match input exactly)
1384+// output may be:
1385+// - nil (no output)
1386+// - a string (stdout)
1387+// - a slice of strings, of length two (stdout, stderr)
1388+func InstallFakeSSH(c *gc.C, input, output interface{}, rc int) testbase.Restorer {
1389+ fakebin := c.MkDir()
1390+ ssh := filepath.Join(fakebin, "ssh")
1391+ switch input := input.(type) {
1392+ case nil:
1393+ case string:
1394+ sshexpectedinput := ssh + ".expected-input"
1395+ err := ioutil.WriteFile(sshexpectedinput, []byte(input), 0644)
1396+ c.Assert(err, gc.IsNil)
1397+ default:
1398+ c.Errorf("input has invalid type: %T", input)
1399+ }
1400+ var stdout, stderr string
1401+ switch output := output.(type) {
1402+ case nil:
1403+ case string:
1404+ stdout = fmt.Sprintf("cat<<EOF\n%s\nEOF", output)
1405+ case []string:
1406+ c.Assert(output, gc.HasLen, 2)
1407+ stdout = fmt.Sprintf("cat<<EOF\n%s\nEOF", output[0])
1408+ stderr = fmt.Sprintf("cat>&2<<EOF\n%s\nEOF", output[1])
1409+ }
1410+ script := fmt.Sprintf(sshscript, stdout, stderr, rc)
1411+ err := ioutil.WriteFile(ssh, []byte(script), 0777)
1412+ c.Assert(err, gc.IsNil)
1413+ return testbase.PatchEnvironment("PATH", fakebin+":"+os.Getenv("PATH"))
1414+}

Subscribers

People subscribed via source and target branches

to status/vote changes: