Merge lp:~dpb/charms/precise/apache2/vhost-config-relation into lp:charms/apache2

Proposed by David Britton
Status: Merged
Merged at revision: 55
Proposed branch: lp:~dpb/charms/precise/apache2/vhost-config-relation
Merge into: lp:charms/apache2
Diff against target: 761 lines (+614/-30)
7 files modified
README.md (+55/-0)
hooks/hooks.py (+129/-30)
hooks/tests/test_config_changed.py (+73/-0)
hooks/tests/test_create_vhost.py (+106/-0)
hooks/tests/test_hooks.py (+34/-0)
hooks/tests/test_vhost_config_relation.py (+215/-0)
metadata.yaml (+2/-0)
To merge this branch: bzr merge lp:~dpb/charms/precise/apache2/vhost-config-relation
Reviewer Review Type Date Requested Status
Charles Butler (community) Approve
Review via email: mp+220295@code.launchpad.net

Description of the change

Add a vhost-config relation to the apache2 charm.

This allows a relating charm to pass the vhosts templates over relation data instead of forcing the juju admin to specify a base64 template in the apache2 juju config. I also stuck in there 'servername' and 'ssl_cert' (if it was self signed), as web apps many times need this data for url writing and for passing on to connecting clients for authentication purposes.

Included with this change are unit tests (make test).

To post a comment you must log in.
75. By David Britton

Add test_hooks file (oops)

76. By David Britton

extra param removed

77. By David Britton

- Adding note about opening non-standard ports to README

78. By David Britton

- Add some variables for testing purposes in config-changed
- Add config-changed tests (simple, only testing changed code)
- Fix small error where empty vhosts files were created in all cases

79. By David Britton

removing lint

80. By David Britton

- fixed failing tests if apache2 was not installed

81. By David Britton

- cleanup some language in README, docstrings.

Revision history for this message
Charles Butler (lazypower) wrote :

David,

Thank you for this high quality submission! I've tested this with a quick and dirty charm created in python that base64 encodes a jinja2 template and base64 encodes it according to the readme documentation.

It would be great if there were more details with specific details on how to plug this into a charm such as here is the jinja2 vhost template for a basic HTML site being served from /var/www/static - and associated example configuration to do so.

With that being said, the tests pass and it took ~ 20 minutes to write/configure/validate the addition. It performs really well from my perspective.

Thanks again! +1

