Merge lp:charms/haproxy into lp:~matthias-cramer/charms/precise/haproxy/appsession

Proposed by Matthias Cramer
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
Reviewer Review Type Date Requested Status
Matthias Cramer Pending
Review via email: mp+148473@code.launchpad.net

Description of the change

Added config parameter listen_appsession to set appsession parameter

example: Added config parameter listen_appsession to set appsession parameter

To post a comment you must log in.
lp:charms/haproxy updated
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.

R=
CC=
https://codereview.appspot.com/56140043

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
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

Subscribers

People subscribed via source and target branches