Merge lp:~bjornt/landscape-charm/package-upload-leader-change into lp:~landscape/landscape-charm/trunk

Proposed by Björn Tillenius
Status: Merged
Approved by: Björn Tillenius
Approved revision: 310
Merged at revision: 301
Proposed branch: lp:~bjornt/landscape-charm/package-upload-leader-change
Merge into: lp:~landscape/landscape-charm/trunk
Prerequisite: lp:~free.ekanayaka/landscape-charm/new-data-provider-flow
Diff against target: 358 lines (+111/-38)
6 files modified
hooks/leader-elected (+9/-0)
lib/relations/haproxy.py (+9/-8)
lib/relations/tests/test_haproxy.py (+71/-9)
lib/services.py (+3/-6)
lib/tests/stubs.py (+4/-9)
lib/tests/test_services.py (+15/-6)
To merge this branch: bzr merge lp:~bjornt/landscape-charm/package-upload-leader-change
Reviewer Review Type Date Requested Status
🤖 Landscape Builder test results Approve
Free Ekanayaka (community) Approve
Adam Collard (community) Approve
Review via email: mp+260942@code.launchpad.net

Commit message

Don't make haproxy break when the landscape-server leader is removed.

If you would have multiple landscape-server units and the leader would
go away, the haproxy config would break. The reason was the
package-upload config was always present in the haproxy config, but the
actual backend was only configured by the leader.

Description of the change

Don't make haproxy break when the landscape-server leader is removed.

If you would have multiple landscape-server units and the leader would
go away, the haproxy config would break. The reason was the
package-upload config was always present in the haproxy config, but the
actual backend was only configured by the leader.

I changed it so that the leader not only adds the backend, but also adds
the package-upload config that references the backend.

I change our code to use the new leader election API, since
the is_elected_leader() function is a bit buggy.

To post a comment you must log in.
Revision history for this message
🤖 Landscape Builder (landscape-builder) wrote :

Command: make ci-test
Result: Success
Revno: 306
Branch: lp:~bjornt/landscape-charm/package-upload-leader-change
Jenkins: https://ci.lscape.net/job/latch-test/1171/

review: Approve (test results)
Revision history for this message
Free Ekanayaka (free.ekanayaka) wrote :

Looks good to me, marking as Needs information because I'd like to ask what do you think of adding some integration tests too.

It could be something like:

== tests/layers.py ==

class TwoLandscapeUnitsLayer(object):

    """Layer for tests meant to run against a deployment with two Landscape units.

    This layer will just add one unit on top the deployment from OneLandscapeUnitLayer,
    and wait for it to be fully functional. The very same unit will be removed upon
    layer tearDown.
    """

== tests/basic/test_leader.py ==

class LeaderTest(IntegrationTest):

   layer = TwoLandscapeUnitsLayer

   def test_package_upload(self):
      """Test that package upload requests are routed to the leader unit."""

review: Needs Information
Revision history for this message
Adam Collard (adam-collard) wrote :

Is there a bug for this? Please link and provide steps for testing this, perhaps with the integration test Free suggested it would be easy to reproduce "live"

review: Needs Information
Revision history for this message
Björn Tillenius (bjornt) wrote :

> Looks good to me, marking as Needs information because I'd like to ask what do
> you think of adding some integration tests too.
>
> It could be something like:
>
> == tests/layers.py ==
>
> class TwoLandscapeUnitsLayer(object):
>
> """Layer for tests meant to run against a deployment with two Landscape
> units.
>
> This layer will just add one unit on top the deployment from
> OneLandscapeUnitLayer,
> and wait for it to be fully functional. The very same unit will be removed
> upon
> layer tearDown.
> """
>
> == tests/basic/test_leader.py ==
>
> class LeaderTest(IntegrationTest):
>
> layer = TwoLandscapeUnitsLayer
>
> def test_package_upload(self):
> """Test that package upload requests are routed to the leader unit."""

Agreed that integration tests would be good, but I'd rather do that in a separate
branch. Especially since the test you're proposing is for existing functionality,
not for something I introduced in this branch.

But I also would like to add an integration test for when the leader is destroyed,
but that's not trivial, since it's destructive. So a separate branch for that
makes more sense to me.

Revision history for this message
Björn Tillenius (bjornt) wrote :

> Is there a bug for this? Please link and provide steps for testing this,
> perhaps with the integration test Free suggested it would be easy to reproduce
> "live"

Sorry, I forgot to link the bug. It includes an explanation of the problem. But
basically:

  make deploy
  juju add-unit landscape-server
  <wait until the new unit has joined the cluster>
  juju destroy-unit landscape-server/0

