Merge lp:~axwalk/juju-core/gocryptossh-proxycommand 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: 2537
Proposed branch: lp:~axwalk/juju-core/gocryptossh-proxycommand
Merge into: lp:~go-bot/juju-core/trunk
Diff against target: 845 lines (+336/-209)
11 files modified
cmd/juju/debughooks.go (+1/-42)
cmd/juju/scp.go (+11/-1)
cmd/juju/scp_test.go (+56/-58)
cmd/juju/ssh.go (+104/-84)
cmd/juju/ssh_test.go (+40/-9)
environs/config/config.go (+10/-0)
environs/config/config_test.go (+1/-0)
provider/local/environprovider.go (+7/-1)
provider/local/environprovider_test.go (+16/-0)
utils/ssh/ssh_gocrypto.go (+51/-14)
utils/ssh/ssh_gocrypto_test.go (+39/-0)
To merge this branch: bzr merge lp:~axwalk/juju-core/gocryptossh-proxycommand
Reviewer Review Type Date Requested Status
Juju Engineering Pending
Review via email: mp+211671@code.launchpad.net

Commit message

ProxyCommand support for go.crypto/ssh client

Implement support for the ProxyCommand option
in the go.crypto/ssh-based client. This is just
a matter of executing the provided command, and
using the process' stdin/stdout to form a
net.Conn on which the SSH session is created.

https://codereview.appspot.com/77300045/

Description of the change

ProxyCommand support for go.crypto/ssh client

Implement support for the ProxyCommand option
in the go.crypto/ssh-based client. This is just
a matter of executing the provided command, and
using the process' stdin/stdout to form a
net.Conn on which the SSH session is created.

https://codereview.appspot.com/77300045/

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

Reviewers: mp+211671_code.launchpad.net,

Message:
Please take a look.

Description:
ProxyCommand support for go.crypto/ssh client

Implement support for the ProxyCommand option
in the go.crypto/ssh-based client. This is just
a matter of executing the provided command, and
using the process' stdin/stdout to form a
net.Conn on which the SSH session is created.

https://code.launchpad.net/~axwalk/juju-core/gocryptossh-proxycommand/+merge/211671

(do not edit description out of merge proposal)

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

Affected files (+99, -17 lines):
   A [revision details]
   M cmd/juju/ssh.go
   M cmd/juju/ssh_test.go
   M utils/ssh/ssh_gocrypto.go
   M utils/ssh/ssh_gocrypto_test.go

Revision history for this message
William Reade (fwereade) wrote :

LGTM so long as it's been used live.

https://codereview.appspot.com/77300045/

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

On 2014/03/24 09:34:49, fwereade wrote:
> LGTM so long as it's been used live.

