Merge lp:~dpb/charms/precise/apache2/vhost-config-relation into lp:charms/apache2
- Precise Pangolin (12.04)
- vhost-config-relation
- Merge into trunk
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 |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Charles Butler (community) | Approve | ||
Review via email: mp+220295@code.launchpad.net |
Commit message
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).
- 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.
Preview Diff
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 |
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>