Merge lp:~dpb/charms/precise/apache2/trunk into lp:charms/apache2

Proposed by David Britton
Status: Merged
Approved by: Brandon Holtsclaw
Approved revision: no longer in the source branch.
Merged at revision: 35
Proposed branch: lp:~dpb/charms/precise/apache2/trunk
Merge into: lp:charms/apache2
Diff against target: 535 lines (+233/-78)
6 files modified
.bzrignore (+2/-0)
README.md (+74/-45)
config.yaml (+36/-5)
hooks/hooks.py (+66/-27)
revision (+1/-1)
scripts/gen-selfsigned-cert (+54/-0)
To merge this branch: bzr merge lp:~dpb/charms/precise/apache2/trunk
Reviewer Review Type Date Requested Status
Brandon Holtsclaw (community) Approve
Review via email: mp+146242@code.launchpad.net

Description of the change

- Fill out README around the reverseproxy use case.
- some minor code-cleanup in hooks.py
- strip disallowed characters from the jinja2 template data
- Add in ssl_key, ssl_cert to allow passing in base64 encoded versions of these files
- Add in ability to generate a self-signed certificate for testing
- Default servername to the public-address of the unit (which juju in turn falls back to private-address)

To post a comment you must log in.
Revision history for this message
Brandon Holtsclaw (imbrandon) wrote :

Changes all look good to me, I'd have like it if this was broken up into several MP of related changes in the future to make it easier to review but other than that Thanks!

review: Approve
lp:~dpb/charms/precise/apache2/trunk updated
35. By Brandon Holtsclaw

Merge of MP# 146242 - davidpbritton

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file '.bzrignore'
2--- .bzrignore 2012-10-26 02:41:40 +0000
3+++ .bzrignore 2013-02-01 23:20:27 +0000
4@@ -1,2 +1,4 @@
5 revision
6 basenode/
7+*.crt
8+*.key
9
10=== renamed file 'README' => 'README.md'
11--- README 2013-01-28 19:09:50 +0000
12+++ README.md 2013-02-01 23:20:27 +0000
13@@ -1,5 +1,5 @@
14 Juju charm apache
15-=====================
16+=================
17
18 The Apache Software Foundation's goal is to build a secure, efficient
19 and extensible HTTP server as standards-compliant open source
20@@ -9,46 +9,60 @@
21 filtering, many flexible authentication schemes, and more.
22
23 How to deploy the charm
24---------------------------
25-juju deploy apache2
26-juju set apache2 "vhost_http_template=$(base64 < vhost.tmpl)"
27-and or
28-juju set apache2 "vhost_https_template=$(base64 < vhost.tmpl)"
29+-----------------------
30+ juju deploy apache2
31+ juju set apache2 "vhost_http_template=$(base64 < vhost.tmpl)"
32+ # and / or
33+ juju set apache2 "vhost_https_template=$(base64 < vhost.tmpl)"
34+ juju add-relation apache2:reverseproxy haproxy:website
35
36 Vhost templates
37---------------------------
38+---------------
39 The charm expects a jinja2 template to be passed in. The variables in
40 the template should relate to the services that apache will be proxying
41-to (obviously no variables need to be specified if no proxying is needed).
42+-- obviously no variables need to be specified if no proxying is needed.
43
44 The charm will create the service variable, with the unit_name, when
45 the reverseproxy relationship is joined and present this to the template
46-at which point the vhost will be generated from the template again.
47+at which point the vhost will be generated from the template again.
48+All config settings are also available to the template.
49
50-For example to access squid then the {{{ squid }}} variable should be used.
51+For example to access squid then the {{ squid }} variable should be used.
52 This will be populated with the hostname:port of the squid service. The
53 individual hostname and port can also be accessed via squid_hostname and
54 squid_port.
55-Note: If an alias is used when deploying a charm then the alias name needs
56- to be used.
57-
58-If the joining charm also present an all_services variable which contains
59-a list of services it provides in yaml format then variables for each
60-service will be created of the format unitname_stanza. For example if
61-haproxy contains stanzas named gunicorn and solr these can be accessed
62-via {{{ haproxy_gunicorn }}} and {{ haproxy_solr }}}.
63-Note: Currently the haproxy does not seem to be generating individual stanzas
64- correctly
65+
66+Note: The service name should be used, not the charm name. If deploying
67+ a charm with a different service name, use that instaed.
68+
69+The joining charm may also set an all_services variable which contains
70+a list of services it provides in yaml format (list of associative arrays):
71+
72+ # ... in haproxy charm, website-relation-joined
73+ relation-set all_services="
74+ - {service_name: gunicorn, service_port: 80}
75+ - {service_name: solr, service_port: 8080}
76+ - {service_name: my-webapp, service_port: 9090}
77+ "
78+
79+then variables for each service would be available to the jinja2 template
80+in <juju_service_name>_<sub_service_name>. In our example above
81+haproxy contains stanzas named gunicorn, solr and my-webapp. These are
82+accessed as {{ haproxy_gunicorn }}, {{ haproxy_solr }} and
83+{{ haproxy_mywebapp }} respectively. If any unsupported characters are in
84+your juju service name or the service names exposed through "all_services",
85+they will be stripped.
86
87 For example a vhost that will pass all traffic on to an haproxy instance:
88-<VirtualHost *:80>
89- ServerName radiotiptop.org.uk
90-
91- CustomLog /var/log/apache2/radiotiptop-access.log combined
92- ErrorLog /var/log/apache2/radiotiptop-error.log
93-
94+
95+ <VirtualHost *:80>
96+ ServerName radiotiptop.org.uk
97+
98+ CustomLog /var/log/apache2/radiotiptop-access.log combined
99+ ErrorLog /var/log/apache2/radiotiptop-error.log
100+
101 DocumentRoot /srv/radiotiptop/www/root
102-
103+
104 ProxyRequests off
105 <Proxy *>
106 Order Allow,Deny
107@@ -58,33 +72,48 @@
108 ErrorDocument 502 /offline.html
109 ErrorDocument 503 /offline.html
110 </Proxy>
111-
112+
113 ProxyPreserveHost off
114 ProxyPassReverse / http://{{ haproxy_gunicorn }}/
115-
116+
117 RewriteEngine on
118-
119+
120 RewriteRule ^/$ /index.html [L]
121 RewriteRule ^/(.*)$ http://{{ haproxy_gunicorn }}/$1 [P,L]
122-
123-</VirtualHost>
124+ </VirtualHost>
125
126 Certs, keys and chains
127---------------------------
128+----------------------
129 ssl_keylocation, ssl_certlocation and ssl_chainlocation are file names in the
130-data directory.
131+charm /data directory. If found, they will be copied as follows:
132+
133+ - /etc/ssl/private/<ssl_keylocation>
134+ - /etc/ssl/certs/<ssl_certlocation>
135+ - /etc/ssl/certs/<ssl_chainlocation>
136+
137+ssl_key and ssl_cert can also be specified which are are assumed to be
138+base64 encoded. If specified, they will be written to appropriate directories
139+given the values in ssl_keylocation and ssl_certlocation as listed above.
140+
141+ssl_cert may also be set to SELFSIGNED, which will generate a certificate.
142+This, of course, is mostly useful for testing and staging purposes. The
143+generated certifcate/key will be placed according to ssl_certlocation and
144+ssl_keylocation as listed above.
145
146 {enable,disable}_modules
147---------------------------
148-Lists of modules to be enabled or disabled. If a module to be enabled cannot be found
149-then the charm will attempt to install it.
150-
151+------------------------
152+Space separated list of modules to be enabled or disabled. If a module to
153+be enabled cannot be found then the charm will attempt to install it.
154
155 TODO:
156-* Method to deliver site content. This maybe by converting the charm to a subordinate
157- and making it the master charms problem
158-* Delivery of SSL key. Implement secure method for delivering key.
159-* Tuning. No tuning options are present. Convert apache2.conf to a template and expose
160- config options
161-* Testing. I have only tested the relationship setup with 1 haproxy instance. Needs testing against
162- multiple instances
163+-----
164+
165+ * Document the use of balancer, nrpe, logging and website-cache
166+ * Method to deliver site content. This maybe by converting the charm to a
167+ subordinate and making it the master charms problem
168+ * Implement secure method for delivering key. Juju will likely need to provide
169+ this.
170+ * Tuning. No tuning options are present. Convert apache2.conf to a template
171+ and expose config options
172+ * Testing. I have only tested the relationship setup with 1 haproxy instance.
173+ Needs testing against multiple instances
174
175=== modified file 'config.yaml'
176--- config.yaml 2013-02-01 15:56:52 +0000
177+++ config.yaml 2013-02-01 23:20:27 +0000
178@@ -1,8 +1,8 @@
179 options:
180 servername:
181 type: string
182- default: 'myvhost'
183- description: ServerName for vhost
184+ default: ''
185+ description: ServerName for vhost, defaults to the units public-address
186 vhost_http_template:
187 type: string
188 default: ''
189@@ -35,15 +35,28 @@
190 ssl_keylocation:
191 type: string
192 default: ''
193- description: Location of ssl key
194+ description: |
195+ Name and location of ssl keyfile in charm/data directory.
196+ If not found, will ignore. Basename of this file will be used
197+ as the basename of the key rooted at /etc/ssl/private. Can
198+ be used in conjuntion with the ssl_key parameter to specify
199+ the key as a configuration setting.
200 ssl_certlocation:
201 type: string
202 default: ''
203- description: Location of ssl cert
204+ description: |
205+ Name and location of ssl certificate in charm/data directory.
206+ If not found, will ignore. Basename of this file will be used
207+ as the basename of the cert rooted at /etc/ssl/certs. Can
208+ be used in conjunction with the ssl_cert parameter to specify
209+ the cert as a configuration setting.
210 ssl_chainlocation:
211 type: string
212 default: ''
213- description: Location of ssl chain file
214+ description: |
215+ Name and location of the ssl chain file. Basename of this file
216+ will be used as the basename of the chain file rooted at
217+ /etc/ssl/certs.
218 lb_balancer_timeout:
219 type: int
220 default: 60
221@@ -108,6 +121,24 @@
222 description: >
223 Use daily extension like YYYMMDD instead of simply adding a number
224 default: True
225+ use_rsyslog:
226+ type: boolean
227+ description: >-
228+ Change logging behaviour to log both access and error logs via rsyslog
229+ default: False
230+ ssl_cert:
231+ type: string
232+ description: |
233+ base64 encoded server certificate. If the keyword 'SELFSIGNED'
234+ is used, the certificate and key will be autogenerated as
235+ self-signed.
236+ default: ''
237+ ssl_key:
238+ type: string
239+ description: |
240+ base64 encoded server certificate key. If ssl_cert is
241+ specified as SELFSIGNED, this will be ignored.
242+ default: ''
243 server_tokens:
244 type: string
245 description: Security setting. Set to one of Full OS Minimal Minor Major Prod
246
247=== modified file 'hooks/hooks.py'
248--- hooks/hooks.py 2013-02-01 15:56:52 +0000
249+++ hooks/hooks.py 2013-02-01 23:20:27 +0000
250@@ -41,6 +41,12 @@
251
252
253 #------------------------------------------------------------------------------
254+# juju_log: Convenience wrapper around juju-log
255+#------------------------------------------------------------------------------
256+def juju_log(msg="MARK"):
257+ subprocess.call(['juju-log', str(msg)])
258+
259+#------------------------------------------------------------------------------
260 # open_port: Convenience function to open a port in juju to
261 # expose a service
262 #------------------------------------------------------------------------------
263@@ -75,8 +81,12 @@
264 config_cmd_line.append(scope)
265 config_cmd_line.append('--format=json')
266 config_data = json.loads(subprocess.check_output(config_cmd_line))
267+ if not config_data["servername"]:
268+ config_data["servername"] = run(
269+ ["unit-get", "public-address"]).rstrip()
270+
271 except Exception, e:
272- subprocess.call(['juju-log', str(e)])
273+ juju_log(e)
274 config_data = None
275 finally:
276 return(config_data)
277@@ -101,7 +111,7 @@
278 if unit_name is not None:
279 relation_cmd_line.append('-')
280 relation_cmd_line.append(unit_name)
281- subprocess.call(['juju-log', 'Calling: %s' % relation_cmd_line])
282+ juju_log('Calling: %s' % relation_cmd_line)
283 relation_data = json.loads(subprocess.check_output(relation_cmd_line))
284 except Exception:
285 relation_data = None
286@@ -114,7 +124,7 @@
287 relation_cmd_line = ['relation-ids', '--format=json']
288 if relation_name is not None:
289 relation_cmd_line.append(relation_name)
290- subprocess.call(['juju-log', 'Calling: %s' % relation_cmd_line])
291+ juju_log('Calling: %s' % relation_cmd_line)
292 relation_ids = json.loads(subprocess.check_output(relation_cmd_line))
293 except Exception:
294 relation_ids = None
295@@ -127,7 +137,7 @@
296 relation_cmd_line = ['relation-list', '--format=json']
297 if relation_id is not None:
298 relation_cmd_line.extend(('-r', relation_id))
299- subprocess.call(['juju-log', 'Calling: %s' % relation_cmd_line])
300+ juju_log('Calling: %s' % relation_cmd_line)
301 relations = json.loads(subprocess.check_output(relation_cmd_line))
302 except Exception:
303 relations = None
304@@ -209,15 +219,12 @@
305 if module is None:
306 return(True)
307 if os.path.exists("/etc/apache2/mods-enabled/%s.load" % (module)):
308- subprocess.call(['juju-log', "Module already loaded"])
309+ juju_log("Module already loaded: %s" % module)
310 return(True)
311 if not os.path.exists("/etc/apache2/mods-available/%s.load" % (module)):
312 retVal = apt_get_install("libapache2-mod-%s" % (module))
313 if retVal != 0:
314- subprocess.call(
315- ['juju-log',
316- "Installing module %s failed" % (module)
317- ])
318+ juju_log("Installing module %s failed" % module)
319 return(False)
320 retVal = subprocess.call(['/usr/sbin/a2enmod', module])
321 if retVal != 0:
322@@ -231,7 +238,7 @@
323 if module is None:
324 return(True)
325 if not os.path.exists("/etc/apache2/mods-enabled/%s.load" % (module)):
326- subprocess.call(['juju-log', "Module already disabled"])
327+ juju_log("Module already disabled: %s" % module)
328 return(True)
329 retVal = subprocess.call(['/usr/sbin/a2dismod', module])
330 if retVal != 0:
331@@ -271,8 +278,14 @@
332 for unit_name in relation_data.keys():
333 if 'port' not in relation_data[unit_name]:
334 return reverseproxy_data
335+
336+ # unit_name: <service-name>-<unit_number>
337+ # jinja2 templates require python-type variables, remove all characters
338+ # that do not comply
339 unit_type = re.sub(r'(.*)-[0-9]*', r'\1', unit_name)
340- subprocess.call(['juju-log', 'unit_type: %s' % unit_type])
341+ unit_type = re.sub('[^a-zA-Z0-9_]*', '', unit_type)
342+ juju_log('unit_type: %s' % unit_type)
343+
344 host = relation_data[unit_name]['private-address']
345 for config_setting in relation_data[unit_name].keys():
346 config_key = '%s_%s' % (unit_type, config_setting)
347@@ -293,7 +306,7 @@
348 relation_data = all_relation_data_get(relation_name='balancer')
349 config_data = config_get()
350 if relation_data is None or len(relation_data) == 0:
351- subprocess.call(['juju-log', 'No relation data exiting'])
352+ juju_log('No relation data exiting')
353 return
354 unit_dict = {}
355 for unit_name in relation_data.keys():
356@@ -320,6 +333,7 @@
357 }
358 template = template_env.get_template(
359 'balancer.template').render(templ_vars)
360+ juju_log("Writing file: %s with data: %s" % (balancer_host_file, templ_vars))
361 with open(balancer_host_file, 'w') as balancer_config:
362 balancer_config.write(str(template))
363
364@@ -327,13 +341,13 @@
365 def update_nrpe_checks():
366 config_data = config_get()
367 if 'nagios_check_http_params' not in config_data or len(config_data['nagios_check_http_params']) == 0:
368- subprocess.call(['juju-log', "No vhost check data, exiting"])
369+ juju_log("No nrpe configuration, skipping")
370 return
371 try:
372 nagios_uid = pwd.getpwnam('nagios').pw_uid
373 nagios_gid = grp.getgrnam('nagios').gr_gid
374 except:
375- subprocess.call(['juju-log', "Nagios user not setup, exiting"])
376+ juju_log("Nagios user not setup, exiting")
377 return
378
379 unit_name = os.environ['JUJU_UNIT_NAME'].replace('/', '-')
380@@ -347,8 +361,7 @@
381 os.mkdir(nagios_logdir)
382 os.chown(nagios_logdir, nagios_uid, nagios_gid)
383 if not os.path.exists(nagios_exportdir):
384- subprocess.call(['juju-log', 'Exiting as %s is not accessible'
385- % (nagios_exportdir)])
386+ juju_log('Exiting as %s is not accessible' % nagios_exportdir)
387 return
388 for f in os.listdir(nagios_exportdir):
389 if re.search('.*check_vhost.cfg', f):
390@@ -454,6 +467,7 @@
391 ports = {'http': 80, 'https': 443}
392 for proto in ports.keys():
393 template_var = 'vhost_%s_template' % (proto)
394+ template_data = dict(config_data.items() + relationship_data.items())
395 close_port(ports[proto])
396 if template_var in config_data:
397 vhost_name = '%s_%s' % (config_data['servername'], proto)
398@@ -461,34 +475,59 @@
399 from jinja2 import Template
400 template = Template(
401 str(base64.b64decode(config_data[template_var])))
402+ juju_log("Writing file: %s with data: %s" % (vhost_file, template_data))
403 with open(vhost_file, 'w') as vhost:
404- vhost.write(
405- str(template.render(
406- dict(config_data.items() +
407- relationship_data.items()))))
408+ vhost.write(str(template.render(template_data)))
409 open_port(ports[proto])
410 subprocess.call(['/usr/sbin/a2ensite', vhost_name])
411
412+ cert_file = None
413 if config_data['ssl_certlocation']:
414+ source = os.path.join(
415+ os.environ['CHARM_DIR'], 'data', config_data['ssl_certlocation'])
416 cert_file = '/etc/ssl/certs/%s' % \
417 (config_data['ssl_certlocation'].rpartition('/')[2])
418- shutil.copy(os.path.join(os.environ['CHARM_DIR'], 'data',
419- config_data['ssl_certlocation']), cert_file)
420+ if os.path.exists(source):
421+ shutil.copy(source, cert_file)
422+ else:
423+ juju_log("Certificate not found, ignoring: %s" % source)
424
425+ chain_file = None
426 if config_data['ssl_chainlocation']:
427 chain_file = '/etc/ssl/certs/%s' % \
428 (config_data['ssl_chainlocation'].rpartition('/')[2])
429 shutil.copy(os.path.join(os.environ['CHARM_DIR'], 'data',
430 config_data['ssl_chainlocation']), chain_file)
431
432+ key_file = None
433 if config_data['ssl_keylocation']:
434+ source = os.path.join(
435+ os.environ['CHARM_DIR'], 'data', config_data['ssl_keylocation'])
436 key_file = '/etc/ssl/private/%s' % \
437 (config_data['ssl_keylocation'].rpartition('/')[2])
438- shutil.copy(os.path.join(os.environ['CHARM_DIR'], 'data',
439- config_data['ssl_keylocation']), key_file)
440- os.chmod(key_file, 0440)
441- os.chown(key_file, pwd.getpwnam('root').pw_uid,
442- grp.getgrnam('ssl-cert').gr_gid)
443+ if os.path.exists(source):
444+ shutil.copy(source, key_file)
445+ os.chmod(key_file, 0440)
446+ os.chown(key_file,
447+ pwd.getpwnam('root').pw_uid, grp.getgrnam('ssl-cert').gr_gid)
448+ else:
449+ juju_log("Key file not found, ignoring: %s" % source)
450+
451+ if config_data['ssl_cert'] and cert_file is not None:
452+ if config_data['ssl_cert'] == "SELFSIGNED" and key_file is not None:
453+ config_data['ssl_key'] = ""
454+ gen_cert = os.path.join(os.environ['CHARM_DIR'], 'scripts',
455+ 'gen-selfsigned-cert')
456+ run([gen_cert, key_file, cert_file])
457+ else:
458+ juju_log("Writing cert from config ssl_cert: %s" % cert_file)
459+ with open(cert_file, 'w') as f:
460+ f.write(str(base64.b64decode(config_data['ssl_cert'])))
461+
462+ if config_data['ssl_key'] and key_file is not None:
463+ juju_log("Writing key from config ssl_key: %s" % key_file)
464+ with open(key_file, 'w') as f:
465+ f.write(str(base64.b64decode(config_data['ssl_key'])))
466
467 # Disable the default website because we don't want people to see the
468 # "It works!" page on production services and remove the
469
470=== modified file 'revision'
471--- revision 2013-01-17 16:32:08 +0000
472+++ revision 2013-02-01 23:20:27 +0000
473@@ -1,1 +1,1 @@
474-1
475+2
476
477=== added directory 'scripts'
478=== added file 'scripts/gen-selfsigned-cert'
479--- scripts/gen-selfsigned-cert 1970-01-01 00:00:00 +0000
480+++ scripts/gen-selfsigned-cert 2013-02-01 23:20:27 +0000
481@@ -0,0 +1,54 @@
482+#!/bin/bash
483+#
484+# Simple helper to generate a certificate
485+# $1 = key location
486+# $2 = certificate location
487+# $3 = public-address (will use unit-get if unspecified)
488+# $4 = private-address (will use unit-get if unspecified)
489+
490+KEY_FILE=${1:-key}
491+CERT_FILE=${2:-cert}
492+PUBLIC=${3:-$(unit-get public-address)}
493+PRIVATE=${4:-$(unit-get private-address)}
494+juju-log "network data: $PUBLIC $PRIVATE"
495+juju-log "Generating cert: $CERT_FILE / key: $KEY_FILE"
496+
497+gen_certificate() {
498+ CN=$PUBLIC
499+ tmpfile=$(mktemp /tmp/XXXXXX) || exit 1
500+ cat > $tmpfile <<EOF
501+RANDFILE = /dev/urandom
502+
503+[ req ]
504+default_bits = 1024
505+default_keyfile = privkey.pem
506+distinguished_name = req_distinguished_name
507+prompt = no
508+policy = policy_anything
509+x509_extensions = v3_ca
510+
511+[ req_distinguished_name ]
512+commonName = $CN
513+
514+[ v3_ca ]
515+# Extensions to add to a certificate request
516+subjectAltName = @alt_names
517+
518+[alt_names]
519+DNS.1 = $PUBLIC
520+DNS.2 = $PRIVATE
521+EOF
522+ cat $tmpfile
523+
524+ openssl req \
525+ -new -x509 -nodes -days 3650 \
526+ -config $tmpfile \
527+ -keyout $KEY_FILE
528+ chmod 0440 $KEYFILE
529+ chown root:ssl-cert $KEYFILE
530+
531+ rm -f $tmpfile
532+}
533+
534+cert=$(gen_certificate)
535+echo "$cert" > $CERT_FILE

Subscribers

People subscribed via source and target branches

to all changes: