Merge lp:~lamont/maas/create-maas-proxy.conf into lp:~maas-committers/maas/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
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/maas/maas-proxy.conf for maas-proxy to use.

Description of the change

Create /var/cache/maas/maas-proxy.conf for maas-proxy to use.

To post a comment you must log in.
Revision history for this message
Blake Rouse (blake-rouse) wrote :

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.

review: Needs Fixing
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