Merge lp:~hduran-8/juju-core/apiworker_force_local_connection into lp:~go-bot/juju-core/trunk

Proposed by Horacio Durán
Status: Merged
Approved by: Horacio Durán
Approved revision: no longer in the source branch.
Merged at revision: 2814
Proposed branch: lp:~hduran-8/juju-core/apiworker_force_local_connection
Merge into: lp:~go-bot/juju-core/trunk
Diff against target: 264 lines (+160/-9)
5 files modified
agent/agent.go (+17/-1)
agent/agent_test.go (+34/-2)
cmd/jujud/agent_test.go (+11/-5)
state/api/apiclient.go (+18/-1)
state/api/apiclient_test.go (+80/-0)
To merge this branch: bzr merge lp:~hduran-8/juju-core/apiworker_force_local_connection
Reviewer Review Type Date Requested Status
Juju Engineering Pending
Review via email: mp+221221@code.launchpad.net

Commit message

APIWorker should only connect to API on localhost

In some cases, APIWorker might try to connect
to a different address than localhost in State
Servers, to achieve this, APIInfo was modified
to add localhost with the configured StateServerPort
to the Addrs list if not present. To make sure
localhost option will be picked first api.Open now
sorts the Addrs list before trying to connect
to make sure local ones will be tried first.

https://codereview.appspot.com/100810045/

R=natefinch

Description of the change

APIWorker should only connect to API on localhost

In some cases, APIWorker might try to connect
to a different address than localhost in State
Servers, for this APIInfo was modified to add
localhost with the configured StateServerPort
to the Addrs list if not present. To make sure
localhost option will be picked first Open now
sorts the Addrs list before trying to connect
to make sure local ones will be tried first.

https://codereview.appspot.com/100810045/

To post a comment you must log in.
Revision history for this message
Horacio Durán (hduran-8) wrote :
Download full text (5.2 KiB)

Reviewers: mp+221221_code.launchpad.net,

Message:
Please take a look.

Description:
APIWorker should only connect to API on localhost

In some cases, APIWorker might try to connect
to a different address than localhost in machines
with the ManageEnviron job, in those cases we
change the api hostname from whatever is set
to localhost and keep the port.

https://code.launchpad.net/~hduran-8/juju-core/apiworker_force_local_connection/+merge/221221

(do not edit description out of merge proposal)

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

Affected files (+83, -5 lines):
   A [revision details]
   M cmd/jujud/agent.go
   M cmd/jujud/agent_test.go

Index: [revision details]
=== added file '[revision details]'
--- [revision details] 2012-01-01 00:00:00 +0000
+++ [revision details] 2012-01-01 00:00:00 +0000
@@ -0,0 +1,2 @@
+Old revision: tarmac-20140519143554-fd3fvpqfvhm5u642
+New revision: <email address hidden>

