Merge lp:~axwalk/juju-core/ssh-gocrypto-client-take2 into lp:~go-bot/juju-core/trunk
- ssh-gocrypto-client-take2
- Merge into trunk
Status: | Merged |
---|---|
Approved by: | Andrew Wilkins |
Approved revision: | no longer in the source branch. |
Merged at revision: | 2217 |
Proposed branch: | lp:~axwalk/juju-core/ssh-gocrypto-client-take2 |
Merge into: | lp:~go-bot/juju-core/trunk |
Diff against target: |
672 lines (+478/-13) 9 files modified
utils/ssh/clientkeys_test.go (+5/-6) utils/ssh/export_test.go (+1/-0) utils/ssh/ssh.go (+16/-3) utils/ssh/ssh_gocrypto.go (+217/-0) utils/ssh/ssh_gocrypto_test.go (+145/-0) utils/ssh/ssh_openssh.go (+19/-3) utils/ssh/ssh_test.go (+18/-1) utils/trivial.go (+33/-0) utils/trivial_test.go (+24/-0) |
To merge this branch: | bzr merge lp:~axwalk/juju-core/ssh-gocrypto-client-take2 |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Juju Engineering | Pending | ||
Review via email: mp+201706@code.launchpad.net |
Commit message
utils/ssh: introduce go.crypto/ssh client
Bootstrap now requires an ssh client. Users
may not have an SSH client available or
configured; this is typical of many Windows
users.
We introduce a go.crypto/ssh-based embedded
client that will be used if OpenSSH is not
available. The embedded client will use any
keys found in $JUJU_HOME/ssh.
Fixes #1263851
Description of the change
utils/ssh: introduce go.crypto/ssh client
Bootstrap now requires an ssh client. Users
may not have an SSH client available or
configured; this is typical of many Windows
users.
We introduce a go.crypto/ssh-based embedded
client that will be used if OpenSSH is not
available. The embedded client will use any
keys found in $JUJU_HOME/ssh.
Fixes #1263851
Andrew Wilkins (axwalk) wrote : | # |
Ian Booth (wallyworld) wrote : | # |
A few questions. I can't see any tests for fallback to the Go ssh client
if OpenSSH not available, nor for the Go ssh client wrapper itself. Does
it matter that copy is not implemented? I guess it's not used during
bootstrap so it doesn't need to be.
https:/
File utils/ssh/ssh.go (right):
https:/
utils/ssh/
OpenSSH.
This comment is now out of date
https:/
File utils/trivial_
https:/
utils/trivial_
There are a some test cases missing eg $
Andrew Wilkins (axwalk) wrote : | # |
I've added some tests for the go.crypto client, but they're not
complete. The problem is that the go.crypto *server* support is not all
there; it has been expanded in a development branch, but it's not quite
ready for consumption yet. I've left a TODO to expand the tests when it
is.
https:/
File utils/ssh/ssh.go (right):
https:/
utils/ssh/
OpenSSH.
On 2014/01/15 23:52:19, wallyworld wrote:
> This comment is now out of date
Done.
https:/
File utils/trivial_
https:/
utils/trivial_
On 2014/01/15 23:52:19, wallyworld wrote:
> There are a some test cases missing eg $
Done.
Andrew Wilkins (axwalk) wrote : | # |
Please take a look.
Tim Penhey (thumper) wrote : | # |
LGTM with one small adjustment :-)
https:/
File utils/ssh/
https:/
utils/ssh/
Man I hate magic bools. Can we have some constants or types or
something: asSCP, asSSH ?
Andrew Wilkins (axwalk) wrote : | # |
Please take a look.
https:/
File utils/ssh/
https:/
utils/ssh/
On 2014/01/17 03:15:23, thumper wrote:
> Man I hate magic bools. Can we have some constants or types or
something: asSCP,
> asSSH ?
Done.
Andrew Wilkins (axwalk) wrote : | # |
On 2014/01/17 03:37:42, axw wrote:
> Please take a look.
https:/
> File utils/ssh/
https:/
> utils/ssh/
> On 2014/01/17 03:15:23, thumper wrote:
> > Man I hate magic bools. Can we have some constants or types or
something:
> asSCP,
> > asSSH ?
> Done.
Just going to do another live test and then I'll land it.
Andrew Wilkins (axwalk) wrote : | # |
Confirmed that it works on both Linux (disabling OpenSSH in code) and Windows (no code changes; OpenSSH not installed).
Go Bot (go-bot) wrote : | # |
The attempt to merge lp:~axwalk/juju-core/ssh-gocrypto-client-take2 into lp:juju-core failed. Below is the output from the failed tests.
ok launchpad.
ok launchpad.
ok launchpad.
ok launchpad.
ok launchpad.
? launchpad.
? launchpad.
ok launchpad.
ok launchpad.
ok launchpad.
ok launchpad.
? launchpad.
? launchpad.
ok launchpad.
ok launchpad.
ok launchpad.
? launchpad.
ok launchpad.
ok launchpad.
ok launchpad.
ok launchpad.
ok launchpad.
? launchpad.
ok launchpad.
? launchpad.
? launchpad.
? launchpad.
ok launchpad.
ok launchpad.
ok launchpad.
ok launchpad.
ok launchpad.
ok launchpad.
ok launchpad.
ok launchpad.
ok launchpad.
? launchpad.
ok launchpad.
ok launchpad.
ok launchpad.
ok launchpad.
? launchpad.
ok launchpad.
ok launchpad.
ok launchpad.
ok launchpad.
ok launchpad.
? launchpad.
ok launchpad.
ok launchpad.
? launchpad.
ok launchpad.
ok launchpad.
? launchpad.
ok launchpad.
ok launchpad.
ok launchpad.
? launchpad.net/j...
Preview Diff
1 | === modified file 'utils/ssh/clientkeys_test.go' |
2 | --- utils/ssh/clientkeys_test.go 2014-01-14 03:35:21 +0000 |
3 | +++ utils/ssh/clientkeys_test.go 2014-01-17 04:36:38 +0000 |
4 | @@ -9,7 +9,6 @@ |
5 | |
6 | gc "launchpad.net/gocheck" |
7 | |
8 | - "launchpad.net/juju-core/environs/config" |
9 | "launchpad.net/juju-core/testing" |
10 | jc "launchpad.net/juju-core/testing/checkers" |
11 | "launchpad.net/juju-core/testing/testbase" |
12 | @@ -58,14 +57,14 @@ |
13 | // All files ending with .pub in the client key dir get picked up. |
14 | priv, pub, err := ssh.GenerateKey("whatever") |
15 | c.Assert(err, gc.IsNil) |
16 | - err = ioutil.WriteFile(config.JujuHomePath("ssh", "whatever.pub"), []byte(pub), 0600) |
17 | + err = ioutil.WriteFile(testing.HomePath(".juju", "ssh", "whatever.pub"), []byte(pub), 0600) |
18 | c.Assert(err, gc.IsNil) |
19 | err = ssh.LoadClientKeys("~/.juju/ssh") |
20 | c.Assert(err, gc.IsNil) |
21 | // The new public key won't be observed until the |
22 | // corresponding private key exists. |
23 | checkPublicKeyFiles(c, "~/.juju/ssh/juju_id_rsa.pub") |
24 | - err = ioutil.WriteFile(config.JujuHomePath("ssh", "whatever"), []byte(priv), 0600) |
25 | + err = ioutil.WriteFile(testing.HomePath(".juju", "ssh", "whatever"), []byte(priv), 0600) |
26 | c.Assert(err, gc.IsNil) |
27 | err = ssh.LoadClientKeys("~/.juju/ssh") |
28 | c.Assert(err, gc.IsNil) |
29 | @@ -81,14 +80,14 @@ |
30 | checkPrivateKeyFiles(c, "~/.juju/ssh/juju_id_rsa") |
31 | priv, pub, err := ssh.GenerateKey("whatever") |
32 | c.Assert(err, gc.IsNil) |
33 | - err = ioutil.WriteFile(config.JujuHomePath("ssh", "whatever"), []byte(priv), 0600) |
34 | + err = ioutil.WriteFile(testing.HomePath(".juju", "ssh", "whatever"), []byte(priv), 0600) |
35 | c.Assert(err, gc.IsNil) |
36 | err = ssh.LoadClientKeys("~/.juju/ssh") |
37 | c.Assert(err, gc.IsNil) |
38 | // The new private key won't be observed until the |
39 | // corresponding public key exists. |
40 | checkPrivateKeyFiles(c, "~/.juju/ssh/juju_id_rsa") |
41 | - err = ioutil.WriteFile(config.JujuHomePath("ssh", "whatever.pub"), []byte(pub), 0600) |
42 | + err = ioutil.WriteFile(testing.HomePath(".juju", "ssh", "whatever.pub"), []byte(pub), 0600) |
43 | c.Assert(err, gc.IsNil) |
44 | // new keys won't be reported until we call LoadClientKeys again |
45 | checkPublicKeyFiles(c, "~/.juju/ssh/juju_id_rsa.pub") |
46 | @@ -100,7 +99,7 @@ |
47 | } |
48 | |
49 | func (s *ClientKeysSuite) TestLoadClientKeysDirExists(c *gc.C) { |
50 | - err := os.MkdirAll(config.JujuHomePath("ssh"), 0755) |
51 | + err := os.MkdirAll(testing.HomePath(".juju", "ssh"), 0755) |
52 | c.Assert(err, gc.IsNil) |
53 | err = ssh.LoadClientKeys("~/.juju/ssh") |
54 | c.Assert(err, gc.IsNil) |
55 | |
56 | === modified file 'utils/ssh/export_test.go' |
57 | --- utils/ssh/export_test.go 2013-12-12 13:44:42 +0000 |
58 | +++ utils/ssh/export_test.go 2014-01-17 04:36:38 +0000 |
59 | @@ -6,4 +6,5 @@ |
60 | var ( |
61 | ReadAuthorisedKeys = readAuthorisedKeys |
62 | WriteAuthorisedKeys = writeAuthorisedKeys |
63 | + InitDefaultClient = initDefaultClient |
64 | ) |
65 | |
66 | === modified file 'utils/ssh/ssh.go' |
67 | --- utils/ssh/ssh.go 2014-01-13 06:25:28 +0000 |
68 | +++ utils/ssh/ssh.go 2014-01-17 04:36:38 +0000 |
69 | @@ -16,6 +16,8 @@ |
70 | |
71 | // Options is a client-implementation independent SSH options set. |
72 | type Options struct { |
73 | + // ssh server port; zero means use the default (22) |
74 | + port int |
75 | // no PTY forced by default |
76 | allocatePTY bool |
77 | // password authentication is disallowed by default |
78 | @@ -26,6 +28,11 @@ |
79 | identities []string |
80 | } |
81 | |
82 | +// SetPort sets the SSH server port to connect to. |
83 | +func (o *Options) SetPort(port int) { |
84 | + o.port = port |
85 | +} |
86 | + |
87 | // EnablePTY forces the allocation of a pseudo-TTY. |
88 | // |
89 | // Forcing a pseudo-TTY is required, for example, for sudo |
90 | @@ -180,14 +187,20 @@ |
91 | |
92 | // DefaultClient is the default SSH client for the process. |
93 | // |
94 | -// There is currently only one client available: OpenSSH. |
95 | -// If the OpenSSH client is not installed, then DefaultClient |
96 | -// will be nil. |
97 | +// If the OpenSSH client is found in $PATH, then it will be |
98 | +// used for DefaultClient; otherwise, DefaultClient will use |
99 | +// an embedded client based on go.crypto/ssh. |
100 | var DefaultClient Client |
101 | |
102 | func init() { |
103 | + initDefaultClient() |
104 | +} |
105 | + |
106 | +func initDefaultClient() { |
107 | if client, err := NewOpenSSHClient(); err == nil { |
108 | DefaultClient = client |
109 | + } else if client, err := NewGoCryptoClient(); err == nil { |
110 | + DefaultClient = client |
111 | } |
112 | } |
113 | |
114 | |
115 | === added file 'utils/ssh/ssh_gocrypto.go' |
116 | --- utils/ssh/ssh_gocrypto.go 1970-01-01 00:00:00 +0000 |
117 | +++ utils/ssh/ssh_gocrypto.go 2014-01-17 04:36:38 +0000 |
118 | @@ -0,0 +1,217 @@ |
119 | +// Copyright 2013 Canonical Ltd. |
120 | +// Licensed under the AGPLv3, see LICENCE file for details. |
121 | + |
122 | +package ssh |
123 | + |
124 | +import ( |
125 | + "fmt" |
126 | + "io" |
127 | + "io/ioutil" |
128 | + "os/user" |
129 | + "strings" |
130 | + |
131 | + "code.google.com/p/go.crypto/ssh" |
132 | + |
133 | + "launchpad.net/juju-core/utils" |
134 | +) |
135 | + |
136 | +const sshDefaultPort = 22 |
137 | + |
138 | +// GoCryptoClient is an implementation of Client that |
139 | +// uses the embedded go.crypto/ssh SSH client. |
140 | +// |
141 | +// GoCryptoClient is intentionally limited in the |
142 | +// functionality that it enables, as it is currently |
143 | +// intended to be used only for non-interactive command |
144 | +// execution. |
145 | +type GoCryptoClient struct { |
146 | + signers []ssh.Signer |
147 | +} |
148 | + |
149 | +// NewGoCryptoClient creates a new GoCryptoClient. |
150 | +// |
151 | +// If no signers are specified, NewGoCryptoClient will |
152 | +// use the private key generated by LoadClientKeys. |
153 | +func NewGoCryptoClient(signers ...ssh.Signer) (*GoCryptoClient, error) { |
154 | + return &GoCryptoClient{signers: signers}, nil |
155 | +} |
156 | + |
157 | +// Command implements Client.Command. |
158 | +func (c *GoCryptoClient) Command(host string, command []string, options *Options) *Cmd { |
159 | + shellCommand := utils.CommandString(command...) |
160 | + signers := c.signers |
161 | + if len(signers) == 0 { |
162 | + signers = privateKeys() |
163 | + } |
164 | + user, host := splitUserHost(host) |
165 | + port := sshDefaultPort |
166 | + if options != nil { |
167 | + if options.port != 0 { |
168 | + port = options.port |
169 | + } |
170 | + } |
171 | + return &Cmd{impl: &goCryptoCommand{ |
172 | + signers: signers, |
173 | + user: user, |
174 | + addr: fmt.Sprintf("%s:%d", host, port), |
175 | + command: shellCommand, |
176 | + }} |
177 | +} |
178 | + |
179 | +// Copy implements Client.Copy. |
180 | +// |
181 | +// Copy is currently unimplemented, and will always return an error. |
182 | +func (c *GoCryptoClient) Copy(source, dest string, options *Options) error { |
183 | + return fmt.Errorf("Copy is not implemented") |
184 | +} |
185 | + |
186 | +type goCryptoCommand struct { |
187 | + signers []ssh.Signer |
188 | + user string |
189 | + addr string |
190 | + command string |
191 | + stdin io.Reader |
192 | + stdout io.Writer |
193 | + stderr io.Writer |
194 | + conn *ssh.ClientConn |
195 | + sess *ssh.Session |
196 | +} |
197 | + |
198 | +func (c *goCryptoCommand) ensureSession() (*ssh.Session, error) { |
199 | + if c.sess != nil { |
200 | + return c.sess, nil |
201 | + } |
202 | + if len(c.signers) == 0 { |
203 | + return nil, fmt.Errorf("no private keys available") |
204 | + } |
205 | + if c.user == "" { |
206 | + currentUser, err := user.Current() |
207 | + if err != nil { |
208 | + return nil, fmt.Errorf("getting current user: %v", err) |
209 | + } |
210 | + c.user = currentUser.Username |
211 | + } |
212 | + config := &ssh.ClientConfig{ |
213 | + User: c.user, |
214 | + Auth: []ssh.ClientAuth{ |
215 | + ssh.ClientAuthKeyring(keyring{c.signers}), |
216 | + }, |
217 | + } |
218 | + conn, err := ssh.Dial("tcp", c.addr, config) |
219 | + if err != nil { |
220 | + return nil, err |
221 | + } |
222 | + sess, err := conn.NewSession() |
223 | + if err != nil { |
224 | + conn.Close() |
225 | + return nil, err |
226 | + } |
227 | + c.conn = conn |
228 | + c.sess = sess |
229 | + c.sess.Stdin = c.stdin |
230 | + c.sess.Stdout = c.stdout |
231 | + c.sess.Stderr = c.stderr |
232 | + return sess, nil |
233 | +} |
234 | + |
235 | +func (c *goCryptoCommand) Start() error { |
236 | + sess, err := c.ensureSession() |
237 | + if err != nil { |
238 | + return err |
239 | + } |
240 | + if c.command == "" { |
241 | + return sess.Shell() |
242 | + } |
243 | + return sess.Start(c.command) |
244 | +} |
245 | + |
246 | +func (c *goCryptoCommand) Close() error { |
247 | + if c.sess == nil { |
248 | + return nil |
249 | + } |
250 | + err0 := c.sess.Close() |
251 | + err1 := c.conn.Close() |
252 | + if err0 == nil { |
253 | + err0 = err1 |
254 | + } |
255 | + c.sess = nil |
256 | + c.conn = nil |
257 | + return err0 |
258 | +} |
259 | + |
260 | +func (c *goCryptoCommand) Wait() error { |
261 | + if c.sess == nil { |
262 | + return fmt.Errorf("Command has not been started") |
263 | + } |
264 | + err := c.sess.Wait() |
265 | + c.Close() |
266 | + return err |
267 | +} |
268 | + |
269 | +func (c *goCryptoCommand) Kill() error { |
270 | + if c.sess == nil { |
271 | + return fmt.Errorf("Command has not been started") |
272 | + } |
273 | + return c.sess.Signal(ssh.SIGKILL) |
274 | +} |
275 | + |
276 | +func (c *goCryptoCommand) SetStdio(stdin io.Reader, stdout, stderr io.Writer) { |
277 | + c.stdin = stdin |
278 | + c.stdout = stdout |
279 | + c.stderr = stderr |
280 | +} |
281 | + |
282 | +func (c *goCryptoCommand) StdinPipe() (io.WriteCloser, io.Reader, error) { |
283 | + sess, err := c.ensureSession() |
284 | + if err != nil { |
285 | + return nil, nil, err |
286 | + } |
287 | + wc, err := sess.StdinPipe() |
288 | + return wc, sess.Stdin, err |
289 | +} |
290 | + |
291 | +func (c *goCryptoCommand) StdoutPipe() (io.ReadCloser, io.Writer, error) { |
292 | + sess, err := c.ensureSession() |
293 | + if err != nil { |
294 | + return nil, nil, err |
295 | + } |
296 | + wc, err := sess.StdoutPipe() |
297 | + return ioutil.NopCloser(wc), sess.Stdout, err |
298 | +} |
299 | + |
300 | +func (c *goCryptoCommand) StderrPipe() (io.ReadCloser, io.Writer, error) { |
301 | + sess, err := c.ensureSession() |
302 | + if err != nil { |
303 | + return nil, nil, err |
304 | + } |
305 | + wc, err := sess.StderrPipe() |
306 | + return ioutil.NopCloser(wc), sess.Stderr, err |
307 | +} |
308 | + |
309 | +// keyring implements ssh.ClientKeyring |
310 | +type keyring struct { |
311 | + signers []ssh.Signer |
312 | +} |
313 | + |
314 | +func (k keyring) Key(i int) (ssh.PublicKey, error) { |
315 | + if i < 0 || i >= len(k.signers) { |
316 | + // nil key marks the end of the keyring; must not return an error. |
317 | + return nil, nil |
318 | + } |
319 | + return k.signers[i].PublicKey(), nil |
320 | +} |
321 | + |
322 | +func (k keyring) Sign(i int, rand io.Reader, data []byte) ([]byte, error) { |
323 | + if i < 0 || i >= len(k.signers) { |
324 | + return nil, fmt.Errorf("no key at position %d", i) |
325 | + } |
326 | + return k.signers[i].Sign(rand, data) |
327 | +} |
328 | + |
329 | +func splitUserHost(s string) (user, host string) { |
330 | + userHost := strings.SplitN(s, "@", 2) |
331 | + if len(userHost) == 2 { |
332 | + return userHost[0], userHost[1] |
333 | + } |
334 | + return "", userHost[0] |
335 | +} |
336 | |
337 | === added file 'utils/ssh/ssh_gocrypto_test.go' |
338 | --- utils/ssh/ssh_gocrypto_test.go 1970-01-01 00:00:00 +0000 |
339 | +++ utils/ssh/ssh_gocrypto_test.go 2014-01-17 04:36:38 +0000 |
340 | @@ -0,0 +1,145 @@ |
341 | +// Copyright 2014 Canonical Ltd. |
342 | +// Licensed under the AGPLv3, see LICENCE file for details. |
343 | + |
344 | +package ssh_test |
345 | + |
346 | +import ( |
347 | + "encoding/binary" |
348 | + "net" |
349 | + "sync" |
350 | + |
351 | + cryptossh "code.google.com/p/go.crypto/ssh" |
352 | + gc "launchpad.net/gocheck" |
353 | + |
354 | + jc "launchpad.net/juju-core/testing/checkers" |
355 | + "launchpad.net/juju-core/testing/testbase" |
356 | + "launchpad.net/juju-core/utils/ssh" |
357 | +) |
358 | + |
359 | +var ( |
360 | + testCommand = []string{"echo", "$abc"} |
361 | + testCommandFlat = `echo "\$abc"` |
362 | +) |
363 | + |
364 | +type sshServer struct { |
365 | + cfg *cryptossh.ServerConfig |
366 | + *cryptossh.Listener |
367 | +} |
368 | + |
369 | +func newServer(c *gc.C) *sshServer { |
370 | + private, _, err := ssh.GenerateKey("test-server") |
371 | + c.Assert(err, gc.IsNil) |
372 | + key, err := cryptossh.ParsePrivateKey([]byte(private)) |
373 | + c.Assert(err, gc.IsNil) |
374 | + server := &sshServer{ |
375 | + cfg: &cryptossh.ServerConfig{}, |
376 | + } |
377 | + server.cfg.AddHostKey(key) |
378 | + server.Listener, err = cryptossh.Listen("tcp", "127.0.0.1:0", server.cfg) |
379 | + c.Assert(err, gc.IsNil) |
380 | + return server |
381 | +} |
382 | + |
383 | +func (s *sshServer) run(c *gc.C) { |
384 | + conn, err := s.Accept() |
385 | + c.Assert(err, gc.IsNil) |
386 | + defer func() { |
387 | + err = conn.Close() |
388 | + c.Assert(err, gc.IsNil) |
389 | + }() |
390 | + err = conn.Handshake() |
391 | + c.Assert(err, gc.IsNil) |
392 | + var wg sync.WaitGroup |
393 | + defer wg.Wait() |
394 | + for { |
395 | + channel, err := conn.Accept() |
396 | + c.Assert(err, gc.IsNil) |
397 | + c.Assert(channel.ChannelType(), gc.Equals, "session") |
398 | + channel.Accept() |
399 | + wg.Add(1) |
400 | + go func() { |
401 | + defer wg.Done() |
402 | + defer channel.Close() |
403 | + _, err := channel.Read(nil) |
404 | + c.Assert(err, gc.FitsTypeOf, cryptossh.ChannelRequest{}) |
405 | + req := err.(cryptossh.ChannelRequest) |
406 | + c.Assert(req.Request, gc.Equals, "exec") |
407 | + c.Assert(req.WantReply, jc.IsTrue) |
408 | + n := binary.BigEndian.Uint32(req.Payload[:4]) |
409 | + command := string(req.Payload[4 : n+4]) |
410 | + c.Assert(command, gc.Equals, testCommandFlat) |
411 | + // TODO(axw) when gosshnew is ready, send reply to client. |
412 | + }() |
413 | + } |
414 | +} |
415 | + |
416 | +type SSHGoCryptoCommandSuite struct { |
417 | + testbase.LoggingSuite |
418 | + client ssh.Client |
419 | +} |
420 | + |
421 | +var _ = gc.Suite(&SSHGoCryptoCommandSuite{}) |
422 | + |
423 | +func (s *SSHGoCryptoCommandSuite) SetUpTest(c *gc.C) { |
424 | + s.LoggingSuite.SetUpTest(c) |
425 | + client, err := ssh.NewGoCryptoClient() |
426 | + c.Assert(err, gc.IsNil) |
427 | + s.client = client |
428 | +} |
429 | + |
430 | +func (s *SSHGoCryptoCommandSuite) TestNewGoCryptoClient(c *gc.C) { |
431 | + _, err := ssh.NewGoCryptoClient() |
432 | + c.Assert(err, gc.IsNil) |
433 | + private, _, err := ssh.GenerateKey("test-client") |
434 | + c.Assert(err, gc.IsNil) |
435 | + key, err := cryptossh.ParsePrivateKey([]byte(private)) |
436 | + c.Assert(err, gc.IsNil) |
437 | + _, err = ssh.NewGoCryptoClient(key) |
438 | + c.Assert(err, gc.IsNil) |
439 | +} |
440 | + |
441 | +func (s *SSHGoCryptoCommandSuite) TestClientNoKeys(c *gc.C) { |
442 | + client, err := ssh.NewGoCryptoClient() |
443 | + c.Assert(err, gc.IsNil) |
444 | + cmd := client.Command("test.invalid", []string{"echo", "123"}, nil) |
445 | + _, err = cmd.Output() |
446 | + c.Assert(err, gc.ErrorMatches, "no private keys available") |
447 | + defer ssh.ClearClientKeys() |
448 | + err = ssh.LoadClientKeys(c.MkDir()) |
449 | + c.Assert(err, gc.IsNil) |
450 | + cmd = client.Command("test.invalid", []string{"echo", "123"}, nil) |
451 | + _, err = cmd.Output() |
452 | + // error message differs based on whether using cgo or not |
453 | + c.Assert(err, gc.ErrorMatches, "(dial tcp: )?lookup test.invalid: no such host") |
454 | +} |
455 | + |
456 | +func (s *SSHGoCryptoCommandSuite) TestCommand(c *gc.C) { |
457 | + private, _, err := ssh.GenerateKey("test-server") |
458 | + c.Assert(err, gc.IsNil) |
459 | + key, err := cryptossh.ParsePrivateKey([]byte(private)) |
460 | + client, err := ssh.NewGoCryptoClient(key) |
461 | + c.Assert(err, gc.IsNil) |
462 | + server := newServer(c) |
463 | + var opts ssh.Options |
464 | + opts.SetPort(server.Addr().(*net.TCPAddr).Port) |
465 | + cmd := client.Command("127.0.0.1", testCommand, &opts) |
466 | + checkedKey := false |
467 | + server.cfg.PublicKeyCallback = func(conn *cryptossh.ServerConn, user, algo string, pubkey []byte) bool { |
468 | + c.Check(pubkey, gc.DeepEquals, cryptossh.MarshalPublicKey(key.PublicKey())) |
469 | + checkedKey = true |
470 | + return true |
471 | + } |
472 | + go server.run(c) |
473 | + out, err := cmd.Output() |
474 | + c.Assert(err, gc.ErrorMatches, "ssh: could not execute command.*") |
475 | + // TODO(axw) when gosshnew is ready, expect reply from server. |
476 | + c.Assert(out, gc.IsNil) |
477 | + c.Assert(checkedKey, jc.IsTrue) |
478 | +} |
479 | + |
480 | +func (s *SSHGoCryptoCommandSuite) TestCopy(c *gc.C) { |
481 | + client, err := ssh.NewGoCryptoClient() |
482 | + c.Assert(err, gc.IsNil) |
483 | + err = client.Copy("test.invalid:b", c.MkDir(), nil) |
484 | + c.Assert(err, gc.ErrorMatches, "Copy is not implemented") |
485 | +} |
486 | |
487 | === modified file 'utils/ssh/ssh_openssh.go' |
488 | --- utils/ssh/ssh_openssh.go 2014-01-13 06:25:28 +0000 |
489 | +++ utils/ssh/ssh_openssh.go 2014-01-17 04:36:38 +0000 |
490 | @@ -14,6 +14,13 @@ |
491 | |
492 | var opensshCommonOptions = []string{"-o", "StrictHostKeyChecking no"} |
493 | |
494 | +type opensshCommandKind int |
495 | + |
496 | +const ( |
497 | + sshKind opensshCommandKind = iota |
498 | + scpKind |
499 | +) |
500 | + |
501 | // sshpassWrap wraps the command/args with sshpass if it is found in $PATH |
502 | // and the SSHPASS environment variable is set. Otherwise, the original |
503 | // command/args are returned. |
504 | @@ -44,7 +51,7 @@ |
505 | return &c, nil |
506 | } |
507 | |
508 | -func opensshOptions(options *Options) []string { |
509 | +func opensshOptions(options *Options, commandKind opensshCommandKind) []string { |
510 | args := append([]string{}, opensshCommonOptions...) |
511 | if options == nil { |
512 | options = &Options{} |
513 | @@ -58,12 +65,21 @@ |
514 | for _, identity := range options.identities { |
515 | args = append(args, "-i", identity) |
516 | } |
517 | + if options.port != 0 { |
518 | + if commandKind == scpKind { |
519 | + // scp uses -P instead of -p (-p means preserve). |
520 | + args = append(args, "-P") |
521 | + } else { |
522 | + args = append(args, "-p") |
523 | + } |
524 | + args = append(args, fmt.Sprint(options.port)) |
525 | + } |
526 | return args |
527 | } |
528 | |
529 | // Command implements Client.Command. |
530 | func (c *OpenSSHClient) Command(host string, command []string, options *Options) *Cmd { |
531 | - args := opensshOptions(options) |
532 | + args := opensshOptions(options, sshKind) |
533 | args = append(args, host) |
534 | if len(command) > 0 { |
535 | args = append(args, "--") |
536 | @@ -80,7 +96,7 @@ |
537 | options = *userOptions |
538 | options.allocatePTY = false // doesn't make sense for scp |
539 | } |
540 | - args := opensshOptions(&options) |
541 | + args := opensshOptions(&options, scpKind) |
542 | args = append(args, source, dest) |
543 | bin, args := sshpassWrap("scp", args) |
544 | cmd := exec.Command(bin, args...) |
545 | |
546 | === modified file 'utils/ssh/ssh_test.go' |
547 | --- utils/ssh/ssh_test.go 2014-01-13 06:25:28 +0000 |
548 | +++ utils/ssh/ssh_test.go 2014-01-17 04:36:38 +0000 |
549 | @@ -55,6 +55,14 @@ |
550 | c.Assert(strings.TrimSpace(string(out)), gc.Equals, expected) |
551 | } |
552 | |
553 | +func (s *SSHCommandSuite) TestDefaultClient(c *gc.C) { |
554 | + ssh.InitDefaultClient() |
555 | + c.Assert(ssh.DefaultClient, gc.FitsTypeOf, &ssh.OpenSSHClient{}) |
556 | + s.PatchEnvironment("PATH", "") |
557 | + ssh.InitDefaultClient() |
558 | + c.Assert(ssh.DefaultClient, gc.FitsTypeOf, &ssh.GoCryptoClient{}) |
559 | +} |
560 | + |
561 | func (s *SSHCommandSuite) TestCommandSSHPass(c *gc.C) { |
562 | // First create a fake sshpass, but don't set $SSHPASS |
563 | fakesshpass := filepath.Join(s.testbin, "sshpass") |
564 | @@ -105,15 +113,24 @@ |
565 | ) |
566 | } |
567 | |
568 | +func (s *SSHCommandSuite) TestCommandPort(c *gc.C) { |
569 | + var opts ssh.Options |
570 | + opts.SetPort(2022) |
571 | + s.assertCommandArgs(c, s.commandOptions([]string{"echo", "123"}, &opts), |
572 | + s.fakessh+" -o StrictHostKeyChecking no -o PasswordAuthentication no -p 2022 localhost -- echo 123", |
573 | + ) |
574 | +} |
575 | + |
576 | func (s *SSHCommandSuite) TestCopy(c *gc.C) { |
577 | var opts ssh.Options |
578 | opts.EnablePTY() |
579 | opts.AllowPasswordAuthentication() |
580 | opts.SetIdentities("x", "y") |
581 | + opts.SetPort(2022) |
582 | err := s.client.Copy("/tmp/blah", "foo@bar.com:baz", &opts) |
583 | c.Assert(err, gc.IsNil) |
584 | out, err := ioutil.ReadFile(s.fakescp + ".args") |
585 | c.Assert(err, gc.IsNil) |
586 | // EnablePTY has no effect for Copy |
587 | - c.Assert(string(out), gc.Equals, s.fakescp+" -o StrictHostKeyChecking no -i x -i y /tmp/blah foo@bar.com:baz\n") |
588 | + c.Assert(string(out), gc.Equals, s.fakescp+" -o StrictHostKeyChecking no -i x -i y -P 2022 /tmp/blah foo@bar.com:baz\n") |
589 | } |
590 | |
591 | === modified file 'utils/trivial.go' |
592 | --- utils/trivial.go 2013-10-10 20:58:54 +0000 |
593 | +++ utils/trivial.go 2014-01-17 04:36:38 +0000 |
594 | @@ -11,6 +11,7 @@ |
595 | "io/ioutil" |
596 | "os" |
597 | "strings" |
598 | + "unicode" |
599 | |
600 | "launchpad.net/goyaml" |
601 | ) |
602 | @@ -61,6 +62,38 @@ |
603 | return `'` + strings.Replace(s, `'`, `'"'"'`, -1) + `'` |
604 | } |
605 | |
606 | +// CommandString flattens a sequence of command arguments into a |
607 | +// string suitable for executing in a shell, escaping slashes, |
608 | +// variables and quotes as necessary; each argument is double-quoted |
609 | +// if and only if necessary. |
610 | +func CommandString(args ...string) string { |
611 | + var buf bytes.Buffer |
612 | + for i, arg := range args { |
613 | + needsQuotes := false |
614 | + var argBuf bytes.Buffer |
615 | + for _, r := range arg { |
616 | + if unicode.IsSpace(r) { |
617 | + needsQuotes = true |
618 | + } else if r == '"' || r == '$' || r == '\\' { |
619 | + needsQuotes = true |
620 | + argBuf.WriteByte('\\') |
621 | + } |
622 | + argBuf.WriteRune(r) |
623 | + } |
624 | + if i > 0 { |
625 | + buf.WriteByte(' ') |
626 | + } |
627 | + if needsQuotes { |
628 | + buf.WriteByte('"') |
629 | + argBuf.WriteTo(&buf) |
630 | + buf.WriteByte('"') |
631 | + } else { |
632 | + argBuf.WriteTo(&buf) |
633 | + } |
634 | + } |
635 | + return buf.String() |
636 | +} |
637 | + |
638 | // Gzip compresses the given data. |
639 | func Gzip(data []byte) []byte { |
640 | var buf bytes.Buffer |
641 | |
642 | === modified file 'utils/trivial_test.go' |
643 | --- utils/trivial_test.go 2013-11-05 05:40:47 +0000 |
644 | +++ utils/trivial_test.go 2014-01-17 04:36:38 +0000 |
645 | @@ -47,3 +47,27 @@ |
646 | c.Assert(err, gc.IsNil) |
647 | c.Assert(data1, gc.DeepEquals, data) |
648 | } |
649 | + |
650 | +func (utilsSuite) TestCommandString(c *gc.C) { |
651 | + type test struct { |
652 | + args []string |
653 | + expected string |
654 | + } |
655 | + tests := []test{ |
656 | + {nil, ""}, |
657 | + {[]string{"a"}, "a"}, |
658 | + {[]string{"a$"}, `"a\$"`}, |
659 | + {[]string{""}, ""}, |
660 | + {[]string{"\\"}, `"\\"`}, |
661 | + {[]string{"a", "'b'"}, "a 'b'"}, |
662 | + {[]string{"a b"}, `"a b"`}, |
663 | + {[]string{"a", `"b"`}, `a "\"b\""`}, |
664 | + {[]string{"a", `"b\"`}, `a "\"b\\\""`}, |
665 | + {[]string{"a\n"}, "\"a\n\""}, |
666 | + } |
667 | + for i, test := range tests { |
668 | + c.Logf("test %d: %q", i, test.args) |
669 | + result := utils.CommandString(test.args...) |
670 | + c.Assert(result, gc.Equals, test.expected) |
671 | + } |
672 | +} |
Reviewers: mp+201706_ code.launchpad. net,
Message:
Please take a look.
Description:
utils/ssh: introduce go.crypto/ssh client
Bootstrap now requires an ssh client. Users
may not have an SSH client available or
configured; this is typical of many Windows
users.
We introduce a go.crypto/ssh-based embedded
client that will be used if OpenSSH is not
available. The embedded client will use any
keys found in $JUJU_HOME/ssh.
Fixes #1263851
https:/ /code.launchpad .net/~axwalk/ juju-core/ ssh-gocrypto- client- take2/+ merge/201706
(do not edit description out of merge proposal)
Please review this at https:/ /codereview. appspot. com/52570043/
Affected files (+270, -6 lines): clientkeys_ test.go ssh_gocrypto. go test.go
A [revision details]
M utils/ssh/
M utils/ssh/ssh.go
A utils/ssh/
M utils/trivial.go
M utils/trivial_