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

Proposed by Andrew Wilkins
Status: Work in progress
Proposed branch: lp:~axwalk/juju-core/ssh-gocrypto-client
Merge into: lp:~go-bot/juju-core/trunk
Diff against target: 1981 lines (+1114/-188)
26 files modified
cloudinit/sshinit/configure.go (+14/-6)
cmd/juju/scp.go (+3/-5)
cmd/juju/scp_test.go (+7/-1)
cmd/juju/ssh.go (+4/-1)
cmd/juju/ssh_test.go (+5/-12)
environs/config/authkeys.go (+14/-6)
environs/manual/fakessh.go (+21/-12)
environs/manual/init.go (+14/-4)
environs/manual/init_test.go (+4/-4)
environs/manual/provisioner.go (+3/-3)
environs/sshstorage/storage.go (+7/-5)
environs/sshstorage/storage_test.go (+26/-18)
environs/testing/bootstrap.go (+2/-1)
juju/conn.go (+10/-8)
juju/conn_test.go (+9/-6)
provider/common/bootstrap.go (+29/-9)
provider/common/bootstrap_test.go (+8/-7)
utils/ssh/authorisedkeys.go (+1/-1)
utils/ssh/clientkeys.go (+193/-0)
utils/ssh/clientkeys_test.go (+139/-0)
utils/ssh/ssh.go (+145/-62)
utils/ssh/ssh_gocrypto.go (+197/-0)
utils/ssh/ssh_openssh.go (+180/-0)
utils/ssh/ssh_test.go (+48/-17)
utils/trivial.go (+10/-0)
utils/trivial_test.go (+21/-0)
To merge this branch: bzr merge lp:~axwalk/juju-core/ssh-gocrypto-client
Reviewer Review Type Date Requested Status
Juju Engineering Pending
Review via email: mp+200628@code.launchpad.net

Description of the change

Introduce go.crypto/ssh support

A new "client key directory" is introduced,
which may contain one or more SSH key pairs.
The client key directory is initialised to
$JUJU_HOME/ssh in juju/conn.go. If the
directory does not exist, it is created with
a new 2048-bit RSA keypair.

If the default SSH client is not available
at bootstrap time, then a go.crypto/ssh client
will be used; this client will attempt to use
each of the key pairs it finds in the client
key directory.

The OpenSSH client (in utils/ssh/ssh_openssh.go)
is modified to use any of the default SSH
identities, or any of the ones found in the
client key directory.

Fixes #1263851

https://codereview.appspot.com/48370043/

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

Reviewers: mp+200628_code.launchpad.net,

Message:
Please take a look.

Description:
Introduce go.crypto/ssh support

A new "client key directory" is introduced,
which may contain one or more SSH key pairs.
The client key directory is initialised to
$JUJU_HOME/ssh in juju/conn.go. If the
directory does not exist, it is created with
a new 2048-bit RSA keypair.

If the default SSH client is not available
at bootstrap time, then a go.crypto/ssh client
will be used; this client will attempt to use
each of the key pairs it finds in the client
key directory.

The OpenSSH client (in utils/ssh/ssh_openssh.go)
is modified to use any of the default SSH
identities, or any of the ones found in the
client key directory.

Fixes #1263851

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

(do not edit description out of merge proposal)

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

