Merge lp:~allenap/maas/dns-serials--bug-1571645 into lp:~maas-committers/maas/trunk

Proposed by Gavin Panella
Status: Merged
Approved by: Gavin Panella
Approved revision: no longer in the source branch.
Merged at revision: 4981
Proposed branch: lp:~allenap/maas/dns-serials--bug-1571645
Merge into: lp:~maas-committers/maas/trunk
Prerequisite: lp:~allenap/maas/dns-serials--bug-1571645--sequence
Diff against target: 1539 lines (+549/-189)
12 files modified
src/maasserver/api/tests/test_domains.py (+33/-13)
src/maasserver/dns/config.py (+3/-5)
src/maasserver/dns/tests/test_config.py (+25/-40)
src/maasserver/migrations/builtin/maasserver/0057_initial_dns_publication.py (+37/-0)
src/maasserver/migrations/builtin/maasserver/0058_bigger_integer_for_dns_publication_serial.py (+22/-0)
src/maasserver/migrations/builtin/maasserver/0059_merge.py (+13/-0)
src/maasserver/models/dnspublication.py (+19/-5)
src/maasserver/models/tests/test_dnspublication.py (+41/-5)
src/maasserver/triggers/system.py (+103/-31)
src/maasserver/triggers/testing.py (+49/-11)
src/maasserver/triggers/tests/test_system_listener.py (+203/-78)
src/maasserver/triggers/tests/test_websocket_listener.py (+1/-1)
To merge this branch: bzr merge lp:~allenap/maas/dns-serials--bug-1571645
Reviewer Review Type Date Requested Status
LaMont Jones (community) Approve
Mike Pontillo (community) Approve
Review via email: mp+293307@code.launchpad.net

Commit message

Drive DNS publication from the DNSPublication model.

Previously a sequence was being used directly for the zone serial. However, sequences are explicitly not transactional: in an HA environment, a zone published on one region host would not necessarily have the same serial as the same zone published on another host. Using a regular table ensures that all region processes use the same serial.

To post a comment you must log in.
Revision history for this message
Mike Pontillo (mpontillo) wrote :

I like this branch; well done. Looks like an elegant solution to the problem. I've commented below with some suggestions. Nothing blocking, though.

Can you add more detail in the commit message about why it's doing what it's doing?

You may need to reorder migrations; I think Blake has a migration, too.

Finally, it would be nice if LaMont reviews this, too.

review: Approve
Revision history for this message
Gavin Panella (allenap) wrote :

> I like this branch; well done. Looks like an elegant solution to the
> problem. I've commented below with some suggestions. Nothing blocking,
> though.

Thanks!

> Can you add more detail in the commit message about why it's doing what
> it's doing?

Sure; done.

> You may need to reorder migrations; I think Blake has a migration,
> too.

Ah, yes. Merged.

> Finally, it would be nice if LaMont reviews this, too.

I will prod him about it.

Revision history for this message
Gavin Panella (allenap) wrote :

LaMont, do you mind sanity checking this?

Revision history for this message
LaMont Jones (lamont) wrote :

A minor test question or two, but looks reasonable to me. No objections here.

review: Approve
Revision history for this message
Gavin Panella (allenap) wrote :

