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

Proposed by Andrew Wilkins
Status: Merged
Approved by: Andrew Wilkins
Approved revision: no longer in the source branch.
Merged at revision: 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
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

https://codereview.appspot.com/52570043/

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

https://codereview.appspot.com/52570043/

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

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):
   A [revision details]
   M utils/ssh/clientkeys_test.go
   M utils/ssh/ssh.go
   A utils/ssh/ssh_gocrypto.go
   M utils/trivial.go
   M utils/trivial_test.go

Revision history for this message
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://codereview.appspot.com/52570043/diff/1/utils/ssh/ssh.go
File utils/ssh/ssh.go (right):

https://codereview.appspot.com/52570043/diff/1/utils/ssh/ssh.go#newcode183
utils/ssh/ssh.go:183: // There is currently only one client available:
OpenSSH.
This comment is now out of date

https://codereview.appspot.com/52570043/diff/1/utils/trivial_test.go
File utils/trivial_test.go (right):

https://codereview.appspot.com/52570043/diff/1/utils/trivial_test.go#newcode63
utils/trivial_test.go:63: {[]string{"a\n"}, "\"a\n\""},
There are a some test cases missing eg $

https://codereview.appspot.com/52570043/

Revision history for this message
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://codereview.appspot.com/52570043/diff/1/utils/ssh/ssh.go
File utils/ssh/ssh.go (right):

https://codereview.appspot.com/52570043/diff/1/utils/ssh/ssh.go#newcode183
utils/ssh/ssh.go:183: // There is currently only one client available:
OpenSSH.
On 2014/01/15 23:52:19, wallyworld wrote:
> This comment is now out of date

Done.

https://codereview.appspot.com/52570043/diff/1/utils/trivial_test.go
File utils/trivial_test.go (right):

https://codereview.appspot.com/52570043/diff/1/utils/trivial_test.go#newcode63
utils/trivial_test.go:63: {[]string{"a\n"}, "\"a\n\""},
On 2014/01/15 23:52:19, wallyworld wrote:
> There are a some test cases missing eg $

Done.

https://codereview.appspot.com/52570043/

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

LGTM with one small adjustment :-)

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

https://codereview.appspot.com/52570043/diff/20001/utils/ssh/ssh_openssh.go#newcode74
utils/ssh/ssh_openssh.go:74: args := opensshOptions(options, false)
Man I hate magic bools. Can we have some constants or types or
something: asSCP, asSSH ?

https://codereview.appspot.com/52570043/

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

Please take a look.

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

https://codereview.appspot.com/52570043/diff/20001/utils/ssh/ssh_openssh.go#newcode74
utils/ssh/ssh_openssh.go:74: args := opensshOptions(options, false)
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.

https://codereview.appspot.com/52570043/

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

On 2014/01/17 03:37:42, axw wrote:
> Please take a look.

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

https://codereview.appspot.com/52570043/diff/20001/utils/ssh/ssh_openssh.go#newcode74
> utils/ssh/ssh_openssh.go:74: args := opensshOptions(options, false)
> 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.

https://codereview.appspot.com/52570043/

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

Confirmed that it works on both Linux (disabling OpenSSH in code) and Windows (no code changes; OpenSSH not installed).

Revision history for this message
Go Bot (go-bot) wrote :
Download full text (8.7 KiB)

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.net/juju-core/agent 0.851s
ok launchpad.net/juju-core/agent/tools 0.304s
ok launchpad.net/juju-core/bzr 7.500s
ok launchpad.net/juju-core/cert 3.225s
ok launchpad.net/juju-core/charm 0.659s
? launchpad.net/juju-core/charm/hooks [no test files]
? launchpad.net/juju-core/charm/testing [no test files]
ok launchpad.net/juju-core/cloudinit 0.033s
ok launchpad.net/juju-core/cloudinit/sshinit 1.009s
ok launchpad.net/juju-core/cmd 0.221s
ok launchpad.net/juju-core/cmd/charm-admin 0.869s
? launchpad.net/juju-core/cmd/charmd [no test files]
? launchpad.net/juju-core/cmd/charmload [no test files]
ok launchpad.net/juju-core/cmd/juju 225.609s
ok launchpad.net/juju-core/cmd/jujud 57.725s
ok launchpad.net/juju-core/cmd/plugins/juju-metadata 10.019s
? launchpad.net/juju-core/cmd/plugins/juju-restore [no test files]
ok launchpad.net/juju-core/constraints 0.030s
ok launchpad.net/juju-core/container 0.038s
ok launchpad.net/juju-core/container/factory 0.069s
ok launchpad.net/juju-core/container/kvm 0.317s
ok launchpad.net/juju-core/container/kvm/mock 0.043s
? launchpad.net/juju-core/container/kvm/testing [no test files]
ok launchpad.net/juju-core/container/lxc 0.376s
? launchpad.net/juju-core/container/lxc/mock [no test files]
? launchpad.net/juju-core/container/lxc/testing [no test files]
? launchpad.net/juju-core/container/testing [no test files]
ok launchpad.net/juju-core/downloader 5.285s
ok launchpad.net/juju-core/environs 3.125s
ok launchpad.net/juju-core/environs/bootstrap 4.559s
ok launchpad.net/juju-core/environs/cloudinit 0.561s
ok launchpad.net/juju-core/environs/config 4.943s
ok launchpad.net/juju-core/environs/configstore 0.044s
ok launchpad.net/juju-core/environs/filestorage 0.033s
ok launchpad.net/juju-core/environs/httpstorage 0.990s
ok launchpad.net/juju-core/environs/imagemetadata 0.483s
? launchpad.net/juju-core/environs/imagemetadata/testing [no test files]
ok launchpad.net/juju-core/environs/instances 0.058s
ok launchpad.net/juju-core/environs/jujutest 0.267s
ok launchpad.net/juju-core/environs/manual 11.024s
ok launchpad.net/juju-core/environs/simplestreams 0.362s
? launchpad.net/juju-core/environs/simplestreams/testing [no test files]
ok launchpad.net/juju-core/environs/sshstorage 1.490s
ok launchpad.net/juju-core/environs/storage 1.228s
ok launchpad.net/juju-core/environs/sync 33.102s
ok launchpad.net/juju-core/environs/testing 0.206s
ok launchpad.net/juju-core/environs/tools 6.839s
? launchpad.net/juju-core/environs/tools/testing [no test files]
ok launchpad.net/juju-core/errors 0.018s
ok launchpad.net/juju-core/instance 0.023s
? launchpad.net/juju-core/instance/testing [no test files]
ok launchpad.net/juju-core/juju 23.221s
ok launchpad.net/juju-core/juju/osenv 0.018s
? launchpad.net/juju-core/juju/testing [no test files]
ok launchpad.net/juju-core/log 0.015s
ok launchpad.net/juju-core/log/syslog 0.025s
ok launchpad.net/juju-core/names 0.054s
? launchpad.net/j...

Read more...

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
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+}

Subscribers

People subscribed via source and target branches

to status/vote changes: