Merge lp:~bjornt/landscape-charm/package-upload-leader-change into lp:~landscape/landscape-charm/trunk
- package-upload-leader-change
- Merge into trunk
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 | ||||
Related bugs: |
|
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.
🤖 Landscape Builder (landscape-builder) wrote : | # |
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 TwoLandscapeUni
"""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 OneLandscapeUni
and wait for it to be fully functional. The very same unit will be removed upon
layer tearDown.
"""
== tests/basic/
class LeaderTest(
layer = TwoLandscapeUni
def test_package_
"""Test that package upload requests are routed to the leader unit."""
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"
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 TwoLandscapeUni
>
> """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
> OneLandscapeUni
> and wait for it to be fully functional. The very same unit will be removed
> upon
> layer tearDown.
> """
>
> == tests/basic/
>
> class LeaderTest(
>
> layer = TwoLandscapeUni
>
> def test_package_
> """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.
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.
Björn Tillenius (bjornt) wrote : | # |
I filed bug 1461816 for adding the integration tests.
🤖 Landscape Builder (landscape-builder) wrote : | # |
Command: make ci-test
Result: Success
Revno: 307
Branch: lp:~bjornt/landscape-charm/package-upload-leader-change
Jenkins: https:/
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 TwoLandscapeUni
>>
>>"""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
>>OneLandscap
>>and wait for it to be fully functional. The very same unit will be removed
>>upon
>>layer tearDown.
>>"""
>>
>>== tests/basic/
>>
>>class LeaderTest(
>>
>>layer = TwoLandscapeUni
>>
>>def test_package_
>>"""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 OneLandscapeUni
"""will run all tests with a fresh single unit (landscape-
class TwoLandscapeUni
"""will run all tests with two units (landscape-
class LeaderUnitChang
"""will remove landscape-server/0 and we'll be left with landscape-
And probably drop the OneLandscapeUni
OneLandscapeUni
logic to just relevant setUpClass/
are used just by one test class).
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.
- 308. By Björn Tillenius
-
Add comment.
- 309. By Björn Tillenius
-
Remove is_leader parameter.
- 310. By Björn Tillenius
-
Remove unused method.
Björn Tillenius (bjornt) wrote : | # |
Replied to the inline comments.
🤖 Landscape Builder (landscape-builder) wrote : | # |
Command: make ci-test
Result: Success
Revno: 310
Branch: lp:~bjornt/landscape-charm/package-upload-leader-change
Jenkins: https:/
Preview Diff
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) |
Command: make ci-test /ci.lscape. net/job/ latch-test/ 1171/
Result: Success
Revno: 306
Branch: lp:~bjornt/landscape-charm/package-upload-leader-change
Jenkins: https:/