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: 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.
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.
Andrew Wilkins (axwalk) wrote : | # |
William Reade (fwereade) wrote : | # |
LGTM so long as it's been used live.
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.
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?
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.
Andrew Wilkins (axwalk) wrote : | # |
Please take a look.
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.
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?
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.
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?
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.
Andrew Wilkins (axwalk) wrote : | # |
Please take a look.
Andrew Wilkins (axwalk) wrote : | # |
Please take a look.
John A Meinel (jameinel) wrote : | # |
I'm testing the bot, will Approve this in a moment.
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.
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 | "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 | +} |
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/