Merge ~cgrabowski/maas:fix_overlapping_subnets_bind_misconfigure into maas:master
- Git
- lp:~cgrabowski/maas
- fix_overlapping_subnets_bind_misconfigure
- Merge into master
Status: | Merged |
---|---|
Approved by: | Christian Grabowski |
Approved revision: | 40348f8e30e9ffebe508d36d3d64ce712b60c00a |
Merge reported by: | MAAS Lander |
Merged at revision: | not available |
Proposed branch: | ~cgrabowski/maas:fix_overlapping_subnets_bind_misconfigure |
Merge into: | maas:master |
Diff against target: |
1493 lines (+876/-360) 5 files modified
src/maasserver/dns/tests/test_zonegenerator.py (+570/-55) src/maasserver/dns/zonegenerator.py (+259/-96) src/maastesting/noseplug.py (+3/-1) src/provisioningserver/dns/tests/test_zoneconfig.py (+26/-152) src/provisioningserver/dns/zoneconfig.py (+18/-56) |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
MAAS Lander | Approve | ||
Jack Lloyd-Walters | Approve | ||
Review via email: mp+457267@code.launchpad.net |
Commit message
fix: overlapping subnets no longer cause BIND to fail configuring
Description of the change
MAAS Lander (maas-lander) wrote : | # |
UNIT TESTS
-b fix_overlapping
STATUS: FAILED
LOG: http://
COMMIT: 8bff38ac17e03b0
MAAS Lander (maas-lander) wrote : | # |
UNIT TESTS
-b fix_overlapping
STATUS: FAILED
LOG: http://
COMMIT: 9fb26758287ea50
Christian Grabowski (cgrabowski) wrote : | # |
jenkins: !test
MAAS Lander (maas-lander) wrote : | # |
UNIT TESTS
-b fix_overlapping
STATUS: FAILED
LOG: http://
COMMIT: 9fb26758287ea50
Christian Grabowski (cgrabowski) wrote : | # |
jenkins: !test
MAAS Lander (maas-lander) wrote : | # |
UNIT TESTS
-b fix_overlapping
STATUS: SUCCESS
COMMIT: 9fb26758287ea50
Jack Lloyd-Walters (lloydwaltersj) wrote : | # |
Looks good, a few inline nits about changing to f-strings, but nothing that affects functionality.
Jack Lloyd-Walters (lloydwaltersj) : | # |
Christian Grabowski (cgrabowski) : | # |
- d68d1b0... by Christian Grabowski
-
fix nits
MAAS Lander (maas-lander) wrote : | # |
UNIT TESTS
-b fix_overlapping
STATUS: FAILED
LOG: http://
COMMIT: 2fc97d3aa94813e
MAAS Lander (maas-lander) wrote : | # |
UNIT TESTS
-b fix_overlapping
STATUS: FAILED
LOG: http://
COMMIT: d68d1b0b3897a05
- 40348f8... by Christian Grabowski
-
fix flaky test
MAAS Lander (maas-lander) wrote : | # |
UNIT TESTS
-b fix_overlapping
STATUS: FAILED
LOG: http://
COMMIT: 654834eb173923c
MAAS Lander (maas-lander) wrote : | # |
UNIT TESTS
-b fix_overlapping
STATUS: SUCCESS
COMMIT: 40348f8e30e9ffe
Preview Diff
1 | diff --git a/src/maasserver/dns/tests/test_zonegenerator.py b/src/maasserver/dns/tests/test_zonegenerator.py | |||
2 | index d213c1a..1555ae6 100644 | |||
3 | --- a/src/maasserver/dns/tests/test_zonegenerator.py | |||
4 | +++ b/src/maasserver/dns/tests/test_zonegenerator.py | |||
5 | @@ -1,6 +1,7 @@ | |||
6 | 1 | # Copyright 2014-2022 Canonical Ltd. This software is licensed under the | 1 | # Copyright 2014-2022 Canonical Ltd. This software is licensed under the |
7 | 2 | # GNU Affero General Public License version 3 (see the file LICENSE). | 2 | # GNU Affero General Public License version 3 (see the file LICENSE). |
8 | 3 | 3 | ||
9 | 4 | import os | ||
10 | 4 | import random | 5 | import random |
11 | 5 | import socket | 6 | import socket |
12 | 6 | from unittest.mock import ANY, call, Mock | 7 | from unittest.mock import ANY, call, Mock |
13 | @@ -37,7 +38,10 @@ from maasserver.enum import IPADDRESS_TYPE, NODE_STATUS, RDNS_MODE | |||
14 | 37 | from maasserver.exceptions import UnresolvableHost | 38 | from maasserver.exceptions import UnresolvableHost |
15 | 38 | from maasserver.models import Config, Domain, Subnet | 39 | from maasserver.models import Config, Domain, Subnet |
16 | 39 | from maasserver.models.dnsdata import HostnameRRsetMapping | 40 | from maasserver.models.dnsdata import HostnameRRsetMapping |
18 | 40 | from maasserver.models.staticipaddress import HostnameIPMapping | 41 | from maasserver.models.staticipaddress import ( |
19 | 42 | HostnameIPMapping, | ||
20 | 43 | StaticIPAddress, | ||
21 | 44 | ) | ||
22 | 41 | from maasserver.testing.config import RegionConfigurationFixture | 45 | from maasserver.testing.config import RegionConfigurationFixture |
23 | 42 | from maasserver.testing.factory import factory | 46 | from maasserver.testing.factory import factory |
24 | 43 | from maasserver.testing.testcase import ( | 47 | from maasserver.testing.testcase import ( |
25 | @@ -49,6 +53,7 @@ from maastesting.factory import factory as maastesting_factory | |||
26 | 49 | from maastesting.fakemethod import FakeMethod | 53 | from maastesting.fakemethod import FakeMethod |
27 | 50 | from maastesting.matchers import MockAnyCall, MockCalledOnceWith, MockNotCalled | 54 | from maastesting.matchers import MockAnyCall, MockCalledOnceWith, MockNotCalled |
28 | 51 | from provisioningserver.dns.config import DynamicDNSUpdate | 55 | from provisioningserver.dns.config import DynamicDNSUpdate |
29 | 56 | from provisioningserver.dns.testing import patch_zone_file_config_path | ||
30 | 52 | from provisioningserver.dns.zoneconfig import ( | 57 | from provisioningserver.dns.zoneconfig import ( |
31 | 53 | DNSForwardZoneConfig, | 58 | DNSForwardZoneConfig, |
32 | 54 | DNSReverseZoneConfig, | 59 | DNSReverseZoneConfig, |
33 | @@ -414,7 +419,7 @@ class TestZoneGenerator(MAASServerTestCase): | |||
34 | 414 | ], # purposely out of order to assert subnets are being sorted | 419 | ], # purposely out of order to assert subnets are being sorted |
35 | 415 | serial=random.randint(0, 65535), | 420 | serial=random.randint(0, 65535), |
36 | 416 | ).as_list() | 421 | ).as_list() |
38 | 417 | self.assertEqual(len(zones), 7) | 422 | self.assertEqual(len(zones), 13) # 5 /24s and 8 from the /21 |
39 | 418 | expected_domains = [ | 423 | expected_domains = [ |
40 | 419 | "overlap", | 424 | "overlap", |
41 | 420 | "36.232.10.in-addr.arpa", | 425 | "36.232.10.in-addr.arpa", |
42 | @@ -728,14 +733,15 @@ class TestZoneGenerator(MAASServerTestCase): | |||
43 | 728 | def test_supernet_inherits_rfc2317_net(self): | 733 | def test_supernet_inherits_rfc2317_net(self): |
44 | 729 | domain = Domain.objects.get_default_domain() | 734 | domain = Domain.objects.get_default_domain() |
45 | 730 | subnet1 = factory.make_Subnet(host_bits=2) | 735 | subnet1 = factory.make_Subnet(host_bits=2) |
48 | 731 | net = IPNetwork(subnet1.cidr) | 736 | net1 = IPNetwork(subnet1.cidr) |
49 | 732 | if net.version == 6: | 737 | if net1.version == 6: |
50 | 733 | prefixlen = random.randint(121, 124) | 738 | prefixlen = random.randint(121, 124) |
51 | 734 | else: | 739 | else: |
52 | 735 | prefixlen = random.randint(22, 24) | 740 | prefixlen = random.randint(22, 24) |
55 | 736 | parent = IPNetwork("%s/%d" % (net.network, prefixlen)) | 741 | parent = IPNetwork(f"{net1.network}/{prefixlen:d}") |
56 | 737 | parent = IPNetwork("%s/%d" % (parent.network, prefixlen)) | 742 | parent = IPNetwork(f"{parent.network}/{prefixlen:d}") |
57 | 738 | subnet2 = factory.make_Subnet(cidr=parent) | 743 | subnet2 = factory.make_Subnet(cidr=parent) |
58 | 744 | net2 = IPNetwork(subnet2.cidr) | ||
59 | 739 | node = factory.make_Node_with_Interface_on_Subnet( | 745 | node = factory.make_Node_with_Interface_on_Subnet( |
60 | 740 | subnet=subnet1, | 746 | subnet=subnet1, |
61 | 741 | vlan=subnet1.vlan, | 747 | vlan=subnet1.vlan, |
62 | @@ -746,56 +752,51 @@ class TestZoneGenerator(MAASServerTestCase): | |||
63 | 746 | factory.make_StaticIPAddress(interface=boot_iface, subnet=subnet1) | 752 | factory.make_StaticIPAddress(interface=boot_iface, subnet=subnet1) |
64 | 747 | default_ttl = random.randint(10, 300) | 753 | default_ttl = random.randint(10, 300) |
65 | 748 | Config.objects.set_config("default_dns_ttl", default_ttl) | 754 | Config.objects.set_config("default_dns_ttl", default_ttl) |
66 | 755 | serial = random.randint(0, 65535) | ||
67 | 749 | zones = ZoneGenerator( | 756 | zones = ZoneGenerator( |
68 | 750 | domain, | 757 | domain, |
69 | 751 | [subnet1, subnet2], | 758 | [subnet1, subnet2], |
70 | 752 | default_ttl=default_ttl, | 759 | default_ttl=default_ttl, |
72 | 753 | serial=random.randint(0, 65535), | 760 | serial=serial, |
73 | 754 | ).as_list() | 761 | ).as_list() |
80 | 755 | self.assertThat( | 762 | expected = [ |
81 | 756 | zones, | 763 | DNSForwardZoneConfig( |
82 | 757 | MatchesSetwise( | 764 | domain.name, |
83 | 758 | forward_zone(domain.name), | 765 | serial=serial, |
84 | 759 | reverse_zone(domain.name, subnet1.cidr), | 766 | default_ttl=default_ttl, |
79 | 760 | reverse_zone(domain.name, subnet2.cidr), | ||
85 | 761 | ), | 767 | ), |
86 | 762 | ) | ||
87 | 763 | self.assertEqual(set(), zones[1]._rfc2317_ranges) | ||
88 | 764 | self.assertEqual({net}, zones[2]._rfc2317_ranges) | ||
89 | 765 | |||
90 | 766 | def test_two_managed_interfaces_yields_one_forward_two_reverse_zones(self): | ||
91 | 767 | default_domain = Domain.objects.get_default_domain().name | ||
92 | 768 | domain = factory.make_Domain() | ||
93 | 769 | subnet1 = factory.make_Subnet() | ||
94 | 770 | subnet2 = factory.make_Subnet() | ||
95 | 771 | expected_zones = [ | ||
96 | 772 | forward_zone(domain.name), | ||
97 | 773 | reverse_zone(default_domain, subnet1.cidr), | ||
98 | 774 | reverse_zone(default_domain, subnet2.cidr), | ||
99 | 775 | ] | 768 | ] |
112 | 776 | subnets = Subnet.objects.all() | 769 | for net in [net1, net2]: |
113 | 777 | 770 | if ( | |
114 | 778 | expected_zones = ( | 771 | net.version == 6 and net.prefixlen < 124 |
115 | 779 | [forward_zone(domain.name)] | 772 | ) or net.prefixlen < 24: |
116 | 780 | + [ | 773 | for network in ZoneGenerator._split_large_subnet(net2): |
117 | 781 | reverse_zone(default_domain, subnet.get_ipnetwork()) | 774 | expected.append( |
118 | 782 | for subnet in subnets | 775 | DNSReverseZoneConfig(domain.name, network=network) |
119 | 783 | ] | 776 | ) |
120 | 784 | + [ | 777 | elif net.version == 6 and net.prefixlen > 124: |
121 | 785 | reverse_zone( | 778 | expected.append( |
122 | 786 | default_domain, | 779 | DNSReverseZoneConfig( |
123 | 787 | self.rfc2317_network(subnet.get_ipnetwork()), | 780 | domain.name, network=IPNetwork(f"{net.network}/124") |
124 | 781 | ) | ||
125 | 788 | ) | 782 | ) |
135 | 789 | for subnet in subnets | 783 | expected.append(DNSReverseZoneConfig(domain.name, network=net)) |
136 | 790 | if self.rfc2317_network(subnet.get_ipnetwork()) is not None | 784 | elif net.version == 4 and net.prefixlen > 24: |
137 | 791 | ] | 785 | expected.append( |
138 | 792 | ) | 786 | DNSReverseZoneConfig( |
139 | 793 | self.assertThat( | 787 | domain.name, network=IPNetwork(f"{net.network}/24") |
140 | 794 | ZoneGenerator( | 788 | ) |
141 | 795 | domain, [subnet1, subnet2], serial=random.randint(0, 65535) | 789 | ) |
142 | 796 | ).as_list(), | 790 | expected.append(DNSReverseZoneConfig(domain.name, network=net)) |
143 | 797 | MatchesSetwise(*expected_zones), | 791 | else: |
144 | 792 | expected.append(DNSReverseZoneConfig(domain.name, network=net)) | ||
145 | 793 | self.assertCountEqual( | ||
146 | 794 | set([(zone.domain, zone._network) for zone in zones]), | ||
147 | 795 | set((e.domain, e._network) for e in expected), | ||
148 | 796 | f"{subnet1} {subnet2}", | ||
149 | 798 | ) | 797 | ) |
150 | 798 | self.assertEqual(set(), zones[1]._rfc2317_ranges) | ||
151 | 799 | self.assertEqual({net1}, zones[2]._rfc2317_ranges) | ||
152 | 799 | 800 | ||
153 | 800 | def test_with_many_yields_many_zones(self): | 801 | def test_with_many_yields_many_zones(self): |
154 | 801 | # This demonstrates ZoneGenerator in all-singing all-dancing mode. | 802 | # This demonstrates ZoneGenerator in all-singing all-dancing mode. |
155 | @@ -806,18 +807,29 @@ class TestZoneGenerator(MAASServerTestCase): | |||
156 | 806 | subnets = Subnet.objects.all() | 807 | subnets = Subnet.objects.all() |
157 | 807 | expected_zones = set() | 808 | expected_zones = set() |
158 | 808 | for domain in domains: | 809 | for domain in domains: |
160 | 809 | expected_zones.add(forward_zone(domain.name)) | 810 | expected_zones.add(DNSForwardZoneConfig(domain.name)) |
161 | 810 | for subnet in subnets: | 811 | for subnet in subnets: |
165 | 811 | expected_zones.add(reverse_zone(default_domain.name, subnet.cidr)) | 812 | networks = ZoneGenerator._split_large_subnet( |
166 | 812 | rfc2317_net = self.rfc2317_network(subnet.get_ipnetwork()) | 813 | IPNetwork(subnet.cidr) |
167 | 813 | if rfc2317_net is not None: | 814 | ) |
168 | 815 | for network in networks: | ||
169 | 814 | expected_zones.add( | 816 | expected_zones.add( |
171 | 815 | reverse_zone(default_domain.name, rfc2317_net.cidr) | 817 | DNSReverseZoneConfig(default_domain.name, network=network) |
172 | 816 | ) | 818 | ) |
173 | 819 | if rfc2317_net := self.rfc2317_network(network): | ||
174 | 820 | expected_zones.add( | ||
175 | 821 | DNSReverseZoneConfig( | ||
176 | 822 | default_domain.name, | ||
177 | 823 | network=IPNetwork(rfc2317_net.cidr), | ||
178 | 824 | ) | ||
179 | 825 | ) | ||
180 | 817 | actual_zones = ZoneGenerator( | 826 | actual_zones = ZoneGenerator( |
181 | 818 | domains, subnets, serial=random.randint(0, 65535) | 827 | domains, subnets, serial=random.randint(0, 65535) |
182 | 819 | ).as_list() | 828 | ).as_list() |
184 | 820 | self.assertThat(actual_zones, MatchesSetwise(*expected_zones)) | 829 | self.assertCountEqual( |
185 | 830 | [(zone.domain, zone._network) for zone in actual_zones], | ||
186 | 831 | [(zone.domain, zone._network) for zone in expected_zones], | ||
187 | 832 | ) | ||
188 | 821 | 833 | ||
189 | 822 | def test_zone_generator_handles_rdns_mode_equal_enabled(self): | 834 | def test_zone_generator_handles_rdns_mode_equal_enabled(self): |
190 | 823 | Domain.objects.get_or_create(name="one") | 835 | Domain.objects.get_or_create(name="one") |
191 | @@ -898,6 +910,359 @@ class TestZoneGenerator(MAASServerTestCase): | |||
192 | 898 | ), | 910 | ), |
193 | 899 | ) | 911 | ) |
194 | 900 | 912 | ||
195 | 913 | def test_configs_are_merged_when_overlapping(self): | ||
196 | 914 | self.patch(warn_loopback) | ||
197 | 915 | default_domain = Domain.objects.get_default_domain() | ||
198 | 916 | subnet1 = factory.make_Subnet(cidr="10.0.1.0/24") | ||
199 | 917 | subnet2 = factory.make_Subnet(cidr="10.0.0.0/21") | ||
200 | 918 | subnet1_ips = [ | ||
201 | 919 | factory.make_StaticIPAddress( | ||
202 | 920 | ip=factory.pick_ip_in_Subnet(subnet1), subnet=subnet1 | ||
203 | 921 | ) | ||
204 | 922 | for _ in range(3) | ||
205 | 923 | ] | ||
206 | 924 | subnet2_ips = [ | ||
207 | 925 | factory.make_StaticIPAddress( | ||
208 | 926 | ip=factory.pick_ip_in_Subnet(subnet2), subnet=subnet2 | ||
209 | 927 | ) | ||
210 | 928 | for _ in range(3) | ||
211 | 929 | ] | ||
212 | 930 | subnet1_records = [ | ||
213 | 931 | factory.make_DNSResource(domain=default_domain, ip_addresses=[ip]) | ||
214 | 932 | for ip in subnet1_ips | ||
215 | 933 | ] | ||
216 | 934 | subnet2_records = [ | ||
217 | 935 | factory.make_DNSResource(domain=default_domain, ip_addresses=[ip]) | ||
218 | 936 | for ip in subnet2_ips | ||
219 | 937 | ] | ||
220 | 938 | serial = random.randint(0, 65535) | ||
221 | 939 | dynamic_updates = [ | ||
222 | 940 | DynamicDNSUpdate( | ||
223 | 941 | operation="INSERT", | ||
224 | 942 | name=record.name, | ||
225 | 943 | zone=default_domain.name, | ||
226 | 944 | rectype="A", | ||
227 | 945 | ttl=record.address_ttl, | ||
228 | 946 | answer=ip.ip, | ||
229 | 947 | ) | ||
230 | 948 | for record in subnet1_records + subnet2_records | ||
231 | 949 | for ip in record.ip_addresses.all() | ||
232 | 950 | ] | ||
233 | 951 | zones = ZoneGenerator( | ||
234 | 952 | [default_domain], | ||
235 | 953 | [subnet1, subnet2], | ||
236 | 954 | serial=serial, | ||
237 | 955 | dynamic_updates=dynamic_updates, | ||
238 | 956 | ).as_list() | ||
239 | 957 | |||
240 | 958 | def _generate_mapping_for_network(network, records): | ||
241 | 959 | mapping = {} | ||
242 | 960 | for record in records: | ||
243 | 961 | if ip_set := set( | ||
244 | 962 | ip.ip | ||
245 | 963 | for ip in record.ip_addresses.all() | ||
246 | 964 | if IPAddress(ip.ip) in network | ||
247 | 965 | ): | ||
248 | 966 | mapping[ | ||
249 | 967 | f"{record.name}.{default_domain.name}" | ||
250 | 968 | ] = HostnameIPMapping( | ||
251 | 969 | None, | ||
252 | 970 | record.address_ttl, | ||
253 | 971 | ip_set, | ||
254 | 972 | None, | ||
255 | 973 | 1, | ||
256 | 974 | None, | ||
257 | 975 | ) | ||
258 | 976 | return mapping | ||
259 | 977 | |||
260 | 978 | expected = [ | ||
261 | 979 | DNSForwardZoneConfig( | ||
262 | 980 | default_domain.name, | ||
263 | 981 | mapping={ | ||
264 | 982 | record.name: HostnameIPMapping( | ||
265 | 983 | None, | ||
266 | 984 | record.address_ttl, | ||
267 | 985 | set(record.ip_addresses.all()), | ||
268 | 986 | None, | ||
269 | 987 | 1, | ||
270 | 988 | None, | ||
271 | 989 | ) | ||
272 | 990 | for record in subnet1_records + subnet2_records | ||
273 | 991 | }, | ||
274 | 992 | dynamic_updates=dynamic_updates, | ||
275 | 993 | ), | ||
276 | 994 | DNSReverseZoneConfig( | ||
277 | 995 | default_domain.name, | ||
278 | 996 | network=IPNetwork(subnet1.cidr), | ||
279 | 997 | mapping=_generate_mapping_for_network( | ||
280 | 998 | IPNetwork(subnet1.cidr), subnet1_records + subnet2_records | ||
281 | 999 | ), | ||
282 | 1000 | dynamic_updates=[ | ||
283 | 1001 | DynamicDNSUpdate.as_reverse_record_update( | ||
284 | 1002 | update, IPNetwork(subnet1.cidr) | ||
285 | 1003 | ) | ||
286 | 1004 | for update in dynamic_updates | ||
287 | 1005 | if update.answer_as_ip in IPNetwork(subnet1.cidr) | ||
288 | 1006 | ], | ||
289 | 1007 | ), | ||
290 | 1008 | DNSReverseZoneConfig( | ||
291 | 1009 | default_domain.name, | ||
292 | 1010 | network=IPNetwork("10.0.0.0/24"), | ||
293 | 1011 | mapping=_generate_mapping_for_network( | ||
294 | 1012 | IPNetwork("10.0.0.0/24"), subnet2_records | ||
295 | 1013 | ), | ||
296 | 1014 | dynamic_updates=[ | ||
297 | 1015 | DynamicDNSUpdate.as_reverse_record_update( | ||
298 | 1016 | update, IPNetwork("10.0.0.0/24") | ||
299 | 1017 | ) | ||
300 | 1018 | for update in dynamic_updates | ||
301 | 1019 | if update.answer_as_ip in IPNetwork("10.0.0.0/24") | ||
302 | 1020 | ], | ||
303 | 1021 | ), | ||
304 | 1022 | DNSReverseZoneConfig( | ||
305 | 1023 | default_domain.name, | ||
306 | 1024 | network=IPNetwork("10.0.2.0/24"), | ||
307 | 1025 | mapping=_generate_mapping_for_network( | ||
308 | 1026 | IPNetwork("10.0.2.0/24"), subnet2_records | ||
309 | 1027 | ), | ||
310 | 1028 | dynamic_updates=[ | ||
311 | 1029 | DynamicDNSUpdate.as_reverse_record_update( | ||
312 | 1030 | update, IPNetwork("10.0.2.0/24") | ||
313 | 1031 | ) | ||
314 | 1032 | for update in dynamic_updates | ||
315 | 1033 | if update.answer_as_ip in IPNetwork("10.0.2.0/24") | ||
316 | 1034 | ], | ||
317 | 1035 | ), | ||
318 | 1036 | DNSReverseZoneConfig( | ||
319 | 1037 | default_domain.name, | ||
320 | 1038 | network=IPNetwork("10.0.3.0/24"), | ||
321 | 1039 | mapping=_generate_mapping_for_network( | ||
322 | 1040 | IPNetwork("10.0.3.0/24"), subnet2_records | ||
323 | 1041 | ), | ||
324 | 1042 | dynamic_updates=[ | ||
325 | 1043 | DynamicDNSUpdate.as_reverse_record_update( | ||
326 | 1044 | update, IPNetwork("10.0.3.0/24") | ||
327 | 1045 | ) | ||
328 | 1046 | for update in dynamic_updates | ||
329 | 1047 | if update.answer_as_ip in IPNetwork("10.0.3.0/24") | ||
330 | 1048 | ], | ||
331 | 1049 | ), | ||
332 | 1050 | DNSReverseZoneConfig( | ||
333 | 1051 | default_domain.name, | ||
334 | 1052 | network=IPNetwork("10.0.4.0/24"), | ||
335 | 1053 | mapping=_generate_mapping_for_network( | ||
336 | 1054 | IPNetwork("10.0.4.0/24"), subnet2_records | ||
337 | 1055 | ), | ||
338 | 1056 | dynamic_updates=[ | ||
339 | 1057 | DynamicDNSUpdate.as_reverse_record_update( | ||
340 | 1058 | update, IPNetwork("10.0.4.0/24") | ||
341 | 1059 | ) | ||
342 | 1060 | for update in dynamic_updates | ||
343 | 1061 | if update.answer_as_ip in IPNetwork("10.0.4.0/24") | ||
344 | 1062 | ], | ||
345 | 1063 | ), | ||
346 | 1064 | DNSReverseZoneConfig( | ||
347 | 1065 | default_domain.name, | ||
348 | 1066 | network=IPNetwork("10.0.5.0/24"), | ||
349 | 1067 | mapping=_generate_mapping_for_network( | ||
350 | 1068 | IPNetwork("10.0.5.0/24"), subnet2_records | ||
351 | 1069 | ), | ||
352 | 1070 | dynamic_updates=[ | ||
353 | 1071 | DynamicDNSUpdate.as_reverse_record_update( | ||
354 | 1072 | update, IPNetwork("10.0.5.0/24") | ||
355 | 1073 | ) | ||
356 | 1074 | for update in dynamic_updates | ||
357 | 1075 | if update.answer_as_ip in IPNetwork("10.0.5.0/24") | ||
358 | 1076 | ], | ||
359 | 1077 | ), | ||
360 | 1078 | DNSReverseZoneConfig( | ||
361 | 1079 | default_domain.name, | ||
362 | 1080 | network=IPNetwork("10.0.6.0/24"), | ||
363 | 1081 | mapping=_generate_mapping_for_network( | ||
364 | 1082 | IPNetwork("10.0.6.0/24"), subnet2_records | ||
365 | 1083 | ), | ||
366 | 1084 | dynamic_updates=[ | ||
367 | 1085 | DynamicDNSUpdate.as_reverse_record_update( | ||
368 | 1086 | update, IPNetwork("10.0.6.0/24") | ||
369 | 1087 | ) | ||
370 | 1088 | for update in dynamic_updates | ||
371 | 1089 | if update.answer_as_ip in IPNetwork("10.0.6.0/24") | ||
372 | 1090 | ], | ||
373 | 1091 | ), | ||
374 | 1092 | DNSReverseZoneConfig( | ||
375 | 1093 | default_domain.name, | ||
376 | 1094 | network=IPNetwork("10.0.7.0/24"), | ||
377 | 1095 | mapping=_generate_mapping_for_network( | ||
378 | 1096 | IPNetwork("10.0.7.0/24"), subnet2_records | ||
379 | 1097 | ), | ||
380 | 1098 | dynamic_updates=[ | ||
381 | 1099 | DynamicDNSUpdate.as_reverse_record_update( | ||
382 | 1100 | update, IPNetwork("10.0.7.0/24") | ||
383 | 1101 | ) | ||
384 | 1102 | for update in dynamic_updates | ||
385 | 1103 | if update.answer_as_ip in IPNetwork("10.0.7.0/24") | ||
386 | 1104 | ], | ||
387 | 1105 | ), | ||
388 | 1106 | ] | ||
389 | 1107 | |||
390 | 1108 | for i, zone in enumerate(zones): | ||
391 | 1109 | self.assertEqual(zone.domain, expected[i].domain) | ||
392 | 1110 | self.assertEqual(zone._network, expected[i]._network) | ||
393 | 1111 | self.assertCountEqual( | ||
394 | 1112 | zone._mapping, | ||
395 | 1113 | expected[i]._mapping, | ||
396 | 1114 | ) | ||
397 | 1115 | self.assertCountEqual( | ||
398 | 1116 | zone._dynamic_updates, expected[i]._dynamic_updates | ||
399 | 1117 | ) | ||
400 | 1118 | if isinstance(zone, DNSReverseZoneConfig): | ||
401 | 1119 | self.assertCountEqual( | ||
402 | 1120 | zone._dynamic_ranges, expected[i]._dynamic_ranges | ||
403 | 1121 | ) | ||
404 | 1122 | self.assertCountEqual( | ||
405 | 1123 | zone._rfc2317_ranges, expected[i]._rfc2317_ranges | ||
406 | 1124 | ) | ||
407 | 1125 | |||
408 | 1126 | def test_configs_are_merged_when_glue_overlaps(self): | ||
409 | 1127 | self.patch(warn_loopback) | ||
410 | 1128 | default_domain = Domain.objects.get_default_domain() | ||
411 | 1129 | subnet1 = factory.make_Subnet(cidr="10.0.1.0/24") | ||
412 | 1130 | subnet2 = factory.make_Subnet(cidr="10.0.1.0/26") | ||
413 | 1131 | subnet1_ips = [ | ||
414 | 1132 | factory.make_StaticIPAddress( | ||
415 | 1133 | ip=f"10.0.1.{253 + i}", | ||
416 | 1134 | subnet=subnet1, # avoid allocation collision | ||
417 | 1135 | ) | ||
418 | 1136 | for i in range(3) | ||
419 | 1137 | ] | ||
420 | 1138 | subnet2_ips = [ | ||
421 | 1139 | factory.make_StaticIPAddress( | ||
422 | 1140 | ip=factory.pick_ip_in_Subnet(subnet2), subnet=subnet2 | ||
423 | 1141 | ) | ||
424 | 1142 | for _ in range(3) | ||
425 | 1143 | ] | ||
426 | 1144 | subnet1_records = [ | ||
427 | 1145 | factory.make_DNSResource(domain=default_domain, ip_addresses=[ip]) | ||
428 | 1146 | for ip in subnet1_ips | ||
429 | 1147 | ] | ||
430 | 1148 | subnet2_records = [ | ||
431 | 1149 | factory.make_DNSResource(domain=default_domain, ip_addresses=[ip]) | ||
432 | 1150 | for ip in subnet2_ips | ||
433 | 1151 | ] | ||
434 | 1152 | serial = random.randint(0, 65535) | ||
435 | 1153 | dynamic_updates = [ | ||
436 | 1154 | DynamicDNSUpdate( | ||
437 | 1155 | operation="INSERT", | ||
438 | 1156 | name=record.name, | ||
439 | 1157 | zone=default_domain.name, | ||
440 | 1158 | rectype="A", | ||
441 | 1159 | ttl=record.address_ttl, | ||
442 | 1160 | answer=ip.ip, | ||
443 | 1161 | ) | ||
444 | 1162 | for record in subnet1_records + subnet2_records | ||
445 | 1163 | for ip in record.ip_addresses.all() | ||
446 | 1164 | ] | ||
447 | 1165 | zones = ZoneGenerator( | ||
448 | 1166 | [default_domain], | ||
449 | 1167 | [subnet1, subnet2], | ||
450 | 1168 | serial=serial, | ||
451 | 1169 | dynamic_updates=dynamic_updates, | ||
452 | 1170 | ).as_list() | ||
453 | 1171 | |||
454 | 1172 | def _generate_mapping_for_network(network, other_network, records): | ||
455 | 1173 | mapping = {} | ||
456 | 1174 | for record in records: | ||
457 | 1175 | ip_set = set( | ||
458 | 1176 | ip.ip | ||
459 | 1177 | for ip in record.ip_addresses.all() | ||
460 | 1178 | if IPAddress(ip.ip) in network | ||
461 | 1179 | and ( | ||
462 | 1180 | IPAddress(ip.ip) not in other_network | ||
463 | 1181 | or other_network.prefixlen < network.prefixlen | ||
464 | 1182 | ) | ||
465 | 1183 | ) | ||
466 | 1184 | if len(ip_set) > 0: | ||
467 | 1185 | mapping[ | ||
468 | 1186 | f"{record.name}.{default_domain.name}" | ||
469 | 1187 | ] = HostnameIPMapping( | ||
470 | 1188 | None, | ||
471 | 1189 | record.address_ttl, | ||
472 | 1190 | ip_set, | ||
473 | 1191 | None, | ||
474 | 1192 | 1, | ||
475 | 1193 | None, | ||
476 | 1194 | ) | ||
477 | 1195 | return mapping | ||
478 | 1196 | |||
479 | 1197 | expected = [ | ||
480 | 1198 | DNSForwardZoneConfig( | ||
481 | 1199 | default_domain.name, | ||
482 | 1200 | mapping={ | ||
483 | 1201 | record.name: HostnameIPMapping( | ||
484 | 1202 | None, | ||
485 | 1203 | record.address_ttl, | ||
486 | 1204 | set(ip.ip for ip in record.ip_addresses.all()), | ||
487 | 1205 | None, | ||
488 | 1206 | 1, | ||
489 | 1207 | None, | ||
490 | 1208 | ) | ||
491 | 1209 | for record in subnet1_records + subnet2_records | ||
492 | 1210 | }, | ||
493 | 1211 | dynamic_updates=dynamic_updates, | ||
494 | 1212 | ), | ||
495 | 1213 | DNSReverseZoneConfig( | ||
496 | 1214 | default_domain.name, | ||
497 | 1215 | network=IPNetwork(subnet2.cidr), | ||
498 | 1216 | mapping=_generate_mapping_for_network( | ||
499 | 1217 | IPNetwork(subnet2.cidr), | ||
500 | 1218 | IPNetwork(subnet1.cidr), | ||
501 | 1219 | subnet1_records + subnet2_records, | ||
502 | 1220 | ), | ||
503 | 1221 | dynamic_updates=[ | ||
504 | 1222 | DynamicDNSUpdate.as_reverse_record_update( | ||
505 | 1223 | update, IPNetwork(subnet2.cidr) | ||
506 | 1224 | ) | ||
507 | 1225 | for update in dynamic_updates | ||
508 | 1226 | if update.answer_as_ip in IPNetwork(subnet2.cidr) | ||
509 | 1227 | ], | ||
510 | 1228 | ), | ||
511 | 1229 | DNSReverseZoneConfig( | ||
512 | 1230 | default_domain.name, | ||
513 | 1231 | network=IPNetwork(subnet1.cidr), | ||
514 | 1232 | mapping=_generate_mapping_for_network( | ||
515 | 1233 | IPNetwork(subnet1.cidr), | ||
516 | 1234 | IPNetwork(subnet2.cidr), | ||
517 | 1235 | subnet1_records + subnet2_records, | ||
518 | 1236 | ), | ||
519 | 1237 | dynamic_updates=[ | ||
520 | 1238 | DynamicDNSUpdate.as_reverse_record_update( | ||
521 | 1239 | update, IPNetwork(subnet1.cidr) | ||
522 | 1240 | ) | ||
523 | 1241 | for update in dynamic_updates | ||
524 | 1242 | if update.answer_as_ip in IPNetwork(subnet1.cidr) | ||
525 | 1243 | ], | ||
526 | 1244 | rfc2317_ranges=set([IPNetwork(subnet2.cidr)]), | ||
527 | 1245 | ), | ||
528 | 1246 | ] | ||
529 | 1247 | |||
530 | 1248 | for i, zone in enumerate(zones): | ||
531 | 1249 | self.assertEqual(zone.domain, expected[i].domain) | ||
532 | 1250 | self.assertEqual(zone._network, expected[i]._network) | ||
533 | 1251 | self.assertCountEqual( | ||
534 | 1252 | zone._mapping, | ||
535 | 1253 | expected[i]._mapping, | ||
536 | 1254 | ) | ||
537 | 1255 | self.assertCountEqual( | ||
538 | 1256 | zone._dynamic_updates, expected[i]._dynamic_updates | ||
539 | 1257 | ) | ||
540 | 1258 | if isinstance(zone, DNSReverseZoneConfig): | ||
541 | 1259 | self.assertCountEqual( | ||
542 | 1260 | zone._dynamic_ranges, expected[i]._dynamic_ranges | ||
543 | 1261 | ) | ||
544 | 1262 | self.assertCountEqual( | ||
545 | 1263 | zone._rfc2317_ranges, expected[i]._rfc2317_ranges | ||
546 | 1264 | ) | ||
547 | 1265 | |||
548 | 901 | 1266 | ||
549 | 902 | class TestZoneGeneratorTTL(MAASTransactionServerTestCase): | 1267 | class TestZoneGeneratorTTL(MAASTransactionServerTestCase): |
550 | 903 | """Tests for TTL in :class:ZoneGenerator`.""" | 1268 | """Tests for TTL in :class:ZoneGenerator`.""" |
551 | @@ -1006,7 +1371,22 @@ class TestZoneGeneratorTTL(MAASTransactionServerTestCase): | |||
552 | 1006 | serial=random.randint(0, 65535), | 1371 | serial=random.randint(0, 65535), |
553 | 1007 | ).as_list() | 1372 | ).as_list() |
554 | 1008 | self.assertEqual(expected_forward, zones[0]._mapping) | 1373 | self.assertEqual(expected_forward, zones[0]._mapping) |
556 | 1009 | self.assertEqual(expected_reverse, zones[1]._mapping) | 1374 | for zone in zones[1:]: |
557 | 1375 | if ip_set := set( | ||
558 | 1376 | ip | ||
559 | 1377 | for ip in expected_reverse[node.fqdn].ips | ||
560 | 1378 | if IPAddress(ip) in zone._network | ||
561 | 1379 | ): | ||
562 | 1380 | expected_rev = { | ||
563 | 1381 | node.fqdn: HostnameIPMapping( | ||
564 | 1382 | node.system_id, | ||
565 | 1383 | node.address_ttl, | ||
566 | 1384 | ip_set, | ||
567 | 1385 | node.node_type, | ||
568 | 1386 | dnsrr.id, | ||
569 | 1387 | ) | ||
570 | 1388 | } | ||
571 | 1389 | self.assertEqual(expected_rev, zone._mapping) | ||
572 | 1010 | 1390 | ||
573 | 1011 | @transactional | 1391 | @transactional |
574 | 1012 | def test_dnsresource_address_overrides_domain(self): | 1392 | def test_dnsresource_address_overrides_domain(self): |
575 | @@ -1058,7 +1438,22 @@ class TestZoneGeneratorTTL(MAASTransactionServerTestCase): | |||
576 | 1058 | serial=random.randint(0, 65535), | 1438 | serial=random.randint(0, 65535), |
577 | 1059 | ).as_list() | 1439 | ).as_list() |
578 | 1060 | self.assertEqual(expected_forward, zones[0]._mapping) | 1440 | self.assertEqual(expected_forward, zones[0]._mapping) |
580 | 1061 | self.assertEqual(expected_reverse, zones[1]._mapping) | 1441 | |
581 | 1442 | for zone in zones[1:]: | ||
582 | 1443 | expected = {} | ||
583 | 1444 | for expected_label, expected_mapping in expected_reverse.items(): | ||
584 | 1445 | if ip_set := set( | ||
585 | 1446 | ip for ip in expected_mapping.ips if ip in zone._network | ||
586 | 1447 | ): | ||
587 | 1448 | expected[expected_label] = HostnameIPMapping( | ||
588 | 1449 | system_id=expected_mapping.system_id, | ||
589 | 1450 | ttl=expected_mapping.ttl, | ||
590 | 1451 | ips=ip_set, | ||
591 | 1452 | node_type=expected_mapping.node_type, | ||
592 | 1453 | dnsresource_id=expected_mapping.dnsresource_id, | ||
593 | 1454 | user_id=expected_mapping.user_id, | ||
594 | 1455 | ) | ||
595 | 1456 | self.assertEqual(expected, zone._mapping) | ||
596 | 1062 | 1457 | ||
597 | 1063 | @transactional | 1458 | @transactional |
598 | 1064 | def test_dnsdata_inherits_global(self): | 1459 | def test_dnsdata_inherits_global(self): |
599 | @@ -1167,3 +1562,123 @@ class TestZoneGeneratorTTL(MAASTransactionServerTestCase): | |||
600 | 1167 | [zone_config] = ZoneGenerator(domains=[domain], subnets=[], serial=123) | 1562 | [zone_config] = ZoneGenerator(domains=[domain], subnets=[], serial=123) |
601 | 1168 | self.assertEqual(domain.name, zone_config.domain) | 1563 | self.assertEqual(domain.name, zone_config.domain) |
602 | 1169 | self.assertEqual(42, zone_config.default_ttl) | 1564 | self.assertEqual(42, zone_config.default_ttl) |
603 | 1565 | |||
604 | 1566 | |||
605 | 1567 | class TestZoneGeneratorEndToEnd(MAASServerTestCase): | ||
606 | 1568 | def _find_most_specific_subnet( | ||
607 | 1569 | self, ip: StaticIPAddress, subnets: list[Subnet] | ||
608 | 1570 | ): | ||
609 | 1571 | networks = [] | ||
610 | 1572 | for subnet in subnets: | ||
611 | 1573 | net = IPNetwork(subnet.cidr) | ||
612 | 1574 | if net.prefixlen < 24: | ||
613 | 1575 | networks += ZoneGenerator._split_large_subnet(net) | ||
614 | 1576 | else: | ||
615 | 1577 | networks.append(net) | ||
616 | 1578 | sorted_nets = sorted(networks, key=lambda net: -1 * net.prefixlen) | ||
617 | 1579 | for net in sorted_nets: | ||
618 | 1580 | if IPAddress(ip.ip) in net: | ||
619 | 1581 | return net | ||
620 | 1582 | |||
621 | 1583 | def test_ZoneGenerator_generates_config_for_zone_files(self): | ||
622 | 1584 | config_path = patch_zone_file_config_path(self) | ||
623 | 1585 | default_domain = Domain.objects.get_default_domain() | ||
624 | 1586 | domain = factory.make_Domain() | ||
625 | 1587 | subnet1 = factory.make_Subnet(cidr="10.0.1.0/24") | ||
626 | 1588 | subnet2 = factory.make_Subnet(cidr="10.0.0.0/22") | ||
627 | 1589 | subnet3 = factory.make_Subnet(cidr="10.0.1.0/27") | ||
628 | 1590 | subnet1_ips = [ | ||
629 | 1591 | factory.make_StaticIPAddress( | ||
630 | 1592 | ip=factory.pick_ip_in_Subnet(subnet1), subnet=subnet1 | ||
631 | 1593 | ) | ||
632 | 1594 | for _ in range(3) | ||
633 | 1595 | ] | ||
634 | 1596 | subnet2_ips = [ | ||
635 | 1597 | factory.make_StaticIPAddress( | ||
636 | 1598 | ip=factory.pick_ip_in_Subnet( | ||
637 | 1599 | subnet2, but_not=list(subnet1.get_ipranges_in_use()) | ||
638 | 1600 | ), | ||
639 | 1601 | subnet=subnet2, | ||
640 | 1602 | ) | ||
641 | 1603 | for _ in range(3) | ||
642 | 1604 | ] | ||
643 | 1605 | subnet3_ips = [ | ||
644 | 1606 | factory.make_StaticIPAddress( | ||
645 | 1607 | ip=factory.pick_ip_in_Subnet( | ||
646 | 1608 | subnet3, | ||
647 | 1609 | but_not=list(subnet1.get_ipranges_in_use()) | ||
648 | 1610 | + list(subnet2.get_ipranges_in_use()), | ||
649 | 1611 | ), | ||
650 | 1612 | subnet=subnet3, | ||
651 | 1613 | ) | ||
652 | 1614 | for _ in range(3) | ||
653 | 1615 | ] | ||
654 | 1616 | subnet1_records = [ | ||
655 | 1617 | factory.make_DNSResource( | ||
656 | 1618 | domain=random.choice((default_domain, domain)), | ||
657 | 1619 | ip_addresses=[ip], | ||
658 | 1620 | ) | ||
659 | 1621 | for ip in subnet1_ips | ||
660 | 1622 | ] | ||
661 | 1623 | subnet2_records = [ | ||
662 | 1624 | factory.make_DNSResource( | ||
663 | 1625 | domain=random.choice((default_domain, domain)), | ||
664 | 1626 | ip_addresses=[ip], | ||
665 | 1627 | ) | ||
666 | 1628 | for ip in subnet2_ips | ||
667 | 1629 | ] | ||
668 | 1630 | subnet3_records = [ | ||
669 | 1631 | factory.make_DNSResource( | ||
670 | 1632 | domain=random.choice((default_domain, domain)), | ||
671 | 1633 | ip_addresses=[ip], | ||
672 | 1634 | ) | ||
673 | 1635 | for ip in subnet3_ips | ||
674 | 1636 | ] | ||
675 | 1637 | all_records = subnet1_records + subnet2_records + subnet3_records | ||
676 | 1638 | zones = ZoneGenerator( | ||
677 | 1639 | [default_domain, domain], | ||
678 | 1640 | [subnet1, subnet2, subnet3], | ||
679 | 1641 | serial=random.randint(0, 65535), | ||
680 | 1642 | ).as_list() | ||
681 | 1643 | for zone in zones: | ||
682 | 1644 | zone.write_config() | ||
683 | 1645 | |||
684 | 1646 | # check forward zones | ||
685 | 1647 | with open( | ||
686 | 1648 | os.path.join(config_path, f"zone.{default_domain.name}"), "r" | ||
687 | 1649 | ) as zf: | ||
688 | 1650 | default_domain_contents = zf.read() | ||
689 | 1651 | |||
690 | 1652 | with open(os.path.join(config_path, f"zone.{domain.name}"), "r") as zf: | ||
691 | 1653 | domain_contents = zf.read() | ||
692 | 1654 | |||
693 | 1655 | for record in all_records: | ||
694 | 1656 | if record.domain == default_domain: | ||
695 | 1657 | contents = default_domain_contents | ||
696 | 1658 | else: | ||
697 | 1659 | contents = domain_contents | ||
698 | 1660 | |||
699 | 1661 | self.assertIn( | ||
700 | 1662 | f"{record.name} 30 IN A {record.ip_addresses.first().ip}", | ||
701 | 1663 | contents, | ||
702 | 1664 | ) | ||
703 | 1665 | |||
704 | 1666 | # check reverse zones | ||
705 | 1667 | for record in all_records: | ||
706 | 1668 | ip = record.ip_addresses.first() | ||
707 | 1669 | subnet = self._find_most_specific_subnet( | ||
708 | 1670 | ip, [subnet1, subnet2, subnet3] | ||
709 | 1671 | ) | ||
710 | 1672 | rev_subnet = ".".join(str(subnet.network).split(".")[2::-1]) | ||
711 | 1673 | if subnet.prefixlen > 24: | ||
712 | 1674 | rev_subnet = f"{str(subnet.network).split('.')[-1]}-{subnet.prefixlen}.{rev_subnet}" | ||
713 | 1675 | with open( | ||
714 | 1676 | os.path.join(config_path, f"zone.{rev_subnet}.in-addr.arpa"), | ||
715 | 1677 | "r", | ||
716 | 1678 | ) as zf: | ||
717 | 1679 | contents = zf.read() | ||
718 | 1680 | self.assertIn( | ||
719 | 1681 | f"{ip.ip.split('.')[-1]} 30 IN PTR {record.fqdn}", | ||
720 | 1682 | contents, | ||
721 | 1683 | f"{subnet} {ip.ip}", | ||
722 | 1684 | ) | ||
723 | diff --git a/src/maasserver/dns/zonegenerator.py b/src/maasserver/dns/zonegenerator.py | |||
724 | index b6fd406..8272e5c 100644 | |||
725 | --- a/src/maasserver/dns/zonegenerator.py | |||
726 | +++ b/src/maasserver/dns/zonegenerator.py | |||
727 | @@ -18,7 +18,11 @@ from maasserver.models.config import Config | |||
728 | 18 | from maasserver.models.dnsdata import DNSData, HostnameRRsetMapping | 18 | from maasserver.models.dnsdata import DNSData, HostnameRRsetMapping |
729 | 19 | from maasserver.models.dnsresource import separate_fqdn | 19 | from maasserver.models.dnsresource import separate_fqdn |
730 | 20 | from maasserver.models.domain import Domain | 20 | from maasserver.models.domain import Domain |
732 | 21 | from maasserver.models.staticipaddress import StaticIPAddress | 21 | from maasserver.models.iprange import IPRange |
733 | 22 | from maasserver.models.staticipaddress import ( | ||
734 | 23 | HostnameIPMapping, | ||
735 | 24 | StaticIPAddress, | ||
736 | 25 | ) | ||
737 | 22 | from maasserver.models.subnet import Subnet | 26 | from maasserver.models.subnet import Subnet |
738 | 23 | from maasserver.server_address import get_maas_facing_server_addresses | 27 | from maasserver.server_address import get_maas_facing_server_addresses |
739 | 24 | from provisioningserver.dns.config import DynamicDNSUpdate | 28 | from provisioningserver.dns.config import DynamicDNSUpdate |
740 | @@ -226,6 +230,7 @@ class ZoneGenerator: | |||
741 | 226 | if self._dynamic_updates is None: | 230 | if self._dynamic_updates is None: |
742 | 227 | self._dynamic_updates = [] | 231 | self._dynamic_updates = [] |
743 | 228 | self.force_config_write = force_config_write # some data changed that nsupdate cannot update if true | 232 | self.force_config_write = force_config_write # some data changed that nsupdate cannot update if true |
744 | 233 | self._existing_subnet_cfgs = {} | ||
745 | 229 | 234 | ||
746 | 230 | @staticmethod | 235 | @staticmethod |
747 | 231 | def _get_mappings(): | 236 | def _get_mappings(): |
748 | @@ -357,18 +362,93 @@ class ZoneGenerator: | |||
749 | 357 | ) | 362 | ) |
750 | 358 | 363 | ||
751 | 359 | @staticmethod | 364 | @staticmethod |
760 | 360 | def _gen_reverse_zones( | 365 | def _split_large_subnet(network: IPNetwork) -> list[IPNetwork]: |
761 | 361 | subnets, | 366 | # Generate the name of the reverse zone file: |
762 | 362 | serial, | 367 | # Use netaddr's reverse_dns() to get the reverse IP name |
763 | 363 | ns_host_name, | 368 | # of the first IP address in the network and then drop the first |
764 | 364 | mappings, | 369 | # octets of that name (i.e. drop the octets that will be specified in |
765 | 365 | default_ttl, | 370 | # the zone file). |
766 | 366 | dynamic_updates, | 371 | # returns a list of (IPNetwork, zone_name, zonefile_path) tuples |
767 | 367 | force_config_write, | 372 | new_networks = [] |
768 | 373 | first = IPAddress(network.first) | ||
769 | 374 | last = IPAddress(network.last) | ||
770 | 375 | if first.version == 6: | ||
771 | 376 | # IPv6. | ||
772 | 377 | # 2001:89ab::/19 yields 8.1.0.0.2.ip6.arpa, and the full list | ||
773 | 378 | # is 8.1.0.0.2.ip6.arpa, 9.1.0.0.2.ip6.arpa | ||
774 | 379 | # The ipv6 reverse dns form is 32 elements of 1 hex digit each. | ||
775 | 380 | # How many elements of the reverse DNS name to we throw away? | ||
776 | 381 | # Prefixlen of 0-3 gives us 1, 4-7 gives us 2, etc. | ||
777 | 382 | # While this seems wrong, we always _add_ a base label back in, | ||
778 | 383 | # so it's correct. | ||
779 | 384 | rest_limit = (132 - network.prefixlen) // 4 | ||
780 | 385 | # What is the prefix for each inner subnet (It will be the next | ||
781 | 386 | # smaller multiple of 4.) If it's the smallest one, then RFC2317 | ||
782 | 387 | # tells us that we're adding an extra blob to the front of the | ||
783 | 388 | # reverse zone name, and we want the entire prefixlen. | ||
784 | 389 | subnet_prefix = (network.prefixlen + 3) // 4 * 4 | ||
785 | 390 | if subnet_prefix == 128: | ||
786 | 391 | subnet_prefix = network.prefixlen | ||
787 | 392 | # How big is the step between subnets? Again, special case for | ||
788 | 393 | # extra small subnets. | ||
789 | 394 | step = 1 << ((128 - network.prefixlen) // 4 * 4) | ||
790 | 395 | if step < 16: | ||
791 | 396 | step = 16 | ||
792 | 397 | # Grab the base (hex) and trailing labels for our reverse zone. | ||
793 | 398 | split_zone = first.reverse_dns.split(".") | ||
794 | 399 | base = int(split_zone[rest_limit - 1], 16) | ||
795 | 400 | else: | ||
796 | 401 | # IPv4. | ||
797 | 402 | # The logic here is the same as for IPv6, but with 8 instead of 4. | ||
798 | 403 | rest_limit = (40 - network.prefixlen) // 8 | ||
799 | 404 | subnet_prefix = (network.prefixlen + 7) // 8 * 8 | ||
800 | 405 | if subnet_prefix == 32: | ||
801 | 406 | subnet_prefix = network.prefixlen | ||
802 | 407 | step = 1 << ((32 - network.prefixlen) // 8 * 8) | ||
803 | 408 | if step < 256: | ||
804 | 409 | step = 256 | ||
805 | 410 | # Grab the base (decimal) and trailing labels for our reverse | ||
806 | 411 | # zone. | ||
807 | 412 | split_zone = first.reverse_dns.split(".") | ||
808 | 413 | base = int(split_zone[rest_limit - 1]) | ||
809 | 414 | |||
810 | 415 | while first <= last: | ||
811 | 416 | if first > last: | ||
812 | 417 | # if the excluding subnet pushes the base IP beyond the bounds of the generating subnet, we've reached the end and return early | ||
813 | 418 | return new_networks | ||
814 | 419 | |||
815 | 420 | new_networks.append(IPNetwork(f"{first}/{subnet_prefix:d}")) | ||
816 | 421 | base += 1 | ||
817 | 422 | try: | ||
818 | 423 | first += step | ||
819 | 424 | except IndexError: | ||
820 | 425 | # IndexError occurs when we go from 255.255.255.255 to | ||
821 | 426 | # 0.0.0.0. If we hit that, we're all fine and done. | ||
822 | 427 | break | ||
823 | 428 | return new_networks | ||
824 | 429 | |||
825 | 430 | @staticmethod | ||
826 | 431 | def _filter_mapping_for_network( | ||
827 | 432 | network: IPNetwork, mappings: dict[str, HostnameIPMapping] | ||
828 | 368 | ): | 433 | ): |
830 | 369 | """Generator of reverse zones, sorted by network.""" | 434 | net_mappings = {} |
831 | 435 | for k, v in mappings.items(): | ||
832 | 436 | if ips_in_net := set( | ||
833 | 437 | ip for ip in v.ips if IPAddress(ip) in network | ||
834 | 438 | ): | ||
835 | 439 | net_mappings[k] = HostnameIPMapping( | ||
836 | 440 | v.system_id, | ||
837 | 441 | v.ttl, | ||
838 | 442 | ips_in_net, | ||
839 | 443 | v.node_type, | ||
840 | 444 | v.dnsresource_id, | ||
841 | 445 | v.user_id, | ||
842 | 446 | ) | ||
843 | 370 | 447 | ||
845 | 371 | subnets = set(subnets) | 448 | return net_mappings |
846 | 449 | |||
847 | 450 | @staticmethod | ||
848 | 451 | def _generate_glue_nets(subnets: list[Subnet]): | ||
849 | 372 | # Generate the list of parent networks for rfc2317 glue. Note that we | 452 | # Generate the list of parent networks for rfc2317 glue. Note that we |
850 | 373 | # need to handle the case where we are controlling both the small net | 453 | # need to handle the case where we are controlling both the small net |
851 | 374 | # and a bigger network containing the /24, not just a /24 network. | 454 | # and a bigger network containing the /24, not just a /24 network. |
852 | @@ -393,6 +473,82 @@ class ZoneGenerator: | |||
853 | 393 | ) | 473 | ) |
854 | 394 | rfc2317_glue.setdefault(basenet, set()).add(network) | 474 | rfc2317_glue.setdefault(basenet, set()).add(network) |
855 | 395 | 475 | ||
856 | 476 | return rfc2317_glue | ||
857 | 477 | |||
858 | 478 | @staticmethod | ||
859 | 479 | def _find_glue_nets( | ||
860 | 480 | network: IPNetwork, rfc2317_glue: defaultdict[str, set[IPNetwork]] | ||
861 | 481 | ): | ||
862 | 482 | # Use the default_domain as the name for the NS host in the reverse | ||
863 | 483 | # zones. If this network is actually a parent rfc2317 glue | ||
864 | 484 | # network, then we need to generate the glue records. | ||
865 | 485 | # We need to detect the need for glue in our networks that are | ||
866 | 486 | # big. | ||
867 | 487 | if ( | ||
868 | 488 | network.version == 6 and network.prefixlen < 124 | ||
869 | 489 | ) or network.prefixlen < 24: | ||
870 | 490 | glue = set() | ||
871 | 491 | # This is the reason for needing the subnets sorted in | ||
872 | 492 | # increasing order of size. | ||
873 | 493 | for net in rfc2317_glue.copy().keys(): | ||
874 | 494 | if net in network: | ||
875 | 495 | glue.update(rfc2317_glue[net]) | ||
876 | 496 | del rfc2317_glue[net] | ||
877 | 497 | elif network in rfc2317_glue: | ||
878 | 498 | glue = rfc2317_glue[network] | ||
879 | 499 | del rfc2317_glue[network] | ||
880 | 500 | else: | ||
881 | 501 | glue = set() | ||
882 | 502 | return glue | ||
883 | 503 | |||
884 | 504 | @staticmethod | ||
885 | 505 | def _merge_into_existing_network( | ||
886 | 506 | network: IPNetwork, | ||
887 | 507 | existing: dict[IPNetwork, DNSReverseZoneConfig], | ||
888 | 508 | mapping: dict[str, HostnameIPMapping], | ||
889 | 509 | dynamic_ranges: list[IPRange] | None = [], | ||
890 | 510 | dynamic_updates: list[DynamicDNSUpdate] | None = [], | ||
891 | 511 | glue: set[IPNetwork] | None = set(), | ||
892 | 512 | is_glue_net: bool = False, | ||
893 | 513 | ): | ||
894 | 514 | # since all dynamic updates are passed and we then filter for those belonging | ||
895 | 515 | # in the network, the existing config already has all updates and we do not need | ||
896 | 516 | # to merge them, just add them if they haven't already | ||
897 | 517 | if not existing[network]._dynamic_updates: | ||
898 | 518 | existing[network]._dynamic_updates = dynamic_updates | ||
899 | 519 | existing[network]._rfc2317_ranges = existing[ | ||
900 | 520 | network | ||
901 | 521 | ]._rfc2317_ranges.union(glue) | ||
902 | 522 | for k, v in mapping.items(): | ||
903 | 523 | if k in existing[network]._mapping: | ||
904 | 524 | existing[network]._mapping[k].ips.union(v.ips) | ||
905 | 525 | else: | ||
906 | 526 | existing[network]._mapping[k] = v | ||
907 | 527 | existing[network]._dynamic_ranges += dynamic_ranges | ||
908 | 528 | for glue_net in glue.union(existing[network]._rfc2317_ranges): | ||
909 | 529 | for k, v in existing[network]._mapping.copy().items(): | ||
910 | 530 | if ip_set := set(ip for ip in v.ips if ip not in glue_net): | ||
911 | 531 | existing[network]._mapping[k].ips = ip_set | ||
912 | 532 | else: | ||
913 | 533 | del existing[network]._mapping[k] | ||
914 | 534 | |||
915 | 535 | @staticmethod | ||
916 | 536 | def _gen_reverse_zones( | ||
917 | 537 | subnets, | ||
918 | 538 | serial, | ||
919 | 539 | ns_host_name, | ||
920 | 540 | mappings, | ||
921 | 541 | default_ttl, | ||
922 | 542 | dynamic_updates, | ||
923 | 543 | force_config_write, | ||
924 | 544 | existing_subnet_cfgs={}, | ||
925 | 545 | ): | ||
926 | 546 | """Generator of reverse zones, sorted by network.""" | ||
927 | 547 | |||
928 | 548 | subnets = set(subnets) | ||
929 | 549 | |||
930 | 550 | rfc2317_glue = ZoneGenerator._generate_glue_nets(subnets) | ||
931 | 551 | |||
932 | 396 | # Since get_hostname_ip_mapping(Subnet) ignores Subnet.id, so we can | 552 | # Since get_hostname_ip_mapping(Subnet) ignores Subnet.id, so we can |
933 | 397 | # just do it once and be happy. LP#1600259 | 553 | # just do it once and be happy. LP#1600259 |
934 | 398 | if len(subnets): | 554 | if len(subnets): |
935 | @@ -412,7 +568,7 @@ class ZoneGenerator: | |||
936 | 412 | key=lambda subnet: IPNetwork(subnet.cidr).prefixlen, | 568 | key=lambda subnet: IPNetwork(subnet.cidr).prefixlen, |
937 | 413 | reverse=True, | 569 | reverse=True, |
938 | 414 | ): | 570 | ): |
940 | 415 | network = IPNetwork(subnet.cidr) | 571 | base_network = IPNetwork(subnet.cidr) |
941 | 416 | if subnet.rdns_mode == RDNS_MODE.DISABLED: | 572 | if subnet.rdns_mode == RDNS_MODE.DISABLED: |
942 | 417 | # If we are not doing reverse dns for this subnet, then just | 573 | # If we are not doing reverse dns for this subnet, then just |
943 | 418 | # skip to the next subnet. | 574 | # skip to the next subnet. |
944 | @@ -421,103 +577,109 @@ class ZoneGenerator: | |||
945 | 421 | ) | 577 | ) |
946 | 422 | continue | 578 | continue |
947 | 423 | 579 | ||
948 | 580 | networks = ZoneGenerator._split_large_subnet(base_network) | ||
949 | 581 | |||
950 | 424 | # 1. Figure out the dynamic ranges. | 582 | # 1. Figure out the dynamic ranges. |
951 | 425 | dynamic_ranges = [ | 583 | dynamic_ranges = [ |
952 | 426 | ip_range.netaddr_iprange | 584 | ip_range.netaddr_iprange |
953 | 427 | for ip_range in subnet.get_dynamic_ranges() | 585 | for ip_range in subnet.get_dynamic_ranges() |
954 | 428 | ] | 586 | ] |
955 | 429 | 587 | ||
983 | 430 | # 2. Start with the map of all of the nodes, including all | 588 | for network in networks: |
984 | 431 | # DNSResource-associated addresses. We will prune this to just | 589 | # 2. Start with the map of all of the nodes, including all |
985 | 432 | # entries for the subnet when we actually generate the zonefile. | 590 | # DNSResource-associated addresses. We will prune this to just |
986 | 433 | # If we get here, then we have subnets, so we noticed that above | 591 | # entries for the subnet when we actually generate the zonefile. |
987 | 434 | # and created mappings['reverse']. LP#1600259 | 592 | # If we get here, then we have subnets, so we noticed that above |
988 | 435 | mapping = mappings["reverse"] | 593 | # and created mappings['reverse']. LP#1600259 |
989 | 436 | 594 | mapping = ZoneGenerator._filter_mapping_for_network( | |
990 | 437 | # Use the default_domain as the name for the NS host in the reverse | 595 | network, mappings["reverse"] |
991 | 438 | # zones. If this network is actually a parent rfc2317 glue | 596 | ) |
965 | 439 | # network, then we need to generate the glue records. | ||
966 | 440 | # We need to detect the need for glue in our networks that are | ||
967 | 441 | # big. | ||
968 | 442 | if ( | ||
969 | 443 | network.version == 6 and network.prefixlen < 124 | ||
970 | 444 | ) or network.prefixlen < 24: | ||
971 | 445 | glue = set() | ||
972 | 446 | # This is the reason for needing the subnets sorted in | ||
973 | 447 | # increasing order of size. | ||
974 | 448 | for net in rfc2317_glue.copy().keys(): | ||
975 | 449 | if net in network: | ||
976 | 450 | glue.update(rfc2317_glue[net]) | ||
977 | 451 | del rfc2317_glue[net] | ||
978 | 452 | elif network in rfc2317_glue: | ||
979 | 453 | glue = rfc2317_glue[network] | ||
980 | 454 | del rfc2317_glue[network] | ||
981 | 455 | else: | ||
982 | 456 | glue = set() | ||
992 | 457 | 597 | ||
1000 | 458 | domain_updates = [ | 598 | glue = ZoneGenerator._find_glue_nets(network, rfc2317_glue) |
1001 | 459 | DynamicDNSUpdate.as_reverse_record_update(update, network) | 599 | domain_updates = [ |
1002 | 460 | for update in dynamic_updates | 600 | DynamicDNSUpdate.as_reverse_record_update(update, network) |
1003 | 461 | if update.answer | 601 | for update in dynamic_updates |
1004 | 462 | and update.answer_is_ip | 602 | if update.answer |
1005 | 463 | and (update.answer_as_ip in network) | 603 | and update.answer_is_ip |
1006 | 464 | ] | 604 | and (update.answer_as_ip in network) |
1007 | 605 | ] | ||
1008 | 606 | |||
1009 | 607 | if network in existing_subnet_cfgs: | ||
1010 | 608 | ZoneGenerator._merge_into_existing_network( | ||
1011 | 609 | network, | ||
1012 | 610 | existing_subnet_cfgs, | ||
1013 | 611 | mapping, | ||
1014 | 612 | dynamic_ranges=dynamic_ranges, | ||
1015 | 613 | dynamic_updates=domain_updates, | ||
1016 | 614 | glue=glue, | ||
1017 | 615 | ) | ||
1018 | 616 | else: | ||
1019 | 617 | existing_subnet_cfgs[network] = DNSReverseZoneConfig( | ||
1020 | 618 | ns_host_name, | ||
1021 | 619 | serial=serial, | ||
1022 | 620 | default_ttl=default_ttl, | ||
1023 | 621 | ns_host_name=ns_host_name, | ||
1024 | 622 | mapping=mapping, | ||
1025 | 623 | network=network, | ||
1026 | 624 | dynamic_ranges=dynamic_ranges, | ||
1027 | 625 | rfc2317_ranges=glue, | ||
1028 | 626 | dynamic_updates=domain_updates, | ||
1029 | 627 | force_config_write=force_config_write, | ||
1030 | 628 | ) | ||
1031 | 465 | 629 | ||
1058 | 466 | yield DNSReverseZoneConfig( | 630 | yield existing_subnet_cfgs[network] |
1059 | 467 | ns_host_name, | 631 | |
1060 | 468 | serial=serial, | 632 | # Now provide any remaining rfc2317 glue networks. |
1061 | 469 | default_ttl=default_ttl, | 633 | for network, ranges in rfc2317_glue.items(): |
1062 | 470 | ns_host_name=ns_host_name, | 634 | exclude_set = { |
1063 | 471 | mapping=mapping, | 635 | IPNetwork(s.cidr) |
1064 | 472 | network=network, | 636 | for s in subnets |
1065 | 473 | dynamic_ranges=dynamic_ranges, | 637 | if network in IPNetwork(s.cidr) |
1066 | 474 | rfc2317_ranges=glue, | 638 | } |
1067 | 475 | exclude={ | 639 | domain_updates = [] |
1068 | 476 | IPNetwork(s.cidr) for s in subnets if s is not subnet | 640 | for update in dynamic_updates: |
1069 | 477 | }, | 641 | glue_update = True |
1070 | 478 | dynamic_updates=domain_updates, | 642 | for exclude_net in exclude_set: |
1071 | 479 | force_config_write=force_config_write, | 643 | if ( |
1072 | 480 | ) | 644 | update.answer |
1073 | 481 | # Now provide any remaining rfc2317 glue networks. | 645 | and update.answer_is_ip |
1074 | 482 | for network, ranges in rfc2317_glue.items(): | 646 | and update.answer_as_ip in exclude_net |
1075 | 483 | exclude_set = { | 647 | ): |
1076 | 484 | IPNetwork(s.cidr) | 648 | glue_update = False |
1077 | 485 | for s in subnets | 649 | break |
1052 | 486 | if network in IPNetwork(s.cidr) | ||
1053 | 487 | } | ||
1054 | 488 | domain_updates = [] | ||
1055 | 489 | for update in dynamic_updates: | ||
1056 | 490 | glue_update = True | ||
1057 | 491 | for exclude_net in exclude_set: | ||
1078 | 492 | if ( | 650 | if ( |
1080 | 493 | update.answer | 651 | glue_update |
1081 | 652 | and update.answer | ||
1082 | 494 | and update.answer_is_ip | 653 | and update.answer_is_ip |
1084 | 495 | and update.answer_as_ip in exclude_net | 654 | and update.answer_as_ip in network |
1085 | 496 | ): | 655 | ): |
1097 | 497 | glue_update = False | 656 | domain_updates.append( |
1098 | 498 | break | 657 | DynamicDNSUpdate.as_reverse_record_update( |
1099 | 499 | if ( | 658 | update, network |
1100 | 500 | glue_update | 659 | ) |
1090 | 501 | and update.answer | ||
1091 | 502 | and update.answer_is_ip | ||
1092 | 503 | and update.answer_as_ip in network | ||
1093 | 504 | ): | ||
1094 | 505 | domain_updates.append( | ||
1095 | 506 | DynamicDNSUpdate.as_reverse_record_update( | ||
1096 | 507 | update, network | ||
1101 | 508 | ) | 660 | ) |
1102 | 661 | |||
1103 | 662 | if network in existing_subnet_cfgs: | ||
1104 | 663 | ZoneGenerator._merge_into_existing_network( | ||
1105 | 664 | network, | ||
1106 | 665 | existing_subnet_cfgs, | ||
1107 | 666 | mapping, | ||
1108 | 667 | dynamic_updates=domain_updates, | ||
1109 | 668 | glue=ranges, | ||
1110 | 669 | is_glue_net=True, | ||
1111 | 509 | ) | 670 | ) |
1123 | 510 | yield DNSReverseZoneConfig( | 671 | else: |
1124 | 511 | ns_host_name, | 672 | existing_subnet_cfgs[network] = DNSReverseZoneConfig( |
1125 | 512 | serial=serial, | 673 | ns_host_name, |
1126 | 513 | default_ttl=default_ttl, | 674 | serial=serial, |
1127 | 514 | network=network, | 675 | default_ttl=default_ttl, |
1128 | 515 | ns_host_name=ns_host_name, | 676 | network=network, |
1129 | 516 | rfc2317_ranges=ranges, | 677 | ns_host_name=ns_host_name, |
1130 | 517 | exclude=exclude_set, | 678 | rfc2317_ranges=ranges, |
1131 | 518 | dynamic_updates=domain_updates, | 679 | dynamic_updates=domain_updates, |
1132 | 519 | force_config_write=force_config_write, | 680 | force_config_write=force_config_write, |
1133 | 520 | ) | 681 | ) |
1134 | 682 | yield existing_subnet_cfgs[network] | ||
1135 | 521 | 683 | ||
1136 | 522 | def __iter__(self): | 684 | def __iter__(self): |
1137 | 523 | """Iterate over zone configs. | 685 | """Iterate over zone configs. |
1138 | @@ -553,6 +715,7 @@ class ZoneGenerator: | |||
1139 | 553 | default_ttl, | 715 | default_ttl, |
1140 | 554 | self._dynamic_updates, | 716 | self._dynamic_updates, |
1141 | 555 | self.force_config_write, | 717 | self.force_config_write, |
1142 | 718 | existing_subnet_cfgs=self._existing_subnet_cfgs, | ||
1143 | 556 | ), | 719 | ), |
1144 | 557 | ) | 720 | ) |
1145 | 558 | 721 | ||
1146 | diff --git a/src/maastesting/noseplug.py b/src/maastesting/noseplug.py | |||
1147 | index 02949ec..ff21c32 100644 | |||
1148 | --- a/src/maastesting/noseplug.py | |||
1149 | +++ b/src/maastesting/noseplug.py | |||
1150 | @@ -474,7 +474,9 @@ class CleanTestToolsFailure(Plugin): | |||
1151 | 474 | ec, ev, tb = err | 474 | ec, ev, tb = err |
1152 | 475 | if ec is not _StringException: | 475 | if ec is not _StringException: |
1153 | 476 | return err | 476 | return err |
1155 | 477 | return Exception, Exception(*ev.args), tb | 477 | if hasattr(ev, "args"): |
1156 | 478 | return Exception, Exception(*ev.args), tb | ||
1157 | 479 | return Exception, Exception(ev), tb | ||
1158 | 478 | 480 | ||
1159 | 479 | formatError = formatFailure | 481 | formatError = formatFailure |
1160 | 480 | 482 | ||
1161 | diff --git a/src/provisioningserver/dns/tests/test_zoneconfig.py b/src/provisioningserver/dns/tests/test_zoneconfig.py | |||
1162 | index 1433493..06e6c87 100644 | |||
1163 | --- a/src/provisioningserver/dns/tests/test_zoneconfig.py | |||
1164 | +++ b/src/provisioningserver/dns/tests/test_zoneconfig.py | |||
1165 | @@ -521,17 +521,16 @@ class TestDNSReverseZoneConfig(MAASTestCase): | |||
1166 | 521 | 521 | ||
1167 | 522 | def test_computes_zone_file_config_file_paths(self): | 522 | def test_computes_zone_file_config_file_paths(self): |
1168 | 523 | domain = factory.make_name("zone") | 523 | domain = factory.make_name("zone") |
1172 | 524 | reverse_file_name = [ | 524 | # in order to merge changes, maasserver.dns.zone_generator.ZoneGenerator will split large subnets |
1173 | 525 | "zone.%d.168.192.in-addr.arpa" % i for i in range(4) | 525 | # meaning there's a 1:1 zonefile and DNSReverseZoneConfig |
1174 | 526 | ] | 526 | reverse_file_name = "zone.0.168.192.in-addr.arpa" |
1175 | 527 | dns_zone_config = DNSReverseZoneConfig( | 527 | dns_zone_config = DNSReverseZoneConfig( |
1177 | 528 | domain, network=IPNetwork("192.168.0.0/22") | 528 | domain, network=IPNetwork("192.168.0.0/24") |
1178 | 529 | ) | ||
1179 | 530 | self.assertEqual( | ||
1180 | 531 | os.path.join(get_zone_file_config_dir(), reverse_file_name), | ||
1181 | 532 | dns_zone_config.zone_info[0].target_path, | ||
1182 | 529 | ) | 533 | ) |
1183 | 530 | for i in range(4): | ||
1184 | 531 | self.assertEqual( | ||
1185 | 532 | os.path.join(get_zone_file_config_dir(), reverse_file_name[i]), | ||
1186 | 533 | dns_zone_config.zone_info[i].target_path, | ||
1187 | 534 | ) | ||
1188 | 535 | 534 | ||
1189 | 536 | def test_computes_zone_file_config_file_paths_for_small_network(self): | 535 | def test_computes_zone_file_config_file_paths_for_small_network(self): |
1190 | 537 | domain = factory.make_name("zone") | 536 | domain = factory.make_name("zone") |
1191 | @@ -555,20 +554,8 @@ class TestDNSReverseZoneConfig(MAASTestCase): | |||
1192 | 555 | # A special case is the small subnet (less than 256 hosts for IPv4, | 554 | # A special case is the small subnet (less than 256 hosts for IPv4, |
1193 | 556 | # less than 16 hosts for IPv6), in which case, we follow RFC2317 with | 555 | # less than 16 hosts for IPv6), in which case, we follow RFC2317 with |
1194 | 557 | # the modern adjustment of using '-' instead of '/'. | 556 | # the modern adjustment of using '-' instead of '/'. |
1195 | 558 | zn = "%d.0.0.0.0.0.0.0.0.0.0.0.4.0.1.f.1.0.8.a.b.0.1.0.0.2.ip6.arpa" | ||
1196 | 559 | expected = [ | 557 | expected = [ |
1197 | 560 | # IPv4 networks. | 558 | # IPv4 networks. |
1198 | 561 | # /22 ==> 4 /24 reverse zones | ||
1199 | 562 | ( | ||
1200 | 563 | IPNetwork("192.168.0.1/22"), | ||
1201 | 564 | [ | ||
1202 | 565 | DomainInfo( | ||
1203 | 566 | IPNetwork("192.168.%d.0/24" % i), | ||
1204 | 567 | "%d.168.192.in-addr.arpa" % i, | ||
1205 | 568 | ) | ||
1206 | 569 | for i in range(4) | ||
1207 | 570 | ], | ||
1208 | 571 | ), | ||
1209 | 572 | # /24 ==> 1 reverse zone | 559 | # /24 ==> 1 reverse zone |
1210 | 573 | ( | 560 | ( |
1211 | 574 | IPNetwork("192.168.0.1/24"), | 561 | IPNetwork("192.168.0.1/24"), |
1212 | @@ -625,27 +612,6 @@ class TestDNSReverseZoneConfig(MAASTestCase): | |||
1213 | 625 | ) | 612 | ) |
1214 | 626 | ], | 613 | ], |
1215 | 627 | ), | 614 | ), |
1216 | 628 | # /2 with hex digits ==> 4 /4 reverse zones | ||
1217 | 629 | ( | ||
1218 | 630 | IPNetwork("8000::/2"), | ||
1219 | 631 | [ | ||
1220 | 632 | DomainInfo(IPNetwork("8000::/4"), "8.ip6.arpa"), | ||
1221 | 633 | DomainInfo(IPNetwork("9000::/4"), "9.ip6.arpa"), | ||
1222 | 634 | DomainInfo(IPNetwork("a000::/4"), "a.ip6.arpa"), | ||
1223 | 635 | DomainInfo(IPNetwork("b000::/4"), "b.ip6.arpa"), | ||
1224 | 636 | ], | ||
1225 | 637 | ), | ||
1226 | 638 | # /103 ==> 2 /104 reverse zones | ||
1227 | 639 | ( | ||
1228 | 640 | IPNetwork("2001:ba8:1f1:400::/103"), | ||
1229 | 641 | [ | ||
1230 | 642 | DomainInfo( | ||
1231 | 643 | IPNetwork("2001:ba8:1f1:400:0:0:%d00:0000/104" % i), | ||
1232 | 644 | zn % i, | ||
1233 | 645 | ) | ||
1234 | 646 | for i in range(2) | ||
1235 | 647 | ], | ||
1236 | 648 | ), | ||
1237 | 649 | # /125 ==> 1 reverse zone, based on RFC2317 | 615 | # /125 ==> 1 reverse zone, based on RFC2317 |
1238 | 650 | ( | 616 | ( |
1239 | 651 | IPNetwork("2001:ba8:1f1:400::/125"), | 617 | IPNetwork("2001:ba8:1f1:400::/125"), |
1240 | @@ -818,7 +784,7 @@ class TestDNSReverseZoneConfig(MAASTestCase): | |||
1241 | 818 | target_dir = patch_zone_file_config_path(self) | 784 | target_dir = patch_zone_file_config_path(self) |
1242 | 819 | domain = factory.make_string() | 785 | domain = factory.make_string() |
1243 | 820 | ns_host_name = factory.make_name("ns") | 786 | ns_host_name = factory.make_name("ns") |
1245 | 821 | network = IPNetwork("192.168.0.1/22") | 787 | network = IPNetwork("192.168.0.1/24") |
1246 | 822 | dynamic_network = IPNetwork("192.168.0.1/28") | 788 | dynamic_network = IPNetwork("192.168.0.1/28") |
1247 | 823 | dns_zone_config = DNSReverseZoneConfig( | 789 | dns_zone_config = DNSReverseZoneConfig( |
1248 | 824 | domain, | 790 | domain, |
1249 | @@ -830,25 +796,24 @@ class TestDNSReverseZoneConfig(MAASTestCase): | |||
1250 | 830 | ], | 796 | ], |
1251 | 831 | ) | 797 | ) |
1252 | 832 | dns_zone_config.write_config() | 798 | dns_zone_config.write_config() |
1269 | 833 | for sub in range(4): | 799 | reverse_file_name = "zone.0.168.192.in-addr.arpa" |
1270 | 834 | reverse_file_name = f"zone.{sub}.168.192.in-addr.arpa" | 800 | expected_GEN_direct = dns_zone_config.get_GENERATE_directives( |
1271 | 835 | expected_GEN_direct = dns_zone_config.get_GENERATE_directives( | 801 | dynamic_network, |
1272 | 836 | dynamic_network, | 802 | domain, |
1273 | 837 | domain, | 803 | DomainInfo( |
1274 | 838 | DomainInfo( | 804 | IPNetwork("192.168.0.0/24"), |
1275 | 839 | IPNetwork(f"192.168.{sub}.0/24"), | 805 | "0.168.192.in-addr.arpa", |
1276 | 840 | f"{sub}.168.192.in-addr.arpa", | 806 | ), |
1277 | 841 | ), | 807 | ) |
1278 | 842 | ) | 808 | with open(os.path.join(target_dir, reverse_file_name), "r") as fh: |
1279 | 843 | with open(os.path.join(target_dir, reverse_file_name), "r") as fh: | 809 | contents = fh.read() |
1280 | 844 | contents = fh.read() | 810 | needles = [f"30 IN NS {ns_host_name}"] + [ |
1281 | 845 | needles = [f"30 IN NS {ns_host_name}"] + [ | 811 | f"$GENERATE {iterator_values} {reverse_dns} IN PTR {hostname}" |
1282 | 846 | f"$GENERATE {iterator_values} {reverse_dns} IN PTR {hostname}" | 812 | for iterator_values, reverse_dns, hostname in expected_GEN_direct |
1283 | 847 | for iterator_values, reverse_dns, hostname in expected_GEN_direct | 813 | ] |
1268 | 848 | ] | ||
1284 | 849 | 814 | ||
1287 | 850 | for needle in needles: | 815 | for needle in needles: |
1288 | 851 | self.assertIn(needle, contents) | 816 | self.assertIn(needle, contents) |
1289 | 852 | 817 | ||
1290 | 853 | def test_writes_reverse_dns_zone_config_for_small_network(self): | 818 | def test_writes_reverse_dns_zone_config_for_small_network(self): |
1291 | 854 | target_dir = patch_zone_file_config_path(self) | 819 | target_dir = patch_zone_file_config_path(self) |
1292 | @@ -1106,97 +1071,6 @@ class TestDNSReverseZoneConfig(MAASTestCase): | |||
1293 | 1106 | ], | 1071 | ], |
1294 | 1107 | ) | 1072 | ) |
1295 | 1108 | 1073 | ||
1296 | 1109 | def test_dynamic_updates_are_only_sent_for_specific_domain_info(self): | ||
1297 | 1110 | patch_zone_file_config_path(self) | ||
1298 | 1111 | domain = factory.make_string() | ||
1299 | 1112 | network = IPNetwork("10.246.64.0/21") | ||
1300 | 1113 | subnetwork1 = IPNetwork("10.246.64.0/24") | ||
1301 | 1114 | subnetwork2 = IPNetwork("10.246.65.0/24") | ||
1302 | 1115 | ip1 = factory.pick_ip_in_network(subnetwork1) | ||
1303 | 1116 | ip2 = factory.pick_ip_in_network(subnetwork2) | ||
1304 | 1117 | hostname1 = f"{factory.make_string()}.{domain}" | ||
1305 | 1118 | hostname2 = f"{factory.make_string()}.{domain}" | ||
1306 | 1119 | fwd_updates = [ | ||
1307 | 1120 | DynamicDNSUpdate( | ||
1308 | 1121 | operation="INSERT", | ||
1309 | 1122 | zone=domain, | ||
1310 | 1123 | name=hostname1, | ||
1311 | 1124 | rectype="A", | ||
1312 | 1125 | answer=ip1, | ||
1313 | 1126 | ), | ||
1314 | 1127 | DynamicDNSUpdate( | ||
1315 | 1128 | operation="INSERT", | ||
1316 | 1129 | zone=domain, | ||
1317 | 1130 | name=hostname2, | ||
1318 | 1131 | rectype="A", | ||
1319 | 1132 | answer=ip2, | ||
1320 | 1133 | ), | ||
1321 | 1134 | ] | ||
1322 | 1135 | rev_updates = [ | ||
1323 | 1136 | DynamicDNSUpdate.as_reverse_record_update(update, network) | ||
1324 | 1137 | for update in fwd_updates | ||
1325 | 1138 | ] | ||
1326 | 1139 | # gets changed to a /24 and any other space in the original | ||
1327 | 1140 | # subnet is split into a separate zone for a given /24 | ||
1328 | 1141 | zone = DNSReverseZoneConfig( | ||
1329 | 1142 | domain, | ||
1330 | 1143 | serial=random.randint(1, 100), | ||
1331 | 1144 | network=network, | ||
1332 | 1145 | dynamic_updates=rev_updates, | ||
1333 | 1146 | ) | ||
1334 | 1147 | |||
1335 | 1148 | run_command = self.patch(actions, "run_command") | ||
1336 | 1149 | zone.write_config() | ||
1337 | 1150 | zone.write_config() | ||
1338 | 1151 | |||
1339 | 1152 | expected_stdin1 = "\n".join( | ||
1340 | 1153 | [ | ||
1341 | 1154 | "server localhost", | ||
1342 | 1155 | "zone 64.246.10.in-addr.arpa", | ||
1343 | 1156 | f"update add {IPAddress(ip1).reverse_dns} {zone.default_ttl} PTR {hostname1}", | ||
1344 | 1157 | f"update add 64.246.10.in-addr.arpa {zone.default_ttl} SOA 64.246.10.in-addr.arpa. nobody.example.com. {zone.serial} 600 1800 604800 {zone.default_ttl}", | ||
1345 | 1158 | "send\n", | ||
1346 | 1159 | ] | ||
1347 | 1160 | ) | ||
1348 | 1161 | |||
1349 | 1162 | expected_stdin2 = "\n".join( | ||
1350 | 1163 | [ | ||
1351 | 1164 | "server localhost", | ||
1352 | 1165 | "zone 65.246.10.in-addr.arpa", | ||
1353 | 1166 | f"update add {IPAddress(ip2).reverse_dns} {zone.default_ttl} PTR {hostname2}", | ||
1354 | 1167 | f"update add 65.246.10.in-addr.arpa {zone.default_ttl} SOA 65.246.10.in-addr.arpa. nobody.example.com. {zone.serial} 600 1800 604800 {zone.default_ttl}", | ||
1355 | 1168 | "send\n", | ||
1356 | 1169 | ] | ||
1357 | 1170 | ) | ||
1358 | 1171 | |||
1359 | 1172 | expected_stdin3 = "\n".join( | ||
1360 | 1173 | [ | ||
1361 | 1174 | "server localhost", | ||
1362 | 1175 | "zone 71.246.10.in-addr.arpa", | ||
1363 | 1176 | f"update add 71.246.10.in-addr.arpa {zone.default_ttl} SOA 71.246.10.in-addr.arpa. nobody.example.com. {zone.serial} 600 1800 604800 {zone.default_ttl}", | ||
1364 | 1177 | "send\n", | ||
1365 | 1178 | ] | ||
1366 | 1179 | ) | ||
1367 | 1180 | |||
1368 | 1181 | run_command.assert_any_call( | ||
1369 | 1182 | "nsupdate", | ||
1370 | 1183 | "-k", | ||
1371 | 1184 | get_nsupdate_key_path(), | ||
1372 | 1185 | stdin=expected_stdin1.encode("ascii"), | ||
1373 | 1186 | ) | ||
1374 | 1187 | run_command.assert_any_call( | ||
1375 | 1188 | "nsupdate", | ||
1376 | 1189 | "-k", | ||
1377 | 1190 | get_nsupdate_key_path(), | ||
1378 | 1191 | stdin=expected_stdin2.encode("ascii"), | ||
1379 | 1192 | ) | ||
1380 | 1193 | run_command.assert_any_call( | ||
1381 | 1194 | "nsupdate", | ||
1382 | 1195 | "-k", | ||
1383 | 1196 | get_nsupdate_key_path(), | ||
1384 | 1197 | stdin=expected_stdin3.encode("ascii"), | ||
1385 | 1198 | ) | ||
1386 | 1199 | |||
1387 | 1200 | 1074 | ||
1388 | 1201 | class TestDNSReverseZoneConfig_GetGenerateDirectives(MAASTestCase): | 1075 | class TestDNSReverseZoneConfig_GetGenerateDirectives(MAASTestCase): |
1389 | 1202 | """Tests for `DNSReverseZoneConfig.get_GENERATE_directives()`.""" | 1076 | """Tests for `DNSReverseZoneConfig.get_GENERATE_directives()`.""" |
1390 | diff --git a/src/provisioningserver/dns/zoneconfig.py b/src/provisioningserver/dns/zoneconfig.py | |||
1391 | index 2e37e19..cf8a8ef 100644 | |||
1392 | --- a/src/provisioningserver/dns/zoneconfig.py | |||
1393 | +++ b/src/provisioningserver/dns/zoneconfig.py | |||
1394 | @@ -413,30 +413,11 @@ class DNSReverseZoneConfig(DomainConfigBase): | |||
1395 | 413 | self._network = kwargs.pop("network", None) | 413 | self._network = kwargs.pop("network", None) |
1396 | 414 | self._dynamic_ranges = kwargs.pop("dynamic_ranges", []) | 414 | self._dynamic_ranges = kwargs.pop("dynamic_ranges", []) |
1397 | 415 | self._rfc2317_ranges = kwargs.pop("rfc2317_ranges", []) | 415 | self._rfc2317_ranges = kwargs.pop("rfc2317_ranges", []) |
1402 | 416 | self._exclude = kwargs.pop("exclude", set()) | 416 | zone_info = self.compose_zone_info(self._network) |
1399 | 417 | zone_info = self.compose_zone_info( | ||
1400 | 418 | self._network, exclude=self._exclude | ||
1401 | 419 | ) | ||
1403 | 420 | super().__init__(domain, zone_info=zone_info, **kwargs) | 417 | super().__init__(domain, zone_info=zone_info, **kwargs) |
1404 | 421 | 418 | ||
1405 | 422 | @classmethod | 419 | @classmethod |
1423 | 423 | def _skip_if_overlaps(cls, first, base, step, network, exclude): | 420 | def compose_zone_info(cls, network): |
1407 | 424 | for other_network in exclude: | ||
1408 | 425 | if ( | ||
1409 | 426 | first in other_network | ||
1410 | 427 | and network.prefixlen < other_network.prefixlen | ||
1411 | 428 | ): # allow the more specific overlapping subnet to create the zone config | ||
1412 | 429 | try: | ||
1413 | 430 | base += 1 | ||
1414 | 431 | first += step | ||
1415 | 432 | except IndexError: | ||
1416 | 433 | # IndexError occurs when we go from 255.255.255.255 to | ||
1417 | 434 | # 0.0.0.0. If we hit that, we're all fine and done. | ||
1418 | 435 | break | ||
1419 | 436 | return (first, base) | ||
1420 | 437 | |||
1421 | 438 | @classmethod | ||
1422 | 439 | def compose_zone_info(cls, network, exclude=()): | ||
1424 | 440 | """Return the names of the reverse zones.""" | 421 | """Return the names of the reverse zones.""" |
1425 | 441 | # Generate the name of the reverse zone file: | 422 | # Generate the name of the reverse zone file: |
1426 | 442 | # Use netaddr's reverse_dns() to get the reverse IP name | 423 | # Use netaddr's reverse_dns() to get the reverse IP name |
1427 | @@ -444,9 +425,7 @@ class DNSReverseZoneConfig(DomainConfigBase): | |||
1428 | 444 | # octets of that name (i.e. drop the octets that will be specified in | 425 | # octets of that name (i.e. drop the octets that will be specified in |
1429 | 445 | # the zone file). | 426 | # the zone file). |
1430 | 446 | # returns a list of (IPNetwork, zone_name, zonefile_path) tuples | 427 | # returns a list of (IPNetwork, zone_name, zonefile_path) tuples |
1431 | 447 | info = [] | ||
1432 | 448 | first = IPAddress(network.first) | 428 | first = IPAddress(network.first) |
1433 | 449 | last = IPAddress(network.last) | ||
1434 | 450 | if first.version == 6: | 429 | if first.version == 6: |
1435 | 451 | # IPv6. | 430 | # IPv6. |
1436 | 452 | # 2001:89ab::/19 yields 8.1.0.0.2.ip6.arpa, and the full list | 431 | # 2001:89ab::/19 yields 8.1.0.0.2.ip6.arpa, and the full list |
1437 | @@ -488,40 +467,23 @@ class DNSReverseZoneConfig(DomainConfigBase): | |||
1438 | 488 | split_zone = first.reverse_dns.split(".") | 467 | split_zone = first.reverse_dns.split(".") |
1439 | 489 | zone_rest = ".".join(split_zone[rest_limit:-1]) | 468 | zone_rest = ".".join(split_zone[rest_limit:-1]) |
1440 | 490 | base = int(split_zone[rest_limit - 1]) | 469 | base = int(split_zone[rest_limit - 1]) |
1460 | 491 | while first <= last: | 470 | |
1461 | 492 | (first, base) = cls._skip_if_overlaps( | 471 | # Rest_limit has bounds of 1..labelcount+1 (5 or 33). |
1462 | 493 | first, base, step, network, exclude | 472 | # If we're stripping any elements, then we just want base.name. |
1463 | 494 | ) | 473 | if rest_limit > 1: |
1464 | 495 | if first > last: | 474 | if first.version == 6: |
1465 | 496 | # if the excluding subnet pushes the base IP beyond the bounds of the generating subnet, we've reached the end and return early | 475 | new_zone = f"{base:x}.{zone_rest}" |
1447 | 497 | return info | ||
1448 | 498 | |||
1449 | 499 | # Rest_limit has bounds of 1..labelcount+1 (5 or 33). | ||
1450 | 500 | # If we're stripping any elements, then we just want base.name. | ||
1451 | 501 | if rest_limit > 1: | ||
1452 | 502 | if first.version == 6: | ||
1453 | 503 | new_zone = f"{base:x}.{zone_rest}" | ||
1454 | 504 | else: | ||
1455 | 505 | new_zone = "%d.%s" % (base, zone_rest) | ||
1456 | 506 | # We didn't actually strip any elemnts, so base goes back with | ||
1457 | 507 | # the prefixlen attached. | ||
1458 | 508 | elif first.version == 6: | ||
1459 | 509 | new_zone = "%x-%d.%s" % (base, network.prefixlen, zone_rest) | ||
1466 | 510 | else: | 476 | else: |
1481 | 511 | new_zone = "%d-%d.%s" % (base, network.prefixlen, zone_rest) | 477 | new_zone = f"{base:d}.{zone_rest}" |
1482 | 512 | info.append( | 478 | # We didn't actually strip any elemnts, so base goes back with |
1483 | 513 | DomainInfo( | 479 | # the prefixlen attached. |
1484 | 514 | IPNetwork("%s/%d" % (first, subnet_prefix)), new_zone | 480 | elif first.version == 6: |
1485 | 515 | ) | 481 | new_zone = f"{base:x}-{network.prefixlen:d}.{zone_rest}" |
1486 | 516 | ) | 482 | else: |
1487 | 517 | base += 1 | 483 | new_zone = f"{base:d}-{network.prefixlen:d}.{zone_rest}" |
1488 | 518 | try: | 484 | return [ |
1489 | 519 | first += step | 485 | DomainInfo(IPNetwork(f"{first}/{subnet_prefix:d}"), new_zone), |
1490 | 520 | except IndexError: | 486 | ] |
1477 | 521 | # IndexError occurs when we go from 255.255.255.255 to | ||
1478 | 522 | # 0.0.0.0. If we hit that, we're all fine and done. | ||
1479 | 523 | break | ||
1480 | 524 | return info | ||
1491 | 525 | 487 | ||
1492 | 526 | @classmethod | 488 | @classmethod |
1493 | 527 | def get_PTR_mapping(cls, mapping, network): | 489 | def get_PTR_mapping(cls, mapping, network): |
UNIT TESTS _subnets_ bind_misconfigu re lp:~cgrabowski/maas/+git/maas into -b master lp:~maas-committers/maas
-b fix_overlapping
STATUS: FAILED maas-ci. internal: 8080/job/ maas-tester/ 4217/console 944ec179a77f072 3727071874
LOG: http://
COMMIT: 8498fd5425c23a1