Thanks LaMont!

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'src/maasserver/api/tests/test_domains.py'
2--- src/maasserver/api/tests/test_domains.py 2016-04-29 11:37:21 +0000
3+++ src/maasserver/api/tests/test_domains.py 2016-04-29 11:37:21 +0000
4@@ -99,19 +99,39 @@
5 'serial': str(serial)})
6 self.assertEqual(
7 http.client.OK, response.status_code, response.content)
8- self.assertEqual(serial, next(zone_serial))
9-
10- def test_set_serial_detects_bad_values(self):
11- zone_serial.create_if_not_exists()
12- self.become_admin()
13- uri = get_domains_uri()
14- serial = random.randint(1, INT_MAX)
15- response = self.client.post(uri, {
16- 'op': 'set_serial',
17- 'serial': str(serial)})
18- self.assertEqual(
19- http.client.OK, response.status_code, response.content)
20- self.assertEqual(serial, next(zone_serial))
21+ # The handler forces a DNS reload by creating a new DNS publication,
22+ # so the serial has already been incremented.
23+ self.assertEqual(serial + 1, next(zone_serial))
24+
25+ def test_set_serial_rejects_serials_less_than_1(self):
26+ zone_serial.create_if_not_exists()
27+ self.become_admin()
28+ uri = get_domains_uri()
29+ # A serial of 1 is fine.
30+ response = self.client.post(
31+ uri, {'op': 'set_serial', 'serial': '1'})
32+ self.assertEqual(
33+ http.client.OK, response.status_code, response.content)
34+ # A serial of 0 is rejected.
35+ response = self.client.post(
36+ uri, {'op': 'set_serial', 'serial': '0'})
37+ self.assertEqual(
38+ http.client.BAD_REQUEST, response.status_code, response.content)
39+
40+ def test_set_serial_rejects_serials_greater_than_4294967295(self):
41+ zone_serial.create_if_not_exists()
42+ self.become_admin()
43+ uri = get_domains_uri()
44+ # A serial of 4294967295 is fine.
45+ response = self.client.post(
46+ uri, {'op': 'set_serial', 'serial': '4294967295'})
47+ self.assertEqual(
48+ http.client.OK, response.status_code, response.content)
49+ # A serial of 4294967296 is rejected.
50+ response = self.client.post(
51+ uri, {'op': 'set_serial', 'serial': '4294967296'})
52+ self.assertEqual(
53+ http.client.BAD_REQUEST, response.status_code, response.content)
54
55
56 class TestDomainAPI(APITestCase):
57
58=== modified file 'src/maasserver/dns/config.py'
59--- src/maasserver/dns/config.py 2016-04-29 11:37:21 +0000
60+++ src/maasserver/dns/config.py 2016-04-29 11:37:21 +0000
61@@ -9,11 +9,10 @@
62 ]
63
64 from django.conf import settings
65-from django.db import connection
66 from maasserver.dns.zonegenerator import ZoneGenerator
67 from maasserver.enum import RDNS_MODE
68 from maasserver.models.config import Config
69-from maasserver.models.dnspublication import zone_serial
70+from maasserver.models.dnspublication import DNSPublication
71 from maasserver.models.domain import Domain
72 from maasserver.models.subnet import Subnet
73 from provisioningserver.dns.actions import (
74@@ -30,7 +29,7 @@
75
76
77 def current_zone_serial():
78- return '%0.10d' % zone_serial.current()
79+ return '%0.10d' % DNSPublication.objects.get_most_recent().serial
80
81
82 def is_dns_enabled():
83@@ -40,8 +39,7 @@
84
85 def dns_force_reload():
86 """Force the DNS to be regenerated."""
87- cursor = connection.cursor()
88- cursor.execute("SELECT pg_notify('sys_dns', '')")
89+ DNSPublication(source="Force reload").save()
90
91
92 def dns_update_all_zones(reload_retry=False):
93
94=== modified file 'src/maasserver/dns/tests/test_config.py'
95--- src/maasserver/dns/tests/test_config.py 2016-03-31 23:34:55 +0000
96+++ src/maasserver/dns/tests/test_config.py 2016-04-29 11:37:21 +0000
97@@ -8,7 +8,6 @@
98 import random
99 import time
100
101-from crochet import wait_for
102 from django.conf import settings
103 from django.core.management import call_command
104 import dns.resolver
105@@ -20,7 +19,6 @@
106 dns_update_all_zones,
107 get_trusted_networks,
108 get_upstream_dns,
109- zone_serial,
110 )
111 from maasserver.enum import (
112 IPADDRESS_TYPE,
113@@ -31,11 +29,10 @@
114 Config,
115 Domain,
116 )
117+from maasserver.models.dnspublication import DNSPublication
118 from maasserver.testing.config import RegionConfigurationFixture
119 from maasserver.testing.factory import factory
120 from maasserver.testing.testcase import MAASServerTestCase
121-from maasserver.utils.orm import transactional
122-from maasserver.utils.threads import deferToDatabase
123 from maastesting.matchers import MockCalledOnceWith
124 from netaddr import IPAddress
125 from provisioningserver.dns.config import (
126@@ -51,16 +48,13 @@
127 BINDServer,
128 )
129 from provisioningserver.testing.tests.test_bindfixture import dig_call
130-from provisioningserver.utils.twisted import (
131- DeferredValue,
132- retries,
133-)
134+from provisioningserver.utils.twisted import retries
135 from testtools.matchers import (
136 Contains,
137+ Equals,
138 FileContains,
139 MatchesStructure,
140 )
141-from twisted.internet.defer import inlineCallbacks
142
143
144 class TestDNSUtilities(MAASServerTestCase):
145@@ -70,34 +64,24 @@
146 self.patch(listener, "HANDLE_NOTIFY_DELAY", 0)
147 return listener
148
149- def test_zone_serial_parameters(self):
150- self.assertThat(
151- zone_serial,
152- MatchesStructure.byEquality(
153- maxvalue=2 ** 32 - 1,
154- minvalue=1,
155- increment=1,
156- )
157- )
158-
159- def test_current_zone_serial_returns_same_serial(self):
160- zone_serial.create_if_not_exists()
161- initial = current_zone_serial()
162- self.assertEquals(initial, current_zone_serial())
163-
164- @wait_for(30)
165- @inlineCallbacks
166- def test_dns_force_reload_sends_notification(self):
167- dv = DeferredValue()
168- listener = self.make_listener_without_delay()
169- listener.register(
170- "sys_dns", lambda *args: dv.set(args))
171- yield listener.startService()
172- try:
173- yield deferToDatabase(transactional(dns_force_reload))
174- yield dv.get(timeout=2)
175- finally:
176- yield listener.stopService()
177+ def test_current_zone_serial_returns_serial_of_latest_publication(self):
178+ publication = DNSPublication(source=factory.make_name("source"))
179+ publication.save()
180+ self.assertThat(
181+ int(current_zone_serial()),
182+ Equals(publication.serial))
183+
184+ def test_dns_force_reload_saves_new_publication(self):
185+ # A 'sys_dns' signal is also sent, but that is a side-effect of
186+ # inserting into the DNS publications table, and is tested as part of
187+ # the system triggers code.
188+ self.assertRaises(
189+ DNSPublication.DoesNotExist,
190+ DNSPublication.objects.get_most_recent)
191+ dns_force_reload()
192+ self.assertThat(
193+ DNSPublication.objects.get_most_recent(),
194+ MatchesStructure.byEquality(source="Force reload"))
195
196
197 class TestDNSServer(MAASServerTestCase):
198@@ -112,8 +96,9 @@
199
200 def setUp(self):
201 super(TestDNSServer, self).setUp()
202- # Make sure the zone_serial is created.
203- zone_serial.create_if_not_exists()
204+ # Ensure there's an initial DNS publication. Outside of tests this is
205+ # guaranteed by a migration.
206+ DNSPublication(source="Initial").save()
207 # Allow test-local changes to configuration.
208 self.useFixture(RegionConfigurationFixture())
209 # Immediately make DNS changes as they're needed.
210@@ -202,7 +187,7 @@
211 else:
212 serial = ans.rrset.items[0].serial
213
214- if serial == zone_serial.current():
215+ if serial == DNSPublication.objects.get_most_recent().serial:
216 # The zone is up-to-date; we're done.
217 return
218 else:
219
220=== added file 'src/maasserver/migrations/builtin/maasserver/0057_initial_dns_publication.py'
221--- src/maasserver/migrations/builtin/maasserver/0057_initial_dns_publication.py 1970-01-01 00:00:00 +0000
222+++ src/maasserver/migrations/builtin/maasserver/0057_initial_dns_publication.py 2016-04-29 11:37:21 +0000
223@@ -0,0 +1,37 @@
224+# -*- coding: utf-8 -*-
225+from django.db import migrations
226+
227+
228+source = "Initial publication"
229+
230+
231+def create_initial_publication(apps, schema_editor):
232+ # We can't import the DNSPublication model directly as it may be a newer
233+ # version than this migration expects. We use the historical version.
234+ DNSPublication = apps.get_model("maasserver", "DNSPublication")
235+ if not DNSPublication.objects.all().exists():
236+ publication = DNSPublication(source=source)
237+ publication.save()
238+
239+
240+def remove_initial_publication(apps, schema_editor):
241+ # We can't import the DNSPublication model directly as it may be a newer
242+ # version than this migration expects. We use the historical version.
243+ DNSPublication = apps.get_model("maasserver", "DNSPublication")
244+ for publication in DNSPublication.objects.order_by("id")[:1]:
245+ if publication.source == source:
246+ publication.delete()
247+
248+
249+class Migration(migrations.Migration):
250+
251+ dependencies = [
252+ ('maasserver', '0056_zone_serial_ownership'),
253+ ]
254+
255+ operations = [
256+ migrations.RunPython(
257+ create_initial_publication,
258+ remove_initial_publication,
259+ ),
260+ ]
261
262=== added file 'src/maasserver/migrations/builtin/maasserver/0058_bigger_integer_for_dns_publication_serial.py'
263--- src/maasserver/migrations/builtin/maasserver/0058_bigger_integer_for_dns_publication_serial.py 1970-01-01 00:00:00 +0000
264+++ src/maasserver/migrations/builtin/maasserver/0058_bigger_integer_for_dns_publication_serial.py 2016-04-29 11:37:21 +0000
265@@ -0,0 +1,22 @@
266+# -*- coding: utf-8 -*-
267+import django.core.validators
268+from django.db import (
269+ migrations,
270+ models,
271+)
272+import maasserver.models.dnspublication
273+
274+
275+class Migration(migrations.Migration):
276+
277+ dependencies = [
278+ ('maasserver', '0057_initial_dns_publication'),
279+ ]
280+
281+ operations = [
282+ migrations.AlterField(
283+ model_name='dnspublication',
284+ name='serial',
285+ field=models.BigIntegerField(editable=False, validators=(django.core.validators.MinValueValidator(1), django.core.validators.MaxValueValidator(4294967295)), default=maasserver.models.dnspublication.next_serial),
286+ ),
287+ ]
288
289=== added file 'src/maasserver/migrations/builtin/maasserver/0059_merge.py'
290--- src/maasserver/migrations/builtin/maasserver/0059_merge.py 1970-01-01 00:00:00 +0000
291+++ src/maasserver/migrations/builtin/maasserver/0059_merge.py 2016-04-29 11:37:21 +0000
292@@ -0,0 +1,13 @@
293+# -*- coding: utf-8 -*-
294+from django.db import migrations
295+
296+
297+class Migration(migrations.Migration):
298+
299+ dependencies = [
300+ ('maasserver', '0058_bigger_integer_for_dns_publication_serial'),
301+ ('maasserver', '0057_merge'),
302+ ]
303+
304+ operations = [
305+ ]
306
307=== modified file 'src/maasserver/models/dnspublication.py'
308--- src/maasserver/models/dnspublication.py 2016-04-29 11:37:21 +0000
309+++ src/maasserver/models/dnspublication.py 2016-04-29 11:37:21 +0000
310@@ -16,9 +16,9 @@
311 Model,
312 )
313 from django.db.models.fields import (
314+ BigIntegerField,
315 CharField,
316 DateTimeField,
317- IntegerField,
318 )
319 from maasserver import DefaultMeta
320 from maasserver.sequence import (
321@@ -42,12 +42,26 @@
322 """Manager for DNS publishing records."""
323
324 def get_most_recent(self):
325- """Return the most recently inserted `DNSPublication`."""
326- return self.order_by("-id")[0]
327+ """Return the most recently inserted `DNSPublication`.
328+
329+ :raise DoesNotExist: If DNS has never been published.
330+ """
331+ for publication in self.order_by("-id")[:1]:
332+ return publication
333+ else:
334+ # This is unlikely to be a problem in a running MAAS installation,
335+ # but it can crop up a lot in tests because we can't, for example,
336+ # use migrations to provide an initial publication.
337+ raise self.model.DoesNotExist() from None
338
339 def collect_garbage(self):
340 """Delete all but the most recently inserted `DNSPublication`."""
341- self.filter(id__lt=self.get_most_recent().id).delete()
342+ try:
343+ publication = self.get_most_recent()
344+ except self.model.DoesNotExist:
345+ pass # Nothing to do.
346+ else:
347+ self.filter(id__lt=publication.id).delete()
348
349
350 class DNSPublication(Model):
351@@ -67,7 +81,7 @@
352
353 # The serial number with which to publish the zone. We don't use the
354 # primary key for this because zone serials are allowed to cycle.
355- serial = IntegerField(
356+ serial = BigIntegerField(
357 editable=False, null=False, default=next_serial, unique=False,
358 validators=(
359 MinValueValidator(zone_serial.minvalue),
360
361=== modified file 'src/maasserver/models/tests/test_dnspublication.py'
362--- src/maasserver/models/tests/test_dnspublication.py 2016-04-21 16:32:47 +0000
363+++ src/maasserver/models/tests/test_dnspublication.py 2016-04-29 11:37:21 +0000
364@@ -11,7 +11,11 @@
365 )
366 from random import randint
367
368-from maasserver.models.dnspublication import DNSPublication
369+from django.db import connection
370+from maasserver.models.dnspublication import (
371+ DNSPublication,
372+ zone_serial,
373+)
374 from maasserver.testing.factory import factory
375 from maasserver.testing.testcase import MAASServerTestCase
376 from testtools.matchers import (
377@@ -24,6 +28,29 @@
378 )
379
380
381+class TestZoneSerial(MAASServerTestCase):
382+ """Tests for the `maasserver_zone_serial_seq` sequence."""
383+
384+ def test_parameters(self):
385+ self.assertThat(
386+ zone_serial, MatchesStructure.byEquality(
387+ maxvalue=2 ** 32 - 1, minvalue=1, increment=1, cycle=True,
388+ owner="maasserver_dnspublication.serial",
389+ ))
390+
391+ def test_parameters_in_database(self):
392+ zone_serial.create_if_not_exists()
393+ query = (
394+ "SELECT start_value, increment_by, max_value, "
395+ "min_value, is_cycled FROM %s" % zone_serial.name
396+ )
397+ with connection.cursor() as cursor:
398+ cursor.execute(query)
399+ self.assertThat(
400+ cursor.fetchone(), Equals(
401+ (1, 1, 2 ** 32 - 1, 1, True)))
402+
403+
404 class TestDNSPublication(MAASServerTestCase):
405 """Test the `DNSPublication` model."""
406
407@@ -68,10 +95,14 @@
408 MatchesStructure(serial=Equals(10)))
409
410 def test_get_most_recent_crashes_when_no_publications(self):
411- # This is okay because we're going to ensure (using a migration) that
412- # there is never less than one publication in the table. If this crash
413- # happens we have bigger problems.
414- self.assertRaises(IndexError, DNSPublication.objects.get_most_recent)
415+ # This is okay because we ensure (using a migration) that there is
416+ # never less than one publication in the table. If this crash happens
417+ # we have bigger problems. However, we do not currently use migrations
418+ # in tests, so it is important to have a deterministic outcome when
419+ # there are no publications.
420+ self.assertRaises(
421+ DNSPublication.DoesNotExist,
422+ DNSPublication.objects.get_most_recent)
423
424 def test_collect_garbage_removes_all_but_most_recent_record(self):
425 for serial in range(10):
426@@ -82,3 +113,8 @@
427 self.assertThat(
428 DNSPublication.objects.get_most_recent(),
429 MatchesStructure(serial=Equals(serial)))
430+
431+ def test_collect_garbage_does_nothing_when_no_publications(self):
432+ self.assertThat(DNSPublication.objects.all(), HasLength(0))
433+ DNSPublication.objects.collect_garbage()
434+ self.assertThat(DNSPublication.objects.all(), HasLength(0))
435
436=== modified file 'src/maasserver/triggers/system.py'
437--- src/maasserver/triggers/system.py 2016-04-29 11:37:21 +0000
438+++ src/maasserver/triggers/system.py 2016-04-29 11:37:21 +0000
439@@ -787,11 +787,26 @@
440 DNS_SUBNET_UPDATE = dedent("""\
441 CREATE OR REPLACE FUNCTION sys_dns_subnet_update()
442 RETURNS trigger as $$
443+ DECLARE
444+ changes text[];
445 BEGIN
446- IF OLD.cidr != NEW.cidr OR OLD.rdns_mode != NEW.rdns_mode THEN
447- -- Increment the zone serial.
448- PERFORM nextval('maasserver_zone_serial_seq');
449- PERFORM pg_notify('sys_dns', '');
450+ IF OLD.cidr != NEW.cidr THEN
451+ changes := changes || (
452+ 'CIDR changed from ' || OLD.cidr || ' to ' || NEW.cidr);
453+ END IF;
454+ IF OLD.rdns_mode != NEW.rdns_mode THEN
455+ changes := changes || (
456+ 'RDNS mode changed from ' || OLD.rdns_mode || ' to ' ||
457+ NEW.rdns_mode);
458+ END IF;
459+ IF array_length(changes, 1) != 0 THEN
460+ INSERT INTO maasserver_dnspublication
461+ (serial, created, source)
462+ VALUES
463+ (nextval('maasserver_zone_serial_seq'), now(),
464+ substring(
465+ ('Subnet ' || NEW.name || ': ' || array_to_string(changes, ', '))
466+ FOR 255));
467 END IF;
468 RETURN NEW;
469 END;
470@@ -804,11 +819,25 @@
471 DNS_NODE_UPDATE = dedent("""\
472 CREATE OR REPLACE FUNCTION sys_dns_node_update()
473 RETURNS trigger as $$
474+ DECLARE
475+ changes text[];
476 BEGIN
477- IF OLD.hostname != NEW.hostname OR OLD.domain_id != NEW.domain_id THEN
478- -- Increment the zone serial.
479- PERFORM nextval('maasserver_zone_serial_seq');
480- PERFORM pg_notify('sys_dns', '');
481+ IF OLD.hostname != NEW.hostname THEN
482+ changes := changes || (
483+ 'hostname changed from ' || OLD.hostname || ' to ' || NEW.hostname);
484+ END IF;
485+ IF OLD.domain_id != NEW.domain_id THEN
486+ changes := changes || 'domain changed'::text;
487+ END IF;
488+ IF array_length(changes, 1) != 0 THEN
489+ INSERT INTO maasserver_dnspublication
490+ (serial, created, source)
491+ VALUES
492+ (nextval('maasserver_zone_serial_seq'), now(),
493+ substring(
494+ ('Node ' || NEW.system_id || ': ' ||
495+ array_to_string(changes, ', '))
496+ FOR 255));
497 END IF;
498 RETURN NEW;
499 END;
500@@ -822,14 +851,29 @@
501 DNS_INTERFACE_UPDATE = dedent("""\
502 CREATE OR REPLACE FUNCTION sys_dns_interface_update()
503 RETURNS trigger as $$
504+ DECLARE
505+ changes text[];
506 BEGIN
507- IF (OLD.name != NEW.name OR
508- (OLD.node_id IS NULL AND NEW.node_id IS NOT NULL) OR
509- (OLD.node_id IS NOT NULL AND NEW.node_id IS NULL) OR
510- (OLD.node_id != NEW.node_id)) THEN
511- -- Increment the zone serial.
512- PERFORM nextval('maasserver_zone_serial_seq');
513- PERFORM pg_notify('sys_dns', '');
514+ IF OLD.name != NEW.name THEN
515+ changes := changes || (
516+ 'renamed from ' || OLD.name || ' to ' || NEW.name);
517+ END IF;
518+ IF OLD.node_id IS NULL AND NEW.node_id IS NOT NULL THEN
519+ changes := changes || 'node set'::text;
520+ ELSIF OLD.node_id IS NOT NULL AND NEW.node_id IS NULL THEN
521+ changes := changes || 'node unset'::text;
522+ ELSIF OLD.node_id != NEW.node_id THEN
523+ changes := changes || 'node changed'::text;
524+ END IF;
525+ IF array_length(changes, 1) != 0 THEN
526+ INSERT INTO maasserver_dnspublication
527+ (serial, created, source)
528+ VALUES
529+ (nextval('maasserver_zone_serial_seq'), now(),
530+ substring(
531+ ('Interface ' || NEW.name || ': ' ||
532+ array_to_string(changes, ', '))
533+ FOR 255));
534 END IF;
535 RETURN NEW;
536 END;
537@@ -847,10 +891,14 @@
538 -- Only care about the
539 IF (NEW.name = 'upstream_dns' OR
540 NEW.name = 'default_dns_ttl' OR
541- NEW.name = 'windows_kms_host') THEN
542- -- Increment the zone serial.
543- PERFORM nextval('maasserver_zone_serial_seq');
544- PERFORM pg_notify('sys_dns', '');
545+ NEW.name = 'windows_kms_host')
546+ THEN
547+ INSERT INTO maasserver_dnspublication
548+ (serial, created, source)
549+ VALUES
550+ (nextval('maasserver_zone_serial_seq'), now(), substring(
551+ ('Configuration ' || NEW.name || ' set to ' || NEW.value)
552+ FOR 255));
553 END IF;
554 RETURN NEW;
555 END;
556@@ -869,16 +917,33 @@
557 IF (OLD.value != NEW.value AND (
558 NEW.name = 'upstream_dns' OR
559 NEW.name = 'default_dns_ttl' OR
560- NEW.name = 'windows_kms_host')) THEN
561- -- Increment the zone serial.
562- PERFORM nextval('maasserver_zone_serial_seq');
563- PERFORM pg_notify('sys_dns', '');
564+ NEW.name = 'windows_kms_host'))
565+ THEN
566+ INSERT INTO maasserver_dnspublication
567+ (serial, created, source)
568+ VALUES
569+ (nextval('maasserver_zone_serial_seq'), now(), substring(
570+ ('Configuration ' || NEW.name || ' changed from ' ||
571+ OLD.value || ' to ' || NEW.value)
572+ FOR 255));
573 END IF;
574 RETURN NEW;
575 END;
576 $$ LANGUAGE plpgsql;
577 """)
578
579+# Triggered when DNS needs to be published. In essense this means on insert
580+# into maasserver_dnspublication.
581+DNS_PUBLISH = dedent("""\
582+ CREATE OR REPLACE FUNCTION sys_dns_publish()
583+ RETURNS trigger AS $$
584+ BEGIN
585+ PERFORM pg_notify('sys_dns', '');
586+ RETURN NEW;
587+ END;
588+ $$ LANGUAGE plpgsql;
589+ """)
590+
591
592 # Triggered when a subnet is updated. Increments notifies that proxy needs to
593 # be updated. Only watches changes on the cidr and allow_proxy.
594@@ -896,22 +961,23 @@
595
596
597 def render_sys_dns_procedure(proc_name, on_delete=False):
598- """Render a database procedure with name `proc_name` that increments
599- the zone serial and notifies that a DNS update is needed.
600+ """Render a database procedure that creates a new DNS publication.
601
602 :param proc_name: Name of the procedure.
603 :param on_delete: True when procedure will be used as a delete trigger.
604 """
605 return dedent("""\
606- CREATE OR REPLACE FUNCTION %s() RETURNS trigger AS $$
607+ CREATE OR REPLACE FUNCTION {proc}() RETURNS trigger AS $$
608 BEGIN
609- -- Increment the zone serial.
610- PERFORM nextval('maasserver_zone_serial_seq');
611- PERFORM pg_notify('sys_dns', '');
612- RETURN %s;
613+ INSERT INTO maasserver_dnspublication
614+ (serial, created, source)
615+ VALUES
616+ (nextval('maasserver_zone_serial_seq'), now(),
617+ substring('Call to {proc}' FOR 255));
618+ RETURN {rval};
619 END;
620 $$ LANGUAGE plpgsql;
621- """ % (proc_name, 'NEW' if not on_delete else 'OLD'))
622+ """).format(proc=proc_name, rval='OLD' if on_delete else 'NEW')
623
624
625 def render_sys_proxy_procedure(proc_name, on_delete=False):
626@@ -1107,6 +1173,12 @@
627 "maasserver_dnsdata",
628 "sys_dns_dnsdata_delete", "delete")
629
630+ # - DNSPublication
631+ register_procedure(DNS_PUBLISH)
632+ register_trigger(
633+ "maasserver_dnspublication",
634+ "sys_dns_publish", "insert")
635+
636 # - Subnet
637 register_procedure(
638 render_sys_dns_procedure("sys_dns_subnet_insert"))
639
640=== renamed file 'src/maasserver/triggers/tests/helper.py' => 'src/maasserver/triggers/testing.py'
641--- src/maasserver/triggers/tests/helper.py 2016-04-29 11:37:21 +0000
642+++ src/maasserver/triggers/testing.py 2016-04-29 11:37:21 +0000
643@@ -17,7 +17,7 @@
644 from maasserver.models.cacheset import CacheSet
645 from maasserver.models.dhcpsnippet import DHCPSnippet
646 from maasserver.models.dnsdata import DNSData
647-from maasserver.models.dnspublication import zone_serial
648+from maasserver.models.dnspublication import DNSPublication
649 from maasserver.models.dnsresource import DNSResource
650 from maasserver.models.domain import Domain
651 from maasserver.models.event import Event
652@@ -55,7 +55,15 @@
653 )
654 from maasserver.utils.threads import deferToDatabase
655 from metadataserver.models import NodeResult
656-from twisted.internet.defer import inlineCallbacks
657+from testtools.matchers import (
658+ GreaterThan,
659+ Is,
660+ Not,
661+)
662+from twisted.internet.defer import (
663+ inlineCallbacks,
664+ returnValue,
665+)
666
667
668 wait_for_reactor = wait_for(30) # 30 seconds.
669@@ -638,12 +646,42 @@
670 class DNSHelpersMixin:
671 """Helper to get the zone serial and to assert it was incremented."""
672
673- def get_zone_serial_current(self):
674- return deferToDatabase(transactional(zone_serial.current))
675-
676- @inlineCallbacks
677- def assertZoneSerialIncrement(self, previous):
678- current = yield deferToDatabase(transactional(zone_serial.current))
679- self.assertTrue(
680- current > previous,
681- "Zone serial was not incremented.")
682+ @transactional
683+ def getPublication(self):
684+ try:
685+ return DNSPublication.objects.get_most_recent()
686+ except DNSPublication.DoesNotExist:
687+ return None
688+
689+ @inlineCallbacks
690+ def capturePublication(self):
691+ """Capture the most recent `DNSPublication` record."""
692+ self.__publication = yield deferToDatabase(self.getPublication)
693+ returnValue(self.__publication)
694+
695+ def getCapturedPublication(self):
696+ """Return the captured publication."""
697+ try:
698+ return self.__publication
699+ except AttributeError:
700+ self.fail(
701+ "No reference publication has been captured; "
702+ "use `capturePublication` before calling "
703+ "`getCapturedPublication`.")
704+
705+ @inlineCallbacks
706+ def assertPublicationUpdated(self):
707+ """Assert there's a newer `DNSPublication` record.
708+
709+ Call `capturePublication` first to obtain a reference record.
710+ """
711+ old = self.getCapturedPublication()
712+ new = yield self.capturePublication()
713+ if old is None:
714+ self.assertThat(
715+ new, Not(Is(None)),
716+ "DNS has not been published at all.")
717+ else:
718+ self.assertThat(
719+ new.serial, GreaterThan(old.serial),
720+ "DNS has not been published again.")
721
722=== modified file 'src/maasserver/triggers/tests/test_system_listener.py'
723--- src/maasserver/triggers/tests/test_system_listener.py 2016-03-31 23:34:55 +0000
724+++ src/maasserver/triggers/tests/test_system_listener.py 2016-04-29 11:37:21 +0000
725@@ -10,6 +10,7 @@
726 datetime,
727 timedelta,
728 )
729+import json
730 import random
731
732 from crochet import wait_for
733@@ -29,7 +30,7 @@
734 from maasserver.testing.factory import factory
735 from maasserver.testing.testcase import MAASTransactionServerTestCase
736 from maasserver.triggers.system import register_system_triggers
737-from maasserver.triggers.tests.helper import (
738+from maasserver.triggers.testing import (
739 DNSHelpersMixin,
740 TransactionalHelpersMixin,
741 )
742@@ -37,6 +38,7 @@
743 from maasserver.utils.threads import deferToDatabase
744 from netaddr import IPAddress
745 from provisioningserver.utils.twisted import DeferredValue
746+from testtools.matchers import Equals
747 from twisted.internet.defer import (
748 CancelledError,
749 inlineCallbacks,
750@@ -2293,7 +2295,7 @@
751 @inlineCallbacks
752 def test_sends_message_for_domain_insert(self):
753 yield deferToDatabase(register_system_triggers)
754- zone_serial = yield self.get_zone_serial_current()
755+ yield self.capturePublication()
756 dv = DeferredValue()
757 listener = self.make_listener_without_delay()
758 listener.register(
759@@ -2302,16 +2304,19 @@
760 try:
761 yield deferToDatabase(self.create_domain)
762 yield dv.get(timeout=2)
763- yield self.assertZoneSerialIncrement(zone_serial)
764+ yield self.assertPublicationUpdated()
765 finally:
766 yield listener.stopService()
767+ self.assertThat(
768+ self.getCapturedPublication().source,
769+ Equals("Call to sys_dns_domain_insert"))
770
771 @wait_for_reactor
772 @inlineCallbacks
773 def test_sends_message_for_domain_update(self):
774 yield deferToDatabase(register_system_triggers)
775 domain = yield deferToDatabase(self.create_domain)
776- zone_serial = yield self.get_zone_serial_current()
777+ yield self.capturePublication()
778 dv = DeferredValue()
779 listener = self.make_listener_without_delay()
780 listener.register(
781@@ -2322,16 +2327,19 @@
782 "name": factory.make_name("domain"),
783 })
784 yield dv.get(timeout=2)
785- yield self.assertZoneSerialIncrement(zone_serial)
786+ yield self.assertPublicationUpdated()
787 finally:
788 yield listener.stopService()
789+ self.assertThat(
790+ self.getCapturedPublication().source,
791+ Equals("Call to sys_dns_domain_update"))
792
793 @wait_for_reactor
794 @inlineCallbacks
795 def test_sends_message_for_domain_delete(self):
796 yield deferToDatabase(register_system_triggers)
797 domain = yield deferToDatabase(self.create_domain)
798- zone_serial = yield self.get_zone_serial_current()
799+ yield self.capturePublication()
800 dv = DeferredValue()
801 listener = self.make_listener_without_delay()
802 listener.register(
803@@ -2340,9 +2348,12 @@
804 try:
805 yield deferToDatabase(self.delete_domain, domain.id)
806 yield dv.get(timeout=2)
807- yield self.assertZoneSerialIncrement(zone_serial)
808+ yield self.assertPublicationUpdated()
809 finally:
810 yield listener.stopService()
811+ self.assertThat(
812+ self.getCapturedPublication().source,
813+ Equals("Call to sys_dns_domain_delete"))
814
815
816 class TestDNSStaticIPAddressListener(
817@@ -2355,7 +2366,7 @@
818 def test_sends_message_for_staticipaddress_update(self):
819 yield deferToDatabase(register_system_triggers)
820 sip = yield deferToDatabase(self.create_staticipaddress)
821- zone_serial = yield self.get_zone_serial_current()
822+ yield self.capturePublication()
823 dv = DeferredValue()
824 listener = self.make_listener_without_delay()
825 listener.register(
826@@ -2366,9 +2377,12 @@
827 "alloc_type": IPADDRESS_TYPE.STICKY,
828 })
829 yield dv.get(timeout=2)
830- yield self.assertZoneSerialIncrement(zone_serial)
831+ yield self.assertPublicationUpdated()
832 finally:
833 yield listener.stopService()
834+ self.assertThat(
835+ self.getCapturedPublication().source,
836+ Equals("Call to sys_dns_staticipaddress_update"))
837
838
839 class TestDNSInterfaceStaticIPAddressListener(
840@@ -2381,7 +2395,7 @@
841 def test_sends_message_for_interface_staticipaddress_link(self):
842 yield deferToDatabase(register_system_triggers)
843 interface = yield deferToDatabase(self.create_interface)
844- zone_serial = yield self.get_zone_serial_current()
845+ yield self.capturePublication()
846 dv = DeferredValue()
847 listener = self.make_listener_without_delay()
848 listener.register(
849@@ -2392,9 +2406,12 @@
850 "interface": interface,
851 })
852 yield dv.get(timeout=2)
853- yield self.assertZoneSerialIncrement(zone_serial)
854+ yield self.assertPublicationUpdated()
855 finally:
856 yield listener.stopService()
857+ self.assertThat(
858+ self.getCapturedPublication().source,
859+ Equals("Call to sys_dns_nic_ip_link"))
860
861 @wait_for_reactor
862 @inlineCallbacks
863@@ -2404,7 +2421,7 @@
864 sip = yield deferToDatabase(self.create_staticipaddress, {
865 "interface": interface,
866 })
867- zone_serial = yield self.get_zone_serial_current()
868+ yield self.capturePublication()
869 dv = DeferredValue()
870 listener = self.make_listener_without_delay()
871 listener.register(
872@@ -2413,9 +2430,12 @@
873 try:
874 yield deferToDatabase(self.delete_staticipaddress, sip.id)
875 yield dv.get(timeout=2)
876- yield self.assertZoneSerialIncrement(zone_serial)
877+ yield self.assertPublicationUpdated()
878 finally:
879 yield listener.stopService()
880+ self.assertThat(
881+ self.getCapturedPublication().source,
882+ Equals("Call to sys_dns_nic_ip_unlink"))
883
884
885 class TestDNSDNSResourceListener(
886@@ -2427,25 +2447,31 @@
887 @inlineCallbacks
888 def test_sends_message_for_dnsresource_insert(self):
889 yield deferToDatabase(register_system_triggers)
890- zone_serial = yield self.get_zone_serial_current()
891+ yield self.capturePublication()
892 dv = DeferredValue()
893 listener = self.make_listener_without_delay()
894 listener.register(
895 "sys_dns", lambda *args: dv.set(args))
896 yield listener.startService()
897 try:
898- yield deferToDatabase(self.create_dnsresource)
899+ # Pass ip_addresses=[] to avoid an extra .save() -- and thus an
900+ # UPDATE -- in the factory method.
901+ yield deferToDatabase(
902+ self.create_dnsresource, {"ip_addresses": []})
903 yield dv.get(timeout=2)
904- yield self.assertZoneSerialIncrement(zone_serial)
905+ yield self.assertPublicationUpdated()
906 finally:
907 yield listener.stopService()
908+ self.assertThat(
909+ self.getCapturedPublication().source,
910+ Equals("Call to sys_dns_dnsresource_insert"))
911
912 @wait_for_reactor
913 @inlineCallbacks
914 def test_sends_message_for_dnsresource_update(self):
915 yield deferToDatabase(register_system_triggers)
916 resource = yield deferToDatabase(self.create_dnsresource)
917- zone_serial = yield self.get_zone_serial_current()
918+ yield self.capturePublication()
919 dv = DeferredValue()
920 listener = self.make_listener_without_delay()
921 listener.register(
922@@ -2456,16 +2482,19 @@
923 "name": factory.make_name("resource"),
924 })
925 yield dv.get(timeout=2)
926- yield self.assertZoneSerialIncrement(zone_serial)
927+ yield self.assertPublicationUpdated()
928 finally:
929 yield listener.stopService()
930+ self.assertThat(
931+ self.getCapturedPublication().source,
932+ Equals("Call to sys_dns_dnsresource_update"))
933
934 @wait_for_reactor
935 @inlineCallbacks
936 def test_sends_message_for_dnsresource_delete(self):
937 yield deferToDatabase(register_system_triggers)
938 resource = yield deferToDatabase(self.create_dnsresource)
939- zone_serial = yield self.get_zone_serial_current()
940+ yield self.capturePublication()
941 dv = DeferredValue()
942 listener = self.make_listener_without_delay()
943 listener.register(
944@@ -2474,9 +2503,12 @@
945 try:
946 yield deferToDatabase(self.delete_dnsresource, resource.id)
947 yield dv.get(timeout=2)
948- yield self.assertZoneSerialIncrement(zone_serial)
949+ yield self.assertPublicationUpdated()
950 finally:
951 yield listener.stopService()
952+ self.assertThat(
953+ self.getCapturedPublication().source,
954+ Equals("Call to sys_dns_dnsresource_delete"))
955
956
957 class TestDNSDNSResourceStaticIPAddressListener(
958@@ -2490,7 +2522,7 @@
959 yield deferToDatabase(register_system_triggers)
960 resource = yield deferToDatabase(self.create_dnsresource)
961 sip = yield deferToDatabase(self.create_staticipaddress)
962- zone_serial = yield self.get_zone_serial_current()
963+ yield self.capturePublication()
964 dv = DeferredValue()
965 listener = self.make_listener_without_delay()
966 listener.register(
967@@ -2499,9 +2531,12 @@
968 try:
969 yield deferToDatabase(resource.ip_addresses.add, sip)
970 yield dv.get(timeout=2)
971- yield self.assertZoneSerialIncrement(zone_serial)
972+ yield self.assertPublicationUpdated()
973 finally:
974 yield listener.stopService()
975+ self.assertThat(
976+ self.getCapturedPublication().source,
977+ Equals("Call to sys_dns_dnsresource_ip_link"))
978
979 @wait_for_reactor
980 @inlineCallbacks
981@@ -2510,7 +2545,7 @@
982 resource = yield deferToDatabase(self.create_dnsresource)
983 sip = yield deferToDatabase(self.create_staticipaddress)
984 yield deferToDatabase(resource.ip_addresses.add, sip)
985- zone_serial = yield self.get_zone_serial_current()
986+ yield self.capturePublication()
987 dv = DeferredValue()
988 listener = self.make_listener_without_delay()
989 listener.register(
990@@ -2519,9 +2554,12 @@
991 try:
992 yield deferToDatabase(resource.ip_addresses.remove, sip)
993 yield dv.get(timeout=2)
994- yield self.assertZoneSerialIncrement(zone_serial)
995+ yield self.assertPublicationUpdated()
996 finally:
997 yield listener.stopService()
998+ self.assertThat(
999+ self.getCapturedPublication().source,
1000+ Equals("Call to sys_dns_dnsresource_ip_unlink"))
1001
1002
1003 class TestDNSDNSDataListener(
1004@@ -2533,7 +2571,7 @@
1005 @inlineCallbacks
1006 def test_sends_message_for_dnsdata_insert(self):
1007 yield deferToDatabase(register_system_triggers)
1008- zone_serial = yield self.get_zone_serial_current()
1009+ yield self.capturePublication()
1010 dv = DeferredValue()
1011 listener = self.make_listener_without_delay()
1012 listener.register(
1013@@ -2542,9 +2580,12 @@
1014 try:
1015 yield deferToDatabase(self.create_dnsdata)
1016 yield dv.get(timeout=2)
1017- yield self.assertZoneSerialIncrement(zone_serial)
1018+ yield self.assertPublicationUpdated()
1019 finally:
1020 yield listener.stopService()
1021+ self.assertThat(
1022+ self.getCapturedPublication().source,
1023+ Equals("Call to sys_dns_dnsdata_insert"))
1024
1025 @wait_for_reactor
1026 @inlineCallbacks
1027@@ -2554,7 +2595,7 @@
1028 "rrtype": "TXT",
1029 "rrdata": factory.make_name("txt"),
1030 })
1031- zone_serial = yield self.get_zone_serial_current()
1032+ yield self.capturePublication()
1033 dv = DeferredValue()
1034 listener = self.make_listener_without_delay()
1035 listener.register(
1036@@ -2565,16 +2606,19 @@
1037 "rrdata": factory.make_name("txt"),
1038 })
1039 yield dv.get(timeout=2)
1040- yield self.assertZoneSerialIncrement(zone_serial)
1041+ yield self.assertPublicationUpdated()
1042 finally:
1043 yield listener.stopService()
1044+ self.assertThat(
1045+ self.getCapturedPublication().source,
1046+ Equals("Call to sys_dns_dnsdata_update"))
1047
1048 @wait_for_reactor
1049 @inlineCallbacks
1050 def test_sends_message_for_dnsdata_delete(self):
1051 yield deferToDatabase(register_system_triggers)
1052 data = yield deferToDatabase(self.create_dnsdata)
1053- zone_serial = yield self.get_zone_serial_current()
1054+ yield self.capturePublication()
1055 dv = DeferredValue()
1056 listener = self.make_listener_without_delay()
1057 listener.register(
1058@@ -2583,9 +2627,12 @@
1059 try:
1060 yield deferToDatabase(self.delete_dnsdata, data.id)
1061 yield dv.get(timeout=2)
1062- yield self.assertZoneSerialIncrement(zone_serial)
1063+ yield self.assertPublicationUpdated()
1064 finally:
1065 yield listener.stopService()
1066+ self.assertThat(
1067+ self.getCapturedPublication().source,
1068+ Equals("Call to sys_dns_dnsdata_delete"))
1069
1070
1071 class TestDNSSubnetListener(
1072@@ -2597,7 +2644,7 @@
1073 @inlineCallbacks
1074 def test_sends_message_for_subnet_insert(self):
1075 yield deferToDatabase(register_system_triggers)
1076- zone_serial = yield self.get_zone_serial_current()
1077+ yield self.capturePublication()
1078 dv = DeferredValue()
1079 listener = self.make_listener_without_delay()
1080 listener.register(
1081@@ -2606,16 +2653,19 @@
1082 try:
1083 yield deferToDatabase(self.create_subnet)
1084 yield dv.get(timeout=2)
1085- yield self.assertZoneSerialIncrement(zone_serial)
1086+ yield self.assertPublicationUpdated()
1087 finally:
1088 yield listener.stopService()
1089+ self.assertThat(
1090+ self.getCapturedPublication().source,
1091+ Equals("Call to sys_dns_subnet_insert"))
1092
1093 @wait_for_reactor
1094 @inlineCallbacks
1095 def test_sends_message_for_subnet_cidr_update(self):
1096 yield deferToDatabase(register_system_triggers)
1097 subnet = yield deferToDatabase(self.create_subnet)
1098- zone_serial = yield self.get_zone_serial_current()
1099+ yield self.capturePublication()
1100 dv = DeferredValue()
1101 listener = self.make_listener_without_delay()
1102 listener.register(
1103@@ -2623,43 +2673,53 @@
1104 yield listener.startService()
1105 try:
1106 network = factory.make_ip4_or_6_network()
1107+ cidr_old, cidr_new = subnet.cidr, str(network.cidr)
1108 yield deferToDatabase(self.update_subnet, subnet.id, {
1109- "cidr": str(network.cidr),
1110+ "cidr": cidr_new,
1111 "gateway_ip": factory.pick_ip_in_network(network),
1112 "dns_servers": [],
1113 })
1114 yield dv.get(timeout=2)
1115- yield self.assertZoneSerialIncrement(zone_serial)
1116+ yield self.assertPublicationUpdated()
1117 finally:
1118 yield listener.stopService()
1119+ self.assertThat(
1120+ self.getCapturedPublication().source, Equals(
1121+ "Subnet %s: CIDR changed from %s to %s"
1122+ % (subnet.name, cidr_old, cidr_new)))
1123
1124 @wait_for_reactor
1125 @inlineCallbacks
1126 def test_sends_message_for_subnet_rdns_mode_update(self):
1127 yield deferToDatabase(register_system_triggers)
1128 subnet = yield deferToDatabase(self.create_subnet)
1129- zone_serial = yield self.get_zone_serial_current()
1130+ yield self.capturePublication()
1131 dv = DeferredValue()
1132 listener = self.make_listener_without_delay()
1133 listener.register(
1134 "sys_dns", lambda *args: dv.set(args))
1135 yield listener.startService()
1136 try:
1137+ rdns_old = subnet.rdns_mode
1138+ rdns_new = factory.pick_enum(RDNS_MODE, but_not=[rdns_old])
1139 yield deferToDatabase(self.update_subnet, subnet.id, {
1140- "rdns_mode": factory.pick_enum(
1141- RDNS_MODE, but_not=[subnet.rdns_mode]),
1142+ "rdns_mode": rdns_new,
1143 })
1144 yield dv.get(timeout=2)
1145- yield self.assertZoneSerialIncrement(zone_serial)
1146+ yield self.assertPublicationUpdated()
1147 finally:
1148 yield listener.stopService()
1149+ self.assertThat(
1150+ self.getCapturedPublication().source, Equals(
1151+ "Subnet %s: RDNS mode changed from %s to %s"
1152+ % (subnet.name, rdns_old, rdns_new)))
1153
1154 @wait_for_reactor
1155 @inlineCallbacks
1156 def test_sends_message_for_subnet_delete(self):
1157 yield deferToDatabase(register_system_triggers)
1158 subnet = yield deferToDatabase(self.create_subnet)
1159- zone_serial = yield self.get_zone_serial_current()
1160+ yield self.capturePublication()
1161 dv = DeferredValue()
1162 listener = self.make_listener_without_delay()
1163 listener.register(
1164@@ -2668,9 +2728,12 @@
1165 try:
1166 yield deferToDatabase(self.delete_subnet, subnet.id)
1167 yield dv.get(timeout=2)
1168- yield self.assertZoneSerialIncrement(zone_serial)
1169+ yield self.assertPublicationUpdated()
1170 finally:
1171 yield listener.stopService()
1172+ self.assertThat(
1173+ self.getCapturedPublication().source,
1174+ Equals("Call to sys_dns_subnet_delete"))
1175
1176
1177 class TestDNSNodeListener(
1178@@ -2683,20 +2746,26 @@
1179 def test_sends_message_for_node_update_hostname(self):
1180 yield deferToDatabase(register_system_triggers)
1181 node = yield deferToDatabase(self.create_node)
1182- zone_serial = yield self.get_zone_serial_current()
1183+ yield self.capturePublication()
1184 dv = DeferredValue()
1185 listener = self.make_listener_without_delay()
1186 listener.register(
1187 "sys_dns", lambda *args: dv.set(args))
1188 yield listener.startService()
1189 try:
1190+ hostname_old = node.hostname
1191+ hostname_new = factory.make_name("hostname")
1192 yield deferToDatabase(self.update_node, node.system_id, {
1193- "hostname": factory.make_name("hostname"),
1194+ "hostname": hostname_new,
1195 })
1196 yield dv.get(timeout=2)
1197- yield self.assertZoneSerialIncrement(zone_serial)
1198+ yield self.assertPublicationUpdated()
1199 finally:
1200 yield listener.stopService()
1201+ self.assertThat(
1202+ self.getCapturedPublication().source, Equals(
1203+ "Node %s: hostname changed from %s to %s"
1204+ % (node.system_id, hostname_old, hostname_new)))
1205
1206 @wait_for_reactor
1207 @inlineCallbacks
1208@@ -2704,7 +2773,7 @@
1209 yield deferToDatabase(register_system_triggers)
1210 node = yield deferToDatabase(self.create_node)
1211 domain = yield deferToDatabase(self.create_domain)
1212- zone_serial = yield self.get_zone_serial_current()
1213+ yield self.capturePublication()
1214 dv = DeferredValue()
1215 listener = self.make_listener_without_delay()
1216 listener.register(
1217@@ -2715,16 +2784,19 @@
1218 "domain": domain,
1219 })
1220 yield dv.get(timeout=2)
1221- yield self.assertZoneSerialIncrement(zone_serial)
1222+ yield self.assertPublicationUpdated()
1223 finally:
1224 yield listener.stopService()
1225+ self.assertThat(
1226+ self.getCapturedPublication().source, Equals(
1227+ "Node %s: domain changed" % node.system_id))
1228
1229 @wait_for_reactor
1230 @inlineCallbacks
1231 def test_sends_message_for_node_delete(self):
1232 yield deferToDatabase(register_system_triggers)
1233 node = yield deferToDatabase(self.create_node)
1234- zone_serial = yield self.get_zone_serial_current()
1235+ yield self.capturePublication()
1236 dv = DeferredValue()
1237 listener = self.make_listener_without_delay()
1238 listener.register(
1239@@ -2733,9 +2805,12 @@
1240 try:
1241 yield deferToDatabase(self.delete_node, node.system_id)
1242 yield dv.get(timeout=2)
1243- yield self.assertZoneSerialIncrement(zone_serial)
1244+ yield self.assertPublicationUpdated()
1245 finally:
1246 yield listener.stopService()
1247+ self.assertThat(
1248+ self.getCapturedPublication().source,
1249+ Equals("Call to sys_dns_node_delete"))
1250
1251
1252 class TestDNSInterfaceListener(
1253@@ -2764,20 +2839,26 @@
1254 def test_sends_message_for_interface_update_name(self):
1255 yield deferToDatabase(register_system_triggers)
1256 interface = yield deferToDatabase(self.create_interface)
1257- zone_serial = yield self.get_zone_serial_current()
1258+ yield self.capturePublication()
1259 dv = DeferredValue()
1260 listener = self.make_listener_without_delay()
1261 listener.register(
1262 "sys_dns", lambda *args: dv.set(args))
1263 yield listener.startService()
1264 try:
1265+ name_old = interface.name
1266+ name_new = factory.make_name("name")
1267 yield deferToDatabase(self.update_interface, interface.id, {
1268- "name": factory.make_name("name"),
1269+ "name": name_new,
1270 })
1271 yield dv.get(timeout=2)
1272- yield self.assertZoneSerialIncrement(zone_serial)
1273+ yield self.assertPublicationUpdated()
1274 finally:
1275 yield listener.stopService()
1276+ self.assertThat(
1277+ self.getCapturedPublication().source, Equals(
1278+ "Interface %s: renamed from %s to %s"
1279+ % (name_new, name_old, name_new)))
1280
1281 @wait_for_reactor
1282 @inlineCallbacks
1283@@ -2785,7 +2866,7 @@
1284 yield deferToDatabase(register_system_triggers)
1285 interface = yield deferToDatabase(self.create_unknown_interface)
1286 node = yield deferToDatabase(self.create_node)
1287- zone_serial = yield self.get_zone_serial_current()
1288+ yield self.capturePublication()
1289 dv = DeferredValue()
1290 listener = self.make_listener_without_delay()
1291 listener.register(
1292@@ -2795,16 +2876,19 @@
1293 yield deferToDatabase(
1294 self.migrate_unknown_to_physical, interface.id, node)
1295 yield dv.get(timeout=2)
1296- yield self.assertZoneSerialIncrement(zone_serial)
1297+ yield self.assertPublicationUpdated()
1298 finally:
1299 yield listener.stopService()
1300+ self.assertThat(
1301+ self.getCapturedPublication().source, Equals(
1302+ "Interface %s: node set" % interface.name))
1303
1304 @wait_for_reactor
1305 @inlineCallbacks
1306 def test_sends_message_for_physical_to_unknown(self):
1307 yield deferToDatabase(register_system_triggers)
1308 interface = yield deferToDatabase(self.create_interface)
1309- zone_serial = yield self.get_zone_serial_current()
1310+ yield self.capturePublication()
1311 dv = DeferredValue()
1312 listener = self.make_listener_without_delay()
1313 listener.register(
1314@@ -2814,9 +2898,12 @@
1315 yield deferToDatabase(
1316 self.migrate_physical_to_unknown, interface.id)
1317 yield dv.get(timeout=2)
1318- yield self.assertZoneSerialIncrement(zone_serial)
1319+ yield self.assertPublicationUpdated()
1320 finally:
1321 yield listener.stopService()
1322+ self.assertThat(
1323+ self.getCapturedPublication().source, Equals(
1324+ "Interface %s: node unset" % interface.name))
1325
1326 @wait_for_reactor
1327 @inlineCallbacks
1328@@ -2824,7 +2911,7 @@
1329 yield deferToDatabase(register_system_triggers)
1330 interface = yield deferToDatabase(self.create_interface)
1331 node = yield deferToDatabase(self.create_node)
1332- zone_serial = yield self.get_zone_serial_current()
1333+ yield self.capturePublication()
1334 dv = DeferredValue()
1335 listener = self.make_listener_without_delay()
1336 listener.register(
1337@@ -2834,9 +2921,12 @@
1338 yield deferToDatabase(
1339 self.update_interface, interface.id, {"node": node})
1340 yield dv.get(timeout=2)
1341- yield self.assertZoneSerialIncrement(zone_serial)
1342+ yield self.assertPublicationUpdated()
1343 finally:
1344 yield listener.stopService()
1345+ self.assertThat(
1346+ self.getCapturedPublication().source, Equals(
1347+ "Interface %s: node changed" % interface.name))
1348
1349
1350 class TestDNSConfigListener(
1351@@ -2847,8 +2937,9 @@
1352 @wait_for_reactor
1353 @inlineCallbacks
1354 def test_sends_message_for_config_upstream_dns_insert(self):
1355+ upstream_dns_new = factory.make_ip_address()
1356 yield deferToDatabase(register_system_triggers)
1357- zone_serial = yield self.get_zone_serial_current()
1358+ yield self.capturePublication()
1359 dv = DeferredValue()
1360 listener = self.make_listener_without_delay()
1361 listener.register(
1362@@ -2857,17 +2948,22 @@
1363 try:
1364 yield deferToDatabase(
1365 Config.objects.set_config,
1366- "upstream_dns", factory.make_ip_address())
1367+ "upstream_dns", upstream_dns_new)
1368 yield dv.get(timeout=2)
1369- yield self.assertZoneSerialIncrement(zone_serial)
1370+ yield self.assertPublicationUpdated()
1371 finally:
1372 yield listener.stopService()
1373+ self.assertThat(
1374+ self.getCapturedPublication().source, Equals(
1375+ "Configuration upstream_dns set to %s"
1376+ % json.dumps(upstream_dns_new)))
1377
1378 @wait_for_reactor
1379 @inlineCallbacks
1380 def test_sends_message_for_config_default_dns_ttl_insert(self):
1381+ default_dns_ttl_new = random.randint(10, 1000)
1382 yield deferToDatabase(register_system_triggers)
1383- zone_serial = yield self.get_zone_serial_current()
1384+ yield self.capturePublication()
1385 dv = DeferredValue()
1386 listener = self.make_listener_without_delay()
1387 listener.register(
1388@@ -2876,17 +2972,22 @@
1389 try:
1390 yield deferToDatabase(
1391 Config.objects.set_config,
1392- "default_dns_ttl", random.randint(10, 1000))
1393+ "default_dns_ttl", default_dns_ttl_new)
1394 yield dv.get(timeout=2)
1395- yield self.assertZoneSerialIncrement(zone_serial)
1396+ yield self.assertPublicationUpdated()
1397 finally:
1398 yield listener.stopService()
1399+ self.assertThat(
1400+ self.getCapturedPublication().source, Equals(
1401+ "Configuration default_dns_ttl set to %s"
1402+ % json.dumps(default_dns_ttl_new)))
1403
1404 @wait_for_reactor
1405 @inlineCallbacks
1406 def test_sends_message_for_config_windows_kms_host_insert(self):
1407+ kms_host_new = factory.make_name("kms-host-new")
1408 yield deferToDatabase(register_system_triggers)
1409- zone_serial = yield self.get_zone_serial_current()
1410+ yield self.capturePublication()
1411 dv = DeferredValue()
1412 listener = self.make_listener_without_delay()
1413 listener.register(
1414@@ -2895,20 +2996,26 @@
1415 try:
1416 yield deferToDatabase(
1417 Config.objects.set_config,
1418- "windows_kms_host", factory.make_name("kms"))
1419+ "windows_kms_host", kms_host_new)
1420 yield dv.get(timeout=2)
1421- yield self.assertZoneSerialIncrement(zone_serial)
1422+ yield self.assertPublicationUpdated()
1423 finally:
1424 yield listener.stopService()
1425+ self.assertThat(
1426+ self.getCapturedPublication().source, Equals(
1427+ "Configuration windows_kms_host set to %s"
1428+ % json.dumps(kms_host_new)))
1429
1430 @wait_for_reactor
1431 @inlineCallbacks
1432 def test_sends_message_for_config_upstream_dns_update(self):
1433+ upstream_dns_old = factory.make_ip_address()
1434+ upstream_dns_new = factory.make_ip_address()
1435 yield deferToDatabase(register_system_triggers)
1436 yield deferToDatabase(
1437 Config.objects.set_config,
1438- "upstream_dns", factory.make_ip_address())
1439- zone_serial = yield self.get_zone_serial_current()
1440+ "upstream_dns", upstream_dns_old)
1441+ yield self.capturePublication()
1442 dv = DeferredValue()
1443 listener = self.make_listener_without_delay()
1444 listener.register(
1445@@ -2917,20 +3024,27 @@
1446 try:
1447 yield deferToDatabase(
1448 Config.objects.set_config,
1449- "upstream_dns", factory.make_ip_address())
1450+ "upstream_dns", upstream_dns_new)
1451 yield dv.get(timeout=2)
1452- yield self.assertZoneSerialIncrement(zone_serial)
1453+ yield self.assertPublicationUpdated()
1454 finally:
1455 yield listener.stopService()
1456+ self.assertThat(
1457+ self.getCapturedPublication().source, Equals(
1458+ "Configuration upstream_dns changed from %s to %s"
1459+ % (json.dumps(upstream_dns_old),
1460+ json.dumps(upstream_dns_new))))
1461
1462 @wait_for_reactor
1463 @inlineCallbacks
1464 def test_sends_message_for_config_default_dns_ttl_update(self):
1465+ default_dns_ttl_old = random.randint(10, 1000)
1466+ default_dns_ttl_new = random.randint(10, 1000)
1467 yield deferToDatabase(register_system_triggers)
1468 yield deferToDatabase(
1469 Config.objects.set_config,
1470- "default_dns_ttl", random.randint(10, 1000))
1471- zone_serial = yield self.get_zone_serial_current()
1472+ "default_dns_ttl", default_dns_ttl_old)
1473+ yield self.capturePublication()
1474 dv = DeferredValue()
1475 listener = self.make_listener_without_delay()
1476 listener.register(
1477@@ -2939,20 +3053,27 @@
1478 try:
1479 yield deferToDatabase(
1480 Config.objects.set_config,
1481- "default_dns_ttl", random.randint(10, 1000))
1482+ "default_dns_ttl", default_dns_ttl_new)
1483 yield dv.get(timeout=2)
1484- yield self.assertZoneSerialIncrement(zone_serial)
1485+ yield self.assertPublicationUpdated()
1486 finally:
1487 yield listener.stopService()
1488+ self.assertThat(
1489+ self.getCapturedPublication().source, Equals(
1490+ "Configuration default_dns_ttl changed from %s to %s"
1491+ % (json.dumps(default_dns_ttl_old),
1492+ json.dumps(default_dns_ttl_new))))
1493
1494 @wait_for_reactor
1495 @inlineCallbacks
1496 def test_sends_message_for_config_windows_kms_host_update(self):
1497+ kms_host_old = factory.make_name("kms-host-old")
1498+ kms_host_new = factory.make_name("kms-host-new")
1499 yield deferToDatabase(register_system_triggers)
1500 yield deferToDatabase(
1501 Config.objects.set_config,
1502- "windows_kms_host", factory.make_name("kms"))
1503- zone_serial = yield self.get_zone_serial_current()
1504+ "windows_kms_host", kms_host_old)
1505+ yield self.capturePublication()
1506 dv = DeferredValue()
1507 listener = self.make_listener_without_delay()
1508 listener.register(
1509@@ -2961,11 +3082,15 @@
1510 try:
1511 yield deferToDatabase(
1512 Config.objects.set_config,
1513- "windows_kms_host", factory.make_name("kms"))
1514+ "windows_kms_host", kms_host_new)
1515 yield dv.get(timeout=2)
1516- yield self.assertZoneSerialIncrement(zone_serial)
1517+ yield self.assertPublicationUpdated()
1518 finally:
1519 yield listener.stopService()
1520+ self.assertThat(
1521+ self.getCapturedPublication().source, Equals(
1522+ "Configuration windows_kms_host changed from %s to %s"
1523+ % (json.dumps(kms_host_old), json.dumps(kms_host_new))))
1524
1525
1526 class TestProxySubnetListener(
1527
1528=== modified file 'src/maasserver/triggers/tests/test_websocket_listener.py'
1529--- src/maasserver/triggers/tests/test_websocket_listener.py 2016-04-11 16:23:26 +0000
1530+++ src/maasserver/triggers/tests/test_websocket_listener.py 2016-04-29 11:37:21 +0000
1531@@ -20,7 +20,7 @@
1532 from maasserver.models.partition import MIN_PARTITION_SIZE
1533 from maasserver.testing.factory import factory
1534 from maasserver.testing.testcase import MAASTransactionServerTestCase
1535-from maasserver.triggers.tests.helper import TransactionalHelpersMixin
1536+from maasserver.triggers.testing import TransactionalHelpersMixin
1537 from maasserver.triggers.websocket import register_websocket_triggers
1538 from maasserver.utils.threads import deferToDatabase
1539 from provisioningserver.utils.twisted import (