Merge lp:~dpb/charms/precise/haproxy/fixes into lp:charms/haproxy
- Precise Pangolin (12.04)
- fixes
- Merge into trunk
Status: | Merged |
---|---|
Merged at revision: | 64 |
Proposed branch: | lp:~dpb/charms/precise/haproxy/fixes |
Merge into: | lp:charms/haproxy |
Diff against target: |
755 lines (+581/-96) 4 files modified
README.md (+119/-0) hooks/hooks.py (+189/-95) hooks/test_hooks.py (+272/-0) revision (+1/-1) |
To merge this branch: | bzr merge lp:~dpb/charms/precise/haproxy/fixes |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Juan L. Negron (community) | Approve | ||
Review via email: mp+148065@code.launchpad.net |
Commit message
Description of the change
Entire change is focused on adding "Relation driven proxying", or
"multi-service proxying":
- Added "Relation driven proxying" method for the charm. This allows each
charm to explicitly hook into haproxy by simply setting realtion variables.
This also allows haproxy to proxy multiple services per charm, and proxy the
same service across multiple charms as long as the agree on service names.
- Refactored service dict parsing out of "create_services" and into separate
method, adding error checking, and allowing the services yaml to come from
relation settings.
- Fixed website relation to be aware of and make use of get_config_services
- cleaned up some existing pep8 lint on code that I touched
- protect main method to make this testable
- Refactored relation parsing out of "create_services" into it's own method
"relation_
- Added unit tests for my changes, and also to test basic functionality of
configuration and service proxying. `test_hooks.py`, including README.md
section outlining usage.
- Added README.md with basics of usage of the charm
Juan L. Negron (negronjl) wrote : | # |
Juan L. Negron (negronjl) wrote : | # |
This looks good to me. Great work on expanding the charm!
Approved.
Merging.
-Juan
Preview Diff
1 | === added file 'README.md' |
2 | --- README.md 1970-01-01 00:00:00 +0000 |
3 | +++ README.md 2013-02-13 23:59:22 +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-01-15 02:04:18 +0000 |
127 | +++ hooks/hooks.py 2013-02-13 23:59:22 +0000 |
128 | @@ -19,12 +19,23 @@ |
129 | default_haproxy_config_dir = "/etc/haproxy" |
130 | default_haproxy_config = "%s/haproxy.cfg" % default_haproxy_config_dir |
131 | default_haproxy_service_config_dir = "/var/run/haproxy" |
132 | -hook_name = os.path.basename(sys.argv[0]) |
133 | +HOOK_NAME = os.path.basename(sys.argv[0]) |
134 | |
135 | ############################################################################### |
136 | # Supporting functions |
137 | ############################################################################### |
138 | |
139 | +def unit_get(*args): |
140 | + """Simple wrapper around unit-get, all arguments passed untouched""" |
141 | + get_args = ["unit-get"] |
142 | + get_args.extend(args) |
143 | + return subprocess.check_output(get_args) |
144 | + |
145 | +def juju_log(*args): |
146 | + """Simple wrapper around juju-log, all arguments are passed untouched""" |
147 | + log_args = ["juju-log"] |
148 | + log_args.extend(args) |
149 | + subprocess.call(log_args) |
150 | |
151 | #------------------------------------------------------------------------------ |
152 | # config_get: Returns a dictionary containing all of the config information |
153 | @@ -297,15 +308,41 @@ |
154 | config_data['monitoring_port'], |
155 | monitoring_config)) |
156 | |
157 | +def get_host_port(services_list): |
158 | + """ |
159 | + Given a services list and global juju information, get a host |
160 | + and port for this system. |
161 | + """ |
162 | + host = services_list[0]["service_host"] |
163 | + port = int(services_list[0]["service_port"]) |
164 | + return (host, port) |
165 | |
166 | -#------------------------------------------------------------------------------ |
167 | -# get_config_services: Convenience function that returns a list |
168 | -# of dictionary entries containing all of the services |
169 | -# configuration |
170 | -#------------------------------------------------------------------------------ |
171 | def get_config_services(): |
172 | + """ |
173 | + Return dict of all services in the configuration, and in the relation |
174 | + where appropriate. If a relation contains a "services" key, read |
175 | + it in as yaml as is the case with the configuration. Set the host and |
176 | + port for any relation initiated service entry as those items cannot be |
177 | + known by the other side of the relation. In the case of a |
178 | + proxy configuration found, ensure the forward for option is set. |
179 | + """ |
180 | config_data = config_get() |
181 | - services_list = yaml.load(config_data['services']) |
182 | + config_services_list = yaml.load(config_data['services']) |
183 | + (host, port) = get_host_port(config_services_list) |
184 | + all_relations = relation_get_all("reverseproxy") |
185 | + services_list = [] |
186 | + if hasattr(all_relations, "iteritems"): |
187 | + for relid, reldata in all_relations.iteritems(): |
188 | + for unit, relation_info in reldata.iteritems(): |
189 | + if relation_info.has_key("services"): |
190 | + rservices = yaml.load(relation_info["services"]) |
191 | + for r in rservices: |
192 | + r["service_host"] = host |
193 | + r["service_port"] = port |
194 | + port += 1 |
195 | + services_list.extend(rservices) |
196 | + if len(services_list) == 0: |
197 | + services_list = config_services_list |
198 | return(services_list) |
199 | |
200 | |
201 | @@ -321,79 +358,136 @@ |
202 | return(None) |
203 | |
204 | |
205 | +def relation_get_all(relation_name): |
206 | + """ |
207 | + Iterate through all relations, and return large data structure with the |
208 | + relation data set: |
209 | + |
210 | + @param relation_name: The name of the relation to check |
211 | + |
212 | + Returns: |
213 | + |
214 | + relation_id: |
215 | + unit: |
216 | + key: value |
217 | + key2: value |
218 | + """ |
219 | + result = {} |
220 | + try: |
221 | + relids = subprocess.Popen( |
222 | + ['relation-ids', relation_name], stdout=subprocess.PIPE) |
223 | + for relid in [x.strip() for x in relids.stdout]: |
224 | + result[relid] = {} |
225 | + for unit in json.loads( |
226 | + subprocess.check_output( |
227 | + ['relation-list', '--format=json', '-r', relid])): |
228 | + result[relid][unit] = relation_get(None, unit, relid) |
229 | + return result |
230 | + except Exception, e: |
231 | + subprocess.call(['juju-log', str(e)]) |
232 | + |
233 | +def get_services_dict(): |
234 | + """ |
235 | + Transform the services list into a dict for easier comprehension, |
236 | + and to ensure that we have only one entry per service type. If multiple |
237 | + relations specify the same server_name, try to union the servers |
238 | + entries. |
239 | + """ |
240 | + services_list = get_config_services() |
241 | + services_dict = {} |
242 | + |
243 | + for service_item in services_list: |
244 | + if not hasattr(service_item, "iteritems"): |
245 | + juju_log("Each 'services' entry must be a dict: %s" % service_item) |
246 | + continue; |
247 | + if "service_name" not in service_item: |
248 | + juju_log("Missing 'service_name': %s" % service_item) |
249 | + continue; |
250 | + name = service_item["service_name"] |
251 | + options = service_item["service_options"] |
252 | + if name in services_dict: |
253 | + if "servers" in services_dict[name]: |
254 | + services_dict[name]["servers"].extend(service_item["servers"]) |
255 | + else: |
256 | + services_dict[name] = service_item |
257 | + if os.path.exists("%s/%s.is.proxy" % ( |
258 | + default_haproxy_service_config_dir, name)): |
259 | + if 'option forwardfor' not in options: |
260 | + options.append("option forwardfor") |
261 | + |
262 | + return services_dict |
263 | + |
264 | +def get_all_services(): |
265 | + """ |
266 | + Transform a services dict into an "all_services" relation setting expected |
267 | + by apache2. This is needed to ensure we have the port and hostname setting |
268 | + correct and in the proper format |
269 | + """ |
270 | + services = get_services_dict() |
271 | + all_services = [] |
272 | + for name in services: |
273 | + s = {"service_name": name, |
274 | + "service_port": services[name]["service_port"]} |
275 | + all_services.append(s) |
276 | + return all_services |
277 | + |
278 | #------------------------------------------------------------------------------ |
279 | # create_services: Function that will create the services configuration |
280 | # from the config data and/or relation information |
281 | #------------------------------------------------------------------------------ |
282 | def create_services(): |
283 | services_list = get_config_services() |
284 | - services_dict = {} |
285 | - for service_item in services_list: |
286 | - service_name = service_item['service_name'] |
287 | - service_host = service_item['service_host'] |
288 | - service_port = service_item['service_port'] |
289 | - service_options = service_item['service_options'] |
290 | - server_options = service_item['server_options'] |
291 | - services_dict[service_name] = {'service_name': service_name, |
292 | - 'service_host': service_host, |
293 | - 'service_port': service_port, |
294 | - 'service_options': service_options, |
295 | - 'server_options': server_options} |
296 | + services_dict = get_services_dict() |
297 | |
298 | - try: |
299 | - relids = subprocess.Popen(['relation-ids', 'reverseproxy'], |
300 | - stdout=subprocess.PIPE) |
301 | - for relid in [x.strip() for x in relids.stdout]: |
302 | - for unit in json.loads( |
303 | - subprocess.check_output(['relation-list', '--format=json', |
304 | - '-r', relid])): |
305 | - relation_info = relation_get(None, unit, relid) |
306 | - unit_name = unit.rpartition('/')[0] |
307 | - if not isinstance(relation_info, dict): |
308 | - sys.exit(0) |
309 | - # Mandatory switches ( hostname, port ) |
310 | - server_name = "%s__%s" % \ |
311 | - (relation_info['hostname'].replace('.', '_'), |
312 | + # service definitions overwrites user specified haproxy file in |
313 | + # a pseudo-template form |
314 | + all_relations = relation_get_all("reverseproxy") |
315 | + for relid, reldata in all_relations.iteritems(): |
316 | + for unit, relation_info in reldata.iteritems(): |
317 | + if not isinstance(relation_info, dict): |
318 | + sys.exit(0) |
319 | + if "services" in relation_info: |
320 | + juju_log("Relation %s has services override defined" % relid) |
321 | + continue; |
322 | + if "hostname" not in relation_info or "port" not in relation_info: |
323 | + juju_log("Relation %s needs hostname and port defined" % relid) |
324 | + continue; |
325 | + juju_service_name = unit.rpartition('/')[0] |
326 | + # Mandatory switches ( hostname, port ) |
327 | + server_name = "%s__%s" % ( |
328 | + relation_info['hostname'].replace('.', '_'), |
329 | relation_info['port']) |
330 | - server_ip = relation_info['hostname'] |
331 | - server_port = relation_info['port'] |
332 | - # Optional switches ( service_name ) |
333 | - if 'service_name' in relation_info: |
334 | - if relation_info['service_name'] in services_dict: |
335 | - service_name = relation_info['service_name'] |
336 | - else: |
337 | - subprocess.call([ |
338 | - 'juju-log', 'service %s does not exists. ' % |
339 | - relation_info['service_name']]) |
340 | - sys.exit(1) |
341 | - elif unit_name in services_dict: |
342 | - service_name = unit_name |
343 | - else: |
344 | - service_name = services_list[0]['service_name'] |
345 | - if os.path.exists("%s/%s.is.proxy" % |
346 | - (default_haproxy_service_config_dir, service_name)): |
347 | - if 'option forwardfor' not in service_options: |
348 | - service_options.append("option forwardfor") |
349 | - # Add the server entries |
350 | - if not 'servers' in services_dict[service_name]: |
351 | - services_dict[service_name]['servers'] = \ |
352 | - [(server_name, server_ip, server_port, |
353 | - services_dict[service_name]['server_options'])] |
354 | - else: |
355 | - services_dict[service_name]['servers'].append(( |
356 | - server_name, server_ip, server_port, |
357 | - services_dict[service_name]['server_options'])) |
358 | - except Exception, e: |
359 | - subprocess.call(['juju-log', str(e)]) |
360 | + server_ip = relation_info['hostname'] |
361 | + server_port = relation_info['port'] |
362 | + # Optional switches ( service_name ) |
363 | + if 'service_name' in relation_info: |
364 | + if relation_info['service_name'] in services_dict: |
365 | + service_name = relation_info['service_name'] |
366 | + else: |
367 | + juju_log("service %s does not exist." % ( |
368 | + relation_info['service_name'])) |
369 | + sys.exit(1) |
370 | + elif juju_service_name + '_service' in services_dict: |
371 | + service_name = juju_service_name + '_service' |
372 | + else: |
373 | + service_name = services_list[0]['service_name'] |
374 | + # Add the server entries |
375 | + if not 'servers' in services_dict[service_name]: |
376 | + services_dict[service_name]['servers'] = [] |
377 | + services_dict[service_name]['servers'].append(( |
378 | + server_name, server_ip, server_port, |
379 | + services_dict[service_name]['server_options'])) |
380 | + |
381 | # Construct the new haproxy.cfg file |
382 | for service in services_dict: |
383 | - print "Service: ", service |
384 | + juju_log("Service: ", service) |
385 | server_entries = None |
386 | if 'servers' in services_dict[service]: |
387 | server_entries = services_dict[service]['servers'] |
388 | - with open("%s/%s.service" % ( |
389 | - default_haproxy_service_config_dir, |
390 | - services_dict[service]['service_name']), 'w') as service_config: |
391 | + service_config_file = "%s/%s.service" % ( |
392 | + default_haproxy_service_config_dir, |
393 | + services_dict[service]['service_name']) |
394 | + with open(service_config_file, 'w') as service_config: |
395 | service_config.write( |
396 | create_listen_stanza(services_dict[service]['service_name'], |
397 | services_dict[service]['service_host'], |
398 | @@ -568,7 +662,6 @@ |
399 | return(None) |
400 | default_port = 80 |
401 | relation_data = relation_get() |
402 | - config_data = config_get() |
403 | |
404 | # If a specfic service has been asked for then return the ip:port for |
405 | # that service, else pass back the default |
406 | @@ -589,9 +682,9 @@ |
407 | # hostname |
408 | if my_host == "localhost": |
409 | my_host = socket.gethostname() |
410 | - subprocess.call(['relation-set', 'port=%d' % |
411 | - my_port, 'hostname=%s' % my_host, 'all_services=%s' % |
412 | - config_data['services']]) |
413 | + subprocess.call( |
414 | + ['relation-set', 'port=%d' % my_port, 'hostname=%s' % my_host, |
415 | + 'all_services=%s' % yaml.dump(get_all_services())]) |
416 | if hook_name == "changed": |
417 | if 'is-proxy' in relation_data: |
418 | service_name = "%s__%d" % \ |
419 | @@ -608,27 +701,28 @@ |
420 | ############################################################################### |
421 | # Main section |
422 | ############################################################################### |
423 | -if hook_name == "install": |
424 | - install_hook() |
425 | -elif hook_name == "config-changed": |
426 | - config_changed() |
427 | - update_nrpe_config() |
428 | -elif hook_name == "start": |
429 | - start_hook() |
430 | -elif hook_name == "stop": |
431 | - stop_hook() |
432 | -elif hook_name == "reverseproxy-relation-broken": |
433 | - config_changed() |
434 | -elif hook_name == "reverseproxy-relation-changed": |
435 | - reverseproxy_interface("changed") |
436 | -elif hook_name == "reverseproxy-relation-departed": |
437 | - reverseproxy_interface("departed") |
438 | -elif hook_name == "website-relation-joined": |
439 | - website_interface("joined") |
440 | -elif hook_name == "website-relation-changed": |
441 | - website_interface("changed") |
442 | -elif hook_name == "nrpe-external-master-relation-changed": |
443 | - update_nrpe_config() |
444 | -else: |
445 | - print "Unknown hook" |
446 | - sys.exit(1) |
447 | +if __name__ == "__main__": |
448 | + if HOOK_NAME == "install": |
449 | + install_hook() |
450 | + elif HOOK_NAME == "config-changed": |
451 | + config_changed() |
452 | + update_nrpe_config() |
453 | + elif HOOK_NAME == "start": |
454 | + start_hook() |
455 | + elif HOOK_NAME == "stop": |
456 | + stop_hook() |
457 | + elif HOOK_NAME == "reverseproxy-relation-broken": |
458 | + config_changed() |
459 | + elif HOOK_NAME == "reverseproxy-relation-changed": |
460 | + reverseproxy_interface("changed") |
461 | + elif HOOK_NAME == "reverseproxy-relation-departed": |
462 | + reverseproxy_interface("departed") |
463 | + elif HOOK_NAME == "website-relation-joined": |
464 | + website_interface("joined") |
465 | + elif HOOK_NAME == "website-relation-changed": |
466 | + website_interface("changed") |
467 | + elif HOOK_NAME == "nrpe-external-master-relation-changed": |
468 | + update_nrpe_config() |
469 | + else: |
470 | + print "Unknown hook" |
471 | + sys.exit(1) |
472 | |
473 | === added file 'hooks/test_hooks.py' |
474 | --- hooks/test_hooks.py 1970-01-01 00:00:00 +0000 |
475 | +++ hooks/test_hooks.py 2013-02-13 23:59:22 +0000 |
476 | @@ -0,0 +1,272 @@ |
477 | +import hooks |
478 | +import yaml |
479 | +from textwrap import dedent |
480 | +from mocker import MockerTestCase, ARGS |
481 | + |
482 | +class JujuHookTest(MockerTestCase): |
483 | + |
484 | + def setUp(self): |
485 | + self.config_services = [{ |
486 | + "service_name": "haproxy_test", |
487 | + "service_host": "0.0.0.0", |
488 | + "service_port": "88", |
489 | + "service_options": ["balance leastconn"], |
490 | + "server_options": "maxconn 25"}] |
491 | + self.config_services_extended = [ |
492 | + {"service_name": "unit_service", |
493 | + "service_host": "supplied-hostname", |
494 | + "service_port": "999", |
495 | + "service_options": ["balance leastconn"], |
496 | + "server_options": "maxconn 99"}] |
497 | + self.relation_services = [ |
498 | + {"service_name": "foo_svc", |
499 | + "service_options": ["balance leastconn"], |
500 | + "servers": [("A", "hA", "1", "oA1 oA2")]}, |
501 | + {"service_name": "bar_svc", |
502 | + "service_options": ["balance leastconn"], |
503 | + "servers": [ |
504 | + ("A", "hA", "1", "oA1 oA2"), ("B", "hB", "2", "oB1 oB2")]}] |
505 | + self.relation_services2 = [ |
506 | + {"service_name": "foo_svc", |
507 | + "service_options": ["balance leastconn"], |
508 | + "servers": [("A2", "hA2", "12", "oA12 oA22")]}] |
509 | + hooks.default_haproxy_config_dir = self.makeDir() |
510 | + hooks.default_haproxy_config = self.makeFile() |
511 | + hooks.default_haproxy_service_config_dir = self.makeDir() |
512 | + obj = self.mocker.replace("hooks.juju_log") |
513 | + obj(ARGS) |
514 | + self.mocker.count(0, None) |
515 | + obj = self.mocker.replace("hooks.unit_get") |
516 | + obj("public-address") |
517 | + self.mocker.result("test-host.example.com") |
518 | + self.mocker.count(0, None) |
519 | + self.maxDiff = None |
520 | + |
521 | + def _expect_config_get(self, **kwargs): |
522 | + result = { |
523 | + "default_timeouts": "queue 1000, connect 1000, client 1000, server 1000", |
524 | + "global_log": "127.0.0.1 local0, 127.0.0.1 local1 notice", |
525 | + "global_spread_checks": 0, |
526 | + "monitoring_allowed_cidr": "127.0.0.1/32", |
527 | + "monitoring_username": "haproxy", |
528 | + "default_log": "global", |
529 | + "global_group": "haproxy", |
530 | + "monitoring_stats_refresh": 3, |
531 | + "default_retries": 3, |
532 | + "services": yaml.dump(self.config_services), |
533 | + "global_maxconn": 4096, |
534 | + "global_user": "haproxy", |
535 | + "default_options": "httplog, dontlognull", |
536 | + "monitoring_port": 10000, |
537 | + "global_debug": False, |
538 | + "nagios_context": "juju", |
539 | + "global_quiet": False, |
540 | + "enable_monitoring": False, |
541 | + "monitoring_password": "changeme", |
542 | + "default_mode": "http"} |
543 | + obj = self.mocker.replace("hooks.config_get") |
544 | + obj() |
545 | + result.update(kwargs) |
546 | + self.mocker.result(result) |
547 | + self.mocker.count(1, None) |
548 | + |
549 | + def _expect_relation_get_all(self, relation, extra={}): |
550 | + obj = self.mocker.replace("hooks.relation_get_all") |
551 | + obj(relation) |
552 | + relation = {"hostname": "10.0.1.2", |
553 | + "private-address": "10.0.1.2", |
554 | + "port": "10000"} |
555 | + relation.update(extra) |
556 | + result = {"1": {"unit/0": relation}} |
557 | + self.mocker.result(result) |
558 | + self.mocker.count(1, None) |
559 | + |
560 | + def _expect_relation_get_all_multiple(self, relation_name): |
561 | + obj = self.mocker.replace("hooks.relation_get_all") |
562 | + obj(relation_name) |
563 | + result = { |
564 | + "1": {"unit/0": { |
565 | + "hostname": "10.0.1.2", |
566 | + "private-address": "10.0.1.2", |
567 | + "port": "10000", |
568 | + "services": yaml.dump(self.relation_services)}}, |
569 | + "2": {"unit/1": { |
570 | + "hostname": "10.0.1.3", |
571 | + "private-address": "10.0.1.3", |
572 | + "port": "10001", |
573 | + "services": yaml.dump(self.relation_services2)}}} |
574 | + self.mocker.result(result) |
575 | + self.mocker.count(1, None) |
576 | + |
577 | + def _expect_relation_get_all_with_services(self, relation, extra={}): |
578 | + extra.update({"services": yaml.dump(self.relation_services)}) |
579 | + return self._expect_relation_get_all(relation, extra) |
580 | + |
581 | + def _expect_relation_get(self): |
582 | + obj = self.mocker.replace("hooks.relation_get") |
583 | + obj() |
584 | + result = {} |
585 | + self.mocker.result(result) |
586 | + self.mocker.count(1, None) |
587 | + |
588 | + def _expect_relation_set(self, args): |
589 | + """ |
590 | + @param args: list of arguments expected to be passed to relation_set |
591 | + """ |
592 | + obj = self.mocker.replace("hooks.relation_set") |
593 | + obj(args) |
594 | + self.relation_set = args |
595 | + self.mocker.count(1,None) |
596 | + |
597 | + def test_create_services(self): |
598 | + """ |
599 | + Simplest use case, config stanza seeded in config file, server line |
600 | + added through simple relation. Many servers can join this, but |
601 | + multiple services will not be presented to the outside |
602 | + """ |
603 | + self._expect_config_get() |
604 | + self._expect_relation_get_all("reverseproxy") |
605 | + self.mocker.replay() |
606 | + hooks.create_services() |
607 | + services = hooks.load_services() |
608 | + stanza = """\ |
609 | + listen haproxy_test 0.0.0.0:88 |
610 | + balance leastconn |
611 | + server 10_0_1_2__10000 10.0.1.2:10000 maxconn 25 |
612 | + |
613 | + """ |
614 | + self.assertEquals(services, dedent(stanza)) |
615 | + |
616 | + def test_create_services_extended_with_relation(self): |
617 | + """ |
618 | + This case covers specifying an up-front services file to ha-proxy |
619 | + in the config. The relation then specifies a singular hostname, |
620 | + port and server_options setting which is filled into the appropriate |
621 | + haproxy stanza based on multiple criteria. |
622 | + """ |
623 | + self._expect_config_get( |
624 | + services=yaml.dump(self.config_services_extended)) |
625 | + self._expect_relation_get_all("reverseproxy") |
626 | + self.mocker.replay() |
627 | + hooks.create_services() |
628 | + services = hooks.load_services() |
629 | + stanza = """\ |
630 | + listen unit_service supplied-hostname:999 |
631 | + balance leastconn |
632 | + server 10_0_1_2__10000 10.0.1.2:10000 maxconn 99 |
633 | + |
634 | + """ |
635 | + self.assertEquals(dedent(stanza), services) |
636 | + |
637 | + def test_create_services_pure_relation(self): |
638 | + """ |
639 | + In this case, the relation is in control of the haproxy config file. |
640 | + Each relation chooses what server it creates in the haproxy file, it |
641 | + relies on the haproxy service only for the hostname and front-end port. |
642 | + Each member of the relation will put a backend server entry under in |
643 | + the desired stanza. Each realtion can in fact supply multiple |
644 | + entries from the same juju service unit if desired. |
645 | + """ |
646 | + self._expect_config_get() |
647 | + self._expect_relation_get_all_with_services("reverseproxy") |
648 | + self.mocker.replay() |
649 | + hooks.create_services() |
650 | + services = hooks.load_services() |
651 | + stanza = """\ |
652 | + listen foo_svc 0.0.0.0:88 |
653 | + balance leastconn |
654 | + server A hA:1 oA1 oA2 |
655 | + """ |
656 | + self.assertIn(dedent(stanza), services) |
657 | + stanza = """\ |
658 | + listen bar_svc 0.0.0.0:89 |
659 | + balance leastconn |
660 | + server A hA:1 oA1 oA2 |
661 | + server B hB:2 oB1 oB2 |
662 | + """ |
663 | + self.assertIn(dedent(stanza), services) |
664 | + |
665 | + def test_create_services_pure_relation_multiple(self): |
666 | + """ |
667 | + This is much liek the pure_relation case, where the relation specifies |
668 | + a "services" override. However, in this case we have multiple relations |
669 | + that partially override each other. We expect that the created haproxy |
670 | + conf file will combine things appropriately. |
671 | + """ |
672 | + self._expect_config_get() |
673 | + self._expect_relation_get_all_multiple("reverseproxy") |
674 | + self.mocker.replay() |
675 | + hooks.create_services() |
676 | + result = hooks.load_services() |
677 | + stanza = """\ |
678 | + listen foo_svc 0.0.0.0:88 |
679 | + balance leastconn |
680 | + server A hA:1 oA1 oA2 |
681 | + server A2 hA2:12 oA12 oA22 |
682 | + """ |
683 | + self.assertIn(dedent(stanza), result) |
684 | + stanza = """\ |
685 | + listen bar_svc 0.0.0.0:89 |
686 | + balance leastconn |
687 | + server A hA:1 oA1 oA2 |
688 | + server B hB:2 oB1 oB2 |
689 | + """ |
690 | + self.assertIn(dedent(stanza), result) |
691 | + |
692 | + def test_get_config_services_config_only(self): |
693 | + """ |
694 | + Attempting to catch the case where a relation is not joined yet |
695 | + """ |
696 | + self._expect_config_get() |
697 | + obj = self.mocker.replace("hooks.relation_get_all") |
698 | + obj("reverseproxy") |
699 | + self.mocker.result(None) |
700 | + self.mocker.replay() |
701 | + result = hooks.get_config_services() |
702 | + self.assertEquals(result, self.config_services) |
703 | + |
704 | + def test_get_config_services_relation_no_services(self): |
705 | + """ |
706 | + If the config specifies services and the realtion does not, just the |
707 | + config services should come through. |
708 | + """ |
709 | + self._expect_config_get() |
710 | + self._expect_relation_get_all("reverseproxy") |
711 | + self.mocker.replay() |
712 | + result = hooks.get_config_services() |
713 | + self.assertEquals(result, self.config_services) |
714 | + |
715 | + def test_get_config_services_relation_with_services(self): |
716 | + """ |
717 | + Testing with both the config and relation providing services should |
718 | + yield the just the relation |
719 | + """ |
720 | + self._expect_config_get() |
721 | + self._expect_relation_get_all_with_services("reverseproxy") |
722 | + self.mocker.replay() |
723 | + result = hooks.get_config_services() |
724 | + # Just test "servers" since hostname and port and maybe other keys |
725 | + # will be added by the hook |
726 | + self.assertEquals(result[0]["servers"], |
727 | + self.relation_services[0]["servers"]) |
728 | + |
729 | + def test_config_generation_indempotent(self): |
730 | + self._expect_config_get() |
731 | + self._expect_relation_get_all_multiple("reverseproxy") |
732 | + self.mocker.replay() |
733 | + |
734 | + # Test that we generate the same haproxy.conf file each time |
735 | + hooks.create_services() |
736 | + result1 = hooks.load_services() |
737 | + hooks.create_services() |
738 | + result2 = hooks.load_services() |
739 | + self.assertEqual(result1, result2) |
740 | + |
741 | + def test_get_all_services(self): |
742 | + self._expect_config_get() |
743 | + self._expect_relation_get_all_multiple("reverseproxy") |
744 | + self.mocker.replay() |
745 | + baseline = [{"service_name": "foo_svc", "service_port": 88}, |
746 | + {"service_name": "bar_svc", "service_port": 89}] |
747 | + services = hooks.get_all_services() |
748 | + self.assertEqual(baseline, services) |
749 | |
750 | === modified file 'revision' |
751 | --- revision 2013-01-15 02:04:18 +0000 |
752 | +++ revision 2013-02-13 23:59:22 +0000 |
753 | @@ -1,1 +1,1 @@ |
754 | -31 |
755 | +39 |
Reviewing this now.
-Juan