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