Merge lp:~allenap/maas/dns-serials--bug-1571645 into lp:~maas-committers/maas/trunk
- dns-serials--bug-1571645
- Merge into trunk
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 | ||||
Related bugs: |
|
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.
Description of the change
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.
Gavin Panella (allenap) wrote : | # |
LaMont, do you mind sanity checking this?
LaMont Jones (lamont) wrote : | # |
A minor test question or two, but looks reasonable to me. No objections here.
Gavin Panella (allenap) wrote : | # |
Thanks LaMont!
Preview Diff
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 ( |
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.