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
1=== modified file 'cmd/juju/debughooks.go'
2--- cmd/juju/debughooks.go 2013-12-17 18:21:26 +0000
3+++ cmd/juju/debughooks.go 2014-04-02 06:17:00 +0000
4@@ -12,7 +12,6 @@
5 "launchpad.net/juju-core/charm/hooks"
6 "launchpad.net/juju-core/cmd"
7 "launchpad.net/juju-core/names"
8- "launchpad.net/juju-core/state/api/params"
9 unitdebug "launchpad.net/juju-core/worker/uniter/debug"
10 )
11
12@@ -55,52 +54,12 @@
13 return nil
14 }
15
16-// getRelationNames1dot16 gets the list of relation hooks directly from the
17-// database, in a fashion compatible with the API server in Juju 1.16 (which
18-// doesn't have the ServiceCharmRelations API). This function can be removed
19-// when we no longer maintain compatibility with 1.16
20-func (c *DebugHooksCommand) getRelationNames1dot16() ([]string, error) {
21- err := c.ensureRawConn()
22- if err != nil {
23- return nil, err
24- }
25- unit, err := c.rawConn.State.Unit(c.Target)
26- if err != nil {
27- return nil, err
28- }
29- service, err := unit.Service()
30- if err != nil {
31- return nil, err
32- }
33- endpoints, err := service.Endpoints()
34- if err != nil {
35- return nil, err
36- }
37- relations := make([]string, len(endpoints))
38- for i, endpoint := range endpoints {
39- relations[i] = endpoint.Relation.Name
40- }
41- return relations, nil
42-}
43-
44-func (c *DebugHooksCommand) getRelationNames(serviceName string) ([]string, error) {
45- relations, err := c.apiClient.ServiceCharmRelations(serviceName)
46- if params.IsCodeNotImplemented(err) {
47- logger.Infof("API server does not support Client.ServiceCharmRelations falling back to 1.16 compatibility mode (direct DB access)")
48- return c.getRelationNames1dot16()
49- }
50- if err != nil {
51- return nil, err
52- }
53- return relations, err
54-}
55-
56 func (c *DebugHooksCommand) validateHooks() error {
57 if len(c.hooks) == 0 {
58 return nil
59 }
60 service := names.UnitService(c.Target)
61- relations, err := c.getRelationNames(service)
62+ relations, err := c.apiClient.ServiceCharmRelations(service)
63 if err != nil {
64 return err
65 }
66
67=== modified file 'cmd/juju/scp.go'
68--- cmd/juju/scp.go 2014-03-20 02:30:15 +0000
69+++ cmd/juju/scp.go 2014-04-02 06:17:00 +0000
70@@ -106,5 +106,15 @@
71 targets = append(targets, arg)
72 }
73 }
74- return ssh.Copy(targets, extraArgs, nil)
75+
76+ var options *ssh.Options
77+ if proxy, err := c.proxySSH(); err != nil {
78+ return err
79+ } else if proxy {
80+ options = new(ssh.Options)
81+ if err := c.setProxyCommand(options); err != nil {
82+ return err
83+ }
84+ }
85+ return ssh.Copy(targets, extraArgs, options)
86 }
87
88=== modified file 'cmd/juju/scp_test.go'
89--- cmd/juju/scp_test.go 2014-03-30 22:34:01 +0000
90+++ cmd/juju/scp_test.go 2014-04-02 06:17:00 +0000
91@@ -27,67 +27,64 @@
92 about string
93 args []string
94 result string
95+ proxy bool
96 error string
97 }{
98 {
99- "scp from machine 0 to current dir",
100- []string{"0:foo", "."},
101- commonArgs + "ubuntu@dummyenv-0.dns:foo .\n",
102- "",
103- },
104- {
105- "scp from machine 0 to current dir with extra args",
106- []string{"0:foo", ".", "-rv -o SomeOption"},
107- commonArgs + "-rv -o SomeOption ubuntu@dummyenv-0.dns:foo .\n",
108- "",
109- },
110- {
111- "scp from current dir to machine 0",
112- []string{"foo", "0:"},
113- commonArgs + "foo ubuntu@dummyenv-0.dns:\n",
114- "",
115- },
116- {
117- "scp from current dir to machine 0 with extra args",
118- []string{"foo", "0:", "-r -v"},
119- commonArgs + "-r -v foo ubuntu@dummyenv-0.dns:\n",
120- "",
121- },
122- {
123- "scp from machine 0 to unit mysql/0",
124- []string{"0:foo", "mysql/0:/foo"},
125- commonArgs + "ubuntu@dummyenv-0.dns:foo ubuntu@dummyenv-0.dns:/foo\n",
126- "",
127- },
128- {
129- "scp from machine 0 to unit mysql/0 and extra args",
130- []string{"0:foo", "mysql/0:/foo", "-q"},
131- commonArgs + "-q ubuntu@dummyenv-0.dns:foo ubuntu@dummyenv-0.dns:/foo\n",
132- "",
133- },
134- {
135- "scp from machine 0 to unit mysql/0 and extra args before",
136- []string{"-q", "-r", "0:foo", "mysql/0:/foo"},
137- "",
138- `unexpected argument "-q"; extra arguments must be last`,
139- },
140- {
141- "scp two local files to unit mysql/0",
142- []string{"file1", "file2", "mysql/0:/foo/"},
143- commonArgs + "file1 file2 ubuntu@dummyenv-0.dns:/foo/\n",
144- "",
145- },
146- {
147- "scp from unit mongodb/1 to unit mongodb/0 and multiple extra args",
148- []string{"mongodb/1:foo", "mongodb/0:", "-r -v -q -l5"},
149- commonArgs + "-r -v -q -l5 ubuntu@dummyenv-2.dns:foo ubuntu@dummyenv-1.dns:\n",
150- "",
151- },
152- {
153- "scp works with IPv6 addresses",
154- []string{"ipv6-svc/0:foo", "bar"},
155- commonArgs + `ubuntu@\[2001:db8::\]:foo bar` + "\n",
156- "",
157+ about: "scp from machine 0 to current dir",
158+ args: []string{"0:foo", "."},
159+ result: commonArgsNoProxy + "ubuntu@dummyenv-0.dns:foo .\n",
160+ },
161+ {
162+ about: "scp from machine 0 to current dir with extra args",
163+ args: []string{"0:foo", ".", "-rv -o SomeOption"},
164+ result: commonArgsNoProxy + "-rv -o SomeOption ubuntu@dummyenv-0.dns:foo .\n",
165+ },
166+ {
167+ about: "scp from current dir to machine 0",
168+ args: []string{"foo", "0:"},
169+ result: commonArgsNoProxy + "foo ubuntu@dummyenv-0.dns:\n",
170+ },
171+ {
172+ about: "scp from current dir to machine 0 with extra args",
173+ args: []string{"foo", "0:", "-r -v"},
174+ result: commonArgsNoProxy + "-r -v foo ubuntu@dummyenv-0.dns:\n",
175+ },
176+ {
177+ about: "scp from machine 0 to unit mysql/0",
178+ args: []string{"0:foo", "mysql/0:/foo"},
179+ result: commonArgsNoProxy + "ubuntu@dummyenv-0.dns:foo ubuntu@dummyenv-0.dns:/foo\n",
180+ },
181+ {
182+ about: "scp from machine 0 to unit mysql/0 and extra args",
183+ args: []string{"0:foo", "mysql/0:/foo", "-q"},
184+ result: commonArgsNoProxy + "-q ubuntu@dummyenv-0.dns:foo ubuntu@dummyenv-0.dns:/foo\n",
185+ },
186+ {
187+ about: "scp from machine 0 to unit mysql/0 and extra args before",
188+ args: []string{"-q", "-r", "0:foo", "mysql/0:/foo"},
189+ error: `unexpected argument "-q"; extra arguments must be last`,
190+ },
191+ {
192+ about: "scp two local files to unit mysql/0",
193+ args: []string{"file1", "file2", "mysql/0:/foo/"},
194+ result: commonArgsNoProxy + "file1 file2 ubuntu@dummyenv-0.dns:/foo/\n",
195+ },
196+ {
197+ about: "scp from unit mongodb/1 to unit mongodb/0 and multiple extra args",
198+ args: []string{"mongodb/1:foo", "mongodb/0:", "-r -v -q -l5"},
199+ result: commonArgsNoProxy + "-r -v -q -l5 ubuntu@dummyenv-2.dns:foo ubuntu@dummyenv-1.dns:\n",
200+ },
201+ {
202+ about: "scp works with IPv6 addresses",
203+ args: []string{"ipv6-svc/0:foo", "bar"},
204+ result: commonArgsNoProxy + `ubuntu@\[2001:db8::\]:foo bar` + "\n",
205+ },
206+ {
207+ about: "scp from machine 0 to unit mysql/0 with proxy",
208+ args: []string{"0:foo", "mysql/0:/foo"},
209+ result: commonArgs + "ubuntu@dummyenv-0.dns:foo ubuntu@dummyenv-0.dns:/foo\n",
210+ proxy: true,
211 },
212 }
213
214@@ -122,6 +119,7 @@
215 c.Logf("test %d: %s -> %s\n", i, t.about, t.args)
216 ctx := coretesting.Context(c)
217 scpcmd := &SCPCommand{}
218+ scpcmd.proxy = t.proxy
219
220 err := scpcmd.Init(t.args)
221 c.Check(err, gc.IsNil)
222
223=== modified file 'cmd/juju/ssh.go'
224--- cmd/juju/ssh.go 2014-04-01 04:53:43 +0000
225+++ cmd/juju/ssh.go 2014-04-02 06:17:00 +0000
226@@ -6,15 +6,19 @@
227 import (
228 "errors"
229 "fmt"
230+ "net"
231+ "os"
232+ "os/exec"
233 "time"
234
235+ "launchpad.net/gnuflag"
236+
237 "launchpad.net/juju-core/cmd"
238 "launchpad.net/juju-core/cmd/envcmd"
239- "launchpad.net/juju-core/instance"
240+ "launchpad.net/juju-core/environs/config"
241 "launchpad.net/juju-core/juju"
242 "launchpad.net/juju-core/names"
243 "launchpad.net/juju-core/state/api"
244- "launchpad.net/juju-core/state/api/params"
245 "launchpad.net/juju-core/utils"
246 "launchpad.net/juju-core/utils/ssh"
247 )
248@@ -27,11 +31,32 @@
249 // SSHCommon provides common methods for SSHCommand, SCPCommand and DebugHooksCommand.
250 type SSHCommon struct {
251 envcmd.EnvCommandBase
252+ proxy bool
253+ pty bool
254 Target string
255 Args []string
256 apiClient *api.Client
257- // Only used for compatibility with 1.16
258- rawConn *juju.Conn
259+ apiAddr string
260+}
261+
262+func (c *SSHCommon) SetFlags(f *gnuflag.FlagSet) {
263+ c.EnvCommandBase.SetFlags(f)
264+ f.BoolVar(&c.proxy, "proxy", true, "proxy through the API server")
265+ f.BoolVar(&c.pty, "pty", true, "enable pseudo-tty allocation")
266+}
267+
268+// setProxyCommand sets the proxy command option.
269+func (c *SSHCommon) setProxyCommand(options *ssh.Options) error {
270+ apiServerHost, _, err := net.SplitHostPort(c.apiAddr)
271+ if err != nil {
272+ return fmt.Errorf("failed to get proxy address: %v", err)
273+ }
274+ juju, err := getJujuExecutable()
275+ if err != nil {
276+ return fmt.Errorf("failed to get juju executable path: %v", err)
277+ }
278+ options.SetProxyCommand(juju, "ssh", "--proxy=false", "--pty=false", apiServerHost, "nc", "-q0", "%h", "%p")
279+ return nil
280 }
281
282 const sshDoc = `
283@@ -80,23 +105,40 @@
284 return nil
285 }
286
287+// getJujuExecutable returns the path to the juju
288+// executable, or an error if it could not be found.
289+var getJujuExecutable = func() (string, error) {
290+ return exec.LookPath(os.Args[0])
291+}
292+
293 // Run resolves c.Target to a machine, to the address of a i
294 // machine or unit forks ssh passing any arguments provided.
295 func (c *SSHCommand) Run(ctx *cmd.Context) error {
296 if c.apiClient == nil {
297- var err error
298- c.apiClient, err = c.initAPIClient()
299- if err != nil {
300- return err
301- }
302- defer c.apiClient.Close()
303+ // If the apClient is not already opened and it is opened
304+ // by ensureAPIClient, then close it when we're done.
305+ defer func() {
306+ if c.apiClient != nil {
307+ c.apiClient.Close()
308+ c.apiClient = nil
309+ }
310+ }()
311 }
312 host, err := c.hostFromTarget(c.Target)
313 if err != nil {
314 return err
315 }
316 var options ssh.Options
317- options.EnablePTY()
318+ if c.pty {
319+ options.EnablePTY()
320+ }
321+ if proxy, err := c.proxySSH(); err != nil {
322+ return err
323+ } else if proxy {
324+ if err := c.setProxyCommand(&options); err != nil {
325+ return err
326+ }
327+ }
328 cmd := ssh.Command("ubuntu@"+host, c.Args, &options)
329 cmd.Stdin = ctx.Stdin
330 cmd.Stdout = ctx.Stdout
331@@ -104,12 +146,45 @@
332 return cmd.Run()
333 }
334
335+// proxySSH returns true iff both c.proxy and
336+// the proxy-ssh environment configuration
337+// are true.
338+func (c *SSHCommon) proxySSH() (bool, error) {
339+ if !c.proxy {
340+ return false, nil
341+ }
342+ if _, err := c.ensureAPIClient(); err != nil {
343+ return false, err
344+ }
345+ var cfg *config.Config
346+ attrs, err := c.apiClient.EnvironmentGet()
347+ if err == nil {
348+ cfg, err = config.New(config.NoDefaults, attrs)
349+ }
350+ if err != nil {
351+ return false, err
352+ }
353+ logger.Debugf("proxy-ssh is %v", cfg.ProxySSH())
354+ return cfg.ProxySSH(), nil
355+}
356+
357+func (c *SSHCommon) ensureAPIClient() (*api.Client, error) {
358+ if c.apiClient != nil {
359+ return c.apiClient, nil
360+ }
361+ return c.initAPIClient()
362+}
363+
364 // initAPIClient initialises the API connection.
365 // It is the caller's responsibility to close the connection.
366 func (c *SSHCommon) initAPIClient() (*api.Client, error) {
367- var err error
368- c.apiClient, err = juju.NewAPIClientFromName(c.EnvName)
369- return c.apiClient, err
370+ st, err := juju.NewAPIFromName(c.EnvName)
371+ if err != nil {
372+ return nil, err
373+ }
374+ c.apiClient = st.Client()
375+ c.apiAddr = st.Addr()
376+ return c.apiClient, nil
377 }
378
379 // attemptStarter is an interface corresponding to utils.AttemptStrategy
380@@ -132,86 +207,31 @@
381 Delay: 500 * time.Millisecond,
382 }
383
384-// ensureRawConn ensures that c.rawConn is valid (or returns an error)
385-// This is only for compatibility with a 1.16 API server (that doesn't have
386-// some of the API added more recently.) It can be removed once we no longer
387-// need compatibility with direct access to the state database
388-func (c *SSHCommon) ensureRawConn() error {
389- if c.rawConn != nil {
390- return nil
391- }
392- var err error
393- c.rawConn, err = juju.NewConnFromName(c.EnvName)
394- return err
395-}
396-
397-func (c *SSHCommon) hostFromTarget1dot16(target string) (string, error) {
398- err := c.ensureRawConn()
399- if err != nil {
400- return "", err
401- }
402- // is the target the id of a machine ?
403- if names.IsMachine(target) {
404- logger.Infof("looking up address for machine %s...", target)
405- // This is not the exact code from the 1.16 client
406- // (machinePublicAddress), however it is the code used in the
407- // apiserver behind the PublicAddress call. (1.16 didn't know
408- // about SelectPublicAddress)
409- // The old code watched for changes on the Machine until it had
410- // an InstanceId and then would return the instance.WaitDNS()
411- machine, err := c.rawConn.State.Machine(target)
412- if err != nil {
413- return "", err
414- }
415- addr := instance.SelectPublicAddress(machine.Addresses())
416- if addr == "" {
417- return "", fmt.Errorf("machine %q has no public address", machine)
418- }
419- return addr, nil
420- }
421- // maybe the target is a unit ?
422- if names.IsUnit(target) {
423- logger.Infof("looking up address for unit %q...", c.Target)
424- unit, err := c.rawConn.State.Unit(target)
425- if err != nil {
426- return "", err
427- }
428- addr, ok := unit.PublicAddress()
429- if !ok {
430- return "", fmt.Errorf("unit %q has no public address", unit)
431- }
432- return addr, nil
433- }
434- return "", fmt.Errorf("unknown unit or machine %q", target)
435-}
436-
437 func (c *SSHCommon) hostFromTarget(target string) (string, error) {
438- var addr string
439- var err error
440- var useStateConn bool
441+ // If the target is neither a machine nor a unit,
442+ // assume it's a hostname and try it directly.
443+ if !names.IsMachine(target) && !names.IsUnit(target) {
444+ return target, nil
445+ }
446 // A target may not initially have an address (e.g. the
447 // address updater hasn't yet run), so we must do this in
448 // a loop.
449+ if _, err := c.ensureAPIClient(); err != nil {
450+ return "", err
451+ }
452+ var err error
453 for a := sshHostFromTargetAttemptStrategy.Start(); a.Next(); {
454- if !useStateConn {
455+ var addr string
456+ if c.proxy {
457+ addr, err = c.apiClient.PrivateAddress(target)
458+ } else {
459 addr, err = c.apiClient.PublicAddress(target)
460- if params.IsCodeNotImplemented(err) {
461- logger.Infof("API server does not support Client.PublicAddress falling back to 1.16 compatibility mode (direct DB access)")
462- useStateConn = true
463- }
464- }
465- if useStateConn {
466- addr, err = c.hostFromTarget1dot16(target)
467 }
468 if err == nil {
469- break
470+ return addr, nil
471 }
472 }
473- if err != nil {
474- return "", err
475- }
476- logger.Infof("Resolved public address of %q: %q", target, addr)
477- return addr, nil
478+ return "", err
479 }
480
481 // AllowInterspersedFlags for ssh/scp is set to false so that
482
483=== modified file 'cmd/juju/ssh_test.go'
484--- cmd/juju/ssh_test.go 2014-04-01 03:37:13 +0000
485+++ cmd/juju/ssh_test.go 2014-04-02 06:17:00 +0000
486@@ -39,6 +39,7 @@
487
488 func (s *SSHCommonSuite) SetUpTest(c *gc.C) {
489 s.JujuConnSuite.SetUpTest(c)
490+ s.PatchValue(&getJujuExecutable, func() (string, error) { return "juju", nil })
491
492 s.bin = c.MkDir()
493 s.PatchEnvPathPrepend(s.bin)
494@@ -53,8 +54,10 @@
495 }
496
497 const (
498- commonArgs = `-o StrictHostKeyChecking no -o PasswordAuthentication no `
499- sshArgs = commonArgs + `-t -t `
500+ commonArgsNoProxy = `-o StrictHostKeyChecking no -o PasswordAuthentication no `
501+ commonArgs = `-o StrictHostKeyChecking no -o ProxyCommand juju ssh --proxy=false --pty=false 127.0.0.1 nc -q0 %h %p -o PasswordAuthentication no `
502+ sshArgs = commonArgs + `-t -t `
503+ sshArgsNoProxy = commonArgsNoProxy + `-t -t `
504 )
505
506 var sshTests = []struct {
507@@ -82,6 +85,11 @@
508 []string{"ssh", "mongodb/1", "ls", "/"},
509 sshArgs + "ubuntu@dummyenv-2.dns ls /\n",
510 },
511+ {
512+ "connect to unit mysql/0 without proxy",
513+ []string{"ssh", "--proxy=false", "mysql/0"},
514+ sshArgsNoProxy + "ubuntu@dummyenv-0.dns\n",
515+ },
516 }
517
518 func (s *SSHSuite) TestSSHCommand(c *gc.C) {
519@@ -114,6 +122,20 @@
520 }
521 }
522
523+func (s *SSHSuite) TestSSHCommandEnvironProxySSH(c *gc.C) {
524+ s.makeMachines(1, c, true)
525+ // Setting proxy-ssh=false in the environment overrides --proxy.
526+ err := s.State.UpdateEnvironConfig(map[string]interface{}{"proxy-ssh": false}, nil, nil)
527+ c.Assert(err, gc.IsNil)
528+ ctx := coretesting.Context(c)
529+ jujucmd := cmd.NewSuperCommand(cmd.SuperCommandParams{})
530+ jujucmd.Register(&SSHCommand{})
531+ code := cmd.Main(jujucmd, ctx, []string{"ssh", "0"})
532+ c.Check(code, gc.Equals, 0)
533+ c.Check(ctx.Stderr.(*bytes.Buffer).String(), gc.Equals, "")
534+ c.Check(ctx.Stdout.(*bytes.Buffer).String(), gc.Equals, sshArgsNoProxy+"ubuntu@dummyenv-0.dns\n")
535+}
536+
537 type callbackAttemptStarter struct {
538 next func() bool
539 }
540@@ -131,10 +153,16 @@
541 }
542
543 func (s *SSHSuite) TestSSHCommandHostAddressRetry(c *gc.C) {
544+ s.testSSHCommandHostAddressRetry(c, false)
545+}
546+
547+func (s *SSHSuite) TestSSHCommandHostAddressRetryProxy(c *gc.C) {
548+ s.testSSHCommandHostAddressRetry(c, true)
549+}
550+
551+func (s *SSHSuite) testSSHCommandHostAddressRetry(c *gc.C, proxy bool) {
552 m := s.makeMachines(1, c, false)
553 ctx := coretesting.Context(c)
554- jujucmd := cmd.NewSuperCommand(cmd.SuperCommandParams{})
555- jujucmd.Register(&SSHCommand{})
556
557 var called int
558 next := func() bool {
559@@ -146,18 +174,21 @@
560
561 // Ensure that the ssh command waits for a public address, or the attempt
562 // strategy's Done method returns false.
563- code := cmd.Main(jujucmd, ctx, []string{"ssh", "0"})
564+ args := []string{"--proxy=" + fmt.Sprint(proxy), "0"}
565+ code := cmd.Main(&SSHCommand{}, ctx, args)
566 c.Check(code, gc.Equals, 1)
567 c.Assert(called, gc.Equals, 2)
568 called = 0
569 attemptStarter.next = func() bool {
570 called++
571- s.setAddress(m[0], c)
572- return false
573+ if called > 1 {
574+ s.setAddress(m[0], c)
575+ }
576+ return true
577 }
578- code = cmd.Main(jujucmd, ctx, []string{"ssh", "0"})
579+ code = cmd.Main(&SSHCommand{}, ctx, args)
580 c.Check(code, gc.Equals, 0)
581- c.Assert(called, gc.Equals, 1)
582+ c.Assert(called, gc.Equals, 2)
583 }
584
585 func (s *SSHCommonSuite) setAddress(m *state.Machine, c *gc.C) {
586
587=== modified file 'environs/config/config.go'
588--- environs/config/config.go 2014-03-20 22:35:39 +0000
589+++ environs/config/config.go 2014-04-02 06:17:00 +0000
590@@ -442,6 +442,13 @@
591 return c.mustString("authorized-keys")
592 }
593
594+// ProxySSH returns a flag indicating whether SSH commands
595+// should be proxied through the API server.
596+func (c *Config) ProxySSH() bool {
597+ value, _ := c.defined["proxy-ssh"].(bool)
598+ return value
599+}
600+
601 // ProxySettings returns all four proxy settings; http, https, ftp, and no
602 // proxy.
603 func (c *Config) ProxySettings() osenv.ProxySettings {
604@@ -716,6 +723,7 @@
605 "bootstrap-retry-delay": schema.ForceInt(),
606 "bootstrap-addresses-delay": schema.ForceInt(),
607 "test-mode": schema.Bool(),
608+ "proxy-ssh": schema.Bool(),
609
610 // Deprecated fields, retain for backwards compatibility.
611 "tools-url": schema.String(),
612@@ -773,6 +781,7 @@
613 // Previously image-stream could be set to an empty value
614 "image-stream": "",
615 "test-mode": false,
616+ "proxy-ssh": false,
617 }
618
619 func allowEmpty(attr string) bool {
620@@ -796,6 +805,7 @@
621 "bootstrap-timeout": DefaultBootstrapSSHTimeout,
622 "bootstrap-retry-delay": DefaultBootstrapSSHRetryDelay,
623 "bootstrap-addresses-delay": DefaultBootstrapSSHAddressesDelay,
624+ "proxy-ssh": true,
625 }
626 for attr, val := range alwaysOptional {
627 if _, ok := d[attr]; !ok {
628
629=== modified file 'environs/config/config_test.go'
630--- environs/config/config_test.go 2014-03-14 02:10:35 +0000
631+++ environs/config/config_test.go 2014-04-02 06:17:00 +0000
632@@ -1026,6 +1026,7 @@
633 attrs["tools-metadata-url"] = ""
634 attrs["tools-url"] = ""
635 attrs["image-stream"] = ""
636+ attrs["proxy-ssh"] = false
637
638 // Default firewall mode is instance
639 attrs["firewall-mode"] = string(config.FwInstance)
640
641=== modified file 'provider/local/environprovider.go'
642--- provider/local/environprovider.go 2014-03-31 03:36:17 +0000
643+++ provider/local/environprovider.go 2014-04-02 06:17:00 +0000
644@@ -103,7 +103,13 @@
645 }
646 // If the user has specified no values for any of the three normal
647 // proxies, then look in the environment and set them.
648- attrs := make(map[string]interface{})
649+ attrs := map[string]interface{}{
650+ // We must not proxy SSH through the API server in a
651+ // local provider environment. Besides not being useful,
652+ // it may not work; there is no requirement for sshd to
653+ // be available on machine-0.
654+ "proxy-ssh": false,
655+ }
656 setIfNotBlank := func(key, value string) {
657 if value != "" {
658 attrs[key] = value
659
660=== modified file 'provider/local/environprovider_test.go'
661--- provider/local/environprovider_test.go 2014-03-19 04:06:58 +0000
662+++ provider/local/environprovider_test.go 2014-04-02 06:17:00 +0000
663@@ -380,3 +380,19 @@
664 c.Assert(value, gc.Equals, test.expectAUFS)
665 }
666 }
667+
668+func (s *prepareSuite) TestPrepareProxySSH(c *gc.C) {
669+ s.PatchValue(local.DetectAptProxies, func() (osenv.ProxySettings, error) {
670+ return osenv.ProxySettings{}, nil
671+ })
672+ basecfg, err := config.New(config.UseDefaults, map[string]interface{}{
673+ "type": "local",
674+ "name": "test",
675+ })
676+ provider, err := environs.Provider("local")
677+ c.Assert(err, gc.IsNil)
678+ env, err := provider.Prepare(coretesting.Context(c), basecfg)
679+ c.Assert(err, gc.IsNil)
680+ // local provider sets proxy-ssh to false
681+ c.Assert(env.Config().ProxySSH(), gc.Equals, false)
682+}
683
684=== modified file 'utils/ssh/ssh_gocrypto.go'
685--- utils/ssh/ssh_gocrypto.go 2014-02-24 16:21:43 +0000
686+++ utils/ssh/ssh_gocrypto.go 2014-04-02 06:17:00 +0000
687@@ -7,6 +7,9 @@
688 "fmt"
689 "io"
690 "io/ioutil"
691+ "net"
692+ "os"
693+ "os/exec"
694 "os/user"
695 "strings"
696
697@@ -45,17 +48,20 @@
698 }
699 user, host := splitUserHost(host)
700 port := sshDefaultPort
701+ var proxyCommand []string
702 if options != nil {
703 if options.port != 0 {
704 port = options.port
705 }
706+ proxyCommand = options.proxyCommand
707 }
708 logger.Debugf(`running (equivalent of): ssh "%s@%s" -p %d '%s'`, user, host, port, shellCommand)
709 return &Cmd{impl: &goCryptoCommand{
710- signers: signers,
711- user: user,
712- addr: fmt.Sprintf("%s:%d", host, port),
713- command: shellCommand,
714+ signers: signers,
715+ user: user,
716+ addr: fmt.Sprintf("%s:%d", host, port),
717+ command: shellCommand,
718+ proxyCommand: proxyCommand,
719 }}
720 }
721
722@@ -67,19 +73,50 @@
723 }
724
725 type goCryptoCommand struct {
726- signers []ssh.Signer
727- user string
728- addr string
729- command string
730- stdin io.Reader
731- stdout io.Writer
732- stderr io.Writer
733- conn *ssh.ClientConn
734- sess *ssh.Session
735+ signers []ssh.Signer
736+ user string
737+ addr string
738+ command string
739+ proxyCommand []string
740+ stdin io.Reader
741+ stdout io.Writer
742+ stderr io.Writer
743+ conn *ssh.ClientConn
744+ sess *ssh.Session
745 }
746
747 var sshDial = ssh.Dial
748
749+var sshDialWithProxy = func(addr string, proxyCommand []string, config *ssh.ClientConfig) (*ssh.ClientConn, error) {
750+ if len(proxyCommand) == 0 {
751+ return sshDial("tcp", addr, config)
752+ }
753+ // User has specified a proxy. Create a pipe and
754+ // redirect the proxy command's stdin/stdout to it.
755+ host, port, err := net.SplitHostPort(addr)
756+ if err != nil {
757+ host = addr
758+ }
759+ for i, arg := range proxyCommand {
760+ arg = strings.Replace(arg, "%h", host, -1)
761+ if port != "" {
762+ arg = strings.Replace(arg, "%p", port, -1)
763+ }
764+ arg = strings.Replace(arg, "%r", config.User, -1)
765+ proxyCommand[i] = arg
766+ }
767+ client, server := net.Pipe()
768+ logger.Debugf(`executing proxy command %q`, proxyCommand)
769+ cmd := exec.Command(proxyCommand[0], proxyCommand[1:]...)
770+ cmd.Stdin = server
771+ cmd.Stdout = server
772+ cmd.Stderr = os.Stderr
773+ if err := cmd.Start(); err != nil {
774+ return nil, err
775+ }
776+ return ssh.Client(client, config)
777+}
778+
779 func (c *goCryptoCommand) ensureSession() (*ssh.Session, error) {
780 if c.sess != nil {
781 return c.sess, nil
782@@ -100,7 +137,7 @@
783 ssh.ClientAuthKeyring(keyring{c.signers}),
784 },
785 }
786- conn, err := sshDial("tcp", c.addr, config)
787+ conn, err := sshDialWithProxy(c.addr, c.proxyCommand, config)
788 if err != nil {
789 return nil, err
790 }
791
792=== modified file 'utils/ssh/ssh_gocrypto_test.go'
793--- utils/ssh/ssh_gocrypto_test.go 2014-03-13 07:54:56 +0000
794+++ utils/ssh/ssh_gocrypto_test.go 2014-04-02 06:17:00 +0000
795@@ -6,7 +6,11 @@
796 import (
797 "encoding/binary"
798 "errors"
799+ "fmt"
800+ "io/ioutil"
801 "net"
802+ "os/exec"
803+ "path/filepath"
804 "sync"
805
806 cryptossh "code.google.com/p/go.crypto/ssh"
807@@ -148,3 +152,38 @@
808 err = client.Copy([]string{"0.1.2.3:b", c.MkDir()}, nil, nil)
809 c.Assert(err, gc.ErrorMatches, `scp command is not implemented \(OpenSSH scp not available in PATH\)`)
810 }
811+
812+func (s *SSHGoCryptoCommandSuite) TestProxyCommand(c *gc.C) {
813+ realNetcat, err := exec.LookPath("nc")
814+ if err != nil {
815+ c.Skip("skipping test, couldn't find netcat: %v")
816+ return
817+ }
818+ netcat := filepath.Join(c.MkDir(), "nc")
819+ err = ioutil.WriteFile(netcat, []byte("#!/bin/sh\necho $0 \"$@\" > $0.args && exec "+realNetcat+" \"$@\""), 0755)
820+ c.Assert(err, gc.IsNil)
821+
822+ private, _, err := ssh.GenerateKey("test-server")
823+ c.Assert(err, gc.IsNil)
824+ key, err := cryptossh.ParsePrivateKey([]byte(private))
825+ client, err := ssh.NewGoCryptoClient(key)
826+ c.Assert(err, gc.IsNil)
827+ server := newServer(c)
828+ var opts ssh.Options
829+ port := server.Addr().(*net.TCPAddr).Port
830+ opts.SetProxyCommand(netcat, "-q0", "%h", "%p")
831+ opts.SetPort(port)
832+ cmd := client.Command("127.0.0.1", testCommand, &opts)
833+ server.cfg.PublicKeyCallback = func(conn *cryptossh.ServerConn, user, algo string, pubkey []byte) bool {
834+ return true
835+ }
836+ go server.run(c)
837+ out, err := cmd.Output()
838+ c.Assert(err, gc.ErrorMatches, "ssh: could not execute command.*")
839+ // TODO(axw) when gosshnew is ready, expect reply from server.
840+ c.Assert(out, gc.IsNil)
841+ // Ensure the proxy command was executed with the appropriate arguments.
842+ data, err := ioutil.ReadFile(netcat + ".args")
843+ c.Assert(err, gc.IsNil)
844+ c.Assert(string(data), gc.Equals, fmt.Sprintf("%s -q0 127.0.0.1 %v\n", netcat, port))
845+}

Subscribers

People subscribed via source and target branches

to status/vote changes: