Merge ~cgrabowski/maas:update_dns_via_nsupdate into maas:master

Proposed by Christian Grabowski
Status: Superseded
Proposed branch: ~cgrabowski/maas:update_dns_via_nsupdate
Merge into: maas:master
Diff against target: 2496 lines (+1623/-87)
22 files modified
debian/maas-region-api.dirs (+1/-0)
debian/maas-region-api.postinst (+5/-0)
src/maasserver/dns/config.py (+143/-10)
src/maasserver/dns/tests/test_config.py (+149/-1)
src/maasserver/dns/zonegenerator.py (+55/-2)
src/maasserver/region_controller.py (+34/-2)
src/maasserver/region_script.py (+1/-0)
src/maasserver/tests/test_region_controller.py (+22/-10)
src/maasserver/triggers/system.py (+260/-0)
src/maasserver/triggers/testing.py (+43/-2)
src/maasserver/triggers/tests/test_init.py (+13/-0)
src/maasserver/triggers/tests/test_system.py (+290/-1)
src/provisioningserver/dns/actions.py (+52/-1)
src/provisioningserver/dns/commands/setup_dns.py (+4/-0)
src/provisioningserver/dns/commands/tests/test_setup_dns.py (+6/-1)
src/provisioningserver/dns/config.py (+95/-2)
src/provisioningserver/dns/testing.py (+10/-0)
src/provisioningserver/dns/tests/test_actions.py (+121/-3)
src/provisioningserver/dns/tests/test_config.py (+74/-1)
src/provisioningserver/dns/tests/test_zoneconfig.py (+165/-17)
src/provisioningserver/dns/zoneconfig.py (+76/-34)
src/provisioningserver/templates/dns/named.conf.template (+4/-0)
Reviewer Review Type Date Requested Status
MAAS Lander Approve
Alexsander de Souza Approve
Review via email: mp+429199@code.launchpad.net

This proposal has been superseded by a proposal from 2022-11-15.

Commit message

cleanup dns updates triggers

add nsupdate key

add tsig to packaging

use nsupdate to send bulk commands

queue up diffs of dns changes and process them
queue up DNS updates

To post a comment you must log in.
Revision history for this message
MAAS Lander (maas-lander) wrote :

UNIT TESTS
-b update_dns_via_nsupdate lp:~cgrabowski/maas/+git/maas into -b master lp:~maas-committers/maas

STATUS: FAILED
LOG: http://maas-ci.internal:8080/job/maas-tester/468/consoleText
COMMIT: c8bd1525bae3aff0a8d360bddbfdf0146e80077c

review: Needs Fixing
Revision history for this message
Jack Lloyd-Walters (lloydwaltersj) wrote :

some minor questions

Revision history for this message
MAAS Lander (maas-lander) wrote :

UNIT TESTS
-b update_dns_via_nsupdate lp:~cgrabowski/maas/+git/maas into -b master lp:~maas-committers/maas

STATUS: FAILED
LOG: http://maas-ci.internal:8080/job/maas-tester/482/consoleText
COMMIT: a5b053f45432fbc744aea674be38d2c31f9ad258

review: Needs Fixing
Revision history for this message
MAAS Lander (maas-lander) wrote :

UNIT TESTS
-b update_dns_via_nsupdate lp:~cgrabowski/maas/+git/maas into -b master lp:~maas-committers/maas

STATUS: FAILED
LOG: http://maas-ci.internal:8080/job/maas-tester/488/consoleText
COMMIT: 4b2b6ef4d224bea21a59050d17e084319f6fc3e0

review: Needs Fixing
Revision history for this message
Christian Grabowski (cgrabowski) :
f1ffb8a... by Christian Grabowski

allow ttls of 0 in dynamic updates

Revision history for this message
MAAS Lander (maas-lander) wrote :

UNIT TESTS
-b update_dns_via_nsupdate lp:~cgrabowski/maas/+git/maas into -b master lp:~maas-committers/maas

STATUS: FAILED
LOG: http://maas-ci.internal:8080/job/maas-tester/489/consoleText
COMMIT: 9d290212c760d6c33ea320aff61c9dc8277fe86e

review: Needs Fixing
Revision history for this message
MAAS Lander (maas-lander) wrote :

UNIT TESTS
-b update_dns_via_nsupdate lp:~cgrabowski/maas/+git/maas into -b master lp:~maas-committers/maas

STATUS: FAILED
LOG: http://maas-ci.internal:8080/job/maas-tester/490/consoleText
COMMIT: 425c4d2f3382e79c4b1721d6cb6c5d8b18b20e50

review: Needs Fixing
417dd73... by Christian Grabowski

update set of triggers to account for dynamic updates

Revision history for this message
Alexsander de Souza (alexsander-souza) wrote :

a few questions inline

review: Needs Information
Revision history for this message
MAAS Lander (maas-lander) wrote :

UNIT TESTS
-b update_dns_via_nsupdate lp:~cgrabowski/maas/+git/maas into -b master lp:~maas-committers/maas

STATUS: SUCCESS
COMMIT: fcd948f51f72e398ab6fa1b162b4c3381f14fd29

review: Approve
Revision history for this message
Christian Grabowski (cgrabowski) :
8be3a3e... by Christian Grabowski

clean up full reload flag

Revision history for this message
Alexsander de Souza (alexsander-souza) wrote :

+1

review: Approve
Revision history for this message
MAAS Lander (maas-lander) wrote :

UNIT TESTS
-b update_dns_via_nsupdate lp:~cgrabowski/maas/+git/maas into -b master lp:~maas-committers/maas

STATUS: FAILED
LOG: http://maas-ci.internal:8080/job/maas-tester/493/consoleText
COMMIT: 8021acff300d749f36c9724cc923bc2e14ac2837

review: Needs Fixing
Revision history for this message
MAAS Lander (maas-lander) wrote :

UNIT TESTS
-b update_dns_via_nsupdate lp:~cgrabowski/maas/+git/maas into -b master lp:~maas-committers/maas

STATUS: FAILED
LOG: http://maas-ci.internal:8080/job/maas-tester/514/consoleText
COMMIT: 3a9b652d51a05cb8e83fa4536d33212ea9fb5161

review: Needs Fixing
8ef5019... by Christian Grabowski

separate out zone file dir from the rest of the dns config so bind can write to these paths as well

Revision history for this message
MAAS Lander (maas-lander) wrote :

UNIT TESTS
-b update_dns_via_nsupdate lp:~cgrabowski/maas/+git/maas into -b master lp:~maas-committers/maas

STATUS: FAILED
LOG: http://maas-ci.internal:8080/job/maas-tester/556/consoleText
COMMIT: 1defa9a5206bc2d223ed61597ec17fd836d6e11a

review: Needs Fixing
Revision history for this message
MAAS Lander (maas-lander) wrote :

UNIT TESTS
-b update_dns_via_nsupdate lp:~cgrabowski/maas/+git/maas into -b master lp:~maas-committers/maas

STATUS: FAILED
LOG: http://maas-ci.internal:8080/job/maas-tester/559/consoleText
COMMIT: 68c4616d4557b29c622eef4fe493e96691ee76a8

review: Needs Fixing
Revision history for this message
MAAS Lander (maas-lander) wrote :

UNIT TESTS
-b update_dns_via_nsupdate lp:~cgrabowski/maas/+git/maas into -b master lp:~maas-committers/maas

STATUS: FAILED
LOG: http://maas-ci.internal:8080/job/maas-tester/564/consoleText
COMMIT: 91834894c931d55620fd93e66e7a8903d5a0f63d

review: Needs Fixing
Revision history for this message
MAAS Lander (maas-lander) wrote :

UNIT TESTS
-b update_dns_via_nsupdate lp:~cgrabowski/maas/+git/maas into -b master lp:~maas-committers/maas

STATUS: FAILED
LOG: http://maas-ci.internal:8080/job/maas-tester/565/consoleText
COMMIT: f5f85a4eac33db34c8553e8ea6362bbb7a8c5d9a

review: Needs Fixing
Revision history for this message
MAAS Lander (maas-lander) wrote :

UNIT TESTS
-b update_dns_via_nsupdate lp:~cgrabowski/maas/+git/maas into -b master lp:~maas-committers/maas

STATUS: FAILED
LOG: http://maas-ci.internal:8080/job/maas-tester/566/consoleText
COMMIT: 64368c19977fd2b14d4a50d02cd002aba07dee7d

review: Needs Fixing
Revision history for this message
MAAS Lander (maas-lander) wrote :

UNIT TESTS
-b update_dns_via_nsupdate lp:~cgrabowski/maas/+git/maas into -b master lp:~maas-committers/maas

STATUS: FAILED
LOG: http://maas-ci.internal:8080/job/maas-tester/575/consoleText
COMMIT: 405b6868c73076743b0baa8b48bc7b332170279c

review: Needs Fixing
2021bea... by Christian Grabowski

add missing patch calls for tests

96fafca... by Christian Grabowski

add helper to test notify triggers

Revision history for this message
Adam Collard (adam-collard) :
Revision history for this message
MAAS Lander (maas-lander) wrote :

UNIT TESTS
-b update_dns_via_nsupdate lp:~cgrabowski/maas/+git/maas into -b master lp:~maas-committers/maas

STATUS: FAILED
LOG: http://maas-ci.internal:8080/job/maas-tester/1154/consoleText
COMMIT: 556b628e5dd110dee9c4ad5561d2a944cfc4059c

review: Needs Fixing
e314a01... by Christian Grabowski

add all trigger testcase

Revision history for this message
MAAS Lander (maas-lander) wrote :

UNIT TESTS
-b update_dns_via_nsupdate lp:~cgrabowski/maas/+git/maas into -b master lp:~maas-committers/maas

STATUS: FAILED
LOG: http://maas-ci.internal:8080/job/maas-tester/1193/consoleText
COMMIT: 09b92908a47c3ccb9c7210424fe31963078dcadf

review: Needs Fixing
Revision history for this message
MAAS Lander (maas-lander) wrote :

UNIT TESTS
-b update_dns_via_nsupdate lp:~cgrabowski/maas/+git/maas into -b master lp:~maas-committers/maas

STATUS: FAILED
LOG: http://maas-ci.internal:8080/job/maas-tester/1204/consoleText
COMMIT: e592b85a0b8c74f506d2e1ec448f22a3857fc9f7

review: Needs Fixing
Revision history for this message
MAAS Lander (maas-lander) wrote :

UNIT TESTS
-b update_dns_via_nsupdate lp:~cgrabowski/maas/+git/maas into -b master lp:~maas-committers/maas

STATUS: FAILED
LOG: http://maas-ci.internal:8080/job/maas-tester/1278/consoleText
COMMIT: 210616767ee57baf8976183f0bfeebbd6ec9deca

review: Needs Fixing
Revision history for this message
MAAS Lander (maas-lander) wrote :

UNIT TESTS
-b update_dns_via_nsupdate lp:~cgrabowski/maas/+git/maas into -b master lp:~maas-committers/maas

STATUS: FAILED
LOG: http://maas-ci.internal:8080/job/maas-tester/1281/consoleText
COMMIT: 85d59ce7f102b1b242847a2c171adfa2a2bb3c97

review: Needs Fixing
Revision history for this message
MAAS Lander (maas-lander) wrote :

UNIT TESTS
-b update_dns_via_nsupdate lp:~cgrabowski/maas/+git/maas into -b master lp:~maas-committers/maas

STATUS: FAILED
LOG: http://maas-ci.internal:8080/job/maas-tester/1282/consoleText
COMMIT: d479aede2dd0692029202e33f03325968905426b

review: Needs Fixing
Revision history for this message
MAAS Lander (maas-lander) wrote :

UNIT TESTS
-b update_dns_via_nsupdate lp:~cgrabowski/maas/+git/maas into -b master lp:~maas-committers/maas

STATUS: FAILED
LOG: http://maas-ci.internal:8080/job/maas-tester/1340/consoleText
COMMIT: e314a01b5cc04452f6783007a4a234037d9c2b45

review: Needs Fixing
4e44d9c... by Christian Grabowski

fix trigger registration tests

Revision history for this message
MAAS Lander (maas-lander) wrote :

UNIT TESTS
-b update_dns_via_nsupdate lp:~cgrabowski/maas/+git/maas into -b master lp:~maas-committers/maas

STATUS: FAILED
LOG: http://maas-ci.internal:8080/job/maas-tester/1342/consoleText
COMMIT: 668bf3a42b042fcb9019af9d1c443f299da571b4

review: Needs Fixing
Revision history for this message
MAAS Lander (maas-lander) wrote :

UNIT TESTS
-b update_dns_via_nsupdate lp:~cgrabowski/maas/+git/maas into -b master lp:~maas-committers/maas

STATUS: FAILED
LOG: http://maas-ci.internal:8080/job/maas-tester/1343/consoleText
COMMIT: a3bb7ab6ba6724d12d3e342a46bbbcefde041e51

review: Needs Fixing
Revision history for this message
MAAS Lander (maas-lander) wrote :

UNIT TESTS
-b update_dns_via_nsupdate lp:~cgrabowski/maas/+git/maas into -b master lp:~maas-committers/maas

STATUS: FAILED
LOG: http://maas-ci.internal:8080/job/maas-tester/1344/consoleText
COMMIT: 939d94111b35d11541eab813c15fd59b8f57fa22

review: Needs Fixing
Revision history for this message
MAAS Lander (maas-lander) wrote :

UNIT TESTS
-b update_dns_via_nsupdate lp:~cgrabowski/maas/+git/maas into -b master lp:~maas-committers/maas

STATUS: FAILED
LOG: http://maas-ci.internal:8080/job/maas-tester/1345/consoleText
COMMIT: 6fb564b57fe58a2c34ac80330e01dc7feec909de

review: Needs Fixing
Revision history for this message
MAAS Lander (maas-lander) wrote :

UNIT TESTS
-b update_dns_via_nsupdate lp:~cgrabowski/maas/+git/maas into -b master lp:~maas-committers/maas

STATUS: SUCCESS
COMMIT: 4e44d9c55fa32e19ccc7b937aa0b75522e8b25d6

review: Approve

Unmerged commits

4e44d9c... by Christian Grabowski

fix trigger registration tests

e314a01... by Christian Grabowski

add all trigger testcase

96fafca... by Christian Grabowski

add helper to test notify triggers

2021bea... by Christian Grabowski

add missing patch calls for tests

8ef5019... by Christian Grabowski

separate out zone file dir from the rest of the dns config so bind can write to these paths as well

8be3a3e... by Christian Grabowski

clean up full reload flag

417dd73... by Christian Grabowski

update set of triggers to account for dynamic updates

f1ffb8a... by Christian Grabowski

allow ttls of 0 in dynamic updates

af28863... by Christian Grabowski

add test for reverse dynamic update

e38c94b... by Christian Grabowski

cleanup dns updates triggers

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
diff --git a/debian/maas-region-api.dirs b/debian/maas-region-api.dirs
index d5ba327..1e65cfa 100644
--- a/debian/maas-region-api.dirs
+++ b/debian/maas-region-api.dirs
@@ -1,2 +1,3 @@
1etc/bind/maas1etc/bind/maas
2var/lib/bind/maas
2var/lib/maas/prometheus3var/lib/maas/prometheus
diff --git a/debian/maas-region-api.postinst b/debian/maas-region-api.postinst
index a1fb1a1..778504d 100644
--- a/debian/maas-region-api.postinst
+++ b/debian/maas-region-api.postinst
@@ -30,6 +30,11 @@ configure_libdir() {
30 if [ -f /var/lib/maas/maas_id ]; then30 if [ -f /var/lib/maas/maas_id ]; then
31 chown maas:maas /var/lib/maas/maas_id31 chown maas:maas /var/lib/maas/maas_id
32 fi32 fi
33
34 if [ -d /var/lib/maas/bind ]; then
35 chown maas:bind /var/lib/bind/maas
36 chmod 0775 /var/lib/bind/maas
37 fi
33}38}
3439
35edit_named_options() {40edit_named_options() {
diff --git a/src/maasserver/dns/config.py b/src/maasserver/dns/config.py
index 102e33d..97d0950 100644
--- a/src/maasserver/dns/config.py
+++ b/src/maasserver/dns/config.py
@@ -18,6 +18,7 @@ from maasserver.dns.zonegenerator import (
18from maasserver.enum import IPADDRESS_TYPE, RDNS_MODE18from maasserver.enum import IPADDRESS_TYPE, RDNS_MODE
19from maasserver.models.config import Config19from maasserver.models.config import Config
20from maasserver.models.dnspublication import DNSPublication20from maasserver.models.dnspublication import DNSPublication
21from maasserver.models.dnsresource import DNSResource
21from maasserver.models.domain import Domain22from maasserver.models.domain import Domain
22from maasserver.models.node import RackController23from maasserver.models.node import RackController
23from maasserver.models.subnet import Subnet24from maasserver.models.subnet import Subnet
@@ -28,7 +29,9 @@ from provisioningserver.dns.actions import (
28 bind_write_options,29 bind_write_options,
29 bind_write_zones,30 bind_write_zones,
30)31)
32from provisioningserver.dns.config import DynamicDNSUpdate
31from provisioningserver.logger import get_maas_logger33from provisioningserver.logger import get_maas_logger
34from provisioningserver.utils.shell import ExternalProcessError
3235
33maaslog = get_maas_logger("dns")36maaslog = get_maas_logger("dns")
3437
@@ -61,7 +64,12 @@ def forward_domains_to_forwarded_zones(forward_domains):
61 ]64 ]
6265
6366
64def dns_update_all_zones(reload_retry=False, reload_timeout=2):67def dns_update_all_zones(
68 reload_retry=False,
69 reload_timeout=2,
70 dynamic_updates=None,
71 requires_reload=False,
72):
65 """Update all zone files for all domains.73 """Update all zone files for all domains.
6674
67 Serving these zone files means updating BIND's configuration to include75 Serving these zone files means updating BIND's configuration to include
@@ -70,10 +78,23 @@ def dns_update_all_zones(reload_retry=False, reload_timeout=2):
70 :param reload_retry: Should the DNS server reload be retried in case78 :param reload_retry: Should the DNS server reload be retried in case
71 of failure? Defaults to `False`.79 of failure? Defaults to `False`.
72 :type reload_retry: bool80 :type reload_retry: bool
81
82 :param reload_timeout: How many seconds to wait for BIND's reload to succeed
83 :type reload_timeout: int
84
85 :param dynamic_updates: A list of updates to send via nsupdate to BIND
86 :type dynamic_updates: list[DynamicDNSUpdate]
87
88 :param requires_reload: If true, dynamic updates are ignored and a full reload will occur
89 :type requires_reload: bool
73 """90 """
74 if not is_dns_enabled():91 if not is_dns_enabled():
75 return92 return
7693
94 if not dynamic_updates:
95 dynamic_updates = []
96
97 reloaded = True
77 domains = Domain.objects.filter(authoritative=True)98 domains = Domain.objects.filter(authoritative=True)
78 forwarded_zones = forward_domains_to_forwarded_zones(99 forwarded_zones = forward_domains_to_forwarded_zones(
79 Domain.objects.get_forward_domains()100 Domain.objects.get_forward_domains()
@@ -87,8 +108,13 @@ def dns_update_all_zones(reload_retry=False, reload_timeout=2):
87 default_ttl,108 default_ttl,
88 serial,109 serial,
89 internal_domains=[get_internal_domain()],110 internal_domains=[get_internal_domain()],
111 dynamic_updates=dynamic_updates,
112 force_config_write=requires_reload,
90 ).as_list()113 ).as_list()
91 bind_write_zones(zones)114 try:
115 bind_write_zones(zones)
116 except ExternalProcessError: # dynamic update failed
117 reloaded = False
92118
93 # We should not be calling bind_write_options() here; call-sites should be119 # We should not be calling bind_write_options() here; call-sites should be
94 # making a separate call. It's a historical legacy, where many sites now120 # making a separate call. It's a historical legacy, where many sites now
@@ -110,14 +136,21 @@ def dns_update_all_zones(reload_retry=False, reload_timeout=2):
110 forwarded_zones=forwarded_zones,136 forwarded_zones=forwarded_zones,
111 )137 )
112138
113 # Reloading with retries may be a legacy from Celery days, or it may be139 if not requires_reload:
114 # necessary to recover from races during start-up. We're not sure if it is140 for zone in zones:
115 # actually needed but it seems safer to maintain this behaviour until we141 if zone.requires_reload:
116 # have a better understanding.142 requires_reload = True
117 if reload_retry:143 break
118 reloaded = bind_reload_with_retries(timeout=reload_timeout)144
119 else:145 if requires_reload:
120 reloaded = bind_reload(timeout=reload_timeout)146 # Reloading with retries may be a legacy from Celery days, or it may be
147 # necessary to recover from races during start-up. We're not sure if it is
148 # actually needed but it seems safer to maintain this behaviour until we
149 # have a better understanding.
150 if reload_retry:
151 reloaded = bind_reload_with_retries(timeout=reload_timeout)
152 else:
153 reloaded = bind_reload(timeout=reload_timeout)
121154
122 # Return the current serial and list of domain names.155 # Return the current serial and list of domain names.
123 return serial, reloaded, [domain.name for domain in domains]156 return serial, reloaded, [domain.name for domain in domains]
@@ -251,3 +284,103 @@ def get_internal_domain():
251 ttl=15,284 ttl=15,
252 resources=resources,285 resources=resources,
253 )286 )
287
288
289def process_dns_update_notify(message):
290 updates = []
291 update_list = message.split(" ")
292 op = update_list[0]
293 if op == "RELOAD":
294 return (updates, True)
295 zone = update_list[1]
296 name = f"{update_list[2]}.{zone}"
297 rectype = update_list[3]
298 if op == "UPDATE":
299 updates.append(
300 DynamicDNSUpdate.create_from_trigger(
301 operation="DELETE",
302 zone=zone,
303 name=name,
304 rectype=rectype,
305 answer=update_list[-1],
306 )
307 )
308 updates.append(
309 DynamicDNSUpdate.create_from_trigger(
310 operation="INSERT",
311 zone=zone,
312 name=name,
313 rectype=rectype,
314 ttl=int(update_list[-2]) if update_list[-2] else None,
315 answer=update_list[-1],
316 )
317 )
318 elif op == "INSERT":
319 updates.append(
320 DynamicDNSUpdate.create_from_trigger(
321 operation=op,
322 zone=zone,
323 name=name,
324 rectype=rectype,
325 ttl=int(update_list[-2]) if update_list[-2] else None,
326 answer=update_list[-1],
327 )
328 )
329 else:
330 # special case where we know an IP has been deleted but, we can't fetch the value
331 # and the rrecord may still have other answers
332 if op == "DELETE-IP":
333 updates.append(
334 DynamicDNSUpdate.create_from_trigger(
335 operation="DELETE",
336 zone=zone,
337 name=name,
338 rectype=rectype,
339 )
340 )
341 if rectype == "A":
342 updates.append(
343 DynamicDNSUpdate.create_from_trigger(
344 operation="DELETE",
345 zone=zone,
346 name=name,
347 rectype="AAAA",
348 )
349 )
350 resource = DNSResource.objects.get(
351 name=update_list[2], domain__name=zone
352 )
353 updates += [
354 DynamicDNSUpdate.create_from_trigger(
355 operation="INSERT",
356 zone=zone,
357 name=name,
358 rectype=rectype,
359 ttl=int(resource.address_ttl)
360 if resource.address_ttl
361 else None,
362 answer=ip.ip,
363 )
364 for ip in resource.ip_addresses.all()
365 ]
366
367 elif len(update_list) > 4: # has an answer
368 updates.append(
369 DynamicDNSUpdate.create_from_trigger(
370 operation=op,
371 zone=zone,
372 name=name,
373 rectype=rectype,
374 answer=update_list[-1],
375 )
376 )
377 else:
378 updates.append(
379 DynamicDNSUpdate.create_from_trigger(
380 operation=op,
381 zone=zone,
382 name=name,
383 rectype=rectype,
384 )
385 )
386 return (updates, False)
diff --git a/src/maasserver/dns/tests/test_config.py b/src/maasserver/dns/tests/test_config.py
index 1b2f15a..913c96e 100644
--- a/src/maasserver/dns/tests/test_config.py
+++ b/src/maasserver/dns/tests/test_config.py
@@ -29,6 +29,7 @@ from maasserver.dns.config import (
29 get_trusted_acls,29 get_trusted_acls,
30 get_trusted_networks,30 get_trusted_networks,
31 get_upstream_dns,31 get_upstream_dns,
32 process_dns_update_notify,
32)33)
33from maasserver.dns.zonegenerator import InternalDomainResourseRecord34from maasserver.dns.zonegenerator import InternalDomainResourseRecord
34from maasserver.enum import IPADDRESS_TYPE, NODE_STATUS35from maasserver.enum import IPADDRESS_TYPE, NODE_STATUS
@@ -40,10 +41,15 @@ from maasserver.testing.factory import factory
40from maasserver.testing.testcase import MAASServerTestCase41from maasserver.testing.testcase import MAASServerTestCase
41from maastesting.matchers import MockCalledOnceWith42from maastesting.matchers import MockCalledOnceWith
42from provisioningserver.dns.commands import get_named_conf, setup_dns43from provisioningserver.dns.commands import get_named_conf, setup_dns
43from provisioningserver.dns.config import compose_config_path, DNSConfig44from provisioningserver.dns.config import (
45 compose_config_path,
46 DNSConfig,
47 DynamicDNSUpdate,
48)
44from provisioningserver.dns.testing import (49from provisioningserver.dns.testing import (
45 patch_dns_config_path,50 patch_dns_config_path,
46 patch_dns_rndc_port,51 patch_dns_rndc_port,
52 patch_zone_file_config_path,
47)53)
48from provisioningserver.testing.bindfixture import allocate_ports, BINDServer54from provisioningserver.testing.bindfixture import allocate_ports, BINDServer
49from provisioningserver.testing.tests.test_bindfixture import dig_call55from provisioningserver.testing.tests.test_bindfixture import dig_call
@@ -127,6 +133,7 @@ class TestDNSServer(MAASServerTestCase):
127 patch_dns_config_path(self, self.bind.config.homedir)133 patch_dns_config_path(self, self.bind.config.homedir)
128 # Use a random port for rndc.134 # Use a random port for rndc.
129 patch_dns_rndc_port(self, allocate_ports("localhost")[0])135 patch_dns_rndc_port(self, allocate_ports("localhost")[0])
136 patch_zone_file_config_path(self, config_dir=self.bind.config.homedir)
130 # This simulates what should happen when the package is137 # This simulates what should happen when the package is
131 # installed:138 # installed:
132 # Create MAAS-specific DNS configuration files.139 # Create MAAS-specific DNS configuration files.
@@ -431,6 +438,28 @@ class TestDNSConfigModifications(TestDNSServer):
431 ),438 ),
432 )439 )
433440
441 def test_dns_update_all_zones_does_not_reload_if_it_does_not_need_to(self):
442 self.patch(settings, "DNS_CONNECT", True)
443 domain = factory.make_Domain()
444 # These domains should not show up. Just to test we create them.
445 for _ in range(3):
446 factory.make_Domain(authoritative=False)
447 node, static = self.create_node_with_static_ip(domain=domain)
448 fake_serial = random.randint(1, 1000)
449 self.patch(
450 dns_config_module, "current_zone_serial"
451 ).return_value = fake_serial
452 reload_call = self.patch(dns_config_module, "bind_reload")
453 serial1, reloaded, _ = dns_update_all_zones(
454 reload_timeout=RELOAD_TIMEOUT
455 )
456 self.assertTrue(reloaded)
457 factory.make_DNSResource(domain=domain)
458 _, _, _ = dns_update_all_zones( # should be a dynamic update
459 reload_timeout=RELOAD_TIMEOUT
460 )
461 reload_call.assert_called_once()
462
434463
435class TestDNSDynamicIPAddresses(TestDNSServer):464class TestDNSDynamicIPAddresses(TestDNSServer):
436 """Allocated nodes with IP addresses in the dynamic range get a DNS465 """Allocated nodes with IP addresses in the dynamic range get a DNS
@@ -754,3 +783,122 @@ class TestGetResourceNameForSubnet(MAASServerTestCase):
754 def test_returns_valid(self):783 def test_returns_valid(self):
755 subnet = factory.make_Subnet(cidr=self.cidr)784 subnet = factory.make_Subnet(cidr=self.cidr)
756 self.assertEqual(self.result, get_resource_name_for_subnet(subnet))785 self.assertEqual(self.result, get_resource_name_for_subnet(subnet))
786
787
788class TestProcessDNSUpdateNotify(MAASServerTestCase):
789 def test_insert(self):
790 domain = factory.make_Domain()
791 resource = factory.make_DNSResource(domain=domain)
792 ip = resource.ip_addresses.first().ip
793 message = f"INSERT {domain.name} {resource.name} A {resource.address_ttl if resource.address_ttl else 60} {ip}"
794 result, _ = process_dns_update_notify(message)
795 self.assertCountEqual(
796 [
797 DynamicDNSUpdate(
798 operation="INSERT",
799 zone=domain.name,
800 name=f"{resource.name}.{domain.name}",
801 ttl=resource.address_ttl if resource.address_ttl else 60,
802 answer=ip,
803 rectype="A" if IPAddress(ip).version == 4 else "AAAA",
804 )
805 ],
806 result,
807 )
808
809 def test_delete_without_ip(self):
810 domain = factory.make_Domain()
811 resource = factory.make_DNSResource(domain=domain)
812 message = f"DELETE {domain.name} {resource.name} A"
813 result, _ = process_dns_update_notify(message)
814 self.assertCountEqual(
815 [
816 DynamicDNSUpdate(
817 operation="DELETE",
818 zone=domain.name,
819 name=f"{resource.name}.{domain.name}",
820 rectype="A",
821 )
822 ],
823 result,
824 )
825
826 def test_delete_with_ip(self):
827 domain = factory.make_Domain()
828 resource = factory.make_DNSResource(domain=domain)
829 ip = resource.ip_addresses.first().ip
830 message = f"DELETE {domain.name} {resource.name} A {ip}"
831 result, _ = process_dns_update_notify(message)
832 self.assertCountEqual(
833 [
834 DynamicDNSUpdate(
835 operation="DELETE",
836 zone=domain.name,
837 name=f"{resource.name}.{domain.name}",
838 answer=ip,
839 rectype="A" if IPAddress(ip).version == 4 else "AAAA",
840 )
841 ],
842 result,
843 )
844
845 def test_update(self):
846 domain = factory.make_Domain()
847 resource = factory.make_DNSResource(domain=domain)
848 ip = resource.ip_addresses.first().ip
849 message = f"UPDATE {domain.name} {resource.name} A {resource.address_ttl if resource.address_ttl else 60} {ip}"
850 result, _ = process_dns_update_notify(message)
851 self.assertCountEqual(
852 [
853 DynamicDNSUpdate(
854 operation="DELETE",
855 zone=domain.name,
856 name=f"{resource.name}.{domain.name}",
857 answer=ip,
858 rectype="A" if IPAddress(ip).version == 4 else "AAAA",
859 ),
860 DynamicDNSUpdate(
861 operation="INSERT",
862 zone=domain.name,
863 name=f"{resource.name}.{domain.name}",
864 ttl=resource.address_ttl if resource.address_ttl else 60,
865 answer=ip,
866 rectype="A" if IPAddress(ip).version == 4 else "AAAA",
867 ),
868 ],
869 result,
870 )
871
872 def test_delete_ip(self):
873 domain = factory.make_Domain()
874 resource = factory.make_DNSResource(domain=domain)
875 ip = resource.ip_addresses.first().ip
876 ip2 = factory.make_StaticIPAddress()
877 resource.ip_addresses.add(ip2)
878 message = f"DELETE-IP {domain.name} {resource.name} A {resource.address_ttl if resource.address_ttl else 60} {ip}"
879 resource.ip_addresses.first().delete()
880 result, _ = process_dns_update_notify(message)
881 self.assertCountEqual(
882 [
883 DynamicDNSUpdate(
884 operation="DELETE",
885 zone=domain.name,
886 name=f"{resource.name}.{domain.name}",
887 rectype="A",
888 ),
889 DynamicDNSUpdate(
890 operation="DELETE",
891 zone=domain.name,
892 name=f"{resource.name}.{domain.name}",
893 rectype="AAAA",
894 ),
895 DynamicDNSUpdate(
896 operation="INSERT",
897 zone=domain.name,
898 name=f"{resource.name}.{domain.name}",
899 rectype="A" if IPAddress(ip2.ip).version == 4 else "AAAA",
900 answer=ip2.ip,
901 ),
902 ],
903 result,
904 )
diff --git a/src/maasserver/dns/zonegenerator.py b/src/maasserver/dns/zonegenerator.py
index 0e4c872..ef31cfa 100644
--- a/src/maasserver/dns/zonegenerator.py
+++ b/src/maasserver/dns/zonegenerator.py
@@ -21,6 +21,7 @@ from maasserver.models.domain import Domain
21from maasserver.models.staticipaddress import StaticIPAddress21from maasserver.models.staticipaddress import StaticIPAddress
22from maasserver.models.subnet import Subnet22from maasserver.models.subnet import Subnet
23from maasserver.server_address import get_maas_facing_server_addresses23from maasserver.server_address import get_maas_facing_server_addresses
24from provisioningserver.dns.config import DynamicDNSUpdate
24from provisioningserver.dns.zoneconfig import (25from provisioningserver.dns.zoneconfig import (
25 DNSForwardZoneConfig,26 DNSForwardZoneConfig,
26 DNSReverseZoneConfig,27 DNSReverseZoneConfig,
@@ -200,6 +201,8 @@ class ZoneGenerator:
200 default_ttl=None,201 default_ttl=None,
201 serial=None,202 serial=None,
202 internal_domains=None,203 internal_domains=None,
204 dynamic_updates=None,
205 force_config_write=False,
203 ):206 ):
204 """207 """
205 :param serial: A serial number to reuse when creating zones in bulk.208 :param serial: A serial number to reuse when creating zones in bulk.
@@ -215,6 +218,10 @@ class ZoneGenerator:
215 self.internal_domains = internal_domains218 self.internal_domains = internal_domains
216 if self.internal_domains is None:219 if self.internal_domains is None:
217 self.internal_domains = []220 self.internal_domains = []
221 self._dynamic_updates = dynamic_updates
222 if self._dynamic_updates is None:
223 self._dynamic_updates = []
224 self.force_config_write = force_config_write # some data changed that nsupdate cannot update if true
218225
219 @staticmethod226 @staticmethod
220 def _get_mappings():227 def _get_mappings():
@@ -235,6 +242,8 @@ class ZoneGenerator:
235 rrset_mappings,242 rrset_mappings,
236 default_ttl,243 default_ttl,
237 internal_domains,244 internal_domains,
245 dynamic_updates,
246 force_config_write,
238 ):247 ):
239 """Generator of forward zones, collated by domain name."""248 """Generator of forward zones, collated by domain name."""
240 dns_ip_list = get_dns_server_addresses(filter_allowed_dns=False)249 dns_ip_list = get_dns_server_addresses(filter_allowed_dns=False)
@@ -290,6 +299,12 @@ class ZoneGenerator:
290 (ttl, "AAAA", dns_ip.format())299 (ttl, "AAAA", dns_ip.format())
291 )300 )
292301
302 domain_updates = [
303 update
304 for update in dynamic_updates
305 if update.zone == domain.name
306 ]
307
293 yield DNSForwardZoneConfig(308 yield DNSForwardZoneConfig(
294 domain.name,309 domain.name,
295 serial=serial,310 serial=serial,
@@ -301,6 +316,8 @@ class ZoneGenerator:
301 ns_host_name=ns_host_name,316 ns_host_name=ns_host_name,
302 other_mapping=other_mapping,317 other_mapping=other_mapping,
303 dynamic_ranges=dynamic_ranges,318 dynamic_ranges=dynamic_ranges,
319 dynamic_updates=domain_updates,
320 force_config_write=force_config_write,
304 )321 )
305322
306 # Create the forward zone config for the internal domains.323 # Create the forward zone config for the internal domains.
@@ -313,6 +330,13 @@ class ZoneGenerator:
313 resource_mapping.rrset.add(330 resource_mapping.rrset.add(
314 (internal_domain.ttl, record.rrtype, record.rrdata)331 (internal_domain.ttl, record.rrtype, record.rrdata)
315 )332 )
333
334 domain_updates = [
335 update
336 for update in dynamic_updates
337 if update.zone == internal_domain.name
338 ]
339
316 yield DNSForwardZoneConfig(340 yield DNSForwardZoneConfig(
317 internal_domain.name,341 internal_domain.name,
318 serial=serial,342 serial=serial,
@@ -324,11 +348,19 @@ class ZoneGenerator:
324 ns_host_name=ns_host_name,348 ns_host_name=ns_host_name,
325 other_mapping=other_mapping,349 other_mapping=other_mapping,
326 dynamic_ranges=[],350 dynamic_ranges=[],
351 dynamic_updates=domain_updates,
352 force_config_write=force_config_write,
327 )353 )
328354
329 @staticmethod355 @staticmethod
330 def _gen_reverse_zones(356 def _gen_reverse_zones(
331 subnets, serial, ns_host_name, mappings, default_ttl357 subnets,
358 serial,
359 ns_host_name,
360 mappings,
361 default_ttl,
362 dynamic_updates,
363 force_config_write,
332 ):364 ):
333 """Generator of reverse zones, sorted by network."""365 """Generator of reverse zones, sorted by network."""
334366
@@ -418,6 +450,15 @@ class ZoneGenerator:
418 del rfc2317_glue[network]450 del rfc2317_glue[network]
419 else:451 else:
420 glue = set()452 glue = set()
453
454 domain_updates = [
455 DynamicDNSUpdate.as_reverse_record_update(update, subnet)
456 for update in dynamic_updates
457 if update.answer
458 and update.answer_is_ip
459 and (IPAddress(update.answer) in IPNetwork(subnet.cidr))
460 ]
461
421 yield DNSReverseZoneConfig(462 yield DNSReverseZoneConfig(
422 ns_host_name,463 ns_host_name,
423 serial=serial,464 serial=serial,
@@ -430,6 +471,8 @@ class ZoneGenerator:
430 exclude={471 exclude={
431 IPNetwork(s.cidr) for s in subnets if s is not subnet472 IPNetwork(s.cidr) for s in subnets if s is not subnet
432 },473 },
474 dynamic_updates=domain_updates,
475 force_config_write=force_config_write,
433 )476 )
434 # Now provide any remaining rfc2317 glue networks.477 # Now provide any remaining rfc2317 glue networks.
435 for network, ranges in rfc2317_glue.items():478 for network, ranges in rfc2317_glue.items():
@@ -445,6 +488,8 @@ class ZoneGenerator:
445 for s in subnets488 for s in subnets
446 if network in IPNetwork(s.cidr)489 if network in IPNetwork(s.cidr)
447 },490 },
491 dynamic_updates=domain_updates,
492 force_config_write=force_config_write,
448 )493 )
449494
450 def __iter__(self):495 def __iter__(self):
@@ -470,9 +515,17 @@ class ZoneGenerator:
470 rrset_mappings,515 rrset_mappings,
471 default_ttl,516 default_ttl,
472 self.internal_domains,517 self.internal_domains,
518 self._dynamic_updates,
519 self.force_config_write,
473 ),520 ),
474 self._gen_reverse_zones(521 self._gen_reverse_zones(
475 self.subnets, serial, ns_host_name, mappings, default_ttl522 self.subnets,
523 serial,
524 ns_host_name,
525 mappings,
526 default_ttl,
527 self._dynamic_updates,
528 self.force_config_write,
476 ),529 ),
477 )530 )
478531
diff --git a/src/maasserver/region_controller.py b/src/maasserver/region_controller.py
index e795afb..7b19149 100644
--- a/src/maasserver/region_controller.py
+++ b/src/maasserver/region_controller.py
@@ -41,7 +41,10 @@ from twisted.internet.task import LoopingCall
41from twisted.names.client import Resolver41from twisted.names.client import Resolver
4242
43from maasserver import eventloop, locks43from maasserver import eventloop, locks
44from maasserver.dns.config import dns_update_all_zones44from maasserver.dns.config import (
45 dns_update_all_zones,
46 process_dns_update_notify,
47)
45from maasserver.macaroon_auth import get_auth_info48from maasserver.macaroon_auth import get_auth_info
46from maasserver.models.dnspublication import DNSPublication49from maasserver.models.dnspublication import DNSPublication
47from maasserver.models.rbacsync import RBAC_ACTION, RBACLastSync, RBACSync50from maasserver.models.rbacsync import RBAC_ACTION, RBACLastSync, RBACSync
@@ -94,6 +97,8 @@ class RegionControllerService(Service):
94 self.needsDNSUpdate = False97 self.needsDNSUpdate = False
95 self.needsProxyUpdate = False98 self.needsProxyUpdate = False
96 self.needsRBACUpdate = False99 self.needsRBACUpdate = False
100 self._dns_updates = []
101 self._dns_requires_full_reload = False
97 self.postgresListener = postgresListener102 self.postgresListener = postgresListener
98 self.dnsResolver = Resolver(103 self.dnsResolver = Resolver(
99 resolv=None,104 resolv=None,
@@ -110,6 +115,9 @@ class RegionControllerService(Service):
110 """Start listening for messages."""115 """Start listening for messages."""
111 super().startService()116 super().startService()
112 self.postgresListener.register("sys_dns", self.markDNSForUpdate)117 self.postgresListener.register("sys_dns", self.markDNSForUpdate)
118 self.postgresListener.register(
119 "sys_dns_updates", self.queueDynamicDNSUpdate
120 )
113 self.postgresListener.register("sys_proxy", self.markProxyForUpdate)121 self.postgresListener.register("sys_proxy", self.markProxyForUpdate)
114 self.postgresListener.register("sys_rbac", self.markRBACForUpdate)122 self.postgresListener.register("sys_rbac", self.markRBACForUpdate)
115 self.postgresListener.register(123 self.postgresListener.register(
@@ -164,6 +172,18 @@ class RegionControllerService(Service):
164 )172 )
165 eventloop.restart()173 eventloop.restart()
166174
175 def queueDynamicDNSUpdate(self, channel, message):
176 """
177 Called when the `sys_dns_update` message is received
178 and queues updates for existing domains
179 """
180 (new_updates, need_reload) = process_dns_update_notify(message)
181
182 self._dns_requires_full_reload = (
183 self._dns_requires_full_reload or need_reload
184 )
185 self._dns_updates += new_updates
186
167 def startProcessing(self):187 def startProcessing(self):
168 """Start the process looping call."""188 """Start the process looping call."""
169 if not self.processing.running:189 if not self.processing.running:
@@ -193,10 +213,22 @@ class RegionControllerService(Service):
193 if delay:213 if delay:
194 return pause(delay)214 return pause(delay)
195215
216 def _clear_dynamic_dns_updates(d):
217 self._dns_updates = []
218 self._dns_requires_full_reload = False
219 return d
220
196 defers = []221 defers = []
197 if self.needsDNSUpdate:222 if self.needsDNSUpdate:
198 self.needsDNSUpdate = False223 self.needsDNSUpdate = False
199 d = deferToDatabase(transactional(dns_update_all_zones))224 d = deferToDatabase(
225 transactional(
226 dns_update_all_zones,
227 ),
228 dynamic_updates=self._dns_updates,
229 requires_reload=self._dns_requires_full_reload,
230 )
231 d.addCallback(_clear_dynamic_dns_updates)
200 d.addCallback(self._checkSerial)232 d.addCallback(self._checkSerial)
201 d.addCallback(self._logDNSReload)233 d.addCallback(self._logDNSReload)
202 # Order here matters, first needsDNSUpdate is set then pass the234 # Order here matters, first needsDNSUpdate is set then pass the
diff --git a/src/maasserver/region_script.py b/src/maasserver/region_script.py
index 0c87c61..7894260 100644
--- a/src/maasserver/region_script.py
+++ b/src/maasserver/region_script.py
@@ -42,6 +42,7 @@ def run_django(is_snap, is_devenv):
42 "MAAS_DNS_CONFIG_DIR": os.path.join(snap_data, "bind"),42 "MAAS_DNS_CONFIG_DIR": os.path.join(snap_data, "bind"),
43 "MAAS_PROXY_CONFIG_DIR": os.path.join(snap_data, "proxy"),43 "MAAS_PROXY_CONFIG_DIR": os.path.join(snap_data, "proxy"),
44 "MAAS_SYSLOG_CONFIG_DIR": os.path.join(snap_data, "syslog"),44 "MAAS_SYSLOG_CONFIG_DIR": os.path.join(snap_data, "syslog"),
45 "MAAS_ZONE_FILE_CONFIG_DIR": os.path.join(snap_data, "bind"),
45 "MAAS_IMAGES_KEYRING_FILEPATH": (46 "MAAS_IMAGES_KEYRING_FILEPATH": (
46 "/snap/maas/current/usr/share/keyrings/"47 "/snap/maas/current/usr/share/keyrings/"
47 "ubuntu-cloudimage-keyring.gpg"48 "ubuntu-cloudimage-keyring.gpg"
diff --git a/src/maasserver/tests/test_region_controller.py b/src/maasserver/tests/test_region_controller.py
index 0995258..4f40354 100644
--- a/src/maasserver/tests/test_region_controller.py
+++ b/src/maasserver/tests/test_region_controller.py
@@ -69,6 +69,7 @@ class TestRegionControllerService(MAASServerTestCase):
69 listener.register,69 listener.register,
70 MockCallsMatch(70 MockCallsMatch(
71 call("sys_dns", service.markDNSForUpdate),71 call("sys_dns", service.markDNSForUpdate),
72 call("sys_dns_updates", service.queueDynamicDNSUpdate),
72 call("sys_proxy", service.markProxyForUpdate),73 call("sys_proxy", service.markProxyForUpdate),
73 call("sys_rbac", service.markRBACForUpdate),74 call("sys_rbac", service.markRBACForUpdate),
74 call("sys_vault_migration", service.restartRegion),75 call("sys_vault_migration", service.restartRegion),
@@ -216,7 +217,9 @@ class TestRegionControllerService(MAASServerTestCase):
216 mock_msg = self.patch(region_controller.log, "msg")217 mock_msg = self.patch(region_controller.log, "msg")
217 service.startProcessing()218 service.startProcessing()
218 yield service.processingDefer219 yield service.processingDefer
219 self.assertThat(mock_dns_update_all_zones, MockCalledOnceWith())220 mock_dns_update_all_zones.assert_called_once_with(
221 dynamic_updates=[], requires_reload=False
222 )
220 self.assertThat(mock_check_serial, MockCalledOnceWith(dns_result))223 self.assertThat(mock_check_serial, MockCalledOnceWith(dns_result))
221 self.assertThat(224 self.assertThat(
222 mock_msg,225 mock_msg,
@@ -258,7 +261,11 @@ class TestRegionControllerService(MAASServerTestCase):
258 service.startProcessing()261 service.startProcessing()
259 yield service.processingDefer262 yield service.processingDefer
260 self.assertThat(263 self.assertThat(
261 mock_dns_update_all_zones, MockCallsMatch(call(), call())264 mock_dns_update_all_zones,
265 MockCallsMatch(
266 call(dynamic_updates=[], requires_reload=False),
267 call(dynamic_updates=[], requires_reload=False),
268 ),
262 )269 )
263 self.assertThat(270 self.assertThat(
264 mock_check_serial,271 mock_check_serial,
@@ -313,7 +320,9 @@ class TestRegionControllerService(MAASServerTestCase):
313 mock_err = self.patch(region_controller.log, "err")320 mock_err = self.patch(region_controller.log, "err")
314 service.startProcessing()321 service.startProcessing()
315 yield service.processingDefer322 yield service.processingDefer
316 self.assertThat(mock_dns_update_all_zones, MockCalledOnceWith())323 mock_dns_update_all_zones.assert_called_once_with(
324 dynamic_updates=[], requires_reload=False
325 )
317 self.assertThat(326 self.assertThat(
318 mock_err, MockCalledOnceWith(ANY, "Failed configuring DNS.")327 mock_err, MockCalledOnceWith(ANY, "Failed configuring DNS.")
319 )328 )
@@ -400,12 +409,11 @@ class TestRegionControllerService(MAASServerTestCase):
400 mock_rbacSync.return_value = None409 mock_rbacSync.return_value = None
401 service.startProcessing()410 service.startProcessing()
402 yield service.processingDefer411 yield service.processingDefer
403 self.assertThat(mock_dns_update_all_zones, MockCalledOnceWith())412 mock_dns_update_all_zones.assert_called_once_with(
404 self.assertThat(mock_check_serial, MockCalledOnceWith(dns_result))413 dynamic_updates=[], requires_reload=False
405 self.assertThat(
406 mock_proxy_update_config, MockCalledOnceWith(reload_proxy=True)
407 )414 )
408 self.assertThat(mock_rbacSync, MockCalledOnceWith())415 mock_proxy_update_config.assert_called_once_with(reload_proxy=True)
416 mock_rbacSync.assert_called_once()
409417
410 def make_soa_result(self, serial):418 def make_soa_result(self, serial):
411 return RRHeader(419 return RRHeader(
@@ -626,7 +634,9 @@ class TestRegionControllerServiceTransactional(MAASTransactionServerTestCase):
626 mock_msg = self.patch(region_controller.log, "msg")634 mock_msg = self.patch(region_controller.log, "msg")
627 service.startProcessing()635 service.startProcessing()
628 yield service.processingDefer636 yield service.processingDefer
629 self.assertThat(mock_dns_update_all_zones, MockCalledOnceWith())637 mock_dns_update_all_zones.assert_called_once_with(
638 dynamic_updates=[], requires_reload=False
639 )
630 self.assertThat(mock_check_serial, MockCalledOnceWith(dns_result))640 self.assertThat(mock_check_serial, MockCalledOnceWith(dns_result))
631 self.assertThat(641 self.assertThat(
632 mock_msg,642 mock_msg,
@@ -670,7 +680,9 @@ class TestRegionControllerServiceTransactional(MAASTransactionServerTestCase):
670 " * %s" % publication.source680 " * %s" % publication.source
671 for publication in reversed(publications[1:])681 for publication in reversed(publications[1:])
672 )682 )
673 self.assertThat(mock_dns_update_all_zones, MockCalledOnceWith())683 mock_dns_update_all_zones.assert_called_once_with(
684 dynamic_updates=[], requires_reload=False
685 )
674 self.assertThat(mock_check_serial, MockCalledOnceWith(dns_result))686 self.assertThat(mock_check_serial, MockCalledOnceWith(dns_result))
675 self.assertThat(mock_msg, MockCalledOnceWith(expected_msg))687 self.assertThat(mock_msg, MockCalledOnceWith(expected_msg))
676688
diff --git a/src/maasserver/triggers/system.py b/src/maasserver/triggers/system.py
index 0cf4e90..e1a60f1 100644
--- a/src/maasserver/triggers/system.py
+++ b/src/maasserver/triggers/system.py
@@ -1901,6 +1901,179 @@ RBAC_RPOOL_DELETE = dedent(
1901)1901)
19021902
19031903
1904# handles dynamic updates when a dnsresource is created
1905# or a StaticIPAddres maps to a resource or was deleted
1906def render_dns_dynamic_update_dnsresource_ip_addresses_procedure(op):
1907 return dedent(
1908 f"""\
1909 CREATE OR REPLACE FUNCTION sys_dns_updates_dns_ip_{op}()
1910 RETURNS trigger as $$
1911 DECLARE
1912 ip_addr text;
1913 rname text;
1914 rdomain_id bigint;
1915 domain text;
1916 ttl integer;
1917 BEGIN
1918 IF TG_WHEN <> 'AFTER' THEN
1919 RAISE EXCEPTION '[%] - May only run as an AFTER trigger', TG_NAME;
1920 ELSIF (TG_LEVEL = 'STATEMENT') THEN
1921 RAISE EXCEPTION '[%] - Should not be used as a STATEMENT level trigger', TG_NAME;
1922 END IF;
1923 IF (TG_OP = 'INSERT' AND TG_LEVEL = 'ROW') THEN
1924 SELECT host(ip) INTO ip_addr FROM maasserver_staticipaddress WHERE id=NEW.staticipaddress_id;
1925 SELECT name, domain_id, COALESCE(address_ttl, 0) INTO rname, rdomain_id, ttl FROM maasserver_dnsresource WHERE id=NEW.dnsresource_id;
1926 SELECT name INTO domain FROM maasserver_domain WHERE id=rdomain_id;
1927 PERFORM pg_notify('sys_dns_updates', 'INSERT ' || domain || ' ' || rname || ' A ' || ttl || ' ' || ip_addr);
1928 ELSIF (TG_OP = 'DELETE' AND TG_LEVEl = 'ROW') THEN
1929 IF EXISTS(SELECT id FROM maasserver_dnsresource WHERE id=OLD.dnsresource_id) THEN
1930 IF EXISTS(SELECT id FROM maasserver_staticipaddress WHERE id=OLD.staticipaddress_id) THEN
1931 SELECT host(ip) INTO ip_addr FROM maasserver_staticipaddress WHERE id=OLD.staticipaddress_id;
1932 SELECT name, domain_id INTO rname, rdomain_id FROM maasserver_dnsresource WHERE id=OLD.dnsresource_id;
1933 SELECT name INTO domain FROM maasserver_domain WHERE id=rdomain_id;
1934 PERFORM pg_notify('sys_dns_updates', 'DELETE ' || domain || ' ' || rname || ' A ' || ip_addr);
1935 ELSE
1936 SELECT name, domain_id INTO rname, rdomain_id FROM maasserver_dnsresource WHERE id=NEW.dnsresource_id;
1937 SELECT name INTO domain FROM maasserver_domain WHERE id=rdomain_id;
1938 PERFORM pg_notify('sys_dns_updates', 'DELETE-IP ' || domain || ' ' || rname || ' A');
1939 PERFORM pg_notify('sys_dns_updates', 'DELETE-IP ' || domain || ' ' || rname || ' AAAA');
1940 END IF;
1941 END IF;
1942 END IF;
1943 RETURN NULL;
1944 END;
1945 $$ LANGUAGE plpgsql;
1946 """
1947 )
1948
1949
1950# handles when ttl or name is modified or resource is deleted,
1951# DNS_DYNAMIC_UPDATE_DNSRESOURCE_STATICIPADDRESS covers the case of insert
1952def render_dns_dynamic_update_dnsresource_procedure(op):
1953 return dedent(
1954 f"""\
1955 CREATE OR REPLACE FUNCTION sys_dns_updates_maasserver_dnsresource_{op}()
1956 RETURNS trigger as $$
1957 DECLARE
1958 ip_addr text;
1959 ips text[];
1960 domain text;
1961 BEGIN
1962 IF TG_WHEN <> 'AFTER' THEN
1963 RAISE EXCEPTION '[%] - May only run as an AFTER trigger', TG_NAME;
1964 ELSIF (TG_LEVEL = 'STATEMENT') THEN
1965 RAISE EXCEPTION '[%] - Should not be used as a STATEMENT level trigger', TG_NAME;
1966 END IF;
1967 PERFORM pg_notify('sys_dns_updates', TG_OP || ' ' || OLD.name || ' ' || NEW.name || ' ' || OLD.address_ttl || ' ' || NEW.address_ttl);
1968 IF (TG_OP = 'UPDATE' AND TG_LEVEL = 'ROW') THEN
1969 IF NEW IS DISTINCT FROM OLD THEN
1970 SELECT array_agg(host(ip)) INTO ips FROM maasserver_dnsresource_ip_addresses m
1971 INNER JOIN maasserver_staticipaddress ON maasserver_staticipaddress.id=m.staticipaddress_id
1972 WHERE dnsresource_id=NEW.id;
1973 SELECT name INTO domain FROM maasserver_domain WHERE id=NEW.domain_id;
1974 IF array_length(ips, 1) > 0 THEN
1975 FOREACH ip_addr IN ARRAY ips
1976 LOOP
1977 IF OLD.name <> NEW.name THEN
1978 PERFORM pg_notify('sys_dns_updates', 'DELETE ' || domain || ' ' || OLD.name || ' A ' || ip_addr);
1979 PERFORM pg_notify('sys_dns_updates', 'INSERT ' || domain || ' ' || NEW.name || ' A ' || NEW.address_ttl || ' ' || ip_addr);
1980 ELSE
1981 PERFORM pg_notify('sys_dns_updates', 'UPDATE ' || domain || ' ' || NEW.name || ' A ' || NEW.address_ttl || ' ' || ip_addr);
1982 END IF;
1983 END LOOP;
1984 END IF;
1985 ELSE
1986 RETURN NULL;
1987 END IF;
1988 ELSIF (TG_OP = 'DELETE' AND TG_LEVEL = 'ROW') THEN
1989 SELECT name INTO domain FROM maasserver_domain WHERE id=NEW.domain_id;
1990 PERFORM pg_notify('sys_dns_updates', 'DELETE ' || domain || ' ' || OLD.name || ' A');
1991 END IF;
1992 RETURN NULL;
1993 END;
1994 $$ LANGUAGE plpgsql;
1995 """
1996 )
1997
1998
1999def render_dns_dynamic_update_dnsdata_procedure(op):
2000 return dedent(
2001 f"""\
2002 CREATE OR REPLACE FUNCTION sys_dns_updates_maasserver_dnsdata_{op}()
2003 RETURNS trigger as $$
2004 DECLARE
2005 rname text;
2006 rdomain_id bigint;
2007 domain text;
2008 ttl int;
2009 BEGIN
2010 IF TG_WHEN <> 'AFTER' THEN
2011 RAISE EXCEPTION '[%] - May only run as an AFTER trigger', TG_NAME;
2012 ELSIF (TG_LEVEL = 'STATEMENT') THEN
2013 RAISE EXCEPTION '[%] - Should not be used as a STATEMENT level trigger', TG_NAME;
2014 END IF;
2015 IF (TG_OP = 'UPDATE' AND TG_LEVEL = 'ROW') THEN
2016 IF NEW IS DISTINCT FROM OLD THEN
2017 SELECT name, domain_id, COALESCE(address_ttl, 0) INTO rname, rdomain_id, ttl from maasserver_dnsresource WHERE id=NEW.dnsresource_id;
2018 SELECT name INTO domain FROM maasserver_domain WHERE id=rdomain_id;
2019 PERFORM pg_notify('sys_dns_updates', 'UPDATE ' || domain || ' ' || rname || ' ' || NEW.rrtype || ' ' || COALESCE(NEW.ttl, ttl) || ' ' || NEW.rrdata);
2020 ELSE
2021 RETURN NULL;
2022 END IF;
2023 ELSIF (TG_OP = 'INSERT' AND TG_LEVEL = 'ROW') THEN
2024 SELECT name, domain_id, COALESCE(address_ttl, 0) INTO rname, rdomain_id, ttl from maasserver_dnsresource WHERE id=NEW.dnsresource_id;
2025 SELECT name INTO domain FROM maasserver_domain WHERE id=rdomain_id;
2026 PERFORM pg_notify('sys_dns_updates', 'INSERT ' || domain || ' ' || rname || ' ' || NEW.rrtype || ' ' || COALESCE(NEW.ttl, ttl) || ' ' || NEW.rrdata);
2027 ELSIF (TG_OP = 'DELETE' AND TG_LEVEL = 'ROW') THEN
2028 SELECT name, domain_id INTO rname, rdomain_id from maasserver_dnsresource WHERE id=OLD.dnsresource_id;
2029 SELECT name INTO domain FROM maasserver_domain WHERE id=rdomain_id;
2030 PERFORM pg_notify('sys_dns_updates', 'DELETE ' || domain || ' ' || rname || ' ' || OLD.rrtype);
2031 END IF;
2032 RETURN NULL;
2033 END;
2034 $$ LANGUAGE plpgsql;
2035 """
2036 )
2037
2038
2039def render_dns_dynamic_update_domain_procedure(op):
2040 return dedent(
2041 f"""\
2042 CREATE OR REPLACE FUNCTION sys_dns_updates_maasserver_domain_{op}()
2043 RETURNS trigger as $$
2044 BEGIN
2045 IF TG_WHEN <> 'AFTER' THEN
2046 RAISE EXCEPTION '[%] - May only run as an AFTER trigger', TG_NAME;
2047 ELSIF (TG_LEVEL = 'STATEMENT') THEN
2048 RAISE EXCEPTION '[%] - Should not be used as a STATEMENT level trigger', TG_NAME;
2049 END IF;
2050 PERFORM pg_notify('sys_dns_updates', 'RELOAD');
2051 RETURN NULL;
2052 END;
2053 $$ LANGUAGE plpgsql;
2054 """
2055 )
2056
2057
2058def render_dns_dynamic_update_subnet_procedure(op):
2059 return dedent(
2060 f"""\
2061 CREATE OR REPLACE FUNCTION sys_dns_updates_maasserver_subnet_{op}()
2062 RETURNS trigger as $$
2063 BEGIN
2064 IF TG_WHEN <> 'AFTER' THEN
2065 RAISE EXCEPTION '[%] - May only run as an AFTER trigger', TG_NAME;
2066 ELSIF (TG_LEVEL = 'STATEMENT') THEN
2067 RAISE EXCEPTION '[%] - Should not be used as a STATEMENT level trigger', TG_NAME;
2068 END IF;
2069 PERFORM pg_notify('sys_dns_updates', 'RELOAD');
2070 RETURN NULL;
2071 END;
2072 $$ LANGUAGE plpgsql;
2073 """
2074 )
2075
2076
1904def render_sys_proxy_procedure(proc_name, on_delete=False):2077def render_sys_proxy_procedure(proc_name, on_delete=False):
1905 """Render a database procedure with name `proc_name` that notifies that a2078 """Render a database procedure with name `proc_name` that notifies that a
1906 proxy update is needed.2079 proxy update is needed.
@@ -2163,3 +2336,90 @@ def register_system_triggers():
2163 register_trigger(2336 register_trigger(
2164 "maasserver_resourcepool", "sys_rbac_rpool_delete", "delete"2337 "maasserver_resourcepool", "sys_rbac_rpool_delete", "delete"
2165 )2338 )
2339
2340 register_procedure(
2341 render_dns_dynamic_update_dnsresource_ip_addresses_procedure("insert")
2342 )
2343 register_trigger(
2344 "maasserver_dnsresource_ip_addresses",
2345 "sys_dns_updates_dns_ip_insert",
2346 "insert",
2347 )
2348 register_procedure(
2349 render_dns_dynamic_update_dnsresource_ip_addresses_procedure("delete")
2350 )
2351 register_trigger(
2352 "maasserver_dnsresource_ip_addresses",
2353 "sys_dns_updates_dns_ip_delete",
2354 "delete",
2355 )
2356 register_procedure(
2357 render_dns_dynamic_update_dnsresource_procedure("update")
2358 )
2359 register_trigger(
2360 "maasserver_dnsresource",
2361 "sys_dns_updates_maasserver_dnsresource_update",
2362 "update",
2363 )
2364 register_procedure(
2365 render_dns_dynamic_update_dnsresource_procedure("delete")
2366 )
2367 register_trigger(
2368 "maasserver_dnsresource",
2369 "sys_dns_updates_maasserver_dnsresource_delete",
2370 "delete",
2371 )
2372 register_procedure(render_dns_dynamic_update_dnsdata_procedure("insert"))
2373 register_trigger(
2374 "maasserver_dnsdata",
2375 "sys_dns_updates_maasserver_dnsdata_insert",
2376 "update",
2377 )
2378 register_procedure(render_dns_dynamic_update_dnsdata_procedure("update"))
2379 register_trigger(
2380 "maasserver_dnsdata",
2381 "sys_dns_updates_maasserver_dnsdata_update",
2382 "update",
2383 )
2384 register_procedure(render_dns_dynamic_update_dnsdata_procedure("delete"))
2385 register_trigger(
2386 "maasserver_dnsdata",
2387 "sys_dns_updates_maasserver_dnsdata_delete",
2388 "delete",
2389 )
2390 register_procedure(render_dns_dynamic_update_domain_procedure("insert"))
2391 register_trigger(
2392 "maasserver_domain",
2393 "sys_dns_updates_maasserver_domain_insert",
2394 "insert",
2395 )
2396 register_procedure(render_dns_dynamic_update_domain_procedure("update"))
2397 register_trigger(
2398 "maasserver_domain",
2399 "sys_dns_updates_maasserver_domain_update",
2400 "update",
2401 )
2402 register_procedure(render_dns_dynamic_update_domain_procedure("delete"))
2403 register_trigger(
2404 "maasserver_domain",
2405 "sys_dns_updates_maasserver_domain_delete",
2406 "delete",
2407 )
2408 register_procedure(render_dns_dynamic_update_subnet_procedure("insert"))
2409 register_trigger(
2410 "maasserver_subnet",
2411 "sys_dns_updates_maasserver_subnet_insert",
2412 "insert",
2413 )
2414 register_procedure(render_dns_dynamic_update_subnet_procedure("update"))
2415 register_trigger(
2416 "maasserver_subnet",
2417 "sys_dns_updates_maasserver_subnet_update",
2418 "update",
2419 )
2420 register_procedure(render_dns_dynamic_update_subnet_procedure("delete"))
2421 register_trigger(
2422 "maasserver_subnet",
2423 "sys_dns_updates_maasserver_subnet_delete",
2424 "delete",
2425 )
diff --git a/src/maasserver/triggers/testing.py b/src/maasserver/triggers/testing.py
index 538fbc4..551ef4d 100644
--- a/src/maasserver/triggers/testing.py
+++ b/src/maasserver/triggers/testing.py
@@ -4,11 +4,10 @@
4"""Helper class for all tests using the `PostgresListenerService` under4"""Helper class for all tests using the `PostgresListenerService` under
5`maasserver.triggers.tests`."""5`maasserver.triggers.tests`."""
66
7
8from django.contrib.auth.models import User7from django.contrib.auth.models import User
9from piston3.models import Token8from piston3.models import Token
10from testtools.matchers import GreaterThan, Is, Not9from testtools.matchers import GreaterThan, Is, Not
11from twisted.internet.defer import inlineCallbacks, returnValue10from twisted.internet.defer import DeferredQueue, inlineCallbacks, returnValue
1211
13from maasserver.enum import INTERFACE_TYPE, NODE_TYPE12from maasserver.enum import INTERFACE_TYPE, NODE_TYPE
14from maasserver.listener import PostgresListenerService13from maasserver.listener import PostgresListenerService
@@ -53,6 +52,7 @@ from maasserver.models.virtualblockdevice import VirtualBlockDevice
53from maasserver.models.vlan import VLAN52from maasserver.models.vlan import VLAN
54from maasserver.models.zone import Zone53from maasserver.models.zone import Zone
55from maasserver.testing.factory import factory, RANDOM54from maasserver.testing.factory import factory, RANDOM
55from maasserver.triggers import register_trigger
56from maasserver.utils.orm import reload_object, transactional56from maasserver.utils.orm import reload_object, transactional
57from maasserver.utils.threads import deferToDatabase57from maasserver.utils.threads import deferToDatabase
58from maastesting.crochet import wait_for58from maastesting.crochet import wait_for
@@ -942,3 +942,44 @@ class RBACHelpersMixin:
942 GreaterThan(old.id),942 GreaterThan(old.id),
943 "RBAC sync tracking has not been modified again.",943 "RBAC sync tracking has not been modified again.",
944 )944 )
945
946
947class NotifyHelperMixin:
948
949 channels = ()
950 channel_queues = {}
951 postgres_listener_service = None
952
953 @inlineCallbacks
954 def set_service(self, listener):
955 self.postgres_listener_service = listener
956 yield self.postgres_listener_service.startService()
957
958 def register_trigger(self, table, channel, ops=(), trigger=None):
959 if channel not in self.channels:
960 self.postgres_listener_service.registerChannel(channel)
961 self.postgres_listener_service.register(channel, self.listen)
962 self.channels = self.channels + (channel,)
963 for op in ops:
964 trigger = trigger or f"{channel}_{table}_{op}"
965 register_trigger(table, trigger, op)
966
967 @inlineCallbacks
968 def listen(self, channel, msg):
969 if msg:
970 yield self.channel_queues[channel].put(msg)
971
972 @inlineCallbacks
973 def get_notify(self, channel):
974 msg = yield self.channel_queues[channel].get()
975 return msg
976
977 def start_reading(self):
978 for channel in self.channels:
979 self.channel_queues[channel] = DeferredQueue()
980 self.postgres_listener_service.startReading()
981
982 def stop_reading(self):
983 for channel in self.channels:
984 del self.channel_queues[channel]
985 self.postgres_listener_service.stopReading()
diff --git a/src/maasserver/triggers/tests/test_init.py b/src/maasserver/triggers/tests/test_init.py
index 7b6e8ee..e4fb3ed 100644
--- a/src/maasserver/triggers/tests/test_init.py
+++ b/src/maasserver/triggers/tests/test_init.py
@@ -70,15 +70,25 @@ class TestTriggersUsed(MAASServerTestCase):
70 "dnsdata_sys_dns_dnsdata_delete",70 "dnsdata_sys_dns_dnsdata_delete",
71 "dnsdata_sys_dns_dnsdata_insert",71 "dnsdata_sys_dns_dnsdata_insert",
72 "dnsdata_sys_dns_dnsdata_update",72 "dnsdata_sys_dns_dnsdata_update",
73 "dnsdata_sys_dns_updates_maasserver_dnsdata_delete",
74 "dnsdata_sys_dns_updates_maasserver_dnsdata_insert",
75 "dnsdata_sys_dns_updates_maasserver_dnsdata_update",
73 "dnspublication_sys_dns_publish",76 "dnspublication_sys_dns_publish",
74 "dnsresource_ip_addresses_sys_dns_dnsresource_ip_link",77 "dnsresource_ip_addresses_sys_dns_dnsresource_ip_link",
75 "dnsresource_ip_addresses_sys_dns_dnsresource_ip_unlink",78 "dnsresource_ip_addresses_sys_dns_dnsresource_ip_unlink",
76 "dnsresource_sys_dns_dnsresource_delete",79 "dnsresource_sys_dns_dnsresource_delete",
77 "dnsresource_sys_dns_dnsresource_insert",80 "dnsresource_sys_dns_dnsresource_insert",
78 "dnsresource_sys_dns_dnsresource_update",81 "dnsresource_sys_dns_dnsresource_update",
82 "dnsresource_sys_dns_updates_maasserver_dnsresource_update",
83 "dnsresource_sys_dns_updates_maasserver_dnsresource_delete",
84 "dnsresource_ip_addresses_sys_dns_updates_dns_ip_insert",
85 "dnsresource_ip_addresses_sys_dns_updates_dns_ip_delete",
79 "domain_sys_dns_domain_delete",86 "domain_sys_dns_domain_delete",
80 "domain_sys_dns_domain_insert",87 "domain_sys_dns_domain_insert",
81 "domain_sys_dns_domain_update",88 "domain_sys_dns_domain_update",
89 "domain_sys_dns_updates_maasserver_domain_delete",
90 "domain_sys_dns_updates_maasserver_domain_insert",
91 "domain_sys_dns_updates_maasserver_domain_update",
82 "interface_ip_addresses_sys_dns_nic_ip_link",92 "interface_ip_addresses_sys_dns_nic_ip_link",
83 "interface_ip_addresses_sys_dns_nic_ip_unlink",93 "interface_ip_addresses_sys_dns_nic_ip_unlink",
84 "interface_sys_dhcp_interface_update",94 "interface_sys_dhcp_interface_update",
@@ -99,6 +109,9 @@ class TestTriggersUsed(MAASServerTestCase):
99 "staticipaddress_sys_dhcp_staticipaddress_insert",109 "staticipaddress_sys_dhcp_staticipaddress_insert",
100 "staticipaddress_sys_dhcp_staticipaddress_update",110 "staticipaddress_sys_dhcp_staticipaddress_update",
101 "staticipaddress_sys_dns_staticipaddress_update",111 "staticipaddress_sys_dns_staticipaddress_update",
112 "subnet_sys_dns_updates_maasserver_subnet_delete",
113 "subnet_sys_dns_updates_maasserver_subnet_insert",
114 "subnet_sys_dns_updates_maasserver_subnet_update",
102 "subnet_sys_dhcp_subnet_delete",115 "subnet_sys_dhcp_subnet_delete",
103 "subnet_sys_dhcp_subnet_update",116 "subnet_sys_dhcp_subnet_update",
104 "subnet_sys_dns_subnet_delete",117 "subnet_sys_dns_subnet_delete",
diff --git a/src/maasserver/triggers/tests/test_system.py b/src/maasserver/triggers/tests/test_system.py
index 7d646ae..f43b014 100644
--- a/src/maasserver/triggers/tests/test_system.py
+++ b/src/maasserver/triggers/tests/test_system.py
@@ -5,13 +5,26 @@
5from contextlib import closing5from contextlib import closing
66
7from django.db import connection7from django.db import connection
8from twisted.internet.defer import inlineCallbacks
89
9from maasserver.models.dnspublication import zone_serial10from maasserver.models.dnspublication import zone_serial
10from maasserver.testing.testcase import MAASServerTestCase11from maasserver.testing.factory import factory
12from maasserver.testing.testcase import (
13 MAASServerTestCase,
14 MAASTransactionServerTestCase,
15)
11from maasserver.triggers.system import register_system_triggers16from maasserver.triggers.system import register_system_triggers
17from maasserver.triggers.testing import (
18 NotifyHelperMixin,
19 TransactionalHelpersMixin,
20)
12from maasserver.utils.orm import psql_array21from maasserver.utils.orm import psql_array
22from maasserver.utils.threads import deferToDatabase
23from maastesting.crochet import wait_for
13from maastesting.matchers import MockCalledOnceWith24from maastesting.matchers import MockCalledOnceWith
1425
26wait_for_reactor = wait_for()
27
1528
16class TestTriggers(MAASServerTestCase):29class TestTriggers(MAASServerTestCase):
17 def test_register_system_triggers(self):30 def test_register_system_triggers(self):
@@ -88,3 +101,279 @@ class TestTriggers(MAASServerTestCase):
88 mock_create = self.patch(zone_serial, "create_if_not_exists")101 mock_create = self.patch(zone_serial, "create_if_not_exists")
89 register_system_triggers()102 register_system_triggers()
90 self.assertThat(mock_create, MockCalledOnceWith())103 self.assertThat(mock_create, MockCalledOnceWith())
104
105
106class TestSysDNSUpdates(
107 MAASTransactionServerTestCase, TransactionalHelpersMixin, NotifyHelperMixin
108):
109 @wait_for_reactor
110 @inlineCallbacks
111 def test_dns_dynamic_update_dnsresource_ip_addresses_insert(self):
112 listener = self.make_listener_without_delay()
113 yield self.set_service(listener)
114 domain = yield deferToDatabase(self.create_domain)
115 yield deferToDatabase(
116 self.register_trigger,
117 "maasserver_dnsresource_ip_addresses",
118 "sys_dns_updates",
119 ops=("insert",),
120 trigger="sys_dns_updates_dns_ip_insert",
121 )
122 self.start_reading()
123 try:
124 static_ip = yield deferToDatabase(self.create_staticipaddress)
125 rec = yield deferToDatabase(
126 self.create_dnsresource,
127 params={"domain": domain, "ip_addresses": [static_ip]},
128 )
129 yield self.get_notify(
130 "sys_dns_updates"
131 ) # ignore RELOAD from domain creation
132 msg = yield self.get_notify("sys_dns_updates")
133 self.assertEqual(
134 msg,
135 f"INSERT {domain.name} {rec.name} A {domain.ttl if domain.ttl else 0} {static_ip.ip}",
136 )
137 finally:
138 self.stop_reading()
139 yield self.postgres_listener_service.stopService()
140
141 @wait_for_reactor
142 @inlineCallbacks
143 def test_dns_dynamic_update_dnsresource_ip_addresses_delete(self):
144 listener = self.make_listener_without_delay()
145 yield self.set_service(listener)
146 domain = yield deferToDatabase(self.create_domain)
147 static_ip = yield deferToDatabase(self.create_staticipaddress)
148 rec = yield deferToDatabase(
149 self.create_dnsresource,
150 params={"domain": domain, "ip_addresses": [static_ip]},
151 )
152 yield deferToDatabase(
153 self.register_trigger,
154 "maasserver_dnsresource_ip_addresses",
155 "sys_dns_updates",
156 ops=("delete",),
157 trigger="sys_dns_updates_dns_ip_delete",
158 )
159 self.start_reading()
160 try:
161 yield deferToDatabase(rec.delete)
162 msg = yield self.get_notify("sys_dns_updates")
163 self.assertEqual(
164 msg, f"DELETE {domain.name} {rec.name} A {static_ip.ip}"
165 )
166 finally:
167 self.stop_reading()
168 yield self.postgres_listener_service.stopService()
169
170 @wait_for_reactor
171 @inlineCallbacks
172 def test_dns_dynamic_update_dnsresource_update(self):
173 listener = self.make_listener_without_delay()
174 yield self.set_service(listener)
175 domain = yield deferToDatabase(self.create_domain)
176 static_ip = yield deferToDatabase(self.create_staticipaddress)
177 rec = yield deferToDatabase(
178 self.create_dnsresource,
179 params={"domain": domain, "ip_addresses": [static_ip]},
180 )
181 yield deferToDatabase(
182 self.register_trigger,
183 "maasserver_dnsresource",
184 "sys_dns_updates",
185 ops=("update",),
186 )
187 self.start_reading()
188 try:
189 rec.address_ttl = 30
190 yield deferToDatabase(rec.save)
191 msg = yield self.get_notify("sys_dns_updates")
192 self.assertEqual(
193 msg, f"UPDATE {domain.name} {rec.name} A 30 {static_ip.ip}"
194 )
195 finally:
196 self.stop_reading()
197 yield self.postgres_listener_service.stopService()
198
199 @wait_for_reactor
200 @inlineCallbacks
201 def test_dns_dynamic_update_dnsresource_delete(self):
202 listener = self.make_listener_without_delay()
203 yield self.set_service(listener)
204 domain = yield deferToDatabase(self.create_domain)
205 static_ip = yield deferToDatabase(self.create_staticipaddress)
206 rec = yield deferToDatabase(
207 self.create_dnsresource,
208 params={"domain": domain, "ip_addresses": [static_ip]},
209 )
210 yield deferToDatabase(
211 self.register_trigger,
212 "maasserver_dnsresource",
213 "sys_dns_updates",
214 ops=("delete",),
215 )
216 self.start_reading()
217 try:
218 yield deferToDatabase(rec.delete)
219 msg = yield self.get_notify("sys_dns_updates")
220 self.assertEqual(
221 msg, f"DELETE {domain.name} {rec.name} A {static_ip.ip}"
222 )
223 finally:
224 self.stop_reading()
225 yield self.postgres_listener_service.stopService()
226
227 @wait_for_reactor
228 @inlineCallbacks
229 def test_dns_dynamic_update_dnsdata_update(self):
230 listener = self.make_listener_without_delay()
231 yield self.set_service(listener)
232 domain = yield deferToDatabase(self.create_domain)
233 rec = yield deferToDatabase(
234 self.create_dnsresource, params={"domain": domain}
235 )
236 yield deferToDatabase(
237 self.register_trigger,
238 "maasserver_dnsdata",
239 "sys_dns_updates",
240 ops=("update",),
241 )
242 self.start_reading()
243 try:
244 dnsdata = yield deferToDatabase(
245 self.create_dnsdata,
246 params={
247 "dnsresource": rec,
248 "rrtype": "TXT",
249 "rrdata": factory.make_name(),
250 },
251 )
252 dnsdata.rrdata = factory.make_name()
253 yield deferToDatabase(dnsdata.save)
254 msg = yield self.get_notify("sys_dns_updates")
255 expected_ttl = 0
256 if dnsdata.ttl:
257 expected_ttl = dnsdata.ttl
258 elif rec.address_ttl:
259 expected_ttl = rec.address_ttl
260 self.assertEqual(
261 msg,
262 f"UPDATE {domain.name} {rec.name} {dnsdata.rrtype} {expected_ttl} {dnsdata.rrdata}",
263 )
264 finally:
265 self.stop_reading()
266 yield self.postgres_listener_service.stopService()
267
268 @wait_for_reactor
269 @inlineCallbacks
270 def test_dns_dynamic_update_dnsdata_insert(self):
271 listener = self.make_listener_without_delay()
272 yield self.set_service(listener)
273 domain = yield deferToDatabase(self.create_domain)
274 rec = yield deferToDatabase(
275 self.create_dnsresource, {"domain": domain}
276 )
277 yield deferToDatabase(
278 self.register_trigger,
279 "maasserver_dnsdata",
280 "sys_dns_updates",
281 ops=("insert",),
282 )
283 self.start_reading()
284 try:
285 dnsdata = yield deferToDatabase(
286 self.create_dnsdata,
287 params={
288 "dnsresource": rec,
289 "rrtype": "TXT",
290 "rrdata": factory.make_name(),
291 },
292 )
293 msg = yield self.get_notify("sys_dns_updates")
294 expected_ttl = 0
295 if dnsdata.ttl:
296 expected_ttl = dnsdata.ttl
297 elif rec.address_ttl:
298 expected_ttl = rec.address_ttl
299 self.assertEqual(
300 msg,
301 f"INSERT {domain.name} {rec.name} {dnsdata.rrtype} {expected_ttl} {dnsdata.rrdata}",
302 )
303 finally:
304 self.stop_reading()
305 yield self.postgres_listener_service.stopService()
306
307 @wait_for_reactor
308 @inlineCallbacks
309 def test_dns_dynamic_update_dnsdata_delete(self):
310 listener = self.make_listener_without_delay()
311 yield self.set_service(listener)
312 domain = yield deferToDatabase(self.create_domain)
313 rec = yield deferToDatabase(
314 self.create_dnsresource, params={"domain": domain}
315 )
316 dnsdata = yield deferToDatabase(
317 self.create_dnsdata,
318 params={
319 "dnsresource": rec,
320 "rrtype": "TXT",
321 "rrdata": factory.make_name(),
322 },
323 )
324 yield deferToDatabase(
325 self.register_trigger,
326 "maasserver_dnsdata",
327 "sys_dns_updates",
328 ops=("delete",),
329 )
330 self.start_reading()
331 try:
332 yield deferToDatabase(dnsdata.delete)
333 msg = yield self.get_notify("sys_dns_updates")
334 self.assertEqual(
335 msg, f"DELETE {domain.name} {rec.name} {dnsdata.rrtype}"
336 )
337 finally:
338 self.stop_reading()
339 yield self.postgres_listener_service.stopService()
340
341 @wait_for_reactor
342 @inlineCallbacks
343 def test_dns_dynamic_update_domain_reload(self):
344 listener = self.make_listener_without_delay()
345 yield self.set_service(listener)
346 yield deferToDatabase(
347 self.register_trigger,
348 "maasserver_domain",
349 "sys_dns_updates",
350 ops=("insert",),
351 )
352 self.start_reading()
353 try:
354 yield deferToDatabase(self.create_domain)
355 msg = yield self.get_notify("sys_dns_updates")
356 self.assertEqual(msg, "RELOAD")
357 finally:
358 self.stop_reading()
359 yield self.postgres_listener_service.stopService()
360
361 @wait_for_reactor
362 @inlineCallbacks
363 def test_dns_dynamic_update_subnet_reload(self):
364 listener = self.make_listener_without_delay()
365 yield self.set_service(listener)
366 yield deferToDatabase(
367 self.register_trigger,
368 "maasserver_subnet",
369 "sys_dns_updates",
370 ops=("insert",),
371 )
372 self.start_reading()
373 try:
374 yield deferToDatabase(self.create_subnet)
375 msg = yield self.get_notify("sys_dns_updates")
376 self.assertEqual(msg, "RELOAD")
377 finally:
378 self.stop_reading()
379 yield self.postgres_listener_service.stopService()
diff --git a/src/provisioningserver/dns/actions.py b/src/provisioningserver/dns/actions.py
index 7b8b1ca..bafbac9 100644
--- a/src/provisioningserver/dns/actions.py
+++ b/src/provisioningserver/dns/actions.py
@@ -10,14 +10,18 @@ from time import sleep
10from provisioningserver.dns.config import (10from provisioningserver.dns.config import (
11 DNSConfig,11 DNSConfig,
12 execute_rndc_command,12 execute_rndc_command,
13 get_nsupdate_key_path,
13 set_up_options_conf,14 set_up_options_conf,
14)15)
15from provisioningserver.logger import get_maas_logger16from provisioningserver.logger import get_maas_logger
16from provisioningserver.utils.shell import ExternalProcessError17from provisioningserver.utils.shell import ExternalProcessError, run_command
1718
18maaslog = get_maas_logger("dns")19maaslog = get_maas_logger("dns")
1920
2021
22MAAS_NSUPDATE_HOST = "localhost"
23
24
21def bind_reconfigure():25def bind_reconfigure():
22 """Ask BIND to reload its configuration and *new* zone files.26 """Ask BIND to reload its configuration and *new* zone files.
2327
@@ -135,3 +139,50 @@ def bind_write_zones(zones):
135 """139 """
136 for zone in zones:140 for zone in zones:
137 zone.write_config()141 zone.write_config()
142
143
144class NSUpdateCommand:
145 executable = "nsupdate"
146
147 def __init__(self, zone, updates, **kwargs):
148 self._zone = zone
149 self._updates = updates
150 self._serial = kwargs.get("serial")
151 self._zone_ttl = kwargs["ttl"]
152
153 def _format_update(self, update):
154 if update.operation == "DELETE":
155 if update.answer:
156 return f"update delete {update.name} {update.rectype} {update.answer}"
157 return f"update delete {update.name} {update.rectype}"
158 ttl = update.ttl
159 if ttl is None:
160 ttl = self._zone_ttl
161 return (
162 f"update add {update.name} {ttl} {update.rectype} {update.answer}"
163 )
164
165 def update(self, server_address=MAAS_NSUPDATE_HOST):
166 stdin = [f"zone {self._zone}"] + [
167 self._format_update(update) for update in self._updates
168 ]
169 if server_address:
170 stdin = [f"server {server_address}"] + stdin
171
172 if self._serial:
173 stdin.append(
174 f"update add {self._zone} {self._zone_ttl} SOA {self._zone}. nobody.example.com. {self._serial} 600 1800 604800 {self._zone_ttl}"
175 )
176
177 stdin.append("send\n")
178
179 cmd = [self.executable, "-k", get_nsupdate_key_path()]
180 if len(self._updates) > 1:
181 cmd.append("-v") # use TCP for bulk payloads
182
183 try:
184 run_command(*cmd, stdin="\n".join(stdin).encode("ascii"))
185 except CalledProcessError as exc:
186 maaslog.error(f"dynamic update of DNS failed: {exc}")
187 ExternalProcessError.upgrade(exc)
188 raise
diff --git a/src/provisioningserver/dns/commands/setup_dns.py b/src/provisioningserver/dns/commands/setup_dns.py
index 65b1166..cdf4e38 100644
--- a/src/provisioningserver/dns/commands/setup_dns.py
+++ b/src/provisioningserver/dns/commands/setup_dns.py
@@ -16,8 +16,10 @@ from textwrap import dedent
1616
17from provisioningserver.dns.config import (17from provisioningserver.dns.config import (
18 DNSConfig,18 DNSConfig,
19 set_up_nsupdate_key,
19 set_up_options_conf,20 set_up_options_conf,
20 set_up_rndc,21 set_up_rndc,
22 set_up_zone_file_dir,
21)23)
2224
2325
@@ -49,6 +51,8 @@ def run(args, stdout=sys.stdout, stderr=sys.stderr):
49 :param stdout: Standard output stream to write to.51 :param stdout: Standard output stream to write to.
50 :param stderr: Standard error stream to write to.52 :param stderr: Standard error stream to write to.
51 """53 """
54 set_up_nsupdate_key()
55 set_up_zone_file_dir()
52 set_up_rndc()56 set_up_rndc()
53 set_up_options_conf(overwrite=not args.no_clobber)57 set_up_options_conf(overwrite=not args.no_clobber)
54 config = DNSConfig()58 config = DNSConfig()
diff --git a/src/provisioningserver/dns/commands/tests/test_setup_dns.py b/src/provisioningserver/dns/commands/tests/test_setup_dns.py
index dc73242..e720cff 100644
--- a/src/provisioningserver/dns/commands/tests/test_setup_dns.py
+++ b/src/provisioningserver/dns/commands/tests/test_setup_dns.py
@@ -17,7 +17,10 @@ from provisioningserver.dns.config import (
17 MAAS_NAMED_CONF_NAME,17 MAAS_NAMED_CONF_NAME,
18 MAAS_RNDC_CONF_NAME,18 MAAS_RNDC_CONF_NAME,
19)19)
20from provisioningserver.dns.testing import patch_dns_config_path20from provisioningserver.dns.testing import (
21 patch_dns_config_path,
22 patch_zone_file_config_path,
23)
2124
2225
23class TestSetupCommand(MAASTestCase):26class TestSetupCommand(MAASTestCase):
@@ -35,6 +38,7 @@ class TestSetupCommand(MAASTestCase):
35 def test_writes_configuration(self):38 def test_writes_configuration(self):
36 dns_conf_dir = self.make_dir()39 dns_conf_dir = self.make_dir()
37 patch_dns_config_path(self, dns_conf_dir)40 patch_dns_config_path(self, dns_conf_dir)
41 patch_zone_file_config_path(self, dns_conf_dir)
38 self.run_command()42 self.run_command()
39 named_config = os.path.join(dns_conf_dir, MAAS_NAMED_CONF_NAME)43 named_config = os.path.join(dns_conf_dir, MAAS_NAMED_CONF_NAME)
40 rndc_conf_path = os.path.join(dns_conf_dir, MAAS_RNDC_CONF_NAME)44 rndc_conf_path = os.path.join(dns_conf_dir, MAAS_RNDC_CONF_NAME)
@@ -43,6 +47,7 @@ class TestSetupCommand(MAASTestCase):
43 def test_does_not_overwrite_config(self):47 def test_does_not_overwrite_config(self):
44 dns_conf_dir = self.make_dir()48 dns_conf_dir = self.make_dir()
45 patch_dns_config_path(self, dns_conf_dir)49 patch_dns_config_path(self, dns_conf_dir)
50 patch_zone_file_config_path(self, dns_conf_dir)
46 random_content = factory.make_string()51 random_content = factory.make_string()
47 factory.make_file(52 factory.make_file(
48 location=dns_conf_dir,53 location=dns_conf_dir,
diff --git a/src/provisioningserver/dns/config.py b/src/provisioningserver/dns/config.py
index d5409e8..6ebc906 100644
--- a/src/provisioningserver/dns/config.py
+++ b/src/provisioningserver/dns/config.py
@@ -6,12 +6,16 @@
66
7from collections import namedtuple7from collections import namedtuple
8from contextlib import contextmanager8from contextlib import contextmanager
9from dataclasses import dataclass
9from datetime import datetime10from datetime import datetime
10import errno11import errno
11import os12import os
12import os.path13import os.path
13import re14import re
14import sys15import sys
16from typing import Optional
17
18from netaddr import AddrFormatError, IPAddress
1519
16from provisioningserver.logger import get_maas_logger20from provisioningserver.logger import get_maas_logger
17from provisioningserver.utils import load_template, locate_config21from provisioningserver.utils import load_template, locate_config
@@ -26,6 +30,62 @@ MAAS_NAMED_CONF_NAME = "named.conf.maas"
26MAAS_NAMED_CONF_OPTIONS_INSIDE_NAME = "named.conf.options.inside.maas"30MAAS_NAMED_CONF_OPTIONS_INSIDE_NAME = "named.conf.options.inside.maas"
27MAAS_NAMED_RNDC_CONF_NAME = "named.conf.rndc.maas"31MAAS_NAMED_RNDC_CONF_NAME = "named.conf.rndc.maas"
28MAAS_RNDC_CONF_NAME = "rndc.conf.maas"32MAAS_RNDC_CONF_NAME = "rndc.conf.maas"
33MAAS_NSUPDATE_KEY_NAME = "keys.conf.maas"
34MAAS_ZONE_FILE_DIR = "/var/lib/bind/maas"
35
36
37@dataclass
38class DynamicDNSUpdate:
39 operation: str
40 name: str
41 zone: str
42 rectype: str
43 ttl: Optional[int] = None
44 subnet: Optional[str] = None # for reverse updates
45 answer: Optional[str] = None
46
47 @classmethod
48 def _is_ip(cls, answer):
49 if not answer:
50 return False
51 try:
52 IPAddress(answer)
53 except AddrFormatError:
54 return False
55 else:
56 return True
57
58 @classmethod
59 def create_from_trigger(cls, **kwargs):
60 answer = kwargs.get("answer")
61 rectype = kwargs.pop("rectype")
62 if answer:
63 del kwargs["answer"]
64 # the DB trigger is unable to figure out if an IP is v6, so we do it here instead
65 if cls._is_ip(answer):
66 ip = IPAddress(answer)
67 if ip.version == 6:
68 rectype = "AAAA"
69 return cls(answer=answer, rectype=rectype, **kwargs)
70
71 @classmethod
72 def as_reverse_record_update(cls, fwd_update, subnet):
73 if not fwd_update.answer_is_ip:
74 return None
75 ip = IPAddress(fwd_update.answer)
76 return cls(
77 operation=fwd_update.operation,
78 name=ip.reverse_dns,
79 zone=fwd_update.zone,
80 subnet=subnet,
81 ttl=fwd_update.ttl,
82 answer=fwd_update.name,
83 rectype="PTR",
84 )
85
86 @property
87 def answer_is_ip(self):
88 return DynamicDNSUpdate._is_ip(self.answer)
2989
3090
31def get_dns_config_dir():91def get_dns_config_dir():
@@ -40,6 +100,19 @@ def get_dns_config_dir():
40 return setting100 return setting
41101
42102
103def get_zone_file_config_dir():
104 """
105 Location of MAAS' zone files, separate from config files
106 so that bind can write to the location as well
107 """
108 setting = os.getenv("MAAS_ZONE_FILE_CONFIG_DIR", MAAS_ZONE_FILE_DIR)
109 if isinstance(setting, bytes):
110 fsenc = sys.getfilesystemencoding()
111 return setting.decode(fsenc)
112 else:
113 return setting
114
115
43def get_bind_config_dir():116def get_bind_config_dir():
44 """Location of bind configuration files."""117 """Location of bind configuration files."""
45 setting = os.getenv(118 setting = os.getenv(
@@ -155,6 +228,21 @@ def get_rndc_conf_path():
155 return compose_config_path(MAAS_RNDC_CONF_NAME)228 return compose_config_path(MAAS_RNDC_CONF_NAME)
156229
157230
231def get_nsupdate_key_path():
232 return compose_config_path(MAAS_NSUPDATE_KEY_NAME)
233
234
235def set_up_nsupdate_key():
236 tsig = call_and_check(["tsig-keygen", "-a", "HMAC-SHA512", "maas."])
237 atomic_write(tsig, get_nsupdate_key_path(), overwrite=True, mode=0o644)
238
239
240def set_up_zone_file_dir():
241 p = get_zone_file_config_dir()
242 if not os.path.exists(p):
243 os.mkdir(p)
244
245
158def set_up_rndc():246def set_up_rndc():
159 """Writes out the two files needed to enable MAAS to use rndc commands:247 """Writes out the two files needed to enable MAAS to use rndc commands:
160 MAAS_RNDC_CONF_NAME and MAAS_NAMED_RNDC_CONF_NAME.248 MAAS_RNDC_CONF_NAME and MAAS_NAMED_RNDC_CONF_NAME.
@@ -232,12 +320,16 @@ def set_up_options_conf(overwrite=True, **kwargs):
232320
233321
234def compose_config_path(filename):322def compose_config_path(filename):
235 """Return the full path for a DNS config or zone file."""323 """Return the full path for a DNS config"""
236 return os.path.join(get_dns_config_dir(), filename)324 return os.path.join(get_dns_config_dir(), filename)
237325
238326
327def compose_zone_file_config_path(filename):
328 return os.path.join(get_zone_file_config_dir(), filename)
329
330
239def compose_bind_config_path(filename):331def compose_bind_config_path(filename):
240 """Return the full path for a DNS config or zone file."""332 """Return the full path for a DNS config"""
241 return os.path.join(get_bind_config_dir(), filename)333 return os.path.join(get_bind_config_dir(), filename)
242334
243335
@@ -309,6 +401,7 @@ class DNSConfig:
309 "forwarded_zones": self.forwarded_zones,401 "forwarded_zones": self.forwarded_zones,
310 "DNS_CONFIG_DIR": get_dns_config_dir(),402 "DNS_CONFIG_DIR": get_dns_config_dir(),
311 "named_rndc_conf_path": get_named_rndc_conf_path(),403 "named_rndc_conf_path": get_named_rndc_conf_path(),
404 "nsupdate_keys_conf_path": get_nsupdate_key_path(),
312 "trusted_networks": trusted_networks,405 "trusted_networks": trusted_networks,
313 "modified": str(datetime.today()),406 "modified": str(datetime.today()),
314 }407 }
diff --git a/src/provisioningserver/dns/testing.py b/src/provisioningserver/dns/testing.py
index 2415e0b..4892f7a 100644
--- a/src/provisioningserver/dns/testing.py
+++ b/src/provisioningserver/dns/testing.py
@@ -17,6 +17,16 @@ def patch_dns_config_path(testcase, config_dir=None):
17 return config_dir17 return config_dir
1818
1919
20def patch_zone_file_config_path(testcase, config_dir=None):
21 """Set the DNS config dir to a temporary directory, and return its path."""
22 if config_dir is None:
23 config_dir = testcase.make_dir()
24 testcase.useFixture(
25 EnvironmentVariable("MAAS_ZONE_FILE_CONFIG_DIR", config_dir)
26 )
27 return config_dir
28
29
20def patch_dns_rndc_port(testcase, port):30def patch_dns_rndc_port(testcase, port):
21 testcase.useFixture(EnvironmentVariable("MAAS_DNS_RNDC_PORT", "%d" % port))31 testcase.useFixture(EnvironmentVariable("MAAS_DNS_RNDC_PORT", "%d" % port))
2232
diff --git a/src/provisioningserver/dns/tests/test_actions.py b/src/provisioningserver/dns/tests/test_actions.py
index 72c5b5a..1087bc0 100644
--- a/src/provisioningserver/dns/tests/test_actions.py
+++ b/src/provisioningserver/dns/tests/test_actions.py
@@ -20,11 +20,19 @@ from maastesting.factory import factory
20from maastesting.matchers import MockCalledOnceWith, MockCallsMatch20from maastesting.matchers import MockCalledOnceWith, MockCallsMatch
21from maastesting.testcase import MAASTestCase21from maastesting.testcase import MAASTestCase
22from provisioningserver.dns import actions22from provisioningserver.dns import actions
23from provisioningserver.dns.actions import (
24 get_nsupdate_key_path,
25 NSUpdateCommand,
26)
23from provisioningserver.dns.config import (27from provisioningserver.dns.config import (
28 DynamicDNSUpdate,
24 MAAS_NAMED_CONF_NAME,29 MAAS_NAMED_CONF_NAME,
25 MAAS_NAMED_CONF_OPTIONS_INSIDE_NAME,30 MAAS_NAMED_CONF_OPTIONS_INSIDE_NAME,
26)31)
27from provisioningserver.dns.testing import patch_dns_config_path32from provisioningserver.dns.testing import (
33 patch_dns_config_path,
34 patch_zone_file_config_path,
35)
28from provisioningserver.dns.tests.test_zoneconfig import HostnameIPMapping36from provisioningserver.dns.tests.test_zoneconfig import HostnameIPMapping
29from provisioningserver.dns.zoneconfig import (37from provisioningserver.dns.zoneconfig import (
30 DNSForwardZoneConfig,38 DNSForwardZoneConfig,
@@ -208,6 +216,7 @@ class TestConfiguration(MAASTestCase):
208 )216 )
209217
210 def test_bind_write_zones_writes_file(self):218 def test_bind_write_zones_writes_file(self):
219 zone_file_dir = patch_zone_file_config_path(self)
211 domain = factory.make_string()220 domain = factory.make_string()
212 network = IPNetwork("192.168.0.3/24")221 network = IPNetwork("192.168.0.3/24")
213 dns_ip_list = [factory.pick_ip_in_network(network)]222 dns_ip_list = [factory.pick_ip_in_network(network)]
@@ -229,8 +238,8 @@ class TestConfiguration(MAASTestCase):
229 forward_file_name = "zone.%s" % domain238 forward_file_name = "zone.%s" % domain
230 reverse_file_name = "zone.0.168.192.in-addr.arpa"239 reverse_file_name = "zone.0.168.192.in-addr.arpa"
231 expected_files = [240 expected_files = [
232 join(self.dns_conf_dir, forward_file_name),241 join(zone_file_dir, forward_file_name),
233 join(self.dns_conf_dir, reverse_file_name),242 join(zone_file_dir, reverse_file_name),
234 ]243 ]
235 self.assertThat(expected_files, AllMatch(FileExists()))244 self.assertThat(expected_files, AllMatch(FileExists()))
236245
@@ -270,3 +279,112 @@ class TestConfiguration(MAASTestCase):
270 self.assertThat(279 self.assertThat(
271 expected_options_file, FileContains(expected_options_content)280 expected_options_file, FileContains(expected_options_content)
272 )281 )
282
283
284class TestNSUpdateCommand(MAASTestCase):
285 def test_format_update_deletion(self):
286 domain = factory.make_name()
287 update = DynamicDNSUpdate(
288 operation="DELETE",
289 zone=domain,
290 name=f"{factory.make_name()}.{domain}",
291 rectype="A",
292 )
293 cmd = NSUpdateCommand(
294 domain,
295 [update],
296 serial=random.randint(1, 100),
297 ttl=random.randint(1, 100),
298 )
299 self.assertEqual(
300 f"update delete {update.name} A", cmd._format_update(update)
301 )
302
303 def test_format_update_addition(self):
304 domain = factory.make_name()
305 update = DynamicDNSUpdate(
306 operation="INSERT",
307 zone=domain,
308 name=f"{factory.make_name()}.{domain}",
309 rectype="A",
310 answer=factory.make_ip_address(),
311 )
312 ttl = random.randint(1, 100)
313 cmd = NSUpdateCommand(
314 domain, [update], serial=random.randint(1, 100), ttl=ttl
315 )
316 self.assertEqual(
317 f"update add {update.name} {ttl} A {update.answer}",
318 cmd._format_update(update),
319 )
320
321 def test_nsupdate_sends_a_single_update(self):
322 domain = factory.make_name()
323 update = DynamicDNSUpdate(
324 operation="INSERT",
325 zone=domain,
326 name=f"{factory.make_name()}.{domain}",
327 rectype="A",
328 answer=factory.make_ip_address(),
329 )
330 serial = random.randint(1, 100)
331 ttl = random.randint(1, 100)
332 cmd = NSUpdateCommand(domain, [update], serial=serial, ttl=ttl)
333 run_command = self.patch(actions, "run_command")
334 cmd.update()
335 run_command.assert_called_once_with(
336 "nsupdate",
337 "-k",
338 get_nsupdate_key_path(),
339 stdin="\n".join(
340 [
341 "server localhost",
342 f"zone {domain}",
343 f"update add {update.name} {ttl} A {update.answer}",
344 f"update add {domain} {ttl} SOA {domain}. nobody.example.com. {serial} 600 1800 604800 {ttl}",
345 "send\n",
346 ]
347 ).encode("ascii"),
348 )
349
350 def test_nsupdate_sends_a_bulk_update(self):
351 domain = factory.make_name()
352 deletions = [
353 DynamicDNSUpdate(
354 operation="DELETE",
355 zone=domain,
356 name=f"{factory.make_name()}.{domain}",
357 rectype="A",
358 )
359 for _ in range(2)
360 ]
361 additions = [
362 DynamicDNSUpdate(
363 operation="INSERT",
364 zone=domain,
365 name=f"{factory.make_name()}.{domain}",
366 rectype="A",
367 answer=factory.make_ip_address(),
368 )
369 for _ in range(2)
370 ]
371 ttl = random.randint(1, 100)
372 cmd = NSUpdateCommand(domain, deletions + additions, ttl=ttl)
373 run_command = self.patch(actions, "run_command")
374 cmd.update()
375 expected_stdin = [
376 "server localhost",
377 f"zone {domain}",
378 f"update delete {deletions[0].name} {deletions[0].rectype}",
379 f"update delete {deletions[1].name} {deletions[1].rectype}",
380 f"update add {additions[0].name} {ttl} {additions[0].rectype} {additions[0].answer}",
381 f"update add {additions[1].name} {ttl} {additions[1].rectype} {additions[1].answer}",
382 "send\n",
383 ]
384 run_command.assert_called_once_with(
385 "nsupdate",
386 "-k",
387 get_nsupdate_key_path(),
388 "-v",
389 stdin="\n".join(expected_stdin).encode("ascii"),
390 )
diff --git a/src/provisioningserver/dns/tests/test_config.py b/src/provisioningserver/dns/tests/test_config.py
index c29c3ca..1e02498 100644
--- a/src/provisioningserver/dns/tests/test_config.py
+++ b/src/provisioningserver/dns/tests/test_config.py
@@ -11,7 +11,7 @@ from textwrap import dedent
11from unittest.mock import Mock, sentinel11from unittest.mock import Mock, sentinel
1212
13from fixtures import EnvironmentVariable13from fixtures import EnvironmentVariable
14from netaddr import IPNetwork14from netaddr import IPAddress, IPNetwork
15from testtools.matchers import (15from testtools.matchers import (
16 AllMatch,16 AllMatch,
17 Contains,17 Contains,
@@ -40,6 +40,7 @@ from provisioningserver.dns.config import (
40 DNSConfig,40 DNSConfig,
41 DNSConfigDirectoryMissing,41 DNSConfigDirectoryMissing,
42 DNSConfigFail,42 DNSConfigFail,
43 DynamicDNSUpdate,
43 execute_rndc_command,44 execute_rndc_command,
44 extract_suggested_named_conf,45 extract_suggested_named_conf,
45 generate_rndc,46 generate_rndc,
@@ -593,3 +594,75 @@ class TestDNSConfig(MAASTestCase):
593 ),594 ),
594 ),595 ),
595 )596 )
597
598
599class TestDynamicDNSUpdate(MAASTestCase):
600 def test_create_from_trigger_v4(self):
601 domain = factory.make_name()
602 update = DynamicDNSUpdate.create_from_trigger(
603 operation="INSERT",
604 zone=domain,
605 name=f"{factory.make_name()}.{domain}",
606 rectype="A",
607 answer=factory.make_ip_address(ipv6=False),
608 )
609 self.assertEqual(update.rectype, "A")
610
611 def test_create_from_trigger_v6(self):
612 domain = factory.make_name()
613 update = DynamicDNSUpdate.create_from_trigger(
614 operation="INSERT",
615 zone=domain,
616 name=f"{factory.make_name()}.{domain}",
617 rectype="A",
618 answer=factory.make_ip_address(ipv6=True),
619 )
620 self.assertEqual(update.rectype, "AAAA")
621
622 def test_answer_is_ip_returns_true_when_answer_is_an_ip(self):
623 domain = factory.make_name()
624 update = DynamicDNSUpdate(
625 operation="INSERT",
626 zone=domain,
627 name=f"{factory.make_name()}.{domain}",
628 rectype="A",
629 answer=factory.make_ip_address(),
630 )
631 self.assertTrue(update.answer_is_ip)
632
633 def test_answer_is_ip_returns_false_when_answer_is_not_an_ip(self):
634 domain = factory.make_name()
635 update = DynamicDNSUpdate(
636 operation="INSERT",
637 zone=domain,
638 name=f"{factory.make_name()}.{domain}",
639 rectype="CNAME",
640 answer=factory.make_name(),
641 )
642 self.assertFalse(update.answer_is_ip)
643
644 def test_as_reverse_record_update(self):
645 domain = factory.make_name()
646 subnet = factory.make_ip4_or_6_network()
647 fwd_update = DynamicDNSUpdate(
648 operation="INSERT",
649 zone=domain,
650 name=f"{factory.make_name()}.{domain}",
651 rectype="A",
652 answer=str(IPAddress(subnet.next())),
653 )
654 expected_rev_update = DynamicDNSUpdate(
655 operation="INSERT",
656 zone=domain,
657 name=IPAddress(fwd_update.answer).reverse_dns,
658 rectype="PTR",
659 ttl=fwd_update.ttl,
660 subnet=str(subnet),
661 answer=fwd_update.name,
662 )
663 rev_update = DynamicDNSUpdate.as_reverse_record_update(
664 fwd_update, str(subnet)
665 )
666 self.assertEqual(expected_rev_update.name, rev_update.name)
667 self.assertEqual(expected_rev_update.rectype, rev_update.rectype)
668 self.assertEqual(expected_rev_update.answer, rev_update.answer)
diff --git a/src/provisioningserver/dns/tests/test_zoneconfig.py b/src/provisioningserver/dns/tests/test_zoneconfig.py
index ba84efb..f97adfd 100644
--- a/src/provisioningserver/dns/tests/test_zoneconfig.py
+++ b/src/provisioningserver/dns/tests/test_zoneconfig.py
@@ -22,8 +22,13 @@ from twisted.python.filepath import FilePath
22from maastesting.factory import factory22from maastesting.factory import factory
23from maastesting.matchers import MockNotCalled23from maastesting.matchers import MockNotCalled
24from maastesting.testcase import MAASTestCase24from maastesting.testcase import MAASTestCase
25from provisioningserver.dns.config import get_dns_config_dir25from provisioningserver.dns import actions
26from provisioningserver.dns.testing import patch_dns_config_path26from provisioningserver.dns.config import (
27 DynamicDNSUpdate,
28 get_nsupdate_key_path,
29 get_zone_file_config_dir,
30)
31from provisioningserver.dns.testing import patch_zone_file_config_path
27from provisioningserver.dns.zoneconfig import (32from provisioningserver.dns.zoneconfig import (
28 DNSForwardZoneConfig,33 DNSForwardZoneConfig,
29 DNSReverseZoneConfig,34 DNSReverseZoneConfig,
@@ -100,7 +105,7 @@ class TestDNSForwardZoneConfig(MAASTestCase):
100 domain = factory.make_name("zone")105 domain = factory.make_name("zone")
101 dns_zone_config = DNSForwardZoneConfig(domain)106 dns_zone_config = DNSForwardZoneConfig(domain)
102 self.assertEqual(107 self.assertEqual(
103 os.path.join(get_dns_config_dir(), "zone.%s" % domain),108 os.path.join(get_zone_file_config_dir(), "zone.%s" % domain),
104 dns_zone_config.zone_info[0].target_path,109 dns_zone_config.zone_info[0].target_path,
105 )110 )
106111
@@ -173,7 +178,7 @@ class TestDNSForwardZoneConfig(MAASTestCase):
173 )178 )
174179
175 def test_handles_slash_32_dynamic_range(self):180 def test_handles_slash_32_dynamic_range(self):
176 target_dir = patch_dns_config_path(self)181 target_dir = patch_zone_file_config_path(self)
177 domain = factory.make_string()182 domain = factory.make_string()
178 network = factory.make_ipv4_network()183 network = factory.make_ipv4_network()
179 ipv4_hostname = factory.make_name("host")184 ipv4_hostname = factory.make_name("host")
@@ -222,7 +227,7 @@ class TestDNSForwardZoneConfig(MAASTestCase):
222 )227 )
223228
224 def test_writes_dns_zone_config(self):229 def test_writes_dns_zone_config(self):
225 target_dir = patch_dns_config_path(self)230 target_dir = patch_zone_file_config_path(self)
226 domain = factory.make_string()231 domain = factory.make_string()
227 network = factory.make_ipv4_network()232 network = factory.make_ipv4_network()
228 ipv4_hostname = factory.make_name("host")233 ipv4_hostname = factory.make_name("host")
@@ -269,7 +274,7 @@ class TestDNSForwardZoneConfig(MAASTestCase):
269 )274 )
270275
271 def test_writes_dns_zone_config_with_NS_record(self):276 def test_writes_dns_zone_config_with_NS_record(self):
272 target_dir = patch_dns_config_path(self)277 target_dir = patch_zone_file_config_path(self)
273 addr_ttl = random.randint(10, 100)278 addr_ttl = random.randint(10, 100)
274 ns_host_name = factory.make_name("ns")279 ns_host_name = factory.make_name("ns")
275 dns_zone_config = DNSForwardZoneConfig(280 dns_zone_config = DNSForwardZoneConfig(
@@ -286,7 +291,7 @@ class TestDNSForwardZoneConfig(MAASTestCase):
286 )291 )
287292
288 def test_ignores_generate_directives_for_v6_dynamic_ranges(self):293 def test_ignores_generate_directives_for_v6_dynamic_ranges(self):
289 patch_dns_config_path(self)294 patch_zone_file_config_path(self)
290 domain = factory.make_string()295 domain = factory.make_string()
291 network = factory.make_ipv4_network()296 network = factory.make_ipv4_network()
292 ipv4_hostname = factory.make_name("host")297 ipv4_hostname = factory.make_name("host")
@@ -314,7 +319,7 @@ class TestDNSForwardZoneConfig(MAASTestCase):
314 self.assertThat(get_generate_directives, MockNotCalled())319 self.assertThat(get_generate_directives, MockNotCalled())
315320
316 def test_config_file_is_world_readable(self):321 def test_config_file_is_world_readable(self):
317 patch_dns_config_path(self)322 patch_zone_file_config_path(self)
318 dns_zone_config = DNSForwardZoneConfig(323 dns_zone_config = DNSForwardZoneConfig(
319 factory.make_string(), serial=random.randint(1, 100)324 factory.make_string(), serial=random.randint(1, 100)
320 )325 )
@@ -322,6 +327,94 @@ class TestDNSForwardZoneConfig(MAASTestCase):
322 filepath = FilePath(dns_zone_config.zone_info[0].target_path)327 filepath = FilePath(dns_zone_config.zone_info[0].target_path)
323 self.assertTrue(filepath.getPermissions().other.read)328 self.assertTrue(filepath.getPermissions().other.read)
324329
330 def test_zone_file_exists(self):
331 patch_zone_file_config_path(self)
332 domain = factory.make_string()
333 network = factory.make_ipv4_network()
334 ipv4_hostname = factory.make_name("host")
335 ipv4_ip = factory.pick_ip_in_network(network)
336 ipv6_hostname = factory.make_name("host")
337 ipv6_ip = factory.make_ipv6_address()
338 ipv6_network = factory.make_ipv6_network()
339 dynamic_range = IPRange(ipv6_network.first, ipv6_network.last)
340 ttl = random.randint(10, 300)
341 mapping = {
342 ipv4_hostname: HostnameIPMapping(None, ttl, {ipv4_ip}),
343 ipv6_hostname: HostnameIPMapping(None, ttl, {ipv6_ip}),
344 }
345 dns_zone_config = DNSForwardZoneConfig(
346 domain,
347 serial=random.randint(1, 100),
348 mapping=mapping,
349 default_ttl=ttl,
350 dynamic_ranges=[dynamic_range],
351 )
352 self.patch(dns_zone_config, "get_GENERATE_directives")
353 self.assertFalse(
354 dns_zone_config.zone_file_exists(dns_zone_config.zone_info[0])
355 )
356 dns_zone_config.write_config()
357 self.assertTrue(
358 dns_zone_config.zone_file_exists(dns_zone_config.zone_info[0])
359 )
360
361 def test_uses_dynamic_update_when_zone_has_been_configured_once(self):
362 patch_zone_file_config_path(self)
363 domain = factory.make_string()
364 network = factory.make_ipv4_network()
365 ipv4_hostname = factory.make_name("host")
366 ipv4_ip = factory.pick_ip_in_network(network)
367 ipv6_hostname = factory.make_name("host")
368 ipv6_ip = factory.make_ipv6_address()
369 ipv6_network = factory.make_ipv6_network()
370 dynamic_range = IPRange(ipv6_network.first, ipv6_network.last)
371 ttl = random.randint(10, 300)
372 mapping = {
373 ipv4_hostname: HostnameIPMapping(None, ttl, {ipv4_ip}),
374 ipv6_hostname: HostnameIPMapping(None, ttl, {ipv6_ip}),
375 }
376 dns_zone_config = DNSForwardZoneConfig(
377 domain,
378 serial=random.randint(1, 100),
379 mapping=mapping,
380 default_ttl=ttl,
381 dynamic_ranges=[dynamic_range],
382 )
383 self.patch(dns_zone_config, "get_GENERATE_directives")
384 run_command = self.patch(actions, "run_command")
385 dns_zone_config.write_config()
386 update = DynamicDNSUpdate.create_from_trigger(
387 operation="INSERT",
388 zone=domain,
389 name=f"{factory.make_name()}.{domain}",
390 rectype="A",
391 answer=factory.make_ip_address(),
392 )
393 new_dns_zone_config = DNSForwardZoneConfig(
394 domain,
395 serial=random.randint(1, 100),
396 mapping=mapping,
397 default_ttl=ttl,
398 dynamic_ranges=[dynamic_range],
399 dynamic_updates=[update],
400 )
401 new_dns_zone_config.write_config()
402 expected_stdin = "\n".join(
403 [
404 "server localhost",
405 f"zone {domain}",
406 f"update add {update.name} {ttl} {'A' if IPAddress(update.answer).version == 4 else 'AAAA'} {update.answer}",
407 f"update add {domain} {new_dns_zone_config.default_ttl} SOA {domain}. nobody.example.com. {new_dns_zone_config.serial} 600 1800 604800 {new_dns_zone_config.default_ttl}",
408 "send\n",
409 ]
410 )
411 run_command.assert_called_once_with(
412 "nsupdate",
413 "-k",
414 get_nsupdate_key_path(),
415 stdin=expected_stdin.encode("ascii"),
416 )
417
325418
326class TestDNSReverseZoneConfig(MAASTestCase):419class TestDNSReverseZoneConfig(MAASTestCase):
327 """Tests for DNSReverseZoneConfig."""420 """Tests for DNSReverseZoneConfig."""
@@ -340,7 +433,7 @@ class TestDNSReverseZoneConfig(MAASTestCase):
340 ),433 ),
341 )434 )
342435
343 def test_computes_dns_config_file_paths(self):436 def test_computes_zone_file_config_file_paths(self):
344 domain = factory.make_name("zone")437 domain = factory.make_name("zone")
345 reverse_file_name = [438 reverse_file_name = [
346 "zone.%d.168.192.in-addr.arpa" % i for i in range(4)439 "zone.%d.168.192.in-addr.arpa" % i for i in range(4)
@@ -350,11 +443,11 @@ class TestDNSReverseZoneConfig(MAASTestCase):
350 )443 )
351 for i in range(4):444 for i in range(4):
352 self.assertEqual(445 self.assertEqual(
353 os.path.join(get_dns_config_dir(), reverse_file_name[i]),446 os.path.join(get_zone_file_config_dir(), reverse_file_name[i]),
354 dns_zone_config.zone_info[i].target_path,447 dns_zone_config.zone_info[i].target_path,
355 )448 )
356449
357 def test_computes_dns_config_file_paths_for_small_network(self):450 def test_computes_zone_file_config_file_paths_for_small_network(self):
358 domain = factory.make_name("zone")451 domain = factory.make_name("zone")
359 reverse_file_name = "zone.192-27.0.168.192.in-addr.arpa"452 reverse_file_name = "zone.192-27.0.168.192.in-addr.arpa"
360 dns_zone_config = DNSReverseZoneConfig(453 dns_zone_config = DNSReverseZoneConfig(
@@ -362,7 +455,7 @@ class TestDNSReverseZoneConfig(MAASTestCase):
362 )455 )
363 self.assertEqual(1, len(dns_zone_config.zone_info))456 self.assertEqual(1, len(dns_zone_config.zone_info))
364 self.assertEqual(457 self.assertEqual(
365 os.path.join(get_dns_config_dir(), reverse_file_name),458 os.path.join(get_zone_file_config_dir(), reverse_file_name),
366 dns_zone_config.zone_info[0].target_path,459 dns_zone_config.zone_info[0].target_path,
367 )460 )
368461
@@ -620,7 +713,7 @@ class TestDNSReverseZoneConfig(MAASTestCase):
620 )713 )
621714
622 def test_writes_dns_zone_config_with_NS_record(self):715 def test_writes_dns_zone_config_with_NS_record(self):
623 target_dir = patch_dns_config_path(self)716 target_dir = patch_zone_file_config_path(self)
624 network = factory.make_ipv4_network()717 network = factory.make_ipv4_network()
625 ns_host_name = factory.make_name("ns")718 ns_host_name = factory.make_name("ns")
626 dns_zone_config = DNSReverseZoneConfig(719 dns_zone_config = DNSReverseZoneConfig(
@@ -637,7 +730,7 @@ class TestDNSReverseZoneConfig(MAASTestCase):
637 )730 )
638731
639 def test_writes_reverse_dns_zone_config(self):732 def test_writes_reverse_dns_zone_config(self):
640 target_dir = patch_dns_config_path(self)733 target_dir = patch_zone_file_config_path(self)
641 domain = factory.make_string()734 domain = factory.make_string()
642 ns_host_name = factory.make_name("ns")735 ns_host_name = factory.make_name("ns")
643 network = IPNetwork("192.168.0.1/22")736 network = IPNetwork("192.168.0.1/22")
@@ -676,7 +769,7 @@ class TestDNSReverseZoneConfig(MAASTestCase):
676 )769 )
677770
678 def test_writes_reverse_dns_zone_config_for_small_network(self):771 def test_writes_reverse_dns_zone_config_for_small_network(self):
679 target_dir = patch_dns_config_path(self)772 target_dir = patch_zone_file_config_path(self)
680 domain = factory.make_string()773 domain = factory.make_string()
681 ns_host_name = factory.make_name("ns")774 ns_host_name = factory.make_name("ns")
682 network = IPNetwork("192.168.0.1/26")775 network = IPNetwork("192.168.0.1/26")
@@ -710,7 +803,7 @@ class TestDNSReverseZoneConfig(MAASTestCase):
710 )803 )
711804
712 def test_ignores_generate_directives_for_v6_dynamic_ranges(self):805 def test_ignores_generate_directives_for_v6_dynamic_ranges(self):
713 patch_dns_config_path(self)806 patch_zone_file_config_path(self)
714 domain = factory.make_string()807 domain = factory.make_string()
715 network = IPNetwork("192.168.0.1/22")808 network = IPNetwork("192.168.0.1/22")
716 dynamic_network = IPNetwork("%s/64" % factory.make_ipv6_address())809 dynamic_network = IPNetwork("%s/64" % factory.make_ipv6_address())
@@ -729,7 +822,7 @@ class TestDNSReverseZoneConfig(MAASTestCase):
729 self.assertThat(get_generate_directives, MockNotCalled())822 self.assertThat(get_generate_directives, MockNotCalled())
730823
731 def test_reverse_config_file_is_world_readable(self):824 def test_reverse_config_file_is_world_readable(self):
732 patch_dns_config_path(self)825 patch_zone_file_config_path(self)
733 dns_zone_config = DNSReverseZoneConfig(826 dns_zone_config = DNSReverseZoneConfig(
734 factory.make_string(),827 factory.make_string(),
735 serial=random.randint(1, 100),828 serial=random.randint(1, 100),
@@ -740,6 +833,61 @@ class TestDNSReverseZoneConfig(MAASTestCase):
740 filepath = FilePath(tgt)833 filepath = FilePath(tgt)
741 self.assertTrue(filepath.getPermissions().other.read)834 self.assertTrue(filepath.getPermissions().other.read)
742835
836 def test_dynamic_update_when_zone_file_exists(self):
837 patch_zone_file_config_path(self)
838 domain = factory.make_string()
839 network = IPNetwork("10.0.0.0/24")
840 ip1 = factory.pick_ip_in_network(network)
841 ip2 = factory.pick_ip_in_network(network)
842 hostname1 = f"{factory.make_string()}.{domain}"
843 hostname2 = f"{factory.make_string()}.{domain}"
844 fwd_updates = [
845 DynamicDNSUpdate(
846 operation="INSERT",
847 zone=domain,
848 name=hostname1,
849 rectype="A",
850 answer=ip1,
851 ),
852 DynamicDNSUpdate(
853 operation="INSERT",
854 zone=domain,
855 name=hostname2,
856 rectype="A",
857 answer=ip2,
858 ),
859 ]
860 rev_updates = [
861 DynamicDNSUpdate.as_reverse_record_update(update, str(network))
862 for update in fwd_updates
863 ]
864 zone = DNSReverseZoneConfig(
865 domain,
866 serial=random.randint(1, 100),
867 network=network,
868 dynamic_updates=rev_updates,
869 )
870 run_command = self.patch(actions, "run_command")
871 zone.write_config()
872 zone.write_config()
873 expected_stdin = "\n".join(
874 [
875 "server localhost",
876 "zone 0.0.10.in-addr.arpa",
877 f"update add {IPAddress(ip1).reverse_dns} {zone.default_ttl} PTR {hostname1}",
878 f"update add {IPAddress(ip2).reverse_dns} {zone.default_ttl} PTR {hostname2}",
879 f"update add 0.0.10.in-addr.arpa {zone.default_ttl} SOA 0.0.10.in-addr.arpa. nobody.example.com. {zone.serial} 600 1800 604800 {zone.default_ttl}",
880 "send\n",
881 ]
882 )
883 run_command.assert_called_once_with(
884 "nsupdate",
885 "-k",
886 get_nsupdate_key_path(),
887 "-v",
888 stdin=expected_stdin.encode("ascii"),
889 )
890
743891
744class TestDNSReverseZoneConfig_GetGenerateDirectives(MAASTestCase):892class TestDNSReverseZoneConfig_GetGenerateDirectives(MAASTestCase):
745 """Tests for `DNSReverseZoneConfig.get_GENERATE_directives()`."""893 """Tests for `DNSReverseZoneConfig.get_GENERATE_directives()`."""
diff --git a/src/provisioningserver/dns/zoneconfig.py b/src/provisioningserver/dns/zoneconfig.py
index d393818..eb28986 100644
--- a/src/provisioningserver/dns/zoneconfig.py
+++ b/src/provisioningserver/dns/zoneconfig.py
@@ -6,12 +6,14 @@
66
7from datetime import datetime7from datetime import datetime
8from itertools import chain8from itertools import chain
9import os
910
10from netaddr import IPAddress, IPNetwork, spanning_cidr11from netaddr import IPAddress, IPNetwork, spanning_cidr
11from netaddr.core import AddrFormatError12from netaddr.core import AddrFormatError
1213
14from provisioningserver.dns.actions import NSUpdateCommand
13from provisioningserver.dns.config import (15from provisioningserver.dns.config import (
14 compose_config_path,16 compose_zone_file_config_path,
15 render_dns_template,17 render_dns_template,
16 report_missing_config_dir,18 report_missing_config_dir,
17)19)
@@ -115,7 +117,9 @@ class DomainInfo:
115 self.subnetwork = subnetwork117 self.subnetwork = subnetwork
116 self.zone_name = zone_name118 self.zone_name = zone_name
117 if target_path is None:119 if target_path is None:
118 self.target_path = compose_config_path("zone.%s" % zone_name)120 self.target_path = compose_zone_file_config_path(
121 "zone.%s" % zone_name
122 )
119 else:123 else:
120 self.target_path = target_path124 self.target_path = target_path
121125
@@ -140,9 +144,12 @@ class DomainConfigBase:
140 self.ns_host_name = kwargs.pop("ns_host_name", None)144 self.ns_host_name = kwargs.pop("ns_host_name", None)
141 self.serial = serial145 self.serial = serial
142 self.zone_info = zone_info146 self.zone_info = zone_info
143 self.target_base = compose_config_path("zone")147 self.target_base = compose_zone_file_config_path("zone")
144 self.default_ttl = kwargs.pop("default_ttl", 30)148 self.default_ttl = kwargs.pop("default_ttl", 30)
145 self.ns_ttl = kwargs.pop("ns_ttl", self.default_ttl)149 self.ns_ttl = kwargs.pop("ns_ttl", self.default_ttl)
150 self.requires_reload = False
151 self._dynamic_updates = kwargs.pop("dynamic_updates", [])
152 self.force_config_write = kwargs.pop("force_config_write", False)
146153
147 def make_parameters(self):154 def make_parameters(self):
148 """Return a dict of the common template parameters."""155 """Return a dict of the common template parameters."""
@@ -155,6 +162,27 @@ class DomainConfigBase:
155 "ns_host_name": self.ns_host_name,162 "ns_host_name": self.ns_host_name,
156 }163 }
157164
165 def zone_file_exists(self, zone_info):
166 try:
167 os.stat(zone_info.target_path)
168 except FileNotFoundError:
169 return False
170 else:
171 return True
172
173 def dynamic_update(self, zone_info):
174 nsupdate = NSUpdateCommand(
175 zone_info.zone_name,
176 [
177 update
178 for update in self._dynamic_updates
179 if update.zone == zone_info.zone_name or update.subnet
180 ],
181 serial=self.serial,
182 ttl=self.default_ttl,
183 )
184 nsupdate.update()
185
158 @classmethod186 @classmethod
159 def write_zone_file(cls, output_file, *parameters):187 def write_zone_file(cls, output_file, *parameters):
160 """Write a zone file based on the zone file template.188 """Write a zone file based on the zone file template.
@@ -292,22 +320,29 @@ class DNSForwardZoneConfig(DomainConfigBase):
292 if dynamic_range.version == 4320 if dynamic_range.version == 4
293 )321 )
294 )322 )
295 self.write_zone_file(323 if not self.force_config_write and self.zone_file_exists(zi):
296 zi.target_path,324 if len(self._dynamic_updates) > 0:
297 self.make_parameters(),325 self.dynamic_update(zi)
298 {326 else:
299 "mappings": {327 self.requires_reload = True
300 "A": self.get_A_mapping(self._mapping, self._ipv4_ttl),328 self.write_zone_file(
301 "AAAA": self.get_AAAA_mapping(329 zi.target_path,
302 self._mapping, self._ipv6_ttl330 self.make_parameters(),
331 {
332 "mappings": {
333 "A": self.get_A_mapping(
334 self._mapping, self._ipv4_ttl
335 ),
336 "AAAA": self.get_AAAA_mapping(
337 self._mapping, self._ipv6_ttl
338 ),
339 },
340 "other_mapping": enumerate_rrset_mapping(
341 self._other_mapping
303 ),342 ),
343 "generate_directives": {"A": generate_directives},
304 },344 },
305 "other_mapping": enumerate_rrset_mapping(345 )
306 self._other_mapping
307 ),
308 "generate_directives": {"A": generate_directives},
309 },
310 )
311346
312347
313class DNSReverseZoneConfig(DomainConfigBase):348class DNSReverseZoneConfig(DomainConfigBase):
@@ -575,21 +610,28 @@ class DNSReverseZoneConfig(DomainConfigBase):
575 if dynamic_range.version == 4610 if dynamic_range.version == 4
576 )611 )
577 )612 )
578 self.write_zone_file(613 if not self.force_config_write and self.zone_file_exists(zi):
579 zi.target_path,614 if len(self._dynamic_updates) > 0:
580 self.make_parameters(),615 self.dynamic_update(zi)
581 {616 else:
582 "mappings": {617 self.requires_reload = True
583 "PTR": self.get_PTR_mapping(618 self.write_zone_file(
584 self._mapping, zi.subnetwork619 zi.target_path,
585 )620 self.make_parameters(),
586 },621 {
587 "other_mapping": [],622 "mappings": {
588 "generate_directives": {623 "PTR": self.get_PTR_mapping(
589 "PTR": generate_directives,624 self._mapping, zi.subnetwork
590 "CNAME": self.get_rfc2317_GENERATE_directives(625 )
591 zi.subnetwork, self._rfc2317_ranges, self.domain626 },
592 ),627 "other_mapping": [],
628 "generate_directives": {
629 "PTR": generate_directives,
630 "CNAME": self.get_rfc2317_GENERATE_directives(
631 zi.subnetwork,
632 self._rfc2317_ranges,
633 self.domain,
634 ),
635 },
593 },636 },
594 },637 )
595 )
diff --git a/src/provisioningserver/templates/dns/named.conf.template b/src/provisioningserver/templates/dns/named.conf.template
index f0f9841..7217ca8 100644
--- a/src/provisioningserver/templates/dns/named.conf.template
+++ b/src/provisioningserver/templates/dns/named.conf.template
@@ -1,4 +1,5 @@
1include "{{named_rndc_conf_path}}";1include "{{named_rndc_conf_path}}";
2include "{{nsupdate_keys_conf_path}}";
23
3# Authoritative Zone declarations.4# Authoritative Zone declarations.
4{{for zone in zones}}5{{for zone in zones}}
@@ -6,6 +7,9 @@ include "{{named_rndc_conf_path}}";
6zone "{{zoneinfo.zone_name}}" {7zone "{{zoneinfo.zone_name}}" {
7 type master;8 type master;
8 file "{{zoneinfo.target_path}}";9 file "{{zoneinfo.target_path}}";
10 allow-update {
11 key maas.;
12 };
9};13};
10{{endfor}}14{{endfor}}
11{{endfor}}15{{endfor}}

Subscribers

People subscribed via source and target branches