If you have any questions/comments/concerns about the review contact us in #juju on irc.freenode.net or email the mailing list <email address hidden>

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'README.md'
2--- README.md 2014-01-30 09:11:53 +0000
3+++ README.md 2014-05-21 18:34:21 +0000
4@@ -63,6 +63,9 @@
5 proxying -- obviously no variables need to be specified if no
6 proxying is needed.
7
8+Virtual host templates can also be specified via relation. See the
9+vhost-config relation section below for more information.
10+
11 ### Using the reverseproxy relation
12
13 The charm will create the service variable, with the `unit_name`,
14@@ -183,6 +186,58 @@
15 RewriteRule ^/(.*)$ balancer://gunicorn/$1 [P,L]
16 </VirtualHost>
17
18+### Using the vhost-config relation
19+
20+The nice thing about this relation, is as long as a charm support it, deploying
21+apache as a front-end for a web service should be as simple as establishing the
22+relation. If you need more details for how to implement this, read on.
23+
24+The template files themselves can be specified via this relation. This makes
25+deployment of your infrastructure simpler, since users no longer need to
26+specify a vhosts config option when using apache2 (though they still can). A
27+candidate charm should provide a relation on the `apache-vhost-config`
28+interface. This charm should simply set the following data when relating:
29+
30+ relation-set vhosts="- {port: '443', template: dGVtcGxhdGU=}\n- {port: '80', template: dGVtcGxhdGU=}\n"
31+
32+Notice the `vhosts` definition is in yaml, the format is simple. `vhosts`
33+should contain a yaml encoded data structure of a list of key value hashes, or
34+dictionaries. In each dictionary, `port` should be set to the port this vhost
35+should listen on, `template` should be set to the base64 encoded template file.
36+You can include as many of these dictionaries as you would like. If you have
37+colliding port numbers across your juju infrastructure, the results will be a
38+bit unpredictable.
39+
40+For example, if using python for your relating charm, the code to generate a
41+yaml_string for a vhost on port `80` would be similar to this:
42+
43+ import yaml
44+ import base64
45+ template = get_template()
46+ vhosts = [{"port": "80", "template": base64.b64encode(template)}]
47+ yaml_string = yaml.dump(vhosts)
48+
49+Note, that if you are opening a non-standard port (80 and 443 are opened and
50+understood by the default install of apache2 in Ubuntu) you will need to
51+instruct Apache to `Listen` on that port in your vhost file. Something like the
52+following will work in your vhost template:
53+
54+ Listen 8080
55+ <VirtualHost *:8080>
56+ ...
57+ </VirtualHost>
58+
59+
60+#### Relation settings that apache2 provides
61+
62+When your charm relates it will be provided with the following:
63+
64+ * `servername` - The Apache2 servername. This is typically needed by web
65+ applications so they know how to write URLs.
66+
67+ * `ssl_cert` - If you asked for a selfsigned certificate, that cert will
68+ be available in this setting as a base64 encoded string.
69+
70
71 ## Certs, keys and chains
72
73
74=== modified file 'hooks/hooks.py'
75--- hooks/hooks.py 2014-02-08 15:03:11 +0000
76+++ hooks/hooks.py 2014-05-21 18:34:21 +0000
77@@ -18,6 +18,8 @@
78 log,
79 config as orig_config_get,
80 relations_of_type,
81+ relation_set,
82+ relation_ids,
83 unit_get
84 )
85 from charmhelpers.contrib.charmsupport import nrpe
86@@ -29,6 +31,7 @@
87 service_affecting_packages = ['apache2']
88 default_apache22_config_dir = "/etc/apache2/conf.d"
89 default_apache24_config_dir = "/etc/apache2/conf-available"
90+default_apache_base_dir = "/etc/apache2"
91
92 juju_warning_header = """#
93 # " "
94@@ -140,9 +143,9 @@
95
96 def site_filename(name, enabled=False):
97 if enabled:
98- sites_dir = "/etc/apache2/sites-enabled"
99+ sites_dir = "%s/sites-enabled" % default_apache_base_dir
100 else:
101- sites_dir = "/etc/apache2/sites-available"
102+ sites_dir = "%s/sites-available" % default_apache_base_dir
103
104 if is_apache24():
105 return "{}/{}.conf".format(sites_dir, name)
106@@ -177,6 +180,33 @@
107 return True
108
109
110+def _get_key_file_location(config_data):
111+ """Look at the config, generate the key file location."""
112+ key_file = None
113+ if config_data['ssl_keylocation']:
114+ key_file = '/etc/ssl/private/%s' % \
115+ (config_data['ssl_keylocation'].rpartition('/')[2])
116+ return key_file
117+
118+
119+def _get_cert_file_location(config_data):
120+ """Look at the config, generate the cert file location."""
121+ cert_file = None
122+ if config_data['ssl_certlocation']:
123+ cert_file = '/etc/ssl/certs/%s' % \
124+ (config_data['ssl_certlocation'].rpartition('/')[2])
125+ return cert_file
126+
127+
128+def _get_chain_file_location(config_data):
129+ """Look at the config, generate the chain file location."""
130+ chain_file = None
131+ if config_data['ssl_chainlocation']:
132+ chain_file = '/etc/ssl/certs/%s' % \
133+ (config_data['ssl_chainlocation'].rpartition('/')[2])
134+ return chain_file
135+
136+
137 ###############################################################################
138 # Hook functions
139 ###############################################################################
140@@ -402,6 +432,41 @@
141 logrotate_conf.write(str(template))
142
143
144+def create_vhost(port, protocol=None, config_key=None, template_str=None,
145+ config_data={}, relationship_data={}):
146+ """
147+ Create and enable a vhost in apache.
148+
149+ @param port: port on which to listen (int)
150+ @param protocol: used to name the vhost file intelligently. If not
151+ specified the port will be used instead. (ex: http, https)
152+ @param config_key: key in the configuration to look up to
153+ retrieve the template.
154+ @param template_str: The template itself as a string.
155+ @param config_data: juju get-config configuration data.
156+ @param relationship_data: if in a relationship, pass in the appropriate
157+ structure. This will be used to inform the template.
158+ """
159+ if protocol is None:
160+ protocol = str(port)
161+ close_port(port)
162+ if template_str is None:
163+ if not config_key or not config_data[config_key]:
164+ log("Vhost Template not provided, not configuring: %s" % port)
165+ return
166+ template_str = config_data[config_key]
167+ from jinja2 import Template
168+ template = Template(str(base64.b64decode(template_str)))
169+ template_data = dict(config_data.items() + relationship_data.items())
170+ vhost_name = '%s_%s' % (config_data['servername'], protocol)
171+ vhost_file = site_filename(vhost_name)
172+ log("Writing file %s with config and relation data" % vhost_file)
173+ with open(vhost_file, 'w') as vhost:
174+ vhost.write(str(template.render(template_data)))
175+ open_port(port)
176+ subprocess.call(['/usr/sbin/a2ensite', vhost_name])
177+
178+
179 def config_changed():
180 relationship_data = {}
181 config_data = config_get()
182@@ -435,35 +500,19 @@
183 apt_get_purge('apache2-mpm-worker')
184 create_mpm_workerfile()
185 create_security()
186+
187 ports = {'http': 80, 'https': 443}
188- for proto in ports.keys():
189- template_var = 'vhost_%s_template' % (proto)
190- template_data = dict(config_data.items() + relationship_data.items())
191- close_port(ports[proto])
192- if template_var in config_data:
193- vhost_name = '%s_%s' % (config_data['servername'], proto)
194- vhost_file = site_filename(vhost_name)
195- from jinja2 import Template
196- template = Template(
197- str(base64.b64decode(config_data[template_var])))
198- log("Writing file %s with config and relation data" % vhost_file)
199- with open(vhost_file, 'w') as vhost:
200- vhost.write(str(template.render(template_data)))
201- open_port(ports[proto])
202- subprocess.call(['/usr/sbin/a2ensite', vhost_name])
203+ for protocol, port in ports.iteritems():
204+ create_vhost(
205+ port,
206+ protocol=protocol,
207+ config_key="vhost_%s_template" % protocol,
208+ config_data=config_data,
209+ relationship_data=relationship_data)
210
211- cert_file = None
212- if config_data['ssl_certlocation']:
213- cert_file = '/etc/ssl/certs/%s' % \
214- (config_data['ssl_certlocation'].rpartition('/')[2])
215- key_file = None
216- if config_data['ssl_keylocation']:
217- key_file = '/etc/ssl/private/%s' % \
218- (config_data['ssl_keylocation'].rpartition('/')[2])
219- chain_file = None
220- if config_data['ssl_chainlocation']:
221- chain_file = '/etc/ssl/certs/%s' % \
222- (config_data['ssl_chainlocation'].rpartition('/')[2])
223+ cert_file = _get_cert_file_location(config_data)
224+ key_file = _get_key_file_location(config_data)
225+ chain_file = _get_chain_file_location(config_data)
226
227 if cert_file is not None and key_file is not None:
228 # ssl_cert is SELFSIGNED so generate self-signed certificate for use.
229@@ -567,14 +616,62 @@
230 if not os.path.exists('/etc/apache2/security'):
231 os.mkdir('/etc/apache2/security', 0755)
232 with open('/etc/apache2/security/allowed-ops.txt', 'w') as f:
233- f.write(config_data['openid_provider'].replace(',','\n'))
234+ f.write(config_data['openid_provider'].replace(',', '\n'))
235 f.write('\n')
236 os.chmod(key_file, 0444)
237
238+ update_vhost_config_relation()
239 update_nrpe_checks()
240 ship_logrotate_conf()
241
242
243+def update_vhost_config_relation():
244+ """
245+ Update the vhost file and include the certificate in the relation
246+ if it is self-signed.
247+ """
248+ relation_data = relations_of_type("vhost-config")
249+ config_data = config_get()
250+ if relation_data is None:
251+ return
252+
253+ for unit_data in relation_data:
254+ if "vhosts" in unit_data:
255+ all_relation_data = {}
256+ all_relation_data.update(
257+ get_reverseproxy_data(relation='reverseproxy'))
258+ all_relation_data.update(
259+ get_reverseproxy_data(relation='website-cache'))
260+ try:
261+ vhosts = yaml.safe_load(unit_data["vhosts"])
262+ for vhost in vhosts:
263+ create_vhost(
264+ vhost["port"],
265+ template_str=vhost["template"],
266+ config_data=config_data,
267+ relationship_data=all_relation_data)
268+ except Exception as e:
269+ log("Error reading configuration data from relation! %s" % e)
270+ raise
271+
272+ if service_apache2("check"):
273+ service_apache2("reload")
274+
275+ vhost_relation_settings = {
276+ "servername": config_data["servername"]}
277+
278+ cert_file = _get_cert_file_location(config_data)
279+ key_file = _get_key_file_location(config_data)
280+
281+ if cert_file is not None and key_file is not None:
282+ if config_data['ssl_cert'] and config_data['ssl_cert'] == "SELFSIGNED":
283+ with open(cert_file, 'r') as f:
284+ cert = base64.b64encode(f.read())
285+ vhost_relation_settings["ssl_cert"] = cert
286+ for id in relation_ids("vhost-config"):
287+ relation_set(relation_id=id, relation_settings=vhost_relation_settings)
288+
289+
290 def start_hook():
291 if service_apache2("status"):
292 return(service_apache2("restart"))
293@@ -640,6 +737,8 @@
294 elif hook_name in ("nrpe-external-master-relation-changed",
295 "local-monitors-relation-changed"):
296 update_nrpe_checks()
297+ elif hook_name == "vhost-config-relation-changed":
298+ config_changed()
299 else:
300 print "Unknown hook"
301 sys.exit(1)
302
303=== added file 'hooks/tests/test_config_changed.py'
304--- hooks/tests/test_config_changed.py 1970-01-01 00:00:00 +0000
305+++ hooks/tests/test_config_changed.py 2014-05-21 18:34:21 +0000
306@@ -0,0 +1,73 @@
307+from testtools import TestCase
308+import hooks
309+from mock import patch
310+import shutil
311+import tempfile
312+import os
313+
314+
315+class ConfigChangedTest(TestCase):
316+ def setUp(self):
317+ super(ConfigChangedTest, self).setUp()
318+ self.dirname = tempfile.mkdtemp()
319+
320+ def tearDown(self):
321+ super(ConfigChangedTest, self).tearDown()
322+ if os.path.exists(self.dirname):
323+ shutil.rmtree(self.dirname)
324+
325+ @patch('hooks.subprocess.call')
326+ @patch('hooks.close_port')
327+ @patch('hooks.open_port')
328+ @patch('hooks.conf_disable')
329+ @patch('hooks.ensure_extra_packages')
330+ @patch('hooks.ensure_package_status')
331+ @patch('hooks.ship_logrotate_conf')
332+ @patch('hooks.update_nrpe_checks')
333+ @patch('hooks.run')
334+ @patch('hooks.create_security')
335+ @patch('hooks.create_mpm_workerfile')
336+ @patch('hooks.log')
337+ @patch('hooks.relation_ids')
338+ @patch('hooks.relations_of_type')
339+ @patch('hooks.config_get')
340+ @patch('hooks.get_reverseproxy_data')
341+ @patch('hooks.service_apache2')
342+ @patch('hooks.relation_set')
343+ def test_config_changed_ensure_empty_site_dir(
344+ self, mock_relation_set, mock_service_apache2,
345+ mock_reverseproxy, mock_config_get,
346+ mock_relations_of_type, mock_relation_ids, mock_log,
347+ mock_create_mpm_workerfile, mock_create_security, mock_run,
348+ mock_update_nrpe_checks, mock_ship_logrotate_conf,
349+ mock_ensure_package_status, mock_ensure_extra_packages,
350+ mock_conf_disable, mock_open_port, mock_close_port, mock_call):
351+ """config-changed hook: Site directories should be empty."""
352+ mock_config_get.return_value = {
353+ "ssl_cert": "",
354+ "ssl_key": "",
355+ "package_status": "",
356+ "enable_modules": "",
357+ "disable_modules": "",
358+ "mpm_type": "",
359+ "ssl_certlocation": "",
360+ "ssl_keylocation": "",
361+ "ssl_chainlocation": "",
362+ "use_rsyslog": "",
363+ "config_change_command": "",
364+ "openid_provider": "",
365+ "servername": "foobar",
366+ "vhost_http_template": "",
367+ "vhost_https_template": "",
368+ }
369+ hooks.default_apache_base_dir = self.dirname
370+ hooks.default_apache22_config_dir = "%s/conf.d" % self.dirname
371+ hooks.default_apache24_config_dir = "%s/conf-available" % self.dirname
372+ os.mkdir("%s/sites-enabled" % self.dirname)
373+ os.mkdir("%s/sites-available" % self.dirname)
374+ os.mkdir("%s/conf.d" % self.dirname)
375+ hooks.config_changed()
376+ self.assertEqual(
377+ len(os.listdir("%s/%s" % (self.dirname, "sites-enabled"))), 0)
378+ self.assertEqual(
379+ len(os.listdir("%s/%s" % (self.dirname, "sites-available"))), 0)
380
381=== added file 'hooks/tests/test_create_vhost.py'
382--- hooks/tests/test_create_vhost.py 1970-01-01 00:00:00 +0000
383+++ hooks/tests/test_create_vhost.py 2014-05-21 18:34:21 +0000
384@@ -0,0 +1,106 @@
385+from testtools import TestCase
386+import hooks
387+import base64
388+import tempfile
389+from mock import patch
390+
391+
392+class CreateVhostTest(TestCase):
393+ def setUp(self):
394+ super(CreateVhostTest, self).setUp()
395+
396+ @patch('hooks.log')
397+ @patch('hooks.close_port')
398+ def test_create_vhost_no_template(self, mock_close_port, mock_log):
399+ """User did not specify a template, error logged."""
400+ hooks.create_vhost("80")
401+ mock_log.assert_called_once_with(
402+ 'Vhost Template not provided, not configuring: 80')
403+
404+ @patch('hooks.close_port')
405+ @patch('hooks.site_filename')
406+ @patch('hooks.open_port')
407+ @patch('hooks.subprocess.call')
408+ def test_create_vhost_template_name_port(
409+ self, mock_call, mock_open_port, mock_site_filename,
410+ mock_close_port):
411+ """Check that name generated is sane as a port."""
412+ config = {"servername": "unused"}
413+ file = tempfile.NamedTemporaryFile()
414+ filename = file.name
415+ mock_site_filename.return_value = filename
416+ hooks.create_vhost(
417+ "80",
418+ config_data=config,
419+ template_str=base64.b64encode("foo"))
420+ mock_site_filename.assert_called_once_with("unused_80")
421+
422+ @patch('hooks.close_port')
423+ @patch('hooks.site_filename')
424+ @patch('hooks.open_port')
425+ @patch('hooks.subprocess.call')
426+ def test_create_vhost_template_name_protocol(
427+ self, mock_call, mock_open_port, mock_site_filename,
428+ mock_close_port):
429+ """Check that name generated is sane as a protocol."""
430+ config = {"servername": "unused"}
431+ file = tempfile.NamedTemporaryFile()
432+ filename = file.name
433+ mock_site_filename.return_value = filename
434+ hooks.create_vhost(
435+ "80",
436+ protocol="httpfoo",
437+ config_data=config,
438+ template_str=base64.b64encode("foo"))
439+ mock_site_filename.assert_called_once_with("unused_httpfoo")
440+
441+ @patch('hooks.close_port')
442+ @patch('hooks.site_filename')
443+ @patch('hooks.open_port')
444+ @patch('hooks.subprocess.call')
445+ def test_create_vhost_template(
446+ self, mock_call, mock_open_port, mock_site_filename,
447+ mock_close_port):
448+ """
449+ Template passed in as string.
450+
451+ Verify relationship and config inform template as well.
452+ """
453+ template = ("{{servername}} {{ foo }}")
454+ config = {"servername": "test_only"}
455+ relationship = {"foo": "bar"}
456+ file = tempfile.NamedTemporaryFile()
457+ filename = file.name
458+ mock_site_filename.return_value = filename
459+ hooks.create_vhost(
460+ "80",
461+ config_data=config,
462+ relationship_data=relationship,
463+ template_str=base64.b64encode(template))
464+ with open(filename, 'r') as f:
465+ contents = f.read()
466+ self.assertEqual(contents, "test_only bar")
467+
468+ @patch('hooks.close_port')
469+ @patch('hooks.site_filename')
470+ @patch('hooks.open_port')
471+ @patch('hooks.subprocess.call')
472+ def test_create_vhost_template_config(
473+ self, mock_call, mock_open_port, mock_site_filename,
474+ mock_close_port):
475+ """Template passed in as config setting."""
476+ template = ("one\n"
477+ "two\n"
478+ "three")
479+ config = {"servername": "unused",
480+ "vhost_template": base64.b64encode(template)}
481+ file = tempfile.NamedTemporaryFile()
482+ filename = file.name
483+ mock_site_filename.return_value = filename
484+ hooks.create_vhost(
485+ "80",
486+ config_key="vhost_template",
487+ config_data=config)
488+ with open(filename, 'r') as f:
489+ contents = f.read()
490+ self.assertEqual(contents, template)
491
492=== added file 'hooks/tests/test_hooks.py'
493--- hooks/tests/test_hooks.py 1970-01-01 00:00:00 +0000
494+++ hooks/tests/test_hooks.py 2014-05-21 18:34:21 +0000
495@@ -0,0 +1,34 @@
496+from testtools import TestCase
497+import hooks
498+
499+
500+class HooksTest(TestCase):
501+ def test__get_key_file_location_empty(self):
502+ """No ssl_keylocation, expect None."""
503+ self.assertEqual(hooks._get_key_file_location(
504+ {"ssl_keylocation": None}), None)
505+
506+ def test__get_key_file_location(self):
507+ """ssl_keylocation, expect correct path."""
508+ self.assertEqual(hooks._get_key_file_location(
509+ {"ssl_keylocation": "foo"}), "/etc/ssl/private/foo")
510+
511+ def test__get_cert_file_location_empty(self):
512+ """No ssl_keylocation, expect None."""
513+ self.assertEqual(hooks._get_cert_file_location(
514+ {"ssl_certlocation": None}), None)
515+
516+ def test__get_cert_file_location(self):
517+ """ssl_keylocation, expect correct path."""
518+ self.assertEqual(hooks._get_cert_file_location(
519+ {"ssl_certlocation": "foo"}), "/etc/ssl/certs/foo")
520+
521+ def test__get_chain_file_location_empty(self):
522+ """No ssl_keylocation, expect None."""
523+ self.assertEqual(hooks._get_chain_file_location(
524+ {"ssl_chainlocation": None}), None)
525+
526+ def test__get_chain_file_location(self):
527+ """ssl_keylocation, expect correct path."""
528+ self.assertEqual(hooks._get_chain_file_location(
529+ {"ssl_chainlocation": "foo"}), "/etc/ssl/certs/foo")
530
531=== added file 'hooks/tests/test_vhost_config_relation.py'
532--- hooks/tests/test_vhost_config_relation.py 1970-01-01 00:00:00 +0000
533+++ hooks/tests/test_vhost_config_relation.py 2014-05-21 18:34:21 +0000
534@@ -0,0 +1,215 @@
535+from testtools import TestCase
536+import hooks
537+from base64 import b64encode
538+from mock import patch, call
539+import yaml
540+import tempfile
541+import os
542+import shutil
543+
544+
545+class CreateVhostTest(TestCase):
546+
547+ def setUp(self):
548+ super(CreateVhostTest, self).setUp()
549+ self.dirname = tempfile.mkdtemp()
550+ os.mkdir("%s/sites-enabled" % self.dirname)
551+ os.mkdir("%s/sites-available" % self.dirname)
552+ os.mkdir("%s/conf.d" % self.dirname)
553+ hooks.default_apache_base_dir = self.dirname
554+ hooks.default_apache22_config_dir = "%s/conf.d" % self.dirname
555+ hooks.default_apache24_config_dir = "%s/conf-available" % self.dirname
556+
557+ def tearDown(self):
558+ super(CreateVhostTest, self).tearDown()
559+ if os.path.exists(self.dirname):
560+ shutil.rmtree(self.dirname)
561+
562+ @patch('hooks.log')
563+ @patch('subprocess.call')
564+ @patch('hooks.close_port')
565+ def test_create_vhost_missing_template(
566+ self, mock_close_port, mock_call, mock_log):
567+ """Create a vhost file, check contents."""
568+ hooks.create_vhost(80)
569+ mock_log.assert_called_once_with(
570+ "Vhost Template not provided, not configuring: %s" % 80)
571+ mock_close_port.assert_called_once()
572+ self.assertEqual(
573+ len(os.listdir("%s/%s" % (self.dirname, "sites-available"))), 0)
574+
575+ @patch('hooks.log')
576+ @patch('subprocess.call')
577+ @patch('hooks.close_port')
578+ @patch('hooks.open_port')
579+ def test_create_vhost_template_through_config_no_protocol(
580+ self, mock_open_port, mock_close_port, mock_call, mock_log):
581+ """Create a vhost file, check contents."""
582+ template = b64encode("http://{{ variable }}/")
583+ config_data = {
584+ "template": template,
585+ "servername": "test_only"}
586+ relationship_data = {
587+ "variable": "fantastic"}
588+ hooks.create_vhost(
589+ 80, config_data=config_data, config_key="template",
590+ relationship_data=relationship_data)
591+ filename = hooks.site_filename("test_only_80")
592+ self.assertTrue(os.path.exists(filename))
593+ with open(filename, 'r') as file:
594+ contents = file.read()
595+ self.assertEqual(contents, 'http://fantastic/')
596+ mock_open_port.assert_called_once()
597+ mock_close_port.assert_called_once()
598+ self.assertEqual(
599+ len(os.listdir("%s/%s" % (self.dirname, "sites-available"))), 1)
600+
601+ @patch('hooks.log')
602+ @patch('subprocess.call')
603+ @patch('hooks.close_port')
604+ @patch('hooks.open_port')
605+ def test_create_vhost_template_through_config_with_protocol(
606+ self, mock_open_port, mock_close_port, mock_call, mock_log):
607+ """Create a vhost file, check contents."""
608+ template = b64encode("http://{{ variable }}/")
609+ config_data = {
610+ "template": template,
611+ "servername": "test_only"}
612+ relationship_data = {
613+ "variable": "fantastic"}
614+ hooks.create_vhost(
615+ 80, config_data=config_data, config_key="template",
616+ protocol='http', relationship_data=relationship_data)
617+ filename = hooks.site_filename("test_only_http")
618+ self.assertTrue(os.path.exists(filename))
619+ with open(filename, 'r') as file:
620+ contents = file.read()
621+ self.assertEqual(contents, 'http://fantastic/')
622+ mock_open_port.assert_called_once()
623+ mock_close_port.assert_called_once()
624+ self.assertEqual(
625+ len(os.listdir("%s/%s" % (self.dirname, "sites-available"))), 1)
626+
627+ @patch('hooks.log')
628+ @patch('subprocess.call')
629+ @patch('hooks.close_port')
630+ @patch('hooks.open_port')
631+ def test_create_vhost_template_directly(
632+ self, mock_open_port, mock_close_port,
633+ mock_call, mock_log):
634+ """Create a vhost file, check contents."""
635+ template = b64encode("http://{{ variable }}/")
636+ config_data = {
637+ "servername": "test_only"}
638+ relationship_data = {
639+ "variable": "fantastic"}
640+ hooks.create_vhost(
641+ 80, template_str=template, config_data=config_data,
642+ config_key="template", relationship_data=relationship_data)
643+ filename = hooks.site_filename("test_only_80")
644+ self.assertTrue(os.path.exists(filename))
645+ with open(filename, 'r') as file:
646+ contents = file.read()
647+ self.assertEqual(contents, 'http://fantastic/')
648+ mock_open_port.assert_called_once()
649+ mock_close_port.assert_called_once()
650+ self.assertEqual(
651+ len(os.listdir("%s/%s" % (self.dirname, "sites-available"))), 1)
652+
653+
654+class VhostConfigRelationTest(TestCase):
655+ @patch('hooks.service_apache2')
656+ @patch('hooks.relation_ids')
657+ @patch('hooks.relations_of_type')
658+ @patch('hooks.get_reverseproxy_data')
659+ @patch('hooks.config_get')
660+ def test_vhost_config_relation_changed_no_relation_data(
661+ self, mock_config_get, mock_relation_get,
662+ mock_relations_of_type, mock_relation_ids,
663+ mock_service_apache2):
664+ """No relation data, do nothing."""
665+ mock_relation_get.return_value = None
666+ hooks.update_vhost_config_relation()
667+
668+ @patch('hooks.relations_of_type')
669+ @patch('hooks.service_apache2')
670+ @patch('hooks.config_get')
671+ @patch('hooks.get_reverseproxy_data')
672+ @patch('hooks.log')
673+ def test_vhost_config_relation_changed_vhost_ports_only(
674+ self, mock_log, mock_reverseproxy, mock_config_get,
675+ mock_service_apache2, mock_relations_of_type):
676+ """vhost_ports only specified, hook should exit with error"""
677+ mock_relations_of_type.return_value = [
678+ {'vhosts': yaml.dump([{'port': "5555"}])}]
679+ mock_config_get.return_value = {}
680+ self.assertRaisesRegexp(
681+ KeyError, "template", hooks.update_vhost_config_relation)
682+
683+ @patch('hooks.log')
684+ @patch('hooks.relation_ids')
685+ @patch('hooks.relations_of_type')
686+ @patch('hooks.config_get')
687+ @patch('hooks.get_reverseproxy_data')
688+ @patch('hooks.create_vhost')
689+ @patch('hooks.service_apache2')
690+ @patch('hooks.relation_set')
691+ def test_vhost_config_relation_changed_vhost_ports_single(
692+ self, mock_relation_set, mock_service_apache2,
693+ mock_create_vhost, mock_reverseproxy, mock_config_get,
694+ mock_relations_of_type, mock_relation_ids, mock_log):
695+ """A single vhost entry is created."""
696+ mock_relation_ids.return_value = ["testonly"]
697+ rel = {"vhosts": yaml.dump([{
698+ 'port': '80',
699+ 'template': b64encode("foo")
700+ }])}
701+ config_data = {
702+ "servername": "unused",
703+ "ssl_certlocation": "unused",
704+ "ssl_keylocation": "unused",
705+ "ssl_cert": ""}
706+ mock_config_get.return_value = config_data
707+ mock_relations_of_type.return_value = [rel]
708+ hooks.update_vhost_config_relation()
709+ mock_create_vhost.assert_called_once_with(
710+ "80",
711+ template_str=b64encode("foo"),
712+ config_data=config_data,
713+ relationship_data={}
714+ )
715+
716+ @patch('hooks.log')
717+ @patch('hooks.relation_ids')
718+ @patch('hooks.relations_of_type')
719+ @patch('hooks.config_get')
720+ @patch('hooks.get_reverseproxy_data')
721+ @patch('hooks.create_vhost')
722+ @patch('hooks.service_apache2')
723+ @patch('hooks.relation_set')
724+ def test_vhost_config_relation_changed_vhost_ports_multi(
725+ self, mock_relation_set, mock_service_apache2,
726+ mock_create_vhost, mock_reverseproxy, mock_config_get,
727+ mock_relations_of_type, mock_relation_ids, mock_log):
728+ """Multiple vhost entries are created."""
729+ mock_relation_ids.return_value = ["testonly"]
730+ rel = {"vhosts": yaml.dump([
731+ {'port': "80", 'template': b64encode("80")},
732+ {'port': "443", 'template': b64encode("443")},
733+ {'port': "444", 'template': b64encode("444")}])}
734+ mock_relations_of_type.return_value = [rel]
735+ config_data = {
736+ "servername": "unused",
737+ "ssl_certlocation": "unused",
738+ "ssl_keylocation": "unused",
739+ "ssl_cert": ""}
740+ mock_config_get.return_value = config_data
741+ hooks.update_vhost_config_relation()
742+ mock_create_vhost.assert_has_calls([
743+ call("80", template_str=b64encode("80"),
744+ config_data=config_data, relationship_data={}),
745+ call("443", template_str=b64encode("443"),
746+ config_data=config_data, relationship_data={}),
747+ call("444", template_str=b64encode("444"),
748+ config_data=config_data, relationship_data={})])
749+ self.assertEqual(mock_create_vhost.call_count, 3)
750
751=== added symlink 'hooks/vhost-config-relation-changed'
752=== target is u'hooks.py'
753=== modified file 'metadata.yaml'
754--- metadata.yaml 2013-05-28 20:19:08 +0000
755+++ metadata.yaml 2014-05-21 18:34:21 +0000
756@@ -27,3 +27,5 @@
757 interface: http
758 logging:
759 interface: syslog
760+ vhost-config:
761+ interface: apache-vhost-config

Subscribers

People subscribed via source and target branches

to all changes: