Merge lp:~dpb/charms/trusty/apache2/avoid-regen-cert into lp:charms/trusty/apache2

Proposed by David Britton
Status: Merged
Merged at revision: 56
Proposed branch: lp:~dpb/charms/trusty/apache2/avoid-regen-cert
Merge into: lp:charms/trusty/apache2
Diff against target: 177 lines (+86/-16)
3 files modified
README.md (+2/-1)
hooks/hooks.py (+75/-15)
hooks/tests/test_balancer_hook.py (+9/-0)
To merge this branch: bzr merge lp:~dpb/charms/trusty/apache2/avoid-regen-cert
Reviewer Review Type Date Requested Status
Charles Butler (community) Approve
Tim Van Steenburgh (community) Approve
Chris Glass (community) Approve
Review via email: mp+223990@code.launchpad.net

Commit message

Don't regen a self-signed cert unless we need to

Description of the change

MP For Precise: https://code.launchpad.net/~davidpbritton/charms/precise/apache2/avoid-regen-cert/+merge/221102

Don't regen a self-signed cert unless we need to:

- pulled out cert generation code
- added test cases for it
- made regeneration dependent on necessity (hostname/ip changed, servername changed, cert is missing/invalid)
- Included small fix for lp:1302645 to install python-yaml

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

merge up

57. By David Britton

apply same importerror check to trusty, keeping precise and trusty in sync.

Revision history for this message
Chris Glass (tribaal) wrote :

Looks good!

review: Approve
Revision history for this message
Tim Van Steenburgh (tvansteenburgh) wrote :

+1 LGTM.

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

DPB, Thanks for the patch submission! I noticed this behavior myself and this is a most welcome fix. The tests are a big +1

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-05-21 18:31:11 +0000
3+++ README.md 2014-06-28 19:27:49 +0000
4@@ -17,7 +17,8 @@
5 sudo apt-get install python-software-properties
6 sudo add-apt-repository ppa:chrisjohnston/flake8
7 sudo apt-get update
8- sudo apt-get install flake8 python-nose python-coverage python-testtools
9+ sudo apt-get install python-flake8 python-nose python-coverage \
10+ python-testtools python-pyasn1 python-pyasn1-modules
11
12 To fetch additional source dependencies and run the tests:
13
14
15=== modified file 'hooks/hooks.py'
16--- hooks/hooks.py 2014-06-23 19:40:44 +0000
17+++ hooks/hooks.py 2014-06-28 19:27:49 +0000
18@@ -49,14 +49,12 @@
19 # Supporting functions
20 ###############################################################################
21
22-#------------------------------------------------------------------------------
23-# apt_get_install( package ): Installs a package
24-#------------------------------------------------------------------------------
25-def apt_get_install(packages=None):
26- if packages is None:
27+def apt_get_install(package=None):
28+ """Install a package."""
29+ if package is None:
30 return False
31 cmd_line = ['apt-get', '-y', 'install', '-qq']
32- cmd_line.append(packages)
33+ cmd_line.append(package)
34 return subprocess.call(cmd_line)
35
36
37@@ -180,6 +178,72 @@
38 return True
39
40
41+def gen_selfsigned_cert(config, cert_file, key_file):
42+ """
43+ Create a self-signed certificate.
44+
45+ @param config: charm data from config-get
46+ @param cert_file: destination path of generated certificate
47+ @param key_file: destination path of generated private key
48+ """
49+ os.environ['OPENSSL_CN'] = config['servername']
50+ os.environ['OPENSSL_PUBLIC'] = unit_get("public-address")
51+ os.environ['OPENSSL_PRIVATE'] = unit_get("private-address")
52+ run(
53+ ['openssl', 'req', '-new', '-x509', '-nodes', '-config',
54+ os.path.join(os.environ['CHARM_DIR'], 'data', 'openssl.cnf'),
55+ '-keyout', key_file, '-out', cert_file])
56+
57+
58+def is_selfsigned_cert_stale(config, cert_file, key_file):
59+ """
60+ Do we need to generate a new self-signed cert?
61+
62+ @param config: charm data from config-get
63+ @param cert_file: destination path of generated certificate
64+ @param key_file: destination path of generated private key
65+ """
66+ # Basic Existence Checks
67+ if not os.path.exists(cert_file):
68+ return True
69+ if not os.path.exists(key_file):
70+ return True
71+
72+ # Common Name
73+ from OpenSSL import crypto
74+ cert = crypto.load_certificate(
75+ crypto.FILETYPE_PEM, file(cert_file).read())
76+ cn = cert.get_subject().commonName
77+ if config['servername'] != cn:
78+ return True
79+
80+ # Subject Alternate Name -- only trusty+ support this
81+ try:
82+ from pyasn1.codec.der import decoder
83+ from pyasn1_modules import rfc2459
84+ except ImportError:
85+ log("Cannot check subjAltName on <= 12.04, skipping.")
86+ return False
87+ cert_addresses = set()
88+ unit_addresses = set(
89+ [unit_get("public-address"), unit_get("private-address")])
90+ for i in range(0, cert.get_extension_count()):
91+ extension = cert.get_extension(i)
92+ try:
93+ names = decoder.decode(
94+ extension.get_data(), asn1Spec=rfc2459.SubjectAltName())[0]
95+ for name in names:
96+ cert_addresses.add(str(name.getComponent()))
97+ except:
98+ pass
99+ if cert_addresses != unit_addresses:
100+ log("subjAltName: Cert (%s) != Unit (%s), assuming stale" % (
101+ cert_addresses, unit_addresses))
102+ return True
103+
104+ return False
105+
106+
107 def _get_key_file_location(config_data):
108 """Look at the config, generate the key file location."""
109 key_file = None
110@@ -207,9 +271,6 @@
111 return chain_file
112
113
114-###############################################################################
115-# Hook functions
116-###############################################################################
117 def config_get(scope=None):
118 """
119 Wrapper around charm helper's config_get to replace an empty servername
120@@ -227,6 +288,9 @@
121 if not os.path.exists(default_apache2_service_config_dir):
122 os.mkdir(default_apache2_service_config_dir, 0600)
123 apt_get_install("python-jinja2")
124+ apt_get_install("python-pyasn1")
125+ apt_get_install("python-pyasn1-modules")
126+ apt_get_install("python-yaml")
127 install_status = apt_get_install("apache2")
128 if install_status == 0:
129 ensure_package_status(service_affecting_packages,
130@@ -518,12 +582,8 @@
131 if cert_file is not None and key_file is not None:
132 # ssl_cert is SELFSIGNED so generate self-signed certificate for use.
133 if config_data['ssl_cert'] and config_data['ssl_cert'] == "SELFSIGNED":
134- os.environ['OPENSSL_CN'] = config_data['servername']
135- os.environ['OPENSSL_PUBLIC'] = unit_get("public-address")
136- os.environ['OPENSSL_PRIVATE'] = unit_get("private-address")
137- run(['openssl', 'req', '-new', '-x509', '-nodes', '-config',
138- os.path.join(os.environ['CHARM_DIR'], 'data', 'openssl.cnf'),
139- '-keyout', key_file, '-out', cert_file])
140+ if is_selfsigned_cert_stale(config_data, cert_file, key_file):
141+ gen_selfsigned_cert(config_data, cert_file, key_file)
142
143 # Use SSL certificate and key provided either as a base64 string or
144 # shipped out with the charm.
145
146=== modified file 'hooks/tests/test_balancer_hook.py'
147--- hooks/tests/test_balancer_hook.py 2014-01-16 12:33:28 +0000
148+++ hooks/tests/test_balancer_hook.py 2014-06-28 19:27:49 +0000
149@@ -515,6 +515,9 @@
150 0600)
151 apt_get_install.assert_has_calls([
152 call('python-jinja2'),
153+ call('python-pyasn1'),
154+ call('python-pyasn1-modules'),
155+ call('python-yaml'),
156 call('apache2'),
157 ])
158
159@@ -534,6 +537,9 @@
160 self.assertEqual(result, 'some result')
161 apt_get_install.assert_has_calls([
162 call('python-jinja2'),
163+ call('python-pyasn1'),
164+ call('python-pyasn1-modules'),
165+ call('python-yaml'),
166 call('apache2'),
167 call('extra'),
168 ])
169@@ -556,6 +562,9 @@
170 self.assertFalse(mkdir.called)
171 apt_get_install.assert_has_calls([
172 call('python-jinja2'),
173+ call('python-pyasn1'),
174+ call('python-pyasn1-modules'),
175+ call('python-yaml'),
176 call('apache2'),
177 ])
178

Subscribers

People subscribed via source and target branches

to all changes: