Merge lp:~abentley/charms/trusty/apache2/apache2-ports into lp:charms/trusty/apache2

Proposed by Aaron Bentley
Status: Merged
Approved by: David Britton
Approved revision: 63
Merged at revision: 59
Proposed branch: lp:~abentley/charms/trusty/apache2/apache2-ports
Merge into: lp:charms/trusty/apache2
Diff against target: 287 lines (+133/-21)
5 files modified
hooks/hooks.py (+56/-16)
hooks/tests/test_balancer_hook.py (+3/-3)
hooks/tests/test_config_changed.py (+4/-1)
hooks/tests/test_hooks.py (+65/-0)
hooks/tests/test_vhost_config_relation.py (+5/-1)
To merge this branch: bzr merge lp:~abentley/charms/trusty/apache2/apache2-ports
Reviewer Review Type Date Requested Status
David Britton (community) Approve
Adam Israel (community) Approve
Review Queue (community) automated testing Needs Fixing
Review via email: mp+245740@code.launchpad.net

Commit message

Unify port handling.

Description of the change

This branch unifies open-port/close-port handling. It is a prerequisite for apache2-website-interface support, which I will introduce in a follow-on branch.

Multiple configuration options may refer to the same port. For example, a vhost_http_template might refer to port 80 and so might a vhost-config relation. But they can have conflicting ideas about whether a port should be open. If the vhost-config has an empty template, port 80 will be closed, even though the vhost_http_template wants it open. And of course, there can be multiple vhost-configs with their own conflicting opinions.

This code unifies the port handling, so that it is done in a single place. If any configuration mechanism wants a paticular port open, it is open. Otherwise it is closed. It also records the current list of ports, so that future hook runs can detect when a port has changed, and close the old port.

To post a comment you must log in.
Revision history for this message
Review Queue (review-queue) wrote :

This items has failed automated testing! Results available here http://reports.vapour.ws/charm-tests/charm-bundle-test-10870-results

review: Needs Fixing (automated testing)
Revision history for this message
Aaron Bentley (abentley) wrote :

The automated test failures are a pre-existing issue:
http://reports.vapour.ws/charm-tests/charm-bundle-test-10700-results

They are not caused by my changes.

Revision history for this message
Adam Israel (aisrael) wrote :

Hi Aaron,

I had the opportunity to review this MP today, and I agree that the failing tests are unrelated to your changes. LGTM. +1

review: Approve
Revision history for this message
David Britton (dpb) wrote :

This looks great, Aaron.

Thanks! +1

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'hooks/hooks.py'
2--- hooks/hooks.py 2014-11-20 00:06:41 +0000
3+++ hooks/hooks.py 2015-01-07 14:04:20 +0000
4@@ -1,5 +1,6 @@
5 #!/usr/bin/env python
6
7+import errno
8 import os
9 import re
10 import socket
11@@ -519,11 +520,10 @@
12 """
13 if protocol is None:
14 protocol = str(port)
15- close_port(port)
16 if template_str is None:
17 if not config_key or not config_data[config_key]:
18 log("Vhost Template not provided, not configuring: %s" % port)
19- return
20+ return False
21 template_str = config_data[config_key]
22 from jinja2 import Template
23 template = Template(str(base64.b64decode(template_str)))
24@@ -533,8 +533,8 @@
25 log("Writing file %s with config and relation data" % vhost_file)
26 with open(vhost_file, 'w') as vhost:
27 vhost.write(str(template.render(template_data)))
28- open_port(port)
29 subprocess.call(['/usr/sbin/a2ensite', vhost_name])
30+ return True
31
32
33 def config_changed():
34@@ -572,13 +572,15 @@
35 create_security()
36
37 ports = {'http': 80, 'https': 443}
38+ all_ports = set()
39 for protocol, port in ports.iteritems():
40- create_vhost(
41- port,
42- protocol=protocol,
43- config_key="vhost_%s_template" % protocol,
44- config_data=config_data,
45- relationship_data=relationship_data)
46+ if create_vhost(
47+ port,
48+ protocol=protocol,
49+ config_key="vhost_%s_template" % protocol,
50+ config_data=config_data,
51+ relationship_data=relationship_data):
52+ all_ports.add(port)
53
54 cert_file = _get_cert_file_location(config_data)
55 key_file = _get_key_file_location(config_data)
56@@ -686,7 +688,8 @@
57 f.write('\n')
58 os.chmod(key_file, 0444)
59
60- update_vhost_config_relation()
61+ all_ports.update(update_vhost_config_relation())
62+ ensure_ports(all_ports)
63 update_nrpe_checks()
64 ship_logrotate_conf()
65
66@@ -696,10 +699,11 @@
67 Update the vhost file and include the certificate in the relation
68 if it is self-signed.
69 """
70+ vhost_ports = set()
71 relation_data = relations_of_type("vhost-config")
72 config_data = config_get()
73 if relation_data is None:
74- return
75+ return vhost_ports
76
77 for unit_data in relation_data:
78 if "vhosts" in unit_data:
79@@ -711,11 +715,13 @@
80 try:
81 vhosts = yaml.safe_load(unit_data["vhosts"])
82 for vhost in vhosts:
83- create_vhost(
84- vhost["port"],
85- template_str=vhost["template"],
86- config_data=config_data,
87- relationship_data=all_relation_data)
88+ port = vhost["port"]
89+ if create_vhost(
90+ port,
91+ template_str=vhost["template"],
92+ config_data=config_data,
93+ relationship_data=all_relation_data):
94+ vhost_ports.add(port)
95 except Exception as e:
96 log("Error reading configuration data from relation! %s" % e)
97 raise
98@@ -736,6 +742,7 @@
99 vhost_relation_settings["ssl_cert"] = cert
100 for id in relation_ids("vhost-config"):
101 relation_set(relation_id=id, relation_settings=vhost_relation_settings)
102+ return vhost_ports
103
104
105 def start_hook():
106@@ -768,6 +775,39 @@
107 'hostname=%s' % my_host])
108
109
110+def ensure_ports(ports):
111+ """Ensure that only the desired ports are open."""
112+ open_ports = set(get_open_ports())
113+ ports = set(ports)
114+ wanted_closed = ports.difference(open_ports)
115+ for port in sorted(wanted_closed):
116+ open_port(port)
117+ unwanted_open = open_ports.difference(ports)
118+ for port in sorted(unwanted_open):
119+ close_port(port)
120+ set_open_ports(list(sorted(ports)))
121+
122+
123+def get_open_ports():
124+ """Get the list of open ports from the standard file."""
125+ try:
126+ pfile = open(os.path.join(os.environ['CHARM_DIR'], 'ports.yaml'))
127+ except IOError as e:
128+ if e.errno == errno.ENOENT:
129+ return []
130+ else:
131+ raise
132+ with pfile:
133+ return yaml.safe_load(pfile)
134+
135+
136+def set_open_ports(ports):
137+ """Write the list of open ports to the standard file."""
138+ ports_path = os.path.join(os.environ['CHARM_DIR'], 'ports.yaml')
139+ with open(ports_path, 'w') as pfile:
140+ yaml.safe_dump(ports, pfile)
141+
142+
143 ###############################################################################
144 # Main section
145 ###############################################################################
146
147=== modified file 'hooks/tests/test_balancer_hook.py'
148--- hooks/tests/test_balancer_hook.py 2014-10-13 07:55:57 +0000
149+++ hooks/tests/test_balancer_hook.py 2015-01-07 14:04:20 +0000
150@@ -207,12 +207,12 @@
151 check_output.assert_called_with('foo', 1, 2, bar='baz')
152
153 @patch('subprocess.check_output')
154- @patch('sys.stdout.write')
155- def test_prints_and_reraises_run_error(self, write, check_output):
156+ @patch('sys.stdout')
157+ def test_prints_and_reraises_run_error(self, out, check_output):
158 check_output.side_effect = RuntimeError('some error')
159
160 self.assertRaises(RuntimeError, hooks.run, 'some command')
161- self.assertEqual(write.mock_calls, [
162+ self.assertEqual(out.write.mock_calls, [
163 call('some error'),
164 call('\n'),
165 ])
166
167=== modified file 'hooks/tests/test_config_changed.py'
168--- hooks/tests/test_config_changed.py 2014-05-21 03:24:19 +0000
169+++ hooks/tests/test_config_changed.py 2015-01-07 14:04:20 +0000
170@@ -16,6 +16,8 @@
171 if os.path.exists(self.dirname):
172 shutil.rmtree(self.dirname)
173
174+ @patch('hooks.get_open_ports')
175+ @patch('hooks.set_open_ports')
176 @patch('hooks.subprocess.call')
177 @patch('hooks.close_port')
178 @patch('hooks.open_port')
179@@ -41,7 +43,8 @@
180 mock_create_mpm_workerfile, mock_create_security, mock_run,
181 mock_update_nrpe_checks, mock_ship_logrotate_conf,
182 mock_ensure_package_status, mock_ensure_extra_packages,
183- mock_conf_disable, mock_open_port, mock_close_port, mock_call):
184+ mock_conf_disable, mock_open_port, mock_close_port, mock_call,
185+ mock_set_open_ports, mock_get_open_ports):
186 """config-changed hook: Site directories should be empty."""
187 mock_config_get.return_value = {
188 "ssl_cert": "",
189
190=== modified file 'hooks/tests/test_hooks.py'
191--- hooks/tests/test_hooks.py 2014-05-20 16:54:57 +0000
192+++ hooks/tests/test_hooks.py 2015-01-07 14:04:20 +0000
193@@ -1,7 +1,27 @@
194+from contextlib import contextmanager
195+import os.path
196+from tempfile import mkdtemp
197+from shutil import rmtree
198+
199 from testtools import TestCase
200+from mock import (
201+ call,
202+ patch,
203+)
204+import yaml
205+
206 import hooks
207
208
209+@contextmanager
210+def temp_dir():
211+ directory = mkdtemp()
212+ try:
213+ yield directory
214+ finally:
215+ rmtree(directory)
216+
217+
218 class HooksTest(TestCase):
219 def test__get_key_file_location_empty(self):
220 """No ssl_keylocation, expect None."""
221@@ -32,3 +52,48 @@
222 """ssl_keylocation, expect correct path."""
223 self.assertEqual(hooks._get_chain_file_location(
224 {"ssl_chainlocation": "foo"}), "/etc/ssl/certs/foo")
225+
226+
227+class TestEnsurePorts(TestCase):
228+
229+ def test_ensure_ports(self):
230+ with patch('hooks.open_port') as hop_mock:
231+ with patch('hooks.set_open_ports') as hsop_mock:
232+ with patch('hooks.get_open_ports', return_value=[]):
233+ hooks.ensure_ports([80, 81])
234+ self.assertEqual(hop_mock.mock_calls, [call(80), call(81)])
235+ hsop_mock.assert_called_once_with([80, 81])
236+
237+ def test_idempotent(self):
238+ with patch('hooks.get_open_ports', return_value=[81, 82]):
239+ with patch('hooks.open_port') as hop_mock:
240+ with patch('hooks.set_open_ports') as hsop_mock:
241+ hooks.ensure_ports([81, 83, 82])
242+ self.assertEqual(hop_mock.mock_calls, [call(83)])
243+ hsop_mock.assert_called_once_with([81, 82, 83])
244+
245+ def test_closes_unneeded(self):
246+ with patch('hooks.get_open_ports', return_value=[81, 82, 83, 84]):
247+ with patch('hooks.close_port') as hcp_mock:
248+ with patch('hooks.set_open_ports') as hsop_mock:
249+ hooks.ensure_ports([81, 82])
250+ self.assertEqual(hcp_mock.mock_calls, [call(83), call(84)])
251+ hsop_mock.assert_called_once_with([81, 82])
252+
253+
254+class TestGetSetOpenPorts(TestCase):
255+
256+ def test_get_open_ports(self):
257+ with temp_dir() as charm_dir:
258+ with patch('os.environ', {'CHARM_DIR': charm_dir}):
259+ self.assertEqual(hooks.get_open_ports(), [])
260+ with open(os.path.join(charm_dir, 'ports.yaml'), 'w') as pfile:
261+ yaml.safe_dump([47, 67], pfile)
262+ self.assertEqual(hooks.get_open_ports(), [47, 67])
263+
264+ def test_set_open_ports(self):
265+ with temp_dir() as charm_dir:
266+ with patch('os.environ', {'CHARM_DIR': charm_dir}):
267+ hooks.set_open_ports([44, 23])
268+ with open(os.path.join(charm_dir, 'ports.yaml')) as pfile:
269+ self.assertEqual(yaml.safe_load(pfile), [44, 23])
270
271=== modified file 'hooks/tests/test_vhost_config_relation.py'
272--- hooks/tests/test_vhost_config_relation.py 2014-05-21 15:32:02 +0000
273+++ hooks/tests/test_vhost_config_relation.py 2015-01-07 14:04:20 +0000
274@@ -208,8 +208,12 @@
275 mock_create_vhost.assert_has_calls([
276 call("80", template_str=b64encode("80"),
277 config_data=config_data, relationship_data={}),
278+ call().__nonzero__(),
279 call("443", template_str=b64encode("443"),
280 config_data=config_data, relationship_data={}),
281+ call().__nonzero__(),
282 call("444", template_str=b64encode("444"),
283- config_data=config_data, relationship_data={})])
284+ config_data=config_data, relationship_data={}),
285+ call().__nonzero__(),
286+ ])
287 self.assertEqual(mock_create_vhost.call_count, 3)

Subscribers

People subscribed via source and target branches

to all changes: