Merge lp:~axwalk/juju-core/lp1246983-cli-api-ssh 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: 2040
Proposed branch: lp:~axwalk/juju-core/lp1246983-cli-api-ssh
Merge into: lp:~go-bot/juju-core/trunk
Diff against target: 615 lines (+289/-89)
10 files modified
cmd/juju/debughooks.go (+21/-23)
cmd/juju/debughooks_test.go (+2/-2)
cmd/juju/scp.go (+2/-3)
cmd/juju/scp_test.go (+1/-1)
cmd/juju/ssh.go (+46/-56)
cmd/juju/ssh_test.go (+60/-4)
state/api/client.go (+17/-0)
state/api/params/params.go (+20/-0)
state/apiserver/client/client.go (+48/-0)
state/apiserver/client/client_test.go (+72/-0)
To merge this branch: bzr merge lp:~axwalk/juju-core/lp1246983-cli-api-ssh
Reviewer Review Type Date Requested Status
Juju Engineering Pending
Review via email: mp+194438@code.launchpad.net

Commit message

Migrate ssh-based CLI commands to API

Two new client API methods are introduced:
  - ServiceCharmRelations
  - PublicAddress

ServiceCharmRelations returns the names of
all charm relations for the given service.
This method is used to validate hook names
for debug-hooks.

PublicAddress takes a string, which must be
either a machine ID or unit ID. The call will
return the machine or unit's public address,
or an error if the entity does not have one.

The ssh-based commands (ssh, scp, debug-log
and debug-hooks) now use these methods instead
of going directly to state. The one major
change is that, previously, they used the
instance.Instance.WaitDNSName method to get
a host address. We now use PublicAddress,
which is populated by the machine-agent's
address updater.

Note that debug-log does not use any new
log-specific API; that is a much larger task,
and out of scope here.

https://codereview.appspot.com/23380045/

Description of the change

Migrate ssh-based CLI commands to API

Two new client API methods are introduced:
  - ServiceCharmRelations
  - PublicAddress

ServiceCharmRelations returns the names of
all charm relations for the given service.
This method is used to validate hook names
for debug-hooks.

PublicAddress takes a string, which must be
either a machine ID or unit ID. The call will
return the machine or unit's public address,
or an error if the entity does not have one.

The ssh-based commands (ssh, scp, debug-log
and debug-hooks) now use these methods instead
of going directly to state. The one major
change is that, previously, they used the
instance.Instance.WaitDNSName method to get
a host address. We now use PublicAddress,
which is populated by the machine-agent's
address updater.

Note that debug-log does not use any new
log-specific API; that is a much larger task,
and out of scope here.

https://codereview.appspot.com/23380045/

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

Reviewers: mp+194438_code.launchpad.net,

Message:
Please take a look.

Description:
Migrate ssh-based CLI commands to API

Two new client API methods are introduced:
   - ServiceCharmRelations
   - PublicAddress

ServiceCharmRelations returns the names of
all charm relations for the given service.
This method is used to validate hook names
for debug-hooks.

PublicAddress takes a string, which must be
either a machine ID or unit ID. The call will
return the machine or unit's public address,
or an error if the entity does not have one.

The ssh-based commands (ssh, scp, debug-log
and debug-hooks) now use these methods instead
of going directly to state. The one major
change is that, previously, they used the
instance.Instance.WaitDNSName method to get
a host address. We now use PublicAddress,
which is populated by the machine-agent's
address updater.

Note that debug-log does not use any new
log-specific API; that is a much larger task,
and out of scope here.

https://code.launchpad.net/~axwalk/juju-core/lp1246983-cli-api-ssh/+merge/194438

(do not edit description out of merge proposal)

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

Affected files (+205, -83 lines):
   A [revision details]
   M cmd/juju/debughooks.go
   M cmd/juju/debughooks_test.go
   M cmd/juju/scp.go
   M cmd/juju/ssh.go
   M cmd/juju/ssh_test.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

Revision history for this message
Ian Booth (wallyworld) wrote :

LGTM with the test issues fixed.
I'd also like to see a test whereby the public address is not
immediately populated and is updated after a short delay to test that
the retry works.

https://codereview.appspot.com/23380045/diff/1/cmd/juju/debughooks.go
File cmd/juju/debughooks.go (right):

https://codereview.appspot.com/23380045/diff/1/cmd/juju/debughooks.go#newcode61
cmd/juju/debughooks.go:61: results, err :=
c.apiClient.ServiceCharmRelations(service)
small nitpick - can we rename results to relations?

https://codereview.appspot.com/23380045/diff/1/state/apiserver/client/client_test.go
File state/apiserver/client/client_test.go (right):

https://codereview.appspot.com/23380045/diff/1/state/apiserver/client/client_test.go#newcode1244
state/apiserver/client/client_test.go:1244: c.Assert(err,
gc.ErrorMatches, `unit "wordpress/0" has no public address`)
I'd like to see the above error checks in a separate test eg
TestPublicAddressErrors

https://codereview.appspot.com/23380045/diff/1/state/apiserver/client/client_test.go#newcode1264
state/apiserver/client/client_test.go:1264: // Public address of unit is
taken from its machine
I'd like to see this as a separate test

https://codereview.appspot.com/23380045/diff/1/state/apiserver/client/client_test.go#newcode1270
state/apiserver/client/client_test.go:1270: // If the unit's machine has
no addresses, the public address
ditto

https://codereview.appspot.com/23380045/

Revision history for this message
Andrew Wilkins (axwalk) wrote :

Please take a look.

https://codereview.appspot.com/23380045/diff/1/cmd/juju/debughooks.go
File cmd/juju/debughooks.go (right):

https://codereview.appspot.com/23380045/diff/1/cmd/juju/debughooks.go#newcode61
cmd/juju/debughooks.go:61: results, err :=
c.apiClient.ServiceCharmRelations(service)
On 2013/11/08 03:32:09, wallyworld wrote:
> small nitpick - can we rename results to relations?

The result is a struct which contains the relation names, so as it is I
don't think that makes sense. I'll just change the API to return the
slice directly - it's not likely to grow any more fields.

https://codereview.appspot.com/23380045/diff/1/state/apiserver/client/client_test.go
File state/apiserver/client/client_test.go (right):

https://codereview.appspot.com/23380045/diff/1/state/apiserver/client/client_test.go#newcode1244
state/apiserver/client/client_test.go:1244: c.Assert(err,
gc.ErrorMatches, `unit "wordpress/0" has no public address`)
On 2013/11/08 03:32:09, wallyworld wrote:
> I'd like to see the above error checks in a separate test eg
> TestPublicAddressErrors

Done.

https://codereview.appspot.com/23380045/diff/1/state/apiserver/client/client_test.go#newcode1264
state/apiserver/client/client_test.go:1264: // Public address of unit is
taken from its machine
On 2013/11/08 03:32:09, wallyworld wrote:
> I'd like to see this as a separate test

Done.

https://codereview.appspot.com/23380045/diff/1/state/apiserver/client/client_test.go#newcode1270
state/apiserver/client/client_test.go:1270: // If the unit's machine has
no addresses, the public address
On 2013/11/08 03:32:09, wallyworld wrote:
> ditto

Done.

https://codereview.appspot.com/23380045/

Revision history for this message
Ian Booth (wallyworld) wrote :

Great thanks :-)
Still LGTM

https://codereview.appspot.com/23380045/

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'cmd/juju/debughooks.go'
2--- cmd/juju/debughooks.go 2013-08-02 10:05:47 +0000
3+++ cmd/juju/debughooks.go 2013-11-08 05:25:52 +0000
4@@ -10,7 +10,7 @@
5
6 "launchpad.net/juju-core/charm/hooks"
7 "launchpad.net/juju-core/cmd"
8- "launchpad.net/juju-core/state"
9+ "launchpad.net/juju-core/names"
10 unitdebug "launchpad.net/juju-core/worker/uniter/debug"
11 )
12
13@@ -38,6 +38,9 @@
14 return errors.New("no unit name specified")
15 }
16 c.Target = args[0]
17+ if !names.IsUnit(c.Target) {
18+ return fmt.Errorf("%q is not a valid unit name", c.Target)
19+ }
20
21 // If any of the hooks is "*", then debug all hooks.
22 c.hooks = append([]string{}, args[1:]...)
23@@ -50,31 +53,29 @@
24 return nil
25 }
26
27-func (c *DebugHooksCommand) validateHooks(unit *state.Unit) error {
28+func (c *DebugHooksCommand) validateHooks() error {
29 if len(c.hooks) == 0 {
30 return nil
31 }
32- service, err := unit.Service()
33- if err != nil {
34- return err
35- }
36- eps, err := service.Endpoints()
37- if err != nil {
38- return err
39- }
40+ service := names.UnitService(c.Target)
41+ relations, err := c.apiClient.ServiceCharmRelations(service)
42+ if err != nil {
43+ return err
44+ }
45+
46 validHooks := make(map[string]bool)
47 for _, hook := range hooks.UnitHooks() {
48 validHooks[string(hook)] = true
49 }
50- for _, ep := range eps {
51+ for _, relation := range relations {
52 for _, hook := range hooks.RelationHooks() {
53- hook := fmt.Sprintf("%s-%s", ep.Relation.Name, hook)
54+ hook := fmt.Sprintf("%s-%s", relation, hook)
55 validHooks[hook] = true
56 }
57 }
58 for _, hook := range c.hooks {
59 if !validHooks[hook] {
60- return fmt.Errorf("unit %q does not contain hook %q", unit.Name(), hook)
61+ return fmt.Errorf("unit %q does not contain hook %q", c.Target, hook)
62 }
63 }
64 return nil
65@@ -84,16 +85,13 @@
66 // and connects to it via SSH to execute the debug-hooks
67 // script.
68 func (c *DebugHooksCommand) Run(ctx *cmd.Context) error {
69- conn, err := c.initConn()
70- if err != nil {
71- return err
72- }
73- defer conn.Close()
74- unit, err := conn.State.Unit(c.Target)
75- if err != nil {
76- return err
77- }
78- err = c.validateHooks(unit)
79+ var err error
80+ c.apiClient, err = c.initAPIClient()
81+ if err != nil {
82+ return err
83+ }
84+ defer c.apiClient.Close()
85+ err = c.validateHooks()
86 if err != nil {
87 return err
88 }
89
90=== modified file 'cmd/juju/debughooks_test.go'
91--- cmd/juju/debughooks_test.go 2013-08-19 11:17:19 +0000
92+++ cmd/juju/debughooks_test.go 2013-11-08 05:25:52 +0000
93@@ -52,7 +52,7 @@
94 }, {
95 info: `invalid unit syntax`,
96 args: []string{"mysql"},
97- code: 1,
98+ code: 2,
99 stderr: `error: "mysql" is not a valid unit name` + "\n",
100 }, {
101 info: `invalid unit`,
102@@ -67,7 +67,7 @@
103 }}
104
105 func (s *DebugHooksSuite) TestDebugHooksCommand(c *gc.C) {
106- machines := s.makeMachines(3, c)
107+ machines := s.makeMachines(3, c, true)
108 dummy := s.AddTestingCharm(c, "dummy")
109 srv, err := s.State.AddService("mysql", dummy)
110 c.Assert(err, gc.IsNil)
111
112=== modified file 'cmd/juju/scp.go'
113--- cmd/juju/scp.go 2013-08-13 19:07:35 +0000
114+++ cmd/juju/scp.go 2013-11-08 05:25:52 +0000
115@@ -38,11 +38,11 @@
116 // forks ssh with c.Args, if provided.
117 func (c *SCPCommand) Run(ctx *cmd.Context) error {
118 var err error
119- c.Conn, err = c.initConn()
120+ c.apiClient, err = c.initAPIClient()
121 if err != nil {
122 return err
123 }
124- defer c.Close()
125+ defer c.apiClient.Close()
126
127 // translate arguments in the form 0:/somepath or service/0:/somepath into
128 // ubuntu@machine:/somepath so they can be presented to scp.
129@@ -63,6 +63,5 @@
130 cmd.Stdin = ctx.Stdin
131 cmd.Stdout = ctx.Stdout
132 cmd.Stderr = ctx.Stderr
133- c.Close()
134 return cmd.Run()
135 }
136
137=== modified file 'cmd/juju/scp_test.go'
138--- cmd/juju/scp_test.go 2013-09-27 07:05:45 +0000
139+++ cmd/juju/scp_test.go 2013-11-08 05:25:52 +0000
140@@ -48,7 +48,7 @@
141 }
142
143 func (s *SCPSuite) TestSCPCommand(c *gc.C) {
144- m := s.makeMachines(3, c)
145+ m := s.makeMachines(3, c, true)
146 ch := coretesting.Charms.Dir("dummy")
147 curl := charm.MustParseURL(
148 fmt.Sprintf("local:quantal/%s-%d", ch.Meta().Name, ch.Revision()),
149
150=== modified file 'cmd/juju/ssh.go'
151--- cmd/juju/ssh.go 2013-10-03 16:51:49 +0000
152+++ cmd/juju/ssh.go 2013-11-08 05:25:52 +0000
153@@ -5,15 +5,13 @@
154
155 import (
156 "errors"
157- "fmt"
158 "os/exec"
159+ "time"
160
161 "launchpad.net/juju-core/cmd"
162- "launchpad.net/juju-core/instance"
163 "launchpad.net/juju-core/juju"
164- "launchpad.net/juju-core/log"
165- "launchpad.net/juju-core/names"
166- "launchpad.net/juju-core/state"
167+ "launchpad.net/juju-core/state/api"
168+ "launchpad.net/juju-core/utils"
169 )
170
171 // SSHCommand is responsible for launching a ssh shell on a given unit or machine.
172@@ -24,9 +22,9 @@
173 // SSHCommon provides common methods for SSHCommand, SCPCommand and DebugHooksCommand.
174 type SSHCommon struct {
175 cmd.EnvCommandBase
176- Target string
177- Args []string
178- *juju.Conn
179+ Target string
180+ Args []string
181+ apiClient *api.Client
182 }
183
184 const sshDoc = `
185@@ -55,13 +53,13 @@
186 // Run resolves c.Target to a machine, to the address of a i
187 // machine or unit forks ssh passing any arguments provided.
188 func (c *SSHCommand) Run(ctx *cmd.Context) error {
189- if c.Conn == nil {
190+ if c.apiClient == nil {
191 var err error
192- c.Conn, err = c.initConn()
193+ c.apiClient, err = c.initAPIClient()
194 if err != nil {
195 return err
196 }
197- defer c.Close()
198+ defer c.apiClient.Close()
199 }
200 host, err := c.hostFromTarget(c.Target)
201 if err != nil {
202@@ -73,62 +71,54 @@
203 cmd.Stdin = ctx.Stdin
204 cmd.Stdout = ctx.Stdout
205 cmd.Stderr = ctx.Stderr
206- c.Close()
207 return cmd.Run()
208 }
209
210-// initConn initialises the state connection.
211+// initAPIClient initialises the state connection.
212 // It is the caller's responsibility to close the connection.
213-func (c *SSHCommon) initConn() (*juju.Conn, error) {
214+func (c *SSHCommon) initAPIClient() (*api.Client, error) {
215 var err error
216- c.Conn, err = juju.NewConnFromName(c.EnvName)
217- return c.Conn, err
218+ c.apiClient, err = juju.NewAPIClientFromName(c.EnvName)
219+ return c.apiClient, err
220+}
221+
222+// attemptStarter is an interface corresponding to utils.AttemptStrategy
223+type attemptStarter interface {
224+ Start() attempt
225+}
226+
227+type attempt interface {
228+ Next() bool
229+}
230+
231+type attemptStrategy utils.AttemptStrategy
232+
233+func (s attemptStrategy) Start() attempt {
234+ return utils.AttemptStrategy(s).Start()
235+}
236+
237+var sshHostFromTargetAttemptStrategy attemptStarter = attemptStrategy{
238+ Total: 5 * time.Second,
239+ Delay: 500 * time.Millisecond,
240 }
241
242 func (c *SSHCommon) hostFromTarget(target string) (string, error) {
243- // is the target the id of a machine ?
244- if names.IsMachine(target) {
245- log.Infof("looking up address for machine %s...", target)
246- // TODO(dfc) maybe we should have machine.PublicAddress() ?
247- return c.machinePublicAddress(target)
248- }
249- // maybe the target is a unit ?
250- if names.IsUnit(target) {
251- log.Infof("looking up address for unit %q...", c.Target)
252- unit, err := c.State.Unit(target)
253- if err != nil {
254- return "", err
255- }
256- addr, ok := unit.PublicAddress()
257- if !ok {
258- return "", fmt.Errorf("unit %q has no public address", unit)
259- }
260- return addr, nil
261- }
262- return "", fmt.Errorf("unknown unit or machine %q", target)
263-}
264-
265-func (c *SSHCommon) machinePublicAddress(id string) (string, error) {
266- machine, err := c.State.Machine(id)
267+ var addr string
268+ var err error
269+ // A target may not initially have an address (e.g. the
270+ // address updater hasn't yet run), so we must do this in
271+ // a loop.
272+ for a := sshHostFromTargetAttemptStrategy.Start(); a.Next(); {
273+ addr, err = c.apiClient.PublicAddress(target)
274+ if err == nil {
275+ break
276+ }
277+ }
278 if err != nil {
279 return "", err
280 }
281- // wait for instance id
282- w := machine.Watch()
283- for _ = range w.Changes() {
284- if instid, err := machine.InstanceId(); err == nil {
285- w.Stop()
286- inst, err := c.Environ.Instances([]instance.Id{instid})
287- if err != nil {
288- return "", err
289- }
290- return inst[0].WaitDNSName()
291- } else if !state.IsNotProvisionedError(err) {
292- return "", err
293- }
294- }
295- // oops, watcher closed before we could get an answer
296- return "", w.Stop()
297+ logger.Infof("Resolved public address of %q: %q", target, addr)
298+ return addr, nil
299 }
300
301 // AllowInterspersedFlags for ssh/scp is set to false so that
302
303=== modified file 'cmd/juju/ssh_test.go'
304--- cmd/juju/ssh_test.go 2013-09-30 19:40:06 +0000
305+++ cmd/juju/ssh_test.go 2013-11-08 05:25:52 +0000
306@@ -97,7 +97,7 @@
307 }
308
309 func (s *SSHSuite) TestSSHCommand(c *gc.C) {
310- m := s.makeMachines(3, c)
311+ m := s.makeMachines(3, c, true)
312 ch := coretesting.Charms.Dir("dummy")
313 curl := charm.MustParseURL(
314 fmt.Sprintf("local:quantal/%s-%d", ch.Meta().Name, ch.Revision()),
315@@ -128,13 +128,69 @@
316 }
317 }
318
319-func (s *SSHCommonSuite) makeMachines(n int, c *gc.C) []*state.Machine {
320+type callbackAttemptStarter struct {
321+ next func() bool
322+}
323+
324+func (s *callbackAttemptStarter) Start() attempt {
325+ return callbackAttempt{next: s.next}
326+}
327+
328+type callbackAttempt struct {
329+ next func() bool
330+}
331+
332+func (a callbackAttempt) Next() bool {
333+ return a.next()
334+}
335+
336+func (s *SSHSuite) TestSSHCommandHostAddressRetry(c *gc.C) {
337+ m := s.makeMachines(1, c, false)
338+ ctx := coretesting.Context(c)
339+ jujucmd := cmd.NewSuperCommand(cmd.SuperCommandParams{})
340+ jujucmd.Register(&SSHCommand{})
341+
342+ var called int
343+ next := func() bool {
344+ called++
345+ return called < 2
346+ }
347+ attemptStarter := &callbackAttemptStarter{next: next}
348+ s.PatchValue(&sshHostFromTargetAttemptStrategy, attemptStarter)
349+
350+ // Ensure that the ssh command waits for a public address, or the attempt
351+ // strategy's Done method returns false.
352+ code := cmd.Main(jujucmd, ctx, []string{"ssh", "0"})
353+ c.Check(code, gc.Equals, 1)
354+ c.Assert(called, gc.Equals, 2)
355+ called = 0
356+ attemptStarter.next = func() bool {
357+ called++
358+ s.setAddress(m[0], c)
359+ return false
360+ }
361+ code = cmd.Main(jujucmd, ctx, []string{"ssh", "0"})
362+ c.Check(code, gc.Equals, 0)
363+ c.Assert(called, gc.Equals, 1)
364+}
365+
366+func (s *SSHCommonSuite) setAddress(m *state.Machine, c *gc.C) {
367+ addr := instance.NewAddress(fmt.Sprintf("dummyenv-%s.dns", m.Id()))
368+ addr.NetworkScope = instance.NetworkPublic
369+ err := m.SetAddresses([]instance.Address{addr})
370+ c.Assert(err, gc.IsNil)
371+}
372+
373+func (s *SSHCommonSuite) makeMachines(n int, c *gc.C, setAddress bool) []*state.Machine {
374 var machines = make([]*state.Machine, n)
375 for i := 0; i < n; i++ {
376 m, err := s.State.AddMachine("quantal", state.JobHostUnits)
377 c.Assert(err, gc.IsNil)
378- // must set an instance id as the ssh command uses that as a signal the machine
379- // has been provisioned
380+ if setAddress {
381+ s.setAddress(m, c)
382+ }
383+ // must set an instance id as the ssh command uses that as a signal the
384+ // machine has been provisioned
385 inst, md := testing.AssertStartInstance(c, s.Conn.Environ, m.Id())
386 c.Assert(m.SetProvisioned(inst.Id(), "fake_nonce", md), gc.IsNil)
387 machines[i] = m
388
389=== modified file 'state/api/client.go'
390--- state/api/client.go 2013-11-07 00:26:44 +0000
391+++ state/api/client.go 2013-11-08 05:25:52 +0000
392@@ -63,6 +63,15 @@
393 return c.st.Call("Client", "", "Resolved", p, nil)
394 }
395
396+// PublicAddress returns the public address of the specified
397+// machine or unit.
398+func (c *Client) PublicAddress(target string) (string, error) {
399+ var results params.PublicAddressResults
400+ p := params.PublicAddress{Target: target}
401+ err := c.st.Call("Client", "", "PublicAddress", p, &results)
402+ return results.PublicAddress, err
403+}
404+
405 // ServiceSetYAML sets configuration options on a service
406 // given options in YAML format.
407 func (c *Client) ServiceSetYAML(service string, yaml string) error {
408@@ -95,6 +104,14 @@
409 return c.st.Call("Client", "", "DestroyRelation", params, nil)
410 }
411
412+// ServiceCharmRelations returns the service's charms relation names.
413+func (c *Client) ServiceCharmRelations(service string) ([]string, error) {
414+ var results params.ServiceCharmRelationsResults
415+ params := params.ServiceCharmRelations{ServiceName: service}
416+ err := c.st.Call("Client", "", "ServiceCharmRelations", params, &results)
417+ return results.CharmRelations, err
418+}
419+
420 // AddMachines adds new machines with the supplied parameters.
421 func (c *Client) AddMachines(machineParams []params.AddMachineParams) ([]params.AddMachinesResult, error) {
422 args := params.AddMachines{
423
424=== modified file 'state/api/params/params.go'
425--- state/api/params/params.go 2013-11-06 22:38:06 +0000
426+++ state/api/params/params.go 2013-11-08 05:25:52 +0000
427@@ -160,11 +160,31 @@
428 Constraints constraints.Value
429 }
430
431+// ServiceCharmRelations holds parameters for making the ServiceCharmRelations call.
432+type ServiceCharmRelations struct {
433+ ServiceName string
434+}
435+
436+// ServiceCharmRelationsResults holds the results of the ServiceCharmRelations call.
437+type ServiceCharmRelationsResults struct {
438+ CharmRelations []string
439+}
440+
441 // ServiceUnexpose holds parameters for the ServiceUnexpose call.
442 type ServiceUnexpose struct {
443 ServiceName string
444 }
445
446+// PublicAddress holds parameters for the PublicAddress call.
447+type PublicAddress struct {
448+ Target string
449+}
450+
451+// PublicAddressResults holds results of the PublicAddress call.
452+type PublicAddressResults struct {
453+ PublicAddress string
454+}
455+
456 // Resolved holds parameters for the Resolved call.
457 type Resolved struct {
458 UnitName string
459
460=== modified file 'state/apiserver/client/client.go'
461--- state/apiserver/client/client.go 2013-11-08 03:09:36 +0000
462+++ state/apiserver/client/client.go 2013-11-08 05:25:52 +0000
463@@ -9,7 +9,9 @@
464
465 "launchpad.net/juju-core/charm"
466 "launchpad.net/juju-core/environs"
467+ "launchpad.net/juju-core/instance"
468 "launchpad.net/juju-core/juju"
469+ "launchpad.net/juju-core/names"
470 "launchpad.net/juju-core/state"
471 "launchpad.net/juju-core/state/api"
472 "launchpad.net/juju-core/state/api/params"
473@@ -131,6 +133,24 @@
474 return serviceSetSettingsYAML(svc, p.Config)
475 }
476
477+// ServiceCharmRelations implements the server side of Client.ServiceCharmRelations.
478+func (c *Client) ServiceCharmRelations(p params.ServiceCharmRelations) (params.ServiceCharmRelationsResults, error) {
479+ var results params.ServiceCharmRelationsResults
480+ service, err := c.api.state.Service(p.ServiceName)
481+ if err != nil {
482+ return results, err
483+ }
484+ endpoints, err := service.Endpoints()
485+ if err != nil {
486+ return results, err
487+ }
488+ results.CharmRelations = make([]string, len(endpoints))
489+ for i, endpoint := range endpoints {
490+ results.CharmRelations[i] = endpoint.Relation.Name
491+ }
492+ return results, nil
493+}
494+
495 // Resolved implements the server side of Client.Resolved.
496 func (c *Client) Resolved(p params.Resolved) error {
497 unit, err := c.api.state.Unit(p.UnitName)
498@@ -140,6 +160,34 @@
499 return unit.Resolve(p.Retry)
500 }
501
502+// PublicAddress implements the server side of Client.PublicAddress.
503+func (c *Client) PublicAddress(p params.PublicAddress) (results params.PublicAddressResults, err error) {
504+ switch {
505+ case names.IsMachine(p.Target):
506+ machine, err := c.api.state.Machine(p.Target)
507+ if err != nil {
508+ return results, err
509+ }
510+ addr := instance.SelectPublicAddress(machine.Addresses())
511+ if addr == "" {
512+ return results, fmt.Errorf("machine %q has no public address", machine)
513+ }
514+ return params.PublicAddressResults{PublicAddress: addr}, nil
515+
516+ case names.IsUnit(p.Target):
517+ unit, err := c.api.state.Unit(p.Target)
518+ if err != nil {
519+ return results, err
520+ }
521+ addr, ok := unit.PublicAddress()
522+ if !ok {
523+ return results, fmt.Errorf("unit %q has no public address", unit)
524+ }
525+ return params.PublicAddressResults{PublicAddress: addr}, nil
526+ }
527+ return results, fmt.Errorf("unknown unit or machine %q", p.Target)
528+}
529+
530 // ServiceExpose changes the juju-managed firewall to expose any ports that
531 // were also explicitly marked by units as open.
532 func (c *Client) ServiceExpose(args params.ServiceExpose) error {
533
534=== modified file 'state/apiserver/client/client_test.go'
535--- state/apiserver/client/client_test.go 2013-11-08 03:09:36 +0000
536+++ state/apiserver/client/client_test.go 2013-11-08 05:25:52 +0000
537@@ -1222,6 +1222,78 @@
538 c.Assert(obtained, gc.DeepEquals, cons)
539 }
540
541+func (s *clientSuite) TestClientServiceCharmRelations(c *gc.C) {
542+ s.setUpScenario(c)
543+ _, err := s.APIState.Client().ServiceCharmRelations("blah")
544+ c.Assert(err, gc.ErrorMatches, `service "blah" not found`)
545+
546+ relations, err := s.APIState.Client().ServiceCharmRelations("wordpress")
547+ c.Assert(err, gc.IsNil)
548+ c.Assert(relations, gc.DeepEquals, []string{
549+ "cache", "db", "juju-info", "logging-dir", "monitoring-port", "url",
550+ })
551+}
552+
553+func (s *clientSuite) TestClientPublicAddressErrors(c *gc.C) {
554+ s.setUpScenario(c)
555+ _, err := s.APIState.Client().PublicAddress("wordpress")
556+ c.Assert(err, gc.ErrorMatches, `unknown unit or machine "wordpress"`)
557+ _, err = s.APIState.Client().PublicAddress("0")
558+ c.Assert(err, gc.ErrorMatches, `machine "0" has no public address`)
559+ _, err = s.APIState.Client().PublicAddress("wordpress/0")
560+ c.Assert(err, gc.ErrorMatches, `unit "wordpress/0" has no public address`)
561+}
562+
563+func (s *clientSuite) TestClientPublicAddressMachine(c *gc.C) {
564+ s.setUpScenario(c)
565+
566+ // Internally, instance.SelectPublicAddress is used; the "most public"
567+ // address is returned.
568+ m1, err := s.State.Machine("1")
569+ c.Assert(err, gc.IsNil)
570+ cloudLocalAddress := instance.NewAddress("cloudlocal")
571+ cloudLocalAddress.NetworkScope = instance.NetworkCloudLocal
572+ publicAddress := instance.NewAddress("public")
573+ publicAddress.NetworkScope = instance.NetworkPublic
574+ err = m1.SetAddresses([]instance.Address{cloudLocalAddress})
575+ c.Assert(err, gc.IsNil)
576+ addr, err := s.APIState.Client().PublicAddress("1")
577+ c.Assert(err, gc.IsNil)
578+ c.Assert(addr, gc.Equals, "cloudlocal")
579+ err = m1.SetAddresses([]instance.Address{cloudLocalAddress, publicAddress})
580+ addr, err = s.APIState.Client().PublicAddress("1")
581+ c.Assert(err, gc.IsNil)
582+ c.Assert(addr, gc.Equals, "public")
583+}
584+
585+func (s *clientSuite) TestClientPublicAddressUnitWithMachine(c *gc.C) {
586+ s.setUpScenario(c)
587+
588+ // Public address of unit is taken from its machine
589+ // (if its machine has addresses).
590+ m1, err := s.State.Machine("1")
591+ publicAddress := instance.NewAddress("public")
592+ publicAddress.NetworkScope = instance.NetworkPublic
593+ err = m1.SetAddresses([]instance.Address{publicAddress})
594+ c.Assert(err, gc.IsNil)
595+ addr, err := s.APIState.Client().PublicAddress("wordpress/0")
596+ c.Assert(err, gc.IsNil)
597+ c.Assert(addr, gc.Equals, "public")
598+}
599+
600+func (s *clientSuite) TestClientPublicAddressUnitWithoutMachine(c *gc.C) {
601+ s.setUpScenario(c)
602+ // If the unit's machine has no addresses, the public address
603+ // comes from the unit's document.
604+ u, err := s.State.Unit("wordpress/1")
605+ c.Assert(err, gc.IsNil)
606+ err = u.SetPublicAddress("127.0.0.1")
607+ c.Assert(err, gc.IsNil)
608+ addr, err := s.APIState.Client().PublicAddress("wordpress/1")
609+ c.Assert(err, gc.IsNil)
610+ c.Assert(addr, gc.Equals, "127.0.0.1")
611+}
612+
613 func (s *clientSuite) TestClientEnvironmentGet(c *gc.C) {
614 envConfig, err := s.State.EnvironConfig()
615 c.Assert(err, gc.IsNil)

Subscribers

People subscribed via source and target branches

to status/vote changes: