Merge ~cgrabowski/maas:update_dns_via_nsupdate into maas:master

Proposed by Christian Grabowski
Status: Merged
Approved by: Christian Grabowski
Approved revision: a1306793412e4e2391470d984523214c06ed4544
Merge reported by: MAAS Lander
Merged at revision: not available
Proposed branch: ~cgrabowski/maas:update_dns_via_nsupdate
Merge into: maas:master
Diff against target: 2500 lines (+1626/-87)
22 files modified
debian/maas-region-api.dirs (+1/-0)
debian/maas-region-api.postinst (+5/-0)
src/maasserver/dns/config.py (+162/-10)
src/maasserver/dns/tests/test_config.py (+149/-1)
src/maasserver/dns/zonegenerator.py (+55/-2)
src/maasserver/region_controller.py (+37/-2)
src/maasserver/region_script.py (+1/-0)
src/maasserver/tests/test_region_controller.py (+22/-10)
src/maasserver/triggers/system.py (+240/-0)
src/maasserver/triggers/testing.py (+43/-2)
src/maasserver/triggers/tests/test_init.py (+13/-0)
src/maasserver/triggers/tests/test_system.py (+280/-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 (+106/-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+433050@code.launchpad.net

This proposal supersedes a proposal from 2022-08-31.

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 : Posted in a previous version of this proposal

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 : Posted in a previous version of this proposal

some minor questions

Revision history for this message
MAAS Lander (maas-lander) wrote : Posted in a previous version of this proposal

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 : Posted in a previous version of this proposal

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) : Posted in a previous version of this proposal
Revision history for this message
MAAS Lander (maas-lander) wrote : Posted in a previous version of this proposal

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 : Posted in a previous version of this proposal

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
Revision history for this message
Alexsander de Souza (alexsander-souza) wrote : Posted in a previous version of this proposal

a few questions inline

review: Needs Information
Revision history for this message
MAAS Lander (maas-lander) wrote : Posted in a previous version of this proposal

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) : Posted in a previous version of this proposal
Revision history for this message
Alexsander de Souza (alexsander-souza) wrote : Posted in a previous version of this proposal

+1

review: Approve
Revision history for this message
MAAS Lander (maas-lander) wrote : Posted in a previous version of this proposal

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 : Posted in a previous version of this proposal

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
Revision history for this message
MAAS Lander (maas-lander) wrote : Posted in a previous version of this proposal

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 : Posted in a previous version of this proposal

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 : Posted in a previous version of this proposal

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 : Posted in a previous version of this proposal

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 : Posted in a previous version of this proposal

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 : Posted in a previous version of this proposal

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
Revision history for this message
Adam Collard (adam-collard) : Posted in a previous version of this proposal
Revision history for this message
MAAS Lander (maas-lander) wrote : Posted in a previous version of this proposal

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
Revision history for this message
MAAS Lander (maas-lander) wrote : Posted in a previous version of this proposal

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 : Posted in a previous version of this proposal

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 : Posted in a previous version of this proposal

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 : Posted in a previous version of this proposal

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 : Posted in a previous version of this proposal

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 : Posted in a previous version of this proposal

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
Revision history for this message
MAAS Lander (maas-lander) wrote : Posted in a previous version of this proposal

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 : Posted in a previous version of this proposal

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 : Posted in a previous version of this proposal

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 : Posted in a previous version of this proposal

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 : Posted in a previous version of this proposal

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
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/1362/consoleText
COMMIT: 4e44d9c55fa32e19ccc7b937aa0b75522e8b25d6

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/1366/consoleText
COMMIT: 8af81b7358c1c4caad6f61574410c0ef9991bafc

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/1367/consoleText
COMMIT: 56138ea436744ab571f9b187f7e6fd94f54f6e71

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/1368/consoleText
COMMIT: a42fc61d6c6030228b9f77957bd0bd6da608a009

review: Needs Fixing
Revision history for this message
Christian Grabowski (cgrabowski) wrote :

jenkins: !test

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: a42fc61d6c6030228b9f77957bd0bd6da608a009

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

+1, a few nits inline

review: Approve
Revision history for this message
Christian Grabowski (cgrabowski) :
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/1386/consoleText
COMMIT: a9b01be54adf1137008717c2b55118aa3b22e363

review: Needs Fixing
Revision history for this message
Christian Grabowski (cgrabowski) wrote :

jenkins: !test

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/1387/consoleText
COMMIT: a9b01be54adf1137008717c2b55118aa3b22e363

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/1388/consoleText
COMMIT: e94338c9ee9e34fe44662b901a4f904f0f411091

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/1389/consoleText
COMMIT: b4cf0e57bb27ea4a5e7e0ed880500b90b3ce2a92

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: a1306793412e4e2391470d984523214c06ed4544

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

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

STATUS: FAILED BUILD
LOG: http://maas-ci.internal:8080/job/maas-tester/1391/consoleText

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

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

STATUS: FAILED BUILD
LOG: http://maas-ci.internal:8080/job/maas-tester/1392/consoleText

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

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

STATUS: FAILED BUILD
LOG: http://maas-ci.internal:8080/job/maas-tester/1393/consoleText

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..c484570 100644
--- a/src/maasserver/dns/config.py
+++ b/src/maasserver/dns/config.py
@@ -17,7 +17,9 @@ from maasserver.dns.zonegenerator import (
17)17)
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.dnsdata import DNSData
20from maasserver.models.dnspublication import DNSPublication21from maasserver.models.dnspublication import DNSPublication
22from maasserver.models.dnsresource import DNSResource
21from maasserver.models.domain import Domain23from maasserver.models.domain import Domain
22from maasserver.models.node import RackController24from maasserver.models.node import RackController
23from maasserver.models.subnet import Subnet25from maasserver.models.subnet import Subnet
@@ -28,7 +30,9 @@ from provisioningserver.dns.actions import (
28 bind_write_options,30 bind_write_options,
29 bind_write_zones,31 bind_write_zones,
30)32)
33from provisioningserver.dns.config import DynamicDNSUpdate
31from provisioningserver.logger import get_maas_logger34from provisioningserver.logger import get_maas_logger
35from provisioningserver.utils.shell import ExternalProcessError
3236
33maaslog = get_maas_logger("dns")37maaslog = get_maas_logger("dns")
3438
@@ -61,7 +65,12 @@ def forward_domains_to_forwarded_zones(forward_domains):
61 ]65 ]
6266
6367
64def dns_update_all_zones(reload_retry=False, reload_timeout=2):68def dns_update_all_zones(
69 reload_retry=False,
70 reload_timeout=2,
71 dynamic_updates=None,
72 requires_reload=False,
73):
65 """Update all zone files for all domains.74 """Update all zone files for all domains.
6675
67 Serving these zone files means updating BIND's configuration to include76 Serving these zone files means updating BIND's configuration to include
@@ -70,10 +79,23 @@ def dns_update_all_zones(reload_retry=False, reload_timeout=2):
70 :param reload_retry: Should the DNS server reload be retried in case79 :param reload_retry: Should the DNS server reload be retried in case
71 of failure? Defaults to `False`.80 of failure? Defaults to `False`.
72 :type reload_retry: bool81 :type reload_retry: bool
82
83 :param reload_timeout: How many seconds to wait for BIND's reload to succeed
84 :type reload_timeout: int
85
86 :param dynamic_updates: A list of updates to send via nsupdate to BIND
87 :type dynamic_updates: list[DynamicDNSUpdate]
88
89 :param requires_reload: If true, dynamic updates are ignored and a full reload will occur
90 :type requires_reload: bool
73 """91 """
74 if not is_dns_enabled():92 if not is_dns_enabled():
75 return93 return
7694
95 if not dynamic_updates:
96 dynamic_updates = []
97
98 reloaded = True
77 domains = Domain.objects.filter(authoritative=True)99 domains = Domain.objects.filter(authoritative=True)
78 forwarded_zones = forward_domains_to_forwarded_zones(100 forwarded_zones = forward_domains_to_forwarded_zones(
79 Domain.objects.get_forward_domains()101 Domain.objects.get_forward_domains()
@@ -87,8 +109,13 @@ def dns_update_all_zones(reload_retry=False, reload_timeout=2):
87 default_ttl,109 default_ttl,
88 serial,110 serial,
89 internal_domains=[get_internal_domain()],111 internal_domains=[get_internal_domain()],
112 dynamic_updates=dynamic_updates,
113 force_config_write=requires_reload,
90 ).as_list()114 ).as_list()
91 bind_write_zones(zones)115 try:
116 bind_write_zones(zones)
117 except ExternalProcessError: # dynamic update failed
118 reloaded = False
92119
93 # We should not be calling bind_write_options() here; call-sites should be120 # 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 now121 # making a separate call. It's a historical legacy, where many sites now
@@ -110,14 +137,21 @@ def dns_update_all_zones(reload_retry=False, reload_timeout=2):
110 forwarded_zones=forwarded_zones,137 forwarded_zones=forwarded_zones,
111 )138 )
112139
113 # Reloading with retries may be a legacy from Celery days, or it may be140 if not requires_reload:
114 # necessary to recover from races during start-up. We're not sure if it is141 for zone in zones:
115 # actually needed but it seems safer to maintain this behaviour until we142 if zone.requires_reload:
116 # have a better understanding.143 requires_reload = True
117 if reload_retry:144 break
118 reloaded = bind_reload_with_retries(timeout=reload_timeout)145
119 else:146 if requires_reload:
120 reloaded = bind_reload(timeout=reload_timeout)147 # Reloading with retries may be a legacy from Celery days, or it may be
148 # necessary to recover from races during start-up. We're not sure if it is
149 # actually needed but it seems safer to maintain this behaviour until we
150 # have a better understanding.
151 if reload_retry:
152 reloaded = bind_reload_with_retries(timeout=reload_timeout)
153 else:
154 reloaded = bind_reload(timeout=reload_timeout)
121155
122 # Return the current serial and list of domain names.156 # Return the current serial and list of domain names.
123 return serial, reloaded, [domain.name for domain in domains]157 return serial, reloaded, [domain.name for domain in domains]
@@ -251,3 +285,121 @@ def get_internal_domain():
251 ttl=15,285 ttl=15,
252 resources=resources,286 resources=resources,
253 )287 )
288
289
290def process_dns_update_notify(message):
291 updates = []
292 update_list = message.split(" ")
293 op = update_list[0]
294 zone = None
295 name = None
296 rectype = None
297 ttl = None
298 answer = None
299 if op == "RELOAD":
300 return (updates, True)
301 elif op == "INSERT-DATA" or op == "UPDATE-DATA":
302 dns_data = DNSData.objects.get(id=int(update_list[1]))
303 zone = dns_data.dnsresource.domain.name
304 name = dns_data.dnsresource.name
305 ttl = dns_data.ttl
306 rectype = dns_data.rrtype
307 answer = dns_data.rrdata
308 else:
309 zone = update_list[1]
310 name = f"{update_list[2]}.{zone}"
311 rectype = update_list[3]
312 if op == "INSERT" or op == "UPDATE":
313 ttl = int(update_list[-2]) if update_list[-2] else None
314 answer = update_list[-1]
315
316 match op:
317 case "UPDATE":
318 updates.append(
319 DynamicDNSUpdate.create_from_trigger(
320 operation="DELETE",
321 zone=zone,
322 name=name,
323 rectype=rectype,
324 answer=answer,
325 )
326 )
327 updates.append(
328 DynamicDNSUpdate.create_from_trigger(
329 operation="INSERT",
330 zone=zone,
331 name=name,
332 rectype=rectype,
333 ttl=ttl,
334 answer=answer,
335 )
336 )
337 case "INSERT":
338 updates.append(
339 DynamicDNSUpdate.create_from_trigger(
340 operation=op,
341 zone=zone,
342 name=name,
343 rectype=rectype,
344 ttl=ttl,
345 answer=answer,
346 )
347 )
348 case _:
349 # special case where we know an IP has been deleted but, we can't fetch the value
350 # and the rrecord may still have other answers
351 if op == "DELETE-IP":
352 updates.append(
353 DynamicDNSUpdate.create_from_trigger(
354 operation="DELETE",
355 zone=zone,
356 name=name,
357 rectype=rectype,
358 )
359 )
360 if rectype == "A":
361 updates.append(
362 DynamicDNSUpdate.create_from_trigger(
363 operation="DELETE",
364 zone=zone,
365 name=name,
366 rectype="AAAA",
367 )
368 )
369 resource = DNSResource.objects.get(
370 name=update_list[2], domain__name=zone
371 )
372 updates += [
373 DynamicDNSUpdate.create_from_trigger(
374 operation="INSERT",
375 zone=zone,
376 name=name,
377 rectype=rectype,
378 ttl=int(resource.address_ttl)
379 if resource.address_ttl
380 else None,
381 answer=ip.ip,
382 )
383 for ip in resource.ip_addresses.all()
384 ]
385
386 elif len(update_list) > 4: # has an answer
387 updates.append(
388 DynamicDNSUpdate.create_from_trigger(
389 operation=op,
390 zone=zone,
391 name=name,
392 rectype=rectype,
393 answer=update_list[-1],
394 )
395 )
396 else:
397 updates.append(
398 DynamicDNSUpdate.create_from_trigger(
399 operation=op,
400 zone=zone,
401 name=name,
402 rectype=rectype,
403 )
404 )
405 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..69eae10 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,21 @@ 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 if message == "":
181 return
182
183 (new_updates, need_reload) = process_dns_update_notify(message)
184
185 self._dns_requires_full_reload = (
186 self._dns_requires_full_reload or need_reload
187 )
188 self._dns_updates += new_updates
189
167 def startProcessing(self):190 def startProcessing(self):
168 """Start the process looping call."""191 """Start the process looping call."""
169 if not self.processing.running:192 if not self.processing.running:
@@ -193,10 +216,22 @@ class RegionControllerService(Service):
193 if delay:216 if delay:
194 return pause(delay)217 return pause(delay)
195218
219 def _clear_dynamic_dns_updates(d):
220 self._dns_updates = []
221 self._dns_requires_full_reload = False
222 return d
223
196 defers = []224 defers = []
197 if self.needsDNSUpdate:225 if self.needsDNSUpdate:
198 self.needsDNSUpdate = False226 self.needsDNSUpdate = False
199 d = deferToDatabase(transactional(dns_update_all_zones))227 d = deferToDatabase(
228 transactional(
229 dns_update_all_zones,
230 ),
231 dynamic_updates=self._dns_updates,
232 requires_reload=self._dns_requires_full_reload,
233 )
234 d.addCallback(_clear_dynamic_dns_updates)
200 d.addCallback(self._checkSerial)235 d.addCallback(self._checkSerial)
201 d.addCallback(self._logDNSReload)236 d.addCallback(self._logDNSReload)
202 # Order here matters, first needsDNSUpdate is set then pass the237 # 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..a18fe8a 100644
--- a/src/maasserver/triggers/system.py
+++ b/src/maasserver/triggers/system.py
@@ -1901,6 +1901,159 @@ 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 ASSERT TG_WHEN = 'AFTER', 'May only run as an AFTER trigger';
1919 ASSERT TG_LEVEL <> 'STATEMENT', 'Should not be used as a STATEMENT level trigger', TG_NAME;
1920 IF (TG_OP = 'INSERT' AND TG_LEVEL = 'ROW') THEN
1921 SELECT host(ip) INTO ip_addr FROM maasserver_staticipaddress WHERE id=NEW.staticipaddress_id;
1922 SELECT name, domain_id, COALESCE(address_ttl, 0) INTO rname, rdomain_id, ttl FROM maasserver_dnsresource WHERE id=NEW.dnsresource_id;
1923 SELECT name INTO domain FROM maasserver_domain WHERE id=rdomain_id;
1924 PERFORM pg_notify('sys_dns_updates', 'INSERT ' || domain || ' ' || rname || ' A ' || ttl || ' ' || ip_addr);
1925 ELSIF (TG_OP = 'DELETE' AND TG_LEVEl = 'ROW') THEN
1926 IF EXISTS(SELECT id FROM maasserver_dnsresource WHERE id=OLD.dnsresource_id) THEN
1927 IF EXISTS(SELECT id FROM maasserver_staticipaddress WHERE id=OLD.staticipaddress_id) THEN
1928 SELECT host(ip) INTO ip_addr FROM maasserver_staticipaddress WHERE id=OLD.staticipaddress_id;
1929 SELECT name, domain_id INTO rname, rdomain_id FROM maasserver_dnsresource WHERE id=OLD.dnsresource_id;
1930 SELECT name INTO domain FROM maasserver_domain WHERE id=rdomain_id;
1931 PERFORM pg_notify('sys_dns_updates', 'DELETE ' || domain || ' ' || rname || ' A ' || ip_addr);
1932 ELSE
1933 SELECT name, domain_id INTO rname, rdomain_id FROM maasserver_dnsresource WHERE id=NEW.dnsresource_id;
1934 SELECT name INTO domain FROM maasserver_domain WHERE id=rdomain_id;
1935 PERFORM pg_notify('sys_dns_updates', 'DELETE-IP ' || domain || ' ' || rname || ' A');
1936 PERFORM pg_notify('sys_dns_updates', 'DELETE-IP ' || domain || ' ' || rname || ' AAAA');
1937 END IF;
1938 END IF;
1939 END IF;
1940 RETURN NULL;
1941 END;
1942 $$ LANGUAGE plpgsql;
1943 """
1944 )
1945
1946
1947# handles when ttl or name is modified or resource is deleted,
1948# DNS_DYNAMIC_UPDATE_DNSRESOURCE_STATICIPADDRESS covers the case of insert
1949def render_dns_dynamic_update_dnsresource_procedure(op):
1950 return dedent(
1951 f"""\
1952 CREATE OR REPLACE FUNCTION sys_dns_updates_maasserver_dnsresource_{op}()
1953 RETURNS trigger as $$
1954 DECLARE
1955 ip_addr text;
1956 ips text[];
1957 domain text;
1958 BEGIN
1959 ASSERT TG_WHEN = 'AFTER', 'May only run as an AFTER trigger';
1960 ASSERT TG_LEVEL <> 'STATEMENT', 'Should not be used as a STATEMENT level trigger', TG_NAME;
1961 IF (TG_OP = 'UPDATE' AND TG_LEVEL = 'ROW') THEN
1962 IF NEW IS DISTINCT FROM OLD THEN
1963 SELECT array_agg(host(ip)) INTO ips FROM maasserver_dnsresource_ip_addresses m
1964 INNER JOIN maasserver_staticipaddress ON maasserver_staticipaddress.id=m.staticipaddress_id
1965 WHERE dnsresource_id=NEW.id;
1966 SELECT name INTO domain FROM maasserver_domain WHERE id=NEW.domain_id;
1967 IF array_length(ips, 1) > 0 THEN
1968 FOREACH ip_addr IN ARRAY ips
1969 LOOP
1970 IF OLD.name <> NEW.name THEN
1971 PERFORM pg_notify('sys_dns_updates', 'DELETE ' || domain || ' ' || OLD.name || ' A ' || ip_addr);
1972 PERFORM pg_notify('sys_dns_updates', 'INSERT ' || domain || ' ' || NEW.name || ' A ' || NEW.address_ttl || ' ' || ip_addr);
1973 ELSE
1974 PERFORM pg_notify('sys_dns_updates', 'UPDATE ' || domain || ' ' || NEW.name || ' A ' || NEW.address_ttl || ' ' || ip_addr);
1975 END IF;
1976 END LOOP;
1977 END IF;
1978 ELSE
1979 RETURN NULL;
1980 END IF;
1981 ELSIF (TG_OP = 'DELETE' AND TG_LEVEL = 'ROW') THEN
1982 SELECT name INTO domain FROM maasserver_domain WHERE id=NEW.domain_id;
1983 PERFORM pg_notify('sys_dns_updates', 'DELETE ' || domain || ' ' || OLD.name || ' A');
1984 END IF;
1985 RETURN NULL;
1986 END;
1987 $$ LANGUAGE plpgsql;
1988 """
1989 )
1990
1991
1992def render_dns_dynamic_update_dnsdata_procedure(op):
1993 return dedent(
1994 f"""\
1995 CREATE OR REPLACE FUNCTION sys_dns_updates_maasserver_dnsdata_{op}()
1996 RETURNS trigger as $$
1997 DECLARE
1998 rname text;
1999 rdomain_id bigint;
2000 domain text;
2001 ttl int;
2002 BEGIN
2003 ASSERT TG_WHEN = 'AFTER', 'May only run as an AFTER trigger';
2004 ASSERT TG_LEVEL <> 'STATEMENT', 'Should not be used as a STATEMENT level trigger', TG_NAME;
2005 IF (TG_OP = 'UPDATE' AND TG_LEVEL = 'ROW') THEN
2006 IF NEW IS DISTINCT FROM OLD THEN
2007 PERFORM pg_notify('sys_dns_updates', 'UPDATE-DATA ' || NEW.id);
2008 ELSE
2009 RETURN NULL;
2010 END IF;
2011 ELSIF (TG_OP = 'INSERT' AND TG_LEVEL = 'ROW') THEN
2012 PERFORM pg_notify('sys_dns_updates', 'INSERT-DATA ' || NEW.id);
2013 ELSIF (TG_OP = 'DELETE' AND TG_LEVEL = 'ROW') THEN
2014 SELECT name, domain_id INTO rname, rdomain_id from maasserver_dnsresource WHERE id=OLD.dnsresource_id;
2015 SELECT name INTO domain FROM maasserver_domain WHERE id=rdomain_id;
2016 PERFORM pg_notify('sys_dns_updates', 'DELETE ' || domain || ' ' || rname || ' ' || OLD.rrtype);
2017 END IF;
2018 RETURN NULL;
2019 END;
2020 $$ LANGUAGE plpgsql;
2021 """
2022 )
2023
2024
2025def render_dns_dynamic_update_domain_procedure(op):
2026 return dedent(
2027 f"""\
2028 CREATE OR REPLACE FUNCTION sys_dns_updates_maasserver_domain_{op}()
2029 RETURNS trigger as $$
2030 BEGIN
2031 ASSERT TG_WHEN = 'AFTER', 'May only run as an AFTER trigger';
2032 ASSERT TG_LEVEL <> 'STATEMENT', 'Should not be used as a STATEMENT level trigger', TG_NAME;
2033 PERFORM pg_notify('sys_dns_updates', 'RELOAD');
2034 RETURN NULL;
2035 END;
2036 $$ LANGUAGE plpgsql;
2037 """
2038 )
2039
2040
2041def render_dns_dynamic_update_subnet_procedure(op):
2042 return dedent(
2043 f"""\
2044 CREATE OR REPLACE FUNCTION sys_dns_updates_maasserver_subnet_{op}()
2045 RETURNS trigger as $$
2046 BEGIN
2047 ASSERT TG_WHEN = 'AFTER', 'May only run as an AFTER trigger';
2048 ASSERT TG_LEVEL <> 'STATEMENT', 'Should not be used as a STATEMENT level trigger', TG_NAME;
2049 PERFORM pg_notify('sys_dns_updates', 'RELOAD');
2050 RETURN NULL;
2051 END;
2052 $$ LANGUAGE plpgsql;
2053 """
2054 )
2055
2056
1904def render_sys_proxy_procedure(proc_name, on_delete=False):2057def render_sys_proxy_procedure(proc_name, on_delete=False):
1905 """Render a database procedure with name `proc_name` that notifies that a2058 """Render a database procedure with name `proc_name` that notifies that a
1906 proxy update is needed.2059 proxy update is needed.
@@ -2163,3 +2316,90 @@ def register_system_triggers():
2163 register_trigger(2316 register_trigger(
2164 "maasserver_resourcepool", "sys_rbac_rpool_delete", "delete"2317 "maasserver_resourcepool", "sys_rbac_rpool_delete", "delete"
2165 )2318 )
2319
2320 register_procedure(
2321 render_dns_dynamic_update_dnsresource_ip_addresses_procedure("insert")
2322 )
2323 register_trigger(
2324 "maasserver_dnsresource_ip_addresses",
2325 "sys_dns_updates_dns_ip_insert",
2326 "insert",
2327 )
2328 register_procedure(
2329 render_dns_dynamic_update_dnsresource_ip_addresses_procedure("delete")
2330 )
2331 register_trigger(
2332 "maasserver_dnsresource_ip_addresses",
2333 "sys_dns_updates_dns_ip_delete",
2334 "delete",
2335 )
2336 register_procedure(
2337 render_dns_dynamic_update_dnsresource_procedure("update")
2338 )
2339 register_trigger(
2340 "maasserver_dnsresource",
2341 "sys_dns_updates_maasserver_dnsresource_update",
2342 "update",
2343 )
2344 register_procedure(
2345 render_dns_dynamic_update_dnsresource_procedure("delete")
2346 )
2347 register_trigger(
2348 "maasserver_dnsresource",
2349 "sys_dns_updates_maasserver_dnsresource_delete",
2350 "delete",
2351 )
2352 register_procedure(render_dns_dynamic_update_dnsdata_procedure("insert"))
2353 register_trigger(
2354 "maasserver_dnsdata",
2355 "sys_dns_updates_maasserver_dnsdata_insert",
2356 "update",
2357 )
2358 register_procedure(render_dns_dynamic_update_dnsdata_procedure("update"))
2359 register_trigger(
2360 "maasserver_dnsdata",
2361 "sys_dns_updates_maasserver_dnsdata_update",
2362 "update",
2363 )
2364 register_procedure(render_dns_dynamic_update_dnsdata_procedure("delete"))
2365 register_trigger(
2366 "maasserver_dnsdata",
2367 "sys_dns_updates_maasserver_dnsdata_delete",
2368 "delete",
2369 )
2370 register_procedure(render_dns_dynamic_update_domain_procedure("insert"))
2371 register_trigger(
2372 "maasserver_domain",
2373 "sys_dns_updates_maasserver_domain_insert",
2374 "insert",
2375 )
2376 register_procedure(render_dns_dynamic_update_domain_procedure("update"))
2377 register_trigger(
2378 "maasserver_domain",
2379 "sys_dns_updates_maasserver_domain_update",
2380 "update",
2381 )
2382 register_procedure(render_dns_dynamic_update_domain_procedure("delete"))
2383 register_trigger(
2384 "maasserver_domain",
2385 "sys_dns_updates_maasserver_domain_delete",
2386 "delete",
2387 )
2388 register_procedure(render_dns_dynamic_update_subnet_procedure("insert"))
2389 register_trigger(
2390 "maasserver_subnet",
2391 "sys_dns_updates_maasserver_subnet_insert",
2392 "insert",
2393 )
2394 register_procedure(render_dns_dynamic_update_subnet_procedure("update"))
2395 register_trigger(
2396 "maasserver_subnet",
2397 "sys_dns_updates_maasserver_subnet_update",
2398 "update",
2399 )
2400 register_procedure(render_dns_dynamic_update_subnet_procedure("delete"))
2401 register_trigger(
2402 "maasserver_subnet",
2403 "sys_dns_updates_maasserver_subnet_delete",
2404 "delete",
2405 )
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..d5222b4 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,269 @@ 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 self.assertEqual(
256 msg,
257 f"UPDATE-DATA {dnsdata.id}",
258 )
259 finally:
260 self.stop_reading()
261 yield self.postgres_listener_service.stopService()
262
263 @wait_for_reactor
264 @inlineCallbacks
265 def test_dns_dynamic_update_dnsdata_insert(self):
266 listener = self.make_listener_without_delay()
267 yield self.set_service(listener)
268 domain = yield deferToDatabase(self.create_domain)
269 rec = yield deferToDatabase(
270 self.create_dnsresource, {"domain": domain}
271 )
272 yield deferToDatabase(
273 self.register_trigger,
274 "maasserver_dnsdata",
275 "sys_dns_updates",
276 ops=("insert",),
277 )
278 self.start_reading()
279 try:
280 dnsdata = yield deferToDatabase(
281 self.create_dnsdata,
282 params={
283 "dnsresource": rec,
284 "rrtype": "TXT",
285 "rrdata": factory.make_name(),
286 },
287 )
288 msg = yield self.get_notify("sys_dns_updates")
289 self.assertEqual(
290 msg,
291 f"INSERT-DATA {dnsdata.id}",
292 )
293 finally:
294 self.stop_reading()
295 yield self.postgres_listener_service.stopService()
296
297 @wait_for_reactor
298 @inlineCallbacks
299 def test_dns_dynamic_update_dnsdata_delete(self):
300 listener = self.make_listener_without_delay()
301 yield self.set_service(listener)
302 domain = yield deferToDatabase(self.create_domain)
303 rec = yield deferToDatabase(
304 self.create_dnsresource, params={"domain": domain}
305 )
306 dnsdata = yield deferToDatabase(
307 self.create_dnsdata,
308 params={
309 "dnsresource": rec,
310 "rrtype": "TXT",
311 "rrdata": factory.make_name(),
312 },
313 )
314 yield deferToDatabase(
315 self.register_trigger,
316 "maasserver_dnsdata",
317 "sys_dns_updates",
318 ops=("delete",),
319 )
320 self.start_reading()
321 try:
322 yield deferToDatabase(dnsdata.delete)
323 msg = yield self.get_notify("sys_dns_updates")
324 self.assertEqual(
325 msg, f"DELETE {domain.name} {rec.name} {dnsdata.rrtype}"
326 )
327 finally:
328 self.stop_reading()
329 yield self.postgres_listener_service.stopService()
330
331 @wait_for_reactor
332 @inlineCallbacks
333 def test_dns_dynamic_update_domain_reload(self):
334 listener = self.make_listener_without_delay()
335 yield self.set_service(listener)
336 yield deferToDatabase(
337 self.register_trigger,
338 "maasserver_domain",
339 "sys_dns_updates",
340 ops=("insert",),
341 )
342 self.start_reading()
343 try:
344 yield deferToDatabase(self.create_domain)
345 msg = yield self.get_notify("sys_dns_updates")
346 self.assertEqual(msg, "RELOAD")
347 finally:
348 self.stop_reading()
349 yield self.postgres_listener_service.stopService()
350
351 @wait_for_reactor
352 @inlineCallbacks
353 def test_dns_dynamic_update_subnet_reload(self):
354 listener = self.make_listener_without_delay()
355 yield self.set_service(listener)
356 yield deferToDatabase(
357 self.register_trigger,
358 "maasserver_subnet",
359 "sys_dns_updates",
360 ops=("insert",),
361 )
362 self.start_reading()
363 try:
364 yield deferToDatabase(self.create_subnet)
365 msg = yield self.get_notify("sys_dns_updates")
366 self.assertEqual(msg, "RELOAD")
367 finally:
368 self.stop_reading()
369 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..5673b4a 100644
--- a/src/provisioningserver/dns/config.py
+++ b/src/provisioningserver/dns/config.py
@@ -6,12 +6,17 @@
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
12import grp
11import os13import os
12import os.path14import os.path
13import re15import re
14import sys16import sys
17from typing import Optional
18
19from netaddr import AddrFormatError, IPAddress
1520
16from provisioningserver.logger import get_maas_logger21from provisioningserver.logger import get_maas_logger
17from provisioningserver.utils import load_template, locate_config22from provisioningserver.utils import load_template, locate_config
@@ -26,6 +31,63 @@ MAAS_NAMED_CONF_NAME = "named.conf.maas"
26MAAS_NAMED_CONF_OPTIONS_INSIDE_NAME = "named.conf.options.inside.maas"31MAAS_NAMED_CONF_OPTIONS_INSIDE_NAME = "named.conf.options.inside.maas"
27MAAS_NAMED_RNDC_CONF_NAME = "named.conf.rndc.maas"32MAAS_NAMED_RNDC_CONF_NAME = "named.conf.rndc.maas"
28MAAS_RNDC_CONF_NAME = "rndc.conf.maas"33MAAS_RNDC_CONF_NAME = "rndc.conf.maas"
34MAAS_NSUPDATE_KEY_NAME = "keys.conf.maas"
35MAAS_ZONE_FILE_DIR = "/var/lib/bind/maas"
36MAAS_ZONE_FILE_GROUP = "bind"
37
38
39@dataclass
40class DynamicDNSUpdate:
41 operation: str
42 name: str
43 zone: str
44 rectype: str
45 ttl: Optional[int] = None
46 subnet: Optional[str] = None # for reverse updates
47 answer: Optional[str] = None
48
49 @classmethod
50 def _is_ip(cls, answer):
51 if not answer:
52 return False
53 try:
54 IPAddress(answer)
55 except AddrFormatError:
56 return False
57 else:
58 return True
59
60 @classmethod
61 def create_from_trigger(cls, **kwargs):
62 answer = kwargs.get("answer")
63 rectype = kwargs.pop("rectype")
64 if answer:
65 del kwargs["answer"]
66 # the DB trigger is unable to figure out if an IP is v6, so we do it here instead
67 if cls._is_ip(answer):
68 ip = IPAddress(answer)
69 if ip.version == 6:
70 rectype = "AAAA"
71 return cls(answer=answer, rectype=rectype, **kwargs)
72
73 @classmethod
74 def as_reverse_record_update(cls, fwd_update, subnet):
75 if not fwd_update.answer_is_ip:
76 return None
77 ip = IPAddress(fwd_update.answer)
78 return cls(
79 operation=fwd_update.operation,
80 name=ip.reverse_dns,
81 zone=fwd_update.zone,
82 subnet=subnet,
83 ttl=fwd_update.ttl,
84 answer=fwd_update.name,
85 rectype="PTR",
86 )
87
88 @property
89 def answer_is_ip(self):
90 return DynamicDNSUpdate._is_ip(self.answer)
2991
3092
31def get_dns_config_dir():93def get_dns_config_dir():
@@ -40,6 +102,19 @@ def get_dns_config_dir():
40 return setting102 return setting
41103
42104
105def get_zone_file_config_dir():
106 """
107 Location of MAAS' zone files, separate from config files
108 so that bind can write to the location as well
109 """
110 setting = os.getenv("MAAS_ZONE_FILE_CONFIG_DIR", MAAS_ZONE_FILE_DIR)
111 if isinstance(setting, bytes):
112 fsenc = sys.getfilesystemencoding()
113 return setting.decode(fsenc)
114 else:
115 return setting
116
117
43def get_bind_config_dir():118def get_bind_config_dir():
44 """Location of bind configuration files."""119 """Location of bind configuration files."""
45 setting = os.getenv(120 setting = os.getenv(
@@ -155,6 +230,30 @@ def get_rndc_conf_path():
155 return compose_config_path(MAAS_RNDC_CONF_NAME)230 return compose_config_path(MAAS_RNDC_CONF_NAME)
156231
157232
233def get_nsupdate_key_path():
234 return compose_config_path(MAAS_NSUPDATE_KEY_NAME)
235
236
237def set_up_nsupdate_key():
238 tsig = call_and_check(["tsig-keygen", "-a", "HMAC-SHA512", "maas."])
239 atomic_write(tsig, get_nsupdate_key_path(), overwrite=True, mode=0o644)
240
241
242def set_up_zone_file_dir():
243 p = get_zone_file_config_dir()
244 if not os.path.exists(p):
245 os.mkdir(p)
246
247 uid = os.getuid()
248 gid = 0
249 group = grp.getgrnam(MAAS_ZONE_FILE_GROUP)
250 if group:
251 gid = group.gr_gid
252
253 os.chown(p, uid, gid)
254 os.chmod(p, 0o775)
255
256
158def set_up_rndc():257def set_up_rndc():
159 """Writes out the two files needed to enable MAAS to use rndc commands:258 """Writes out the two files needed to enable MAAS to use rndc commands:
160 MAAS_RNDC_CONF_NAME and MAAS_NAMED_RNDC_CONF_NAME.259 MAAS_RNDC_CONF_NAME and MAAS_NAMED_RNDC_CONF_NAME.
@@ -232,12 +331,16 @@ def set_up_options_conf(overwrite=True, **kwargs):
232331
233332
234def compose_config_path(filename):333def compose_config_path(filename):
235 """Return the full path for a DNS config or zone file."""334 """Return the full path for a DNS config"""
236 return os.path.join(get_dns_config_dir(), filename)335 return os.path.join(get_dns_config_dir(), filename)
237336
238337
338def compose_zone_file_config_path(filename):
339 return os.path.join(get_zone_file_config_dir(), filename)
340
341
239def compose_bind_config_path(filename):342def compose_bind_config_path(filename):
240 """Return the full path for a DNS config or zone file."""343 """Return the full path for a DNS config"""
241 return os.path.join(get_bind_config_dir(), filename)344 return os.path.join(get_bind_config_dir(), filename)
242345
243346
@@ -309,6 +412,7 @@ class DNSConfig:
309 "forwarded_zones": self.forwarded_zones,412 "forwarded_zones": self.forwarded_zones,
310 "DNS_CONFIG_DIR": get_dns_config_dir(),413 "DNS_CONFIG_DIR": get_dns_config_dir(),
311 "named_rndc_conf_path": get_named_rndc_conf_path(),414 "named_rndc_conf_path": get_named_rndc_conf_path(),
415 "nsupdate_keys_conf_path": get_nsupdate_key_path(),
312 "trusted_networks": trusted_networks,416 "trusted_networks": trusted_networks,
313 "modified": str(datetime.today()),417 "modified": str(datetime.today()),
314 }418 }
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