Merge lp:~axwalk/juju-core/ssh-gocrypto-client into lp:~go-bot/juju-core/trunk
- ssh-gocrypto-client
- Merge into trunk
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 |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Juju Engineering | Pending | ||
Review via email: mp+200628@code.launchpad.net |
Commit message
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/
is modified to use any of the default SSH
identities, or any of the ones found in the
client key directory.
Fixes #1263851
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.
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:/
File cloudinit/
https:/
cloudinit/
One thing I noticed was the need for a carriage return on the end.
Does the script end with a new line?
https:/
File environs/
https:/
environs/
Wondering if this function should perhaps be moved into utils/ssh?
https:/
File environs/
https:/
environs/
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:/
File environs/
https:/
environs/
[]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:/
File utils/ssh/ssh.go (right):
https:/
utils/ssh/
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:/
File utils/ssh/
https:/
utils/ssh/
string, command []string, options *Options) *Cmd {
Lots of public functions missing docs.
https:/
File utils/trivial.go (right):
https:/
utils/trivial.
single-quoted.
this isn't escaping slashes... is it?
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
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 | +} |
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): sshinit/ configure. go scp_test. go ssh_test. go config/ authkeys. go manual/ fakessh. go manual/ init.go manual/ init_test. go manual/ provisioner. go sshstorage/ storage. go sshstorage/ storage_ test.go testing/ bootstrap. go common/ bootstrap. go common/ bootstrap_ test.go authorisedkeys. go clientkeys. go clientkeys_ test.go ssh_gocrypto. go ssh_openssh. go ssh_test. go test.go
A [revision details]
M cloudinit/
M cmd/juju/scp.go
M cmd/juju/
M cmd/juju/ssh.go
M cmd/juju/
M environs/
M environs/
M environs/
M environs/
M environs/
M environs/
M environs/
M environs/
M juju/conn.go
M juju/conn_test.go
M provider/
M provider/
M utils/ssh/
A utils/ssh/
A utils/ssh/
M utils/ssh/ssh.go
A utils/ssh/
A utils/ssh/
M utils/ssh/
M utils/trivial.go
M utils/trivial_