Merge lp:charms/haproxy into lp:~matthias-cramer/charms/precise/haproxy/appsession
- Precise Pangolin (12.04)
- trunk
- Merge into appsession
Status: | Rejected |
---|---|
Rejected by: | Matthias Cramer |
Proposed branch: | lp:charms/haproxy |
Merge into: | lp:~matthias-cramer/charms/precise/haproxy/appsession |
Diff against target: |
806 lines (+606/-87) (has conflicts) 4 files modified
README.md (+119/-0) hooks/hooks.py (+220/-87) hooks/test_hooks.py (+263/-0) revision (+4/-0) Text conflict in hooks/hooks.py Text conflict in revision |
To merge this branch: | bzr merge lp:charms/haproxy |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Matthias Cramer | Pending | ||
Review via email: mp+148473@code.launchpad.net |
Commit message
Description of the change
Added config parameter listen_appsession to set appsession parameter
example: Added config parameter listen_appsession to set appsession parameter
- 64. By Juan L. Negron
-
Merging LP:148065 Relation driven proxying or multi-service proxying
- 65. By Juan L. Negron
-
When the reverseproxy relation changes, the website proxy will likely need to be updated, so trigger a config-changed to notify the other side of the relation that it will need to update.
I'm not sure if this the appropriate way to handle this situation, but in practice it seems to work. Please let me know if there is a more accurate or correct way to do it. LP:148605
Unmerged revisions
- 85. By Cory Johns
-
[jacekn, m=johnsca] SSL support and monitoring improvements
- 84. By Matt Bruzek
-
[mbruzek] Adding python-virtualenv and -y to the Makefile.
- 83. By Matt Bruzek
-
[tvansteenburgh] Fix proof errors and 'make test'.
- 82. By José Antonio Rey
-
Charles Butler 2014-08-30 Adds python-yaml as a dependency before invoking the charmhelpers based python install hook
- 81. By JuanJo Ciarlante
-
[james-w, r=jjo] Fix the queue depth monitor script.
- 80. By Charles Butler
-
James Westby 2014-05-29 Call install_hook() on upgrade-charm.
- 79. By David Ames
-
[james_w,r=dames] Allow sending haproxy metrics to external statsd service.
- 78. By Marco Ceppi
-
[tvansteenburgh] Enable sticky sessions by default
- 77. By Marco Ceppi
-
"Add revision file"
- 76. By Matt Bruzek
-
Added amulet tests for haproxy charm.
Preview Diff
1 | === added file 'README.md' |
2 | --- README.md 1970-01-01 00:00:00 +0000 |
3 | +++ README.md 2013-02-15 04:57:24 +0000 |
4 | @@ -0,0 +1,119 @@ |
5 | +Juju charm haproxy |
6 | +================== |
7 | + |
8 | +HAProxy is a free, very fast and reliable solution offering high availability, |
9 | +load balancing, and proxying for TCP and HTTP-based applications. It is |
10 | +particularly suited for web sites crawling under very high loads while needing |
11 | +persistence or Layer7 processing. Supporting tens of thousands of connections |
12 | +is clearly realistic with todays hardware. Its mode of operation makes its |
13 | +integration into existing architectures very easy and riskless, while still |
14 | +offering the possibility not to expose fragile web servers to the Net. |
15 | + |
16 | +How to deploy the charm |
17 | +----------------------- |
18 | + juju deploy haproxy |
19 | + juju deploy my-web-app |
20 | + juju add-relation my-web-app:website haproxy:reverseproxy |
21 | + juju add-unit my-web-app |
22 | + ... |
23 | + |
24 | +Reverseproxy Relation |
25 | +--------------------- |
26 | + |
27 | +The reverse proxy relation is used to distribute connections from one frontend |
28 | +port to many backend services (typically different Juju _units_). You can use |
29 | +haproxy just like this, but typically in a production service you would |
30 | +frontend this service with apache2 to handle the SSL negotiation, etc. See |
31 | +the "Website Relation" section for more information about that. |
32 | + |
33 | +When your charm hooks into reverseproxy you have two general approaches |
34 | +which can be used to notify haproxy about what services you are running. |
35 | +1) Single-service proxying or 2) Multi-service or relation-driven proxying. |
36 | + |
37 | +** 1) Single-Service Proxying ** |
38 | + |
39 | +In this case, your website relation will join underneath a single `listen` |
40 | +stanza in haproxy. This stanza will have one `service` entry for each unit |
41 | +connecting. By convention, this is typically called "website". The |
42 | +following is an example of a relation-joined or changed hook: |
43 | + |
44 | + #!/bin/bash |
45 | + # hooks/website-relation-joined |
46 | + |
47 | + relation-set "hostname=$(unit-get private-address)" |
48 | + relation-set "port=80" |
49 | + |
50 | + # Set an optional service name, allowing more config-based |
51 | + # customization |
52 | + relation-set "service_name=my_web_app" |
53 | + |
54 | +If you set the `service_name` relation setting, the configuration `services` |
55 | +yaml mapping will be consulted to lookup 3 other options based on service |
56 | +name. |
57 | + |
58 | + * `{service_name}_servers` - sets the `server` line in the listen stanza |
59 | + explicitly. |
60 | + * `{service_name}_server_options` - Will append to the charm-generated |
61 | + server line for for each joining unit in the reverseproxy relation. |
62 | + * `{service_name}_service_options` - expected to be a list of strings. Will |
63 | + set each item as an option under the listen stanza. |
64 | + |
65 | + |
66 | +** 2) Relation-Driven Proxying ** |
67 | + |
68 | +In this relation style, your charm should specify these relation settings |
69 | +directly as relation variables when joining reverseproxy. Your charm's |
70 | +website-relation-changed hook would look something like this: |
71 | + |
72 | + #!/bin/bash |
73 | + # hooks/website-relation-changed |
74 | + |
75 | + host=$(unit-get private-address) |
76 | + port=80 |
77 | + |
78 | + relation-set "services= |
79 | + - { service_name: my_web_app, |
80 | + service_options: [mode http, balance leastconn], |
81 | + servers: [[my_web_app_1, $host, $port, option httpchk GET / HTTP/1.0], |
82 | + [... optionally more servers here ...]]} |
83 | + - { ... optionally more services here ... } |
84 | + " |
85 | + |
86 | +Once set, haproxy will union multiple `servers` stanzas from any units |
87 | +joining with the same `service_name` under one listen stanza. |
88 | +`service-options` and `server_options` will be overwritten, so ensure they |
89 | +are set uniformly on all services with the same name. |
90 | + |
91 | +Website Relation |
92 | +---------------- |
93 | + |
94 | +The website relation is the other side of haproxy. It can communicate with |
95 | +charms written like apache2 that can act as a front-end for haproxy to take of |
96 | +things like ssl encryption. When joining a service like apache2 on its |
97 | +reverseproxy relation, haproxy's website relation will set an `all_services` |
98 | +varaible that conforms to the spec layed out in the apache2 charm. |
99 | + |
100 | +These settings can then be used when crafting your vhost template to make sure |
101 | +traffic goes to the correct haproxy listener which will in turn forward the |
102 | +traffic to the correct backend server/port |
103 | + |
104 | +Configuration |
105 | +------------- |
106 | +Many of the haproxy settings can be altered via the standard juju configuration |
107 | +settings. Please see the config.yaml file as each is fairly clearly documented. |
108 | + |
109 | +Testing |
110 | +------- |
111 | +This charm has a simple unit-test program. Please expand it and make sure new |
112 | +changes are covered by simple unit tests. To run the unit tests: |
113 | + |
114 | + sudo apt-get install python-mocker |
115 | + sudo apt-get install python-twisted-core |
116 | + cd hooks; trial test_hooks |
117 | + |
118 | +TODO: |
119 | +----- |
120 | + |
121 | + * Expand Single-Service section as I have not tested that mode fully. |
122 | + * Trigger website-relation-changed when the reverse-proxy relation changes |
123 | + |
124 | |
125 | === modified file 'hooks/hooks.py' |
126 | --- hooks/hooks.py 2013-02-14 15:05:42 +0000 |
127 | +++ hooks/hooks.py 2013-02-15 04:57:24 +0000 |
128 | @@ -11,6 +11,7 @@ |
129 | import sys |
130 | import yaml |
131 | import nrpe |
132 | +import time |
133 | |
134 | |
135 | ############################################################################### |
136 | @@ -19,12 +20,23 @@ |
137 | default_haproxy_config_dir = "/etc/haproxy" |
138 | default_haproxy_config = "%s/haproxy.cfg" % default_haproxy_config_dir |
139 | default_haproxy_service_config_dir = "/var/run/haproxy" |
140 | -hook_name = os.path.basename(sys.argv[0]) |
141 | +HOOK_NAME = os.path.basename(sys.argv[0]) |
142 | |
143 | ############################################################################### |
144 | # Supporting functions |
145 | ############################################################################### |
146 | |
147 | +def unit_get(*args): |
148 | + """Simple wrapper around unit-get, all arguments passed untouched""" |
149 | + get_args = ["unit-get"] |
150 | + get_args.extend(args) |
151 | + return subprocess.check_output(get_args) |
152 | + |
153 | +def juju_log(*args): |
154 | + """Simple wrapper around juju-log, all arguments are passed untouched""" |
155 | + log_args = ["juju-log"] |
156 | + log_args.extend(args) |
157 | + subprocess.call(log_args) |
158 | |
159 | #------------------------------------------------------------------------------ |
160 | # config_get: Returns a dictionary containing all of the config information |
161 | @@ -73,6 +85,17 @@ |
162 | finally: |
163 | return(relation_data) |
164 | |
165 | +def relation_set(arguments, relation_id=None): |
166 | + """ |
167 | + Wrapper around relation-set |
168 | + @param arguments: list of command line arguments |
169 | + @param relation_id: optional relation-id (passed to -r parameter) to use |
170 | + """ |
171 | + set_args = ["relation-set"] |
172 | + if relation_id is not None: |
173 | + set_args.extend(["-r", str(relation_id)]) |
174 | + set_args.extend(arguments) |
175 | + subprocess.check_call(set_args) |
176 | |
177 | #------------------------------------------------------------------------------ |
178 | # apt_get_install( package ): Installs a package |
179 | @@ -299,15 +322,41 @@ |
180 | config_data['monitoring_port'], |
181 | monitoring_config)) |
182 | |
183 | +def get_host_port(services_list): |
184 | + """ |
185 | + Given a services list and global juju information, get a host |
186 | + and port for this system. |
187 | + """ |
188 | + host = services_list[0]["service_host"] |
189 | + port = int(services_list[0]["service_port"]) |
190 | + return (host, port) |
191 | |
192 | -#------------------------------------------------------------------------------ |
193 | -# get_config_services: Convenience function that returns a list |
194 | -# of dictionary entries containing all of the services |
195 | -# configuration |
196 | -#------------------------------------------------------------------------------ |
197 | def get_config_services(): |
198 | + """ |
199 | + Return dict of all services in the configuration, and in the relation |
200 | + where appropriate. If a relation contains a "services" key, read |
201 | + it in as yaml as is the case with the configuration. Set the host and |
202 | + port for any relation initiated service entry as those items cannot be |
203 | + known by the other side of the relation. In the case of a |
204 | + proxy configuration found, ensure the forward for option is set. |
205 | + """ |
206 | config_data = config_get() |
207 | - services_list = yaml.load(config_data['services']) |
208 | + config_services_list = yaml.load(config_data['services']) |
209 | + (host, port) = get_host_port(config_services_list) |
210 | + all_relations = relation_get_all("reverseproxy") |
211 | + services_list = [] |
212 | + if hasattr(all_relations, "iteritems"): |
213 | + for relid, reldata in all_relations.iteritems(): |
214 | + for unit, relation_info in reldata.iteritems(): |
215 | + if relation_info.has_key("services"): |
216 | + rservices = yaml.load(relation_info["services"]) |
217 | + for r in rservices: |
218 | + r["service_host"] = host |
219 | + r["service_port"] = port |
220 | + port += 1 |
221 | + services_list.extend(rservices) |
222 | + if len(services_list) == 0: |
223 | + services_list = config_services_list |
224 | return(services_list) |
225 | |
226 | |
227 | @@ -323,12 +372,86 @@ |
228 | return(None) |
229 | |
230 | |
231 | +def relation_get_all(relation_name): |
232 | + """ |
233 | + Iterate through all relations, and return large data structure with the |
234 | + relation data set: |
235 | + |
236 | + @param relation_name: The name of the relation to check |
237 | + |
238 | + Returns: |
239 | + |
240 | + relation_id: |
241 | + unit: |
242 | + key: value |
243 | + key2: value |
244 | + """ |
245 | + result = {} |
246 | + try: |
247 | + relids = subprocess.Popen( |
248 | + ['relation-ids', relation_name], stdout=subprocess.PIPE) |
249 | + for relid in [x.strip() for x in relids.stdout]: |
250 | + result[relid] = {} |
251 | + for unit in json.loads( |
252 | + subprocess.check_output( |
253 | + ['relation-list', '--format=json', '-r', relid])): |
254 | + result[relid][unit] = relation_get(None, unit, relid) |
255 | + return result |
256 | + except Exception, e: |
257 | + subprocess.call(['juju-log', str(e)]) |
258 | + |
259 | +def get_services_dict(): |
260 | + """ |
261 | + Transform the services list into a dict for easier comprehension, |
262 | + and to ensure that we have only one entry per service type. If multiple |
263 | + relations specify the same server_name, try to union the servers |
264 | + entries. |
265 | + """ |
266 | + services_list = get_config_services() |
267 | + services_dict = {} |
268 | + |
269 | + for service_item in services_list: |
270 | + if not hasattr(service_item, "iteritems"): |
271 | + juju_log("Each 'services' entry must be a dict: %s" % service_item) |
272 | + continue; |
273 | + if "service_name" not in service_item: |
274 | + juju_log("Missing 'service_name': %s" % service_item) |
275 | + continue; |
276 | + name = service_item["service_name"] |
277 | + options = service_item["service_options"] |
278 | + if name in services_dict: |
279 | + if "servers" in services_dict[name]: |
280 | + services_dict[name]["servers"].extend(service_item["servers"]) |
281 | + else: |
282 | + services_dict[name] = service_item |
283 | + if os.path.exists("%s/%s.is.proxy" % ( |
284 | + default_haproxy_service_config_dir, name)): |
285 | + if 'option forwardfor' not in options: |
286 | + options.append("option forwardfor") |
287 | + |
288 | + return services_dict |
289 | + |
290 | +def get_all_services(): |
291 | + """ |
292 | + Transform a services dict into an "all_services" relation setting expected |
293 | + by apache2. This is needed to ensure we have the port and hostname setting |
294 | + correct and in the proper format |
295 | + """ |
296 | + services = get_services_dict() |
297 | + all_services = [] |
298 | + for name in services: |
299 | + s = {"service_name": name, |
300 | + "service_port": services[name]["service_port"]} |
301 | + all_services.append(s) |
302 | + return all_services |
303 | + |
304 | #------------------------------------------------------------------------------ |
305 | # create_services: Function that will create the services configuration |
306 | # from the config data and/or relation information |
307 | #------------------------------------------------------------------------------ |
308 | def create_services(): |
309 | services_list = get_config_services() |
310 | +<<<<<<< TREE |
311 | services_dict = {} |
312 | for service_item in services_list: |
313 | service_name = service_item['service_name'] |
314 | @@ -344,61 +467,59 @@ |
315 | |
316 | config_data = config_get() |
317 | listen_appsession = yaml.load(config_data['listen_appsession']) |
318 | +======= |
319 | + services_dict = get_services_dict() |
320 | +>>>>>>> MERGE-SOURCE |
321 | |
322 | - try: |
323 | - relids = subprocess.Popen(['relation-ids', 'reverseproxy'], |
324 | - stdout=subprocess.PIPE) |
325 | - for relid in [x.strip() for x in relids.stdout]: |
326 | - for unit in json.loads( |
327 | - subprocess.check_output(['relation-list', '--format=json', |
328 | - '-r', relid])): |
329 | - relation_info = relation_get(None, unit, relid) |
330 | - unit_name = unit.rpartition('/')[0] |
331 | - if not isinstance(relation_info, dict): |
332 | - sys.exit(0) |
333 | - # Mandatory switches ( hostname, port ) |
334 | - server_name = "%s__%s" % \ |
335 | - (relation_info['hostname'].replace('.', '_'), |
336 | + # service definitions overwrites user specified haproxy file in |
337 | + # a pseudo-template form |
338 | + all_relations = relation_get_all("reverseproxy") |
339 | + for relid, reldata in all_relations.iteritems(): |
340 | + for unit, relation_info in reldata.iteritems(): |
341 | + if not isinstance(relation_info, dict): |
342 | + sys.exit(0) |
343 | + if "services" in relation_info: |
344 | + juju_log("Relation %s has services override defined" % relid) |
345 | + continue; |
346 | + if "hostname" not in relation_info or "port" not in relation_info: |
347 | + juju_log("Relation %s needs hostname and port defined" % relid) |
348 | + continue; |
349 | + juju_service_name = unit.rpartition('/')[0] |
350 | + # Mandatory switches ( hostname, port ) |
351 | + server_name = "%s__%s" % ( |
352 | + relation_info['hostname'].replace('.', '_'), |
353 | relation_info['port']) |
354 | - server_ip = relation_info['hostname'] |
355 | - server_port = relation_info['port'] |
356 | - # Optional switches ( service_name ) |
357 | - if 'service_name' in relation_info: |
358 | - if relation_info['service_name'] in services_dict: |
359 | - service_name = relation_info['service_name'] |
360 | - else: |
361 | - subprocess.call([ |
362 | - 'juju-log', 'service %s does not exists. ' % |
363 | - relation_info['service_name']]) |
364 | - sys.exit(1) |
365 | - elif unit_name in services_dict: |
366 | - service_name = unit_name |
367 | - else: |
368 | - service_name = services_list[0]['service_name'] |
369 | - if os.path.exists("%s/%s.is.proxy" % |
370 | - (default_haproxy_service_config_dir, service_name)): |
371 | - if 'option forwardfor' not in service_options: |
372 | - service_options.append("option forwardfor") |
373 | - # Add the server entries |
374 | - if not 'servers' in services_dict[service_name]: |
375 | - services_dict[service_name]['servers'] = \ |
376 | - [(server_name, server_ip, server_port, |
377 | - services_dict[service_name]['server_options'])] |
378 | - else: |
379 | - services_dict[service_name]['servers'].append(( |
380 | - server_name, server_ip, server_port, |
381 | - services_dict[service_name]['server_options'])) |
382 | - except Exception, e: |
383 | - subprocess.call(['juju-log', str(e)]) |
384 | + server_ip = relation_info['hostname'] |
385 | + server_port = relation_info['port'] |
386 | + # Optional switches ( service_name ) |
387 | + if 'service_name' in relation_info: |
388 | + if relation_info['service_name'] in services_dict: |
389 | + service_name = relation_info['service_name'] |
390 | + else: |
391 | + juju_log("service %s does not exist." % ( |
392 | + relation_info['service_name'])) |
393 | + sys.exit(1) |
394 | + elif juju_service_name + '_service' in services_dict: |
395 | + service_name = juju_service_name + '_service' |
396 | + else: |
397 | + service_name = services_list[0]['service_name'] |
398 | + # Add the server entries |
399 | + if not 'servers' in services_dict[service_name]: |
400 | + services_dict[service_name]['servers'] = [] |
401 | + services_dict[service_name]['servers'].append(( |
402 | + server_name, server_ip, server_port, |
403 | + services_dict[service_name]['server_options'])) |
404 | + |
405 | # Construct the new haproxy.cfg file |
406 | for service in services_dict: |
407 | - print "Service: ", service |
408 | + juju_log("Service: ", service) |
409 | server_entries = None |
410 | if 'servers' in services_dict[service]: |
411 | server_entries = services_dict[service]['servers'] |
412 | - with open("%s/%s.service" % ( |
413 | - default_haproxy_service_config_dir, |
414 | - services_dict[service]['service_name']), 'w') as service_config: |
415 | + service_config_file = "%s/%s.service" % ( |
416 | + default_haproxy_service_config_dir, |
417 | + services_dict[service]['service_name']) |
418 | + with open(service_config_file, 'w') as service_config: |
419 | service_config.write( |
420 | create_listen_stanza(services_dict[service]['service_name'], |
421 | services_dict[service]['service_host'], |
422 | @@ -512,6 +633,16 @@ |
423 | else: |
424 | return(False) |
425 | |
426 | +def website_notify(): |
427 | + """ |
428 | + Notify any webiste relations of any configuration changes. |
429 | + """ |
430 | + juju_log("Notifying all website relations of change") |
431 | + all_relations = relation_get_all("website") |
432 | + if hasattr(all_relations, "iteritems"): |
433 | + for relid, reldata in all_relations.iteritems(): |
434 | + relation_set(["time=%s" % time.time()], relation_id=relid) |
435 | + |
436 | |
437 | ############################################################################### |
438 | # Hook functions |
439 | @@ -563,17 +694,18 @@ |
440 | def reverseproxy_interface(hook_name=None): |
441 | if hook_name is None: |
442 | return(None) |
443 | - if hook_name == "changed": |
444 | - config_changed() |
445 | - if hook_name=="departed": |
446 | - config_changed() |
447 | + elif hook_name == "changed": |
448 | + config_changed() |
449 | + website_notify() |
450 | + elif hook_name=="departed": |
451 | + config_changed() |
452 | + website_notify() |
453 | |
454 | def website_interface(hook_name=None): |
455 | if hook_name is None: |
456 | return(None) |
457 | default_port = 80 |
458 | relation_data = relation_get() |
459 | - config_data = config_get() |
460 | |
461 | # If a specfic service has been asked for then return the ip:port for |
462 | # that service, else pass back the default |
463 | @@ -594,9 +726,9 @@ |
464 | # hostname |
465 | if my_host == "localhost": |
466 | my_host = socket.gethostname() |
467 | - subprocess.call(['relation-set', 'port=%d' % |
468 | - my_port, 'hostname=%s' % my_host, 'all_services=%s' % |
469 | - config_data['services']]) |
470 | + subprocess.call( |
471 | + ['relation-set', 'port=%d' % my_port, 'hostname=%s' % my_host, |
472 | + 'all_services=%s' % yaml.dump(get_all_services())]) |
473 | if hook_name == "changed": |
474 | if 'is-proxy' in relation_data: |
475 | service_name = "%s__%d" % \ |
476 | @@ -613,27 +745,28 @@ |
477 | ############################################################################### |
478 | # Main section |
479 | ############################################################################### |
480 | -if hook_name == "install": |
481 | - install_hook() |
482 | -elif hook_name == "config-changed": |
483 | - config_changed() |
484 | - update_nrpe_config() |
485 | -elif hook_name == "start": |
486 | - start_hook() |
487 | -elif hook_name == "stop": |
488 | - stop_hook() |
489 | -elif hook_name == "reverseproxy-relation-broken": |
490 | - config_changed() |
491 | -elif hook_name == "reverseproxy-relation-changed": |
492 | - reverseproxy_interface("changed") |
493 | -elif hook_name == "reverseproxy-relation-departed": |
494 | - reverseproxy_interface("departed") |
495 | -elif hook_name == "website-relation-joined": |
496 | - website_interface("joined") |
497 | -elif hook_name == "website-relation-changed": |
498 | - website_interface("changed") |
499 | -elif hook_name == "nrpe-external-master-relation-changed": |
500 | - update_nrpe_config() |
501 | -else: |
502 | - print "Unknown hook" |
503 | - sys.exit(1) |
504 | +if __name__ == "__main__": |
505 | + if HOOK_NAME == "install": |
506 | + install_hook() |
507 | + elif HOOK_NAME == "config-changed": |
508 | + config_changed() |
509 | + update_nrpe_config() |
510 | + elif HOOK_NAME == "start": |
511 | + start_hook() |
512 | + elif HOOK_NAME == "stop": |
513 | + stop_hook() |
514 | + elif HOOK_NAME == "reverseproxy-relation-broken": |
515 | + config_changed() |
516 | + elif HOOK_NAME == "reverseproxy-relation-changed": |
517 | + reverseproxy_interface("changed") |
518 | + elif HOOK_NAME == "reverseproxy-relation-departed": |
519 | + reverseproxy_interface("departed") |
520 | + elif HOOK_NAME == "website-relation-joined": |
521 | + website_interface("joined") |
522 | + elif HOOK_NAME == "website-relation-changed": |
523 | + website_interface("changed") |
524 | + elif HOOK_NAME == "nrpe-external-master-relation-changed": |
525 | + update_nrpe_config() |
526 | + else: |
527 | + print "Unknown hook" |
528 | + sys.exit(1) |
529 | |
530 | === added file 'hooks/test_hooks.py' |
531 | --- hooks/test_hooks.py 1970-01-01 00:00:00 +0000 |
532 | +++ hooks/test_hooks.py 2013-02-15 04:57:24 +0000 |
533 | @@ -0,0 +1,263 @@ |
534 | +import hooks |
535 | +import yaml |
536 | +from textwrap import dedent |
537 | +from mocker import MockerTestCase, ARGS |
538 | + |
539 | +class JujuHookTest(MockerTestCase): |
540 | + |
541 | + def setUp(self): |
542 | + self.config_services = [{ |
543 | + "service_name": "haproxy_test", |
544 | + "service_host": "0.0.0.0", |
545 | + "service_port": "88", |
546 | + "service_options": ["balance leastconn"], |
547 | + "server_options": "maxconn 25"}] |
548 | + self.config_services_extended = [ |
549 | + {"service_name": "unit_service", |
550 | + "service_host": "supplied-hostname", |
551 | + "service_port": "999", |
552 | + "service_options": ["balance leastconn"], |
553 | + "server_options": "maxconn 99"}] |
554 | + self.relation_services = [ |
555 | + {"service_name": "foo_svc", |
556 | + "service_options": ["balance leastconn"], |
557 | + "servers": [("A", "hA", "1", "oA1 oA2")]}, |
558 | + {"service_name": "bar_svc", |
559 | + "service_options": ["balance leastconn"], |
560 | + "servers": [ |
561 | + ("A", "hA", "1", "oA1 oA2"), ("B", "hB", "2", "oB1 oB2")]}] |
562 | + self.relation_services2 = [ |
563 | + {"service_name": "foo_svc", |
564 | + "service_options": ["balance leastconn"], |
565 | + "servers": [("A2", "hA2", "12", "oA12 oA22")]}] |
566 | + hooks.default_haproxy_config_dir = self.makeDir() |
567 | + hooks.default_haproxy_config = self.makeFile() |
568 | + hooks.default_haproxy_service_config_dir = self.makeDir() |
569 | + obj = self.mocker.replace("hooks.juju_log") |
570 | + obj(ARGS) |
571 | + self.mocker.count(0, None) |
572 | + obj = self.mocker.replace("hooks.unit_get") |
573 | + obj("public-address") |
574 | + self.mocker.result("test-host.example.com") |
575 | + self.mocker.count(0, None) |
576 | + self.maxDiff = None |
577 | + |
578 | + def _expect_config_get(self, **kwargs): |
579 | + result = { |
580 | + "default_timeouts": "queue 1000, connect 1000, client 1000, server 1000", |
581 | + "global_log": "127.0.0.1 local0, 127.0.0.1 local1 notice", |
582 | + "global_spread_checks": 0, |
583 | + "monitoring_allowed_cidr": "127.0.0.1/32", |
584 | + "monitoring_username": "haproxy", |
585 | + "default_log": "global", |
586 | + "global_group": "haproxy", |
587 | + "monitoring_stats_refresh": 3, |
588 | + "default_retries": 3, |
589 | + "services": yaml.dump(self.config_services), |
590 | + "global_maxconn": 4096, |
591 | + "global_user": "haproxy", |
592 | + "default_options": "httplog, dontlognull", |
593 | + "monitoring_port": 10000, |
594 | + "global_debug": False, |
595 | + "nagios_context": "juju", |
596 | + "global_quiet": False, |
597 | + "enable_monitoring": False, |
598 | + "monitoring_password": "changeme", |
599 | + "default_mode": "http"} |
600 | + obj = self.mocker.replace("hooks.config_get") |
601 | + obj() |
602 | + result.update(kwargs) |
603 | + self.mocker.result(result) |
604 | + self.mocker.count(1, None) |
605 | + |
606 | + def _expect_relation_get_all(self, relation, extra={}): |
607 | + obj = self.mocker.replace("hooks.relation_get_all") |
608 | + obj(relation) |
609 | + relation = {"hostname": "10.0.1.2", |
610 | + "private-address": "10.0.1.2", |
611 | + "port": "10000"} |
612 | + relation.update(extra) |
613 | + result = {"1": {"unit/0": relation}} |
614 | + self.mocker.result(result) |
615 | + self.mocker.count(1, None) |
616 | + |
617 | + def _expect_relation_get_all_multiple(self, relation_name): |
618 | + obj = self.mocker.replace("hooks.relation_get_all") |
619 | + obj(relation_name) |
620 | + result = { |
621 | + "1": {"unit/0": { |
622 | + "hostname": "10.0.1.2", |
623 | + "private-address": "10.0.1.2", |
624 | + "port": "10000", |
625 | + "services": yaml.dump(self.relation_services)}}, |
626 | + "2": {"unit/1": { |
627 | + "hostname": "10.0.1.3", |
628 | + "private-address": "10.0.1.3", |
629 | + "port": "10001", |
630 | + "services": yaml.dump(self.relation_services2)}}} |
631 | + self.mocker.result(result) |
632 | + self.mocker.count(1, None) |
633 | + |
634 | + def _expect_relation_get_all_with_services(self, relation, extra={}): |
635 | + extra.update({"services": yaml.dump(self.relation_services)}) |
636 | + return self._expect_relation_get_all(relation, extra) |
637 | + |
638 | + def _expect_relation_get(self): |
639 | + obj = self.mocker.replace("hooks.relation_get") |
640 | + obj() |
641 | + result = {} |
642 | + self.mocker.result(result) |
643 | + self.mocker.count(1, None) |
644 | + |
645 | + def test_create_services(self): |
646 | + """ |
647 | + Simplest use case, config stanza seeded in config file, server line |
648 | + added through simple relation. Many servers can join this, but |
649 | + multiple services will not be presented to the outside |
650 | + """ |
651 | + self._expect_config_get() |
652 | + self._expect_relation_get_all("reverseproxy") |
653 | + self.mocker.replay() |
654 | + hooks.create_services() |
655 | + services = hooks.load_services() |
656 | + stanza = """\ |
657 | + listen haproxy_test 0.0.0.0:88 |
658 | + balance leastconn |
659 | + server 10_0_1_2__10000 10.0.1.2:10000 maxconn 25 |
660 | + |
661 | + """ |
662 | + self.assertEquals(services, dedent(stanza)) |
663 | + |
664 | + def test_create_services_extended_with_relation(self): |
665 | + """ |
666 | + This case covers specifying an up-front services file to ha-proxy |
667 | + in the config. The relation then specifies a singular hostname, |
668 | + port and server_options setting which is filled into the appropriate |
669 | + haproxy stanza based on multiple criteria. |
670 | + """ |
671 | + self._expect_config_get( |
672 | + services=yaml.dump(self.config_services_extended)) |
673 | + self._expect_relation_get_all("reverseproxy") |
674 | + self.mocker.replay() |
675 | + hooks.create_services() |
676 | + services = hooks.load_services() |
677 | + stanza = """\ |
678 | + listen unit_service supplied-hostname:999 |
679 | + balance leastconn |
680 | + server 10_0_1_2__10000 10.0.1.2:10000 maxconn 99 |
681 | + |
682 | + """ |
683 | + self.assertEquals(dedent(stanza), services) |
684 | + |
685 | + def test_create_services_pure_relation(self): |
686 | + """ |
687 | + In this case, the relation is in control of the haproxy config file. |
688 | + Each relation chooses what server it creates in the haproxy file, it |
689 | + relies on the haproxy service only for the hostname and front-end port. |
690 | + Each member of the relation will put a backend server entry under in |
691 | + the desired stanza. Each realtion can in fact supply multiple |
692 | + entries from the same juju service unit if desired. |
693 | + """ |
694 | + self._expect_config_get() |
695 | + self._expect_relation_get_all_with_services("reverseproxy") |
696 | + self.mocker.replay() |
697 | + hooks.create_services() |
698 | + services = hooks.load_services() |
699 | + stanza = """\ |
700 | + listen foo_svc 0.0.0.0:88 |
701 | + balance leastconn |
702 | + server A hA:1 oA1 oA2 |
703 | + """ |
704 | + self.assertIn(dedent(stanza), services) |
705 | + stanza = """\ |
706 | + listen bar_svc 0.0.0.0:89 |
707 | + balance leastconn |
708 | + server A hA:1 oA1 oA2 |
709 | + server B hB:2 oB1 oB2 |
710 | + """ |
711 | + self.assertIn(dedent(stanza), services) |
712 | + |
713 | + def test_create_services_pure_relation_multiple(self): |
714 | + """ |
715 | + This is much liek the pure_relation case, where the relation specifies |
716 | + a "services" override. However, in this case we have multiple relations |
717 | + that partially override each other. We expect that the created haproxy |
718 | + conf file will combine things appropriately. |
719 | + """ |
720 | + self._expect_config_get() |
721 | + self._expect_relation_get_all_multiple("reverseproxy") |
722 | + self.mocker.replay() |
723 | + hooks.create_services() |
724 | + result = hooks.load_services() |
725 | + stanza = """\ |
726 | + listen foo_svc 0.0.0.0:88 |
727 | + balance leastconn |
728 | + server A hA:1 oA1 oA2 |
729 | + server A2 hA2:12 oA12 oA22 |
730 | + """ |
731 | + self.assertIn(dedent(stanza), result) |
732 | + stanza = """\ |
733 | + listen bar_svc 0.0.0.0:89 |
734 | + balance leastconn |
735 | + server A hA:1 oA1 oA2 |
736 | + server B hB:2 oB1 oB2 |
737 | + """ |
738 | + self.assertIn(dedent(stanza), result) |
739 | + |
740 | + def test_get_config_services_config_only(self): |
741 | + """ |
742 | + Attempting to catch the case where a relation is not joined yet |
743 | + """ |
744 | + self._expect_config_get() |
745 | + obj = self.mocker.replace("hooks.relation_get_all") |
746 | + obj("reverseproxy") |
747 | + self.mocker.result(None) |
748 | + self.mocker.replay() |
749 | + result = hooks.get_config_services() |
750 | + self.assertEquals(result, self.config_services) |
751 | + |
752 | + def test_get_config_services_relation_no_services(self): |
753 | + """ |
754 | + If the config specifies services and the realtion does not, just the |
755 | + config services should come through. |
756 | + """ |
757 | + self._expect_config_get() |
758 | + self._expect_relation_get_all("reverseproxy") |
759 | + self.mocker.replay() |
760 | + result = hooks.get_config_services() |
761 | + self.assertEquals(result, self.config_services) |
762 | + |
763 | + def test_get_config_services_relation_with_services(self): |
764 | + """ |
765 | + Testing with both the config and relation providing services should |
766 | + yield the just the relation |
767 | + """ |
768 | + self._expect_config_get() |
769 | + self._expect_relation_get_all_with_services("reverseproxy") |
770 | + self.mocker.replay() |
771 | + result = hooks.get_config_services() |
772 | + # Just test "servers" since hostname and port and maybe other keys |
773 | + # will be added by the hook |
774 | + self.assertEquals(result[0]["servers"], |
775 | + self.relation_services[0]["servers"]) |
776 | + |
777 | + def test_config_generation_indempotent(self): |
778 | + self._expect_config_get() |
779 | + self._expect_relation_get_all_multiple("reverseproxy") |
780 | + self.mocker.replay() |
781 | + |
782 | + # Test that we generate the same haproxy.conf file each time |
783 | + hooks.create_services() |
784 | + result1 = hooks.load_services() |
785 | + hooks.create_services() |
786 | + result2 = hooks.load_services() |
787 | + self.assertEqual(result1, result2) |
788 | + |
789 | + def test_get_all_services(self): |
790 | + self._expect_config_get() |
791 | + self._expect_relation_get_all_multiple("reverseproxy") |
792 | + self.mocker.replay() |
793 | + baseline = [{"service_name": "foo_svc", "service_port": 88}, |
794 | + {"service_name": "bar_svc", "service_port": 89}] |
795 | + services = hooks.get_all_services() |
796 | + self.assertEqual(baseline, services) |
797 | |
798 | === modified file 'revision' |
799 | --- revision 2013-02-14 15:05:42 +0000 |
800 | +++ revision 2013-02-15 04:57:24 +0000 |
801 | @@ -1,1 +1,5 @@ |
802 | +<<<<<<< TREE |
803 | 32 |
804 | +======= |
805 | +42 |
806 | +>>>>>>> MERGE-SOURCE |