307. By Björn Tillenius

Merge trunk.

Revision history for this message
Björn Tillenius (bjornt) wrote :

I filed bug 1461816 for adding the integration tests.

Revision history for this message
🤖 Landscape Builder (landscape-builder) wrote :

Command: make ci-test
Result: Success
Revno: 307
Branch: lp:~bjornt/landscape-charm/package-upload-leader-change
Jenkins: https://ci.lscape.net/job/latch-test/1180/

review: Approve (test results)
Revision history for this message
Free Ekanayaka (free.ekanayaka) wrote :

|--==> On Thu, 04 Jun 2015 07:07:44 -0000, Björn Tillenius <email address hidden> said:

  >>Looks good to me, marking as Needs information because I'd like to ask what do
  >>you think of adding some integration tests too.
  >>
  >>It could be something like:
  >>
  >>== tests/layers.py ==
  >>
  >>class TwoLandscapeUnitsLayer(object):
  >>
  >>"""Layer for tests meant to run against a deployment with two Landscape
  >>units.
  >>
  >>This layer will just add one unit on top the deployment from
  >>OneLandscapeUnitLayer,
  >>and wait for it to be fully functional. The very same unit will be removed
  >>upon
  >>layer tearDown.
  >>"""
  >>
  >>== tests/basic/test_leader.py ==
  >>
  >>class LeaderTest(IntegrationTest):
  >>
  >>layer = TwoLandscapeUnitsLayer
  >>
  >>def test_package_upload(self):
  >>"""Test that package upload requests are routed to the leader unit."""

  > Agreed that integration tests would be good, but I'd rather do that in a separate
  > branch. Especially since the test you're proposing is for existing functionality,
  > not for something I introduced in this branch.

Works for me, I was suggesting it since the branch is relatively small
and the integration tests would be strictly related. Not blocking tho.

  > But I also would like to add an integration test for when the leader is destroyed,
  > but that's not trivial, since it's destructive. So a separate branch for that
  > makes more sense to me.

Yes, I think this can be made as a separate layer, e.g.:

class OneLandscapeUnitLayer(object):
    """will run all tests with a fresh single unit (landscape-server/0)"""

class TwoLandscapeUnitsLayer(OneLandscapeUnitLayer):
    """will run all tests with two units (landscape-server/{0,1})"""

class LeaderUnitChangeLayer(TwoLandscapeUnitsLayer):
    """will remove landscape-server/0 and we'll be left with landscape-server/1"""

And probably drop the OneLandscapeUnitNoCronLayer and
OneLandscapeUnitCustomSSLCertificateLayer layers, moving that
logic to just relevant setUpClass/tearDownClass (since those layers
are used just by one test class).

Revision history for this message
Adam Collard (adam-collard) wrote :

I share Free's concern about the (seemingly) left-over code, but you can deal with that on his review. Tested as described and all looks good, confirmed with a GET to /upload after killing the leader that it was routed through properly.

review: Approve
308. By Björn Tillenius

Add comment.

309. By Björn Tillenius

Remove is_leader parameter.

310. By Björn Tillenius

Remove unused method.

Revision history for this message
Björn Tillenius (bjornt) wrote :

Replied to the inline comments.

Revision history for this message
Free Ekanayaka (free.ekanayaka) wrote :

+1

review: Approve
Revision history for this message
🤖 Landscape Builder (landscape-builder) wrote :

Command: make ci-test
Result: Success
Revno: 310
Branch: lp:~bjornt/landscape-charm/package-upload-leader-change
Jenkins: https://ci.lscape.net/job/latch-test/1187/

review: Approve (test results)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== added file 'hooks/leader-elected'
2--- hooks/leader-elected 1970-01-01 00:00:00 +0000
3+++ hooks/leader-elected 2015-06-04 09:40:39 +0000
4@@ -0,0 +1,9 @@
5+#!/usr/bin/python
6+import sys
7+
8+from lib.services import ServicesHook
9+
10+
11+if __name__ == "__main__":
12+ hook = ServicesHook()
13+ sys.exit(hook())
14
15=== modified file 'lib/relations/haproxy.py'
16--- lib/relations/haproxy.py 2015-05-25 09:11:59 +0000
17+++ lib/relations/haproxy.py 2015-06-04 09:40:39 +0000
18@@ -37,10 +37,8 @@
19 "http-request set-header X-Forwarded-Proto https",
20 "acl message path_beg -i /message-system",
21 "acl api path_beg -i /api",
22- "acl package-upload path_beg -i /upload",
23 "use_backend landscape-message if message",
24 "use_backend landscape-api if api",
25- "use_backend landscape-package-upload if package-upload",
26 ],
27 }
28 SERVER_BASE_PORTS = {
29@@ -73,12 +71,10 @@
30 interface = "http"
31 required_keys = ["services"]
32
33- def __init__(self, service_counts, hookenv=hookenv, paths=default_paths,
34- is_leader=False):
35+ def __init__(self, service_counts, hookenv=hookenv, paths=default_paths):
36 self._hookenv = hookenv
37 self._service_counts = service_counts
38 self._paths = paths
39- self._is_leader = is_leader
40 super(HAProxyProvider, self).__init__()
41
42 def provide_data(self):
43@@ -100,14 +96,18 @@
44 def _get_https(self):
45 """Return the service configuration for the HTTPS frontend."""
46
47+ service = self._get_service("https")
48 backends = [
49 self._get_backend("message", self._get_servers("message-server")),
50 self._get_backend("api", self._get_servers("api")),
51 ]
52- if self._is_leader:
53+ if self._hookenv.is_leader():
54 self._hookenv.log(
55 "This unit is the juju leader: Writing package-upload backends"
56 " entry.")
57+ service["service_options"].extend([
58+ "acl package-upload path_beg -i /upload",
59+ "use_backend landscape-package-upload if package-upload"])
60 backends.append(
61 self._get_backend(
62 "package-upload", self._get_servers("package-upload")))
63@@ -116,7 +116,6 @@
64 "This unit is not the juju leader: not writing package-upload"
65 " backends entry.")
66
67- service = self._get_service("https")
68 service.update({
69 "crts": self._get_ssl_certificate(),
70 "servers": self._get_servers("appserver"),
71@@ -136,7 +135,9 @@
72 "service_name": "landscape-%s" % name,
73 "service_host": "0.0.0.0",
74 "service_port": SERVICE_PORTS[name],
75- "service_options": SERVICE_OPTIONS[name],
76+ # Copy the service options, since it will be modified if
77+ # we're the leader.
78+ "service_options": SERVICE_OPTIONS[name][:],
79 "errorfiles": self._get_error_files()
80 }
81
82
83=== modified file 'lib/relations/tests/test_haproxy.py'
84--- lib/relations/tests/test_haproxy.py 2015-06-02 09:21:44 +0000
85+++ lib/relations/tests/test_haproxy.py 2015-06-04 09:40:39 +0000
86@@ -38,8 +38,9 @@
87 The HAProxyProvider class feeds haproxy with the services that this
88 Landscape unit runs. By default all services are run.
89 """
90+ self.hookenv.leader = False
91 relation = HAProxyProvider(
92- SAMPLE_SERVICE_COUNT_DATA, paths=self.paths)
93+ SAMPLE_SERVICE_COUNT_DATA, paths=self.paths, hookenv=self.hookenv)
94
95 # Provide some fake ssl-cert and ssl-key config entries
96 config = self.hookenv.config()
97@@ -96,10 +97,8 @@
98 "http-request set-header X-Forwarded-Proto https",
99 "acl message path_beg -i /message-system",
100 "acl api path_beg -i /api",
101- "acl package-upload path_beg -i /upload",
102 "use_backend landscape-message if message",
103- "use_backend landscape-api if api",
104- "use_backend landscape-package-upload if package-upload"],
105+ "use_backend landscape-api if api"],
106 "errorfiles": expected_errorfiles,
107 "crts": expected_certs,
108 "servers": [
109@@ -137,7 +136,7 @@
110 fd.write("{} error page".format(name))
111
112 relation = HAProxyProvider(
113- SAMPLE_SERVICE_COUNT_DATA, paths=self.paths)
114+ SAMPLE_SERVICE_COUNT_DATA, paths=self.paths, hookenv=self.hookenv)
115
116 data = relation.provide_data()
117 services = yaml.safe_load(data["services"])
118@@ -158,17 +157,80 @@
119 # Create an empty root tree
120 temp_dir = self.useFixture(TempDir())
121 provider = HAProxyProvider(
122- SAMPLE_SERVICE_COUNT_DATA, paths=Paths(temp_dir.path))
123+ SAMPLE_SERVICE_COUNT_DATA, paths=Paths(temp_dir.path),
124+ hookenv=self.hookenv)
125
126 self.assertRaises(HookError, provider.provide_data)
127
128+ def test_provide_data_package_upload_leader(self):
129+ """
130+ If the unit is a leader, package-upload config is provided for
131+ the https service, but not for http.
132+ """
133+ self.hookenv.leader = True
134+ relation = HAProxyProvider(
135+ SAMPLE_SERVICE_COUNT_DATA, paths=self.paths, hookenv=self.hookenv)
136+
137+ data = relation.provide_data()
138+
139+ [http, https] = yaml.safe_load(data["services"])
140+ self.assertNotIn(
141+ "acl package-upload path_beg -i /upload",
142+ http["service_options"])
143+ self.assertNotIn(
144+ "use_backend landscape-package-upload if package-upload",
145+ http["service_options"])
146+ self.assertNotIn(
147+ "landscape-package-upload",
148+ [backend["backend_name"] for backend in http["backends"]])
149+ self.assertIn(
150+ "acl package-upload path_beg -i /upload",
151+ https["service_options"])
152+ self.assertIn(
153+ "use_backend landscape-package-upload if package-upload",
154+ https["service_options"])
155+ self.assertIn(
156+ "landscape-package-upload",
157+ [backend["backend_name"] for backend in https["backends"]])
158+
159+ def test_provide_data_package_upload_no_leader(self):
160+ """
161+ If the unit is not a leader, package-upload config isn't
162+ provided for neither the https nor http services.
163+ """
164+ self.hookenv.leader = False
165+ relation = HAProxyProvider(
166+ SAMPLE_SERVICE_COUNT_DATA, paths=self.paths, hookenv=self.hookenv)
167+
168+ data = relation.provide_data()
169+
170+ [http, https] = yaml.safe_load(data["services"])
171+ self.assertNotIn(
172+ "acl package-upload path_beg -i /upload",
173+ http["service_options"])
174+ self.assertNotIn(
175+ "use_backend landscape-package-upload if package-upload",
176+ http["service_options"])
177+ self.assertNotIn(
178+ "landscape-package-upload",
179+ [backend["backend_name"] for backend in http["backends"]])
180+ self.assertNotIn(
181+ "acl package-upload path_beg -i /upload",
182+ https["service_options"])
183+ self.assertNotIn(
184+ "use_backend landscape-package-upload if package-upload",
185+ https["service_options"])
186+ self.assertNotIn(
187+ "landscape-package-upload",
188+ [backend["backend_name"] for backend in https["backends"]])
189+
190 def test_default_ssl_cert_is_used_without_config_keys(self):
191 """
192 If no "ssl-cert" is specified, the provide_data method returns
193 ["DEFAULT"] for the HAproxy SSL cert.
194 """
195 provider = HAProxyProvider(
196- SAMPLE_SERVICE_COUNT_DATA, paths=self.paths)
197+ SAMPLE_SERVICE_COUNT_DATA, paths=self.paths, hookenv=self.hookenv)
198 data = provider.provide_data()
199 services = yaml.safe_load(data["services"])
200
201@@ -258,9 +320,9 @@
202 The landscape service leader writes a server entry in the
203 landscape-package-upload backend.
204 """
205+ self.hookenv.leader = True
206 provider = HAProxyProvider(
207- SAMPLE_SERVICE_COUNT_DATA, paths=self.paths, hookenv=self.hookenv,
208- is_leader=True)
209+ SAMPLE_SERVICE_COUNT_DATA, paths=self.paths, hookenv=self.hookenv)
210
211 data = provider.provide_data()
212 services = yaml.safe_load(data["services"])
213
214=== modified file 'lib/services.py'
215--- lib/services.py 2015-06-03 12:23:06 +0000
216+++ lib/services.py 2015-06-04 09:40:39 +0000
217@@ -5,7 +5,6 @@
218 from charmhelpers.core import host
219 from charmhelpers.core.services.base import ServiceManager
220 from charmhelpers.core.services.helpers import render_template
221-from charmhelpers.contrib.hahelpers import cluster
222
223 from lib.hook import Hook
224 from lib.paths import default_paths
225@@ -36,11 +35,10 @@
226 all relation data we need in order to configure this Landscape unit, and
227 proceed with the configuration if ready.
228 """
229- def __init__(self, hookenv=hookenv, cluster=cluster, host=host,
230+ def __init__(self, hookenv=hookenv, host=host,
231 subprocess=subprocess, paths=default_paths, fetch=fetch):
232 super(ServicesHook, self).__init__(hookenv=hookenv)
233 self._hookenv = hookenv
234- self._cluster = cluster
235 self._host = host
236 self._paths = paths
237 self._subprocess = subprocess
238@@ -48,7 +46,7 @@
239
240 def _run(self):
241 leader_context = None
242- is_leader = self._cluster.is_elected_leader(None)
243+ is_leader = self._hookenv.is_leader()
244 if is_leader:
245 leader_context = LandscapeLeaderContext(
246 host=self._host, hookenv=self._hookenv)
247@@ -58,8 +56,7 @@
248 "ports": [],
249 "provided_data": [
250 LandscapeProvider(leader_context),
251- HAProxyProvider(
252- SERVICE_COUNTS, paths=self._paths, is_leader=is_leader),
253+ HAProxyProvider(SERVICE_COUNTS, paths=self._paths),
254 RabbitMQProvider(),
255 ],
256 # Required data is available to the render_template calls below.
257
258=== modified file 'lib/tests/stubs.py'
259--- lib/tests/stubs.py 2015-06-02 07:06:30 +0000
260+++ lib/tests/stubs.py 2015-06-04 09:40:39 +0000
261@@ -10,6 +10,7 @@
262 hook = "some-hook"
263 unit = "landscape-server/0"
264 relid = None
265+ leader = True
266
267 def __init__(self, charm_dir):
268 self.messages = []
269@@ -68,6 +69,9 @@
270 def action_fail(self, message):
271 self._action_fails.append(message)
272
273+ def is_leader(self):
274+ return self.leader
275+
276
277 class FetchStub(object):
278 """Provide a testable stub for C{charmhelpers.fetch}."""
279@@ -92,15 +96,6 @@
280 self.installed.append((packages, options, fatal))
281
282
283-class ClusterStub(object):
284- """Testable stub for C{charmhelpers.contrib.hahelpers.cluster}."""
285-
286- leader = True
287-
288- def is_elected_leader(self, resource):
289- return self.leader
290-
291-
292 class HostStub(object):
293 """Testable stub for C{charmhelpers.core.host}."""
294
295
296=== modified file 'lib/tests/test_services.py'
297--- lib/tests/test_services.py 2015-06-03 12:23:06 +0000
298+++ lib/tests/test_services.py 2015-06-04 09:40:39 +0000
299@@ -5,7 +5,7 @@
300 from charmhelpers.core import templating
301
302 from lib.tests.helpers import HookenvTest
303-from lib.tests.stubs import ClusterStub, HostStub, SubprocessStub, FetchStub
304+from lib.tests.stubs import HostStub, SubprocessStub, FetchStub
305 from lib.tests.sample import (
306 SAMPLE_DB_UNIT_DATA, SAMPLE_LEADER_CONTEXT_DATA, SAMPLE_AMQP_UNIT_DATA,
307 SAMPLE_CONFIG_LICENSE_DATA, SAMPLE_CONFIG_OPENID_DATA, SAMPLE_HOSTED_DATA,
308@@ -21,7 +21,6 @@
309
310 def setUp(self):
311 super(ServicesHookTest, self).setUp()
312- self.cluster = ClusterStub()
313 self.host = HostStub()
314 self.subprocess = SubprocessStub()
315 self.subprocess.add_fake_executable(SCHEMA_SCRIPT)
316@@ -31,8 +30,8 @@
317 self.root_dir = self.useFixture(RootDir())
318 self.fetch = FetchStub()
319 self.hook = ServicesHook(
320- hookenv=self.hookenv, cluster=self.cluster, host=self.host,
321- subprocess=self.subprocess, paths=self.paths, fetch=self.fetch)
322+ hookenv=self.hookenv, host=self.host, subprocess=self.subprocess,
323+ paths=self.paths, fetch=self.fetch)
324
325 # XXX Monkey patch the templating API, charmhelpers doesn't sport
326 # any dependency injection here as well.
327@@ -175,7 +174,7 @@
328 If we're not the leader unit and we didn't yet get relation data from
329 the leader, we are not ready.
330 """
331- self.cluster.leader = False
332+ self.hookenv.leader = False
333 self.hook()
334 self.assertIn(
335 ("Incomplete relation: LandscapeRequirer", "DEBUG"),
336@@ -186,7 +185,7 @@
337 If we're not the leader unit and we got relation data from the leader,
338 along with the rest of required relations, then we're good.
339 """
340- self.cluster.leader = False
341+ self.hookenv.leader = False
342 self.hookenv.relations.update({
343 "cluster": {
344 "cluster:1": {
345@@ -232,3 +231,13 @@
346 self.hook()
347 data = yaml.load(self.hookenv.relations["website:1"]["services"])
348 self.assertIsNotNone(data)
349+
350+ def test_leader_elected(self):
351+ """
352+ When a leader is elected, the ServicesHook sets the haproxy
353+ relation data.
354+ """
355+ self.hookenv.hook = "leader-elected"
356+ self.hook()
357+ data = yaml.load(self.hookenv.relations["website:1"]["services"])
358+ self.assertIsNotNone(data)

Subscribers

People subscribed via source and target branches