Merge lp:~axwalk/juju-core/ssh-proxycommand into lp:~go-bot/juju-core/trunk

Proposed by Andrew Wilkins
Status: Merged
Approved by: Andrew Wilkins
Approved revision: no longer in the source branch.
Merged at revision: 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
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.

https://codereview.appspot.com/77340046/

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.

https://codereview.appspot.com/77340046/

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

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-proxycommand/+merge/211662

(do not edit description out of merge proposal)

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

Affected files (+300, -89 lines):
   A [revision details]
   M cmd/juju/scp.go
   M cmd/juju/scp_test.go
   M cmd/juju/ssh.go
   M cmd/juju/ssh_test.go
   M juju/api.go
   M juju/apiconn_test.go
   M juju/export_test.go
   M state/api/apiclient.go
   M state/api/client.go
   M state/api/params/params.go
   M state/apiserver/client/client.go
   M state/apiserver/client/client_test.go
   M utils/ssh/ssh.go
   M utils/ssh/ssh_openssh.go

Revision history for this message
Tim Penhey (thumper) wrote :

LGTM - the code looks fine, and I'm assuming you have tested it.

https://codereview.appspot.com/77340046/

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.

https://codereview.appspot.com/77340046/

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'cmd/juju/scp.go'
--- cmd/juju/scp.go 2014-02-24 16:21:43 +0000
+++ cmd/juju/scp.go 2014-03-19 03:21:48 +0000
@@ -106,5 +106,13 @@
106 targets = append(targets, arg)106 targets = append(targets, arg)
107 }107 }
108 }108 }
109 return ssh.Copy(targets, extraArgs, nil)109
110 var options *ssh.Options
111 if c.proxy {
112 options = new(ssh.Options)
113 if err := c.setProxyCommand(options); err != nil {
114 return err
115 }
116 }
117 return ssh.Copy(targets, extraArgs, options)
110}118}
111119
=== modified file 'cmd/juju/scp_test.go'
--- cmd/juju/scp_test.go 2014-02-24 16:21:43 +0000
+++ cmd/juju/scp_test.go 2014-03-19 03:21:48 +0000
@@ -27,67 +27,64 @@
27 about string27 about string
28 args []string28 args []string
29 result string29 result string
30 proxy bool
30 error string31 error string
31}{32}{
32 {33 {
33 "scp from machine 0 to current dir",34 about: "scp from machine 0 to current dir",
34 []string{"0:foo", "."},35 args: []string{"0:foo", "."},
35 commonArgs + "ubuntu@dummyenv-0.dns:foo .\n",36 result: commonArgsNoProxy + "ubuntu@dummyenv-0.dns:foo .\n",
36 "",37 },
37 },38 {
38 {39 about: "scp from machine 0 to current dir with extra args",
39 "scp from machine 0 to current dir with extra args",40 args: []string{"0:foo", ".", "-rv -o SomeOption"},
40 []string{"0:foo", ".", "-rv -o SomeOption"},41 result: commonArgsNoProxy + "-rv -o SomeOption ubuntu@dummyenv-0.dns:foo .\n",
41 commonArgs + "-rv -o SomeOption ubuntu@dummyenv-0.dns:foo .\n",42 },
42 "",43 {
43 },44 about: "scp from current dir to machine 0",
44 {45 args: []string{"foo", "0:"},
45 "scp from current dir to machine 0",46 result: commonArgsNoProxy + "foo ubuntu@dummyenv-0.dns:\n",
46 []string{"foo", "0:"},47 },
47 commonArgs + "foo ubuntu@dummyenv-0.dns:\n",48 {
48 "",49 about: "scp from current dir to machine 0 with extra args",
49 },50 args: []string{"foo", "0:", "-r -v"},
50 {51 result: commonArgsNoProxy + "-r -v foo ubuntu@dummyenv-0.dns:\n",
51 "scp from current dir to machine 0 with extra args",52 },
52 []string{"foo", "0:", "-r -v"},53 {
53 commonArgs + "-r -v foo ubuntu@dummyenv-0.dns:\n",54 about: "scp from machine 0 to unit mysql/0",
54 "",55 args: []string{"0:foo", "mysql/0:/foo"},
55 },56 result: commonArgsNoProxy + "ubuntu@dummyenv-0.dns:foo ubuntu@dummyenv-0.dns:/foo\n",
56 {57 },
57 "scp from machine 0 to unit mysql/0",58 {
58 []string{"0:foo", "mysql/0:/foo"},59 about: "scp from machine 0 to unit mysql/0 and extra args",
59 commonArgs + "ubuntu@dummyenv-0.dns:foo ubuntu@dummyenv-0.dns:/foo\n",60 args: []string{"0:foo", "mysql/0:/foo", "-q"},
60 "",61 result: commonArgsNoProxy + "-q ubuntu@dummyenv-0.dns:foo ubuntu@dummyenv-0.dns:/foo\n",
61 },62 },
62 {63 {
63 "scp from machine 0 to unit mysql/0 and extra args",64 about: "scp from machine 0 to unit mysql/0 and extra args before",
64 []string{"0:foo", "mysql/0:/foo", "-q"},65 args: []string{"-q", "-r", "0:foo", "mysql/0:/foo"},
65 commonArgs + "-q ubuntu@dummyenv-0.dns:foo ubuntu@dummyenv-0.dns:/foo\n",66 error: `unexpected argument "-q"; extra arguments must be last`,
66 "",67 },
67 },68 {
68 {69 about: "scp two local files to unit mysql/0",
69 "scp from machine 0 to unit mysql/0 and extra args before",70 args: []string{"file1", "file2", "mysql/0:/foo/"},
70 []string{"-q", "-r", "0:foo", "mysql/0:/foo"},71 result: commonArgsNoProxy + "file1 file2 ubuntu@dummyenv-0.dns:/foo/\n",
71 "",72 },
72 `unexpected argument "-q"; extra arguments must be last`,73 {
73 },74 about: "scp from unit mongodb/1 to unit mongodb/0 and multiple extra args",
74 {75 args: []string{"mongodb/1:foo", "mongodb/0:", "-r -v -q -l5"},
75 "scp two local files to unit mysql/0",76 result: commonArgsNoProxy + "-r -v -q -l5 ubuntu@dummyenv-2.dns:foo ubuntu@dummyenv-1.dns:\n",
76 []string{"file1", "file2", "mysql/0:/foo/"},77 },
77 commonArgs + "file1 file2 ubuntu@dummyenv-0.dns:/foo/\n",78 {
78 "",79 about: "scp works with IPv6 addresses",
79 },80 args: []string{"ipv6-svc/0:foo", "bar"},
80 {81 result: commonArgsNoProxy + `ubuntu@\[2001:db8::\]:foo bar` + "\n",
81 "scp from unit mongodb/1 to unit mongodb/0 and multiple extra args",82 },
82 []string{"mongodb/1:foo", "mongodb/0:", "-r -v -q -l5"},83 {
83 commonArgs + "-r -v -q -l5 ubuntu@dummyenv-2.dns:foo ubuntu@dummyenv-1.dns:\n",84 about: "scp from machine 0 to unit mysql/0 with proxy",
84 "",85 args: []string{"0:foo", "mysql/0:/foo"},
85 },86 result: commonArgs + "ubuntu@dummyenv-0.dns:foo ubuntu@dummyenv-0.dns:/foo\n",
86 {87 proxy: true,
87 "scp works with IPv6 addresses",
88 []string{"ipv6-svc/0:foo", "bar"},
89 commonArgs + `ubuntu@\[2001:db8::\]:foo bar` + "\n",
90 "",
91 },88 },
92}89}
9390
@@ -122,6 +119,7 @@
122 c.Logf("test %d: %s -> %s\n", i, t.about, t.args)119 c.Logf("test %d: %s -> %s\n", i, t.about, t.args)
123 ctx := coretesting.Context(c)120 ctx := coretesting.Context(c)
124 scpcmd := &SCPCommand{}121 scpcmd := &SCPCommand{}
122 scpcmd.proxy = t.proxy
125123
126 err := scpcmd.Init(t.args)124 err := scpcmd.Init(t.args)
127 c.Check(err, gc.IsNil)125 c.Check(err, gc.IsNil)
128126
=== modified file 'cmd/juju/ssh.go'
--- cmd/juju/ssh.go 2014-02-22 13:45:10 +0000
+++ cmd/juju/ssh.go 2014-03-19 03:21:48 +0000
@@ -6,8 +6,13 @@
6import (6import (
7 "errors"7 "errors"
8 "fmt"8 "fmt"
9 "net"
10 "os"
11 "os/exec"
9 "time"12 "time"
1013
14 "launchpad.net/gnuflag"
15
11 "launchpad.net/juju-core/cmd"16 "launchpad.net/juju-core/cmd"
12 "launchpad.net/juju-core/instance"17 "launchpad.net/juju-core/instance"
13 "launchpad.net/juju-core/juju"18 "launchpad.net/juju-core/juju"
@@ -26,13 +31,34 @@
26// SSHCommon provides common methods for SSHCommand, SCPCommand and DebugHooksCommand.31// SSHCommon provides common methods for SSHCommand, SCPCommand and DebugHooksCommand.
27type SSHCommon struct {32type SSHCommon struct {
28 cmd.EnvCommandBase33 cmd.EnvCommandBase
34 proxy bool
29 Target string35 Target string
30 Args []string36 Args []string
31 apiClient *api.Client37 apiClient *api.Client
38 apiAddr string
32 // Only used for compatibility with 1.1639 // Only used for compatibility with 1.16
33 rawConn *juju.Conn40 rawConn *juju.Conn
34}41}
3542
43func (c *SSHCommon) SetFlags(f *gnuflag.FlagSet) {
44 c.EnvCommandBase.SetFlags(f)
45 f.BoolVar(&c.proxy, "proxy", true, "proxy through the API server")
46}
47
48// setProxyCommand sets the proxy command option.
49func (c *SSHCommon) setProxyCommand(options *ssh.Options) error {
50 apiServerHost, _, err := net.SplitHostPort(c.apiAddr)
51 if err != nil {
52 return fmt.Errorf("failed to get proxy address: %v", err)
53 }
54 juju, err := getJujuExecutable()
55 if err != nil {
56 return fmt.Errorf("failed to get juju executable path: %v", err)
57 }
58 options.SetProxyCommand(juju, "ssh", "--proxy=false", apiServerHost, "-T", "nc -q0 %h %p")
59 return nil
60}
61
36const sshDoc = `62const sshDoc = `
37Launch an ssh shell on the machine identified by the <target> parameter.63Launch an ssh shell on the machine identified by the <target> parameter.
38<target> can be either a machine id as listed by "juju status" in the64<target> can be either a machine id as listed by "juju status" in the
@@ -75,6 +101,12 @@
75 return nil101 return nil
76}102}
77103
104// getJujuExecutable returns the path to the juju
105// executable, or an error if it could not be found.
106var getJujuExecutable = func() (string, error) {
107 return exec.LookPath(os.Args[0])
108}
109
78// Run resolves c.Target to a machine, to the address of a i110// Run resolves c.Target to a machine, to the address of a i
79// machine or unit forks ssh passing any arguments provided.111// machine or unit forks ssh passing any arguments provided.
80func (c *SSHCommand) Run(ctx *cmd.Context) error {112func (c *SSHCommand) Run(ctx *cmd.Context) error {
@@ -92,6 +124,11 @@
92 }124 }
93 var options ssh.Options125 var options ssh.Options
94 options.EnablePTY()126 options.EnablePTY()
127 if c.proxy {
128 if err := c.setProxyCommand(&options); err != nil {
129 return err
130 }
131 }
95 cmd := ssh.Command("ubuntu@"+host, c.Args, &options)132 cmd := ssh.Command("ubuntu@"+host, c.Args, &options)
96 cmd.Stdin = ctx.Stdin133 cmd.Stdin = ctx.Stdin
97 cmd.Stdout = ctx.Stdout134 cmd.Stdout = ctx.Stdout
@@ -102,9 +139,13 @@
102// initAPIClient initialises the API connection.139// initAPIClient initialises the API connection.
103// It is the caller's responsibility to close the connection.140// It is the caller's responsibility to close the connection.
104func (c *SSHCommon) initAPIClient() (*api.Client, error) {141func (c *SSHCommon) initAPIClient() (*api.Client, error) {
105 var err error142 st, err := juju.NewAPIFromName(c.EnvName)
106 c.apiClient, err = juju.NewAPIClientFromName(c.EnvName)143 if err != nil {
107 return c.apiClient, err144 return nil, err
145 }
146 c.apiClient = st.Client()
147 c.apiAddr = st.Addr()
148 return c.apiClient, nil
108}149}
109150
110// attemptStarter is an interface corresponding to utils.AttemptStrategy151// attemptStarter is an interface corresponding to utils.AttemptStrategy
@@ -158,9 +199,15 @@
158 if err != nil {199 if err != nil {
159 return "", err200 return "", err
160 }201 }
161 addr := instance.SelectPublicAddress(machine.Addresses())202 var addr string
162 if addr == "" {203 if c.proxy {
163 return "", fmt.Errorf("machine %q has no public address", machine)204 if addr = instance.SelectInternalAddress(machine.Addresses(), false); addr == "" {
205 return "", fmt.Errorf("machine %q has no internal address", machine)
206 }
207 } else {
208 if addr = instance.SelectPublicAddress(machine.Addresses()); addr == "" {
209 return "", fmt.Errorf("machine %q has no public address", machine)
210 }
164 }211 }
165 return addr, nil212 return addr, nil
166 }213 }
@@ -171,9 +218,16 @@
171 if err != nil {218 if err != nil {
172 return "", err219 return "", err
173 }220 }
174 addr, ok := unit.PublicAddress()221 var addr string
175 if !ok {222 var ok bool
176 return "", fmt.Errorf("unit %q has no public address", unit)223 if c.proxy {
224 if addr, ok = unit.PrivateAddress(); !ok {
225 return "", fmt.Errorf("unit %q has no internal address", unit)
226 }
227 } else {
228 if addr, ok = unit.PublicAddress(); !ok {
229 return "", fmt.Errorf("unit %q has no public address", unit)
230 }
177 }231 }
178 return addr, nil232 return addr, nil
179 }233 }
@@ -181,6 +235,11 @@
181}235}
182236
183func (c *SSHCommon) hostFromTarget(target string) (string, error) {237func (c *SSHCommon) hostFromTarget(target string) (string, error) {
238 // If the target is neither a machine nor a unit,
239 // assume it's a hostname and try it directly.
240 if !names.IsMachine(target) && !names.IsUnit(target) {
241 return target, nil
242 }
184 var addr string243 var addr string
185 var err error244 var err error
186 var useStateConn bool245 var useStateConn bool
@@ -189,9 +248,13 @@
189 // a loop.248 // a loop.
190 for a := sshHostFromTargetAttemptStrategy.Start(); a.Next(); {249 for a := sshHostFromTargetAttemptStrategy.Start(); a.Next(); {
191 if !useStateConn {250 if !useStateConn {
192 addr, err = c.apiClient.PublicAddress(target)251 if c.proxy {
252 addr, err = c.apiClient.PrivateAddress(target)
253 } else {
254 addr, err = c.apiClient.PublicAddress(target)
255 }
193 if params.IsCodeNotImplemented(err) {256 if params.IsCodeNotImplemented(err) {
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)")
195 useStateConn = true258 useStateConn = true
196 }259 }
197 }260 }
198261
=== modified file 'cmd/juju/ssh_test.go'
--- cmd/juju/ssh_test.go 2014-02-22 13:45:10 +0000
+++ cmd/juju/ssh_test.go 2014-03-19 03:21:48 +0000
@@ -39,6 +39,7 @@
3939
40func (s *SSHCommonSuite) SetUpTest(c *gc.C) {40func (s *SSHCommonSuite) SetUpTest(c *gc.C) {
41 s.JujuConnSuite.SetUpTest(c)41 s.JujuConnSuite.SetUpTest(c)
42 s.PatchValue(&getJujuExecutable, func() (string, error) { return "juju", nil })
4243
43 s.bin = c.MkDir()44 s.bin = c.MkDir()
44 s.PatchEnvPathPrepend(s.bin)45 s.PatchEnvPathPrepend(s.bin)
@@ -53,8 +54,10 @@
53}54}
5455
55const (56const (
56 commonArgs = `-o StrictHostKeyChecking no -o PasswordAuthentication no `57 commonArgsNoProxy = `-o StrictHostKeyChecking no -o PasswordAuthentication no `
57 sshArgs = commonArgs + `-t -t `58 commonArgs = `-o StrictHostKeyChecking no -o ProxyCommand juju ssh --proxy=false 127.0.0.1 -T "nc -q0 %h %p" -o PasswordAuthentication no `
59 sshArgs = commonArgs + `-t -t `
60 sshArgsNoProxy = commonArgsNoProxy + `-t -t `
58)61)
5962
60var sshTests = []struct {63var sshTests = []struct {
@@ -82,6 +85,11 @@
82 []string{"ssh", "mongodb/1", "ls", "/"},85 []string{"ssh", "mongodb/1", "ls", "/"},
83 sshArgs + "ubuntu@dummyenv-2.dns ls /\n",86 sshArgs + "ubuntu@dummyenv-2.dns ls /\n",
84 },87 },
88 {
89 "connect to unit mysql/0 without proxy",
90 []string{"ssh", "--proxy=false", "mysql/0"},
91 sshArgsNoProxy + "ubuntu@dummyenv-0.dns\n",
92 },
85}93}
8694
87func (s *SSHSuite) TestSSHCommand(c *gc.C) {95func (s *SSHSuite) TestSSHCommand(c *gc.C) {
8896
=== modified file 'juju/api.go'
--- juju/api.go 2014-03-12 10:59:17 +0000
+++ juju/api.go 2014-03-19 03:21:48 +0000
@@ -100,17 +100,24 @@
100 return keymanager.NewClient(st), nil100 return keymanager.NewClient(st), nil
101}101}
102102
103// NewAPIFromName returns an api.State connected to the API Server for
104// the named environment. If envName is "", the default environment will
105// be used.
106func NewAPIFromName(envName string) (*api.State, error) {
107 return newAPIClient(envName)
108}
109
103func newAPIClient(envName string) (*api.State, error) {110func newAPIClient(envName string) (*api.State, error) {
104 store, err := configstore.NewDisk(osenv.JujuHome())111 store, err := configstore.NewDisk(osenv.JujuHome())
105 if err != nil {112 if err != nil {
106 return nil, err113 return nil, err
107 }114 }
108 return newAPIFromName(envName, store)115 return newAPIFromStore(envName, store)
109}116}
110117
111// newAPIFromName implements the bulk of NewAPIClientFromName118// newAPIFromStore implements the bulk of NewAPIClientFromName
112// but is separate for testing purposes.119// but is separate for testing purposes.
113func newAPIFromName(envName string, store configstore.Storage) (*api.State, error) {120func newAPIFromStore(envName string, store configstore.Storage) (*api.State, error) {
114 // Try to read the default environment configuration file.121 // Try to read the default environment configuration file.
115 // If it doesn't exist, we carry on in case122 // If it doesn't exist, we carry on in case
116 // there's some environment info for that environment.123 // there's some environment info for that environment.
117124
=== modified file 'juju/apiconn_test.go'
--- juju/apiconn_test.go 2014-03-13 07:54:56 +0000
+++ juju/apiconn_test.go 2014-03-19 03:21:48 +0000
@@ -139,11 +139,11 @@
139 called++139 called++
140 return expectState, nil140 return expectState, nil
141 }141 }
142 // Give NewAPIFromName a store interface that can report when the142 // Give NewAPIFromStore a store interface that can report when the
143 // config was written to, to ensure the cache isn't updated.143 // config was written to, to ensure the cache isn't updated.
144 defer testbase.PatchValue(juju.APIOpen, apiOpen).Restore()144 defer testbase.PatchValue(juju.APIOpen, apiOpen).Restore()
145 mockStore := &storageWithWriteNotify{store: store}145 mockStore := &storageWithWriteNotify{store: store}
146 st, err := juju.NewAPIFromName("noconfig", mockStore)146 st, err := juju.NewAPIFromStore("noconfig", mockStore)
147 c.Assert(err, gc.IsNil)147 c.Assert(err, gc.IsNil)
148 c.Assert(st, gc.Equals, expectState)148 c.Assert(st, gc.Equals, expectState)
149 c.Assert(called, gc.Equals, 1)149 c.Assert(called, gc.Equals, 1)
@@ -186,7 +186,7 @@
186 return expectState, nil186 return expectState, nil
187 }187 }
188 defer testbase.PatchValue(juju.APIOpen, apiOpen).Restore()188 defer testbase.PatchValue(juju.APIOpen, apiOpen).Restore()
189 st, err := juju.NewAPIFromName("myenv", store)189 st, err := juju.NewAPIFromStore("myenv", store)
190 c.Assert(err, gc.IsNil)190 c.Assert(err, gc.IsNil)
191 c.Assert(st, gc.Equals, expectState)191 c.Assert(st, gc.Equals, expectState)
192 c.Assert(called, gc.Equals, 1)192 c.Assert(called, gc.Equals, 1)
@@ -209,7 +209,7 @@
209 expectErr := fmt.Errorf("an error")209 expectErr := fmt.Errorf("an error")
210 store := newConfigStoreWithError(expectErr)210 store := newConfigStoreWithError(expectErr)
211 defer testbase.PatchValue(juju.APIOpen, panicAPIOpen).Restore()211 defer testbase.PatchValue(juju.APIOpen, panicAPIOpen).Restore()
212 client, err := juju.NewAPIFromName("noconfig", store)212 client, err := juju.NewAPIFromStore("noconfig", store)
213 c.Assert(err, gc.Equals, expectErr)213 c.Assert(err, gc.Equals, expectErr)
214 c.Assert(client, gc.IsNil)214 c.Assert(client, gc.IsNil)
215}215}
@@ -228,7 +228,7 @@
228 })228 })
229 defer testbase.PatchValue(juju.APIOpen, panicAPIOpen).Restore()229 defer testbase.PatchValue(juju.APIOpen, panicAPIOpen).Restore()
230230
231 st, err := juju.NewAPIFromName("noconfig", store)231 st, err := juju.NewAPIFromStore("noconfig", store)
232 c.Assert(err, gc.ErrorMatches, `environment "noconfig" not found`)232 c.Assert(err, gc.ErrorMatches, `environment "noconfig" not found`)
233 c.Assert(st, gc.IsNil)233 c.Assert(st, gc.IsNil)
234}234}
@@ -246,7 +246,7 @@
246 return nil, expectErr246 return nil, expectErr
247 }247 }
248 defer testbase.PatchValue(juju.APIOpen, apiOpen).Restore()248 defer testbase.PatchValue(juju.APIOpen, apiOpen).Restore()
249 st, err := juju.NewAPIFromName("noconfig", store)249 st, err := juju.NewAPIFromStore("noconfig", store)
250 c.Assert(err, gc.Equals, expectErr)250 c.Assert(err, gc.Equals, expectErr)
251 c.Assert(st, gc.IsNil)251 c.Assert(st, gc.IsNil)
252}252}
@@ -277,7 +277,7 @@
277 defer restoreAPIClose.Restore()277 defer restoreAPIClose.Restore()
278278
279 startTime := time.Now()279 startTime := time.Now()
280 st, err := juju.NewAPIFromName(coretesting.SampleEnvName, store)280 st, err := juju.NewAPIFromStore(coretesting.SampleEnvName, store)
281 c.Assert(err, gc.IsNil)281 c.Assert(err, gc.IsNil)
282 // The connection logic should wait for some time before opening282 // The connection logic should wait for some time before opening
283 // the API from the configuration.283 // the API from the configuration.
@@ -342,7 +342,7 @@
342342
343 done := make(chan struct{})343 done := make(chan struct{})
344 go func() {344 go func() {
345 st, err := juju.NewAPIFromName(coretesting.SampleEnvName, store)345 st, err := juju.NewAPIFromStore(coretesting.SampleEnvName, store)
346 c.Check(err, gc.IsNil)346 c.Check(err, gc.IsNil)
347 c.Check(st, gc.Equals, infoOpenedState)347 c.Check(st, gc.Equals, infoOpenedState)
348 close(done)348 close(done)
@@ -360,7 +360,7 @@
360 c.Fatalf("api never opened via config")360 c.Fatalf("api never opened via config")
361 }361 }
362 // Let the info endpoint open go ahead and362 // Let the info endpoint open go ahead and
363 // check that the NewAPIFromName call returns.363 // check that the NewAPIFromStore call returns.
364 infoEndpointOpened <- struct{}{}364 infoEndpointOpened <- struct{}{}
365 select {365 select {
366 case <-done:366 case <-done:
@@ -393,7 +393,7 @@
393 return nil, fmt.Errorf("config connect failed")393 return nil, fmt.Errorf("config connect failed")
394 }394 }
395 defer testbase.PatchValue(juju.APIOpen, apiOpen).Restore()395 defer testbase.PatchValue(juju.APIOpen, apiOpen).Restore()
396 st, err := juju.NewAPIFromName(coretesting.SampleEnvName, store)396 st, err := juju.NewAPIFromStore(coretesting.SampleEnvName, store)
397 c.Check(err, gc.ErrorMatches, "config connect failed")397 c.Check(err, gc.ErrorMatches, "config connect failed")
398 c.Check(st, gc.IsNil)398 c.Check(st, gc.IsNil)
399}399}
@@ -426,7 +426,7 @@
426 err = os.Remove(osenv.JujuHomePath("environments.yaml"))426 err = os.Remove(osenv.JujuHomePath("environments.yaml"))
427 c.Assert(err, gc.IsNil)427 c.Assert(err, gc.IsNil)
428428
429 st, err := juju.NewAPIFromName(coretesting.SampleEnvName, store)429 st, err := juju.NewAPIFromStore(coretesting.SampleEnvName, store)
430 c.Check(err, gc.IsNil)430 c.Check(err, gc.IsNil)
431 st.Close()431 st.Close()
432}432}
@@ -454,7 +454,7 @@
454 // Now we have info for envName2 which will actually454 // Now we have info for envName2 which will actually
455 // cause a connection to the originally bootstrapped455 // cause a connection to the originally bootstrapped
456 // state.456 // state.
457 st, err := juju.NewAPIFromName(envName2, store)457 st, err := juju.NewAPIFromStore(envName2, store)
458 c.Check(err, gc.IsNil)458 c.Check(err, gc.IsNil)
459 st.Close()459 st.Close()
460460
@@ -464,7 +464,7 @@
464 // Disable for now until an upcoming branch fixes it.464 // Disable for now until an upcoming branch fixes it.
465 // err = info2.Destroy()465 // err = info2.Destroy()
466 // c.Assert(err, gc.IsNil)466 // c.Assert(err, gc.IsNil)
467 // st, err = juju.NewAPIFromName(envName2, store)467 // st, err = juju.NewAPIFromStore(envName2, store)
468 // if err == nil {468 // if err == nil {
469 // st.Close()469 // st.Close()
470 // }470 // }
471471
=== modified file 'juju/export_test.go'
--- juju/export_test.go 2013-12-04 10:18:57 +0000
+++ juju/export_test.go 2014-03-19 03:21:48 +0000
@@ -4,5 +4,5 @@
4 APIOpen = &apiOpen4 APIOpen = &apiOpen
5 APIClose = &apiClose5 APIClose = &apiClose
6 ProviderConnectDelay = &providerConnectDelay6 ProviderConnectDelay = &providerConnectDelay
7 NewAPIFromName = newAPIFromName7 NewAPIFromStore = newAPIFromStore
8)8)
99
=== modified file 'state/api/apiclient.go'
--- state/api/apiclient.go 2014-02-20 01:09:59 +0000
+++ state/api/apiclient.go 2014-03-19 03:21:48 +0000
@@ -25,6 +25,7 @@
25type State struct {25type State struct {
26 client *rpc.Conn26 client *rpc.Conn
27 conn *websocket.Conn27 conn *websocket.Conn
28 addr string
2829
29 // authTag holds the authenticated entity's tag after login.30 // authTag holds the authenticated entity's tag after login.
30 authTag string31 authTag string
@@ -127,6 +128,7 @@
127 st := &State{128 st := &State{
128 client: client,129 client: client,
129 conn: conn,130 conn: conn,
131 addr: cfg.Location.Host,
130 serverRoot: "https://" + cfg.Location.Host,132 serverRoot: "https://" + cfg.Location.Host,
131 tag: info.Tag,133 tag: info.Tag,
132 password: info.Password,134 password: info.Password,
@@ -186,3 +188,8 @@
186func (s *State) RPCClient() *rpc.Conn {188func (s *State) RPCClient() *rpc.Conn {
187 return s.client189 return s.client
188}190}
191
192// Addr returns the address used to connect to the RPC server.
193func (s *State) Addr() string {
194 return s.addr
195}
189196
=== modified file 'state/api/client.go'
--- state/api/client.go 2014-03-13 22:47:05 +0000
+++ state/api/client.go 2014-03-19 03:21:48 +0000
@@ -143,6 +143,15 @@
143 return results.PublicAddress, err143 return results.PublicAddress, err
144}144}
145145
146// PrivateAddress returns the private address of the specified
147// machine or unit.
148func (c *Client) PrivateAddress(target string) (string, error) {
149 var results params.PrivateAddressResults
150 p := params.PrivateAddress{Target: target}
151 err := c.st.Call("Client", "", "PrivateAddress", p, &results)
152 return results.PrivateAddress, err
153}
154
146// ServiceSetYAML sets configuration options on a service155// ServiceSetYAML sets configuration options on a service
147// given options in YAML format.156// given options in YAML format.
148func (c *Client) ServiceSetYAML(service string, yaml string) error {157func (c *Client) ServiceSetYAML(service string, yaml string) error {
149158
=== modified file 'state/api/params/params.go'
--- state/api/params/params.go 2014-03-13 13:42:50 +0000
+++ state/api/params/params.go 2014-03-19 03:21:48 +0000
@@ -236,6 +236,16 @@
236 PublicAddress string236 PublicAddress string
237}237}
238238
239// PrivateAddress holds parameters for the PrivateAddress call.
240type PrivateAddress struct {
241 Target string
242}
243
244// PrivateAddressResults holds results of the PrivateAddress call.
245type PrivateAddressResults struct {
246 PrivateAddress string
247}
248
239// Resolved holds parameters for the Resolved call.249// Resolved holds parameters for the Resolved call.
240type Resolved struct {250type Resolved struct {
241 UnitName string251 UnitName string
242252
=== modified file 'state/apiserver/client/client.go'
--- state/apiserver/client/client.go 2014-03-18 02:36:58 +0000
+++ state/apiserver/client/client.go 2014-03-19 03:21:48 +0000
@@ -184,6 +184,34 @@
184 return results, fmt.Errorf("unknown unit or machine %q", p.Target)184 return results, fmt.Errorf("unknown unit or machine %q", p.Target)
185}185}
186186
187// PrivateAddress implements the server side of Client.PrivateAddress.
188func (c *Client) PrivateAddress(p params.PrivateAddress) (results params.PrivateAddressResults, err error) {
189 switch {
190 case names.IsMachine(p.Target):
191 machine, err := c.api.state.Machine(p.Target)
192 if err != nil {
193 return results, err
194 }
195 addr := instance.SelectInternalAddress(machine.Addresses(), false)
196 if addr == "" {
197 return results, fmt.Errorf("machine %q has no internal address", machine)
198 }
199 return params.PrivateAddressResults{PrivateAddress: addr}, nil
200
201 case names.IsUnit(p.Target):
202 unit, err := c.api.state.Unit(p.Target)
203 if err != nil {
204 return results, err
205 }
206 addr, ok := unit.PrivateAddress()
207 if !ok {
208 return results, fmt.Errorf("unit %q has no internal address", unit)
209 }
210 return params.PrivateAddressResults{PrivateAddress: addr}, nil
211 }
212 return results, fmt.Errorf("unknown unit or machine %q", p.Target)
213}
214
187// ServiceExpose changes the juju-managed firewall to expose any ports that215// ServiceExpose changes the juju-managed firewall to expose any ports that
188// were also explicitly marked by units as open.216// were also explicitly marked by units as open.
189func (c *Client) ServiceExpose(args params.ServiceExpose) error {217func (c *Client) ServiceExpose(args params.ServiceExpose) error {
190218
=== modified file 'state/apiserver/client/client_test.go'
--- state/apiserver/client/client_test.go 2014-03-18 02:36:58 +0000
+++ state/apiserver/client/client_test.go 2014-03-19 03:21:48 +0000
@@ -1413,6 +1413,66 @@
1413 c.Assert(addr, gc.Equals, "127.0.0.1")1413 c.Assert(addr, gc.Equals, "127.0.0.1")
1414}1414}
14151415
1416func (s *clientSuite) TestClientPrivateAddressErrors(c *gc.C) {
1417 s.setUpScenario(c)
1418 _, err := s.APIState.Client().PrivateAddress("wordpress")
1419 c.Assert(err, gc.ErrorMatches, `unknown unit or machine "wordpress"`)
1420 _, err = s.APIState.Client().PrivateAddress("0")
1421 c.Assert(err, gc.ErrorMatches, `machine "0" has no internal address`)
1422 _, err = s.APIState.Client().PrivateAddress("wordpress/0")
1423 c.Assert(err, gc.ErrorMatches, `unit "wordpress/0" has no internal address`)
1424}
1425
1426func (s *clientSuite) TestClientPrivateAddressMachine(c *gc.C) {
1427 s.setUpScenario(c)
1428
1429 // Internally, instance.SelectInternalAddress is used; the public
1430 // address if no cloud-local one is available.
1431 m1, err := s.State.Machine("1")
1432 c.Assert(err, gc.IsNil)
1433 cloudLocalAddress := instance.NewAddress("cloudlocal")
1434 cloudLocalAddress.NetworkScope = instance.NetworkCloudLocal
1435 publicAddress := instance.NewAddress("public")
1436 publicAddress.NetworkScope = instance.NetworkCloudLocal
1437 err = m1.SetAddresses([]instance.Address{publicAddress})
1438 c.Assert(err, gc.IsNil)
1439 addr, err := s.APIState.Client().PrivateAddress("1")
1440 c.Assert(err, gc.IsNil)
1441 c.Assert(addr, gc.Equals, "public")
1442 err = m1.SetAddresses([]instance.Address{cloudLocalAddress, publicAddress})
1443 addr, err = s.APIState.Client().PrivateAddress("1")
1444 c.Assert(err, gc.IsNil)
1445 c.Assert(addr, gc.Equals, "cloudlocal")
1446}
1447
1448func (s *clientSuite) TestClientPrivateAddressUnitWithMachine(c *gc.C) {
1449 s.setUpScenario(c)
1450
1451 // Private address of unit is taken from its machine
1452 // (if its machine has addresses).
1453 m1, err := s.State.Machine("1")
1454 publicAddress := instance.NewAddress("public")
1455 publicAddress.NetworkScope = instance.NetworkCloudLocal
1456 err = m1.SetAddresses([]instance.Address{publicAddress})
1457 c.Assert(err, gc.IsNil)
1458 addr, err := s.APIState.Client().PrivateAddress("wordpress/0")
1459 c.Assert(err, gc.IsNil)
1460 c.Assert(addr, gc.Equals, "public")
1461}
1462
1463func (s *clientSuite) TestClientPrivateAddressUnitWithoutMachine(c *gc.C) {
1464 s.setUpScenario(c)
1465 // If the unit's machine has no addresses, the public address
1466 // comes from the unit's document.
1467 u, err := s.State.Unit("wordpress/1")
1468 c.Assert(err, gc.IsNil)
1469 err = u.SetPrivateAddress("127.0.0.1")
1470 c.Assert(err, gc.IsNil)
1471 addr, err := s.APIState.Client().PrivateAddress("wordpress/1")
1472 c.Assert(err, gc.IsNil)
1473 c.Assert(addr, gc.Equals, "127.0.0.1")
1474}
1475
1416func (s *clientSuite) TestClientEnvironmentGet(c *gc.C) {1476func (s *clientSuite) TestClientEnvironmentGet(c *gc.C) {
1417 envConfig, err := s.State.EnvironConfig()1477 envConfig, err := s.State.EnvironConfig()
1418 c.Assert(err, gc.IsNil)1478 c.Assert(err, gc.IsNil)
14191479
=== modified file 'utils/ssh/ssh.go'
--- utils/ssh/ssh.go 2014-03-10 20:22:44 +0000
+++ utils/ssh/ssh.go 2014-03-19 03:21:48 +0000
@@ -19,6 +19,9 @@
1919
20// Options is a client-implementation independent SSH options set.20// Options is a client-implementation independent SSH options set.
21type Options struct {21type Options struct {
22 // proxyCommand specifies the command to
23 // execute to proxy SSH traffic through.
24 proxyCommand []string
22 // ssh server port; zero means use the default (22)25 // ssh server port; zero means use the default (22)
23 port int26 port int
24 // no PTY forced by default27 // no PTY forced by default
@@ -31,6 +34,11 @@
31 identities []string34 identities []string
32}35}
3336
37// SetProxyCommand sets a command to execute to proxy traffic through.
38func (o *Options) SetProxyCommand(command ...string) {
39 o.proxyCommand = append([]string{}, command...)
40}
41
34// SetPort sets the SSH server port to connect to.42// SetPort sets the SSH server port to connect to.
35func (o *Options) SetPort(port int) {43func (o *Options) SetPort(port int) {
36 o.port = port44 o.port = port
3745
=== modified file 'utils/ssh/ssh_openssh.go'
--- utils/ssh/ssh_openssh.go 2014-02-22 13:45:10 +0000
+++ utils/ssh/ssh_openssh.go 2014-03-19 03:21:48 +0000
@@ -71,6 +71,9 @@
71 if options == nil {71 if options == nil {
72 options = &Options{}72 options = &Options{}
73 }73 }
74 if len(options.proxyCommand) > 0 {
75 args["-o"] = append(args["-o"], "ProxyCommand "+utils.CommandString(options.proxyCommand...))
76 }
74 if !options.passwordAuthAllowed {77 if !options.passwordAuthAllowed {
75 args["-o"] = append(args["-o"], "PasswordAuthentication no")78 args["-o"] = append(args["-o"], "PasswordAuthentication no")
76 }79 }

Subscribers

People subscribed via source and target branches

to status/vote changes: