Merge lp:~dpb/charms/precise/haproxy/fixes into lp:charms/haproxy

Proposed by David Britton
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
Reviewer Review Type Date Requested Status
Juan L. Negron (community) Approve
Review via email: mp+148065@code.launchpad.net

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_get_all".

- 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

To post a comment you must log in.
Revision history for this message
Juan L. Negron (negronjl) wrote :

Reviewing this now.

-Juan

Revision history for this message
Juan L. Negron (negronjl) wrote :

This looks good to me. Great work on expanding the charm!

Approved.

Merging.

-Juan

review: Approve

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-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

Subscribers

People subscribed via source and target branches