Index: cmd/jujud/agent.go
=== modified file 'cmd/jujud/agent.go'
--- cmd/jujud/agent.go 2014-05-14 21:13:17 +0000
+++ cmd/jujud/agent.go 2014-05-28 19:00:09 +0000
@@ -6,7 +6,9 @@
  import (
   "fmt"
   "io"
+ "net"
   "path/filepath"
+ "strconv"
   "sync"
   "time"

@@ -191,6 +193,25 @@

  type configChanger func(c *agent.Config)

+func pickBestHosts(apiInfo *api.Info, jobs []params.MachineJob) error {
+ for _, job := range jobs {
+ if job == params.JobManageEnviron {
+ firstAddr := apiInfo.Addrs[0]
+ _, port, err := net.SplitHostPort(firstAddr)
+ if err != nil {
+ return err
+ }
+ portNum, err := strconv.Atoi(port)
+ if err != nil {
+ return fmt.Errorf("bad port number %q", port)
+ }
+ apiInfo.Addrs = []string{fmt.Sprintf("localhost:%d", portNum)}
+ break
+ }
+ }
+ return nil
+}
+
  // openAPIState opens the API using the given information, and
  // returns the opened state and the api entity with
  // the given tag. The given changeConfig function is
@@ -205,6 +226,12 @@
   // then the worker that's calling this cannot
   // be interrupted.
   info := agentConfig.APIInfo()
+ // Ensure that we conect trough localhost
+ agentConfigJobs := agentConfig.Jobs()
+ if err := pickBestHosts(info, agentConfigJobs); err != nil {
+ return nil, nil, err
+ }
+
   st, err := apiOpen(info, api.DialOpts{})
   usedOldPassword := false
   if params.IsCodeUnauthorized(err) {

Index: cmd/jujud/agent_test.go
=== modified file 'cmd/jujud/agent_test.go'
--- cmd/jujud/agent_test.go 2014-04-30 23:18:40 +0000
+++ cmd/jujud/agent_test.go 2014-05-28 19:02:43 +0000
@@ -105,17 +105,23 @@
   return "old"
  }

+func (fakeAPIOpenConfig) Jobs() []params.MachineJob {
+ return []params.MachineJob{}
+}
+
  var _ = gc.Suite(&apiOpenSuite{})

+type replaceErrors struct {
+ openErr error
+ replaceErr error
+}
+
  func (s *apiOpenSuite) TestOpenAPIStateReplaceErrors(c *gc.C) {
   var apiError error
   s.PatchValue(&apiOpen, func(info *api.Info, opts api.DialOpts)
(*api.State, error) {
    return nil, apiError
   })
- for i, test := range []struct {
- openErr error
- replaceErr error
- }{{
+ errReplacePairs := []replaceErrors{{
    fmt.Errorf("blah"), nil,
   }, {
    openErr: &p...

Read more...

Revision history for this message
Nate Finch (natefinch) wrote :

I think we need to test that we're actually passing the results of the
address picker into apiOpen.

https://codereview.appspot.com/100810045/diff/20001/cmd/jujud/agent_test.go
File cmd/jujud/agent_test.go (right):

https://codereview.appspot.com/100810045/diff/20001/cmd/jujud/agent_test.go#newcode114
cmd/jujud/agent_test.go:114: type replaceErrors struct {
You can put this type definition right in the test function, and I think
you should :)

https://codereview.appspot.com/100810045/diff/20001/cmd/jujud/agent_test.go#newcode362
cmd/jujud/agent_test.go:362: type providedExpectedAddresses struct {
move this into the test, and you can give it a less long name.

Also, please give each test a unique description and log that
description inside the test's loop, that way it's clear what test fails
(otherwise there's no way to know which of the tests in the loop
failed). It can be something like "State machine should always use
localhost" and "non-state machine should use the address given".

https://codereview.appspot.com/100810045/

Revision history for this message
William Reade (fwereade) wrote :

Forgive the pontification, but I think this demands a bit more thought.
As it is it feel like just slapping another layer on top to cover the
deficiencies below, and that rarely ends well...

https://codereview.appspot.com/100810045/diff/20001/cmd/jujud/agent.go
File cmd/jujud/agent.go (right):

https://codereview.appspot.com/100810045/diff/20001/cmd/jujud/agent.go#newcode196
cmd/jujud/agent.go:196: func pickBestHosts(apiInfo *api.Info, jobs
[]params.MachineJob) error {
On general principles, I think I'd prefer it if we copied and returned a
new *api.Info, rather than mutating the one we passed in. It might be
the case that nobody else has a reference to this one, but then it also
might not ;).

https://codereview.appspot.com/100810045/diff/20001/cmd/jujud/agent.go#newcode233
cmd/jujud/agent.go:233: }
Hmm. This feels like a band-aid-style solution... I'd prefer to record
the addresses we mean to connect to in the first place.

But then that involves throwing away information we'd need if we were
ever demoted, so it's probably not viable. And we can't have state
servers reject API connections from other state servers, or how will we
ever promote a machine to a state server? Bah.

However... apiOpen *does* try different servers if it can't connect,
right? What would you think of the combination of:

1) APIInfo() uses the presence of a StateServingInfo to *also* return
localhost:port-this-machine-is-configured-to-serve-the-api-on (so we
don't ever actually lie, but we do return what's likely to be the best
address to use.)

...and...

2) apiOpen (or the layer below) prioritises localhost: addresses (which
STM like a good idea anyway..?)

..?

The other option would be to have the API call that returns state server
addresses figure out the correct addresses from the POV of the affected
entity, and return localhost where appropriate in the first place. I
think that'd be best, but I'm not sure it's best enough to be worth the
effort. I *am* really bothered about encoding the one-api-port-only
assumption in this snippet of code, though -- picking an (effectively)
random address from which to take the port feels all kinds of wrong.

Thoughts?

https://codereview.appspot.com/100810045/

Revision history for this message
Horacio Durán (hduran-8) wrote :
Revision history for this message
Nate Finch (natefinch) wrote :

LGTM with a couple minor modifications.

https://codereview.appspot.com/100810045/diff/40001/agent/agent.go
File agent/agent.go (right):

https://codereview.appspot.com/100810045/diff/40001/agent/agent.go#newcode615
agent/agent.go:615: if eachAddress == localApiAddr {
I don't really like this naming convention. I get that you're making it
"for eachAddress in addresses"... but then the if statement looks like
it's saying "if each address equal local" which in English means "if
*all* addresses equal local". Plus, the variable is used directly under
where it is declared, so it's not like you need a long variable name to
remember that this is one item in the list.

I'd prefer

for _, addr := range addrs {
     if addr == localApiAddr {

each here doesn't really add anything, except give me more words to look
at and try to understand.

https://codereview.appspot.com/100810045/diff/40001/agent/agent_test.go
File agent/agent_test.go (right):

https://codereview.appspot.com/100810045/diff/40001/agent/agent_test.go#newcode461
agent/agent_test.go:461: c.Assert(localhostAddressFound, gc.Equals,
true)
There's a jc.IsTrue, by the way.

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

https://codereview.appspot.com/100810045/diff/40001/state/api/apiclient.go#newcode112
state/api/apiclient.go:112: func SortLocalHostFirst(addrs []string)
[]string {
you should just implement a sort type:

type localFirst []string
func (l localFirst) Len() int {
     return len(l)
}
func (l LocalFirst) Swap(i, j int) {
     l[i], l[j] = l[j], l[i]
}
func Less(i, j int) bool {
     return strings.HasPrefix(l[i])
}

then you can just do

addrs := sort.Sort(localFirst(info.Addrs))

And that way I don't have to debug your sorting algorithm ;)

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

https://codereview.appspot.com/100810045/diff/40001/state/api/apiclient_test.go#newcode76
state/api/apiclient_test.go:76: listener, err := net.Listen("tcp",
"localhost:27017")
Don't use the same port as built in mongo port, since that may conflict
with mongo if it's running.

https://codereview.appspot.com/100810045/

Revision history for this message
Wayne Witzel III (wwitzel3) wrote :

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

https://codereview.appspot.com/100810045/diff/40001/state/api/apiclient.go#newcode111
state/api/apiclient.go:111:
We should use the built-in sort here.

http://golang.org/pkg/sort/

type LocalhostFirst []string

func (a LocalhostFirst) Len() int { return len(a) }
func (a LocalhostFirst) Swap(i int, j int) { a[i], a[j] = a[j], a[i] }
func (a LocalhostFirst) Less(i int, j int) bool { return
strings.HasPrefix(a[i], "localhost") }

return sort.Sort(LocalhostFirst(addrs))

https://codereview.appspot.com/100810045/

Revision history for this message
John A Meinel (jameinel) wrote :

On 2014/05/30 14:12:32, wwitzel3 wrote:

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

https://codereview.appspot.com/100810045/diff/40001/state/api/apiclient.go#newcode111
> state/api/apiclient.go:111:
> We should use the built-in sort here.

> http://golang.org/pkg/sort/

> type LocalhostFirst []string

> func (a LocalhostFirst) Len() int { return len(a) }
> func (a LocalhostFirst) Swap(i int, j int) { a[i], a[j] = a[j], a[i] }
> func (a LocalhostFirst) Less(i int, j int) bool { return
strings.HasPrefix(a[i],
> "localhost") }

> return sort.Sort(LocalhostFirst(addrs))

So I think the sort.Sort is actually incorrect, having worked in this
code a bit.

Specifically, this breaks the order for *everything else*. And we
intentionally move the last-successful-connection to the beginning of
the list, so the order isn't random.

So for things like EC2, where you have public and private (etc)
addresses, this now essentially randomizes which one we are going to try
to connect to first. (Actually, it probably sorts the private addresses
first because 10. comes before 172.).

I can see the point of wanting to try localhost first, and in HA mode we
really do want localhost. However, I think what we *really* want is to
check "if JobManageEnviron" then *only* connect to localhost, right?

https://codereview.appspot.com/100810045/

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'agent/agent.go'
2--- agent/agent.go 2014-05-13 04:50:10 +0000
3+++ agent/agent.go 2014-05-30 16:13:04 +0000
4@@ -605,8 +605,24 @@
5 }
6
7 func (c *configInternal) APIInfo() *api.Info {
8+ servingInfo, isStateServer := c.StateServingInfo()
9+ addrs := c.apiDetails.addresses
10+ if isStateServer {
11+ port := servingInfo.APIPort
12+ localApiAddr := fmt.Sprintf("localhost:%d", port)
13+ addrInAddrs := false
14+ for _, addr := range addrs {
15+ if addr == localApiAddr {
16+ addrInAddrs = true
17+ break
18+ }
19+ }
20+ if !addrInAddrs {
21+ addrs = append(addrs, localApiAddr)
22+ }
23+ }
24 return &api.Info{
25- Addrs: c.apiDetails.addresses,
26+ Addrs: addrs,
27 Password: c.apiDetails.password,
28 CACert: c.caCert,
29 Tag: c.tag,
30
31=== modified file 'agent/agent_test.go'
32--- agent/agent_test.go 2014-05-20 04:27:02 +0000
33+++ agent/agent_test.go 2014-05-30 16:13:04 +0000
34@@ -437,13 +437,45 @@
35 c.Assert(reread, jc.DeepEquals, conf)
36 }
37
38+func (*suite) TestAPIInfoAddsLocalhostWhenServingInfoPesent(c *gc.C) {
39+ attrParams := attributeParams
40+ servingInfo := params.StateServingInfo{
41+ Cert: "old cert",
42+ PrivateKey: "old key",
43+ StatePort: 69,
44+ APIPort: 1492,
45+ SharedSecret: "shared",
46+ SystemIdentity: "identity",
47+ }
48+ conf, err := agent.NewStateMachineConfig(attrParams, servingInfo)
49+ c.Assert(err, gc.IsNil)
50+ apiinfo := conf.APIInfo()
51+ c.Check(apiinfo.Addrs, gc.HasLen, len(attrParams.APIAddresses)+1)
52+ localhostAddressFound := false
53+ for _, eachApiAddress := range apiinfo.Addrs {
54+ if eachApiAddress == "localhost:1492" {
55+ localhostAddressFound = true
56+ break
57+ }
58+ }
59+ c.Assert(localhostAddressFound, jc.IsTrue)
60+}
61+
62+func (*suite) TestAPIInfoDoesntAddLocalhostWhenNoServingInfo(c *gc.C) {
63+ attrParams := attributeParams
64+ conf, err := agent.NewAgentConfig(attrParams)
65+ c.Assert(err, gc.IsNil)
66+ apiinfo := conf.APIInfo()
67+ c.Assert(apiinfo.Addrs, gc.DeepEquals, attrParams.APIAddresses)
68+}
69+
70 func (*suite) TestSetPassword(c *gc.C) {
71 attrParams := attributeParams
72 servingInfo := params.StateServingInfo{
73 Cert: "old cert",
74 PrivateKey: "old key",
75- StatePort: 69,
76- APIPort: 47,
77+ StatePort: 1234,
78+ APIPort: 1235,
79 SharedSecret: "shared",
80 SystemIdentity: "identity",
81 }
82
83=== modified file 'cmd/jujud/agent_test.go'
84--- cmd/jujud/agent_test.go 2014-05-28 10:56:55 +0000
85+++ cmd/jujud/agent_test.go 2014-05-30 16:13:04 +0000
86@@ -104,17 +104,22 @@
87 return "old"
88 }
89
90+func (fakeAPIOpenConfig) Jobs() []params.MachineJob {
91+ return []params.MachineJob{}
92+}
93+
94 var _ = gc.Suite(&apiOpenSuite{})
95
96 func (s *apiOpenSuite) TestOpenAPIStateReplaceErrors(c *gc.C) {
97+ type replaceErrors struct {
98+ openErr error
99+ replaceErr error
100+ }
101 var apiError error
102 s.PatchValue(&apiOpen, func(info *api.Info, opts api.DialOpts) (*api.State, error) {
103 return nil, apiError
104 })
105- for i, test := range []struct {
106- openErr error
107- replaceErr error
108- }{{
109+ errReplacePairs := []replaceErrors{{
110 fmt.Errorf("blah"), nil,
111 }, {
112 openErr: &params.Error{Code: params.CodeNotProvisioned},
113@@ -122,7 +127,8 @@
114 }, {
115 openErr: &params.Error{Code: params.CodeUnauthorized},
116 replaceErr: worker.ErrTerminateAgent,
117- }} {
118+ }}
119+ for i, test := range errReplacePairs {
120 c.Logf("test %d", i)
121 apiError = test.openErr
122 _, _, err := openAPIState(fakeAPIOpenConfig{}, nil)
123
124=== modified file 'state/api/apiclient.go'
125--- state/api/apiclient.go 2014-04-30 23:18:40 +0000
126+++ state/api/apiclient.go 2014-05-30 16:13:04 +0000
127@@ -8,6 +8,8 @@
128 "crypto/x509"
129 "fmt"
130 "io"
131+ "sort"
132+ "strings"
133 "time"
134
135 "code.google.com/p/go.net/websocket"
136@@ -108,6 +110,18 @@
137 }
138 }
139
140+type LocalFirst []string
141+
142+func (l LocalFirst) Len() int {
143+ return len(l)
144+}
145+func (l LocalFirst) Swap(i, j int) {
146+ l[i], l[j] = l[j], l[i]
147+}
148+func (l LocalFirst) Less(i, j int) bool {
149+ return strings.HasPrefix(l[i], "localhost") && !strings.HasPrefix(l[j], "localhost")
150+}
151+
152 func Open(info *Info, opts DialOpts) (*State, error) {
153 if len(info.Addrs) == 0 {
154 return nil, fmt.Errorf("no API addresses to connect to")
155@@ -122,7 +136,10 @@
156 // Dial all addresses at reasonable intervals.
157 try := parallel.NewTry(0, nil)
158 defer try.Kill()
159- for _, addr := range info.Addrs {
160+ var addrs []string
161+ addrs = append(addrs, info.Addrs...)
162+ sort.Sort(LocalFirst(addrs))
163+ for _, addr := range addrs {
164 err := dialWebsocket(addr, opts, pool, try)
165 if err == parallel.ErrStopped {
166 break
167
168=== modified file 'state/api/apiclient_test.go'
169--- state/api/apiclient_test.go 2014-04-02 09:43:39 +0000
170+++ state/api/apiclient_test.go 2014-05-30 16:13:04 +0000
171@@ -6,6 +6,7 @@
172 import (
173 "io"
174 "net"
175+ "sort"
176
177 gc "launchpad.net/gocheck"
178
179@@ -20,6 +21,85 @@
180
181 var _ = gc.Suite(&apiclientSuite{})
182
183+func (s *apiclientSuite) TestSortLocalhost(c *gc.C) {
184+ addrs := []string{
185+ "notlocalhost1",
186+ "notlocalhost2",
187+ "notlocalhost3",
188+ "localhost1",
189+ "localhost2",
190+ "localhost3",
191+ }
192+ expectedAddrs := []string{
193+ "localhost1",
194+ "localhost2",
195+ "localhost3",
196+ "notlocalhost1",
197+ "notlocalhost2",
198+ "notlocalhost3",
199+ }
200+ var sortedAddrs []string
201+ sortedAddrs = append(sortedAddrs, addrs...)
202+ sort.Sort(api.LocalFirst(sortedAddrs))
203+ c.Assert(addrs, gc.Not(gc.DeepEquals), sortedAddrs)
204+ c.Assert(sortedAddrs, gc.HasLen, 6)
205+ c.Assert(sortedAddrs, gc.DeepEquals, expectedAddrs)
206+
207+}
208+
209+func (s *apiclientSuite) TestSortLocalhostIdempotent(c *gc.C) {
210+ addrs := []string{
211+ "localhost1",
212+ "localhost2",
213+ "localhost3",
214+ "notlocalhost1",
215+ "notlocalhost2",
216+ "notlocalhost3",
217+ }
218+ expectedAddrs := []string{
219+ "localhost1",
220+ "localhost2",
221+ "localhost3",
222+ "notlocalhost1",
223+ "notlocalhost2",
224+ "notlocalhost3",
225+ }
226+ var sortedAddrs []string
227+ sortedAddrs = append(sortedAddrs, addrs...)
228+ sort.Sort(api.LocalFirst(sortedAddrs))
229+ c.Assert(sortedAddrs, gc.DeepEquals, expectedAddrs)
230+
231+}
232+
233+func (s *apiclientSuite) TestOpenPrefersLocalhostIfPresent(c *gc.C) {
234+ // Create a socket that proxies to the API server though our localhost address.
235+ info := s.APIInfo(c)
236+ serverAddr := info.Addrs[0]
237+ server, err := net.Dial("tcp", serverAddr)
238+ c.Assert(err, gc.IsNil)
239+ defer server.Close()
240+ listener, err := net.Listen("tcp", "localhost:26104")
241+ c.Assert(err, gc.IsNil)
242+ defer listener.Close()
243+ go func() {
244+ for {
245+ client, err := listener.Accept()
246+ if err != nil {
247+ return
248+ }
249+ go io.Copy(client, server)
250+ go io.Copy(server, client)
251+ }
252+ }()
253+
254+ // Check that we are using our working address to connect
255+ info.Addrs = []string{"fakeAddress:1", "fakeAddress:1", "localhost:26104"}
256+ st, err := api.Open(info, api.DialOpts{})
257+ c.Assert(err, gc.IsNil)
258+ defer st.Close()
259+ c.Assert(st.Addr(), gc.Equals, "localhost:26104")
260+}
261+
262 func (s *apiclientSuite) TestOpenMultiple(c *gc.C) {
263 // Create a socket that proxies to the API server.
264 info := s.APIInfo(c)

Subscribers

People subscribed via source and target branches

to status/vote changes: