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