Thanks. It has been tested live, but the prereq was rolled back as it
broke local (if machine-0 doesn't have sshd). I'll wait till 1.18 it
out.

https://codereview.appspot.com/77300045/

Revision history for this message
John A Meinel (jameinel) wrote :

It seems odd to have an in-process SSH library that is doing our connecting, only to have it spawn out to a required "ssh" executable.
I thought the point of using gocrypto's ssh was only when an actual "ssh" wasn't available, which means ProxyCommand doesn't seem very useful.
I realize there could be other use cases for ProxyCommand, though I haven't seen them myself.
Is there a different use case that I'm missing?

Is this intended so that we can proxy via the API server?

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

> It seems odd to have an in-process SSH library that is doing our connecting,
> only to have it spawn out to a required "ssh" executable.
> I thought the point of using gocrypto's ssh was only when an actual "ssh"
> wasn't available, which means ProxyCommand doesn't seem very useful.

In cmd/juju/ssh.go, we set ProxyCommand to "juju ssh ...", not "ssh". Thus, it proxies through the same go.crypto/ssh-based implementation. There's still no requirement for OpenSSH to be available.

> I realize there could be other use cases for ProxyCommand, though I haven't
> seen them myself.
> Is there a different use case that I'm missing?
>
> Is this intended so that we can proxy via the API server?

Yes, that is currently the only use case.

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

On 2014/03/26 08:20:05, axw wrote:
> Please take a look.

Updated this CL to add a proxy-config attribute to base config which
defaults to true for all providers but local.

https://codereview.appspot.com/77300045/

Revision history for this message
William Reade (fwereade) wrote :

Couple of significant fixes needed for the proxy stuff I'm afraid.

https://codereview.appspot.com/77300045/diff/20001/cmd/juju/ssh.go
File cmd/juju/ssh.go (right):

https://codereview.appspot.com/77300045/diff/20001/cmd/juju/ssh.go#newcode151
cmd/juju/ssh.go:151: // this avoids another API round-trip.
Not valid, sadly: we can't depend on bootstrap config being available.

https://codereview.appspot.com/77300045/diff/20001/environs/config/config.go
File environs/config/config.go (right):

https://codereview.appspot.com/77300045/diff/20001/environs/config/config.go#newcode450
environs/config/config.go:450: panic(fmt.Errorf("proxy-ssh missing in
configuration"))
Configs written by older versions must remain valid. Can we not just
treat it as false-if-missing?

https://codereview.appspot.com/77300045/diff/20001/provider/local/environprovider.go
File provider/local/environprovider.go (right):

https://codereview.appspot.com/77300045/diff/20001/provider/local/environprovider.go#newcode111
provider/local/environprovider.go:111: "proxy-ssh": false,
This is bugging me a bit. Rather than silently overwriting potential bad
config, can we instead just refuse it and error out?

https://codereview.appspot.com/77300045/

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

Please take a look.

https://codereview.appspot.com/77300045/diff/20001/cmd/juju/ssh.go
File cmd/juju/ssh.go (right):

https://codereview.appspot.com/77300045/diff/20001/cmd/juju/ssh.go#newcode151
cmd/juju/ssh.go:151: // this avoids another API round-trip.
On 2014/03/27 12:28:57, fwereade wrote:
> Not valid, sadly: we can't depend on bootstrap config being available.

Updated to use bootstrap config if there, else go via API. Is this
valid?

https://codereview.appspot.com/77300045/diff/20001/environs/config/config.go
File environs/config/config.go (right):

https://codereview.appspot.com/77300045/diff/20001/environs/config/config.go#newcode450
environs/config/config.go:450: panic(fmt.Errorf("proxy-ssh missing in
configuration"))
On 2014/03/27 12:28:57, fwereade wrote:
> Configs written by older versions must remain valid. Can we not just
treat it as
> false-if-missing?

We do. See alwaysOptional & allDefaults. If missing, proxy-ssh takes its
value from alwaysOptional (=false); new environments take the default
from allDefaults (=true).

Got rid of the panic anyway.

https://codereview.appspot.com/77300045/

Revision history for this message
William Reade (fwereade) wrote :

LGTM, if you drop the local config check -- it adds an implicit
dependency to cmd, on an implementation detail of environs/config; and
we already have a conn open, so it shouldn't take too long to just ask
remotely. Ping me to discuss if you feel really strongly...

https://codereview.appspot.com/77300045/diff/20001/cmd/juju/ssh.go
File cmd/juju/ssh.go (right):

https://codereview.appspot.com/77300045/diff/20001/cmd/juju/ssh.go#newcode151
cmd/juju/ssh.go:151: // this avoids another API round-trip.
On 2014/03/27 13:50:54, axw wrote:
> On 2014/03/27 12:28:57, fwereade wrote:
> > Not valid, sadly: we can't depend on bootstrap config being
available.

> Updated to use bootstrap config if there, else go via API. Is this
valid?

Well, I don't love depending on the immutability, and we already have a
conn open. How much time do we save here, and how often?

https://codereview.appspot.com/77300045/

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

On 2014/03/28 13:38:15, fwereade wrote:
> LGTM, if you drop the local config check -- it adds an implicit
dependency to
> cmd, on an implementation detail of environs/config; and we already
have a conn
> open, so it shouldn't take too long to just ask remotely. Ping me to
discuss if
> you feel really strongly...

Fair call. I'll remove the check, and also make proxy-ssh mutable.

> https://codereview.appspot.com/77300045/diff/20001/cmd/juju/ssh.go
> File cmd/juju/ssh.go (right):

https://codereview.appspot.com/77300045/diff/20001/cmd/juju/ssh.go#newcode151
> cmd/juju/ssh.go:151: // this avoids another API round-trip.
> On 2014/03/27 13:50:54, axw wrote:
> > On 2014/03/27 12:28:57, fwereade wrote:
> > > Not valid, sadly: we can't depend on bootstrap config being
available.
> >
> > Updated to use bootstrap config if there, else go via API. Is this
valid?

> Well, I don't love depending on the immutability, and we already have
a conn
> open. How much time do we save here, and how often?

It adds about 1s in my tests against Azure (10s vs. 9s). Not a big deal
I suppose.
I just realised that the second "juju ssh" call is making an unnecessary
API connection, so I'll fix that up.

https://codereview.appspot.com/77300045/

Revision history for this message
Andrew Wilkins (axwalk) wrote :
Revision history for this message
Andrew Wilkins (axwalk) wrote :
Revision history for this message
John A Meinel (jameinel) wrote :

I'm testing the bot, will Approve this in a moment.

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

The attempt to merge lp:~axwalk/juju-core/gocryptossh-proxycommand into lp:juju-core failed. Below is the output from the failed tests.

ok launchpad.net/juju-core 0.014s
ok launchpad.net/juju-core/agent 1.039s
ok launchpad.net/juju-core/agent/mongo 0.509s
ok launchpad.net/juju-core/agent/tools 0.235s
ok launchpad.net/juju-core/bzr 5.129s
ok launchpad.net/juju-core/cert 2.674s
ok launchpad.net/juju-core/charm 0.388s
? 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.029s
ok launchpad.net/juju-core/cloudinit/sshinit 0.929s
ok launchpad.net/juju-core/cmd 0.166s
ok launchpad.net/juju-core/cmd/charm-admin 0.730s
? launchpad.net/juju-core/cmd/charmd [no test files]
? launchpad.net/juju-core/cmd/charmload [no test files]
ok launchpad.net/juju-core/cmd/envcmd 0.181s
ok launchpad.net/juju-core/cmd/juju 205.919s
[jujuc whatever]
[remote]
[/path/to/remote]
[remote --help]
[unknown]
[remote --error borken]
[remote --unknown]
[remote unwanted]

----------------------------------------------------------------------
FAIL: unit_test.go:137: UnitSuite.TestRunStop

[LOG] 10.59072 DEBUG juju.environs.configstore Making /tmp/juju-core-test.Ax6TNv/gocheck-6334824724549167320/18/home/ubuntu/.juju/environments
[LOG] 10.65625 DEBUG juju.environs.tools reading v1.* tools
[LOG] 10.65628 INFO juju environs/testing: uploading FAKE tools 1.19.0-precise-amd64
[LOG] 10.65682 DEBUG juju.environs.tools no architecture specified when finding tools, looking for any
[LOG] 10.65684 DEBUG juju.environs.tools no series specified when finding tools, looking for any
[LOG] 10.65687 DEBUG juju.environs.simplestreams fetchData failed for "tools/streams/v1/index.sjson": file "tools/streams/v1/index.sjson" not found not found
[LOG] 10.65689 DEBUG juju.environs.simplestreams cannot load index "streams/v1/index.sjson": invalid URL "tools/streams/v1/index.sjson" not found
[LOG] 10.65694 DEBUG juju.environs.simplestreams fetchData failed for "tools/streams/v1/index.json": file "tools/streams/v1/index.json" not found not found
[LOG] 10.65696 DEBUG juju.environs.simplestreams cannot load index "streams/v1/index.json": invalid URL "tools/streams/v1/index.json" not found
[LOG] 10.65716 INFO juju.environs.tools Writing tools/streams/v1/index.json
[LOG] 10.65720 INFO juju.environs.tools Writing tools/streams/v1/com.ubuntu.juju:released:tools.json
[LOG] 10.65725 DEBUG juju.environs.bootstrap environment "dummyenv" supports service/machine networks: true
[LOG] 10.65726 INFO juju.environs.bootstrap bootstrapping environment "dummyenv"
[LOG] 10.65728 DEBUG juju.environs.bootstrap looking for bootstrap tools: series="precise", arch=<nil>, version=1.19.0
[LOG] 10.65729 INFO juju.environs.tools reading tools with major.minor version 1.19
[LOG] 10.65730 INFO juju.environs.tools filtering tools by version: 1.19.0
[LOG] 10.65731 INFO juju.environs.tools filtering tools by series: precise
[LOG] 10.65732 DEBUG juju.environs.tools no architecture specified when finding tools, looking for any
[LOG] 10.65740 DEBUG juju.environs.simplestreams fetchData failed for "tools/streams/...

Revision history for this message
Go Bot (go-bot) wrote :

The attempt to merge lp:~axwalk/juju-core/gocryptossh-proxycommand into lp:juju-core failed. Below is the output from the failed tests.

mktemp: failed to create directory via template `/tmp/juju-core-test.XXXXXX': No such file or directory
mkdir /tmp/go-build216606492: no such file or directory
mongod: no process found

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'cmd/juju/debughooks.go'
--- cmd/juju/debughooks.go 2013-12-17 18:21:26 +0000
+++ cmd/juju/debughooks.go 2014-04-02 06:17:00 +0000
@@ -12,7 +12,6 @@
12 "launchpad.net/juju-core/charm/hooks"12 "launchpad.net/juju-core/charm/hooks"
13 "launchpad.net/juju-core/cmd"13 "launchpad.net/juju-core/cmd"
14 "launchpad.net/juju-core/names"14 "launchpad.net/juju-core/names"
15 "launchpad.net/juju-core/state/api/params"
16 unitdebug "launchpad.net/juju-core/worker/uniter/debug"15 unitdebug "launchpad.net/juju-core/worker/uniter/debug"
17)16)
1817
@@ -55,52 +54,12 @@
55 return nil54 return nil
56}55}
5756
58// getRelationNames1dot16 gets the list of relation hooks directly from the
59// database, in a fashion compatible with the API server in Juju 1.16 (which
60// doesn't have the ServiceCharmRelations API). This function can be removed
61// when we no longer maintain compatibility with 1.16
62func (c *DebugHooksCommand) getRelationNames1dot16() ([]string, error) {
63 err := c.ensureRawConn()
64 if err != nil {
65 return nil, err
66 }
67 unit, err := c.rawConn.State.Unit(c.Target)
68 if err != nil {
69 return nil, err
70 }
71 service, err := unit.Service()
72 if err != nil {
73 return nil, err
74 }
75 endpoints, err := service.Endpoints()
76 if err != nil {
77 return nil, err
78 }
79 relations := make([]string, len(endpoints))
80 for i, endpoint := range endpoints {
81 relations[i] = endpoint.Relation.Name
82 }
83 return relations, nil
84}
85
86func (c *DebugHooksCommand) getRelationNames(serviceName string) ([]string, error) {
87 relations, err := c.apiClient.ServiceCharmRelations(serviceName)
88 if params.IsCodeNotImplemented(err) {
89 logger.Infof("API server does not support Client.ServiceCharmRelations falling back to 1.16 compatibility mode (direct DB access)")
90 return c.getRelationNames1dot16()
91 }
92 if err != nil {
93 return nil, err
94 }
95 return relations, err
96}
97
98func (c *DebugHooksCommand) validateHooks() error {57func (c *DebugHooksCommand) validateHooks() error {
99 if len(c.hooks) == 0 {58 if len(c.hooks) == 0 {
100 return nil59 return nil
101 }60 }
102 service := names.UnitService(c.Target)61 service := names.UnitService(c.Target)
103 relations, err := c.getRelationNames(service)62 relations, err := c.apiClient.ServiceCharmRelations(service)
104 if err != nil {63 if err != nil {
105 return err64 return err
106 }65 }
10766
=== modified file 'cmd/juju/scp.go'
--- cmd/juju/scp.go 2014-03-20 02:30:15 +0000
+++ cmd/juju/scp.go 2014-04-02 06:17:00 +0000
@@ -106,5 +106,15 @@
106 targets = append(targets, arg)106 targets = append(targets, arg)
107 }107 }
108 }108 }
109 return ssh.Copy(targets, extraArgs, nil)109
110 var options *ssh.Options
111 if proxy, err := c.proxySSH(); err != nil {
112 return err
113 } else if proxy {
114 options = new(ssh.Options)
115 if err := c.setProxyCommand(options); err != nil {
116 return err
117 }
118 }
119 return ssh.Copy(targets, extraArgs, options)
110}120}
111121
=== modified file 'cmd/juju/scp_test.go'
--- cmd/juju/scp_test.go 2014-03-30 22:34:01 +0000
+++ cmd/juju/scp_test.go 2014-04-02 06:17:00 +0000
@@ -27,67 +27,64 @@
27 about string27 about string
28 args []string28 args []string
29 result string29 result string
30 proxy bool
30 error string31 error string
31}{32}{
32 {33 {
33 "scp from machine 0 to current dir",34 about: "scp from machine 0 to current dir",
34 []string{"0:foo", "."},35 args: []string{"0:foo", "."},
35 commonArgs + "ubuntu@dummyenv-0.dns:foo .\n",36 result: commonArgsNoProxy + "ubuntu@dummyenv-0.dns:foo .\n",
36 "",37 },
37 },38 {
38 {39 about: "scp from machine 0 to current dir with extra args",
39 "scp from machine 0 to current dir with extra args",40 args: []string{"0:foo", ".", "-rv -o SomeOption"},
40 []string{"0:foo", ".", "-rv -o SomeOption"},41 result: commonArgsNoProxy + "-rv -o SomeOption ubuntu@dummyenv-0.dns:foo .\n",
41 commonArgs + "-rv -o SomeOption ubuntu@dummyenv-0.dns:foo .\n",42 },
42 "",43 {
43 },44 about: "scp from current dir to machine 0",
44 {45 args: []string{"foo", "0:"},
45 "scp from current dir to machine 0",46 result: commonArgsNoProxy + "foo ubuntu@dummyenv-0.dns:\n",
46 []string{"foo", "0:"},47 },
47 commonArgs + "foo ubuntu@dummyenv-0.dns:\n",48 {
48 "",49 about: "scp from current dir to machine 0 with extra args",
49 },50 args: []string{"foo", "0:", "-r -v"},
50 {51 result: commonArgsNoProxy + "-r -v foo ubuntu@dummyenv-0.dns:\n",
51 "scp from current dir to machine 0 with extra args",52 },
52 []string{"foo", "0:", "-r -v"},53 {
53 commonArgs + "-r -v foo ubuntu@dummyenv-0.dns:\n",54 about: "scp from machine 0 to unit mysql/0",
54 "",55 args: []string{"0:foo", "mysql/0:/foo"},
55 },56 result: commonArgsNoProxy + "ubuntu@dummyenv-0.dns:foo ubuntu@dummyenv-0.dns:/foo\n",
56 {57 },
57 "scp from machine 0 to unit mysql/0",58 {
58 []string{"0:foo", "mysql/0:/foo"},59 about: "scp from machine 0 to unit mysql/0 and extra args",
59 commonArgs + "ubuntu@dummyenv-0.dns:foo ubuntu@dummyenv-0.dns:/foo\n",60 args: []string{"0:foo", "mysql/0:/foo", "-q"},
60 "",61 result: commonArgsNoProxy + "-q ubuntu@dummyenv-0.dns:foo ubuntu@dummyenv-0.dns:/foo\n",
61 },62 },
62 {63 {
63 "scp from machine 0 to unit mysql/0 and extra args",64 about: "scp from machine 0 to unit mysql/0 and extra args before",
64 []string{"0:foo", "mysql/0:/foo", "-q"},65 args: []string{"-q", "-r", "0:foo", "mysql/0:/foo"},
65 commonArgs + "-q ubuntu@dummyenv-0.dns:foo ubuntu@dummyenv-0.dns:/foo\n",66 error: `unexpected argument "-q"; extra arguments must be last`,
66 "",67 },
67 },68 {
68 {69 about: "scp two local files to unit mysql/0",
69 "scp from machine 0 to unit mysql/0 and extra args before",70 args: []string{"file1", "file2", "mysql/0:/foo/"},
70 []string{"-q", "-r", "0:foo", "mysql/0:/foo"},71 result: commonArgsNoProxy + "file1 file2 ubuntu@dummyenv-0.dns:/foo/\n",
71 "",72 },
72 `unexpected argument "-q"; extra arguments must be last`,73 {
73 },74 about: "scp from unit mongodb/1 to unit mongodb/0 and multiple extra args",
74 {75 args: []string{"mongodb/1:foo", "mongodb/0:", "-r -v -q -l5"},
75 "scp two local files to unit mysql/0",76 result: commonArgsNoProxy + "-r -v -q -l5 ubuntu@dummyenv-2.dns:foo ubuntu@dummyenv-1.dns:\n",
76 []string{"file1", "file2", "mysql/0:/foo/"},77 },
77 commonArgs + "file1 file2 ubuntu@dummyenv-0.dns:/foo/\n",78 {
78 "",79 about: "scp works with IPv6 addresses",
79 },80 args: []string{"ipv6-svc/0:foo", "bar"},
80 {81 result: commonArgsNoProxy + `ubuntu@\[2001:db8::\]:foo bar` + "\n",
81 "scp from unit mongodb/1 to unit mongodb/0 and multiple extra args",82 },
82 []string{"mongodb/1:foo", "mongodb/0:", "-r -v -q -l5"},83 {
83 commonArgs + "-r -v -q -l5 ubuntu@dummyenv-2.dns:foo ubuntu@dummyenv-1.dns:\n",84 about: "scp from machine 0 to unit mysql/0 with proxy",
84 "",85 args: []string{"0:foo", "mysql/0:/foo"},
85 },86 result: commonArgs + "ubuntu@dummyenv-0.dns:foo ubuntu@dummyenv-0.dns:/foo\n",
86 {87 proxy: true,
87 "scp works with IPv6 addresses",
88 []string{"ipv6-svc/0:foo", "bar"},
89 commonArgs + `ubuntu@\[2001:db8::\]:foo bar` + "\n",
90 "",
91 },88 },
92}89}
9390
@@ -122,6 +119,7 @@
122 c.Logf("test %d: %s -> %s\n", i, t.about, t.args)119 c.Logf("test %d: %s -> %s\n", i, t.about, t.args)
123 ctx := coretesting.Context(c)120 ctx := coretesting.Context(c)
124 scpcmd := &SCPCommand{}121 scpcmd := &SCPCommand{}
122 scpcmd.proxy = t.proxy
125123
126 err := scpcmd.Init(t.args)124 err := scpcmd.Init(t.args)
127 c.Check(err, gc.IsNil)125 c.Check(err, gc.IsNil)
128126
=== modified file 'cmd/juju/ssh.go'
--- cmd/juju/ssh.go 2014-04-01 04:53:43 +0000
+++ cmd/juju/ssh.go 2014-04-02 06:17:00 +0000
@@ -6,15 +6,19 @@
6import (6import (
7 "errors"7 "errors"
8 "fmt"8 "fmt"
9 "net"
10 "os"
11 "os/exec"
9 "time"12 "time"
1013
14 "launchpad.net/gnuflag"
15
11 "launchpad.net/juju-core/cmd"16 "launchpad.net/juju-core/cmd"
12 "launchpad.net/juju-core/cmd/envcmd"17 "launchpad.net/juju-core/cmd/envcmd"
13 "launchpad.net/juju-core/instance"18 "launchpad.net/juju-core/environs/config"
14 "launchpad.net/juju-core/juju"19 "launchpad.net/juju-core/juju"
15 "launchpad.net/juju-core/names"20 "launchpad.net/juju-core/names"
16 "launchpad.net/juju-core/state/api"21 "launchpad.net/juju-core/state/api"
17 "launchpad.net/juju-core/state/api/params"
18 "launchpad.net/juju-core/utils"22 "launchpad.net/juju-core/utils"
19 "launchpad.net/juju-core/utils/ssh"23 "launchpad.net/juju-core/utils/ssh"
20)24)
@@ -27,11 +31,32 @@
27// SSHCommon provides common methods for SSHCommand, SCPCommand and DebugHooksCommand.31// SSHCommon provides common methods for SSHCommand, SCPCommand and DebugHooksCommand.
28type SSHCommon struct {32type SSHCommon struct {
29 envcmd.EnvCommandBase33 envcmd.EnvCommandBase
34 proxy bool
35 pty bool
30 Target string36 Target string
31 Args []string37 Args []string
32 apiClient *api.Client38 apiClient *api.Client
33 // Only used for compatibility with 1.1639 apiAddr string
34 rawConn *juju.Conn40}
41
42func (c *SSHCommon) SetFlags(f *gnuflag.FlagSet) {
43 c.EnvCommandBase.SetFlags(f)
44 f.BoolVar(&c.proxy, "proxy", true, "proxy through the API server")
45 f.BoolVar(&c.pty, "pty", true, "enable pseudo-tty allocation")
46}
47
48// setProxyCommand sets the proxy command option.
49func (c *SSHCommon) setProxyCommand(options *ssh.Options) error {
50 apiServerHost, _, err := net.SplitHostPort(c.apiAddr)
51 if err != nil {
52 return fmt.Errorf("failed to get proxy address: %v", err)
53 }
54 juju, err := getJujuExecutable()
55 if err != nil {
56 return fmt.Errorf("failed to get juju executable path: %v", err)
57 }
58 options.SetProxyCommand(juju, "ssh", "--proxy=false", "--pty=false", apiServerHost, "nc", "-q0", "%h", "%p")
59 return nil
35}60}
3661
37const sshDoc = `62const sshDoc = `
@@ -80,23 +105,40 @@
80 return nil105 return nil
81}106}
82107
108// getJujuExecutable returns the path to the juju
109// executable, or an error if it could not be found.
110var getJujuExecutable = func() (string, error) {
111 return exec.LookPath(os.Args[0])
112}
113
83// Run resolves c.Target to a machine, to the address of a i114// Run resolves c.Target to a machine, to the address of a i
84// machine or unit forks ssh passing any arguments provided.115// machine or unit forks ssh passing any arguments provided.
85func (c *SSHCommand) Run(ctx *cmd.Context) error {116func (c *SSHCommand) Run(ctx *cmd.Context) error {
86 if c.apiClient == nil {117 if c.apiClient == nil {
87 var err error118 // If the apClient is not already opened and it is opened
88 c.apiClient, err = c.initAPIClient()119 // by ensureAPIClient, then close it when we're done.
89 if err != nil {120 defer func() {
90 return err121 if c.apiClient != nil {
91 }122 c.apiClient.Close()
92 defer c.apiClient.Close()123 c.apiClient = nil
124 }
125 }()
93 }126 }
94 host, err := c.hostFromTarget(c.Target)127 host, err := c.hostFromTarget(c.Target)
95 if err != nil {128 if err != nil {
96 return err129 return err
97 }130 }
98 var options ssh.Options131 var options ssh.Options
99 options.EnablePTY()132 if c.pty {
133 options.EnablePTY()
134 }
135 if proxy, err := c.proxySSH(); err != nil {
136 return err
137 } else if proxy {
138 if err := c.setProxyCommand(&options); err != nil {
139 return err
140 }
141 }
100 cmd := ssh.Command("ubuntu@"+host, c.Args, &options)142 cmd := ssh.Command("ubuntu@"+host, c.Args, &options)
101 cmd.Stdin = ctx.Stdin143 cmd.Stdin = ctx.Stdin
102 cmd.Stdout = ctx.Stdout144 cmd.Stdout = ctx.Stdout
@@ -104,12 +146,45 @@
104 return cmd.Run()146 return cmd.Run()
105}147}
106148
149// proxySSH returns true iff both c.proxy and
150// the proxy-ssh environment configuration
151// are true.
152func (c *SSHCommon) proxySSH() (bool, error) {
153 if !c.proxy {
154 return false, nil
155 }
156 if _, err := c.ensureAPIClient(); err != nil {
157 return false, err
158 }
159 var cfg *config.Config
160 attrs, err := c.apiClient.EnvironmentGet()
161 if err == nil {
162 cfg, err = config.New(config.NoDefaults, attrs)
163 }
164 if err != nil {
165 return false, err
166 }
167 logger.Debugf("proxy-ssh is %v", cfg.ProxySSH())
168 return cfg.ProxySSH(), nil
169}
170
171func (c *SSHCommon) ensureAPIClient() (*api.Client, error) {
172 if c.apiClient != nil {
173 return c.apiClient, nil
174 }
175 return c.initAPIClient()
176}
177
107// initAPIClient initialises the API connection.178// initAPIClient initialises the API connection.
108// It is the caller's responsibility to close the connection.179// It is the caller's responsibility to close the connection.
109func (c *SSHCommon) initAPIClient() (*api.Client, error) {180func (c *SSHCommon) initAPIClient() (*api.Client, error) {
110 var err error181 st, err := juju.NewAPIFromName(c.EnvName)
111 c.apiClient, err = juju.NewAPIClientFromName(c.EnvName)182 if err != nil {
112 return c.apiClient, err183 return nil, err
184 }
185 c.apiClient = st.Client()
186 c.apiAddr = st.Addr()
187 return c.apiClient, nil
113}188}
114189
115// attemptStarter is an interface corresponding to utils.AttemptStrategy190// attemptStarter is an interface corresponding to utils.AttemptStrategy
@@ -132,86 +207,31 @@
132 Delay: 500 * time.Millisecond,207 Delay: 500 * time.Millisecond,
133}208}
134209
135// ensureRawConn ensures that c.rawConn is valid (or returns an error)
136// This is only for compatibility with a 1.16 API server (that doesn't have
137// some of the API added more recently.) It can be removed once we no longer
138// need compatibility with direct access to the state database
139func (c *SSHCommon) ensureRawConn() error {
140 if c.rawConn != nil {
141 return nil
142 }
143 var err error
144 c.rawConn, err = juju.NewConnFromName(c.EnvName)
145 return err
146}
147
148func (c *SSHCommon) hostFromTarget1dot16(target string) (string, error) {
149 err := c.ensureRawConn()
150 if err != nil {
151 return "", err
152 }
153 // is the target the id of a machine ?
154 if names.IsMachine(target) {
155 logger.Infof("looking up address for machine %s...", target)
156 // This is not the exact code from the 1.16 client
157 // (machinePublicAddress), however it is the code used in the
158 // apiserver behind the PublicAddress call. (1.16 didn't know
159 // about SelectPublicAddress)
160 // The old code watched for changes on the Machine until it had
161 // an InstanceId and then would return the instance.WaitDNS()
162 machine, err := c.rawConn.State.Machine(target)
163 if err != nil {
164 return "", err
165 }
166 addr := instance.SelectPublicAddress(machine.Addresses())
167 if addr == "" {
168 return "", fmt.Errorf("machine %q has no public address", machine)
169 }
170 return addr, nil
171 }
172 // maybe the target is a unit ?
173 if names.IsUnit(target) {
174 logger.Infof("looking up address for unit %q...", c.Target)
175 unit, err := c.rawConn.State.Unit(target)
176 if err != nil {
177 return "", err
178 }
179 addr, ok := unit.PublicAddress()
180 if !ok {
181 return "", fmt.Errorf("unit %q has no public address", unit)
182 }
183 return addr, nil
184 }
185 return "", fmt.Errorf("unknown unit or machine %q", target)
186}
187
188func (c *SSHCommon) hostFromTarget(target string) (string, error) {210func (c *SSHCommon) hostFromTarget(target string) (string, error) {
189 var addr string211 // If the target is neither a machine nor a unit,
190 var err error212 // assume it's a hostname and try it directly.
191 var useStateConn bool213 if !names.IsMachine(target) && !names.IsUnit(target) {
214 return target, nil
215 }
192 // A target may not initially have an address (e.g. the216 // A target may not initially have an address (e.g. the
193 // address updater hasn't yet run), so we must do this in217 // address updater hasn't yet run), so we must do this in
194 // a loop.218 // a loop.
219 if _, err := c.ensureAPIClient(); err != nil {
220 return "", err
221 }
222 var err error
195 for a := sshHostFromTargetAttemptStrategy.Start(); a.Next(); {223 for a := sshHostFromTargetAttemptStrategy.Start(); a.Next(); {
196 if !useStateConn {224 var addr string
225 if c.proxy {
226 addr, err = c.apiClient.PrivateAddress(target)
227 } else {
197 addr, err = c.apiClient.PublicAddress(target)228 addr, err = c.apiClient.PublicAddress(target)
198 if params.IsCodeNotImplemented(err) {
199 logger.Infof("API server does not support Client.PublicAddress falling back to 1.16 compatibility mode (direct DB access)")
200 useStateConn = true
201 }
202 }
203 if useStateConn {
204 addr, err = c.hostFromTarget1dot16(target)
205 }229 }
206 if err == nil {230 if err == nil {
207 break231 return addr, nil
208 }232 }
209 }233 }
210 if err != nil {234 return "", err
211 return "", err
212 }
213 logger.Infof("Resolved public address of %q: %q", target, addr)
214 return addr, nil
215}235}
216236
217// AllowInterspersedFlags for ssh/scp is set to false so that237// AllowInterspersedFlags for ssh/scp is set to false so that
218238
=== modified file 'cmd/juju/ssh_test.go'
--- cmd/juju/ssh_test.go 2014-04-01 03:37:13 +0000
+++ cmd/juju/ssh_test.go 2014-04-02 06:17:00 +0000
@@ -39,6 +39,7 @@
3939
40func (s *SSHCommonSuite) SetUpTest(c *gc.C) {40func (s *SSHCommonSuite) SetUpTest(c *gc.C) {
41 s.JujuConnSuite.SetUpTest(c)41 s.JujuConnSuite.SetUpTest(c)
42 s.PatchValue(&getJujuExecutable, func() (string, error) { return "juju", nil })
4243
43 s.bin = c.MkDir()44 s.bin = c.MkDir()
44 s.PatchEnvPathPrepend(s.bin)45 s.PatchEnvPathPrepend(s.bin)
@@ -53,8 +54,10 @@
53}54}
5455
55const (56const (
56 commonArgs = `-o StrictHostKeyChecking no -o PasswordAuthentication no `57 commonArgsNoProxy = `-o StrictHostKeyChecking no -o PasswordAuthentication no `
57 sshArgs = commonArgs + `-t -t `58 commonArgs = `-o StrictHostKeyChecking no -o ProxyCommand juju ssh --proxy=false --pty=false 127.0.0.1 nc -q0 %h %p -o PasswordAuthentication no `
59 sshArgs = commonArgs + `-t -t `
60 sshArgsNoProxy = commonArgsNoProxy + `-t -t `
58)61)
5962
60var sshTests = []struct {63var sshTests = []struct {
@@ -82,6 +85,11 @@
82 []string{"ssh", "mongodb/1", "ls", "/"},85 []string{"ssh", "mongodb/1", "ls", "/"},
83 sshArgs + "ubuntu@dummyenv-2.dns ls /\n",86 sshArgs + "ubuntu@dummyenv-2.dns ls /\n",
84 },87 },
88 {
89 "connect to unit mysql/0 without proxy",
90 []string{"ssh", "--proxy=false", "mysql/0"},
91 sshArgsNoProxy + "ubuntu@dummyenv-0.dns\n",
92 },
85}93}
8694
87func (s *SSHSuite) TestSSHCommand(c *gc.C) {95func (s *SSHSuite) TestSSHCommand(c *gc.C) {
@@ -114,6 +122,20 @@
114 }122 }
115}123}
116124
125func (s *SSHSuite) TestSSHCommandEnvironProxySSH(c *gc.C) {
126 s.makeMachines(1, c, true)
127 // Setting proxy-ssh=false in the environment overrides --proxy.
128 err := s.State.UpdateEnvironConfig(map[string]interface{}{"proxy-ssh": false}, nil, nil)
129 c.Assert(err, gc.IsNil)
130 ctx := coretesting.Context(c)
131 jujucmd := cmd.NewSuperCommand(cmd.SuperCommandParams{})
132 jujucmd.Register(&SSHCommand{})
133 code := cmd.Main(jujucmd, ctx, []string{"ssh", "0"})
134 c.Check(code, gc.Equals, 0)
135 c.Check(ctx.Stderr.(*bytes.Buffer).String(), gc.Equals, "")
136 c.Check(ctx.Stdout.(*bytes.Buffer).String(), gc.Equals, sshArgsNoProxy+"ubuntu@dummyenv-0.dns\n")
137}
138
117type callbackAttemptStarter struct {139type callbackAttemptStarter struct {
118 next func() bool140 next func() bool
119}141}
@@ -131,10 +153,16 @@
131}153}
132154
133func (s *SSHSuite) TestSSHCommandHostAddressRetry(c *gc.C) {155func (s *SSHSuite) TestSSHCommandHostAddressRetry(c *gc.C) {
156 s.testSSHCommandHostAddressRetry(c, false)
157}
158
159func (s *SSHSuite) TestSSHCommandHostAddressRetryProxy(c *gc.C) {
160 s.testSSHCommandHostAddressRetry(c, true)
161}
162
163func (s *SSHSuite) testSSHCommandHostAddressRetry(c *gc.C, proxy bool) {
134 m := s.makeMachines(1, c, false)164 m := s.makeMachines(1, c, false)
135 ctx := coretesting.Context(c)165 ctx := coretesting.Context(c)
136 jujucmd := cmd.NewSuperCommand(cmd.SuperCommandParams{})
137 jujucmd.Register(&SSHCommand{})
138166
139 var called int167 var called int
140 next := func() bool {168 next := func() bool {
@@ -146,18 +174,21 @@
146174
147 // Ensure that the ssh command waits for a public address, or the attempt175 // Ensure that the ssh command waits for a public address, or the attempt
148 // strategy's Done method returns false.176 // strategy's Done method returns false.
149 code := cmd.Main(jujucmd, ctx, []string{"ssh", "0"})177 args := []string{"--proxy=" + fmt.Sprint(proxy), "0"}
178 code := cmd.Main(&SSHCommand{}, ctx, args)
150 c.Check(code, gc.Equals, 1)179 c.Check(code, gc.Equals, 1)
151 c.Assert(called, gc.Equals, 2)180 c.Assert(called, gc.Equals, 2)
152 called = 0181 called = 0
153 attemptStarter.next = func() bool {182 attemptStarter.next = func() bool {
154 called++183 called++
155 s.setAddress(m[0], c)184 if called > 1 {
156 return false185 s.setAddress(m[0], c)
186 }
187 return true
157 }188 }
158 code = cmd.Main(jujucmd, ctx, []string{"ssh", "0"})189 code = cmd.Main(&SSHCommand{}, ctx, args)
159 c.Check(code, gc.Equals, 0)190 c.Check(code, gc.Equals, 0)
160 c.Assert(called, gc.Equals, 1)191 c.Assert(called, gc.Equals, 2)
161}192}
162193
163func (s *SSHCommonSuite) setAddress(m *state.Machine, c *gc.C) {194func (s *SSHCommonSuite) setAddress(m *state.Machine, c *gc.C) {
164195
=== modified file 'environs/config/config.go'
--- environs/config/config.go 2014-03-20 22:35:39 +0000
+++ environs/config/config.go 2014-04-02 06:17:00 +0000
@@ -442,6 +442,13 @@
442 return c.mustString("authorized-keys")442 return c.mustString("authorized-keys")
443}443}
444444
445// ProxySSH returns a flag indicating whether SSH commands
446// should be proxied through the API server.
447func (c *Config) ProxySSH() bool {
448 value, _ := c.defined["proxy-ssh"].(bool)
449 return value
450}
451
445// ProxySettings returns all four proxy settings; http, https, ftp, and no452// ProxySettings returns all four proxy settings; http, https, ftp, and no
446// proxy.453// proxy.
447func (c *Config) ProxySettings() osenv.ProxySettings {454func (c *Config) ProxySettings() osenv.ProxySettings {
@@ -716,6 +723,7 @@
716 "bootstrap-retry-delay": schema.ForceInt(),723 "bootstrap-retry-delay": schema.ForceInt(),
717 "bootstrap-addresses-delay": schema.ForceInt(),724 "bootstrap-addresses-delay": schema.ForceInt(),
718 "test-mode": schema.Bool(),725 "test-mode": schema.Bool(),
726 "proxy-ssh": schema.Bool(),
719727
720 // Deprecated fields, retain for backwards compatibility.728 // Deprecated fields, retain for backwards compatibility.
721 "tools-url": schema.String(),729 "tools-url": schema.String(),
@@ -773,6 +781,7 @@
773 // Previously image-stream could be set to an empty value781 // Previously image-stream could be set to an empty value
774 "image-stream": "",782 "image-stream": "",
775 "test-mode": false,783 "test-mode": false,
784 "proxy-ssh": false,
776}785}
777786
778func allowEmpty(attr string) bool {787func allowEmpty(attr string) bool {
@@ -796,6 +805,7 @@
796 "bootstrap-timeout": DefaultBootstrapSSHTimeout,805 "bootstrap-timeout": DefaultBootstrapSSHTimeout,
797 "bootstrap-retry-delay": DefaultBootstrapSSHRetryDelay,806 "bootstrap-retry-delay": DefaultBootstrapSSHRetryDelay,
798 "bootstrap-addresses-delay": DefaultBootstrapSSHAddressesDelay,807 "bootstrap-addresses-delay": DefaultBootstrapSSHAddressesDelay,
808 "proxy-ssh": true,
799 }809 }
800 for attr, val := range alwaysOptional {810 for attr, val := range alwaysOptional {
801 if _, ok := d[attr]; !ok {811 if _, ok := d[attr]; !ok {
802812
=== modified file 'environs/config/config_test.go'
--- environs/config/config_test.go 2014-03-14 02:10:35 +0000
+++ environs/config/config_test.go 2014-04-02 06:17:00 +0000
@@ -1026,6 +1026,7 @@
1026 attrs["tools-metadata-url"] = ""1026 attrs["tools-metadata-url"] = ""
1027 attrs["tools-url"] = ""1027 attrs["tools-url"] = ""
1028 attrs["image-stream"] = ""1028 attrs["image-stream"] = ""
1029 attrs["proxy-ssh"] = false
10291030
1030 // Default firewall mode is instance1031 // Default firewall mode is instance
1031 attrs["firewall-mode"] = string(config.FwInstance)1032 attrs["firewall-mode"] = string(config.FwInstance)
10321033
=== modified file 'provider/local/environprovider.go'
--- provider/local/environprovider.go 2014-03-31 03:36:17 +0000
+++ provider/local/environprovider.go 2014-04-02 06:17:00 +0000
@@ -103,7 +103,13 @@
103 }103 }
104 // If the user has specified no values for any of the three normal104 // If the user has specified no values for any of the three normal
105 // proxies, then look in the environment and set them.105 // proxies, then look in the environment and set them.
106 attrs := make(map[string]interface{})106 attrs := map[string]interface{}{
107 // We must not proxy SSH through the API server in a
108 // local provider environment. Besides not being useful,
109 // it may not work; there is no requirement for sshd to
110 // be available on machine-0.
111 "proxy-ssh": false,
112 }
107 setIfNotBlank := func(key, value string) {113 setIfNotBlank := func(key, value string) {
108 if value != "" {114 if value != "" {
109 attrs[key] = value115 attrs[key] = value
110116
=== modified file 'provider/local/environprovider_test.go'
--- provider/local/environprovider_test.go 2014-03-19 04:06:58 +0000
+++ provider/local/environprovider_test.go 2014-04-02 06:17:00 +0000
@@ -380,3 +380,19 @@
380 c.Assert(value, gc.Equals, test.expectAUFS)380 c.Assert(value, gc.Equals, test.expectAUFS)
381 }381 }
382}382}
383
384func (s *prepareSuite) TestPrepareProxySSH(c *gc.C) {
385 s.PatchValue(local.DetectAptProxies, func() (osenv.ProxySettings, error) {
386 return osenv.ProxySettings{}, nil
387 })
388 basecfg, err := config.New(config.UseDefaults, map[string]interface{}{
389 "type": "local",
390 "name": "test",
391 })
392 provider, err := environs.Provider("local")
393 c.Assert(err, gc.IsNil)
394 env, err := provider.Prepare(coretesting.Context(c), basecfg)
395 c.Assert(err, gc.IsNil)
396 // local provider sets proxy-ssh to false
397 c.Assert(env.Config().ProxySSH(), gc.Equals, false)
398}
383399
=== modified file 'utils/ssh/ssh_gocrypto.go'
--- utils/ssh/ssh_gocrypto.go 2014-02-24 16:21:43 +0000
+++ utils/ssh/ssh_gocrypto.go 2014-04-02 06:17:00 +0000
@@ -7,6 +7,9 @@
7 "fmt"7 "fmt"
8 "io"8 "io"
9 "io/ioutil"9 "io/ioutil"
10 "net"
11 "os"
12 "os/exec"
10 "os/user"13 "os/user"
11 "strings"14 "strings"
1215
@@ -45,17 +48,20 @@
45 }48 }
46 user, host := splitUserHost(host)49 user, host := splitUserHost(host)
47 port := sshDefaultPort50 port := sshDefaultPort
51 var proxyCommand []string
48 if options != nil {52 if options != nil {
49 if options.port != 0 {53 if options.port != 0 {
50 port = options.port54 port = options.port
51 }55 }
56 proxyCommand = options.proxyCommand
52 }57 }
53 logger.Debugf(`running (equivalent of): ssh "%s@%s" -p %d '%s'`, user, host, port, shellCommand)58 logger.Debugf(`running (equivalent of): ssh "%s@%s" -p %d '%s'`, user, host, port, shellCommand)
54 return &Cmd{impl: &goCryptoCommand{59 return &Cmd{impl: &goCryptoCommand{
55 signers: signers,60 signers: signers,
56 user: user,61 user: user,
57 addr: fmt.Sprintf("%s:%d", host, port),62 addr: fmt.Sprintf("%s:%d", host, port),
58 command: shellCommand,63 command: shellCommand,
64 proxyCommand: proxyCommand,
59 }}65 }}
60}66}
6167
@@ -67,19 +73,50 @@
67}73}
6874
69type goCryptoCommand struct {75type goCryptoCommand struct {
70 signers []ssh.Signer76 signers []ssh.Signer
71 user string77 user string
72 addr string78 addr string
73 command string79 command string
74 stdin io.Reader80 proxyCommand []string
75 stdout io.Writer81 stdin io.Reader
76 stderr io.Writer82 stdout io.Writer
77 conn *ssh.ClientConn83 stderr io.Writer
78 sess *ssh.Session84 conn *ssh.ClientConn
85 sess *ssh.Session
79}86}
8087
81var sshDial = ssh.Dial88var sshDial = ssh.Dial
8289
90var sshDialWithProxy = func(addr string, proxyCommand []string, config *ssh.ClientConfig) (*ssh.ClientConn, error) {
91 if len(proxyCommand) == 0 {
92 return sshDial("tcp", addr, config)
93 }
94 // User has specified a proxy. Create a pipe and
95 // redirect the proxy command's stdin/stdout to it.
96 host, port, err := net.SplitHostPort(addr)
97 if err != nil {
98 host = addr
99 }
100 for i, arg := range proxyCommand {
101 arg = strings.Replace(arg, "%h", host, -1)
102 if port != "" {
103 arg = strings.Replace(arg, "%p", port, -1)
104 }
105 arg = strings.Replace(arg, "%r", config.User, -1)
106 proxyCommand[i] = arg
107 }
108 client, server := net.Pipe()
109 logger.Debugf(`executing proxy command %q`, proxyCommand)
110 cmd := exec.Command(proxyCommand[0], proxyCommand[1:]...)
111 cmd.Stdin = server
112 cmd.Stdout = server
113 cmd.Stderr = os.Stderr
114 if err := cmd.Start(); err != nil {
115 return nil, err
116 }
117 return ssh.Client(client, config)
118}
119
83func (c *goCryptoCommand) ensureSession() (*ssh.Session, error) {120func (c *goCryptoCommand) ensureSession() (*ssh.Session, error) {
84 if c.sess != nil {121 if c.sess != nil {
85 return c.sess, nil122 return c.sess, nil
@@ -100,7 +137,7 @@
100 ssh.ClientAuthKeyring(keyring{c.signers}),137 ssh.ClientAuthKeyring(keyring{c.signers}),
101 },138 },
102 }139 }
103 conn, err := sshDial("tcp", c.addr, config)140 conn, err := sshDialWithProxy(c.addr, c.proxyCommand, config)
104 if err != nil {141 if err != nil {
105 return nil, err142 return nil, err
106 }143 }
107144
=== modified file 'utils/ssh/ssh_gocrypto_test.go'
--- utils/ssh/ssh_gocrypto_test.go 2014-03-13 07:54:56 +0000
+++ utils/ssh/ssh_gocrypto_test.go 2014-04-02 06:17:00 +0000
@@ -6,7 +6,11 @@
6import (6import (
7 "encoding/binary"7 "encoding/binary"
8 "errors"8 "errors"
9 "fmt"
10 "io/ioutil"
9 "net"11 "net"
12 "os/exec"
13 "path/filepath"
10 "sync"14 "sync"
1115
12 cryptossh "code.google.com/p/go.crypto/ssh"16 cryptossh "code.google.com/p/go.crypto/ssh"
@@ -148,3 +152,38 @@
148 err = client.Copy([]string{"0.1.2.3:b", c.MkDir()}, nil, nil)152 err = client.Copy([]string{"0.1.2.3:b", c.MkDir()}, nil, nil)
149 c.Assert(err, gc.ErrorMatches, `scp command is not implemented \(OpenSSH scp not available in PATH\)`)153 c.Assert(err, gc.ErrorMatches, `scp command is not implemented \(OpenSSH scp not available in PATH\)`)
150}154}
155
156func (s *SSHGoCryptoCommandSuite) TestProxyCommand(c *gc.C) {
157 realNetcat, err := exec.LookPath("nc")
158 if err != nil {
159 c.Skip("skipping test, couldn't find netcat: %v")
160 return
161 }
162 netcat := filepath.Join(c.MkDir(), "nc")
163 err = ioutil.WriteFile(netcat, []byte("#!/bin/sh\necho $0 \"$@\" > $0.args && exec "+realNetcat+" \"$@\""), 0755)
164 c.Assert(err, gc.IsNil)
165
166 private, _, err := ssh.GenerateKey("test-server")
167 c.Assert(err, gc.IsNil)
168 key, err := cryptossh.ParsePrivateKey([]byte(private))
169 client, err := ssh.NewGoCryptoClient(key)
170 c.Assert(err, gc.IsNil)
171 server := newServer(c)
172 var opts ssh.Options
173 port := server.Addr().(*net.TCPAddr).Port
174 opts.SetProxyCommand(netcat, "-q0", "%h", "%p")
175 opts.SetPort(port)
176 cmd := client.Command("127.0.0.1", testCommand, &opts)
177 server.cfg.PublicKeyCallback = func(conn *cryptossh.ServerConn, user, algo string, pubkey []byte) bool {
178 return true
179 }
180 go server.run(c)
181 out, err := cmd.Output()
182 c.Assert(err, gc.ErrorMatches, "ssh: could not execute command.*")
183 // TODO(axw) when gosshnew is ready, expect reply from server.
184 c.Assert(out, gc.IsNil)
185 // Ensure the proxy command was executed with the appropriate arguments.
186 data, err := ioutil.ReadFile(netcat + ".args")
187 c.Assert(err, gc.IsNil)
188 c.Assert(string(data), gc.Equals, fmt.Sprintf("%s -q0 127.0.0.1 %v\n", netcat, port))
189}

Subscribers

People subscribed via source and target branches

to status/vote changes: