Merge lp:~axwalk/juju-core/client-cache-apihostports 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: 2517
Proposed branch: lp:~axwalk/juju-core/client-cache-apihostports
Merge into: lp:~go-bot/juju-core/trunk
Prerequisite: lp:~axwalk/juju-core/apihostports-api
Diff against target: 876 lines (+350/-109)
9 files modified
juju/api.go (+92/-42)
juju/apiconn_test.go (+58/-54)
juju/export_test.go (+16/-3)
juju/mock_test.go (+28/-0)
state/api/apiclient.go (+25/-2)
state/api/params/params.go (+5/-0)
state/api/state.go (+42/-1)
state/apiserver/admin.go (+13/-6)
state/apiserver/login_test.go (+71/-1)
To merge this branch: bzr merge lp:~axwalk/juju-core/client-cache-apihostports
Reviewer Review Type Date Requested Status
Juju Engineering Pending
Review via email: mp+213217@code.launchpad.net

Commit message

Cache API server addresses in .jenv

We now return API server addresses from
the Login API, and cache those addresses
in the client's configstore. If the client
connects using environments.yaml, then it
is expected to get updated addresses from
provider-state.

https://codereview.appspot.com/81780043/

Description of the change

Cache API server addresses in .jenv

We now return API server addresses from
the Login API, and cache those addresses
in the client's configstore. If the client
connects using environments.yaml, then it
is expected to get updated addresses from
provider-state.

https://codereview.appspot.com/81780043/

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

Reviewers: mp+213217_code.launchpad.net,

Message:
Please take a look.

Description:
Cache API server addresses in .jenv

We now return API server addresses from
the Login API, and cache those addresses
in the client's configstore. If the client
connects using environments.yaml, then it
is expected to get updated addresses from
provider-state.

https://code.launchpad.net/~axwalk/juju-core/client-cache-apihostports/+merge/213217

Requires:
https://code.launchpad.net/~axwalk/juju-core/apihostports-api/+merge/213200

(do not edit description out of merge proposal)

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

Affected files (+173, -11 lines):
   A [revision details]
   M juju/api.go
   M juju/apiconn_test.go
   M state/api/apiclient.go
   M state/api/params/params.go
   M state/api/state.go
   M state/apiserver/admin.go
   M state/apiserver/login_test.go

Revision history for this message
Roger Peppe (rogpeppe) wrote :

Great direction, thanks, with a few suggestions below.

https://codereview.appspot.com/81780043/diff/1/juju/api.go
File juju/api.go (right):

https://codereview.appspot.com/81780043/diff/1/juju/api.go#newcode252
juju/api.go:252: logger.Warningf(err.Error())
This isn't right (the error might contain % characters).

logger.Warningf("Cannot cache cache API addresses: %v", err)

(and change the error returned from cacheChangedAPIAddresses
so that the message won't be repetitious)

https://codereview.appspot.com/81780043/diff/1/juju/api.go#newcode340
juju/api.go:340: endpoint.Addresses = addrs
I'd like to add a HostPorts entry to the APIEndpoint struct
(we could perhaps deprecate the Addresses field)
and store all the APIHostPorts info there.

So all the code here would deal only with instance.HostPort,
not address strings. This leaves us free to have clients
that are more intelligent about dialling in the future.

https://codereview.appspot.com/81780043/diff/1/juju/api.go#newcode356
juju/api.go:356: sort.Strings(a)
I don't think I'd sort the addresses - the order can be significant.

https://codereview.appspot.com/81780043/diff/1/state/api/apiclient.go
File state/api/apiclient.go (right):

https://codereview.appspot.com/81780043/diff/1/state/api/apiclient.go#newcode35
state/api/apiclient.go:35: addrs []string
I think it would probably be better to store the hostports exactly as
returned by APIHostPorts here, leaving it up to client code to decide
how best to use the information.

https://codereview.appspot.com/81780043/diff/1/state/api/apiclient.go#newcode206
state/api/apiclient.go:206: func (s *State) Addrs() []string {
I'd rename this HostPorts and return all
the APIHostPorts results, with the connecting
address added as an extra element if it's
not found in the result.

Also, I wonder if this logic wouldn't live more naturally
inside the Login method.

https://codereview.appspot.com/81780043/diff/1/state/api/params/params.go
File state/api/params/params.go (right):

https://codereview.appspot.com/81780043/diff/1/state/api/params/params.go#newcode645
state/api/params/params.go:645: APIHostPortsResult
I think I'd just include the field explicitly here
rather than embedding.

https://codereview.appspot.com/81780043/diff/1/state/api/state.go
File state/api/state.go (right):

https://codereview.appspot.com/81780043/diff/1/state/api/state.go#newcode35
state/api/state.go:35: // The client must attempt *all* addresses, or
else
This comment seems like it might be more helpful to put as part of the
Addrs (or HostPorts) doc comment.

https://codereview.appspot.com/81780043/diff/1/state/apiserver/admin.go
File state/apiserver/admin.go (right):

https://codereview.appspot.com/81780043/diff/1/state/apiserver/admin.go#newcode88
state/apiserver/admin.go:88: // state; failure here is non-fatal.
I think it's perfectly reasonable to make the error fatal here.
If we can't access the APIHostPorts document, something is seriously
wrong.

https://codereview.appspot.com/81780043/

Revision history for this message
Andrew Wilkins (axwalk) wrote :
Download full text (3.7 KiB)

Please take a look.

https://codereview.appspot.com/81780043/diff/1/juju/api.go
File juju/api.go (right):

https://codereview.appspot.com/81780043/diff/1/juju/api.go#newcode252
juju/api.go:252: logger.Warningf(err.Error())
On 2014/03/28 10:02:12, rog wrote:
> This isn't right (the error might contain % characters).

> logger.Warningf("Cannot cache cache API addresses: %v", err)

> (and change the error returned from cacheChangedAPIAddresses
> so that the message won't be repetitious)

I copied that from newAPIFromStore and clearly I didn't look too close.
Fixed both.

https://codereview.appspot.com/81780043/diff/1/juju/api.go#newcode340
juju/api.go:340: endpoint.Addresses = addrs
On 2014/03/28 10:02:12, rog wrote:
> I'd like to add a HostPorts entry to the APIEndpoint struct
> (we could perhaps deprecate the Addresses field)
> and store all the APIHostPorts info there.

> So all the code here would deal only with instance.HostPort,
> not address strings. This leaves us free to have clients
> that are more intelligent about dialling in the future.

+1

https://codereview.appspot.com/81780043/diff/1/juju/api.go#newcode356
juju/api.go:356: sort.Strings(a)
On 2014/03/28 10:02:12, rog wrote:
> I don't think I'd sort the addresses - the order can be significant.

Done.

https://codereview.appspot.com/81780043/diff/1/state/api/apiclient.go
File state/api/apiclient.go (right):

https://codereview.appspot.com/81780043/diff/1/state/api/apiclient.go#newcode35
state/api/apiclient.go:35: addrs []string
On 2014/03/28 10:02:12, rog wrote:
> I think it would probably be better to store the hostports exactly as
returned
> by APIHostPorts here, leaving it up to client code to decide how best
to use the
> information.

Done.

https://codereview.appspot.com/81780043/diff/1/state/api/apiclient.go#newcode206
state/api/apiclient.go:206: func (s *State) Addrs() []string {
On 2014/03/28 10:02:12, rog wrote:
> I'd rename this HostPorts and return all
> the APIHostPorts results, with the connecting
> address added as an extra element if it's
> not found in the result.

Done. If the connecting address isn't found, it's added with
NetworkUnknown scope.

> Also, I wonder if this logic wouldn't live more naturally
> inside the Login method.

Yeah, it does, because there's a theoretical error when converting
s.addr->HostPort. Moved.

https://codereview.appspot.com/81780043/diff/1/state/api/params/params.go
File state/api/params/params.go (right):

https://codereview.appspot.com/81780043/diff/1/state/api/params/params.go#newcode645
state/api/params/params.go:645: APIHostPortsResult
On 2014/03/28 10:02:12, rog wrote:
> I think I'd just include the field explicitly here
> rather than embedding.

Done.

https://codereview.appspot.com/81780043/diff/1/state/api/state.go
File state/api/state.go (right):

https://codereview.appspot.com/81780043/diff/1/state/api/state.go#newcode35
state/api/state.go:35: // The client must attempt *all* addresses, or
else
On 2014/03/28 10:02:12, rog wrote:
> This comment seems like it might be more helpful to put as part of the
Addrs (or
> HostPorts) doc comment.

Done.

https://codereview.appspot.com/81780043/diff/1/state/apiserver/admin.go
File st...

Read more...

Revision history for this message
Roger Peppe (rogpeppe) wrote :

LGTM with the juju tests fixed so they don't rely on calling methods on
the zero value, and a few other suggestions below.

https://codereview.appspot.com/81780043/diff/20001/juju/apiconn_test.go
File juju/apiconn_test.go (right):

https://codereview.appspot.com/81780043/diff/20001/juju/apiconn_test.go#newcode160
juju/apiconn_test.go:160: // be updated (to an empty slice).
As discussed online, I'm not sure that this is the correct approach - we
shouldn't rely on calling methods on types that we've created with New.

https://codereview.appspot.com/81780043/diff/20001/state/api/apiclient.go
File state/api/apiclient.go (right):

https://codereview.appspot.com/81780043/diff/20001/state/api/apiclient.go#newcode212
state/api/apiclient.go:212: func (s *State) HostPorts()
[][]instance.HostPort {
On further reflection, I think this method should be named APIHostPorts,
mirroring the same function in State, and making it hopefully clearer
what the returned addresses are about.

https://codereview.appspot.com/81780043/diff/20001/state/api/apiclient.go#newcode213
state/api/apiclient.go:213: return append([][]instance.HostPort{},
s.hostPorts...)
If you're serious about returning a copy, then you should probably copy
all the sub-slices too.

Otherwise, just return s.hostPorts and document that the returned slice
should not be modified.

https://codereview.appspot.com/81780043/diff/20001/state/api/state.go
File state/api/state.go (right):

https://codereview.appspot.com/81780043/diff/20001/state/api/state.go#newcode40
state/api/state.go:40: if err := st.addAddrHostPort(); err != nil {
rather than have addAddrHostPort be a method that mutates st.hostPorts,
perhaps it might be clearer to have something like:

hostPorts, err := addAddress(result.Servers, st.addr)
if err != nil {
     st.Close()
     return err
}
st.hostPorts = hostPort

// addAddress appends a new server derived from the given
// address to servers if the address is not already found
// there.
func addAddress(servers [][]instance.HostPort, addr string)
([][]instance.HostPort, error)

Then addAddress is easily testable if you want to.

BTW I have mixed feelings about returning an error if we can't split the
dial address. It means that if the dial address isn't parseable for some
reason (e.g. the port is non-numeric) we'll fail the connection, even
though we actually managed to connect.

https://codereview.appspot.com/81780043/

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

Please take a look.

https://codereview.appspot.com/81780043/diff/20001/juju/apiconn_test.go
File juju/apiconn_test.go (right):

https://codereview.appspot.com/81780043/diff/20001/juju/apiconn_test.go#newcode160
juju/apiconn_test.go:160: // be updated (to an empty slice).
On 2014/03/31 08:37:33, rog wrote:
> As discussed online, I'm not sure that this is the correct approach -
we
> shouldn't rely on calling methods on types that we've created with
New.

Done.

https://codereview.appspot.com/81780043/diff/20001/state/api/apiclient.go
File state/api/apiclient.go (right):

https://codereview.appspot.com/81780043/diff/20001/state/api/apiclient.go#newcode212
state/api/apiclient.go:212: func (s *State) HostPorts()
[][]instance.HostPort {
On 2014/03/31 08:37:33, rog wrote:
> On further reflection, I think this method should be named
APIHostPorts,
> mirroring the same function in State, and making it hopefully clearer
what the
> returned addresses are about.

Done.

https://codereview.appspot.com/81780043/diff/20001/state/api/apiclient.go#newcode213
state/api/apiclient.go:213: return append([][]instance.HostPort{},
s.hostPorts...)
On 2014/03/31 08:37:33, rog wrote:
> If you're serious about returning a copy, then you should probably
copy all the
> sub-slices too.

> Otherwise, just return s.hostPorts and document that the returned
slice should
> not be modified.

Hah, thanks for that. Fixed.

https://codereview.appspot.com/81780043/diff/20001/state/api/state.go
File state/api/state.go (right):

https://codereview.appspot.com/81780043/diff/20001/state/api/state.go#newcode40
state/api/state.go:40: if err := st.addAddrHostPort(); err != nil {
On 2014/03/31 08:37:33, rog wrote:
> rather than have addAddrHostPort be a method that mutates
st.hostPorts, perhaps
> it might be clearer to have something like:

> hostPorts, err := addAddress(result.Servers, st.addr)
> if err != nil {
> st.Close()
> return err
> }
> st.hostPorts = hostPort

> // addAddress appends a new server derived from the given
> // address to servers if the address is not already found
> // there.
> func addAddress(servers [][]instance.HostPort, addr string)
> ([][]instance.HostPort, error)

Done.

> Then addAddress is easily testable if you want to.

> BTW I have mixed feelings about returning an error if we can't split
the dial
> address. It means that if the dial address isn't parseable for some
reason (e.g.
> the port is non-numeric) we'll fail the connection, even though we
actually
> managed to connect.

Me too, but at the moment that remains theoretical. I'd rather have it
fail spectacularly if/when we decide to have a non-numeric
/etc/services-style port, and fix it then.

https://codereview.appspot.com/81780043/

Revision history for this message
Go Bot (go-bot) wrote :
Download full text (10.4 KiB)

The attempt to merge lp:~axwalk/juju-core/client-cache-apihostports into lp:juju-core failed. Below is the output from the failed tests.

ok launchpad.net/juju-core 0.014s
ok launchpad.net/juju-core/agent 1.133s
ok launchpad.net/juju-core/agent/mongo 0.544s
ok launchpad.net/juju-core/agent/tools 0.458s
ok launchpad.net/juju-core/bzr 5.334s
ok launchpad.net/juju-core/cert 3.032s
ok launchpad.net/juju-core/charm 0.347s
? launchpad.net/juju-core/charm/hooks [no test files]
? launchpad.net/juju-core/charm/testing [no test files]
ok launchpad.net/juju-core/cloudinit 0.033s
ok launchpad.net/juju-core/cloudinit/sshinit 0.848s
ok launchpad.net/juju-core/cmd 0.183s
ok launchpad.net/juju-core/cmd/charm-admin 0.742s
? launchpad.net/juju-core/cmd/charmd [no test files]
? launchpad.net/juju-core/cmd/charmload [no test files]
ok launchpad.net/juju-core/cmd/juju 201.222s
ok launchpad.net/juju-core/cmd/jujud 61.311s
ok launchpad.net/juju-core/cmd/plugins/juju-metadata 8.095s
? launchpad.net/juju-core/cmd/plugins/juju-restore [no test files]
ok launchpad.net/juju-core/cmd/plugins/local 0.219s
? launchpad.net/juju-core/cmd/plugins/local/juju-local [no test files]
ok launchpad.net/juju-core/constraints 0.022s
ok launchpad.net/juju-core/container 0.032s
ok launchpad.net/juju-core/container/factory 0.041s
ok launchpad.net/juju-core/container/kvm 0.184s
ok launchpad.net/juju-core/container/kvm/mock 0.035s
? launchpad.net/juju-core/container/kvm/testing [no test files]
ok launchpad.net/juju-core/container/lxc 4.328s
? launchpad.net/juju-core/container/lxc/mock [no test files]
? launchpad.net/juju-core/container/lxc/testing [no test files]
? launchpad.net/juju-core/container/testing [no test files]
ok launchpad.net/juju-core/downloader 5.222s
ok launchpad.net/juju-core/environs 2.492s
ok launchpad.net/juju-core/environs/bootstrap 10.562s
ok launchpad.net/juju-core/environs/cloudinit 0.494s
ok launchpad.net/juju-core/environs/config 2.452s
ok launchpad.net/juju-core/environs/configstore 0.029s
ok launchpad.net/juju-core/environs/filestorage 0.025s
ok launchpad.net/juju-core/environs/httpstorage 0.669s
ok launchpad.net/juju-core/environs/imagemetadata 0.412s
? launchpad.net/juju-core/environs/imagemetadata/testing [no test files]
ok launchpad.net/juju-core/environs/instances 0.036s
ok launchpad.net/juju-core/environs/jujutest 0.181s
ok launchpad.net/juju-core/environs/manual 11.573s
ok launchpad.net/juju-core/environs/simplestreams 0.245s
? launchpad.net/juju-core/environs/simplestreams/testing [no test files]
ok launchpad.net/juju-core/environs/sshstorage 0.806s
ok launchpad.net/juju-core/environs/storage 0.805s
ok launchpad.net/juju-core/environs/sync 43.459s
ok launchpad.net/juju-core/environs/testing 0.116s
ok launchpad.net/juju-core/environs/tools 4.456s
? launchpad.net/juju-core/environs/tools/testing [no test files]
ok launchpad.net/juju-core/errors 0.011s
ok launchpad.net/juju-core/instance 0.018s
? launchpad.net/juju-core/instance/testing [no test files]
ok launchpad.net/juju-core/juju 18.117s
ok launchpad.net/juju-core/juju/arch 0.0...

Revision history for this message
Roger Peppe (rogpeppe) wrote :

still LGTM

https://codereview.appspot.com/81780043/diff/40001/state/api/apiclient.go
File state/api/apiclient.go (right):

https://codereview.appspot.com/81780043/diff/40001/state/api/apiclient.go#newcode215
state/api/apiclient.go:215: hostPorts[i] = append([]instance.HostPort{},
server...)
or hostPorts[i] = append(hostPorts[i], server...)
(slightly shorter)

https://codereview.appspot.com/81780043/

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'juju/api.go'
2--- juju/api.go 2014-03-19 03:18:41 +0000
3+++ juju/api.go 2014-03-31 12:32:07 +0000
4@@ -14,6 +14,7 @@
5 "launchpad.net/juju-core/environs/config"
6 "launchpad.net/juju-core/environs/configstore"
7 "launchpad.net/juju-core/errors"
8+ "launchpad.net/juju-core/instance"
9 "launchpad.net/juju-core/juju/osenv"
10 "launchpad.net/juju-core/names"
11 "launchpad.net/juju-core/state/api"
12@@ -26,24 +27,25 @@
13 // The following are variables so that they can be
14 // changed by tests.
15 var (
16- apiOpen = api.Open
17- apiClose = (*api.State).Close
18 providerConnectDelay = 2 * time.Second
19 )
20
21-// apiState wraps an api.State, redefining its Close method
22-// so we can abuse it for testing purposes.
23-type apiState struct {
24- st *api.State
25+// apiState provides a subset of api.State's public
26+// interface, defined here so it can be mocked.
27+type apiState interface {
28+ Close() error
29+ APIHostPorts() [][]instance.HostPort
30+}
31+
32+type apiOpenFunc func(*api.Info, api.DialOpts) (apiState, error)
33+
34+type apiStateCachedInfo struct {
35+ apiState
36 // If cachedInfo is non-nil, it indicates that the info has been
37 // newly retrieved, and should be cached in the config store.
38 cachedInfo *api.Info
39 }
40
41-func (st apiState) Close() error {
42- return apiClose(st.st)
43-}
44-
45 // APIConn holds a connection to a juju environment and its
46 // associated state through its API interface.
47 type APIConn struct {
48@@ -62,7 +64,7 @@
49 return nil, err
50 }
51
52- st, err := apiOpen(info, dialOpts)
53+ st, err := api.Open(info, dialOpts)
54 // TODO(rog): handle errUnauthorized when the API handles passwords.
55 if err != nil {
56 return nil, err
57@@ -76,7 +78,7 @@
58 // Close terminates the connection to the environment and releases
59 // any associated resources.
60 func (c *APIConn) Close() error {
61- return apiClose(c.State)
62+ return c.State.Close()
63 }
64
65 // NewAPIClientFromName returns an api.Client connected to the API Server for
66@@ -112,12 +114,19 @@
67 if err != nil {
68 return nil, err
69 }
70- return newAPIFromStore(envName, store)
71+ apiOpen := func(info *api.Info, opts api.DialOpts) (apiState, error) {
72+ return api.Open(info, opts)
73+ }
74+ st, err := newAPIFromStore(envName, store, apiOpen)
75+ if err != nil {
76+ return nil, err
77+ }
78+ return st.(*api.State), nil
79 }
80
81 // newAPIFromStore implements the bulk of NewAPIClientFromName
82 // but is separate for testing purposes.
83-func newAPIFromStore(envName string, store configstore.Storage) (*api.State, error) {
84+func newAPIFromStore(envName string, store configstore.Storage, apiOpen apiOpenFunc) (apiState, error) {
85 // Try to read the default environment configuration file.
86 // If it doesn't exist, we carry on in case
87 // there's some environment info for that environment.
88@@ -170,7 +179,7 @@
89 if info != nil && len(info.APIEndpoint().Addresses) > 0 {
90 logger.Debugf("trying cached API connection settings")
91 try.Start(func(stop <-chan struct{}) (io.Closer, error) {
92- return apiInfoConnect(store, info, stop)
93+ return apiInfoConnect(store, info, apiOpen, stop)
94 })
95 // Delay the config connection until we've spent
96 // some time trying to connect to the cached info.
97@@ -179,7 +188,7 @@
98 logger.Debugf("no cached API connection settings found")
99 }
100 try.Start(func(stop <-chan struct{}) (io.Closer, error) {
101- return apiConfigConnect(info, envs, envName, stop, delay)
102+ return apiConfigConnect(info, envs, envName, apiOpen, stop, delay)
103 })
104 try.Close()
105 val0, err := try.Result()
106@@ -190,20 +199,23 @@
107 }
108 return nil, err
109 }
110+
111 val := val0.(apiState)
112-
113- if val.cachedInfo != nil && info != nil {
114- // Cache the connection settings only if we used the
115- // environment config, but any errors are just logged
116- // as warnings, because they're not fatal.
117- err = cacheAPIInfo(info, val.cachedInfo)
118- if err != nil {
119- logger.Warningf(err.Error())
120- } else {
121- logger.Debugf("updated API connection settings cache")
122+ if cachedInfo, ok := val.(apiStateCachedInfo); ok {
123+ val = cachedInfo.apiState
124+ if cachedInfo.cachedInfo != nil && info != nil {
125+ // Cache the connection settings only if we used the
126+ // environment config, but any errors are just logged
127+ // as warnings, because they're not fatal.
128+ err = cacheAPIInfo(info, cachedInfo.cachedInfo)
129+ if err != nil {
130+ logger.Warningf("cannot cache API connection settings: %v", err.Error())
131+ } else {
132+ logger.Infof("updated API connection settings cache")
133+ }
134 }
135 }
136- return val.st, nil
137+ return val, nil
138 }
139
140 func errorImportance(err error) int {
141@@ -230,10 +242,10 @@
142
143 // apiInfoConnect looks for endpoint on the given environment and
144 // tries to connect to it, sending the result on the returned channel.
145-func apiInfoConnect(store configstore.Storage, info configstore.EnvironInfo, stop <-chan struct{}) (apiState, error) {
146+func apiInfoConnect(store configstore.Storage, info configstore.EnvironInfo, apiOpen apiOpenFunc, stop <-chan struct{}) (apiState, error) {
147 endpoint := info.APIEndpoint()
148 if info == nil || len(endpoint.Addresses) == 0 {
149- return apiState{}, &infoConnectError{fmt.Errorf("no cached addresses")}
150+ return nil, &infoConnectError{fmt.Errorf("no cached addresses")}
151 }
152 logger.Infof("connecting to API addresses: %v", endpoint.Addresses)
153 apiInfo := &api.Info{
154@@ -244,9 +256,19 @@
155 }
156 st, err := apiOpen(apiInfo, api.DefaultDialOpts())
157 if err != nil {
158- return apiState{}, &infoConnectError{err}
159- }
160- return apiState{st, nil}, err
161+ return nil, &infoConnectError{err}
162+ }
163+ var addrs []string
164+ for _, serverHostPorts := range st.APIHostPorts() {
165+ for _, hostPort := range serverHostPorts {
166+ addrs = append(addrs, hostPort.NetAddr())
167+ }
168+ }
169+ // Update API addresses if they've changed. Error is non-fatal.
170+ if err := cacheChangedAPIAddresses(info, addrs); err != nil {
171+ logger.Warningf("cannot cache API addresses: %v", err.Error())
172+ }
173+ return st, err
174 }
175
176 // apiConfigConnect looks for configuration info on the given environment,
177@@ -254,7 +276,7 @@
178 // its endpoint. It only starts the attempt after the given delay,
179 // to allow the faster apiInfoConnect to hopefully succeed first.
180 // It returns nil if there was no configuration information found.
181-func apiConfigConnect(info configstore.EnvironInfo, envs *environs.Environs, envName string, stop <-chan struct{}, delay time.Duration) (apiState, error) {
182+func apiConfigConnect(info configstore.EnvironInfo, envs *environs.Environs, envName string, apiOpen apiOpenFunc, stop <-chan struct{}, delay time.Duration) (apiState, error) {
183 var cfg *config.Config
184 var err error
185 if info != nil && len(info.BootstrapConfig()) > 0 {
186@@ -262,30 +284,30 @@
187 } else if envs != nil {
188 cfg, err = envs.Config(envName)
189 if errors.IsNotFoundError(err) {
190- return apiState{}, err
191+ return nil, err
192 }
193 } else {
194- return apiState{}, errors.NotFoundf("environment %q", envName)
195+ return nil, errors.NotFoundf("environment %q", envName)
196 }
197 select {
198 case <-time.After(delay):
199 case <-stop:
200- return apiState{}, errAborted
201+ return nil, errAborted
202 }
203 environ, err := environs.New(cfg)
204 if err != nil {
205- return apiState{}, err
206+ return nil, err
207 }
208 apiInfo, err := environAPIInfo(environ)
209 if err != nil {
210- return apiState{}, err
211+ return nil, err
212 }
213 st, err := apiOpen(apiInfo, api.DefaultDialOpts())
214 // TODO(rog): handle errUnauthorized when the API handles passwords.
215 if err != nil {
216- return apiState{}, err
217+ return nil, err
218 }
219- return apiState{st, apiInfo}, nil
220+ return apiStateCachedInfo{st, apiInfo}, nil
221 }
222
223 func environAPIInfo(environ environs.Environ) (*api.Info, error) {
224@@ -312,14 +334,42 @@
225 })
226 _, username, err := names.ParseTag(apiInfo.Tag, names.UserTagKind)
227 if err != nil {
228- return fmt.Errorf("not caching API connection settings: invalid API user tag: %v", err)
229+ return fmt.Errorf("invalid API user tag: %v", err)
230 }
231 info.SetAPICredentials(configstore.APICredentials{
232 User: username,
233 Password: apiInfo.Password,
234 })
235+ return info.Write()
236+}
237+
238+// cacheChangedAPIAddresses updates the local environment settings (.jenv file)
239+// with the provided API server addresses if they have changed.
240+func cacheChangedAPIAddresses(info configstore.EnvironInfo, addrs []string) error {
241+ endpoint := info.APIEndpoint()
242+ if !addrsChanged(endpoint.Addresses, addrs) {
243+ return nil
244+ }
245+ logger.Debugf("API addresses changed from %q to %q", endpoint.Addresses, addrs)
246+ endpoint.Addresses = addrs
247+ info.SetAPIEndpoint(endpoint)
248 if err := info.Write(); err != nil {
249- return fmt.Errorf("cannot cache API connection settings: %v", err)
250+ return err
251 }
252+ logger.Infof("updated API connection settings cache")
253 return nil
254 }
255+
256+// addrsChanged returns true iff the two
257+// slices are not equal. Order is important.
258+func addrsChanged(a, b []string) bool {
259+ if len(a) != len(b) {
260+ return true
261+ }
262+ for i := range a {
263+ if a[i] != b[i] {
264+ return true
265+ }
266+ }
267+ return false
268+}
269
270=== modified file 'juju/apiconn_test.go'
271--- juju/apiconn_test.go 2014-03-19 22:16:15 +0000
272+++ juju/apiconn_test.go 2014-03-31 12:32:07 +0000
273@@ -8,7 +8,6 @@
274 "os"
275 "time"
276
277- "github.com/juju/testing"
278 jc "github.com/juju/testing/checkers"
279 gc "launchpad.net/gocheck"
280
281@@ -18,6 +17,7 @@
282 "launchpad.net/juju-core/environs/config"
283 "launchpad.net/juju-core/environs/configstore"
284 envtesting "launchpad.net/juju-core/environs/testing"
285+ "launchpad.net/juju-core/instance"
286 "launchpad.net/juju-core/juju"
287 "launchpad.net/juju-core/juju/osenv"
288 "launchpad.net/juju-core/provider/dummy"
289@@ -131,8 +131,12 @@
290 store := newConfigStore("noconfig", storeConfig)
291
292 called := 0
293- expectState := new(api.State)
294- apiOpen := func(apiInfo *api.Info, opts api.DialOpts) (*api.State, error) {
295+ expectState := &mockAPIState{
296+ apiHostPorts: [][]instance.HostPort{
297+ instance.AddressesWithPort([]instance.Address{instance.NewAddress("0.1.2.3")}, 1234),
298+ },
299+ }
300+ apiOpen := func(apiInfo *api.Info, opts api.DialOpts) (juju.APIState, error) {
301 c.Check(apiInfo.Tag, gc.Equals, "user-foo")
302 c.Check(string(apiInfo.CACert), gc.Equals, "certificated")
303 c.Check(apiInfo.Password, gc.Equals, "foopass")
304@@ -140,14 +144,25 @@
305 called++
306 return expectState, nil
307 }
308+
309 // Give NewAPIFromStore a store interface that can report when the
310- // config was written to, to ensure the cache isn't updated.
311- s.PatchValue(juju.APIOpen, apiOpen)
312+ // config was written to, to check if the cache is updated.
313 mockStore := &storageWithWriteNotify{store: store}
314- st, err := juju.NewAPIFromStore("noconfig", mockStore)
315+ st, err := juju.NewAPIFromStore("noconfig", mockStore, apiOpen)
316 c.Assert(err, gc.IsNil)
317 c.Assert(st, gc.Equals, expectState)
318 c.Assert(called, gc.Equals, 1)
319+ c.Assert(mockStore.written, jc.IsTrue)
320+ info, err := store.ReadInfo("noconfig")
321+ c.Assert(err, gc.IsNil)
322+ c.Assert(info.APIEndpoint().Addresses, gc.DeepEquals, []string{"0.1.2.3:1234"})
323+ mockStore.written = false
324+
325+ // If APIHostPorts haven't changed, then the store won't be updated.
326+ st, err = juju.NewAPIFromStore("noconfig", mockStore, apiOpen)
327+ c.Assert(err, gc.IsNil)
328+ c.Assert(st, gc.Equals, expectState)
329+ c.Assert(called, gc.Equals, 2)
330 c.Assert(mockStore.written, jc.IsFalse)
331 }
332
333@@ -177,8 +192,8 @@
334 c.Assert(info.APICredentials(), jc.DeepEquals, configstore.APICredentials{})
335
336 called := 0
337- expectState := new(api.State)
338- apiOpen := func(apiInfo *api.Info, opts api.DialOpts) (*api.State, error) {
339+ expectState := &mockAPIState{}
340+ apiOpen := func(apiInfo *api.Info, opts api.DialOpts) (juju.APIState, error) {
341 c.Check(apiInfo.Tag, gc.Equals, "user-admin")
342 c.Check(string(apiInfo.CACert), gc.Not(gc.Equals), "")
343 c.Check(apiInfo.Password, gc.Equals, "adminpass")
344@@ -186,8 +201,7 @@
345 called++
346 return expectState, nil
347 }
348- s.PatchValue(juju.APIOpen, apiOpen)
349- st, err := juju.NewAPIFromStore("myenv", store)
350+ st, err := juju.NewAPIFromStore("myenv", store, apiOpen)
351 c.Assert(err, gc.IsNil)
352 c.Assert(st, gc.Equals, expectState)
353 c.Assert(called, gc.Equals, 1)
354@@ -209,16 +223,11 @@
355 defer coretesting.MakeEmptyFakeHome(c).Restore()
356 expectErr := fmt.Errorf("an error")
357 store := newConfigStoreWithError(expectErr)
358- s.PatchValue(juju.APIOpen, panicAPIOpen)
359- client, err := juju.NewAPIFromStore("noconfig", store)
360+ client, err := juju.NewAPIFromStore("noconfig", store, panicAPIOpen)
361 c.Assert(err, gc.Equals, expectErr)
362 c.Assert(client, gc.IsNil)
363 }
364
365-func panicAPIOpen(apiInfo *api.Info, opts api.DialOpts) (*api.State, error) {
366- panic("api.Open called unexpectedly")
367-}
368-
369 func (s *NewAPIClientSuite) TestWithInfoNoAddresses(c *gc.C) {
370 defer coretesting.MakeEmptyFakeHome(c).Restore()
371 store := newConfigStore("noconfig", &environInfo{
372@@ -227,9 +236,7 @@
373 CACert: "certificated",
374 },
375 })
376- s.PatchValue(juju.APIOpen, panicAPIOpen)
377-
378- st, err := juju.NewAPIFromStore("noconfig", store)
379+ st, err := juju.NewAPIFromStore("noconfig", store, panicAPIOpen)
380 c.Assert(err, gc.ErrorMatches, `environment "noconfig" not found`)
381 c.Assert(st, gc.IsNil)
382 }
383@@ -243,11 +250,10 @@
384 })
385
386 expectErr := fmt.Errorf("an error")
387- apiOpen := func(apiInfo *api.Info, opts api.DialOpts) (*api.State, error) {
388+ apiOpen := func(apiInfo *api.Info, opts api.DialOpts) (juju.APIState, error) {
389 return nil, expectErr
390 }
391- s.PatchValue(juju.APIOpen, apiOpen)
392- st, err := juju.NewAPIFromStore("noconfig", store)
393+ st, err := juju.NewAPIFromStore("noconfig", store, apiOpen)
394 c.Assert(err, gc.Equals, expectErr)
395 c.Assert(st, gc.IsNil)
396 }
397@@ -258,27 +264,30 @@
398 bootstrapEnv(c, coretesting.SampleEnvName, store)
399 setEndpointAddress(c, store, coretesting.SampleEnvName, "infoapi.invalid")
400
401- infoOpenedState := new(api.State)
402+ infoOpenedState := &mockAPIState{}
403 infoEndpointOpened := make(chan struct{})
404- cfgOpenedState := new(api.State)
405+ cfgOpenedState := &mockAPIState{}
406 // On a sample run with no delay, the logic took 45ms to run, so
407 // we make the delay slightly more than that, so that if the
408 // logic doesn't delay at all, the test will fail reasonably consistently.
409 s.PatchValue(juju.ProviderConnectDelay, 50*time.Millisecond)
410- apiOpen := func(info *api.Info, opts api.DialOpts) (*api.State, error) {
411+ apiOpen := func(info *api.Info, opts api.DialOpts) (juju.APIState, error) {
412 if info.Addrs[0] == "infoapi.invalid" {
413 infoEndpointOpened <- struct{}{}
414 return infoOpenedState, nil
415 }
416 return cfgOpenedState, nil
417 }
418- s.PatchValue(juju.APIOpen, apiOpen)
419
420- stateClosed, restoreAPIClose := setAPIClosed()
421- defer restoreAPIClose.Restore()
422+ stateClosed := make(chan juju.APIState)
423+ infoOpenedState.close = func(st juju.APIState) error {
424+ stateClosed <- st
425+ return nil
426+ }
427+ cfgOpenedState.close = infoOpenedState.close
428
429 startTime := time.Now()
430- st, err := juju.NewAPIFromStore(coretesting.SampleEnvName, store)
431+ st, err := juju.NewAPIFromStore(coretesting.SampleEnvName, store, apiOpen)
432 c.Assert(err, gc.IsNil)
433 // The connection logic should wait for some time before opening
434 // the API from the configuration.
435@@ -320,13 +329,13 @@
436 bootstrapEnv(c, coretesting.SampleEnvName, store)
437 setEndpointAddress(c, store, coretesting.SampleEnvName, "infoapi.invalid")
438
439- infoOpenedState := new(api.State)
440+ infoOpenedState := &mockAPIState{}
441 infoEndpointOpened := make(chan struct{})
442- cfgOpenedState := new(api.State)
443+ cfgOpenedState := &mockAPIState{}
444 cfgEndpointOpened := make(chan struct{})
445
446 s.PatchValue(juju.ProviderConnectDelay, 0*time.Second)
447- apiOpen := func(info *api.Info, opts api.DialOpts) (*api.State, error) {
448+ apiOpen := func(info *api.Info, opts api.DialOpts) (juju.APIState, error) {
449 if info.Addrs[0] == "infoapi.invalid" {
450 infoEndpointOpened <- struct{}{}
451 <-infoEndpointOpened
452@@ -336,14 +345,17 @@
453 <-cfgEndpointOpened
454 return cfgOpenedState, nil
455 }
456- s.PatchValue(juju.APIOpen, apiOpen)
457
458- stateClosed, restoreAPIClose := setAPIClosed()
459- defer restoreAPIClose.Restore()
460+ stateClosed := make(chan juju.APIState)
461+ infoOpenedState.close = func(st juju.APIState) error {
462+ stateClosed <- st
463+ return nil
464+ }
465+ cfgOpenedState.close = infoOpenedState.close
466
467 done := make(chan struct{})
468 go func() {
469- st, err := juju.NewAPIFromStore(coretesting.SampleEnvName, store)
470+ st, err := juju.NewAPIFromStore(coretesting.SampleEnvName, store, apiOpen)
471 c.Check(err, gc.IsNil)
472 c.Check(st, gc.Equals, infoOpenedState)
473 close(done)
474@@ -387,14 +399,13 @@
475 setEndpointAddress(c, store, coretesting.SampleEnvName, "infoapi.invalid")
476
477 s.PatchValue(juju.ProviderConnectDelay, 0*time.Second)
478- apiOpen := func(info *api.Info, opts api.DialOpts) (*api.State, error) {
479+ apiOpen := func(info *api.Info, opts api.DialOpts) (juju.APIState, error) {
480 if info.Addrs[0] == "infoapi.invalid" {
481 return nil, fmt.Errorf("info connect failed")
482 }
483 return nil, fmt.Errorf("config connect failed")
484 }
485- s.PatchValue(juju.APIOpen, apiOpen)
486- st, err := juju.NewAPIFromStore(coretesting.SampleEnvName, store)
487+ st, err := juju.NewAPIFromStore(coretesting.SampleEnvName, store, apiOpen)
488 c.Check(err, gc.ErrorMatches, "config connect failed")
489 c.Check(st, gc.IsNil)
490 }
491@@ -427,7 +438,10 @@
492 err = os.Remove(osenv.JujuHomePath("environments.yaml"))
493 c.Assert(err, gc.IsNil)
494
495- st, err := juju.NewAPIFromStore(coretesting.SampleEnvName, store)
496+ apiOpen := func(*api.Info, api.DialOpts) (juju.APIState, error) {
497+ return &mockAPIState{}, nil
498+ }
499+ st, err := juju.NewAPIFromStore(coretesting.SampleEnvName, store, apiOpen)
500 c.Check(err, gc.IsNil)
501 st.Close()
502 }
503@@ -455,7 +469,10 @@
504 // Now we have info for envName2 which will actually
505 // cause a connection to the originally bootstrapped
506 // state.
507- st, err := juju.NewAPIFromStore(envName2, store)
508+ apiOpen := func(*api.Info, api.DialOpts) (juju.APIState, error) {
509+ return &mockAPIState{}, nil
510+ }
511+ st, err := juju.NewAPIFromStore(envName2, store, apiOpen)
512 c.Check(err, gc.IsNil)
513 st.Close()
514
515@@ -478,19 +495,6 @@
516 c.Assert(envInfo.Name, gc.Equals, expectName)
517 }
518
519-func setAPIClosed() (<-chan *api.State, testing.Restorer) {
520- stateClosed := make(chan *api.State)
521- apiClose := func(st *api.State) error {
522- stateClosed <- st
523- return nil
524- }
525- return stateClosed, testing.PatchValue(juju.APIClose, apiClose)
526-}
527-
528-func updateSecretsNoop(_ environs.Environ, _ *api.State) error {
529- return nil
530-}
531-
532 // newConfigStoreWithError that will return the given
533 // error from ReadInfo.
534 func newConfigStoreWithError(err error) configstore.Storage {
535
536=== modified file 'juju/export_test.go'
537--- juju/export_test.go 2014-03-19 03:18:41 +0000
538+++ juju/export_test.go 2014-03-31 12:32:07 +0000
539@@ -1,8 +1,21 @@
540 package juju
541
542+import (
543+ "launchpad.net/juju-core/environs/configstore"
544+ "launchpad.net/juju-core/state/api"
545+)
546+
547 var (
548- APIOpen = &apiOpen
549- APIClose = &apiClose
550 ProviderConnectDelay = &providerConnectDelay
551- NewAPIFromStore = newAPIFromStore
552 )
553+
554+type APIState apiState
555+
556+type APIOpenFunc func(*api.Info, api.DialOpts) (APIState, error)
557+
558+func NewAPIFromStore(envName string, store configstore.Storage, f APIOpenFunc) (APIState, error) {
559+ apiOpen := func(info *api.Info, opts api.DialOpts) (apiState, error) {
560+ return f(info, opts)
561+ }
562+ return newAPIFromStore(envName, store, apiOpen)
563+}
564
565=== added file 'juju/mock_test.go'
566--- juju/mock_test.go 1970-01-01 00:00:00 +0000
567+++ juju/mock_test.go 2014-03-31 12:32:07 +0000
568@@ -0,0 +1,28 @@
569+package juju_test
570+
571+import (
572+ "launchpad.net/juju-core/instance"
573+ "launchpad.net/juju-core/juju"
574+ "launchpad.net/juju-core/state/api"
575+)
576+
577+type mockAPIState struct {
578+ close func(juju.APIState) error
579+
580+ apiHostPorts [][]instance.HostPort
581+}
582+
583+func (s *mockAPIState) Close() error {
584+ if s.close != nil {
585+ return s.close(s)
586+ }
587+ return nil
588+}
589+
590+func (s *mockAPIState) APIHostPorts() [][]instance.HostPort {
591+ return s.apiHostPorts
592+}
593+
594+func panicAPIOpen(apiInfo *api.Info, opts api.DialOpts) (juju.APIState, error) {
595+ panic("api.Open called unexpectedly")
596+}
597
598=== modified file 'state/api/apiclient.go'
599--- state/api/apiclient.go 2014-03-19 03:18:41 +0000
600+++ state/api/apiclient.go 2014-03-31 12:32:07 +0000
601@@ -11,6 +11,7 @@
602 "code.google.com/p/go.net/websocket"
603
604 "launchpad.net/juju-core/cert"
605+ "launchpad.net/juju-core/instance"
606 "launchpad.net/juju-core/log"
607 "launchpad.net/juju-core/rpc"
608 "launchpad.net/juju-core/rpc/jsoncodec"
609@@ -25,7 +26,13 @@
610 type State struct {
611 client *rpc.Conn
612 conn *websocket.Conn
613- addr string
614+
615+ // addr is the address used to connect to the API server.
616+ addr string
617+
618+ // hostPorts is the API server addresses returned from Login,
619+ // which the client may cache and use for failover.
620+ hostPorts [][]instance.HostPort
621
622 // authTag holds the authenticated entity's tag after login.
623 authTag string
624@@ -189,7 +196,23 @@
625 return s.client
626 }
627
628-// Addr returns the address used to connect to the RPC server.
629+// Addr returns the address used to connect to the API server.
630 func (s *State) Addr() string {
631 return s.addr
632 }
633+
634+// APIHostPorts returns addresses that may be used to connect
635+// to the API server, including the address used to connect.
636+//
637+// The addresses are scoped (public, cloud-internal, etc.), so
638+// the client may choose which addresses to attempt. For the
639+// Juju CLI, all addresses must be attempted, as the CLI may
640+// be invoked both within and outside the environment (think
641+// private clouds).
642+func (s *State) APIHostPorts() [][]instance.HostPort {
643+ hostPorts := make([][]instance.HostPort, len(s.hostPorts))
644+ for i, server := range s.hostPorts {
645+ hostPorts[i] = append([]instance.HostPort{}, server...)
646+ }
647+ return hostPorts
648+}
649
650=== modified file 'state/api/params/params.go'
651--- state/api/params/params.go 2014-03-28 13:31:26 +0000
652+++ state/api/params/params.go 2014-03-31 12:32:07 +0000
653@@ -640,6 +640,11 @@
654 Servers [][]instance.HostPort
655 }
656
657+// LoginResult holds the result of a Login call.
658+type LoginResult struct {
659+ Servers [][]instance.HostPort
660+}
661+
662 // EnsureAvailability contains arguments for
663 // the EnsureAvailability client API call.
664 type EnsureAvailability struct {
665
666=== modified file 'state/api/state.go'
667--- state/api/state.go 2014-02-27 05:24:43 +0000
668+++ state/api/state.go 2014-03-31 12:32:07 +0000
669@@ -4,6 +4,10 @@
670 package api
671
672 import (
673+ "net"
674+ "strconv"
675+
676+ "launchpad.net/juju-core/instance"
677 "launchpad.net/juju-core/state/api/agent"
678 "launchpad.net/juju-core/state/api/charmrevisionupdater"
679 "launchpad.net/juju-core/state/api/deployer"
680@@ -24,17 +28,54 @@
681 // method is usually called automatically by Open. The machine nonce
682 // should be empty unless logging in as a machine agent.
683 func (st *State) Login(tag, password, nonce string) error {
684+ var result params.LoginResult
685 err := st.Call("Admin", "", "Login", &params.Creds{
686 AuthTag: tag,
687 Password: password,
688 Nonce: nonce,
689- }, nil)
690+ }, &result)
691 if err == nil {
692 st.authTag = tag
693+ hostPorts, err := addAddress(result.Servers, st.addr)
694+ if err != nil {
695+ st.Close()
696+ return err
697+ }
698+ st.hostPorts = hostPorts
699 }
700 return err
701 }
702
703+// addAddress appends a new server derived from the given
704+// address to servers if the address is not already found
705+// there.
706+func addAddress(servers [][]instance.HostPort, addr string) ([][]instance.HostPort, error) {
707+ for _, server := range servers {
708+ for _, hostPort := range server {
709+ if hostPort.NetAddr() == addr {
710+ return servers, nil
711+ }
712+ }
713+ }
714+ host, portString, err := net.SplitHostPort(addr)
715+ if err != nil {
716+ return nil, err
717+ }
718+ port, err := strconv.Atoi(portString)
719+ if err != nil {
720+ return nil, err
721+ }
722+ hostPort := instance.HostPort{
723+ Address: instance.Address{
724+ Value: host,
725+ Type: instance.DeriveAddressType(host),
726+ NetworkScope: instance.NetworkUnknown,
727+ },
728+ Port: port,
729+ }
730+ return append(servers, []instance.HostPort{hostPort}), nil
731+}
732+
733 // Client returns an object that can be used
734 // to access client-specific functionality.
735 func (st *State) Client() *Client {
736
737=== modified file 'state/apiserver/admin.go'
738--- state/apiserver/admin.go 2014-01-17 19:33:42 +0000
739+++ state/apiserver/admin.go 2014-03-31 12:32:07 +0000
740@@ -63,16 +63,16 @@
741 // Login logs in with the provided credentials.
742 // All subsequent requests on the connection will
743 // act as the authenticated user.
744-func (a *srvAdmin) Login(c params.Creds) error {
745+func (a *srvAdmin) Login(c params.Creds) (params.LoginResult, error) {
746 a.mu.Lock()
747 defer a.mu.Unlock()
748 if a.loggedIn {
749 // This can only happen if Login is called concurrently.
750- return errAlreadyLoggedIn
751+ return params.LoginResult{}, errAlreadyLoggedIn
752 }
753 entity, err := checkCreds(a.root.srv.state, c)
754 if err != nil {
755- return err
756+ return params.LoginResult{}, err
757 }
758 if a.reqNotifier != nil {
759 a.reqNotifier.login(entity.Tag())
760@@ -81,11 +81,18 @@
761 // to serve to them.
762 newRoot, err := a.apiRootForEntity(entity, c)
763 if err != nil {
764- return err
765- }
766+ return params.LoginResult{}, err
767+ }
768+
769+ // Fetch the API server addresses from state.
770+ hostPorts, err := a.root.srv.state.APIHostPorts()
771+ if err != nil {
772+ return params.LoginResult{}, err
773+ }
774+ logger.Debugf("hostPorts: %v", hostPorts)
775
776 a.root.rpcConn.Serve(newRoot, serverError)
777- return nil
778+ return params.LoginResult{hostPorts}, nil
779 }
780
781 func checkCreds(st *state.State, c params.Creds) (taggedAuthenticator, error) {
782
783=== modified file 'state/apiserver/login_test.go'
784--- state/apiserver/login_test.go 2014-03-13 13:42:50 +0000
785+++ state/apiserver/login_test.go 2014-03-31 12:32:07 +0000
786@@ -4,10 +4,14 @@
787 package apiserver_test
788
789 import (
790+ "net"
791+ "strconv"
792+
793 "github.com/juju/loggo"
794 jc "github.com/juju/testing/checkers"
795 gc "launchpad.net/gocheck"
796
797+ "launchpad.net/juju-core/instance"
798 jujutesting "launchpad.net/juju-core/juju/testing"
799 "launchpad.net/juju-core/state"
800 "launchpad.net/juju-core/state/api"
801@@ -160,8 +164,74 @@
802 // Now that we are logged in, we see the entity's tag
803 // [0-9.umns] is to handle timestamps that are ns, us, ms, or s
804 // long, though we expect it to be in the 'ms' range.
805- `-> \[[0-9A-F]+\] machine-0 [0-9.]+[umn]?s {"RequestId":1,"Response":{}} Admin\[""\].Login`,
806+ `-> \[[0-9A-F]+\] machine-0 [0-9.]+[umn]?s {"RequestId":1,"Response":{"Servers":\[\]}} Admin\[""\].Login`,
807 `<- \[[0-9A-F]+\] machine-0 {"RequestId":2,"Type":"Machiner","Request":"Life","Params":{"Entities":\[{"Tag":"machine-0"}\]}}`,
808 `-> \[[0-9A-F]+\] machine-0 [0-9.umns]+ {"RequestId":2,"Response":{"Results":\[{"Life":"alive","Error":null}\]}} Machiner\[""\]\.Life`,
809 })
810 }
811+
812+func (s *loginSuite) TestLoginAddrs(c *gc.C) {
813+ machine, err := s.State.AddMachine("quantal", state.JobHostUnits)
814+ c.Assert(err, gc.IsNil)
815+ err = machine.SetProvisioned("foo", "fake_nonce", nil)
816+ c.Assert(err, gc.IsNil)
817+ password, err := utils.RandomPassword()
818+ c.Assert(err, gc.IsNil)
819+ err = machine.SetPassword(password)
820+ c.Assert(err, gc.IsNil)
821+ info, cleanup := s.setupServer(c)
822+ defer cleanup()
823+ info.Tag = machine.Tag()
824+ info.Password = password
825+ info.Nonce = "fake_nonce"
826+
827+ // Initially just the address we connect with is returned,
828+ // despite there being no APIHostPorts in state.
829+ connectedAddr, hostPorts := s.loginHostPorts(c, info)
830+ connectedAddrHost, connectedAddrPortString, err := net.SplitHostPort(connectedAddr)
831+ c.Assert(err, gc.IsNil)
832+ connectedAddrPort, err := strconv.Atoi(connectedAddrPortString)
833+ c.Assert(err, gc.IsNil)
834+ connectedAddrHostPorts := [][]instance.HostPort{
835+ []instance.HostPort{{
836+ instance.NewAddress(connectedAddrHost),
837+ connectedAddrPort,
838+ }},
839+ }
840+ c.Assert(hostPorts, gc.DeepEquals, connectedAddrHostPorts)
841+
842+ // After storing APIHostPorts in state, Login should store
843+ // all of them and the address we connected with.
844+ server1Addresses := []instance.Address{{
845+ Value: "server-1",
846+ Type: instance.HostName,
847+ NetworkScope: instance.NetworkPublic,
848+ }, {
849+ Value: "10.0.0.1",
850+ Type: instance.Ipv4Address,
851+ NetworkName: "internal",
852+ NetworkScope: instance.NetworkCloudLocal,
853+ }}
854+ server2Addresses := []instance.Address{{
855+ Value: "::1",
856+ Type: instance.Ipv6Address,
857+ NetworkName: "loopback",
858+ NetworkScope: instance.NetworkMachineLocal,
859+ }}
860+ stateAPIHostPorts := [][]instance.HostPort{
861+ instance.AddressesWithPort(server1Addresses, 123),
862+ instance.AddressesWithPort(server2Addresses, 456),
863+ }
864+ err = s.State.SetAPIHostPorts(stateAPIHostPorts)
865+ c.Assert(err, gc.IsNil)
866+ connectedAddr, hostPorts = s.loginHostPorts(c, info)
867+ stateAPIHostPorts = append(stateAPIHostPorts, connectedAddrHostPorts...)
868+ c.Assert(hostPorts, gc.DeepEquals, stateAPIHostPorts)
869+}
870+
871+func (s *loginSuite) loginHostPorts(c *gc.C, info *api.Info) (connectedAddr string, hostPorts [][]instance.HostPort) {
872+ st, err := api.Open(info, fastDialOpts)
873+ c.Assert(err, gc.IsNil)
874+ defer st.Close()
875+ return st.Addr(), st.APIHostPorts()
876+}

Subscribers

People subscribed via source and target branches

to status/vote changes: