Merge lp:~axwalk/juju-core/ssh-proxycommand into lp:~go-bot/juju-core/trunk
- ssh-proxycommand
- Merge into trunk
Proposed by
Andrew Wilkins
Status: | Merged |
---|---|
Approved by: | Andrew Wilkins |
Approved revision: | no longer in the source branch. |
Merged at revision: | 2442 |
Proposed branch: | lp:~axwalk/juju-core/ssh-proxycommand |
Merge into: | lp:~go-bot/juju-core/trunk |
Diff against target: |
732 lines (+298/-89) 14 files modified
cmd/juju/scp.go (+9/-1) cmd/juju/scp_test.go (+56/-58) cmd/juju/ssh.go (+74/-11) cmd/juju/ssh_test.go (+10/-2) juju/api.go (+10/-3) juju/apiconn_test.go (+13/-13) juju/export_test.go (+1/-1) state/api/apiclient.go (+7/-0) state/api/client.go (+9/-0) state/api/params/params.go (+10/-0) state/apiserver/client/client.go (+28/-0) state/apiserver/client/client_test.go (+60/-0) utils/ssh/ssh.go (+8/-0) utils/ssh/ssh_openssh.go (+3/-0) |
To merge this branch: | bzr merge lp:~axwalk/juju-core/ssh-proxycommand |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Juju Engineering | Pending | ||
Review via email: mp+211662@code.launchpad.net |
Commit message
Add --proxy option to "juju ssh"
juju ssh will now, by default, proxy
connections through the API server host.
I have not yet implemented support for the
ProxyCommand option for the go.crypto/ssh
client; this will be done as a follow up.
Description of the change
Add --proxy option to "juju ssh"
juju ssh will now, by default, proxy
connections through the API server host.
I have not yet implemented support for the
ProxyCommand option for the go.crypto/ssh
client; this will be done as a follow up.
To post a comment you must log in.
Revision history for this message
Andrew Wilkins (axwalk) wrote : | # |
Revision history for this message
Tim Penhey (thumper) wrote : | # |
LGTM - the code looks fine, and I'm assuming you have tested it.
Revision history for this message
Andrew Wilkins (axwalk) wrote : | # |
On 2014/03/19 03:41:35, thumper wrote:
> LGTM - the code looks fine, and I'm assuming you have tested it.
Tested with local, canonistack and azure. Verified that ssh, scp,
debug-log, and debug-hooks.
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | === modified file 'cmd/juju/scp.go' | |||
2 | --- cmd/juju/scp.go 2014-02-24 16:21:43 +0000 | |||
3 | +++ cmd/juju/scp.go 2014-03-19 03:21:48 +0000 | |||
4 | @@ -106,5 +106,13 @@ | |||
5 | 106 | targets = append(targets, arg) | 106 | targets = append(targets, arg) |
6 | 107 | } | 107 | } |
7 | 108 | } | 108 | } |
9 | 109 | return ssh.Copy(targets, extraArgs, nil) | 109 | |
10 | 110 | var options *ssh.Options | ||
11 | 111 | if c.proxy { | ||
12 | 112 | options = new(ssh.Options) | ||
13 | 113 | if err := c.setProxyCommand(options); err != nil { | ||
14 | 114 | return err | ||
15 | 115 | } | ||
16 | 116 | } | ||
17 | 117 | return ssh.Copy(targets, extraArgs, options) | ||
18 | 110 | } | 118 | } |
19 | 111 | 119 | ||
20 | === modified file 'cmd/juju/scp_test.go' | |||
21 | --- cmd/juju/scp_test.go 2014-02-24 16:21:43 +0000 | |||
22 | +++ cmd/juju/scp_test.go 2014-03-19 03:21:48 +0000 | |||
23 | @@ -27,67 +27,64 @@ | |||
24 | 27 | about string | 27 | about string |
25 | 28 | args []string | 28 | args []string |
26 | 29 | result string | 29 | result string |
27 | 30 | proxy bool | ||
28 | 30 | error string | 31 | error string |
29 | 31 | }{ | 32 | }{ |
30 | 32 | { | 33 | { |
89 | 33 | "scp from machine 0 to current dir", | 34 | about: "scp from machine 0 to current dir", |
90 | 34 | []string{"0:foo", "."}, | 35 | args: []string{"0:foo", "."}, |
91 | 35 | commonArgs + "ubuntu@dummyenv-0.dns:foo .\n", | 36 | result: commonArgsNoProxy + "ubuntu@dummyenv-0.dns:foo .\n", |
92 | 36 | "", | 37 | }, |
93 | 37 | }, | 38 | { |
94 | 38 | { | 39 | about: "scp from machine 0 to current dir with extra args", |
95 | 39 | "scp from machine 0 to current dir with extra args", | 40 | args: []string{"0:foo", ".", "-rv -o SomeOption"}, |
96 | 40 | []string{"0:foo", ".", "-rv -o SomeOption"}, | 41 | result: commonArgsNoProxy + "-rv -o SomeOption ubuntu@dummyenv-0.dns:foo .\n", |
97 | 41 | commonArgs + "-rv -o SomeOption ubuntu@dummyenv-0.dns:foo .\n", | 42 | }, |
98 | 42 | "", | 43 | { |
99 | 43 | }, | 44 | about: "scp from current dir to machine 0", |
100 | 44 | { | 45 | args: []string{"foo", "0:"}, |
101 | 45 | "scp from current dir to machine 0", | 46 | result: commonArgsNoProxy + "foo ubuntu@dummyenv-0.dns:\n", |
102 | 46 | []string{"foo", "0:"}, | 47 | }, |
103 | 47 | commonArgs + "foo ubuntu@dummyenv-0.dns:\n", | 48 | { |
104 | 48 | "", | 49 | about: "scp from current dir to machine 0 with extra args", |
105 | 49 | }, | 50 | args: []string{"foo", "0:", "-r -v"}, |
106 | 50 | { | 51 | result: commonArgsNoProxy + "-r -v foo ubuntu@dummyenv-0.dns:\n", |
107 | 51 | "scp from current dir to machine 0 with extra args", | 52 | }, |
108 | 52 | []string{"foo", "0:", "-r -v"}, | 53 | { |
109 | 53 | commonArgs + "-r -v foo ubuntu@dummyenv-0.dns:\n", | 54 | about: "scp from machine 0 to unit mysql/0", |
110 | 54 | "", | 55 | args: []string{"0:foo", "mysql/0:/foo"}, |
111 | 55 | }, | 56 | result: commonArgsNoProxy + "ubuntu@dummyenv-0.dns:foo ubuntu@dummyenv-0.dns:/foo\n", |
112 | 56 | { | 57 | }, |
113 | 57 | "scp from machine 0 to unit mysql/0", | 58 | { |
114 | 58 | []string{"0:foo", "mysql/0:/foo"}, | 59 | about: "scp from machine 0 to unit mysql/0 and extra args", |
115 | 59 | commonArgs + "ubuntu@dummyenv-0.dns:foo ubuntu@dummyenv-0.dns:/foo\n", | 60 | args: []string{"0:foo", "mysql/0:/foo", "-q"}, |
116 | 60 | "", | 61 | result: commonArgsNoProxy + "-q ubuntu@dummyenv-0.dns:foo ubuntu@dummyenv-0.dns:/foo\n", |
117 | 61 | }, | 62 | }, |
118 | 62 | { | 63 | { |
119 | 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", |
120 | 64 | []string{"0:foo", "mysql/0:/foo", "-q"}, | 65 | args: []string{"-q", "-r", "0:foo", "mysql/0:/foo"}, |
121 | 65 | commonArgs + "-q ubuntu@dummyenv-0.dns:foo ubuntu@dummyenv-0.dns:/foo\n", | 66 | error: `unexpected argument "-q"; extra arguments must be last`, |
122 | 66 | "", | 67 | }, |
123 | 67 | }, | 68 | { |
124 | 68 | { | 69 | about: "scp two local files to unit mysql/0", |
125 | 69 | "scp from machine 0 to unit mysql/0 and extra args before", | 70 | args: []string{"file1", "file2", "mysql/0:/foo/"}, |
126 | 70 | []string{"-q", "-r", "0:foo", "mysql/0:/foo"}, | 71 | result: commonArgsNoProxy + "file1 file2 ubuntu@dummyenv-0.dns:/foo/\n", |
127 | 71 | "", | 72 | }, |
128 | 72 | `unexpected argument "-q"; extra arguments must be last`, | 73 | { |
129 | 73 | }, | 74 | about: "scp from unit mongodb/1 to unit mongodb/0 and multiple extra args", |
130 | 74 | { | 75 | args: []string{"mongodb/1:foo", "mongodb/0:", "-r -v -q -l5"}, |
131 | 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", |
132 | 76 | []string{"file1", "file2", "mysql/0:/foo/"}, | 77 | }, |
133 | 77 | commonArgs + "file1 file2 ubuntu@dummyenv-0.dns:/foo/\n", | 78 | { |
134 | 78 | "", | 79 | about: "scp works with IPv6 addresses", |
135 | 79 | }, | 80 | args: []string{"ipv6-svc/0:foo", "bar"}, |
136 | 80 | { | 81 | result: commonArgsNoProxy + `ubuntu@\[2001:db8::\]:foo bar` + "\n", |
137 | 81 | "scp from unit mongodb/1 to unit mongodb/0 and multiple extra args", | 82 | }, |
138 | 82 | []string{"mongodb/1:foo", "mongodb/0:", "-r -v -q -l5"}, | 83 | { |
139 | 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", |
140 | 84 | "", | 85 | args: []string{"0:foo", "mysql/0:/foo"}, |
141 | 85 | }, | 86 | result: commonArgs + "ubuntu@dummyenv-0.dns:foo ubuntu@dummyenv-0.dns:/foo\n", |
142 | 86 | { | 87 | proxy: true, |
85 | 87 | "scp works with IPv6 addresses", | ||
86 | 88 | []string{"ipv6-svc/0:foo", "bar"}, | ||
87 | 89 | commonArgs + `ubuntu@\[2001:db8::\]:foo bar` + "\n", | ||
88 | 90 | "", | ||
143 | 91 | }, | 88 | }, |
144 | 92 | } | 89 | } |
145 | 93 | 90 | ||
146 | @@ -122,6 +119,7 @@ | |||
147 | 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) |
148 | 123 | ctx := coretesting.Context(c) | 120 | ctx := coretesting.Context(c) |
149 | 124 | scpcmd := &SCPCommand{} | 121 | scpcmd := &SCPCommand{} |
150 | 122 | scpcmd.proxy = t.proxy | ||
151 | 125 | 123 | ||
152 | 126 | err := scpcmd.Init(t.args) | 124 | err := scpcmd.Init(t.args) |
153 | 127 | c.Check(err, gc.IsNil) | 125 | c.Check(err, gc.IsNil) |
154 | 128 | 126 | ||
155 | === modified file 'cmd/juju/ssh.go' | |||
156 | --- cmd/juju/ssh.go 2014-02-22 13:45:10 +0000 | |||
157 | +++ cmd/juju/ssh.go 2014-03-19 03:21:48 +0000 | |||
158 | @@ -6,8 +6,13 @@ | |||
159 | 6 | import ( | 6 | import ( |
160 | 7 | "errors" | 7 | "errors" |
161 | 8 | "fmt" | 8 | "fmt" |
162 | 9 | "net" | ||
163 | 10 | "os" | ||
164 | 11 | "os/exec" | ||
165 | 9 | "time" | 12 | "time" |
166 | 10 | 13 | ||
167 | 14 | "launchpad.net/gnuflag" | ||
168 | 15 | |||
169 | 11 | "launchpad.net/juju-core/cmd" | 16 | "launchpad.net/juju-core/cmd" |
170 | 12 | "launchpad.net/juju-core/instance" | 17 | "launchpad.net/juju-core/instance" |
171 | 13 | "launchpad.net/juju-core/juju" | 18 | "launchpad.net/juju-core/juju" |
172 | @@ -26,13 +31,34 @@ | |||
173 | 26 | // SSHCommon provides common methods for SSHCommand, SCPCommand and DebugHooksCommand. | 31 | // SSHCommon provides common methods for SSHCommand, SCPCommand and DebugHooksCommand. |
174 | 27 | type SSHCommon struct { | 32 | type SSHCommon struct { |
175 | 28 | cmd.EnvCommandBase | 33 | cmd.EnvCommandBase |
176 | 34 | proxy bool | ||
177 | 29 | Target string | 35 | Target string |
178 | 30 | Args []string | 36 | Args []string |
179 | 31 | apiClient *api.Client | 37 | apiClient *api.Client |
180 | 38 | apiAddr string | ||
181 | 32 | // Only used for compatibility with 1.16 | 39 | // Only used for compatibility with 1.16 |
182 | 33 | rawConn *juju.Conn | 40 | rawConn *juju.Conn |
183 | 34 | } | 41 | } |
184 | 35 | 42 | ||
185 | 43 | func (c *SSHCommon) SetFlags(f *gnuflag.FlagSet) { | ||
186 | 44 | c.EnvCommandBase.SetFlags(f) | ||
187 | 45 | f.BoolVar(&c.proxy, "proxy", true, "proxy through the API server") | ||
188 | 46 | } | ||
189 | 47 | |||
190 | 48 | // setProxyCommand sets the proxy command option. | ||
191 | 49 | func (c *SSHCommon) setProxyCommand(options *ssh.Options) error { | ||
192 | 50 | apiServerHost, _, err := net.SplitHostPort(c.apiAddr) | ||
193 | 51 | if err != nil { | ||
194 | 52 | return fmt.Errorf("failed to get proxy address: %v", err) | ||
195 | 53 | } | ||
196 | 54 | juju, err := getJujuExecutable() | ||
197 | 55 | if err != nil { | ||
198 | 56 | return fmt.Errorf("failed to get juju executable path: %v", err) | ||
199 | 57 | } | ||
200 | 58 | options.SetProxyCommand(juju, "ssh", "--proxy=false", apiServerHost, "-T", "nc -q0 %h %p") | ||
201 | 59 | return nil | ||
202 | 60 | } | ||
203 | 61 | |||
204 | 36 | const sshDoc = ` | 62 | const sshDoc = ` |
205 | 37 | Launch an ssh shell on the machine identified by the <target> parameter. | 63 | Launch an ssh shell on the machine identified by the <target> parameter. |
206 | 38 | <target> can be either a machine id as listed by "juju status" in the | 64 | <target> can be either a machine id as listed by "juju status" in the |
207 | @@ -75,6 +101,12 @@ | |||
208 | 75 | return nil | 101 | return nil |
209 | 76 | } | 102 | } |
210 | 77 | 103 | ||
211 | 104 | // getJujuExecutable returns the path to the juju | ||
212 | 105 | // executable, or an error if it could not be found. | ||
213 | 106 | var getJujuExecutable = func() (string, error) { | ||
214 | 107 | return exec.LookPath(os.Args[0]) | ||
215 | 108 | } | ||
216 | 109 | |||
217 | 78 | // Run resolves c.Target to a machine, to the address of a i | 110 | // Run resolves c.Target to a machine, to the address of a i |
218 | 79 | // machine or unit forks ssh passing any arguments provided. | 111 | // machine or unit forks ssh passing any arguments provided. |
219 | 80 | func (c *SSHCommand) Run(ctx *cmd.Context) error { | 112 | func (c *SSHCommand) Run(ctx *cmd.Context) error { |
220 | @@ -92,6 +124,11 @@ | |||
221 | 92 | } | 124 | } |
222 | 93 | var options ssh.Options | 125 | var options ssh.Options |
223 | 94 | options.EnablePTY() | 126 | options.EnablePTY() |
224 | 127 | if c.proxy { | ||
225 | 128 | if err := c.setProxyCommand(&options); err != nil { | ||
226 | 129 | return err | ||
227 | 130 | } | ||
228 | 131 | } | ||
229 | 95 | cmd := ssh.Command("ubuntu@"+host, c.Args, &options) | 132 | cmd := ssh.Command("ubuntu@"+host, c.Args, &options) |
230 | 96 | cmd.Stdin = ctx.Stdin | 133 | cmd.Stdin = ctx.Stdin |
231 | 97 | cmd.Stdout = ctx.Stdout | 134 | cmd.Stdout = ctx.Stdout |
232 | @@ -102,9 +139,13 @@ | |||
233 | 102 | // initAPIClient initialises the API connection. | 139 | // initAPIClient initialises the API connection. |
234 | 103 | // It is the caller's responsibility to close the connection. | 140 | // It is the caller's responsibility to close the connection. |
235 | 104 | func (c *SSHCommon) initAPIClient() (*api.Client, error) { | 141 | func (c *SSHCommon) initAPIClient() (*api.Client, error) { |
239 | 105 | var err error | 142 | st, err := juju.NewAPIFromName(c.EnvName) |
240 | 106 | c.apiClient, err = juju.NewAPIClientFromName(c.EnvName) | 143 | if err != nil { |
241 | 107 | return c.apiClient, err | 144 | return nil, err |
242 | 145 | } | ||
243 | 146 | c.apiClient = st.Client() | ||
244 | 147 | c.apiAddr = st.Addr() | ||
245 | 148 | return c.apiClient, nil | ||
246 | 108 | } | 149 | } |
247 | 109 | 150 | ||
248 | 110 | // attemptStarter is an interface corresponding to utils.AttemptStrategy | 151 | // attemptStarter is an interface corresponding to utils.AttemptStrategy |
249 | @@ -158,9 +199,15 @@ | |||
250 | 158 | if err != nil { | 199 | if err != nil { |
251 | 159 | return "", err | 200 | return "", err |
252 | 160 | } | 201 | } |
256 | 161 | addr := instance.SelectPublicAddress(machine.Addresses()) | 202 | var addr string |
257 | 162 | if addr == "" { | 203 | if c.proxy { |
258 | 163 | return "", fmt.Errorf("machine %q has no public address", machine) | 204 | if addr = instance.SelectInternalAddress(machine.Addresses(), false); addr == "" { |
259 | 205 | return "", fmt.Errorf("machine %q has no internal address", machine) | ||
260 | 206 | } | ||
261 | 207 | } else { | ||
262 | 208 | if addr = instance.SelectPublicAddress(machine.Addresses()); addr == "" { | ||
263 | 209 | return "", fmt.Errorf("machine %q has no public address", machine) | ||
264 | 210 | } | ||
265 | 164 | } | 211 | } |
266 | 165 | return addr, nil | 212 | return addr, nil |
267 | 166 | } | 213 | } |
268 | @@ -171,9 +218,16 @@ | |||
269 | 171 | if err != nil { | 218 | if err != nil { |
270 | 172 | return "", err | 219 | return "", err |
271 | 173 | } | 220 | } |
275 | 174 | addr, ok := unit.PublicAddress() | 221 | var addr string |
276 | 175 | if !ok { | 222 | var ok bool |
277 | 176 | return "", fmt.Errorf("unit %q has no public address", unit) | 223 | if c.proxy { |
278 | 224 | if addr, ok = unit.PrivateAddress(); !ok { | ||
279 | 225 | return "", fmt.Errorf("unit %q has no internal address", unit) | ||
280 | 226 | } | ||
281 | 227 | } else { | ||
282 | 228 | if addr, ok = unit.PublicAddress(); !ok { | ||
283 | 229 | return "", fmt.Errorf("unit %q has no public address", unit) | ||
284 | 230 | } | ||
285 | 177 | } | 231 | } |
286 | 178 | return addr, nil | 232 | return addr, nil |
287 | 179 | } | 233 | } |
288 | @@ -181,6 +235,11 @@ | |||
289 | 181 | } | 235 | } |
290 | 182 | 236 | ||
291 | 183 | func (c *SSHCommon) hostFromTarget(target string) (string, error) { | 237 | func (c *SSHCommon) hostFromTarget(target string) (string, error) { |
292 | 238 | // If the target is neither a machine nor a unit, | ||
293 | 239 | // assume it's a hostname and try it directly. | ||
294 | 240 | if !names.IsMachine(target) && !names.IsUnit(target) { | ||
295 | 241 | return target, nil | ||
296 | 242 | } | ||
297 | 184 | var addr string | 243 | var addr string |
298 | 185 | var err error | 244 | var err error |
299 | 186 | var useStateConn bool | 245 | var useStateConn bool |
300 | @@ -189,9 +248,13 @@ | |||
301 | 189 | // a loop. | 248 | // a loop. |
302 | 190 | for a := sshHostFromTargetAttemptStrategy.Start(); a.Next(); { | 249 | for a := sshHostFromTargetAttemptStrategy.Start(); a.Next(); { |
303 | 191 | if !useStateConn { | 250 | if !useStateConn { |
305 | 192 | addr, err = c.apiClient.PublicAddress(target) | 251 | if c.proxy { |
306 | 252 | addr, err = c.apiClient.PrivateAddress(target) | ||
307 | 253 | } else { | ||
308 | 254 | addr, err = c.apiClient.PublicAddress(target) | ||
309 | 255 | } | ||
310 | 193 | if params.IsCodeNotImplemented(err) { | 256 | if params.IsCodeNotImplemented(err) { |
312 | 194 | logger.Infof("API server does not support Client.PublicAddress falling back to 1.16 compatibility mode (direct DB access)") | 257 | logger.Infof("API server does not support Client.PrivateAddress falling back to 1.16 compatibility mode (direct DB access)") |
313 | 195 | useStateConn = true | 258 | useStateConn = true |
314 | 196 | } | 259 | } |
315 | 197 | } | 260 | } |
316 | 198 | 261 | ||
317 | === modified file 'cmd/juju/ssh_test.go' | |||
318 | --- cmd/juju/ssh_test.go 2014-02-22 13:45:10 +0000 | |||
319 | +++ cmd/juju/ssh_test.go 2014-03-19 03:21:48 +0000 | |||
320 | @@ -39,6 +39,7 @@ | |||
321 | 39 | 39 | ||
322 | 40 | func (s *SSHCommonSuite) SetUpTest(c *gc.C) { | 40 | func (s *SSHCommonSuite) SetUpTest(c *gc.C) { |
323 | 41 | s.JujuConnSuite.SetUpTest(c) | 41 | s.JujuConnSuite.SetUpTest(c) |
324 | 42 | s.PatchValue(&getJujuExecutable, func() (string, error) { return "juju", nil }) | ||
325 | 42 | 43 | ||
326 | 43 | s.bin = c.MkDir() | 44 | s.bin = c.MkDir() |
327 | 44 | s.PatchEnvPathPrepend(s.bin) | 45 | s.PatchEnvPathPrepend(s.bin) |
328 | @@ -53,8 +54,10 @@ | |||
329 | 53 | } | 54 | } |
330 | 54 | 55 | ||
331 | 55 | const ( | 56 | const ( |
334 | 56 | commonArgs = `-o StrictHostKeyChecking no -o PasswordAuthentication no ` | 57 | commonArgsNoProxy = `-o StrictHostKeyChecking no -o PasswordAuthentication no ` |
335 | 57 | sshArgs = commonArgs + `-t -t ` | 58 | commonArgs = `-o StrictHostKeyChecking no -o ProxyCommand juju ssh --proxy=false 127.0.0.1 -T "nc -q0 %h %p" -o PasswordAuthentication no ` |
336 | 59 | sshArgs = commonArgs + `-t -t ` | ||
337 | 60 | sshArgsNoProxy = commonArgsNoProxy + `-t -t ` | ||
338 | 58 | ) | 61 | ) |
339 | 59 | 62 | ||
340 | 60 | var sshTests = []struct { | 63 | var sshTests = []struct { |
341 | @@ -82,6 +85,11 @@ | |||
342 | 82 | []string{"ssh", "mongodb/1", "ls", "/"}, | 85 | []string{"ssh", "mongodb/1", "ls", "/"}, |
343 | 83 | sshArgs + "ubuntu@dummyenv-2.dns ls /\n", | 86 | sshArgs + "ubuntu@dummyenv-2.dns ls /\n", |
344 | 84 | }, | 87 | }, |
345 | 88 | { | ||
346 | 89 | "connect to unit mysql/0 without proxy", | ||
347 | 90 | []string{"ssh", "--proxy=false", "mysql/0"}, | ||
348 | 91 | sshArgsNoProxy + "ubuntu@dummyenv-0.dns\n", | ||
349 | 92 | }, | ||
350 | 85 | } | 93 | } |
351 | 86 | 94 | ||
352 | 87 | func (s *SSHSuite) TestSSHCommand(c *gc.C) { | 95 | func (s *SSHSuite) TestSSHCommand(c *gc.C) { |
353 | 88 | 96 | ||
354 | === modified file 'juju/api.go' | |||
355 | --- juju/api.go 2014-03-12 10:59:17 +0000 | |||
356 | +++ juju/api.go 2014-03-19 03:21:48 +0000 | |||
357 | @@ -100,17 +100,24 @@ | |||
358 | 100 | return keymanager.NewClient(st), nil | 100 | return keymanager.NewClient(st), nil |
359 | 101 | } | 101 | } |
360 | 102 | 102 | ||
361 | 103 | // NewAPIFromName returns an api.State connected to the API Server for | ||
362 | 104 | // the named environment. If envName is "", the default environment will | ||
363 | 105 | // be used. | ||
364 | 106 | func NewAPIFromName(envName string) (*api.State, error) { | ||
365 | 107 | return newAPIClient(envName) | ||
366 | 108 | } | ||
367 | 109 | |||
368 | 103 | func newAPIClient(envName string) (*api.State, error) { | 110 | func newAPIClient(envName string) (*api.State, error) { |
369 | 104 | store, err := configstore.NewDisk(osenv.JujuHome()) | 111 | store, err := configstore.NewDisk(osenv.JujuHome()) |
370 | 105 | if err != nil { | 112 | if err != nil { |
371 | 106 | return nil, err | 113 | return nil, err |
372 | 107 | } | 114 | } |
374 | 108 | return newAPIFromName(envName, store) | 115 | return newAPIFromStore(envName, store) |
375 | 109 | } | 116 | } |
376 | 110 | 117 | ||
378 | 111 | // newAPIFromName implements the bulk of NewAPIClientFromName | 118 | // newAPIFromStore implements the bulk of NewAPIClientFromName |
379 | 112 | // but is separate for testing purposes. | 119 | // but is separate for testing purposes. |
381 | 113 | func newAPIFromName(envName string, store configstore.Storage) (*api.State, error) { | 120 | func newAPIFromStore(envName string, store configstore.Storage) (*api.State, error) { |
382 | 114 | // Try to read the default environment configuration file. | 121 | // Try to read the default environment configuration file. |
383 | 115 | // If it doesn't exist, we carry on in case | 122 | // If it doesn't exist, we carry on in case |
384 | 116 | // there's some environment info for that environment. | 123 | // there's some environment info for that environment. |
385 | 117 | 124 | ||
386 | === modified file 'juju/apiconn_test.go' | |||
387 | --- juju/apiconn_test.go 2014-03-13 07:54:56 +0000 | |||
388 | +++ juju/apiconn_test.go 2014-03-19 03:21:48 +0000 | |||
389 | @@ -139,11 +139,11 @@ | |||
390 | 139 | called++ | 139 | called++ |
391 | 140 | return expectState, nil | 140 | return expectState, nil |
392 | 141 | } | 141 | } |
394 | 142 | // Give NewAPIFromName a store interface that can report when the | 142 | // Give NewAPIFromStore a store interface that can report when the |
395 | 143 | // config was written to, to ensure the cache isn't updated. | 143 | // config was written to, to ensure the cache isn't updated. |
396 | 144 | defer testbase.PatchValue(juju.APIOpen, apiOpen).Restore() | 144 | defer testbase.PatchValue(juju.APIOpen, apiOpen).Restore() |
397 | 145 | mockStore := &storageWithWriteNotify{store: store} | 145 | mockStore := &storageWithWriteNotify{store: store} |
399 | 146 | st, err := juju.NewAPIFromName("noconfig", mockStore) | 146 | st, err := juju.NewAPIFromStore("noconfig", mockStore) |
400 | 147 | c.Assert(err, gc.IsNil) | 147 | c.Assert(err, gc.IsNil) |
401 | 148 | c.Assert(st, gc.Equals, expectState) | 148 | c.Assert(st, gc.Equals, expectState) |
402 | 149 | c.Assert(called, gc.Equals, 1) | 149 | c.Assert(called, gc.Equals, 1) |
403 | @@ -186,7 +186,7 @@ | |||
404 | 186 | return expectState, nil | 186 | return expectState, nil |
405 | 187 | } | 187 | } |
406 | 188 | defer testbase.PatchValue(juju.APIOpen, apiOpen).Restore() | 188 | defer testbase.PatchValue(juju.APIOpen, apiOpen).Restore() |
408 | 189 | st, err := juju.NewAPIFromName("myenv", store) | 189 | st, err := juju.NewAPIFromStore("myenv", store) |
409 | 190 | c.Assert(err, gc.IsNil) | 190 | c.Assert(err, gc.IsNil) |
410 | 191 | c.Assert(st, gc.Equals, expectState) | 191 | c.Assert(st, gc.Equals, expectState) |
411 | 192 | c.Assert(called, gc.Equals, 1) | 192 | c.Assert(called, gc.Equals, 1) |
412 | @@ -209,7 +209,7 @@ | |||
413 | 209 | expectErr := fmt.Errorf("an error") | 209 | expectErr := fmt.Errorf("an error") |
414 | 210 | store := newConfigStoreWithError(expectErr) | 210 | store := newConfigStoreWithError(expectErr) |
415 | 211 | defer testbase.PatchValue(juju.APIOpen, panicAPIOpen).Restore() | 211 | defer testbase.PatchValue(juju.APIOpen, panicAPIOpen).Restore() |
417 | 212 | client, err := juju.NewAPIFromName("noconfig", store) | 212 | client, err := juju.NewAPIFromStore("noconfig", store) |
418 | 213 | c.Assert(err, gc.Equals, expectErr) | 213 | c.Assert(err, gc.Equals, expectErr) |
419 | 214 | c.Assert(client, gc.IsNil) | 214 | c.Assert(client, gc.IsNil) |
420 | 215 | } | 215 | } |
421 | @@ -228,7 +228,7 @@ | |||
422 | 228 | }) | 228 | }) |
423 | 229 | defer testbase.PatchValue(juju.APIOpen, panicAPIOpen).Restore() | 229 | defer testbase.PatchValue(juju.APIOpen, panicAPIOpen).Restore() |
424 | 230 | 230 | ||
426 | 231 | st, err := juju.NewAPIFromName("noconfig", store) | 231 | st, err := juju.NewAPIFromStore("noconfig", store) |
427 | 232 | c.Assert(err, gc.ErrorMatches, `environment "noconfig" not found`) | 232 | c.Assert(err, gc.ErrorMatches, `environment "noconfig" not found`) |
428 | 233 | c.Assert(st, gc.IsNil) | 233 | c.Assert(st, gc.IsNil) |
429 | 234 | } | 234 | } |
430 | @@ -246,7 +246,7 @@ | |||
431 | 246 | return nil, expectErr | 246 | return nil, expectErr |
432 | 247 | } | 247 | } |
433 | 248 | defer testbase.PatchValue(juju.APIOpen, apiOpen).Restore() | 248 | defer testbase.PatchValue(juju.APIOpen, apiOpen).Restore() |
435 | 249 | st, err := juju.NewAPIFromName("noconfig", store) | 249 | st, err := juju.NewAPIFromStore("noconfig", store) |
436 | 250 | c.Assert(err, gc.Equals, expectErr) | 250 | c.Assert(err, gc.Equals, expectErr) |
437 | 251 | c.Assert(st, gc.IsNil) | 251 | c.Assert(st, gc.IsNil) |
438 | 252 | } | 252 | } |
439 | @@ -277,7 +277,7 @@ | |||
440 | 277 | defer restoreAPIClose.Restore() | 277 | defer restoreAPIClose.Restore() |
441 | 278 | 278 | ||
442 | 279 | startTime := time.Now() | 279 | startTime := time.Now() |
444 | 280 | st, err := juju.NewAPIFromName(coretesting.SampleEnvName, store) | 280 | st, err := juju.NewAPIFromStore(coretesting.SampleEnvName, store) |
445 | 281 | c.Assert(err, gc.IsNil) | 281 | c.Assert(err, gc.IsNil) |
446 | 282 | // The connection logic should wait for some time before opening | 282 | // The connection logic should wait for some time before opening |
447 | 283 | // the API from the configuration. | 283 | // the API from the configuration. |
448 | @@ -342,7 +342,7 @@ | |||
449 | 342 | 342 | ||
450 | 343 | done := make(chan struct{}) | 343 | done := make(chan struct{}) |
451 | 344 | go func() { | 344 | go func() { |
453 | 345 | st, err := juju.NewAPIFromName(coretesting.SampleEnvName, store) | 345 | st, err := juju.NewAPIFromStore(coretesting.SampleEnvName, store) |
454 | 346 | c.Check(err, gc.IsNil) | 346 | c.Check(err, gc.IsNil) |
455 | 347 | c.Check(st, gc.Equals, infoOpenedState) | 347 | c.Check(st, gc.Equals, infoOpenedState) |
456 | 348 | close(done) | 348 | close(done) |
457 | @@ -360,7 +360,7 @@ | |||
458 | 360 | c.Fatalf("api never opened via config") | 360 | c.Fatalf("api never opened via config") |
459 | 361 | } | 361 | } |
460 | 362 | // Let the info endpoint open go ahead and | 362 | // Let the info endpoint open go ahead and |
462 | 363 | // check that the NewAPIFromName call returns. | 363 | // check that the NewAPIFromStore call returns. |
463 | 364 | infoEndpointOpened <- struct{}{} | 364 | infoEndpointOpened <- struct{}{} |
464 | 365 | select { | 365 | select { |
465 | 366 | case <-done: | 366 | case <-done: |
466 | @@ -393,7 +393,7 @@ | |||
467 | 393 | return nil, fmt.Errorf("config connect failed") | 393 | return nil, fmt.Errorf("config connect failed") |
468 | 394 | } | 394 | } |
469 | 395 | defer testbase.PatchValue(juju.APIOpen, apiOpen).Restore() | 395 | defer testbase.PatchValue(juju.APIOpen, apiOpen).Restore() |
471 | 396 | st, err := juju.NewAPIFromName(coretesting.SampleEnvName, store) | 396 | st, err := juju.NewAPIFromStore(coretesting.SampleEnvName, store) |
472 | 397 | c.Check(err, gc.ErrorMatches, "config connect failed") | 397 | c.Check(err, gc.ErrorMatches, "config connect failed") |
473 | 398 | c.Check(st, gc.IsNil) | 398 | c.Check(st, gc.IsNil) |
474 | 399 | } | 399 | } |
475 | @@ -426,7 +426,7 @@ | |||
476 | 426 | err = os.Remove(osenv.JujuHomePath("environments.yaml")) | 426 | err = os.Remove(osenv.JujuHomePath("environments.yaml")) |
477 | 427 | c.Assert(err, gc.IsNil) | 427 | c.Assert(err, gc.IsNil) |
478 | 428 | 428 | ||
480 | 429 | st, err := juju.NewAPIFromName(coretesting.SampleEnvName, store) | 429 | st, err := juju.NewAPIFromStore(coretesting.SampleEnvName, store) |
481 | 430 | c.Check(err, gc.IsNil) | 430 | c.Check(err, gc.IsNil) |
482 | 431 | st.Close() | 431 | st.Close() |
483 | 432 | } | 432 | } |
484 | @@ -454,7 +454,7 @@ | |||
485 | 454 | // Now we have info for envName2 which will actually | 454 | // Now we have info for envName2 which will actually |
486 | 455 | // cause a connection to the originally bootstrapped | 455 | // cause a connection to the originally bootstrapped |
487 | 456 | // state. | 456 | // state. |
489 | 457 | st, err := juju.NewAPIFromName(envName2, store) | 457 | st, err := juju.NewAPIFromStore(envName2, store) |
490 | 458 | c.Check(err, gc.IsNil) | 458 | c.Check(err, gc.IsNil) |
491 | 459 | st.Close() | 459 | st.Close() |
492 | 460 | 460 | ||
493 | @@ -464,7 +464,7 @@ | |||
494 | 464 | // Disable for now until an upcoming branch fixes it. | 464 | // Disable for now until an upcoming branch fixes it. |
495 | 465 | // err = info2.Destroy() | 465 | // err = info2.Destroy() |
496 | 466 | // c.Assert(err, gc.IsNil) | 466 | // c.Assert(err, gc.IsNil) |
498 | 467 | // st, err = juju.NewAPIFromName(envName2, store) | 467 | // st, err = juju.NewAPIFromStore(envName2, store) |
499 | 468 | // if err == nil { | 468 | // if err == nil { |
500 | 469 | // st.Close() | 469 | // st.Close() |
501 | 470 | // } | 470 | // } |
502 | 471 | 471 | ||
503 | === modified file 'juju/export_test.go' | |||
504 | --- juju/export_test.go 2013-12-04 10:18:57 +0000 | |||
505 | +++ juju/export_test.go 2014-03-19 03:21:48 +0000 | |||
506 | @@ -4,5 +4,5 @@ | |||
507 | 4 | APIOpen = &apiOpen | 4 | APIOpen = &apiOpen |
508 | 5 | APIClose = &apiClose | 5 | APIClose = &apiClose |
509 | 6 | ProviderConnectDelay = &providerConnectDelay | 6 | ProviderConnectDelay = &providerConnectDelay |
511 | 7 | NewAPIFromName = newAPIFromName | 7 | NewAPIFromStore = newAPIFromStore |
512 | 8 | ) | 8 | ) |
513 | 9 | 9 | ||
514 | === modified file 'state/api/apiclient.go' | |||
515 | --- state/api/apiclient.go 2014-02-20 01:09:59 +0000 | |||
516 | +++ state/api/apiclient.go 2014-03-19 03:21:48 +0000 | |||
517 | @@ -25,6 +25,7 @@ | |||
518 | 25 | type State struct { | 25 | type State struct { |
519 | 26 | client *rpc.Conn | 26 | client *rpc.Conn |
520 | 27 | conn *websocket.Conn | 27 | conn *websocket.Conn |
521 | 28 | addr string | ||
522 | 28 | 29 | ||
523 | 29 | // authTag holds the authenticated entity's tag after login. | 30 | // authTag holds the authenticated entity's tag after login. |
524 | 30 | authTag string | 31 | authTag string |
525 | @@ -127,6 +128,7 @@ | |||
526 | 127 | st := &State{ | 128 | st := &State{ |
527 | 128 | client: client, | 129 | client: client, |
528 | 129 | conn: conn, | 130 | conn: conn, |
529 | 131 | addr: cfg.Location.Host, | ||
530 | 130 | serverRoot: "https://" + cfg.Location.Host, | 132 | serverRoot: "https://" + cfg.Location.Host, |
531 | 131 | tag: info.Tag, | 133 | tag: info.Tag, |
532 | 132 | password: info.Password, | 134 | password: info.Password, |
533 | @@ -186,3 +188,8 @@ | |||
534 | 186 | func (s *State) RPCClient() *rpc.Conn { | 188 | func (s *State) RPCClient() *rpc.Conn { |
535 | 187 | return s.client | 189 | return s.client |
536 | 188 | } | 190 | } |
537 | 191 | |||
538 | 192 | // Addr returns the address used to connect to the RPC server. | ||
539 | 193 | func (s *State) Addr() string { | ||
540 | 194 | return s.addr | ||
541 | 195 | } | ||
542 | 189 | 196 | ||
543 | === modified file 'state/api/client.go' | |||
544 | --- state/api/client.go 2014-03-13 22:47:05 +0000 | |||
545 | +++ state/api/client.go 2014-03-19 03:21:48 +0000 | |||
546 | @@ -143,6 +143,15 @@ | |||
547 | 143 | return results.PublicAddress, err | 143 | return results.PublicAddress, err |
548 | 144 | } | 144 | } |
549 | 145 | 145 | ||
550 | 146 | // PrivateAddress returns the private address of the specified | ||
551 | 147 | // machine or unit. | ||
552 | 148 | func (c *Client) PrivateAddress(target string) (string, error) { | ||
553 | 149 | var results params.PrivateAddressResults | ||
554 | 150 | p := params.PrivateAddress{Target: target} | ||
555 | 151 | err := c.st.Call("Client", "", "PrivateAddress", p, &results) | ||
556 | 152 | return results.PrivateAddress, err | ||
557 | 153 | } | ||
558 | 154 | |||
559 | 146 | // ServiceSetYAML sets configuration options on a service | 155 | // ServiceSetYAML sets configuration options on a service |
560 | 147 | // given options in YAML format. | 156 | // given options in YAML format. |
561 | 148 | func (c *Client) ServiceSetYAML(service string, yaml string) error { | 157 | func (c *Client) ServiceSetYAML(service string, yaml string) error { |
562 | 149 | 158 | ||
563 | === modified file 'state/api/params/params.go' | |||
564 | --- state/api/params/params.go 2014-03-13 13:42:50 +0000 | |||
565 | +++ state/api/params/params.go 2014-03-19 03:21:48 +0000 | |||
566 | @@ -236,6 +236,16 @@ | |||
567 | 236 | PublicAddress string | 236 | PublicAddress string |
568 | 237 | } | 237 | } |
569 | 238 | 238 | ||
570 | 239 | // PrivateAddress holds parameters for the PrivateAddress call. | ||
571 | 240 | type PrivateAddress struct { | ||
572 | 241 | Target string | ||
573 | 242 | } | ||
574 | 243 | |||
575 | 244 | // PrivateAddressResults holds results of the PrivateAddress call. | ||
576 | 245 | type PrivateAddressResults struct { | ||
577 | 246 | PrivateAddress string | ||
578 | 247 | } | ||
579 | 248 | |||
580 | 239 | // Resolved holds parameters for the Resolved call. | 249 | // Resolved holds parameters for the Resolved call. |
581 | 240 | type Resolved struct { | 250 | type Resolved struct { |
582 | 241 | UnitName string | 251 | UnitName string |
583 | 242 | 252 | ||
584 | === modified file 'state/apiserver/client/client.go' | |||
585 | --- state/apiserver/client/client.go 2014-03-18 02:36:58 +0000 | |||
586 | +++ state/apiserver/client/client.go 2014-03-19 03:21:48 +0000 | |||
587 | @@ -184,6 +184,34 @@ | |||
588 | 184 | return results, fmt.Errorf("unknown unit or machine %q", p.Target) | 184 | return results, fmt.Errorf("unknown unit or machine %q", p.Target) |
589 | 185 | } | 185 | } |
590 | 186 | 186 | ||
591 | 187 | // PrivateAddress implements the server side of Client.PrivateAddress. | ||
592 | 188 | func (c *Client) PrivateAddress(p params.PrivateAddress) (results params.PrivateAddressResults, err error) { | ||
593 | 189 | switch { | ||
594 | 190 | case names.IsMachine(p.Target): | ||
595 | 191 | machine, err := c.api.state.Machine(p.Target) | ||
596 | 192 | if err != nil { | ||
597 | 193 | return results, err | ||
598 | 194 | } | ||
599 | 195 | addr := instance.SelectInternalAddress(machine.Addresses(), false) | ||
600 | 196 | if addr == "" { | ||
601 | 197 | return results, fmt.Errorf("machine %q has no internal address", machine) | ||
602 | 198 | } | ||
603 | 199 | return params.PrivateAddressResults{PrivateAddress: addr}, nil | ||
604 | 200 | |||
605 | 201 | case names.IsUnit(p.Target): | ||
606 | 202 | unit, err := c.api.state.Unit(p.Target) | ||
607 | 203 | if err != nil { | ||
608 | 204 | return results, err | ||
609 | 205 | } | ||
610 | 206 | addr, ok := unit.PrivateAddress() | ||
611 | 207 | if !ok { | ||
612 | 208 | return results, fmt.Errorf("unit %q has no internal address", unit) | ||
613 | 209 | } | ||
614 | 210 | return params.PrivateAddressResults{PrivateAddress: addr}, nil | ||
615 | 211 | } | ||
616 | 212 | return results, fmt.Errorf("unknown unit or machine %q", p.Target) | ||
617 | 213 | } | ||
618 | 214 | |||
619 | 187 | // ServiceExpose changes the juju-managed firewall to expose any ports that | 215 | // ServiceExpose changes the juju-managed firewall to expose any ports that |
620 | 188 | // were also explicitly marked by units as open. | 216 | // were also explicitly marked by units as open. |
621 | 189 | func (c *Client) ServiceExpose(args params.ServiceExpose) error { | 217 | func (c *Client) ServiceExpose(args params.ServiceExpose) error { |
622 | 190 | 218 | ||
623 | === modified file 'state/apiserver/client/client_test.go' | |||
624 | --- state/apiserver/client/client_test.go 2014-03-18 02:36:58 +0000 | |||
625 | +++ state/apiserver/client/client_test.go 2014-03-19 03:21:48 +0000 | |||
626 | @@ -1413,6 +1413,66 @@ | |||
627 | 1413 | c.Assert(addr, gc.Equals, "127.0.0.1") | 1413 | c.Assert(addr, gc.Equals, "127.0.0.1") |
628 | 1414 | } | 1414 | } |
629 | 1415 | 1415 | ||
630 | 1416 | func (s *clientSuite) TestClientPrivateAddressErrors(c *gc.C) { | ||
631 | 1417 | s.setUpScenario(c) | ||
632 | 1418 | _, err := s.APIState.Client().PrivateAddress("wordpress") | ||
633 | 1419 | c.Assert(err, gc.ErrorMatches, `unknown unit or machine "wordpress"`) | ||
634 | 1420 | _, err = s.APIState.Client().PrivateAddress("0") | ||
635 | 1421 | c.Assert(err, gc.ErrorMatches, `machine "0" has no internal address`) | ||
636 | 1422 | _, err = s.APIState.Client().PrivateAddress("wordpress/0") | ||
637 | 1423 | c.Assert(err, gc.ErrorMatches, `unit "wordpress/0" has no internal address`) | ||
638 | 1424 | } | ||
639 | 1425 | |||
640 | 1426 | func (s *clientSuite) TestClientPrivateAddressMachine(c *gc.C) { | ||
641 | 1427 | s.setUpScenario(c) | ||
642 | 1428 | |||
643 | 1429 | // Internally, instance.SelectInternalAddress is used; the public | ||
644 | 1430 | // address if no cloud-local one is available. | ||
645 | 1431 | m1, err := s.State.Machine("1") | ||
646 | 1432 | c.Assert(err, gc.IsNil) | ||
647 | 1433 | cloudLocalAddress := instance.NewAddress("cloudlocal") | ||
648 | 1434 | cloudLocalAddress.NetworkScope = instance.NetworkCloudLocal | ||
649 | 1435 | publicAddress := instance.NewAddress("public") | ||
650 | 1436 | publicAddress.NetworkScope = instance.NetworkCloudLocal | ||
651 | 1437 | err = m1.SetAddresses([]instance.Address{publicAddress}) | ||
652 | 1438 | c.Assert(err, gc.IsNil) | ||
653 | 1439 | addr, err := s.APIState.Client().PrivateAddress("1") | ||
654 | 1440 | c.Assert(err, gc.IsNil) | ||
655 | 1441 | c.Assert(addr, gc.Equals, "public") | ||
656 | 1442 | err = m1.SetAddresses([]instance.Address{cloudLocalAddress, publicAddress}) | ||
657 | 1443 | addr, err = s.APIState.Client().PrivateAddress("1") | ||
658 | 1444 | c.Assert(err, gc.IsNil) | ||
659 | 1445 | c.Assert(addr, gc.Equals, "cloudlocal") | ||
660 | 1446 | } | ||
661 | 1447 | |||
662 | 1448 | func (s *clientSuite) TestClientPrivateAddressUnitWithMachine(c *gc.C) { | ||
663 | 1449 | s.setUpScenario(c) | ||
664 | 1450 | |||
665 | 1451 | // Private address of unit is taken from its machine | ||
666 | 1452 | // (if its machine has addresses). | ||
667 | 1453 | m1, err := s.State.Machine("1") | ||
668 | 1454 | publicAddress := instance.NewAddress("public") | ||
669 | 1455 | publicAddress.NetworkScope = instance.NetworkCloudLocal | ||
670 | 1456 | err = m1.SetAddresses([]instance.Address{publicAddress}) | ||
671 | 1457 | c.Assert(err, gc.IsNil) | ||
672 | 1458 | addr, err := s.APIState.Client().PrivateAddress("wordpress/0") | ||
673 | 1459 | c.Assert(err, gc.IsNil) | ||
674 | 1460 | c.Assert(addr, gc.Equals, "public") | ||
675 | 1461 | } | ||
676 | 1462 | |||
677 | 1463 | func (s *clientSuite) TestClientPrivateAddressUnitWithoutMachine(c *gc.C) { | ||
678 | 1464 | s.setUpScenario(c) | ||
679 | 1465 | // If the unit's machine has no addresses, the public address | ||
680 | 1466 | // comes from the unit's document. | ||
681 | 1467 | u, err := s.State.Unit("wordpress/1") | ||
682 | 1468 | c.Assert(err, gc.IsNil) | ||
683 | 1469 | err = u.SetPrivateAddress("127.0.0.1") | ||
684 | 1470 | c.Assert(err, gc.IsNil) | ||
685 | 1471 | addr, err := s.APIState.Client().PrivateAddress("wordpress/1") | ||
686 | 1472 | c.Assert(err, gc.IsNil) | ||
687 | 1473 | c.Assert(addr, gc.Equals, "127.0.0.1") | ||
688 | 1474 | } | ||
689 | 1475 | |||
690 | 1416 | func (s *clientSuite) TestClientEnvironmentGet(c *gc.C) { | 1476 | func (s *clientSuite) TestClientEnvironmentGet(c *gc.C) { |
691 | 1417 | envConfig, err := s.State.EnvironConfig() | 1477 | envConfig, err := s.State.EnvironConfig() |
692 | 1418 | c.Assert(err, gc.IsNil) | 1478 | c.Assert(err, gc.IsNil) |
693 | 1419 | 1479 | ||
694 | === modified file 'utils/ssh/ssh.go' | |||
695 | --- utils/ssh/ssh.go 2014-03-10 20:22:44 +0000 | |||
696 | +++ utils/ssh/ssh.go 2014-03-19 03:21:48 +0000 | |||
697 | @@ -19,6 +19,9 @@ | |||
698 | 19 | 19 | ||
699 | 20 | // Options is a client-implementation independent SSH options set. | 20 | // Options is a client-implementation independent SSH options set. |
700 | 21 | type Options struct { | 21 | type Options struct { |
701 | 22 | // proxyCommand specifies the command to | ||
702 | 23 | // execute to proxy SSH traffic through. | ||
703 | 24 | proxyCommand []string | ||
704 | 22 | // ssh server port; zero means use the default (22) | 25 | // ssh server port; zero means use the default (22) |
705 | 23 | port int | 26 | port int |
706 | 24 | // no PTY forced by default | 27 | // no PTY forced by default |
707 | @@ -31,6 +34,11 @@ | |||
708 | 31 | identities []string | 34 | identities []string |
709 | 32 | } | 35 | } |
710 | 33 | 36 | ||
711 | 37 | // SetProxyCommand sets a command to execute to proxy traffic through. | ||
712 | 38 | func (o *Options) SetProxyCommand(command ...string) { | ||
713 | 39 | o.proxyCommand = append([]string{}, command...) | ||
714 | 40 | } | ||
715 | 41 | |||
716 | 34 | // SetPort sets the SSH server port to connect to. | 42 | // SetPort sets the SSH server port to connect to. |
717 | 35 | func (o *Options) SetPort(port int) { | 43 | func (o *Options) SetPort(port int) { |
718 | 36 | o.port = port | 44 | o.port = port |
719 | 37 | 45 | ||
720 | === modified file 'utils/ssh/ssh_openssh.go' | |||
721 | --- utils/ssh/ssh_openssh.go 2014-02-22 13:45:10 +0000 | |||
722 | +++ utils/ssh/ssh_openssh.go 2014-03-19 03:21:48 +0000 | |||
723 | @@ -71,6 +71,9 @@ | |||
724 | 71 | if options == nil { | 71 | if options == nil { |
725 | 72 | options = &Options{} | 72 | options = &Options{} |
726 | 73 | } | 73 | } |
727 | 74 | if len(options.proxyCommand) > 0 { | ||
728 | 75 | args["-o"] = append(args["-o"], "ProxyCommand "+utils.CommandString(options.proxyCommand...)) | ||
729 | 76 | } | ||
730 | 74 | if !options.passwordAuthAllowed { | 77 | if !options.passwordAuthAllowed { |
731 | 75 | args["-o"] = append(args["-o"], "PasswordAuthentication no") | 78 | args["-o"] = append(args["-o"], "PasswordAuthentication no") |
732 | 76 | } | 79 | } |
Reviewers: mp+211662_ code.launchpad. net,
Message:
Please take a look.
Description:
Add --proxy option to "juju ssh"
juju ssh will now, by default, proxy
connections through the API server host.
I have not yet implemented support for the
ProxyCommand option for the go.crypto/ssh
client; this will be done as a follow up.
https:/ /code.launchpad .net/~axwalk/ juju-core/ ssh-proxycomman d/+merge/ 211662
(do not edit description out of merge proposal)
Please review this at https:/ /codereview. appspot. com/77340046/
Affected files (+300, -89 lines): scp_test. go ssh_test. go test.go apiclient. go params/ params. go /client/ client. go /client/ client_ test.go ssh_openssh. go
A [revision details]
M cmd/juju/scp.go
M cmd/juju/
M cmd/juju/ssh.go
M cmd/juju/
M juju/api.go
M juju/apiconn_
M juju/export_test.go
M state/api/
M state/api/client.go
M state/api/
M state/apiserver
M state/apiserver
M utils/ssh/ssh.go
M utils/ssh/