Affected files (+1116, -188 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/config/authkeys.go
   M environs/manual/fakessh.go
   M environs/manual/init.go
   M environs/manual/init_test.go
   M environs/manual/provisioner.go
   M environs/sshstorage/storage.go
   M environs/sshstorage/storage_test.go
   M environs/testing/bootstrap.go
   M juju/conn.go
   M juju/conn_test.go
   M provider/common/bootstrap.go
   M provider/common/bootstrap_test.go
   M utils/ssh/authorisedkeys.go
   A utils/ssh/clientkeys.go
   A utils/ssh/clientkeys_test.go
   M utils/ssh/ssh.go
   A utils/ssh/ssh_gocrypto.go
   A utils/ssh/ssh_openssh.go
   M utils/ssh/ssh_test.go
   M utils/trivial.go
   M utils/trivial_test.go

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

On 2014/01/07 06:38:02, axw wrote:
> Please take a look.

I'm going to split this up, it's rather large. I would appreciate
comments on direction, though.

https://codereview.appspot.com/48370043/

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

I think the general approach is good, but there will be issues with the
identity file that my branch has introduced.

https://codereview.appspot.com/48370043/diff/1/cloudinit/sshinit/configure.go
File cloudinit/sshinit/configure.go (right):

https://codereview.appspot.com/48370043/diff/1/cloudinit/sshinit/configure.go#newcode54
cloudinit/sshinit/configure.go:54: cmd.Stdin = strings.NewReader(script)
One thing I noticed was the need for a carriage return on the end.
Does the script end with a new line?

https://codereview.appspot.com/48370043/diff/1/environs/config/authkeys.go
File environs/config/authkeys.go (right):

https://codereview.appspot.com/48370043/diff/1/environs/config/authkeys.go#newcode40
environs/config/authkeys.go:40: var err error
Wondering if this function should perhaps be moved into utils/ssh?

https://codereview.appspot.com/48370043/diff/1/environs/manual/fakessh.go
File environs/manual/fakessh.go (right):

https://codereview.appspot.com/48370043/diff/1/environs/manual/fakessh.go#newcode33
environs/manual/fakessh.go:33: head >/dev/null
Given the number of different places we mock out the ssh executable,
wondering if we should have some standard place for things like this,
perhaps "testing/ssh" ?

https://codereview.appspot.com/48370043/diff/1/environs/sshstorage/storage.go
File environs/sshstorage/storage.go (right):

https://codereview.appspot.com/48370043/diff/1/environs/sshstorage/storage.go#newcode48
environs/sshstorage/storage.go:48: return ssh.Command(host,
[]string{"bash", "-c", command}, &options)
bash or /bin/bash ??

We have both in the codebase, and I think we should be consistent with
ourselves.

Personally I don't care too much which we choose, just that we have one.

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

https://codereview.appspot.com/48370043/diff/1/utils/ssh/ssh.go#newcode18
utils/ssh/ssh.go:18: passwordAuthDisabled bool
hmm.. one of my branches adds an identity file option using the old way.
  This will need to be taken into account some how.

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

https://codereview.appspot.com/48370043/diff/1/utils/ssh/ssh_gocrypto.go#newcode39
utils/ssh/ssh_gocrypto.go:39: func (c *GoCryptoClient) Command(host
string, command []string, options *Options) *Cmd {
Lots of public functions missing docs.

https://codereview.appspot.com/48370043/diff/1/utils/trivial.go
File utils/trivial.go (right):

https://codereview.appspot.com/48370043/diff/1/utils/trivial.go#newcode66
utils/trivial.go:66: // quotes as necessary; each argument is
single-quoted.
this isn't escaping slashes... is it?

https://codereview.appspot.com/48370043/

Unmerged revisions

2165. By Andrew Wilkins

Fixes, finishing touches

2164. By Andrew Wilkins

Merge with trunk

Also fix various tests.

2163. By Andrew Wilkins

utils/ssh: use ssh.GenerateKey

2162. By Andrew Wilkins

Merge with trunk

2161. By Andrew Wilkins

Missed changes

2160. By Andrew Wilkins

Auto-generate SSH client keys

At initialisation time, Juju now creates $JUJU_HOME/ssh
if it doesn't exist, and populates it with a new key pair.
This key pair will be used if the client cannot connect
with any other key pair on the system (or if there are none).

The utils/ssh package now falls back to the go.crypto/ssh
client, which will use the auto-generated key.

2159. By Andrew Wilkins

Enable bootstrapping via go.crypto/ssh

If the default SSH client is not available
at bootstrap time, then a new key pair is
generated (in memory), and go.crypto/ssh
is used to bootstrap with that. The private
key is not recorded anywhere.

2158. By Andrew Wilkins

Merge with trunk

2157. By Andrew Wilkins

Merge with trunk

2156. By Andrew Wilkins

utils/ssh: go.crypto and PuTTY clients

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-03 05:14:11 +0000
3+++ cloudinit/sshinit/configure.go 2014-01-07 06:33:34 +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@@ -37,13 +40,18 @@
24 if err != nil {
25 return err
26 }
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+ var options ssh.Options
31+ options.DisablePasswordAuthentication()
32+ client := params.Client
33+ if client == nil {
34+ client = ssh.DefaultClient
35+ }
36+ cmd := client.Command(
37 params.Host,
38- []string{"sudo", "-n", fmt.Sprintf("bash -c '%s'", script)},
39- ssh.NoPasswordAuthentication,
40+ []string{"sudo", "bash"},
41+ &options,
42 )
43+ cmd.Stdin = strings.NewReader(script)
44 cmd.Stderr = params.Stderr
45 return cmd.Run()
46 }
47
48=== modified file 'cmd/juju/scp.go'
49--- cmd/juju/scp.go 2013-12-03 06:04:43 +0000
50+++ cmd/juju/scp.go 2014-01-07 06:33:34 +0000
51@@ -80,9 +80,7 @@
52 }
53 }
54
55- cmd := ssh.ScpCommand(c.Args[0], c.Args[1], ssh.NoPasswordAuthentication)
56- cmd.Stdin = ctx.Stdin
57- cmd.Stdout = ctx.Stdout
58- cmd.Stderr = ctx.Stderr
59- return cmd.Run()
60+ var options ssh.Options
61+ options.DisablePasswordAuthentication()
62+ return ssh.Copy(c.Args[0], c.Args[1], &options)
63 }
64
65=== modified file 'cmd/juju/scp_test.go'
66--- cmd/juju/scp_test.go 2013-12-03 14:19:47 +0000
67+++ cmd/juju/scp_test.go 2014-01-07 06:33:34 +0000
68@@ -6,7 +6,9 @@
69 import (
70 "bytes"
71 "fmt"
72+ "io/ioutil"
73 "net/url"
74+ "path/filepath"
75
76 gc "launchpad.net/gocheck"
77
78@@ -69,7 +71,11 @@
79 ctx := coretesting.Context(c)
80 code := cmd.Main(&SCPCommand{}, ctx, t.args)
81 c.Check(code, gc.Equals, 0)
82+ // we suppress stdout from scp.
83 c.Check(ctx.Stderr.(*bytes.Buffer).String(), gc.Equals, "")
84- c.Check(ctx.Stdout.(*bytes.Buffer).String(), gc.Equals, t.result)
85+ c.Check(ctx.Stdout.(*bytes.Buffer).String(), gc.Equals, "")
86+ data, err := ioutil.ReadFile(filepath.Join(s.bin, "scp.args"))
87+ c.Assert(err, gc.IsNil)
88+ c.Assert(string(data), gc.Equals, t.result)
89 }
90 }
91
92=== modified file 'cmd/juju/ssh.go'
93--- cmd/juju/ssh.go 2013-12-17 18:21:26 +0000
94+++ cmd/juju/ssh.go 2014-01-07 06:33:34 +0000
95@@ -88,7 +88,10 @@
96 // it from the CLI for backwards compatibility.
97 args = args[1:]
98 }
99- cmd := ssh.Command("ubuntu@"+host, args, ssh.NoPasswordAuthentication, ssh.AllocateTTY)
100+ var options ssh.Options
101+ options.EnablePTY()
102+ options.DisablePasswordAuthentication()
103+ cmd := ssh.Command("ubuntu@"+host, args, &options)
104 cmd.Stdin = ctx.Stdin
105 cmd.Stdout = ctx.Stdout
106 cmd.Stderr = ctx.Stderr
107
108=== modified file 'cmd/juju/ssh_test.go'
109--- cmd/juju/ssh_test.go 2013-12-03 14:19:47 +0000
110+++ cmd/juju/ssh_test.go 2014-01-07 06:33:34 +0000
111@@ -28,23 +28,21 @@
112
113 type SSHCommonSuite struct {
114 testing.JujuConnSuite
115- oldpath string
116+ bin string
117 }
118
119 // fakecommand outputs its arguments to stdout for verification
120 var fakecommand = `#!/bin/bash
121
122-echo $@
123+echo $@ | tee $0.args
124 `
125
126 func (s *SSHCommonSuite) SetUpTest(c *gc.C) {
127 s.JujuConnSuite.SetUpTest(c)
128-
129- path := c.MkDir()
130- s.oldpath = os.Getenv("PATH")
131- os.Setenv("PATH", path+":"+s.oldpath)
132+ s.bin = c.MkDir()
133+ s.PatchEnvironment("PATH", s.bin+":"+os.Getenv("PATH"))
134 for _, name := range []string{"ssh", "scp"} {
135- f, err := os.OpenFile(filepath.Join(path, name), os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0777)
136+ f, err := os.OpenFile(filepath.Join(s.bin, name), os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0777)
137 c.Assert(err, gc.IsNil)
138 _, err = f.Write([]byte(fakecommand))
139 c.Assert(err, gc.IsNil)
140@@ -53,11 +51,6 @@
141 }
142 }
143
144-func (s *SSHCommonSuite) TearDownTest(c *gc.C) {
145- os.Setenv("PATH", s.oldpath)
146- s.JujuConnSuite.TearDownTest(c)
147-}
148-
149 const (
150 commonArgs = `-o StrictHostKeyChecking no -o PasswordAuthentication no `
151 sshArgs = commonArgs + `-t `
152
153=== modified file 'environs/config/authkeys.go'
154--- environs/config/authkeys.go 2014-01-03 03:35:25 +0000
155+++ environs/config/authkeys.go 2014-01-07 06:33:34 +0000
156@@ -15,6 +15,8 @@
157
158 "launchpad.net/juju-core/cert"
159 "launchpad.net/juju-core/juju/osenv"
160+ "launchpad.net/juju-core/utils"
161+ "launchpad.net/juju-core/utils/ssh"
162 )
163
164 func expandTilde(f string) string {
165@@ -35,18 +37,24 @@
166 // to $HOME/.ssh.
167 func ReadAuthorizedKeys(path string) (string, error) {
168 var files []string
169+ var err error
170 if path == "" {
171- files = []string{"id_dsa.pub", "id_rsa.pub", "identity.pub"}
172+ files, err = ssh.PublicKeyFiles()
173 } else {
174- files = []string{path}
175+ path, err = utils.NormalizePath(path)
176+ if err == nil {
177+ if !filepath.IsAbs(path) {
178+ path = filepath.Join(osenv.Home(), ".ssh", path)
179+ }
180+ files = append(files, path)
181+ }
182+ }
183+ if err != nil {
184+ return "", err
185 }
186 var firstError error
187 var keyData []byte
188 for _, f := range files {
189- f = expandTilde(f)
190- if !filepath.IsAbs(f) {
191- f = filepath.Join(osenv.Home(), ".ssh", f)
192- }
193 data, err := ioutil.ReadFile(f)
194 if err != nil {
195 if firstError == nil && !os.IsNotExist(err) {
196
197=== modified file 'environs/manual/fakessh.go'
198--- environs/manual/fakessh.go 2014-01-06 07:17:46 +0000
199+++ environs/manual/fakessh.go 2014-01-07 06:33:34 +0000
200@@ -22,11 +22,15 @@
201 var sshscript = `#!/bin/bash --norc
202 if [ ! -e "$0.run" ]; then
203 touch "$0.run"
204- diff "$0.expected-input" -
205- exitcode=$?
206- if [ $exitcode -ne 0 ]; then
207- echo "ERROR: did not match expected input" >&2
208- exit $exitcode
209+ if [ -e "$0.expected-input" ]; then
210+ diff "$0.expected-input" - >&2
211+ exitcode=$?
212+ if [ $exitcode -ne 0 ]; then
213+ echo "ERROR: did not match expected input" >&2
214+ exit $exitcode
215+ fi
216+ else
217+ head >/dev/null
218 fi
219 # stdout
220 %s
221@@ -42,14 +46,21 @@
222 // updates $PATH, and returns a function to reset $PATH to
223 // its original value when called.
224 //
225+// input may be:
226+// - nil (ignore input)
227+// - a string (match input exactly)
228 // output may be:
229 // - nil (no output)
230 // - a string (stdout)
231 // - a slice of strings, of length two (stdout, stderr)
232-func installFakeSSH(c *gc.C, input string, output interface{}, rc int) testbase.Restorer {
233+func installFakeSSH(c *gc.C, input, output interface{}, rc int) testbase.Restorer {
234 fakebin := c.MkDir()
235 ssh := filepath.Join(fakebin, "ssh")
236- sshexpectedinput := ssh + ".expected-input"
237+ if input, ok := input.(string); ok {
238+ sshexpectedinput := ssh + ".expected-input"
239+ err := ioutil.WriteFile(sshexpectedinput, []byte(input), 0644)
240+ c.Assert(err, gc.IsNil)
241+ }
242 var stdout, stderr string
243 switch output := output.(type) {
244 case nil:
245@@ -62,8 +73,6 @@
246 script := fmt.Sprintf(sshscript, stdout, stderr, rc)
247 err := ioutil.WriteFile(ssh, []byte(script), 0777)
248 c.Assert(err, gc.IsNil)
249- err = ioutil.WriteFile(sshexpectedinput, []byte(input), 0644)
250- c.Assert(err, gc.IsNil)
251 return testbase.PatchEnvironment("PATH", fakebin+":"+os.Getenv("PATH"))
252 }
253
254@@ -121,11 +130,11 @@
255 // output and exit codes.
256 func (r fakeSSH) install(c *gc.C) testbase.Restorer {
257 var restore testbase.Restorer
258- add := func(input string, output interface{}, rc int) {
259+ add := func(input, output interface{}, rc int) {
260 restore = restore.Add(installFakeSSH(c, input, output, rc))
261 }
262 if !r.SkipProvisionAgent {
263- add("", nil, r.ProvisionAgentExitCode)
264+ add(nil, nil, r.ProvisionAgentExitCode)
265 }
266 if !r.SkipDetection {
267 restore.Add(installDetectionFakeSSH(c, r.Series, r.Arch))
268@@ -134,7 +143,7 @@
269 if r.Provisioned {
270 checkProvisionedOutput = "/etc/init/jujud-machine-0.conf"
271 }
272- add("", checkProvisionedOutput, r.CheckProvisionedExitCode)
273+ add(checkProvisionedScript, checkProvisionedOutput, r.CheckProvisionedExitCode)
274 if r.InitUbuntuUser {
275 add("", nil, 0)
276 }
277
278=== modified file 'environs/manual/init.go'
279--- environs/manual/init.go 2014-01-03 05:14:11 +0000
280+++ environs/manual/init.go 2014-01-07 06:33:34 +0000
281@@ -27,8 +27,11 @@
282 // exist on the host machine.
283 func checkProvisioned(host string) (bool, error) {
284 logger.Infof("Checking if %s is already provisioned", host)
285- cmd := ssh.Command("ubuntu@"+host, []string{"bash", "-c", utils.ShQuote(checkProvisionedScript)}, ssh.NoPasswordAuthentication)
286+ var options ssh.Options
287+ options.DisablePasswordAuthentication()
288+ cmd := ssh.Command("ubuntu@"+host, []string{"bash"}, &options)
289 var stdout, stderr bytes.Buffer
290+ cmd.Stdin = strings.NewReader(checkProvisionedScript)
291 cmd.Stdout = &stdout
292 cmd.Stderr = &stderr
293 if err := cmd.Run(); err != nil {
294@@ -52,7 +55,9 @@
295 // by connecting to the machine and executing a bash script.
296 func DetectSeriesAndHardwareCharacteristics(host string) (hc instance.HardwareCharacteristics, series string, err error) {
297 logger.Infof("Detecting series and characteristics on %s", host)
298- cmd := ssh.Command("ubuntu@"+host, []string{"bash"}, ssh.NoPasswordAuthentication)
299+ var options ssh.Options
300+ options.DisablePasswordAuthentication()
301+ cmd := ssh.Command("ubuntu@"+host, []string{"bash"}, &options)
302 var stdout, stderr bytes.Buffer
303 cmd.Stdout = &stdout
304 cmd.Stderr = &stderr
305@@ -160,8 +165,11 @@
306 //
307 // Note that we explicitly do not allocate a PTY, so we
308 // get a failure if sudo prompts.
309- cmd := ssh.Command("ubuntu@"+host, []string{"sudo", "-n", "true"}, ssh.NoPasswordAuthentication)
310+ var options ssh.Options
311+ options.DisablePasswordAuthentication()
312+ cmd := ssh.Command("ubuntu@"+host, []string{"sudo", "-n", "true"}, &options)
313 if cmd.Run() == nil {
314+ logger.Infof("ubuntu user is already initialised")
315 return nil
316 }
317
318@@ -171,7 +179,9 @@
319 host = login + "@" + host
320 }
321 script := fmt.Sprintf(initUbuntuScript, utils.ShQuote(authorizedKeys))
322- cmd = ssh.Command(host, []string{"sudo", "bash -c " + utils.ShQuote(script)}, ssh.AllocateTTY)
323+ options = ssh.Options{} // reset options
324+ options.EnablePTY()
325+ cmd = ssh.Command(host, []string{"sudo", "bash -c " + utils.ShQuote(script)}, &options)
326 var stderr bytes.Buffer
327 cmd.Stdin = stdin
328 cmd.Stdout = stdout // for sudo prompt
329
330=== modified file 'environs/manual/init_test.go'
331--- environs/manual/init_test.go 2014-01-03 05:14:11 +0000
332+++ environs/manual/init_test.go 2014-01-07 06:33:34 +0000
333@@ -113,25 +113,25 @@
334 }
335
336 func (s *initialisationSuite) TestCheckProvisioned(c *gc.C) {
337- defer installFakeSSH(c, "", "", 0)()
338+ defer installFakeSSH(c, checkProvisionedScript, "", 0)()
339 provisioned, err := checkProvisioned("example.com")
340 c.Assert(err, gc.IsNil)
341 c.Assert(provisioned, jc.IsFalse)
342
343- defer installFakeSSH(c, "", "non-empty", 0)()
344+ defer installFakeSSH(c, checkProvisionedScript, "non-empty", 0)()
345 provisioned, err = checkProvisioned("example.com")
346 c.Assert(err, gc.IsNil)
347 c.Assert(provisioned, jc.IsTrue)
348
349 // stderr should not affect result.
350- defer installFakeSSH(c, "", []string{"", "non-empty-stderr"}, 0)()
351+ defer installFakeSSH(c, checkProvisionedScript, []string{"", "non-empty-stderr"}, 0)()
352 provisioned, err = checkProvisioned("example.com")
353 c.Assert(err, gc.IsNil)
354 c.Assert(provisioned, jc.IsFalse)
355
356 // if the script fails for whatever reason, then checkProvisioned
357 // will return an error. stderr will be included in the error message.
358- defer installFakeSSH(c, "", []string{"non-empty-stdout", "non-empty-stderr"}, 255)()
359+ defer installFakeSSH(c, checkProvisionedScript, []string{"non-empty-stdout", "non-empty-stderr"}, 255)()
360 _, err = checkProvisioned("example.com")
361 c.Assert(err, gc.ErrorMatches, "exit status 255 \\(non-empty-stderr\\)")
362 }
363
364=== modified file 'environs/manual/provisioner.go'
365--- environs/manual/provisioner.go 2014-01-03 05:14:11 +0000
366+++ environs/manual/provisioner.go 2014-01-07 06:33:34 +0000
367@@ -82,9 +82,9 @@
368 }()
369
370 // Create the "ubuntu" user and initialise passwordless sudo. We populate
371- // the ubuntu user's authorized_keys file with the public keys in the current
372- // user's ~/.ssh directory. The authenticationworker will later update the
373- // ubuntu user's authorized_keys.
374+ // the ubuntu user's authorized_keys file with the public keys in the
375+ // current user's ~/.ssh directory. The authenticationworker will later
376+ // update the ubuntu user's authorized_keys.
377 user, host := splitUserHost(args.Host)
378 authorizedKeys, err := config.ReadAuthorizedKeys("")
379 if err := InitUbuntuUser(host, user, authorizedKeys, args.Stdin, args.Stdout); err != nil {
380
381=== modified file 'environs/sshstorage/storage.go'
382--- environs/sshstorage/storage.go 2014-01-03 05:14:11 +0000
383+++ environs/sshstorage/storage.go 2014-01-07 06:33:34 +0000
384@@ -11,7 +11,6 @@
385 "fmt"
386 "io"
387 "io/ioutil"
388- "os/exec"
389 "path"
390 "sort"
391 "strconv"
392@@ -37,14 +36,16 @@
393 remotepath string
394 tmpdir string
395
396- cmd *exec.Cmd
397+ cmd *ssh.Cmd
398 stdin io.WriteCloser
399 stdout io.ReadCloser
400 scanner *bufio.Scanner
401 }
402
403-var sshCommand = func(host string, command string) *exec.Cmd {
404- return ssh.Command(host, []string{command}, ssh.NoPasswordAuthentication)
405+var sshCommand = func(host string, command string) *ssh.Cmd {
406+ var options ssh.Options
407+ options.DisablePasswordAuthentication()
408+ return ssh.Command(host, []string{"bash", "-c", command}, &options)
409 }
410
411 type flockmode string
412@@ -87,9 +88,10 @@
413 utils.ShQuote(params.TmpDir),
414 )
415
416- cmd := sshCommand(params.Host, fmt.Sprintf("sudo -n bash -c %s", utils.ShQuote(script)))
417+ cmd := sshCommand(params.Host, "sudo -n bash")
418 var stderr bytes.Buffer
419 cmd.Stderr = &stderr
420+ cmd.Stdin = strings.NewReader(script)
421 if err := cmd.Run(); err != nil {
422 err = fmt.Errorf("failed to create storage dir: %v (%v)", err, strings.TrimSpace(stderr.String()))
423 return nil, err
424
425=== modified file 'environs/sshstorage/storage_test.go'
426--- environs/sshstorage/storage_test.go 2014-01-03 05:14:11 +0000
427+++ environs/sshstorage/storage_test.go 2014-01-07 06:33:34 +0000
428@@ -22,22 +22,27 @@
429 jc "launchpad.net/juju-core/testing/checkers"
430 "launchpad.net/juju-core/testing/testbase"
431 "launchpad.net/juju-core/utils"
432+ "launchpad.net/juju-core/utils/ssh"
433 )
434
435 type storageSuite struct {
436 testbase.LoggingSuite
437+ bin string // temporary bin dir
438 }
439
440 var _ = gc.Suite(&storageSuite{})
441
442-func sshCommandTesting(host string, command string) *exec.Cmd {
443- cmd := exec.Command("bash", "-c", command)
444- uid := fmt.Sprint(os.Getuid())
445- gid := fmt.Sprint(os.Getgid())
446- defer testbase.PatchEnvironment("SUDO_UID", uid)()
447- defer testbase.PatchEnvironment("SUDO_GID", gid)()
448- cmd.Env = os.Environ()
449- return cmd
450+func (s *storageSuite) sshCommand(host string, command string) *ssh.Cmd {
451+ script := []byte("#!/bin/bash\n" + command)
452+ err := ioutil.WriteFile(filepath.Join(s.bin, "ssh"), script, 0755)
453+ if err != nil {
454+ panic(err)
455+ }
456+ client, err := ssh.NewOpenSSHClient()
457+ if err != nil {
458+ panic(err)
459+ }
460+ return client.Command(host, []string{command}, nil)
461 }
462
463 func newSSHStorage(host, storageDir, tmpDir string) (*SSHStorage, error) {
464@@ -59,19 +64,22 @@
465 flockBin, err = exec.LookPath("flock")
466 c.Assert(err, gc.IsNil)
467
468- bin := c.MkDir()
469- restoreEnv := testbase.PatchEnvironment("PATH", bin+":"+os.Getenv("PATH"))
470+ s.bin = c.MkDir()
471+ restoreEnv := testbase.PatchEnvironment("PATH", s.bin+":"+os.Getenv("PATH"))
472 s.AddSuiteCleanup(func(*gc.C) { restoreEnv() })
473
474- // Create a "sudo" command which shifts away the "-n" and executes the remaining args.
475- err = ioutil.WriteFile(filepath.Join(bin, "sudo"), []byte("#!/bin/sh\nshift; exec \"$@\""), 0755)
476+ // Create a "sudo" command which shifts away the "-n", sets
477+ // SUDO_UID/SUDO_GID, and executes the remaining args.
478+ err = ioutil.WriteFile(filepath.Join(s.bin, "sudo"), []byte(
479+ "#!/bin/sh\nshift; export SUDO_UID=`id -u` SUDO_GID=`id -g`; exec \"$@\"",
480+ ), 0755)
481 c.Assert(err, gc.IsNil)
482- restoreSshCommand := testbase.PatchValue(&sshCommand, sshCommandTesting)
483+ restoreSshCommand := testbase.PatchValue(&sshCommand, s.sshCommand)
484 s.AddSuiteCleanup(func(*gc.C) { restoreSshCommand() })
485
486 // Create a new "flock" which calls the original, but in non-blocking mode.
487 data := []byte(fmt.Sprintf("#!/bin/sh\nexec %s --nonblock \"$@\"", flockBin))
488- err = ioutil.WriteFile(filepath.Join(bin, "flock"), data, 0755)
489+ err = ioutil.WriteFile(filepath.Join(s.bin, "flock"), data, 0755)
490 c.Assert(err, gc.IsNil)
491 }
492
493@@ -167,18 +175,18 @@
494 // 3: second "install"
495 // 4: touch
496 var invocations int
497- badSshCommand := func(host string, command string) *exec.Cmd {
498+ badSshCommand := func(host string, command string) *ssh.Cmd {
499 invocations++
500 switch invocations {
501 case 1, 3:
502- return exec.Command("true")
503+ return s.sshCommand(host, "true")
504 case 2:
505 // Note: must close stdin before responding the first time, or
506 // the second command will race with closing stdin, and may
507 // flush first.
508- return exec.Command("bash", "-c", "head -n 1 > /dev/null; exec 0<&-; echo JUJU-RC: 0; echo blah blah; echo more")
509+ return s.sshCommand(host, "head -n 1 > /dev/null; exec 0<&-; echo JUJU-RC: 0; echo blah blah; echo more")
510 case 4:
511- 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`)
512+ return s.sshCommand(host, `head -n 1 > /dev/null; echo "Hey it's JUJU-RC: , but not at the beginning of the line"; echo more`)
513 default:
514 c.Errorf("unexpected invocation: #%d, %s", invocations, command)
515 return nil
516
517=== modified file 'environs/testing/bootstrap.go'
518--- environs/testing/bootstrap.go 2013-12-20 02:38:56 +0000
519+++ environs/testing/bootstrap.go 2014-01-07 06:33:34 +0000
520@@ -14,6 +14,7 @@
521 "launchpad.net/juju-core/instance"
522 "launchpad.net/juju-core/provider/common"
523 "launchpad.net/juju-core/testing/testbase"
524+ "launchpad.net/juju-core/utils/ssh"
525 )
526
527 var logger = loggo.GetLogger("juju.environs.testing")
528@@ -22,7 +23,7 @@
529 // do not attempt to SSH to non-existent machines. The result is a function
530 // that restores finishBootstrap.
531 func DisableFinishBootstrap() func() {
532- f := func(environs.BootstrapContext, instance.Instance, *cloudinit.MachineConfig) error {
533+ f := func(environs.BootstrapContext, ssh.Client, instance.Instance, *cloudinit.MachineConfig) error {
534 logger.Warningf("provider/common.FinishBootstrap is disabled")
535 return nil
536 }
537
538=== modified file 'juju/conn.go'
539--- juju/conn.go 2013-12-19 21:17:26 +0000
540+++ juju/conn.go 2014-01-07 06:33:34 +0000
541@@ -12,7 +12,6 @@
542 "io/ioutil"
543 "net/url"
544 "os"
545- "path/filepath"
546 "time"
547
548 "launchpad.net/juju-core/charm"
549@@ -21,9 +20,9 @@
550 "launchpad.net/juju-core/environs/configstore"
551 "launchpad.net/juju-core/errors"
552 "launchpad.net/juju-core/juju/osenv"
553- "launchpad.net/juju-core/log"
554 "launchpad.net/juju-core/state"
555 "launchpad.net/juju-core/utils"
556+ "launchpad.net/juju-core/utils/ssh"
557 )
558
559 // Conn holds a connection to a juju environment and its
560@@ -59,7 +58,7 @@
561 opts := state.DefaultDialOpts()
562 st, err := state.Open(info, opts)
563 if errors.IsUnauthorizedError(err) {
564- log.Noticef("juju: authorization error while connecting to state server; retrying")
565+ logger.Infof("juju: authorization error while connecting to state server; retrying")
566 // We can't connect with the administrator password,;
567 // perhaps this was the first connection and the
568 // password has not been changed yet.
569@@ -233,7 +232,7 @@
570 return nil, err
571 }
572 stor := conn.Environ.Storage()
573- log.Infof("writing charm to storage [%d bytes]", size)
574+ logger.Infof("writing charm to storage [%d bytes]", size)
575 if err := stor.Put(name, f, size); err != nil {
576 return nil, fmt.Errorf("cannot put charm: %v", err)
577 }
578@@ -245,7 +244,7 @@
579 if err != nil {
580 return nil, fmt.Errorf("cannot parse storage URL: %v", err)
581 }
582- log.Infof("adding charm to state")
583+ logger.Infof("adding charm to state")
584 sch, err := conn.State.AddCharm(ch, curl, u, digest)
585 if err != nil {
586 return nil, fmt.Errorf("cannot add charm: %v", err)
587@@ -253,8 +252,8 @@
588 return sch, nil
589 }
590
591-// InitJujuHome initializes the charm and environs/config packages to use
592-// default paths based on the $JUJU_HOME or $HOME environment variables.
593+// InitJujuHome initializes the charm, environs/config and utils/ssh packages
594+// to use default paths based on the $JUJU_HOME or $HOME environment variables.
595 // This function should be called before calling NewConn or Conn.Deploy.
596 func InitJujuHome() error {
597 jujuHome := osenv.JujuHomeDir()
598@@ -263,6 +262,9 @@
599 "cannot determine juju home, required environment variables are not set")
600 }
601 config.SetJujuHome(jujuHome)
602- charm.CacheDir = filepath.Join(jujuHome, "charmcache")
603+ charm.CacheDir = config.JujuHomePath("charmcache")
604+ if err := ssh.InitClientKeyDir(config.JujuHomePath("ssh")); err != nil {
605+ return fmt.Errorf("cannot initialise ssh client key directory: %v", err)
606+ }
607 return nil
608 }
609
610=== modified file 'juju/conn_test.go'
611--- juju/conn_test.go 2013-12-20 09:25:57 +0000
612+++ juju/conn_test.go 2014-01-07 06:33:34 +0000
613@@ -657,18 +657,20 @@
614 }
615
616 func (s *InitJujuHomeSuite) TestJujuHome(c *gc.C) {
617- os.Setenv("JUJU_HOME", "/my/juju/home")
618+ jujuHome := c.MkDir()
619+ os.Setenv("JUJU_HOME", jujuHome)
620 err := juju.InitJujuHome()
621 c.Assert(err, gc.IsNil)
622- c.Assert(config.JujuHome(), gc.Equals, "/my/juju/home")
623+ c.Assert(config.JujuHome(), gc.Equals, jujuHome)
624 }
625
626 func (s *InitJujuHomeSuite) TestHome(c *gc.C) {
627+ osHome := c.MkDir()
628 os.Setenv("JUJU_HOME", "")
629- osenv.SetHome("/my/home/")
630+ osenv.SetHome(osHome)
631 err := juju.InitJujuHome()
632 c.Assert(err, gc.IsNil)
633- c.Assert(config.JujuHome(), gc.Equals, "/my/home/.juju")
634+ c.Assert(config.JujuHome(), gc.Equals, filepath.Join(osHome, ".juju"))
635 }
636
637 func (s *InitJujuHomeSuite) TestError(c *gc.C) {
638@@ -679,9 +681,10 @@
639 }
640
641 func (s *InitJujuHomeSuite) TestCacheDir(c *gc.C) {
642- os.Setenv("JUJU_HOME", "/foo/bar")
643+ jujuHome := c.MkDir()
644+ os.Setenv("JUJU_HOME", jujuHome)
645 c.Assert(charm.CacheDir, gc.Equals, "")
646 err := juju.InitJujuHome()
647 c.Assert(err, gc.IsNil)
648- c.Assert(charm.CacheDir, gc.Equals, "/foo/bar/charmcache")
649+ c.Assert(charm.CacheDir, gc.Equals, filepath.Join(jujuHome, "charmcache"))
650 }
651
652=== modified file 'provider/common/bootstrap.go'
653--- provider/common/bootstrap.go 2014-01-06 07:38:53 +0000
654+++ provider/common/bootstrap.go 2014-01-07 06:33:34 +0000
655@@ -39,6 +39,15 @@
656 var inst instance.Instance
657 defer func() { handleBootstrapError(err, ctx, inst, env) }()
658
659+ // Get the bootstrap SSH client. Do this early, so we know
660+ // not to bother with any of the below if we can't finish the job.
661+ client := ssh.DefaultClient
662+ if client == nil {
663+ // This should never happen: if we don't have OpenSSH, then
664+ // go.crypto/ssh should be used with an auto-generated key.
665+ return fmt.Errorf("no SSH client available")
666+ }
667+
668 // Create an empty bootstrap state file so we can get its URL.
669 // It will be updated with the instance id and hardware characteristics
670 // after the bootstrap instance is started.
671@@ -78,7 +87,8 @@
672 if err != nil {
673 return fmt.Errorf("cannot save state: %v", err)
674 }
675- return FinishBootstrap(ctx, inst, machineConfig)
676+
677+ return FinishBootstrap(ctx, client, inst, machineConfig)
678 }
679
680 // GenerateSystemSSHKey creates a new key for the system identity. The
681@@ -143,7 +153,7 @@
682 // to the instance via SSH and carrying out the cloud-config.
683 //
684 // Note: FinishBootstrap is exposed so it can be replaced for testing.
685-var FinishBootstrap = func(ctx environs.BootstrapContext, inst instance.Instance, machineConfig *cloudinit.MachineConfig) error {
686+var FinishBootstrap = func(ctx environs.BootstrapContext, client ssh.Client, inst instance.Instance, machineConfig *cloudinit.MachineConfig) error {
687 interrupted := make(chan os.Signal, 1)
688 ctx.InterruptNotify(interrupted)
689 defer ctx.StopInterruptNotify(interrupted)
690@@ -168,7 +178,7 @@
691 // TODO: jam 2013-12-04 bug #1257649
692 // It would be nice if users had some controll over their bootstrap
693 // timeout, since it is unlikely to be a perfect match for all clouds.
694- addr, err := waitSSH(ctx, interrupted, checkNonceCommand, inst, DefaultBootstrapSSHTimeout())
695+ addr, err := waitSSH(ctx, interrupted, client, checkNonceCommand, inst, DefaultBootstrapSSHTimeout())
696 if err != nil {
697 return err
698 }
699@@ -184,6 +194,7 @@
700 }
701 return sshinit.Configure(sshinit.ConfigureParams{
702 Host: "ubuntu@" + addr,
703+ Client: client,
704 Config: cloudcfg,
705 Stderr: ctx.Stderr(),
706 })
707@@ -229,6 +240,9 @@
708 type hostChecker struct {
709 addr instance.Address
710
711+ // client is the ssh.Client to use to check the host.
712+ client ssh.Client
713+
714 // checkDelay is the amount of time to wait between retries.
715 checkDelay time.Duration
716
717@@ -256,7 +270,7 @@
718 var lastErr error
719 for {
720 go func() {
721- done <- connectSSH(hc.addr.Value, hc.checkHostScript)
722+ done <- connectSSH(hc.client, hc.addr.Value, hc.checkHostScript)
723 }()
724 select {
725 case <-hc.closed:
726@@ -267,6 +281,7 @@
727 if lastErr == nil {
728 return hc, nil
729 }
730+ logger.Errorf("failed check for %q: %v", hc.addr.Value, lastErr)
731 }
732 select {
733 case <-hc.closed:
734@@ -279,6 +294,8 @@
735 type parallelHostChecker struct {
736 *parallel.Try
737
738+ client ssh.Client
739+
740 stderr io.Writer
741
742 // active is a map of adresses to channels for addresses actively
743@@ -305,6 +322,7 @@
744 hc := &hostChecker{
745 addr: addr,
746 checkDelay: p.checkDelay,
747+ client: p.client,
748 checkHostScript: p.checkHostScript,
749 closed: closed,
750 }
751@@ -329,10 +347,11 @@
752
753 // connectSSH is called to connect to the specified host and
754 // execute the "checkHostScript" bash script on it.
755-var connectSSH = func(host, checkHostScript string) error {
756- cmd := ssh.Command("ubuntu@"+host, []string{
757- "/bin/bash", "-c", utils.ShQuote(checkHostScript),
758- }, ssh.NoPasswordAuthentication)
759+var connectSSH = func(client ssh.Client, host, checkHostScript string) error {
760+ var options ssh.Options
761+ options.DisablePasswordAuthentication()
762+ cmd := client.Command("ubuntu@"+host, []string{"bash"}, &options)
763+ cmd.Stdin = strings.NewReader(checkHostScript)
764 output, err := cmd.CombinedOutput()
765 if err != nil && len(output) > 0 {
766 err = fmt.Errorf("%s", strings.TrimSpace(string(output)))
767@@ -349,7 +368,7 @@
768 // the presence of a file on the machine that contains the
769 // machine's nonce. The "checkHostScript" is a bash script
770 // that performs this file check.
771-func waitSSH(ctx environs.BootstrapContext, interrupted <-chan os.Signal, checkHostScript string, inst addresser, timeout SSHTimeoutOpts) (addr string, err error) {
772+func waitSSH(ctx environs.BootstrapContext, interrupted <-chan os.Signal, client ssh.Client, checkHostScript string, inst addresser, timeout SSHTimeoutOpts) (addr string, err error) {
773 globalTimeout := time.After(timeout.Timeout)
774 pollAddresses := time.NewTimer(0)
775
776@@ -360,6 +379,7 @@
777 Try: parallel.NewTry(0, nil),
778 stderr: ctx.Stderr(),
779 active: make(map[instance.Address]chan struct{}),
780+ client: client,
781 checkDelay: timeout.ConnectDelay,
782 checkHostScript: checkHostScript,
783 }
784
785=== modified file 'provider/common/bootstrap_test.go'
786--- provider/common/bootstrap_test.go 2014-01-06 01:38:49 +0000
787+++ provider/common/bootstrap_test.go 2014-01-07 06:33:34 +0000
788@@ -25,6 +25,7 @@
789 jc "launchpad.net/juju-core/testing/checkers"
790 "launchpad.net/juju-core/testing/testbase"
791 "launchpad.net/juju-core/tools"
792+ "launchpad.net/juju-core/utils/ssh"
793 )
794
795 type BootstrapSuite struct {
796@@ -41,7 +42,7 @@
797 func (s *BootstrapSuite) SetUpTest(c *gc.C) {
798 s.LoggingSuite.SetUpTest(c)
799 s.ToolsFixture.SetUpTest(c)
800- s.PatchValue(common.ConnectSSH, func(host, checkHostScript string) error {
801+ s.PatchValue(common.ConnectSSH, func(_ ssh.Client, host, checkHostScript string) error {
802 return fmt.Errorf("mock connection failure to %s", host)
803 })
804 }
805@@ -268,7 +269,7 @@
806
807 func (s *BootstrapSuite) TestWaitSSHTimesOutWaitingForAddresses(c *gc.C) {
808 ctx, stderr := bootstrapContext(c)
809- _, err := common.WaitSSH(ctx, nil, "/bin/true", neverAddresses{}, testSSHTimeout)
810+ _, err := common.WaitSSH(ctx, nil, ssh.DefaultClient, "/bin/true", neverAddresses{}, testSSHTimeout)
811 c.Check(err, gc.ErrorMatches, `waited for `+testSSHTimeout.Timeout.String()+` without getting any addresses`)
812 c.Check(stderr.String(), gc.Matches, "Waiting for address\n")
813 }
814@@ -280,7 +281,7 @@
815 <-time.After(2 * time.Millisecond)
816 interrupted <- os.Interrupt
817 }()
818- _, err := common.WaitSSH(ctx, interrupted, "/bin/true", neverAddresses{}, testSSHTimeout)
819+ _, err := common.WaitSSH(ctx, interrupted, ssh.DefaultClient, "/bin/true", neverAddresses{}, testSSHTimeout)
820 c.Check(err, gc.ErrorMatches, "interrupted")
821 c.Check(stderr.String(), gc.Matches, "Waiting for address\n")
822 }
823@@ -295,7 +296,7 @@
824
825 func (s *BootstrapSuite) TestWaitSSHStopsOnBadError(c *gc.C) {
826 ctx, stderr := bootstrapContext(c)
827- _, err := common.WaitSSH(ctx, nil, "/bin/true", brokenAddresses{}, testSSHTimeout)
828+ _, err := common.WaitSSH(ctx, nil, ssh.DefaultClient, "/bin/true", brokenAddresses{}, testSSHTimeout)
829 c.Check(err, gc.ErrorMatches, "getting addresses: Addresses will never work")
830 c.Check(stderr.String(), gc.Equals, "Waiting for address\n")
831 }
832@@ -312,7 +313,7 @@
833 func (s *BootstrapSuite) TestWaitSSHTimesOutWaitingForDial(c *gc.C) {
834 ctx, stderr := bootstrapContext(c)
835 // 0.x.y.z addresses are always invalid
836- _, err := common.WaitSSH(ctx, nil, "/bin/true", &neverOpensPort{addr: "0.1.2.3"}, testSSHTimeout)
837+ _, err := common.WaitSSH(ctx, nil, ssh.DefaultClient, "/bin/true", &neverOpensPort{addr: "0.1.2.3"}, testSSHTimeout)
838 c.Check(err, gc.ErrorMatches,
839 `waited for `+testSSHTimeout.Timeout.String()+` without being able to connect: mock connection failure to 0.1.2.3`)
840 c.Check(stderr.String(), gc.Matches,
841@@ -342,7 +343,7 @@
842 timeout := testSSHTimeout
843 timeout.Timeout = 1 * time.Minute
844 interrupted := make(chan os.Signal, 1)
845- _, err := common.WaitSSH(ctx, interrupted, "", &interruptOnDial{name: "0.1.2.3", interrupted: interrupted}, timeout)
846+ _, err := common.WaitSSH(ctx, interrupted, ssh.DefaultClient, "", &interruptOnDial{name: "0.1.2.3", interrupted: interrupted}, timeout)
847 c.Check(err, gc.ErrorMatches, "interrupted")
848 // Exact timing is imprecise but it should have tried a few times before being killed
849 c.Check(stderr.String(), gc.Matches,
850@@ -371,7 +372,7 @@
851
852 func (s *BootstrapSuite) TestWaitSSHRefreshAddresses(c *gc.C) {
853 ctx, stderr := bootstrapContext(c)
854- _, err := common.WaitSSH(ctx, nil, "", &addressesChange{addrs: [][]string{
855+ _, err := common.WaitSSH(ctx, nil, ssh.DefaultClient, "", &addressesChange{addrs: [][]string{
856 nil,
857 nil,
858 []string{"0.1.2.3"},
859
860=== modified file 'utils/ssh/authorisedkeys.go'
861--- utils/ssh/authorisedkeys.go 2013-12-23 05:18:58 +0000
862+++ utils/ssh/authorisedkeys.go 2014-01-07 06:33:34 +0000
863@@ -19,7 +19,7 @@
864 "launchpad.net/juju-core/utils"
865 )
866
867-var logger = loggo.GetLogger("juju.ssh")
868+var logger = loggo.GetLogger("juju.utils.ssh")
869
870 type ListMode bool
871
872
873=== added file 'utils/ssh/clientkeys.go'
874--- utils/ssh/clientkeys.go 1970-01-01 00:00:00 +0000
875+++ utils/ssh/clientkeys.go 2014-01-07 06:33:34 +0000
876@@ -0,0 +1,193 @@
877+// Copyright 2014 Canonical Ltd.
878+// Licensed under the AGPLv3, see LICENCE file for details.
879+
880+package ssh
881+
882+import (
883+ "fmt"
884+ "io/ioutil"
885+ "os"
886+ "path/filepath"
887+ "strings"
888+ "sync"
889+
890+ "code.google.com/p/go.crypto/ssh"
891+
892+ "launchpad.net/juju-core/utils"
893+)
894+
895+const clientKeyName = "juju_id_rsa"
896+
897+var (
898+ clientKeyMutex sync.Mutex
899+
900+ // clientKeyDir may be set to a filesystem directory
901+ // in which additional private and public keys will be
902+ // located. SSH client implementations will use these
903+ // keys as well as any defaults found in ~/.ssh.
904+ clientKeyDir string
905+
906+ // clientPrivateKeys is a cached slice of ssh.Signers,
907+ // which gets recomputed when clientKeyDir changes.
908+ clientPrivateKeys []ssh.Signer
909+)
910+
911+// InitClientKeyDir initialises a directory in which
912+// client keys may be located; the directory is created
913+// if it does not already exist, and populated with a
914+// new public/private key.
915+func InitClientKeyDir(dir string) error {
916+ clientKeyMutex.Lock()
917+ defer clientKeyMutex.Unlock()
918+ if dir == "" {
919+ // Primarily for testing.
920+ clientKeyDir = ""
921+ return nil
922+ }
923+ if dir == clientKeyDir {
924+ return nil
925+ }
926+ dir, err := utils.NormalizePath(dir)
927+ if err != nil {
928+ return err
929+ }
930+ if _, err := os.Stat(dir); err == nil {
931+ // If the directory exists without keys,
932+ // the user must remove the directory.
933+ if err = loadClientKeys(dir); err == nil {
934+ clientKeyDir = dir
935+ }
936+ return err
937+ }
938+ if err := os.MkdirAll(dir, 0700); err != nil {
939+ return err
940+ }
941+ err = generateClientKey(dir)
942+ if err != nil {
943+ os.RemoveAll(dir)
944+ return err
945+ }
946+ clientKeyDir = dir
947+ return nil
948+}
949+
950+func generateClientKey(dir string) error {
951+ private, public, err := GenerateKey("juju-client-key")
952+ if err != nil {
953+ return err
954+ }
955+ clientPrivateKey, err := ssh.ParsePrivateKey([]byte(private))
956+ if err != nil {
957+ return err
958+ }
959+ privkeyFilename := filepath.Join(dir, clientKeyName)
960+ if err = ioutil.WriteFile(privkeyFilename, []byte(private), 0600); err != nil {
961+ return err
962+ }
963+ if err := ioutil.WriteFile(privkeyFilename+".pub", []byte(public), 0600); err != nil {
964+ os.Remove(privkeyFilename)
965+ return err
966+ }
967+ clientPrivateKeys = []ssh.Signer{clientPrivateKey}
968+ return nil
969+}
970+
971+func loadClientKeys(dir string) error {
972+ publicKeyFiles, err := clientPublicKeyFiles(dir)
973+ if err != nil {
974+ return err
975+ }
976+ privateKeyFiles := privateKeyFiles(publicKeyFiles)
977+ clientPrivateKeysLocal := make([]ssh.Signer, len(privateKeyFiles))
978+ for i, filename := range privateKeyFiles {
979+ data, err := ioutil.ReadFile(filename)
980+ if err != nil {
981+ return err
982+ }
983+ clientPrivateKeysLocal[i], err = ssh.ParsePrivateKey(data)
984+ if err != nil {
985+ return fmt.Errorf("parsing key file %q: %v", filename, err)
986+ }
987+ }
988+ clientPrivateKeys = clientPrivateKeysLocal
989+ return nil
990+}
991+
992+// getClientPrivateKeys returns the private keys in the client key dir.
993+func getClientPrivateKeys() (signers []ssh.Signer) {
994+ clientKeyMutex.Lock()
995+ signers = append(signers, clientPrivateKeys...)
996+ clientKeyMutex.Unlock()
997+ return signers
998+}
999+
1000+// PrivateKeyFiles returns the filenames of private SSH keys for the
1001+// current user. The private keys are made up of the files with
1002+// a corresponding public key (i.e. the private key filename + ".pub").
1003+func PrivateKeyFiles() ([]string, error) {
1004+ pubkeys, err := PublicKeyFiles()
1005+ if err != nil {
1006+ return nil, err
1007+ }
1008+ return privateKeyFiles(pubkeys), nil
1009+}
1010+
1011+func privateKeyFiles(pubkeys []string) []string {
1012+ privkeys := make([]string, 0, len(pubkeys))
1013+ for _, pubkey := range pubkeys {
1014+ privkey := pubkey[:len(pubkey)-len(".pub")]
1015+ if _, err := os.Stat(privkey); err == nil {
1016+ privkeys = append(privkeys, privkey)
1017+ }
1018+ }
1019+ return privkeys
1020+}
1021+
1022+// PublicKeyFiles returns the filenames of the public SSH keys for the
1023+// current user. The public keys are made up of the files
1024+// ~/.ssh/{id_rsa,id_dsa,identity}.pub, as well as *.pub in the client
1025+// key directory initialised with InitClientKeyDir ($JUJU_HOME/ssh).
1026+func PublicKeyFiles() ([]string, error) {
1027+ var keys []string
1028+ sshDir, err := utils.NormalizePath("~/.ssh")
1029+ if err != nil {
1030+ return nil, err
1031+ }
1032+ for _, file := range []string{"id_dsa.pub", "id_rsa.pub", "identity.pub"} {
1033+ key := filepath.Join(sshDir, file)
1034+ if _, err := os.Stat(key); err == nil {
1035+ keys = append(keys, key)
1036+ }
1037+ }
1038+ clientKeyMutex.Lock()
1039+ defer clientKeyMutex.Unlock()
1040+ keys2, err := clientPublicKeyFiles(clientKeyDir)
1041+ if err != nil {
1042+ return nil, err
1043+ }
1044+ return append(keys, keys2...), nil
1045+}
1046+
1047+// clientPublicKeyFiles returns the filenames of public SSH keys
1048+// in the specified directory.
1049+func clientPublicKeyFiles(clientKeyDir string) ([]string, error) {
1050+ if clientKeyDir == "" {
1051+ return nil, nil
1052+ }
1053+ var keys []string
1054+ dir, err := os.Open(clientKeyDir)
1055+ if err != nil {
1056+ return nil, err
1057+ }
1058+ names, err := dir.Readdirnames(-1)
1059+ dir.Close()
1060+ if err != nil {
1061+ return nil, err
1062+ }
1063+ for _, name := range names {
1064+ if strings.HasSuffix(name, ".pub") {
1065+ keys = append(keys, filepath.Join(dir.Name(), name))
1066+ }
1067+ }
1068+ return keys, nil
1069+}
1070
1071=== added file 'utils/ssh/clientkeys_test.go'
1072--- utils/ssh/clientkeys_test.go 1970-01-01 00:00:00 +0000
1073+++ utils/ssh/clientkeys_test.go 2014-01-07 06:33:34 +0000
1074@@ -0,0 +1,139 @@
1075+// Copyright 2013 Canonical Ltd.
1076+// Licensed under the AGPLv3, see LICENCE file for details.
1077+
1078+package ssh_test
1079+
1080+import (
1081+ "io/ioutil"
1082+ "os"
1083+ "path/filepath"
1084+
1085+ gc "launchpad.net/gocheck"
1086+
1087+ "launchpad.net/juju-core/testing"
1088+ jc "launchpad.net/juju-core/testing/checkers"
1089+ "launchpad.net/juju-core/testing/testbase"
1090+ "launchpad.net/juju-core/utils"
1091+ "launchpad.net/juju-core/utils/ssh"
1092+)
1093+
1094+var defaultIdentities = []string{"~/.ssh/identity", "~/.ssh/id_rsa", "~/.ssh/id_dsa"}
1095+
1096+type ClientKeysSuite struct {
1097+ testbase.LoggingSuite
1098+}
1099+
1100+var _ = gc.Suite(&ClientKeysSuite{})
1101+
1102+func (s *ClientKeysSuite) SetUpTest(c *gc.C) {
1103+ s.LoggingSuite.SetUpTest(c)
1104+ fakeHome := testing.MakeFakeHomeNoEnvironments(c)
1105+ s.AddCleanup(func(*gc.C) { fakeHome.Restore() })
1106+ s.AddCleanup(func(*gc.C) { ssh.InitClientKeyDir("") })
1107+
1108+ // fake home is created with a public key file; kill it.
1109+ err := os.RemoveAll(testing.HomePath(".ssh"))
1110+ c.Assert(err, gc.IsNil)
1111+ err = os.RemoveAll(testing.HomePath("juju", "ssh"))
1112+ c.Assert(err, gc.IsNil)
1113+}
1114+
1115+// createEmptyFile creates an empty file, relative to the fake home.
1116+func createEmptyFile(c *gc.C, file string) string {
1117+ file, err := utils.NormalizePath(file)
1118+ c.Assert(err, gc.IsNil)
1119+ err = os.MkdirAll(filepath.Dir(file), 0700)
1120+ c.Assert(err, gc.IsNil)
1121+ err = ioutil.WriteFile(file, nil, 0600)
1122+ c.Assert(err, gc.IsNil)
1123+ return file
1124+}
1125+
1126+func checkFiles(c *gc.C, obtained, expected []string) {
1127+ var err error
1128+ for i, e := range expected {
1129+ expected[i], err = utils.NormalizePath(e)
1130+ c.Assert(err, gc.IsNil)
1131+ }
1132+ c.Assert(obtained, jc.SameContents, expected)
1133+}
1134+
1135+func checkPublicKeyFiles(c *gc.C, expected ...string) {
1136+ keys, err := ssh.PublicKeyFiles()
1137+ c.Assert(err, gc.IsNil)
1138+ checkFiles(c, keys, expected)
1139+}
1140+
1141+func checkPrivateKeyFiles(c *gc.C, expected ...string) {
1142+ keys, err := ssh.PrivateKeyFiles()
1143+ c.Assert(err, gc.IsNil)
1144+ checkFiles(c, keys, expected)
1145+}
1146+
1147+func (s *ClientKeysSuite) TestDefaultIdentities(c *gc.C) {
1148+ // No .ssh dir, no .juju/ssh dir: no keys.
1149+ checkPrivateKeyFiles(c)
1150+ checkPublicKeyFiles(c)
1151+
1152+ // Only three whitelisted filenames get picked up from
1153+ // ~/.ssh: identity.pub, id_rsa.pub, and id_dsa.pub.
1154+ expectedPrivate := make([]string, len(defaultIdentities))
1155+ expectedPublic := make([]string, len(defaultIdentities))
1156+ for i, ident := range defaultIdentities {
1157+ expectedPrivate[i] = ident
1158+ expectedPublic[i] = ident + ".pub"
1159+ createEmptyFile(c, expectedPrivate[i])
1160+ createEmptyFile(c, expectedPublic[i])
1161+ }
1162+ createEmptyFile(c, "~/.ssh/identity2.pub")
1163+ checkPublicKeyFiles(c, expectedPublic...)
1164+ checkPrivateKeyFiles(c, expectedPrivate...)
1165+}
1166+
1167+func (s *ClientKeysSuite) TestPublicKeyFiles(c *gc.C) {
1168+ expected := make([]string, len(defaultIdentities))
1169+ for i, ident := range defaultIdentities {
1170+ expected[i] = ident + ".pub"
1171+ createEmptyFile(c, expected[i])
1172+ }
1173+ checkPublicKeyFiles(c, expected...)
1174+
1175+ // InitClientKeyDir will create the specified directory
1176+ // and populate it with a key pair.
1177+ err := ssh.InitClientKeyDir("~/.juju/ssh")
1178+ c.Assert(err, gc.IsNil)
1179+ expected = append(expected, "~/.juju/ssh/juju_id_rsa.pub")
1180+ checkPublicKeyFiles(c, expected...)
1181+
1182+ // All files ending with .pub in the client key dir get picked up.
1183+ createEmptyFile(c, "~/.juju/ssh/identity2.pub")
1184+ expected = append(expected, "~/.juju/ssh/identity2.pub")
1185+ checkPublicKeyFiles(c, expected...)
1186+}
1187+
1188+func (s *ClientKeysSuite) TestPrivateKeyFiles(c *gc.C) {
1189+ // Create various public keys. Then, go through each
1190+ // and create the corresponding private key, verifying
1191+ // that PrivateKeyFiles returns them.
1192+ for _, ident := range defaultIdentities {
1193+ createEmptyFile(c, ident+".pub")
1194+ }
1195+ err := ssh.InitClientKeyDir("~/.juju/ssh")
1196+ c.Assert(err, gc.IsNil)
1197+ err = os.Remove(testing.HomePath(".juju", "ssh", "juju_id_rsa"))
1198+ c.Assert(err, gc.IsNil)
1199+ createEmptyFile(c, "~/.juju/ssh/identity2.pub")
1200+
1201+ // No private keys yet.
1202+ checkPrivateKeyFiles(c)
1203+
1204+ pubkeys, err := ssh.PublicKeyFiles()
1205+ c.Assert(err, gc.IsNil)
1206+ expected := make([]string, 0, len(pubkeys))
1207+ for _, pubkey := range pubkeys {
1208+ privkey := pubkey[:len(pubkey)-len(".pub")]
1209+ createEmptyFile(c, privkey)
1210+ expected = append(expected, privkey)
1211+ }
1212+ checkPrivateKeyFiles(c, expected...)
1213+}
1214
1215=== modified file 'utils/ssh/ssh.go'
1216--- utils/ssh/ssh.go 2013-12-17 08:43:06 +0000
1217+++ utils/ssh/ssh.go 2014-01-07 06:33:34 +0000
1218@@ -4,69 +4,152 @@
1219 // Package ssh contains utilities for dealing with SSH connections,
1220 // key management, and so on. All SSH-based command executions in
1221 // Juju should use the Command/ScpCommand functions in this package.
1222-//
1223-// TODO(axw) use PuTTY/plink if it's available on Windows.
1224-// TODO(axw) fallback to go.crypto/ssh if no native client is available.
1225 package ssh
1226
1227 import (
1228- "os"
1229- "os/exec"
1230-)
1231-
1232-type Option []string
1233-
1234-var (
1235- commonOptions Option = []string{"-o", "StrictHostKeyChecking no"}
1236-
1237- // AllocateTTY forces pseudo-TTY allocation, which is required,
1238- // for example, for sudo password prompts on the target host.
1239- AllocateTTY Option = []string{"-t"}
1240-
1241- // NoPasswordAuthentication disallows password-based authentication.
1242- NoPasswordAuthentication Option = []string{"-o", "PasswordAuthentication no"}
1243-)
1244-
1245-// sshpassWrap wraps the command/args with sshpass if it is found in $PATH
1246-// and the SSHPASS environment variable is set. Otherwise, the original
1247-// command/args are returned.
1248-func sshpassWrap(cmd string, args []string) (string, []string) {
1249- if os.Getenv("SSHPASS") != "" {
1250- if path, err := exec.LookPath("sshpass"); err == nil {
1251- return path, append([]string{"-e", cmd}, args...)
1252- }
1253- }
1254- return cmd, args
1255-}
1256-
1257-// Command initialises an os/exec.Cmd to execute the native ssh program.
1258-//
1259-// If the SSHPASS environment variable is set, and the sshpass program
1260-// is available in $PATH, then the ssh command will be run with "sshpass -e".
1261-func Command(host string, command []string, options ...Option) *exec.Cmd {
1262- args := append([]string{}, commonOptions...)
1263- for _, option := range options {
1264- args = append(args, option...)
1265- }
1266- args = append(args, host)
1267- if len(command) > 0 {
1268- args = append(args, "--")
1269- args = append(args, command...)
1270- }
1271- bin, args := sshpassWrap("ssh", args)
1272- return exec.Command(bin, args...)
1273-}
1274-
1275-// ScpCommand initialises an os/exec.Cmd to execute the native scp program.
1276-//
1277-// If the SSHPASS environment variable is set, and the sshpass program
1278-// is available in $PATH, then the scp command will be run with "sshpass -e".
1279-func ScpCommand(source, destination string, options ...Option) *exec.Cmd {
1280- args := append([]string{}, commonOptions...)
1281- for _, option := range options {
1282- args = append(args, option...)
1283- }
1284- args = append(args, source, destination)
1285- bin, args := sshpassWrap("scp", args)
1286- return exec.Command(bin, args...)
1287+ "bytes"
1288+ "errors"
1289+ "io"
1290+)
1291+
1292+// Options is a client-implementation independent SSH options set.
1293+type Options struct {
1294+ allocatePTY bool
1295+ passwordAuthDisabled bool
1296+}
1297+
1298+// EnablePTY forces the allocation of a pseudo-TTY.
1299+//
1300+// Forcing a pseudo-TTY is required, for example, for sudo
1301+// prompts on the target host.
1302+func (o *Options) EnablePTY() {
1303+ o.allocatePTY = true
1304+}
1305+
1306+// DisablePasswordAuthentication prevents the SSH
1307+// client from prompting the user for a password.
1308+func (o *Options) DisablePasswordAuthentication() {
1309+ o.passwordAuthDisabled = true
1310+}
1311+
1312+// Client is an interface for SSH clients to implement
1313+type Client interface {
1314+ // Command returns a Command for executing a command
1315+ // on the specified host. Each Command is executed
1316+ // within its own SSH session.
1317+ Command(host string, command []string, options *Options) *Cmd
1318+
1319+ // Copy copies a file between the local host and
1320+ // target host. Paths are specified in the scp format,
1321+ // [[user@]host:]path.
1322+ Copy(source, dest string, options *Options) error
1323+}
1324+
1325+type Cmd struct {
1326+ Stdin io.Reader
1327+ Stdout io.Writer
1328+ Stderr io.Writer
1329+ impl command
1330+}
1331+
1332+func (c *Cmd) CombinedOutput() ([]byte, error) {
1333+ if c.Stdout != nil {
1334+ return nil, errors.New("ssh: Stdout already set")
1335+ }
1336+ if c.Stderr != nil {
1337+ return nil, errors.New("ssh: Stderr already set")
1338+ }
1339+ var b bytes.Buffer
1340+ c.Stdout = &b
1341+ c.Stderr = &b
1342+ err := c.Run()
1343+ return b.Bytes(), err
1344+}
1345+
1346+func (c *Cmd) Output() ([]byte, error) {
1347+ if c.Stdout != nil {
1348+ return nil, errors.New("ssh: Stdout already set")
1349+ }
1350+ var b bytes.Buffer
1351+ c.Stdout = &b
1352+ err := c.Run()
1353+ return b.Bytes(), err
1354+}
1355+
1356+func (c *Cmd) Run() error {
1357+ if err := c.Start(); err != nil {
1358+ return err
1359+ }
1360+ return c.Wait()
1361+}
1362+
1363+func (c *Cmd) Start() error {
1364+ c.impl.SetStdio(c.Stdin, c.Stdout, c.Stderr)
1365+ return c.impl.Start()
1366+}
1367+
1368+func (c *Cmd) Wait() error {
1369+ return c.impl.Wait()
1370+}
1371+
1372+func (c *Cmd) StdinPipe() (io.WriteCloser, error) {
1373+ wc, r, err := c.impl.StdinPipe()
1374+ if err != nil {
1375+ return nil, err
1376+ }
1377+ c.Stdin = r
1378+ return wc, nil
1379+}
1380+
1381+func (c *Cmd) StdoutPipe() (io.ReadCloser, error) {
1382+ rc, w, err := c.impl.StdoutPipe()
1383+ if err != nil {
1384+ return nil, err
1385+ }
1386+ c.Stdout = w
1387+ return rc, nil
1388+}
1389+
1390+func (c *Cmd) StderrPipe() (io.ReadCloser, error) {
1391+ rc, w, err := c.impl.StderrPipe()
1392+ if err != nil {
1393+ return nil, err
1394+ }
1395+ c.Stderr = w
1396+ return rc, nil
1397+}
1398+
1399+// command is an implementation-specific representation of a
1400+// command prepared to execute against a specific host.
1401+type command interface {
1402+ Start() error
1403+ Wait() error
1404+ SetStdio(stdin io.Reader, stdout, stderr io.Writer)
1405+ StdinPipe() (io.WriteCloser, io.Reader, error)
1406+ StdoutPipe() (io.ReadCloser, io.Writer, error)
1407+ StderrPipe() (io.ReadCloser, io.Writer, error)
1408+}
1409+
1410+// DefaultClient is the default SSH client for the process.
1411+//
1412+// If OpenSSH is available, that will be used. Otherwise,
1413+// the embedded go.crypto/ssh client will be used. We
1414+// We cannot use PuTTY due to reliance on disabling strict
1415+// host-key checking.
1416+var DefaultClient Client
1417+
1418+func init() {
1419+ if client, err := NewOpenSSHClient(); err == nil {
1420+ DefaultClient = client
1421+ } else if client, err := NewGoCryptoClient(); err == nil {
1422+ DefaultClient = client
1423+ }
1424+}
1425+
1426+func Command(host string, command []string, options *Options) *Cmd {
1427+ return DefaultClient.Command(host, command, options)
1428+}
1429+
1430+func Copy(source, dest string, options *Options) error {
1431+ return DefaultClient.Copy(source, dest, options)
1432 }
1433
1434=== added file 'utils/ssh/ssh_gocrypto.go'
1435--- utils/ssh/ssh_gocrypto.go 1970-01-01 00:00:00 +0000
1436+++ utils/ssh/ssh_gocrypto.go 2014-01-07 06:33:34 +0000
1437@@ -0,0 +1,197 @@
1438+// Copyright 2013 Canonical Ltd.
1439+// Licensed under the AGPLv3, see LICENCE file for details.
1440+
1441+package ssh
1442+
1443+import (
1444+ "fmt"
1445+ "io"
1446+ "io/ioutil"
1447+ "os/user"
1448+ "strings"
1449+
1450+ "code.google.com/p/go.crypto/ssh"
1451+
1452+ "launchpad.net/juju-core/utils"
1453+)
1454+
1455+// GoCryptoClient is an implementation of Client that
1456+// uses the embedded go.crypto/ssh SSH client.
1457+//
1458+// GoCryptoClient is intentionally limited in the
1459+// functionality that it enables, as it is currently
1460+// intended to be used only for non-interactive command
1461+// execution.
1462+type GoCryptoClient struct {
1463+ signers []ssh.Signer
1464+}
1465+
1466+// NewGoCryptoClient creates a new GoCryptoClient.
1467+//
1468+// If no signers are specified, NewGoCryptoClient will
1469+// use the private key generated by InitClientKeyDir.
1470+func NewGoCryptoClient(signers ...ssh.Signer) (*GoCryptoClient, error) {
1471+ var c GoCryptoClient
1472+ c.signers = signers
1473+ return &c, nil
1474+}
1475+
1476+func (c *GoCryptoClient) Command(host string, command []string, options *Options) *Cmd {
1477+ shellCommand := utils.CommandString(command...)
1478+ return &Cmd{impl: &goCryptoCommand{
1479+ signers: c.signers,
1480+ host: host,
1481+ command: shellCommand,
1482+ }}
1483+}
1484+
1485+func (c *GoCryptoClient) Copy(source, dest string, options *Options) error {
1486+ return fmt.Errorf("Copy not implemented")
1487+}
1488+
1489+type goCryptoCommand struct {
1490+ signers []ssh.Signer
1491+ host string
1492+ command string
1493+ stdin io.Reader
1494+ stdout io.Writer
1495+ stderr io.Writer
1496+ conn *ssh.ClientConn
1497+ sess *ssh.Session
1498+}
1499+
1500+func (c *goCryptoCommand) ensureSession() (*ssh.Session, error) {
1501+ if c.sess != nil {
1502+ return c.sess, nil
1503+ }
1504+ username, host := splitUserHost(c.host)
1505+ if username == "" {
1506+ currentUser, err := user.Current()
1507+ if err != nil {
1508+ return nil, fmt.Errorf("getting current user: %v", err)
1509+ }
1510+ username = currentUser.Username
1511+ }
1512+ if len(c.signers) == 0 {
1513+ c.signers = getClientPrivateKeys()
1514+ if len(c.signers) == 0 {
1515+ return nil, fmt.Errorf("client private key not set")
1516+ }
1517+ }
1518+ config := &ssh.ClientConfig{
1519+ User: username,
1520+ Auth: []ssh.ClientAuth{
1521+ ssh.ClientAuthKeyring(keyring{c.signers}),
1522+ },
1523+ }
1524+ conn, err := ssh.Dial("tcp", host+":22", config)
1525+ if err != nil {
1526+ return nil, err
1527+ }
1528+ sess, err := conn.NewSession()
1529+ if err != nil {
1530+ conn.Close()
1531+ return nil, err
1532+ }
1533+ c.conn = conn
1534+ c.sess = sess
1535+ c.sess.Stdin = c.stdin
1536+ c.sess.Stdout = c.stdout
1537+ c.sess.Stderr = c.stderr
1538+ return sess, nil
1539+}
1540+
1541+func (c *goCryptoCommand) Start() error {
1542+ sess, err := c.ensureSession()
1543+ if err != nil {
1544+ return err
1545+ }
1546+ if c.command == "" {
1547+ return sess.Shell()
1548+ }
1549+ return sess.Start(c.command)
1550+}
1551+
1552+func (c *goCryptoCommand) Close() error {
1553+ if c.sess == nil {
1554+ return nil
1555+ }
1556+ err0 := c.sess.Close()
1557+ err1 := c.conn.Close()
1558+ if err0 == nil {
1559+ err0 = err1
1560+ }
1561+ c.sess = nil
1562+ c.conn = nil
1563+ return err0
1564+}
1565+
1566+func (c *goCryptoCommand) Wait() error {
1567+ if c.sess == nil {
1568+ return fmt.Errorf("Command has not been started")
1569+ }
1570+ err := c.sess.Wait()
1571+ c.Close()
1572+ return err
1573+}
1574+
1575+func (c *goCryptoCommand) SetStdio(stdin io.Reader, stdout, stderr io.Writer) {
1576+ c.stdin = stdin
1577+ c.stdout = stdout
1578+ c.stderr = stderr
1579+}
1580+
1581+func (c *goCryptoCommand) StdinPipe() (io.WriteCloser, io.Reader, error) {
1582+ sess, err := c.ensureSession()
1583+ if err != nil {
1584+ return nil, nil, err
1585+ }
1586+ wc, err := sess.StdinPipe()
1587+ return wc, sess.Stdin, err
1588+}
1589+
1590+func (c *goCryptoCommand) StdoutPipe() (io.ReadCloser, io.Writer, error) {
1591+ sess, err := c.ensureSession()
1592+ if err != nil {
1593+ return nil, nil, err
1594+ }
1595+ wc, err := sess.StdoutPipe()
1596+ return ioutil.NopCloser(wc), sess.Stdout, err
1597+}
1598+
1599+func (c *goCryptoCommand) StderrPipe() (io.ReadCloser, io.Writer, error) {
1600+ sess, err := c.ensureSession()
1601+ if err != nil {
1602+ return nil, nil, err
1603+ }
1604+ wc, err := sess.StderrPipe()
1605+ return ioutil.NopCloser(wc), sess.Stderr, err
1606+}
1607+
1608+// keyring implements ssh.ClientKeyring
1609+type keyring struct {
1610+ signers []ssh.Signer
1611+}
1612+
1613+func (k keyring) Key(i int) (ssh.PublicKey, error) {
1614+ if i < 0 || i >= len(k.signers) {
1615+ // nil key marks the end of the keyring; must not return an error.
1616+ return nil, nil
1617+ }
1618+ return k.signers[i].PublicKey(), nil
1619+}
1620+
1621+func (k keyring) Sign(i int, rand io.Reader, data []byte) ([]byte, error) {
1622+ if i < 0 || i >= len(k.signers) {
1623+ return nil, fmt.Errorf("no key at position %d", i)
1624+ }
1625+ return k.signers[i].Sign(rand, data)
1626+}
1627+
1628+func splitUserHost(s string) (user, host string) {
1629+ userHost := strings.SplitN(s, "@", 2)
1630+ if len(userHost) == 2 {
1631+ return userHost[0], userHost[1]
1632+ }
1633+ return "", userHost[0]
1634+}
1635
1636=== added file 'utils/ssh/ssh_openssh.go'
1637--- utils/ssh/ssh_openssh.go 1970-01-01 00:00:00 +0000
1638+++ utils/ssh/ssh_openssh.go 2014-01-07 06:33:34 +0000
1639@@ -0,0 +1,180 @@
1640+// Copyright 2013 Canonical Ltd.
1641+// Licensed under the AGPLv3, see LICENCE file for details.
1642+
1643+package ssh
1644+
1645+import (
1646+ "bytes"
1647+ "fmt"
1648+ "io"
1649+ "os"
1650+ "os/exec"
1651+ "strings"
1652+)
1653+
1654+var opensshCommonOptions = []string{"-o", "StrictHostKeyChecking no"}
1655+
1656+// OpenSSHClient is an implementation of Client that
1657+// uses the ssh and scp executables found in $PATH.
1658+type OpenSSHClient struct{}
1659+
1660+func NewOpenSSHClient() (*OpenSSHClient, error) {
1661+ var c OpenSSHClient
1662+ if _, err := exec.LookPath("ssh"); err != nil {
1663+ return nil, err
1664+ }
1665+ if _, err := exec.LookPath("scp"); err != nil {
1666+ return nil, err
1667+ }
1668+ return &c, nil
1669+}
1670+
1671+// sshpassWrap wraps the command/args with sshpass if it is found in $PATH
1672+// and the SSHPASS environment variable is set. Otherwise, the original
1673+// command/args are returned.
1674+func sshpassWrap(cmd string, args []string) (string, []string) {
1675+ if os.Getenv("SSHPASS") != "" {
1676+ if path, err := exec.LookPath("sshpass"); err == nil {
1677+ return path, append([]string{"-e", cmd}, args...)
1678+ }
1679+ }
1680+ return cmd, args
1681+}
1682+
1683+func opensshOptions(options *Options) []string {
1684+ args := append([]string{}, opensshCommonOptions...)
1685+ if options == nil {
1686+ return args
1687+ }
1688+ if options.passwordAuthDisabled {
1689+ args = append(args, "-o", "PasswordAuthentication no")
1690+ }
1691+ if options.allocatePTY {
1692+ args = append(args, "-t")
1693+ }
1694+ return args
1695+}
1696+
1697+func identityArgs() ([]string, error) {
1698+ ids, err := PrivateKeyFiles()
1699+ if err != nil {
1700+ return nil, err
1701+ }
1702+ args := make([]string, len(ids)*2)
1703+ for i, id := range ids {
1704+ args[2*i] = "-i"
1705+ args[2*i+1] = id
1706+ }
1707+ return args, nil
1708+}
1709+
1710+func (c *OpenSSHClient) Command(host string, command []string, options *Options) *Cmd {
1711+ args := opensshOptions(options)
1712+ args = append(args, host)
1713+ if len(command) > 0 {
1714+ args = append(args, "--")
1715+ args = append(args, command...)
1716+ }
1717+ return &Cmd{impl: &opensshCmd{args: args}}
1718+}
1719+
1720+func (c *OpenSSHClient) Copy(source, dest string, options *Options) error {
1721+ args := opensshOptions(options)
1722+ idArgs, err := identityArgs()
1723+ if err != nil {
1724+ return err
1725+ }
1726+ args = append(args, idArgs...)
1727+ args = append(args, source, dest)
1728+ bin, args := sshpassWrap("scp", args)
1729+ cmd := exec.Command(bin, args...)
1730+ var stderr bytes.Buffer
1731+ cmd.Stderr = &stderr
1732+ if err := cmd.Run(); err != nil {
1733+ stderr := strings.TrimSpace(stderr.String())
1734+ if len(stderr) > 0 {
1735+ err = fmt.Errorf("%v (%v)", err, stderr)
1736+ }
1737+ return err
1738+ }
1739+ return nil
1740+}
1741+
1742+type opensshCmd struct {
1743+ *exec.Cmd
1744+
1745+ args []string
1746+ stdin io.Reader
1747+ stdout io.Writer
1748+ stderr io.Writer
1749+}
1750+
1751+func (c *opensshCmd) ensureCmd() error {
1752+ if c.Cmd != nil {
1753+ return nil
1754+ }
1755+ idArgs, err := identityArgs()
1756+ if err != nil {
1757+ return err
1758+ }
1759+ c.args = append(idArgs, c.args...)
1760+ bin, args := sshpassWrap("ssh", c.args)
1761+ c.Cmd = exec.Command(bin, args...)
1762+ return nil
1763+}
1764+
1765+func (c *opensshCmd) Start() error {
1766+ if err := c.ensureCmd(); err != nil {
1767+ return err
1768+ }
1769+ c.Cmd.Stdin = c.stdin
1770+ c.Cmd.Stdout = c.stdout
1771+ c.Cmd.Stderr = c.stderr
1772+ return c.Cmd.Start()
1773+}
1774+
1775+func (c *opensshCmd) Wait() error {
1776+ if err := c.ensureCmd(); err != nil {
1777+ return err
1778+ }
1779+ return c.Cmd.Wait()
1780+}
1781+
1782+func (c *opensshCmd) SetStdio(stdin io.Reader, stdout, stderr io.Writer) {
1783+ c.stdin = stdin
1784+ c.stdout = stdout
1785+ c.stderr = stderr
1786+}
1787+
1788+func (c *opensshCmd) StdinPipe() (io.WriteCloser, io.Reader, error) {
1789+ if err := c.ensureCmd(); err != nil {
1790+ return nil, nil, err
1791+ }
1792+ wc, err := c.Cmd.StdinPipe()
1793+ if err != nil {
1794+ return nil, nil, err
1795+ }
1796+ return wc, c.Stdin, nil
1797+}
1798+
1799+func (c *opensshCmd) StdoutPipe() (io.ReadCloser, io.Writer, error) {
1800+ if err := c.ensureCmd(); err != nil {
1801+ return nil, nil, err
1802+ }
1803+ rc, err := c.Cmd.StdoutPipe()
1804+ if err != nil {
1805+ return nil, nil, err
1806+ }
1807+ return rc, c.Stdout, nil
1808+}
1809+
1810+func (c *opensshCmd) StderrPipe() (io.ReadCloser, io.Writer, error) {
1811+ if err := c.ensureCmd(); err != nil {
1812+ return nil, nil, err
1813+ }
1814+ rc, err := c.Cmd.StderrPipe()
1815+ if err != nil {
1816+ return nil, nil, err
1817+ }
1818+ return rc, c.Stderr, nil
1819+}
1820
1821=== modified file 'utils/ssh/ssh_test.go'
1822--- utils/ssh/ssh_test.go 2013-12-17 08:43:06 +0000
1823+++ utils/ssh/ssh_test.go 2014-01-07 06:33:34 +0000
1824@@ -7,9 +7,11 @@
1825 "io/ioutil"
1826 "os"
1827 "path/filepath"
1828+ "strings"
1829
1830 gc "launchpad.net/gocheck"
1831
1832+ "launchpad.net/juju-core/testing"
1833 "launchpad.net/juju-core/testing/testbase"
1834 "launchpad.net/juju-core/utils/ssh"
1835 )
1836@@ -18,49 +20,78 @@
1837 testbase.LoggingSuite
1838 testbin string
1839 fakessh string
1840+ fakescp string
1841+ client ssh.Client
1842 }
1843
1844+const echoCommandScript = "#!/bin/sh\necho $0 $*"
1845+
1846 var _ = gc.Suite(&SSHCommandSuite{})
1847
1848 func (s *SSHCommandSuite) SetUpTest(c *gc.C) {
1849 s.LoggingSuite.SetUpTest(c)
1850+ fakeHome := testing.MakeFakeHomeNoEnvironments(c)
1851+ s.AddCleanup(func(*gc.C) { fakeHome.Restore() })
1852+ s.AddCleanup(func(*gc.C) { ssh.InitClientKeyDir("") })
1853 s.testbin = c.MkDir()
1854 s.fakessh = filepath.Join(s.testbin, "ssh")
1855- err := ioutil.WriteFile(s.fakessh, nil, 0755)
1856+ s.fakescp = filepath.Join(s.testbin, "scp")
1857+ err := ioutil.WriteFile(s.fakessh, []byte(echoCommandScript), 0755)
1858+ c.Assert(err, gc.IsNil)
1859+ err = ioutil.WriteFile(s.fakescp, []byte(echoCommandScript), 0755)
1860 c.Assert(err, gc.IsNil)
1861 s.PatchEnvironment("PATH", s.testbin)
1862+ s.client, err = ssh.NewOpenSSHClient()
1863+ c.Assert(err, gc.IsNil)
1864 }
1865
1866 func (s *SSHCommandSuite) TestCommand(c *gc.C) {
1867- s.assertCommandArgs(c, "localhost", []string{"echo", "123"}, []string{
1868- "ssh", "-o", "StrictHostKeyChecking no", "localhost", "--", "echo", "123",
1869- })
1870+ s.assertCommandArgs(c, "localhost", []string{"echo", "123"},
1871+ s.fakessh+" -o StrictHostKeyChecking no localhost -- echo 123",
1872+ )
1873 }
1874
1875-func (s *SSHCommandSuite) assertCommandArgs(c *gc.C, hostname string, command []string, expected []string) {
1876- cmd := ssh.Command("localhost", []string{"echo", "123"})
1877+func (s *SSHCommandSuite) assertCommandArgs(c *gc.C, hostname string, command []string, expected string) {
1878+ cmd := s.client.Command("localhost", []string{"echo", "123"}, nil)
1879 c.Assert(cmd, gc.NotNil)
1880- c.Assert(cmd.Args, gc.DeepEquals, expected)
1881+ out, err := cmd.Output()
1882+ c.Assert(err, gc.IsNil)
1883+ c.Assert(strings.TrimSpace(string(out)), gc.Equals, expected)
1884 }
1885
1886 func (s *SSHCommandSuite) TestCommandSSHPass(c *gc.C) {
1887 // First create a fake sshpass, but don't set SSHPASS
1888 fakesshpass := filepath.Join(s.testbin, "sshpass")
1889- err := ioutil.WriteFile(fakesshpass, nil, 0755)
1890- s.assertCommandArgs(c, "localhost", []string{"echo", "123"}, []string{
1891- "ssh", "-o", "StrictHostKeyChecking no", "localhost", "--", "echo", "123",
1892- })
1893+ err := ioutil.WriteFile(fakesshpass, []byte(echoCommandScript), 0755)
1894+ s.assertCommandArgs(c, "localhost", []string{"echo", "123"},
1895+ s.fakessh+" -o StrictHostKeyChecking no localhost -- echo 123",
1896+ )
1897
1898 // Now set SSHPASS.
1899 s.PatchEnvironment("SSHPASS", "anyoldthing")
1900- s.assertCommandArgs(c, "localhost", []string{"echo", "123"}, []string{
1901- fakesshpass, "-e", "ssh", "-o", "StrictHostKeyChecking no", "localhost", "--", "echo", "123",
1902- })
1903+ s.assertCommandArgs(c, "localhost", []string{"echo", "123"},
1904+ fakesshpass+" -e ssh -o StrictHostKeyChecking no localhost -- echo 123",
1905+ )
1906
1907 // Finally, remove sshpass from $PATH.
1908 err = os.Remove(fakesshpass)
1909 c.Assert(err, gc.IsNil)
1910- s.assertCommandArgs(c, "localhost", []string{"echo", "123"}, []string{
1911- "ssh", "-o", "StrictHostKeyChecking no", "localhost", "--", "echo", "123",
1912- })
1913+ s.assertCommandArgs(c, "localhost", []string{"echo", "123"},
1914+ s.fakessh+" -o StrictHostKeyChecking no localhost -- echo 123",
1915+ )
1916+}
1917+
1918+func (s *SSHCommandSuite) TestCommandSSHIdentities(c *gc.C) {
1919+ // No keys? No -i args.
1920+ s.assertCommandArgs(c, "localhost", []string{"echo", "123"},
1921+ s.fakessh+" -o StrictHostKeyChecking no localhost -- echo 123",
1922+ )
1923+ // Otherwise, there are as many -i args as there are identity files.
1924+ id0 := createEmptyFile(c, "~/.ssh/id_rsa")
1925+ err := ssh.InitClientKeyDir("~/.juju/ssh")
1926+ c.Assert(err, gc.IsNil)
1927+ id1 := testing.HomePath(".juju", "ssh", "juju_id_rsa")
1928+ s.assertCommandArgs(c, "localhost", []string{"echo", "123"},
1929+ s.fakessh+" -i "+id0+" -i "+id1+" -o StrictHostKeyChecking no localhost -- echo 123",
1930+ )
1931 }
1932
1933=== modified file 'utils/trivial.go'
1934--- utils/trivial.go 2013-10-10 20:58:54 +0000
1935+++ utils/trivial.go 2014-01-07 06:33:34 +0000
1936@@ -61,6 +61,16 @@
1937 return `'` + strings.Replace(s, `'`, `'"'"'`, -1) + `'`
1938 }
1939
1940+// CommandString flattens a sequence of command arguments into a
1941+// string suitable for executing in a shell, escaping slashes and
1942+// quotes as necessary; each argument is single-quoted.
1943+func CommandString(args ...string) string {
1944+ for i, arg := range args {
1945+ args[i] = "'" + strings.Replace(arg, "'", "'\\''", -1) + "'"
1946+ }
1947+ return strings.Join(args, " ")
1948+}
1949+
1950 // Gzip compresses the given data.
1951 func Gzip(data []byte) []byte {
1952 var buf bytes.Buffer
1953
1954=== modified file 'utils/trivial_test.go'
1955--- utils/trivial_test.go 2013-11-05 05:40:47 +0000
1956+++ utils/trivial_test.go 2014-01-07 06:33:34 +0000
1957@@ -47,3 +47,24 @@
1958 c.Assert(err, gc.IsNil)
1959 c.Assert(data1, gc.DeepEquals, data)
1960 }
1961+
1962+func (utilsSuite) TestCommandString(c *gc.C) {
1963+ type test struct {
1964+ args []string
1965+ expected string
1966+ }
1967+ tests := []test{
1968+ {nil, ""},
1969+ {[]string{"a"}, "'a'"},
1970+ {[]string{"a", "'b'"}, "'a' ''\\''b'\\'''"},
1971+ {[]string{"a b"}, `'a b'`},
1972+ {[]string{"a", `"b"`}, `'a' '"b"'`},
1973+ {[]string{"a", `"b\"`}, `'a' '"b\"'`},
1974+ {[]string{"a\n"}, "'a\n'"},
1975+ }
1976+ for i, test := range tests {
1977+ c.Logf("test %d: %q", i, test.args)
1978+ result := utils.CommandString(test.args...)
1979+ c.Assert(result, gc.Equals, test.expected)
1980+ }
1981+}

Subscribers

People subscribed via source and target branches

to status/vote changes: