Merge lp:~lamont/maas/create-maas-proxy.conf into lp:~maas-committers/maas/trunk
- create-maas-proxy.conf
- Merge into trunk
Proposed by
LaMont Jones
Status: | Merged |
---|---|
Approved by: | LaMont Jones |
Approved revision: | no longer in the source branch. |
Merged at revision: | 4862 |
Proposed branch: | lp:~lamont/maas/create-maas-proxy.conf |
Merge into: | lp:~maas-committers/maas/trunk |
Prerequisite: | lp:~lamont/maas/bug-1379567 |
Diff against target: |
793 lines (+521/-14) 13 files modified
etc/maas/templates/dns/zone.template (+0/-3) src/maas/demo.py (+3/-0) src/maas/development.py (+4/-0) src/maas/settings.py (+5/-0) src/maasserver/migrations/builtin/maasserver/0048_add_subnet_allow_proxy.py (+5/-2) src/maasserver/proxyconfig.py (+86/-0) src/maasserver/region_controller.py (+34/-4) src/maasserver/tests/test_proxyconfig.py (+111/-0) src/maasserver/tests/test_region_controller.py (+96/-5) src/maasserver/triggers/system.py (+50/-0) src/maasserver/triggers/tests/test_system.py (+6/-0) src/maasserver/triggers/tests/test_system_listener.py (+76/-0) src/provisioningserver/templates/proxy/maas-proxy.conf.template (+45/-0) |
To merge this branch: | bzr merge lp:~lamont/maas/create-maas-proxy.conf |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
LaMont Jones (community) | Approve | ||
Blake Rouse (community) | Needs Fixing | ||
Review via email: mp+290154@code.launchpad.net |
Commit message
Create /var/cache/
Description of the change
Create /var/cache/
To post a comment you must log in.
Revision history for this message
LaMont Jones (lamont) wrote : | # |
Marking this approved since merging Blake's changes that were the cause of his "needs fixing".
review:
Approve
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | === modified file 'etc/maas/templates/dns/zone.template' |
2 | --- etc/maas/templates/dns/zone.template 2016-02-03 09:11:25 +0000 |
3 | +++ etc/maas/templates/dns/zone.template 2016-03-31 23:44:54 +0000 |
4 | @@ -1,7 +1,4 @@ |
5 | ; Zone file modified: {{modified}}. |
6 | -; Note that the modification time of this file doesn't reflect |
7 | -; the actual modification time. MAAS controls the modification time |
8 | -; of this file to be able to force the zone to be reloaded by BIND. |
9 | $TTL {{ttl}} |
10 | @ IN SOA {{domain}}. nobody.example.com. ( |
11 | {{serial}} ; serial |
12 | |
13 | === modified file 'src/maas/demo.py' |
14 | --- src/maas/demo.py 2016-03-25 14:52:23 +0000 |
15 | +++ src/maas/demo.py 2016-03-31 23:44:54 +0000 |
16 | @@ -27,6 +27,9 @@ |
17 | # Connect to the DHCP server. TODO: Use the signals manager instead. |
18 | DHCP_CONNECT = True |
19 | |
20 | +# Connect to the PROXY server. TODO: Use the signals manager instead. |
21 | +PROXY_CONNECT = True |
22 | + |
23 | MAAS_CLI = abspath("bin/maas-region") |
24 | |
25 | # For demo purposes, give nodes unauthenticated access to their metadata |
26 | |
27 | === modified file 'src/maas/development.py' |
28 | --- src/maas/development.py 2016-03-28 20:03:45 +0000 |
29 | +++ src/maas/development.py 2016-03-31 23:44:54 +0000 |
30 | @@ -33,6 +33,10 @@ |
31 | # basis. TODO: Use the signals manager instead. |
32 | DHCP_CONNECT = False |
33 | |
34 | +# Don't setup PROXY servers in tests, this will be enabled on a case per case |
35 | +# basis. TODO: Use the signals manager instead. |
36 | +PROXY_CONNECT = False |
37 | + |
38 | # Invalid strings should be visible. |
39 | TEMPLATE_STRING_IF_INVALID = '#### INVALID STRING ####' |
40 | |
41 | |
42 | === modified file 'src/maas/settings.py' |
43 | --- src/maas/settings.py 2016-03-25 14:52:23 +0000 |
44 | +++ src/maas/settings.py 2016-03-31 23:44:54 +0000 |
45 | @@ -78,6 +78,11 @@ |
46 | # machinery. TODO: Use the signals manager instead. |
47 | DHCP_CONNECT = True |
48 | |
49 | +# Should the PROXY features be enabled? Having this config option is a |
50 | +# debugging/testing feature to be able to quickly disconnect the PROXY |
51 | +# machinery. TODO: Use the signals manager instead. |
52 | +PROXY_CONNECT = True |
53 | + |
54 | # The MAAS CLI. |
55 | MAAS_CLI = 'sudo maas-region' |
56 | |
57 | |
58 | === renamed file 'src/maasserver/migrations/builtin/maasserver/0046_add_subnet_allow_proxy.py' => 'src/maasserver/migrations/builtin/maasserver/0048_add_subnet_allow_proxy.py' |
59 | --- src/maasserver/migrations/builtin/maasserver/0046_add_subnet_allow_proxy.py 2016-03-31 23:44:54 +0000 |
60 | +++ src/maasserver/migrations/builtin/maasserver/0048_add_subnet_allow_proxy.py 2016-03-31 23:44:54 +0000 |
61 | @@ -1,13 +1,16 @@ |
62 | # -*- coding: utf-8 -*- |
63 | from __future__ import unicode_literals |
64 | |
65 | -from django.db import migrations, models |
66 | +from django.db import ( |
67 | + migrations, |
68 | + models, |
69 | +) |
70 | |
71 | |
72 | class Migration(migrations.Migration): |
73 | |
74 | dependencies = [ |
75 | - ('maasserver', '0045_add_node_to_filesystem'), |
76 | + ('maasserver', '0047_fix_spelling_of_degraded'), |
77 | ] |
78 | |
79 | operations = [ |
80 | |
81 | === added file 'src/maasserver/proxyconfig.py' |
82 | --- src/maasserver/proxyconfig.py 1970-01-01 00:00:00 +0000 |
83 | +++ src/maasserver/proxyconfig.py 2016-03-31 23:44:54 +0000 |
84 | @@ -0,0 +1,86 @@ |
85 | +# Copyright 2016 Canonical Ltd. This software is licensed under the |
86 | +# GNU Affero General Public License version 3 (see the file LICENSE). |
87 | + |
88 | +"""Proxy config management module.""" |
89 | + |
90 | +__all__ = [ |
91 | + 'proxy_update_config', |
92 | + ] |
93 | + |
94 | +import datetime |
95 | +import os |
96 | +import socket |
97 | +import sys |
98 | + |
99 | +from django.conf import settings |
100 | +from maasserver.models.subnet import Subnet |
101 | +from maasserver.service_monitor import service_monitor |
102 | +from maasserver.utils.orm import transactional |
103 | +from maasserver.utils.threads import deferToDatabase |
104 | +from provisioningserver.logger import get_maas_logger |
105 | +from provisioningserver.utils import locate_template |
106 | +from provisioningserver.utils.fs import atomic_write |
107 | +from provisioningserver.utils.twisted import asynchronous |
108 | +import tempita |
109 | +from twisted.internet.defer import succeed |
110 | + |
111 | + |
112 | +maaslog = get_maas_logger("dns") |
113 | +MAAS_PROXY_CONF_NAME = 'maas-proxy.conf' |
114 | +MAAS_PROXY_CONF_TEMPLATE = 'maas-proxy.conf.template' |
115 | + |
116 | + |
117 | +def is_proxy_enabled(): |
118 | + """Is MAAS configured to manage the PROXY?""" |
119 | + return settings.PROXY_CONNECT |
120 | + |
121 | + |
122 | +class ProxyConfigFail(Exception): |
123 | + """Raised if there is a problem with the proxy configuration.""" |
124 | + |
125 | + |
126 | +def get_proxy_config_dir(): |
127 | + """Location of bind configuration files.""" |
128 | + setting = os.getenv("MAAS_PROXY_CONFIG_DIR", "/var/lib/maas") |
129 | + if isinstance(setting, bytes): |
130 | + fsenc = sys.getfilesystemencoding() |
131 | + return setting.decode(fsenc) |
132 | + else: |
133 | + return setting |
134 | + |
135 | + |
136 | +@asynchronous |
137 | +def proxy_update_config(reload_proxy=True): |
138 | + """Regenerate the proxy configuration file.""" |
139 | + |
140 | + @transactional |
141 | + def write_config(): |
142 | + allowed_subnets = Subnet.objects.filter(allow_proxy=True) |
143 | + cidrs = [subnet.cidr for subnet in allowed_subnets] |
144 | + context = { |
145 | + 'allowed': allowed_subnets, |
146 | + 'modified': str(datetime.date.today()), |
147 | + 'fqdn': socket.getfqdn(), |
148 | + 'cidrs': cidrs, |
149 | + } |
150 | + template_path = locate_template('proxy', MAAS_PROXY_CONF_TEMPLATE) |
151 | + template = tempita.Template.from_filename( |
152 | + template_path, encoding="UTF-8") |
153 | + try: |
154 | + content = template.substitute(context) |
155 | + except NameError as error: |
156 | + raise ProxyConfigFail(*error.args) |
157 | + # Squid prefers ascii. |
158 | + content = content.encode("ascii") |
159 | + target_path = os.sep.join( |
160 | + [get_proxy_config_dir(), MAAS_PROXY_CONF_NAME]) |
161 | + atomic_write(content, target_path, overwrite=True, mode=0o644) |
162 | + |
163 | + if is_proxy_enabled(): |
164 | + d = deferToDatabase(write_config) |
165 | + if reload_proxy: |
166 | + d.addCallback( |
167 | + lambda _: service_monitor.reloadService("proxy", if_on=True)) |
168 | + return d |
169 | + else: |
170 | + return succeed(None) |
171 | |
172 | === modified file 'src/maasserver/region_controller.py' |
173 | --- src/maasserver/region_controller.py 2016-03-28 20:03:45 +0000 |
174 | +++ src/maasserver/region_controller.py 2016-03-31 23:44:54 +0000 |
175 | @@ -11,6 +11,12 @@ |
176 | 'sys_dns'. Any time a message is recieved on that channel the DNS is marked |
177 | as requiring an update. Once marked for update the DNS configuration is |
178 | updated and bind9 is told to reload. |
179 | + |
180 | +Proxy: |
181 | + The regiond process listens for messages from Postgres on channel |
182 | + 'sys_proxy'. Any time a message is recieved on that channel the maas-proxy |
183 | + is marked as requiring an update. Once marked for update the proxy |
184 | + configuration is updated and maas-proxy is told to reload. |
185 | """ |
186 | |
187 | __all__ = [ |
188 | @@ -18,6 +24,7 @@ |
189 | ] |
190 | |
191 | from maasserver.dns.config import dns_update_all_zones |
192 | +from maasserver.proxyconfig import proxy_update_config |
193 | from maasserver.utils.orm import transactional |
194 | from maasserver.utils.threads import deferToDatabase |
195 | from provisioningserver.utils.twisted import ( |
196 | @@ -26,6 +33,7 @@ |
197 | ) |
198 | from twisted.application.service import Service |
199 | from twisted.internet import reactor |
200 | +from twisted.internet.defer import DeferredList |
201 | from twisted.internet.task import LoopingCall |
202 | from twisted.python import log |
203 | |
204 | @@ -51,6 +59,7 @@ |
205 | self.processing.clock = self.clock |
206 | self.processingDefer = None |
207 | self.needsDNSUpdate = False |
208 | + self.needsProxyUpdate = False |
209 | self.postgresListener = postgresListener |
210 | |
211 | @asynchronous(timeout=FOREVER) |
212 | @@ -58,15 +67,18 @@ |
213 | """Start listening for messages.""" |
214 | super(RegionControllerService, self).startService() |
215 | self.postgresListener.register("sys_dns", self.markDNSForUpdate) |
216 | + self.postgresListener.register("sys_proxy", self.markProxyForUpdate) |
217 | |
218 | - # Update DNS on first start. |
219 | + # Update DNS and proxy on first start. |
220 | self.markDNSForUpdate(None, None) |
221 | + self.markProxyForUpdate(None, None) |
222 | |
223 | @asynchronous(timeout=FOREVER) |
224 | def stopService(self): |
225 | """Close the controller.""" |
226 | super(RegionControllerService, self).stopService() |
227 | self.postgresListener.unregister("sys_dns", self.markDNSForUpdate) |
228 | + self.postgresListener.unregister("sys_proxy", self.markProxyForUpdate) |
229 | if self.processingDefer is not None: |
230 | self.processingDefer, d = None, self.processingDefer |
231 | self.processing.stop() |
232 | @@ -77,13 +89,19 @@ |
233 | self.needsDNSUpdate = True |
234 | self.startProcessing() |
235 | |
236 | + def markProxyForUpdate(self, channel, message): |
237 | + """Called when the `sys_proxy` message is received.""" |
238 | + self.needsProxyUpdate = True |
239 | + self.startProcessing() |
240 | + |
241 | def startProcessing(self): |
242 | """Start the process looping call.""" |
243 | if not self.processing.running: |
244 | self.processingDefer = self.processing.start(0.1, now=False) |
245 | |
246 | def process(self): |
247 | - """Process the DNS update.""" |
248 | + """Process the DNS and/or proxy update.""" |
249 | + defers = [] |
250 | if self.needsDNSUpdate: |
251 | self.needsDNSUpdate = False |
252 | d = deferToDatabase(transactional(dns_update_all_zones)) |
253 | @@ -93,8 +111,20 @@ |
254 | d.addErrback( |
255 | log.err, |
256 | "Failed configuring DNS.") |
257 | - return d |
258 | - else: |
259 | + defers.append(d) |
260 | + if self.needsProxyUpdate: |
261 | + self.needsProxyUpdate = False |
262 | + d = proxy_update_config(reload_proxy=True) |
263 | + d.addCallback( |
264 | + lambda _: log.msg( |
265 | + "Successfully configured proxy.")) |
266 | + d.addErrback( |
267 | + log.err, |
268 | + "Failed configuring proxy.") |
269 | + defers.append(d) |
270 | + if len(defers) == 0: |
271 | # Nothing more to do. |
272 | self.processing.stop() |
273 | self.processingDefer = None |
274 | + else: |
275 | + return DeferredList(defers) |
276 | |
277 | === added file 'src/maasserver/tests/test_proxyconfig.py' |
278 | --- src/maasserver/tests/test_proxyconfig.py 1970-01-01 00:00:00 +0000 |
279 | +++ src/maasserver/tests/test_proxyconfig.py 2016-03-31 23:44:54 +0000 |
280 | @@ -0,0 +1,111 @@ |
281 | +# Copyright 2016 Canonical Ltd. This software is licensed under the |
282 | +# GNU Affero General Public License version 3 (see the file LICENSE). |
283 | + |
284 | +"""Tests for the proxyconfig.""" |
285 | + |
286 | +__all__ = [] |
287 | + |
288 | +import os |
289 | + |
290 | +from crochet import wait_for |
291 | +from django.conf import settings |
292 | +from fixtures import EnvironmentVariableFixture |
293 | +from maasserver import proxyconfig |
294 | +from maasserver.testing.factory import factory |
295 | +from maasserver.testing.testcase import ( |
296 | + MAASServerTestCase, |
297 | + MAASTransactionServerTestCase, |
298 | +) |
299 | +from maasserver.utils.orm import transactional |
300 | +from maasserver.utils.threads import deferToDatabase |
301 | +from maastesting.matchers import ( |
302 | + MockCalledOnceWith, |
303 | + MockNotCalled, |
304 | +) |
305 | +from testtools.matchers import ( |
306 | + Contains, |
307 | + FileContains, |
308 | + Not, |
309 | +) |
310 | +from twisted.internet.defer import inlineCallbacks |
311 | + |
312 | + |
313 | +wait_for_reactor = wait_for(30) # 30 seconds. |
314 | + |
315 | + |
316 | +class TestGetConfigDir(MAASServerTestCase): |
317 | + """Tests for `maasserver.proxyconfig.get_proxy_config_dir`.""" |
318 | + |
319 | + def test_returns_default(self): |
320 | + self.assertEquals( |
321 | + "/var/lib/maas", proxyconfig.get_proxy_config_dir()) |
322 | + |
323 | + def test_env_overrides_default(self): |
324 | + os.environ['MAAS_PROXY_CONFIG_DIR'] = factory.make_name('env') |
325 | + self.assertEquals( |
326 | + os.environ['MAAS_PROXY_CONFIG_DIR'], |
327 | + proxyconfig.get_proxy_config_dir()) |
328 | + del(os.environ['MAAS_PROXY_CONFIG_DIR']) |
329 | + |
330 | + |
331 | +class TestProxyUpdateConfig(MAASTransactionServerTestCase): |
332 | + """Tests for `maasserver.proxyconfig`.""" |
333 | + |
334 | + def setUp(self): |
335 | + super(TestProxyUpdateConfig, self).setUp() |
336 | + self.tmpdir = self.make_dir() |
337 | + self.service_monitor = self.patch(proxyconfig, "service_monitor") |
338 | + self.useFixture( |
339 | + EnvironmentVariableFixture('MAAS_PROXY_CONFIG_DIR', self.tmpdir)) |
340 | + |
341 | + @transactional |
342 | + def make_subnet(self, allow_proxy=True): |
343 | + return factory.make_Subnet(allow_proxy=allow_proxy) |
344 | + |
345 | + @wait_for_reactor |
346 | + @inlineCallbacks |
347 | + def test__only_enabled_subnets_are_present(self): |
348 | + self.patch(settings, "PROXY_CONNECT", True) |
349 | + disabled = yield deferToDatabase(self.make_subnet, allow_proxy=False) |
350 | + enabled = yield deferToDatabase(self.make_subnet) |
351 | + yield proxyconfig.proxy_update_config(reload_proxy=False) |
352 | + # enabled's cidr must be present |
353 | + matcher = Contains("acl localnet src %s" % enabled.cidr) |
354 | + self.assertThat( |
355 | + "%s/%s" % (self.tmpdir, proxyconfig.MAAS_PROXY_CONF_NAME), |
356 | + FileContains(matcher=matcher)) |
357 | + # disabled's cidr must not be present |
358 | + matcher = Not(Contains("acl localnet src %s" % disabled.cidr)) |
359 | + self.assertThat( |
360 | + "%s/%s" % (self.tmpdir, proxyconfig.MAAS_PROXY_CONF_NAME), |
361 | + FileContains(matcher=matcher)) |
362 | + |
363 | + @wait_for_reactor |
364 | + @inlineCallbacks |
365 | + def test__calls_reloadService(self): |
366 | + self.patch(settings, "PROXY_CONNECT", True) |
367 | + yield deferToDatabase(self.make_subnet) |
368 | + yield proxyconfig.proxy_update_config() |
369 | + self.assertThat( |
370 | + self.service_monitor.reloadService, |
371 | + MockCalledOnceWith("proxy", if_on=True)) |
372 | + |
373 | + @wait_for_reactor |
374 | + @inlineCallbacks |
375 | + def test__doesnt_call_reloadService_when_PROXY_CONNECT_False(self): |
376 | + self.patch(settings, "PROXY_CONNECT", False) |
377 | + yield deferToDatabase(self.make_subnet) |
378 | + yield proxyconfig.proxy_update_config() |
379 | + self.assertThat( |
380 | + self.service_monitor.reloadService, |
381 | + MockNotCalled()) |
382 | + |
383 | + @wait_for_reactor |
384 | + @inlineCallbacks |
385 | + def test__doesnt_call_reloadService_when_reload_proxy_False(self): |
386 | + self.patch(settings, "PROXY_CONNECT", True) |
387 | + yield deferToDatabase(self.make_subnet) |
388 | + yield proxyconfig.proxy_update_config(reload_proxy=False) |
389 | + self.assertThat( |
390 | + self.service_monitor.reloadService, |
391 | + MockNotCalled()) |
392 | |
393 | === modified file 'src/maasserver/tests/test_region_controller.py' |
394 | --- src/maasserver/tests/test_region_controller.py 2016-03-28 20:03:45 +0000 |
395 | +++ src/maasserver/tests/test_region_controller.py 2016-03-31 23:44:54 +0000 |
396 | @@ -12,16 +12,22 @@ |
397 | from maasserver.testing.testcase import MAASServerTestCase |
398 | from maastesting.matchers import ( |
399 | MockCalledOnceWith, |
400 | + MockCallsMatch, |
401 | MockNotCalled, |
402 | ) |
403 | from mock import ( |
404 | ANY, |
405 | + call, |
406 | MagicMock, |
407 | sentinel, |
408 | ) |
409 | from testtools.matchers import MatchesStructure |
410 | from twisted.internet import reactor |
411 | -from twisted.internet.defer import inlineCallbacks |
412 | +from twisted.internet.defer import ( |
413 | + fail, |
414 | + inlineCallbacks, |
415 | + succeed, |
416 | +) |
417 | |
418 | |
419 | wait_for_reactor = wait_for(30) # 30 seconds. |
420 | @@ -45,22 +51,34 @@ |
421 | service.startService() |
422 | self.assertThat( |
423 | listener.register, |
424 | - MockCalledOnceWith("sys_dns", service.markDNSForUpdate)) |
425 | + MockCallsMatch( |
426 | + call("sys_dns", service.markDNSForUpdate), |
427 | + call("sys_proxy", service.markProxyForUpdate))) |
428 | |
429 | - def test_startService_sets_needsDNSUpdate_calls_startProcessing(self): |
430 | + def test_startService_calls_markDNSForUpdate(self): |
431 | listener = MagicMock() |
432 | service = RegionControllerService(listener) |
433 | mock_markDNSForUpdate = self.patch(service, "markDNSForUpdate") |
434 | service.startService() |
435 | self.assertThat(mock_markDNSForUpdate, MockCalledOnceWith(None, None)) |
436 | |
437 | + def test_startService_calls_markProxyForUpdate(self): |
438 | + listener = MagicMock() |
439 | + service = RegionControllerService(listener) |
440 | + mock_markProxyForUpdate = self.patch(service, "markProxyForUpdate") |
441 | + service.startService() |
442 | + self.assertThat( |
443 | + mock_markProxyForUpdate, MockCalledOnceWith(None, None)) |
444 | + |
445 | def test_stopService_calls_unregister_on_the_listener(self): |
446 | listener = MagicMock() |
447 | service = RegionControllerService(listener) |
448 | service.stopService() |
449 | self.assertThat( |
450 | listener.unregister, |
451 | - MockCalledOnceWith("sys_dns", service.markDNSForUpdate)) |
452 | + MockCallsMatch( |
453 | + call("sys_dns", service.markDNSForUpdate), |
454 | + call("sys_proxy", service.markProxyForUpdate))) |
455 | |
456 | @wait_for_reactor |
457 | @inlineCallbacks |
458 | @@ -71,7 +89,7 @@ |
459 | yield service.stopService() |
460 | self.assertIsNone(service.processingDefer) |
461 | |
462 | - def test_markDNSForUpdate_sets_needsDNSUpdate_and_starts_processing(self): |
463 | + def test_markDNSForUpdate_sets_needsDNSUpdate_and_starts_process(self): |
464 | listener = MagicMock() |
465 | service = RegionControllerService(listener) |
466 | mock_startProcessing = self.patch(service, "startProcessing") |
467 | @@ -79,6 +97,14 @@ |
468 | self.assertTrue(service.needsDNSUpdate) |
469 | self.assertThat(mock_startProcessing, MockCalledOnceWith()) |
470 | |
471 | + def test_markProxyForUpdate_sets_needsProxyUpdate_and_starts_process(self): |
472 | + listener = MagicMock() |
473 | + service = RegionControllerService(listener) |
474 | + mock_startProcessing = self.patch(service, "startProcessing") |
475 | + service.markProxyForUpdate(None, None) |
476 | + self.assertTrue(service.needsProxyUpdate) |
477 | + self.assertThat(mock_startProcessing, MockCalledOnceWith()) |
478 | + |
479 | def test_startProcessing_doesnt_call_start_when_looping_call_running(self): |
480 | service = RegionControllerService(sentinel.listener) |
481 | mock_start = self.patch(service.processing, "start") |
482 | @@ -107,6 +133,17 @@ |
483 | |
484 | @wait_for_reactor |
485 | @inlineCallbacks |
486 | + def test_process_doesnt_proxy_update_config_when_nothing_to_process(self): |
487 | + service = RegionControllerService(sentinel.listener) |
488 | + service.needsProxyUpdate = False |
489 | + mock_proxy_update_config = self.patch( |
490 | + region_controller, "proxy_update_config") |
491 | + service.startProcessing() |
492 | + yield service.processingDefer |
493 | + self.assertThat(mock_proxy_update_config, MockNotCalled()) |
494 | + |
495 | + @wait_for_reactor |
496 | + @inlineCallbacks |
497 | def test_process_stops_processing(self): |
498 | service = RegionControllerService(sentinel.listener) |
499 | service.needsDNSUpdate = False |
500 | @@ -132,6 +169,24 @@ |
501 | |
502 | @wait_for_reactor |
503 | @inlineCallbacks |
504 | + def test_process_updates_proxy(self): |
505 | + service = RegionControllerService(sentinel.listener) |
506 | + service.needsProxyUpdate = True |
507 | + mock_proxy_update_config = self.patch( |
508 | + region_controller, "proxy_update_config") |
509 | + mock_proxy_update_config.return_value = succeed(None) |
510 | + mock_msg = self.patch( |
511 | + region_controller.log, "msg") |
512 | + service.startProcessing() |
513 | + yield service.processingDefer |
514 | + self.assertThat( |
515 | + mock_proxy_update_config, MockCalledOnceWith(reload_proxy=True)) |
516 | + self.assertThat( |
517 | + mock_msg, |
518 | + MockCalledOnceWith("Successfully configured proxy.")) |
519 | + |
520 | + @wait_for_reactor |
521 | + @inlineCallbacks |
522 | def test_process_updates_zones_logs_failure(self): |
523 | service = RegionControllerService(sentinel.listener) |
524 | service.needsDNSUpdate = True |
525 | @@ -146,3 +201,39 @@ |
526 | self.assertThat( |
527 | mock_err, |
528 | MockCalledOnceWith(ANY, "Failed configuring DNS.")) |
529 | + |
530 | + @wait_for_reactor |
531 | + @inlineCallbacks |
532 | + def test_process_updates_proxy_logs_failure(self): |
533 | + service = RegionControllerService(sentinel.listener) |
534 | + service.needsProxyUpdate = True |
535 | + mock_proxy_update_config = self.patch( |
536 | + region_controller, "proxy_update_config") |
537 | + mock_proxy_update_config.return_value = fail(factory.make_exception()) |
538 | + mock_err = self.patch( |
539 | + region_controller.log, "err") |
540 | + service.startProcessing() |
541 | + yield service.processingDefer |
542 | + self.assertThat( |
543 | + mock_proxy_update_config, MockCalledOnceWith(reload_proxy=True)) |
544 | + self.assertThat( |
545 | + mock_err, |
546 | + MockCalledOnceWith(ANY, "Failed configuring proxy.")) |
547 | + |
548 | + @wait_for_reactor |
549 | + @inlineCallbacks |
550 | + def test_process_updates_bind_and_proxy(self): |
551 | + service = RegionControllerService(sentinel.listener) |
552 | + service.needsDNSUpdate = True |
553 | + service.needsProxyUpdate = True |
554 | + mock_dns_update_all_zones = self.patch( |
555 | + region_controller, "dns_update_all_zones") |
556 | + mock_proxy_update_config = self.patch( |
557 | + region_controller, "proxy_update_config") |
558 | + mock_proxy_update_config.return_value = succeed(None) |
559 | + service.startProcessing() |
560 | + yield service.processingDefer |
561 | + self.assertThat( |
562 | + mock_dns_update_all_zones, MockCalledOnceWith()) |
563 | + self.assertThat( |
564 | + mock_proxy_update_config, MockCalledOnceWith(reload_proxy=True)) |
565 | |
566 | === modified file 'src/maasserver/triggers/system.py' |
567 | --- src/maasserver/triggers/system.py 2016-03-30 13:57:14 +0000 |
568 | +++ src/maasserver/triggers/system.py 2016-03-31 23:44:54 +0000 |
569 | @@ -880,6 +880,21 @@ |
570 | """) |
571 | |
572 | |
573 | +# Triggered when a subnet is updated. Increments notifies that proxy needs to |
574 | +# be updated. Only watches changes on the cidr and allow_proxy. |
575 | +PROXY_SUBNET_UPDATE = dedent("""\ |
576 | + CREATE OR REPLACE FUNCTION sys_proxy_subnet_update() |
577 | + RETURNS trigger as $$ |
578 | + BEGIN |
579 | + IF OLD.cidr != NEW.cidr OR OLD.allow_proxy != NEW.allow_proxy THEN |
580 | + PERFORM pg_notify('sys_proxy', ''); |
581 | + END IF; |
582 | + RETURN NEW; |
583 | + END; |
584 | + $$ LANGUAGE plpgsql; |
585 | + """) |
586 | + |
587 | + |
588 | def render_sys_dns_procedure(proc_name, on_delete=False): |
589 | """Render a database procedure with name `proc_name` that increments |
590 | the zone serial and notifies that a DNS update is needed. |
591 | @@ -899,6 +914,23 @@ |
592 | """ % (proc_name, 'NEW' if not on_delete else 'OLD')) |
593 | |
594 | |
595 | +def render_sys_proxy_procedure(proc_name, on_delete=False): |
596 | + """Render a database procedure with name `proc_name` that notifies that a |
597 | + proxy update is needed. |
598 | + |
599 | + :param proc_name: Name of the procedure. |
600 | + :param on_delete: True when procedure will be used as a delete trigger. |
601 | + """ |
602 | + return dedent("""\ |
603 | + CREATE OR REPLACE FUNCTION %s() RETURNS trigger AS $$ |
604 | + BEGIN |
605 | + PERFORM pg_notify('sys_proxy', ''); |
606 | + RETURN %s; |
607 | + END; |
608 | + $$ LANGUAGE plpgsql; |
609 | + """ % (proc_name, 'NEW' if not on_delete else 'OLD')) |
610 | + |
611 | + |
612 | @transactional |
613 | def register_system_triggers(): |
614 | """Register all system triggers into the database.""" |
615 | @@ -1115,3 +1147,21 @@ |
616 | "maasserver_config", "sys_dns_config_insert", "insert") |
617 | register_trigger( |
618 | "maasserver_config", "sys_dns_config_update", "update") |
619 | + |
620 | + # Proxy |
621 | + |
622 | + ## Subnet |
623 | + register_procedure( |
624 | + render_sys_proxy_procedure("sys_proxy_subnet_insert")) |
625 | + register_trigger( |
626 | + "maasserver_subnet", |
627 | + "sys_proxy_subnet_insert", "insert") |
628 | + register_procedure(PROXY_SUBNET_UPDATE) |
629 | + register_trigger( |
630 | + "maasserver_subnet", |
631 | + "sys_proxy_subnet_update", "update") |
632 | + register_procedure( |
633 | + render_sys_proxy_procedure("sys_proxy_subnet_delete", on_delete=True)) |
634 | + register_trigger( |
635 | + "maasserver_subnet", |
636 | + "sys_proxy_subnet_delete", "delete") |
637 | |
638 | === modified file 'src/maasserver/triggers/tests/test_system.py' |
639 | --- src/maasserver/triggers/tests/test_system.py 2016-03-29 20:35:17 +0000 |
640 | +++ src/maasserver/triggers/tests/test_system.py 2016-03-31 23:44:54 +0000 |
641 | @@ -50,11 +50,17 @@ |
642 | "dnsdata_sys_dns_dnsdata_insert", |
643 | "dnsdata_sys_dns_dnsdata_update", |
644 | "dnsdata_sys_dns_dnsdata_delete", |
645 | + "subnet_sys_dns_subnet_insert", |
646 | + "subnet_sys_dns_subnet_update", |
647 | + "subnet_sys_dns_subnet_delete", |
648 | "node_sys_dns_node_update", |
649 | "node_sys_dns_node_delete", |
650 | "interface_sys_dns_interface_update", |
651 | "config_sys_dns_config_insert", |
652 | "config_sys_dns_config_update", |
653 | + "subnet_sys_proxy_subnet_insert", |
654 | + "subnet_sys_proxy_subnet_update", |
655 | + "subnet_sys_proxy_subnet_delete", |
656 | ] |
657 | sql, args = psql_array(triggers, sql_type="text") |
658 | with closing(connection.cursor()) as cursor: |
659 | |
660 | === modified file 'src/maasserver/triggers/tests/test_system_listener.py' |
661 | --- src/maasserver/triggers/tests/test_system_listener.py 2016-03-30 13:57:14 +0000 |
662 | +++ src/maasserver/triggers/tests/test_system_listener.py 2016-03-31 23:44:54 +0000 |
663 | @@ -2966,3 +2966,79 @@ |
664 | yield self.assertZoneSerialIncrement(zone_serial) |
665 | finally: |
666 | yield listener.stopService() |
667 | + |
668 | + |
669 | +class TestProxySubnetListener( |
670 | + MAASTransactionServerTestCase, TransactionalHelpersMixin): |
671 | + """End-to-end test for the proxy triggers code.""" |
672 | + |
673 | + @wait_for_reactor |
674 | + @inlineCallbacks |
675 | + def test_sends_message_for_subnet_insert(self): |
676 | + yield deferToDatabase(register_system_triggers) |
677 | + dv = DeferredValue() |
678 | + listener = self.make_listener_without_delay() |
679 | + listener.register( |
680 | + "sys_proxy", lambda *args: dv.set(args)) |
681 | + yield listener.startService() |
682 | + try: |
683 | + yield deferToDatabase(self.create_subnet) |
684 | + yield dv.get(timeout=2) |
685 | + finally: |
686 | + yield listener.stopService() |
687 | + |
688 | + @wait_for_reactor |
689 | + @inlineCallbacks |
690 | + def test_sends_message_for_subnet_cidr_update(self): |
691 | + yield deferToDatabase(register_system_triggers) |
692 | + subnet = yield deferToDatabase(self.create_subnet) |
693 | + dv = DeferredValue() |
694 | + listener = self.make_listener_without_delay() |
695 | + listener.register( |
696 | + "sys_proxy", lambda *args: dv.set(args)) |
697 | + yield listener.startService() |
698 | + try: |
699 | + network = factory.make_ip4_or_6_network() |
700 | + yield deferToDatabase(self.update_subnet, subnet.id, { |
701 | + "cidr": str(network.cidr), |
702 | + "gateway_ip": factory.pick_ip_in_network(network), |
703 | + "dns_servers": [], |
704 | + }) |
705 | + yield dv.get(timeout=2) |
706 | + finally: |
707 | + yield listener.stopService() |
708 | + |
709 | + @wait_for_reactor |
710 | + @inlineCallbacks |
711 | + def test_sends_message_for_subnet_allow_proxy_update(self): |
712 | + yield deferToDatabase(register_system_triggers) |
713 | + subnet = yield deferToDatabase( |
714 | + self.create_subnet, {"allow_proxy": False}) |
715 | + dv = DeferredValue() |
716 | + listener = self.make_listener_without_delay() |
717 | + listener.register( |
718 | + "sys_proxy", lambda *args: dv.set(args)) |
719 | + yield listener.startService() |
720 | + try: |
721 | + yield deferToDatabase(self.update_subnet, subnet.id, { |
722 | + "allow_proxy": True, |
723 | + }) |
724 | + yield dv.get(timeout=2) |
725 | + finally: |
726 | + yield listener.stopService() |
727 | + |
728 | + @wait_for_reactor |
729 | + @inlineCallbacks |
730 | + def test_sends_message_for_subnet_delete(self): |
731 | + yield deferToDatabase(register_system_triggers) |
732 | + subnet = yield deferToDatabase(self.create_subnet) |
733 | + dv = DeferredValue() |
734 | + listener = self.make_listener_without_delay() |
735 | + listener.register( |
736 | + "sys_proxy", lambda *args: dv.set(args)) |
737 | + yield listener.startService() |
738 | + try: |
739 | + yield deferToDatabase(self.delete_subnet, subnet.id) |
740 | + yield dv.get(timeout=2) |
741 | + finally: |
742 | + yield listener.stopService() |
743 | |
744 | === added directory 'src/provisioningserver/templates/proxy' |
745 | === added file 'src/provisioningserver/templates/proxy/maas-proxy.conf.template' |
746 | --- src/provisioningserver/templates/proxy/maas-proxy.conf.template 1970-01-01 00:00:00 +0000 |
747 | +++ src/provisioningserver/templates/proxy/maas-proxy.conf.template 2016-03-31 23:44:54 +0000 |
748 | @@ -0,0 +1,45 @@ |
749 | +# DO NOT EDIT. This file is automatically created by MAAS. |
750 | +# Last updated at {{modified}}. |
751 | + |
752 | +# Inspired by UDS's conference proxy |
753 | + |
754 | +acl maas_proxy_manager proto cache_object |
755 | +acl localhost src 127.0.0.1/32 ::1 |
756 | +acl to_localhost dst 127.0.0.0/8 0.0.0.0/32 ::1 |
757 | +{{for cidr in cidrs}} |
758 | +acl localnet src {{cidr}} |
759 | +{{endfor}} |
760 | +acl SSL_ports port 443 |
761 | +acl Safe_ports port 80 # http |
762 | +acl Safe_ports port 21 # ftp |
763 | +acl Safe_ports port 443 # https |
764 | +acl Safe_ports port 1025-65535 # unregistered ports |
765 | +acl CONNECT method CONNECT |
766 | +http_access allow maas_proxy_manager localhost |
767 | +http_access deny maas_proxy_manager |
768 | +http_access deny !Safe_ports |
769 | +http_access deny CONNECT !SSL_ports |
770 | +http_access allow localnet |
771 | +http_access allow localhost |
772 | +http_access deny all |
773 | +http_port 3128 transparent |
774 | +http_port 8000 |
775 | +coredump_dir /var/spool/maas-proxy |
776 | +refresh_pattern ^ftp: 1440 20% 10080 |
777 | +refresh_pattern -i (/cgi-bin/|\?) 0 0% 0 |
778 | +refresh_pattern \/Release(|\.gpg)$ 0 0% 0 refresh-ims |
779 | +refresh_pattern \/InRelease$ 0 0% 0 refresh-ims |
780 | +refresh_pattern \/(Packages|Sources)(|\.bz2|\.gz|\.xz)$ 0 0% 0 refresh-ims |
781 | +refresh_pattern \/(Translation-.*)(|\.bz2|\.gz|\.xz)$ 0 0% 0 refresh-ims |
782 | +refresh_pattern . 0 20% 4320 |
783 | +forwarded_for delete |
784 | +visible_hostname {{fqdn}} |
785 | +cache_mem 512 MB |
786 | +minimum_object_size 0 MB |
787 | +maximum_object_size 1024 MB |
788 | +maximum_object_size_in_memory 100 MB |
789 | +cache_dir aufs /var/spool/maas-proxy 40000 16 256 |
790 | +# use different logs |
791 | +cache_access_log /var/log/maas/proxy/access.log |
792 | +cache_log /var/log/maas/proxy/cache.log |
793 | +cache_store_log /var/log/maas/proxy/store.log |
Overall this looks really good. The service reloading needs an improvement to use the serviceMonitor so the service status is kept in sync with what as known about the service.