Merge lp:~lamont/maas/dns-2.0 into lp:~maas-committers/maas/trunk
- dns-2.0
- Merge into trunk
Status: | Merged |
---|---|
Approved by: | LaMont Jones |
Approved revision: | no longer in the source branch. |
Merged at revision: | 4528 |
Proposed branch: | lp:~lamont/maas/dns-2.0 |
Merge into: | lp:~maas-committers/maas/trunk |
Diff against target: |
917 lines (+389/-151) 7 files modified
etc/maas/templates/dns/named.conf.template (+4/-2) src/maasserver/dns/config.py (+4/-3) src/provisioningserver/dns/actions.py (+17/-12) src/provisioningserver/dns/config.py (+15/-4) src/provisioningserver/dns/tests/test_actions.py (+4/-4) src/provisioningserver/dns/tests/test_zoneconfig.py (+179/-52) src/provisioningserver/dns/zoneconfig.py (+166/-74) |
To merge this branch: | bzr merge lp:~lamont/maas/dns-2.0 |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Mike Pontillo (community) | Approve | ||
Review via email: mp+278879@code.launchpad.net |
Commit message
Correct the management of reverse DNS zones in MAAS. (a /22 means that we create 4 /24 reverse zones, not one /16 reverse zone.)
Description of the change
Correct the management of reverse DNS zones in MAAS. (a /22 means that we create 4 /24 reverse zones, not one /16 reverse zone.)
LaMont Jones (lamont) wrote : | # |
Mike Pontillo (mpontillo) wrote : | # |
Nice work, LaMont.
I commented on a few minor issues below; in my mind, the only thing keeping this branch from landing is the 'assert', which we should gracefully log and skip instead, rather than crashing regiond.
See comments below.
Mike Pontillo (mpontillo) wrote : | # |
Looks good. Thanks for the fixes!
Andres Rodriguez (andreserl) wrote : | # |
Hi Guys,
Just keep in mind that fixing this means that the behavior of DNS completely changes, and as such, it is something that's not back portable.
LaMont Jones (lamont) wrote : | # |
> Hi Guys,
>
> Just keep in mind that fixing this means that the behavior of DNS completely
> changes, and as such, it is something that's not back portable.
Acknowledged.
- 4528. By LaMont Jones
-
[r=mpontillo][bug=1356012][author=lamont] Correct the management of reverse DNS zones in MAAS. (a /22 means that we create 4 /24 reverse zones, not one /16 reverse zone.)
Preview Diff
1 | === modified file 'etc/maas/templates/dns/named.conf.template' | |||
2 | --- etc/maas/templates/dns/named.conf.template 2014-11-14 21:50:42 +0000 | |||
3 | +++ etc/maas/templates/dns/named.conf.template 2015-11-28 05:29:35 +0000 | |||
4 | @@ -2,11 +2,13 @@ | |||
5 | 2 | 2 | ||
6 | 3 | # Zone declarations. | 3 | # Zone declarations. |
7 | 4 | {{for zone in zones}} | 4 | {{for zone in zones}} |
9 | 5 | zone "{{zone.zone_name}}" { | 5 | {{for zoneinfo in zone.zone_info}} |
10 | 6 | zone "{{zoneinfo.zone_name}}" { | ||
11 | 6 | type master; | 7 | type master; |
13 | 7 | file "{{zone.target_path}}"; | 8 | file "{{zoneinfo.target_path}}"; |
14 | 8 | }; | 9 | }; |
15 | 9 | {{endfor}} | 10 | {{endfor}} |
16 | 11 | {{endfor}} | ||
17 | 10 | 12 | ||
18 | 11 | # Access control for recursive queries. See named.conf.options.inside.maas | 13 | # Access control for recursive queries. See named.conf.options.inside.maas |
19 | 12 | # for the directives used on this ACL. | 14 | # for the directives used on this ACL. |
20 | 13 | 15 | ||
21 | === modified file 'src/maasserver/dns/config.py' | |||
22 | --- src/maasserver/dns/config.py 2015-09-24 16:22:12 +0000 | |||
23 | +++ src/maasserver/dns/config.py 2015-11-28 05:29:35 +0000 | |||
24 | @@ -50,7 +50,7 @@ | |||
25 | 50 | bind_reconfigure, | 50 | bind_reconfigure, |
26 | 51 | bind_reload, | 51 | bind_reload, |
27 | 52 | bind_reload_with_retries, | 52 | bind_reload_with_retries, |
29 | 53 | bind_reload_zone, | 53 | bind_reload_zones, |
30 | 54 | bind_write_configuration, | 54 | bind_write_configuration, |
31 | 55 | bind_write_options, | 55 | bind_write_options, |
32 | 56 | bind_write_zones, | 56 | bind_write_zones, |
33 | @@ -144,9 +144,10 @@ | |||
34 | 144 | 144 | ||
35 | 145 | serial = next_zone_serial() | 145 | serial = next_zone_serial() |
36 | 146 | for zone in ZoneGenerator(clusters, serial): | 146 | for zone in ZoneGenerator(clusters, serial): |
38 | 147 | maaslog.info("Generating new DNS zone file for %s", zone.zone_name) | 147 | names = [zi.zone_name for zi in zone.zone_info] |
39 | 148 | maaslog.info("Generating new DNS zone file for %s", " ".join(names)) | ||
40 | 148 | bind_write_zones([zone]) | 149 | bind_write_zones([zone]) |
42 | 149 | bind_reload_zone(zone.zone_name) | 150 | bind_reload_zones(names) |
43 | 150 | 151 | ||
44 | 151 | 152 | ||
45 | 152 | def dns_update_zones(clusters): | 153 | def dns_update_zones(clusters): |
46 | 153 | 154 | ||
47 | === modified file 'src/provisioningserver/dns/actions.py' | |||
48 | --- src/provisioningserver/dns/actions.py 2015-10-22 00:06:43 +0000 | |||
49 | +++ src/provisioningserver/dns/actions.py 2015-11-28 05:29:35 +0000 | |||
50 | @@ -15,7 +15,7 @@ | |||
51 | 15 | __all__ = [ | 15 | __all__ = [ |
52 | 16 | "bind_reconfigure", | 16 | "bind_reconfigure", |
53 | 17 | "bind_reload", | 17 | "bind_reload", |
55 | 18 | "bind_reload_zone", | 18 | "bind_reload_zones", |
56 | 19 | "bind_write_configuration", | 19 | "bind_write_configuration", |
57 | 20 | "bind_write_options", | 20 | "bind_write_options", |
58 | 21 | "bind_write_zones", | 21 | "bind_write_zones", |
59 | @@ -87,21 +87,26 @@ | |||
60 | 87 | sleep(interval) | 87 | sleep(interval) |
61 | 88 | 88 | ||
62 | 89 | 89 | ||
64 | 90 | def bind_reload_zone(zone_name): | 90 | def bind_reload_zones(zone_list): |
65 | 91 | """Ask BIND to reload the zone file for the given zone. | 91 | """Ask BIND to reload the zone file for the given zone. |
66 | 92 | 92 | ||
68 | 93 | :param zone_name: The name of the zone to reload. | 93 | :param zone_list: A list of zone names to reload, or a single name as a |
69 | 94 | string. | ||
70 | 94 | :return: True if success, False otherwise. | 95 | :return: True if success, False otherwise. |
71 | 95 | """ | 96 | """ |
81 | 96 | try: | 97 | ret = True |
82 | 97 | execute_rndc_command(("reload", zone_name)) | 98 | if not isinstance(zone_list, list): |
83 | 98 | return True | 99 | zone_list = [zone_list] |
84 | 99 | except CalledProcessError as exc: | 100 | for name in zone_list: |
85 | 100 | maaslog.error( | 101 | try: |
86 | 101 | "Reloading BIND zone %r failed (is it running?): %s", | 102 | execute_rndc_command(("reload", name)) |
87 | 102 | zone_name, | 103 | except CalledProcessError as exc: |
88 | 103 | exc) | 104 | maaslog.error( |
89 | 104 | return False | 105 | "Reloading BIND zone %r failed (is it running?): %s", |
90 | 106 | name, | ||
91 | 107 | exc) | ||
92 | 108 | ret = False | ||
93 | 109 | return ret | ||
94 | 105 | 110 | ||
95 | 106 | 111 | ||
96 | 107 | def bind_write_configuration(zones, trusted_networks): | 112 | def bind_write_configuration(zones, trusted_networks): |
97 | 108 | 113 | ||
98 | === modified file 'src/provisioningserver/dns/config.py' | |||
99 | --- src/provisioningserver/dns/config.py 2015-07-09 22:25:27 +0000 | |||
100 | +++ src/provisioningserver/dns/config.py 2015-11-28 05:29:35 +0000 | |||
101 | @@ -29,6 +29,7 @@ | |||
102 | 29 | import re | 29 | import re |
103 | 30 | import sys | 30 | import sys |
104 | 31 | 31 | ||
105 | 32 | from provisioningserver.logger import get_maas_logger | ||
106 | 32 | from provisioningserver.utils import locate_config | 33 | from provisioningserver.utils import locate_config |
107 | 33 | from provisioningserver.utils.fs import atomic_write | 34 | from provisioningserver.utils.fs import atomic_write |
108 | 34 | from provisioningserver.utils.isc import read_isc_file | 35 | from provisioningserver.utils.isc import read_isc_file |
109 | @@ -36,6 +37,7 @@ | |||
110 | 36 | import tempita | 37 | import tempita |
111 | 37 | 38 | ||
112 | 38 | 39 | ||
113 | 40 | maaslog = get_maas_logger("dns") | ||
114 | 39 | NAMED_CONF_OPTIONS = 'named.conf.options' | 41 | NAMED_CONF_OPTIONS = 'named.conf.options' |
115 | 40 | MAAS_NAMED_CONF_NAME = 'named.conf.maas' | 42 | MAAS_NAMED_CONF_NAME = 'named.conf.maas' |
116 | 41 | MAAS_NAMED_CONF_OPTIONS_INSIDE_NAME = 'named.conf.options.inside.maas' | 43 | MAAS_NAMED_CONF_OPTIONS_INSIDE_NAME = 'named.conf.options.inside.maas' |
117 | @@ -319,7 +321,16 @@ | |||
118 | 319 | 321 | ||
119 | 320 | @classmethod | 322 | @classmethod |
120 | 321 | def get_include_snippet(cls): | 323 | def get_include_snippet(cls): |
125 | 322 | target_path = compose_config_path(cls.target_file_name) | 324 | snippet = "" |
126 | 323 | assert '"' not in target_path, ( | 325 | if isinstance(cls.target_file_name, list): |
127 | 324 | "DNS config path contains quote: %s." % target_path) | 326 | target_file_names = cls.target_file_name |
128 | 325 | return 'include "%s";\n' % target_path | 327 | else: |
129 | 328 | target_file_names = [cls.target_file_name] | ||
130 | 329 | for target_file_name in target_file_names: | ||
131 | 330 | target_path = compose_config_path(target_file_name) | ||
132 | 331 | if '"' in target_path: | ||
133 | 332 | maaslog.error( | ||
134 | 333 | "DNS config path contains quote: %s." % target_path) | ||
135 | 334 | else: | ||
136 | 335 | snippet += 'include "%s";\n' % target_path | ||
137 | 336 | return snippet | ||
138 | 326 | 337 | ||
139 | === modified file 'src/provisioningserver/dns/tests/test_actions.py' | |||
140 | --- src/provisioningserver/dns/tests/test_actions.py 2015-10-22 00:06:43 +0000 | |||
141 | +++ src/provisioningserver/dns/tests/test_actions.py 2015-11-28 05:29:35 +0000 | |||
142 | @@ -144,11 +144,11 @@ | |||
143 | 144 | 144 | ||
144 | 145 | 145 | ||
145 | 146 | class TestReloadZone(MAASTestCase): | 146 | class TestReloadZone(MAASTestCase): |
147 | 147 | """Tests for :py:func:`actions.bind_reload_zone`.""" | 147 | """Tests for :py:func:`actions.bind_reload_zones`.""" |
148 | 148 | 148 | ||
149 | 149 | def test__executes_rndc_command(self): | 149 | def test__executes_rndc_command(self): |
150 | 150 | self.patch_autospec(actions, "execute_rndc_command") | 150 | self.patch_autospec(actions, "execute_rndc_command") |
152 | 151 | self.assertTrue(actions.bind_reload_zone(sentinel.zone)) | 151 | self.assertTrue(actions.bind_reload_zones(sentinel.zone)) |
153 | 152 | self.assertThat( | 152 | self.assertThat( |
154 | 153 | actions.execute_rndc_command, | 153 | actions.execute_rndc_command, |
155 | 154 | MockCalledOnceWith(("reload", sentinel.zone))) | 154 | MockCalledOnceWith(("reload", sentinel.zone))) |
156 | @@ -157,7 +157,7 @@ | |||
157 | 157 | erc = self.patch_autospec(actions, "execute_rndc_command") | 157 | erc = self.patch_autospec(actions, "execute_rndc_command") |
158 | 158 | erc.side_effect = factory.make_CalledProcessError() | 158 | erc.side_effect = factory.make_CalledProcessError() |
159 | 159 | with FakeLogger("maas") as logger: | 159 | with FakeLogger("maas") as logger: |
161 | 160 | self.assertFalse(actions.bind_reload_zone(sentinel.zone)) | 160 | self.assertFalse(actions.bind_reload_zones(sentinel.zone)) |
162 | 161 | self.assertDocTestMatches( | 161 | self.assertDocTestMatches( |
163 | 162 | "Reloading BIND zone ... failed (is it running?): " | 162 | "Reloading BIND zone ... failed (is it running?): " |
164 | 163 | "Command ... returned non-zero exit status ...", | 163 | "Command ... returned non-zero exit status ...", |
165 | @@ -166,7 +166,7 @@ | |||
166 | 166 | def test__false_on_subprocess_error(self): | 166 | def test__false_on_subprocess_error(self): |
167 | 167 | erc = self.patch_autospec(actions, "execute_rndc_command") | 167 | erc = self.patch_autospec(actions, "execute_rndc_command") |
168 | 168 | erc.side_effect = factory.make_CalledProcessError() | 168 | erc.side_effect = factory.make_CalledProcessError() |
170 | 169 | self.assertFalse(actions.bind_reload_zone(sentinel.zone)) | 169 | self.assertFalse(actions.bind_reload_zones(sentinel.zone)) |
171 | 170 | 170 | ||
172 | 171 | 171 | ||
173 | 172 | class TestConfiguration(PservTestCase): | 172 | class TestConfiguration(PservTestCase): |
174 | 173 | 173 | ||
175 | === modified file 'src/provisioningserver/dns/tests/test_zoneconfig.py' | |||
176 | --- src/provisioningserver/dns/tests/test_zoneconfig.py 2015-05-07 18:14:38 +0000 | |||
177 | +++ src/provisioningserver/dns/tests/test_zoneconfig.py 2015-11-28 05:29:35 +0000 | |||
178 | @@ -37,6 +37,7 @@ | |||
179 | 37 | from provisioningserver.dns.zoneconfig import ( | 37 | from provisioningserver.dns.zoneconfig import ( |
180 | 38 | DNSForwardZoneConfig, | 38 | DNSForwardZoneConfig, |
181 | 39 | DNSReverseZoneConfig, | 39 | DNSReverseZoneConfig, |
182 | 40 | DNSZoneInfo, | ||
183 | 40 | ) | 41 | ) |
184 | 41 | from testtools.matchers import ( | 42 | from testtools.matchers import ( |
185 | 42 | Contains, | 43 | Contains, |
186 | @@ -102,7 +103,7 @@ | |||
187 | 102 | dns_zone_config = DNSForwardZoneConfig(domain) | 103 | dns_zone_config = DNSForwardZoneConfig(domain) |
188 | 103 | self.assertEqual( | 104 | self.assertEqual( |
189 | 104 | os.path.join(get_dns_config_dir(), 'zone.%s' % domain), | 105 | os.path.join(get_dns_config_dir(), 'zone.%s' % domain), |
191 | 105 | dns_zone_config.target_path) | 106 | dns_zone_config.zone_info[0].target_path) |
192 | 106 | 107 | ||
193 | 107 | def test_get_a_mapping_returns_ipv4_mapping(self): | 108 | def test_get_a_mapping_returns_ipv4_mapping(self): |
194 | 108 | name = factory.make_string() | 109 | name = factory.make_string() |
195 | @@ -266,7 +267,7 @@ | |||
196 | 266 | factory.make_string(), serial=random.randint(1, 100), | 267 | factory.make_string(), serial=random.randint(1, 100), |
197 | 267 | dns_ip=factory.make_ipv4_address()) | 268 | dns_ip=factory.make_ipv4_address()) |
198 | 268 | dns_zone_config.write_config() | 269 | dns_zone_config.write_config() |
200 | 269 | filepath = FilePath(dns_zone_config.target_path) | 270 | filepath = FilePath(dns_zone_config.zone_info[0].target_path) |
201 | 270 | self.assertTrue(filepath.getPermissions().other.read) | 271 | self.assertTrue(filepath.getPermissions().other.read) |
202 | 271 | 272 | ||
203 | 272 | 273 | ||
204 | @@ -290,34 +291,109 @@ | |||
205 | 290 | 291 | ||
206 | 291 | def test_computes_dns_config_file_paths(self): | 292 | def test_computes_dns_config_file_paths(self): |
207 | 292 | domain = factory.make_name('zone') | 293 | domain = factory.make_name('zone') |
209 | 293 | reverse_file_name = 'zone.168.192.in-addr.arpa' | 294 | reverse_file_name = [ |
210 | 295 | 'zone.%d.168.192.in-addr.arpa' % i for i in range(4)] | ||
211 | 294 | dns_zone_config = DNSReverseZoneConfig( | 296 | dns_zone_config = DNSReverseZoneConfig( |
212 | 295 | domain, network=IPNetwork("192.168.0.0/22")) | 297 | domain, network=IPNetwork("192.168.0.0/22")) |
213 | 298 | for i in range(4): | ||
214 | 299 | self.assertEqual( | ||
215 | 300 | os.path.join(get_dns_config_dir(), reverse_file_name[i]), | ||
216 | 301 | dns_zone_config.zone_info[i].target_path) | ||
217 | 302 | |||
218 | 303 | def test_computes_dns_config_file_paths_for_small_network(self): | ||
219 | 304 | domain = factory.make_name('zone') | ||
220 | 305 | reverse_file_name = 'zone.192-27.0.168.192.in-addr.arpa' | ||
221 | 306 | dns_zone_config = DNSReverseZoneConfig( | ||
222 | 307 | domain, network=IPNetwork("192.168.0.192/27")) | ||
223 | 308 | self.assertEqual(1, len(dns_zone_config.zone_info)) | ||
224 | 296 | self.assertEqual( | 309 | self.assertEqual( |
225 | 297 | os.path.join(get_dns_config_dir(), reverse_file_name), | 310 | os.path.join(get_dns_config_dir(), reverse_file_name), |
227 | 298 | dns_zone_config.target_path) | 311 | dns_zone_config.zone_info[0].target_path) |
228 | 299 | 312 | ||
229 | 300 | def test_reverse_zone_file(self): | 313 | def test_reverse_zone_file(self): |
230 | 301 | # DNSReverseZoneConfig calculates the reverse zone file name | 314 | # DNSReverseZoneConfig calculates the reverse zone file name |
231 | 302 | # correctly for IPv4 and IPv6 networks. | 315 | # correctly for IPv4 and IPv6 networks. |
232 | 316 | # As long as the network size ends on a "nice" boundary (multiple of | ||
233 | 317 | # 8 for IPv4, multiple of 4 for IPv6), then there will be one reverse | ||
234 | 318 | # zone for the subnet. When it isn't, then there will be 2-128 | ||
235 | 319 | # individual reverse zones for the subnet. | ||
236 | 320 | # A special case is the small subnet (less than 256 hosts for IPv4, | ||
237 | 321 | # less than 16 hosts for IPv6), in which case, we follow RFC2317 with | ||
238 | 322 | # the modern adjustment of using '-' instead of '/'. | ||
239 | 323 | 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" | ||
240 | 303 | expected = [ | 324 | expected = [ |
241 | 304 | # IPv4 networks. | 325 | # IPv4 networks. |
244 | 305 | (IPNetwork('192.168.0.1/22'), '168.192.in-addr.arpa'), | 326 | # /22 ==> 4 /24 reverse zones |
245 | 306 | (IPNetwork('192.168.0.1/24'), '0.168.192.in-addr.arpa'), | 327 | ( |
246 | 328 | IPNetwork('192.168.0.1/22'), | ||
247 | 329 | [DNSZoneInfo( | ||
248 | 330 | IPNetwork('192.168.%d.0/24' % i), | ||
249 | 331 | '%d.168.192.in-addr.arpa' % i) for i in range(4)] | ||
250 | 332 | ), | ||
251 | 333 | # /24 ==> 1 reverse zone | ||
252 | 334 | ( | ||
253 | 335 | IPNetwork('192.168.0.1/24'), | ||
254 | 336 | [DNSZoneInfo( | ||
255 | 337 | IPNetwork('192.168.0.0/24'), | ||
256 | 338 | '0.168.192.in-addr.arpa')] | ||
257 | 339 | ), | ||
258 | 340 | # /29 ==> 1 reverse zones, per RFC2317 | ||
259 | 341 | ( | ||
260 | 342 | IPNetwork('192.168.0.241/29'), | ||
261 | 343 | [DNSZoneInfo( | ||
262 | 344 | IPNetwork('192.168.0.240/29'), | ||
263 | 345 | '240-29.0.168.192.in-addr.arpa')] | ||
264 | 346 | ), | ||
265 | 307 | # IPv6 networks. | 347 | # IPv6 networks. |
268 | 308 | (IPNetwork('3ffe:801::/32'), '1.0.8.0.e.f.f.3.ip6.arpa'), | 348 | # /32, 48, 56, 64 ==> 1 reverse zones |
269 | 309 | (IPNetwork('2001:db8:0::/48'), '0.0.0.0.8.b.d.0.1.0.0.2.ip6.arpa'), | 349 | ( |
270 | 350 | IPNetwork('3ffe:801::/32'), | ||
271 | 351 | [DNSZoneInfo( | ||
272 | 352 | IPNetwork('3ffe:801::32'), | ||
273 | 353 | '1.0.8.0.e.f.f.3.ip6.arpa')]), | ||
274 | 354 | ( | ||
275 | 355 | IPNetwork('2001:db8:0::/48'), | ||
276 | 356 | [DNSZoneInfo( | ||
277 | 357 | IPNetwork('2001:db8:0::/48'), | ||
278 | 358 | '0.0.0.0.8.b.d.0.1.0.0.2.ip6.arpa')] | ||
279 | 359 | ), | ||
280 | 310 | ( | 360 | ( |
281 | 311 | IPNetwork('2001:ba8:1f1:400::/56'), | 361 | IPNetwork('2001:ba8:1f1:400::/56'), |
283 | 312 | '4.0.1.f.1.0.8.a.b.0.1.0.0.2.ip6.arpa' | 362 | [DNSZoneInfo( |
284 | 363 | IPNetwork('2001:ba8:1f1:400::/56'), | ||
285 | 364 | '4.0.1.f.1.0.8.a.b.0.1.0.0.2.ip6.arpa')], | ||
286 | 313 | ), | 365 | ), |
287 | 314 | ( | 366 | ( |
288 | 315 | IPNetwork('2610:8:6800:1::/64'), | 367 | IPNetwork('2610:8:6800:1::/64'), |
291 | 316 | '1.0.0.0.0.0.8.6.8.0.0.0.0.1.6.2.ip6.arpa', | 368 | [DNSZoneInfo( |
292 | 317 | ), | 369 | IPNetwork('2610:8:6800:1::/64'), |
293 | 370 | '1.0.0.0.0.0.8.6.8.0.0.0.0.1.6.2.ip6.arpa')], | ||
294 | 371 | ), | ||
295 | 372 | # /2 with hex digits ==> 4 /4 reverse zones | ||
296 | 373 | ( | ||
297 | 374 | IPNetwork('8000::/2'), | ||
298 | 375 | [ | ||
299 | 376 | DNSZoneInfo(IPNetwork('8000::/4'), '8.ip6.arpa'), | ||
300 | 377 | DNSZoneInfo(IPNetwork('9000::/4'), '9.ip6.arpa'), | ||
301 | 378 | DNSZoneInfo(IPNetwork('a000::/4'), 'a.ip6.arpa'), | ||
302 | 379 | DNSZoneInfo(IPNetwork('b000::/4'), 'b.ip6.arpa'), | ||
303 | 380 | ], | ||
304 | 381 | ), | ||
305 | 382 | # /103 ==> 2 /104 reverse zones | ||
306 | 318 | ( | 383 | ( |
307 | 319 | IPNetwork('2001:ba8:1f1:400::/103'), | 384 | IPNetwork('2001:ba8:1f1:400::/103'), |
309 | 320 | '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', | 385 | [DNSZoneInfo( |
310 | 386 | IPNetwork('2001:ba8:1f1:400:0:0:%d00:0000/104' % i), | ||
311 | 387 | zn % i) for i in range(2)], | ||
312 | 388 | ), | ||
313 | 389 | # /125 ==> 1 reverse zone, based on RFC2317 | ||
314 | 390 | ( | ||
315 | 391 | IPNetwork('2001:ba8:1f1:400::/125'), | ||
316 | 392 | [DNSZoneInfo( | ||
317 | 393 | IPNetwork('2001:ba8:1f1:400::/125'), | ||
318 | 394 | "0-125.%s" % ( | ||
319 | 395 | IPAddress('2001:ba8:1f1:400::').reverse_dns[2:-1]))], | ||
320 | 396 | |||
321 | 321 | ), | 397 | ), |
322 | 322 | 398 | ||
323 | 323 | ] | 399 | ] |
324 | @@ -325,8 +401,15 @@ | |||
325 | 325 | for network, _ in expected: | 401 | for network, _ in expected: |
326 | 326 | domain = factory.make_name('zone') | 402 | domain = factory.make_name('zone') |
327 | 327 | dns_zone_config = DNSReverseZoneConfig(domain, network=network) | 403 | dns_zone_config = DNSReverseZoneConfig(domain, network=network) |
330 | 328 | results.append((network, dns_zone_config.zone_name)) | 404 | results.append((network, dns_zone_config.zone_info)) |
331 | 329 | self.assertEqual(expected, results) | 405 | # Make sure we have the right number of elements. |
332 | 406 | self.assertEqual(len(expected), len(results)) | ||
333 | 407 | # And that the zone names chosen for each element are correct. | ||
334 | 408 | for net in range(len(expected)): | ||
335 | 409 | for zi in range(len(expected[net][1])): | ||
336 | 410 | self.assertItemsEqual( | ||
337 | 411 | expected[net][1][zi].zone_name, | ||
338 | 412 | results[net][1][zi].zone_name) | ||
339 | 330 | 413 | ||
340 | 331 | def test_get_ptr_mapping(self): | 414 | def test_get_ptr_mapping(self): |
341 | 332 | name = factory.make_string() | 415 | name = factory.make_string() |
342 | @@ -378,11 +461,12 @@ | |||
343 | 378 | factory.make_string(), serial=random.randint(1, 100), | 461 | factory.make_string(), serial=random.randint(1, 100), |
344 | 379 | network=network) | 462 | network=network) |
345 | 380 | dns_zone_config.write_config() | 463 | dns_zone_config.write_config() |
351 | 381 | self.assertThat( | 464 | for zone_name in [zi.zone_name for zi in dns_zone_config.zone_info]: |
352 | 382 | os.path.join( | 465 | self.assertThat( |
353 | 383 | target_dir, 'zone.%s' % dns_zone_config.zone_name), | 466 | os.path.join( |
354 | 384 | FileContains( | 467 | target_dir, 'zone.%s' % zone_name), |
355 | 385 | matcher=Contains('IN NS %s.' % dns_zone_config.domain))) | 468 | FileContains( |
356 | 469 | matcher=Contains('IN NS %s.' % dns_zone_config.domain))) | ||
357 | 386 | 470 | ||
358 | 387 | def test_writes_reverse_dns_zone_config(self): | 471 | def test_writes_reverse_dns_zone_config(self): |
359 | 388 | target_dir = patch_dns_config_path(self) | 472 | target_dir = patch_dns_config_path(self) |
360 | @@ -394,9 +478,41 @@ | |||
361 | 394 | dynamic_ranges=[ | 478 | dynamic_ranges=[ |
362 | 395 | IPRange(dynamic_network.first, dynamic_network.last)]) | 479 | IPRange(dynamic_network.first, dynamic_network.last)]) |
363 | 396 | dns_zone_config.write_config() | 480 | dns_zone_config.write_config() |
367 | 397 | reverse_file_name = 'zone.168.192.in-addr.arpa' | 481 | for sub in range(4): |
368 | 398 | expected_generate_directives = dns_zone_config.get_GENERATE_directives( | 482 | reverse_file_name = 'zone.%d.168.192.in-addr.arpa' % sub |
369 | 399 | dynamic_network, domain) | 483 | expected_GEN_direct = dns_zone_config.get_GENERATE_directives( |
370 | 484 | dynamic_network, domain, | ||
371 | 485 | DNSZoneInfo( | ||
372 | 486 | IPNetwork('192.168.%d.0/24' % sub), | ||
373 | 487 | "%d.168.192.in-addr.arpa" % sub)) | ||
374 | 488 | expected = ContainsAll( | ||
375 | 489 | [ | ||
376 | 490 | 'IN NS %s' % domain | ||
377 | 491 | ] + | ||
378 | 492 | [ | ||
379 | 493 | '$GENERATE %s %s IN PTR %s' % ( | ||
380 | 494 | iterator_values, reverse_dns, hostname) | ||
381 | 495 | for iterator_values, reverse_dns, hostname in | ||
382 | 496 | expected_GEN_direct | ||
383 | 497 | ]) | ||
384 | 498 | self.assertThat( | ||
385 | 499 | os.path.join(target_dir, reverse_file_name), | ||
386 | 500 | FileContains(matcher=expected)) | ||
387 | 501 | |||
388 | 502 | def test_writes_reverse_dns_zone_config_for_small_network(self): | ||
389 | 503 | target_dir = patch_dns_config_path(self) | ||
390 | 504 | domain = factory.make_string() | ||
391 | 505 | network = IPNetwork('192.168.0.1/26') | ||
392 | 506 | dynamic_network = IPNetwork('192.168.0.1/28') | ||
393 | 507 | dns_zone_config = DNSReverseZoneConfig( | ||
394 | 508 | domain, serial=random.randint(1, 100), network=network, | ||
395 | 509 | dynamic_ranges=[ | ||
396 | 510 | IPRange(dynamic_network.first, dynamic_network.last)]) | ||
397 | 511 | dns_zone_config.write_config() | ||
398 | 512 | reverse_zone_name = '0-26.0.168.192.in-addr.arpa' | ||
399 | 513 | reverse_file_name = 'zone.0-26.0.168.192.in-addr.arpa' | ||
400 | 514 | expected_GEN_direct = dns_zone_config.get_GENERATE_directives( | ||
401 | 515 | dynamic_network, domain, DNSZoneInfo(network, reverse_zone_name)) | ||
402 | 400 | expected = ContainsAll( | 516 | expected = ContainsAll( |
403 | 401 | [ | 517 | [ |
404 | 402 | 'IN NS %s' % domain | 518 | 'IN NS %s' % domain |
405 | @@ -405,7 +521,7 @@ | |||
406 | 405 | '$GENERATE %s %s IN PTR %s' % ( | 521 | '$GENERATE %s %s IN PTR %s' % ( |
407 | 406 | iterator_values, reverse_dns, hostname) | 522 | iterator_values, reverse_dns, hostname) |
408 | 407 | for iterator_values, reverse_dns, hostname in | 523 | for iterator_values, reverse_dns, hostname in |
410 | 408 | expected_generate_directives | 524 | expected_GEN_direct |
411 | 409 | ]) | 525 | ]) |
412 | 410 | self.assertThat( | 526 | self.assertThat( |
413 | 411 | os.path.join(target_dir, reverse_file_name), | 527 | os.path.join(target_dir, reverse_file_name), |
414 | @@ -431,8 +547,9 @@ | |||
415 | 431 | factory.make_string(), serial=random.randint(1, 100), | 547 | factory.make_string(), serial=random.randint(1, 100), |
416 | 432 | network=factory.make_ipv4_network()) | 548 | network=factory.make_ipv4_network()) |
417 | 433 | dns_zone_config.write_config() | 549 | dns_zone_config.write_config() |
420 | 434 | filepath = FilePath(dns_zone_config.target_path) | 550 | for tgt in [zi.target_path for zi in dns_zone_config.zone_info]: |
421 | 435 | self.assertTrue(filepath.getPermissions().other.read) | 551 | filepath = FilePath(tgt) |
422 | 552 | self.assertTrue(filepath.getPermissions().other.read) | ||
423 | 436 | 553 | ||
424 | 437 | 554 | ||
425 | 438 | class TestDNSReverseZoneConfig_GetGenerateDirectives(MAASTestCase): | 555 | class TestDNSReverseZoneConfig_GetGenerateDirectives(MAASTestCase): |
426 | @@ -451,7 +568,11 @@ | |||
427 | 451 | self.assertItemsEqual( | 568 | self.assertItemsEqual( |
428 | 452 | expected_directives, | 569 | expected_directives, |
429 | 453 | DNSReverseZoneConfig.get_GENERATE_directives( | 570 | DNSReverseZoneConfig.get_GENERATE_directives( |
431 | 454 | ip_range, domain="domain")) | 571 | ip_range, |
432 | 572 | domain="domain", | ||
433 | 573 | zone_info=DNSZoneInfo( | ||
434 | 574 | IPNetwork('192.168.0.0/16'), | ||
435 | 575 | "168.192.in-addr.arpa"))) | ||
436 | 455 | 576 | ||
437 | 456 | def get_expected_generate_directives(self, network, domain): | 577 | def get_expected_generate_directives(self, network, domain): |
438 | 457 | ip_parts = network.network.format().split('.') | 578 | ip_parts = network.network.format().split('.') |
439 | @@ -481,8 +602,11 @@ | |||
440 | 481 | relevant_ip_parts.reverse() | 602 | relevant_ip_parts.reverse() |
441 | 482 | expected_rdns_base = ( | 603 | expected_rdns_base = ( |
442 | 483 | "%s.%s.in-addr.arpa." % tuple(relevant_ip_parts)) | 604 | "%s.%s.in-addr.arpa." % tuple(relevant_ip_parts)) |
445 | 484 | expected_rdns_template = "$.%s.%s" % ( | 605 | if network.size >= 256: |
446 | 485 | num + second_octet_offset, expected_rdns_base) | 606 | expected_rdns_template = "$.%s.%s" % ( |
447 | 607 | num + second_octet_offset, expected_rdns_base) | ||
448 | 608 | else: | ||
449 | 609 | expected_rdns_template = "$" | ||
450 | 486 | expected_generate_directives.append( | 610 | expected_generate_directives.append( |
451 | 487 | ( | 611 | ( |
452 | 488 | "%s-%s" % (iterator_low, iterator_high), | 612 | "%s-%s" % (iterator_low, iterator_high), |
453 | @@ -494,22 +618,25 @@ | |||
454 | 494 | 618 | ||
455 | 495 | def test_returns_single_entry_for_slash_24_network(self): | 619 | def test_returns_single_entry_for_slash_24_network(self): |
456 | 496 | network = IPNetwork("%s/24" % factory.make_ipv4_address()) | 620 | network = IPNetwork("%s/24" % factory.make_ipv4_address()) |
457 | 621 | reverse = ".".join(IPAddress(network).reverse_dns.split('.')[1:-1]) | ||
458 | 497 | domain = factory.make_string() | 622 | domain = factory.make_string() |
459 | 498 | expected_generate_directives = self.get_expected_generate_directives( | 623 | expected_generate_directives = self.get_expected_generate_directives( |
460 | 499 | network, domain) | 624 | network, domain) |
461 | 500 | directives = DNSReverseZoneConfig.get_GENERATE_directives( | 625 | directives = DNSReverseZoneConfig.get_GENERATE_directives( |
463 | 501 | network, domain) | 626 | network, domain, DNSZoneInfo(network, reverse)) |
464 | 502 | self.expectThat(directives, HasLength(1)) | 627 | self.expectThat(directives, HasLength(1)) |
465 | 503 | self.assertItemsEqual(expected_generate_directives, directives) | 628 | self.assertItemsEqual(expected_generate_directives, directives) |
466 | 504 | 629 | ||
467 | 505 | def test_returns_single_entry_for_tiny_network(self): | 630 | def test_returns_single_entry_for_tiny_network(self): |
468 | 506 | network = IPNetwork("%s/28" % factory.make_ipv4_address()) | 631 | network = IPNetwork("%s/28" % factory.make_ipv4_address()) |
469 | 632 | reverse = IPAddress(network).reverse_dns.split('.') | ||
470 | 633 | reverse = ".".join(["%s-28" % reverse[0]] + reverse[1:-1]) | ||
471 | 507 | domain = factory.make_string() | 634 | domain = factory.make_string() |
472 | 508 | 635 | ||
473 | 509 | expected_generate_directives = self.get_expected_generate_directives( | 636 | expected_generate_directives = self.get_expected_generate_directives( |
474 | 510 | network, domain) | 637 | network, domain) |
475 | 511 | directives = DNSReverseZoneConfig.get_GENERATE_directives( | 638 | directives = DNSReverseZoneConfig.get_GENERATE_directives( |
477 | 512 | network, domain) | 639 | network, domain, DNSZoneInfo(network, reverse)) |
478 | 513 | self.expectThat(directives, HasLength(1)) | 640 | self.expectThat(directives, HasLength(1)) |
479 | 514 | self.assertItemsEqual(expected_generate_directives, directives) | 641 | self.assertItemsEqual(expected_generate_directives, directives) |
480 | 515 | 642 | ||
481 | @@ -517,31 +644,22 @@ | |||
482 | 517 | ip_range = IPRange('10.0.0.1', '10.0.0.255') | 644 | ip_range = IPRange('10.0.0.1', '10.0.0.255') |
483 | 518 | domain = factory.make_string() | 645 | domain = factory.make_string() |
484 | 519 | directives = DNSReverseZoneConfig.get_GENERATE_directives( | 646 | directives = DNSReverseZoneConfig.get_GENERATE_directives( |
486 | 520 | ip_range, domain) | 647 | ip_range, domain, |
487 | 648 | DNSZoneInfo(IPNetwork('10.0.0.0/24'), '0.0.10.in-addr.arpa')) | ||
488 | 521 | self.expectThat(directives, HasLength(1)) | 649 | self.expectThat(directives, HasLength(1)) |
489 | 522 | 650 | ||
505 | 523 | def test_dtrt_for_larger_networks(self): | 651 | # generate 2 zones, rather than 1 zone with 2 GENERATEs. |
506 | 524 | # For every other network size that we're not explicitly | 652 | def test_returns_256_entries_for_slash_16_network(self): |
507 | 525 | # testing here, | 653 | network = IPNetwork(factory.make_ipv4_network(slash=16)) |
508 | 526 | # DNSReverseZoneConfig.get_GENERATE_directives() will return | 654 | reverse = IPAddress(network.first).reverse_dns.split('.')[2:-1] |
509 | 527 | # one GENERATE directive for every 255 addresses in the network. | 655 | reverse = ".".join(reverse) |
495 | 528 | for prefixlen in range(23, 17): | ||
496 | 529 | network = IPNetwork( | ||
497 | 530 | "%s/%s" % (factory.make_ipv4_address(), prefixlen)) | ||
498 | 531 | domain = factory.make_string() | ||
499 | 532 | directives = DNSReverseZoneConfig.get_GENERATE_directives( | ||
500 | 533 | network, domain) | ||
501 | 534 | self.expectThat(directives, HasLength(network.size / 256)) | ||
502 | 535 | |||
503 | 536 | def test_returns_two_entries_for_slash_23_network(self): | ||
504 | 537 | network = IPNetwork(factory.make_ipv4_network(slash=23)) | ||
510 | 538 | domain = factory.make_string() | 656 | domain = factory.make_string() |
511 | 539 | 657 | ||
512 | 540 | expected_generate_directives = self.get_expected_generate_directives( | 658 | expected_generate_directives = self.get_expected_generate_directives( |
513 | 541 | network, domain) | 659 | network, domain) |
514 | 542 | directives = DNSReverseZoneConfig.get_GENERATE_directives( | 660 | directives = DNSReverseZoneConfig.get_GENERATE_directives( |
517 | 543 | network, domain) | 661 | network, domain, DNSZoneInfo(network, reverse)) |
518 | 544 | self.expectThat(directives, HasLength(2)) | 662 | self.expectThat(directives, HasLength(256)) |
519 | 545 | self.assertItemsEqual(expected_generate_directives, directives) | 663 | self.assertItemsEqual(expected_generate_directives, directives) |
520 | 546 | 664 | ||
521 | 547 | def test_ignores_network_larger_than_slash_16(self): | 665 | def test_ignores_network_larger_than_slash_16(self): |
522 | @@ -549,7 +667,8 @@ | |||
523 | 549 | self.assertEqual( | 667 | self.assertEqual( |
524 | 550 | [], | 668 | [], |
525 | 551 | DNSReverseZoneConfig.get_GENERATE_directives( | 669 | DNSReverseZoneConfig.get_GENERATE_directives( |
527 | 552 | network, factory.make_string())) | 670 | network, factory.make_string(), |
528 | 671 | DNSZoneInfo(network, "do not care"))) | ||
529 | 553 | 672 | ||
530 | 554 | def test_ignores_networks_that_span_slash_16s(self): | 673 | def test_ignores_networks_that_span_slash_16s(self): |
531 | 555 | # If the upper and lower bounds of a range span two /16 networks | 674 | # If the upper and lower bounds of a range span two /16 networks |
532 | @@ -557,7 +676,8 @@ | |||
533 | 557 | # get_GENERATE_directives() will return early | 676 | # get_GENERATE_directives() will return early |
534 | 558 | ip_range = IPRange('10.0.0.55', '10.1.0.54') | 677 | ip_range = IPRange('10.0.0.55', '10.1.0.54') |
535 | 559 | directives = DNSReverseZoneConfig.get_GENERATE_directives( | 678 | directives = DNSReverseZoneConfig.get_GENERATE_directives( |
537 | 560 | ip_range, factory.make_string()) | 679 | ip_range, factory.make_string(), |
538 | 680 | DNSZoneInfo(IPNetwork('10.0.0.0/15'), "do not care")) | ||
539 | 561 | self.assertEqual([], directives) | 681 | self.assertEqual([], directives) |
540 | 562 | 682 | ||
541 | 563 | def test_sorts_output_by_hostname(self): | 683 | def test_sorts_output_by_hostname(self): |
542 | @@ -568,12 +688,19 @@ | |||
543 | 568 | expected_rdns = "$.%s.0.10.in-addr.arpa." | 688 | expected_rdns = "$.%s.0.10.in-addr.arpa." |
544 | 569 | 689 | ||
545 | 570 | directives = list(DNSReverseZoneConfig.get_GENERATE_directives( | 690 | directives = list(DNSReverseZoneConfig.get_GENERATE_directives( |
547 | 571 | network, domain)) | 691 | network, domain, |
548 | 692 | DNSZoneInfo(IPNetwork('10.0.0.0/24'), '0.0.10.in-addr.arpa'))) | ||
549 | 572 | self.expectThat( | 693 | self.expectThat( |
550 | 573 | directives[0], Equals( | 694 | directives[0], Equals( |
551 | 574 | ("0-255", expected_rdns % "0", expected_hostname % "0"))) | 695 | ("0-255", expected_rdns % "0", expected_hostname % "0"))) |
552 | 696 | |||
553 | 697 | expected_hostname = "10-0-%s-$." + domain + "." | ||
554 | 698 | expected_rdns = "$.%s.0.10.in-addr.arpa." | ||
555 | 699 | directives = list(DNSReverseZoneConfig.get_GENERATE_directives( | ||
556 | 700 | network, domain, | ||
557 | 701 | DNSZoneInfo(IPNetwork('10.0.1.0/24'), '1.0.10.in-addr.arpa'))) | ||
558 | 575 | self.expectThat( | 702 | self.expectThat( |
560 | 576 | directives[1], Equals( | 703 | directives[0], Equals( |
561 | 577 | ("0-255", expected_rdns % "1", expected_hostname % "1"))) | 704 | ("0-255", expected_rdns % "1", expected_hostname % "1"))) |
562 | 578 | 705 | ||
563 | 579 | 706 | ||
564 | 580 | 707 | ||
565 | === modified file 'src/provisioningserver/dns/zoneconfig.py' | |||
566 | --- src/provisioningserver/dns/zoneconfig.py 2015-05-07 18:14:38 +0000 | |||
567 | +++ src/provisioningserver/dns/zoneconfig.py 2015-11-28 05:29:35 +0000 | |||
568 | @@ -15,13 +15,13 @@ | |||
569 | 15 | __all__ = [ | 15 | __all__ = [ |
570 | 16 | 'DNSForwardZoneConfig', | 16 | 'DNSForwardZoneConfig', |
571 | 17 | 'DNSReverseZoneConfig', | 17 | 'DNSReverseZoneConfig', |
572 | 18 | 'DNSZoneInfo', | ||
573 | 18 | ] | 19 | ] |
574 | 19 | 20 | ||
575 | 20 | 21 | ||
576 | 21 | from abc import ABCMeta | 22 | from abc import ABCMeta |
577 | 22 | from datetime import datetime | 23 | from datetime import datetime |
578 | 23 | from itertools import chain | 24 | from itertools import chain |
579 | 24 | import math | ||
580 | 25 | 25 | ||
581 | 26 | from netaddr import ( | 26 | from netaddr import ( |
582 | 27 | IPAddress, | 27 | IPAddress, |
583 | @@ -106,6 +106,24 @@ | |||
584 | 106 | return intersecting_subnets, prefix, rdns_suffix | 106 | return intersecting_subnets, prefix, rdns_suffix |
585 | 107 | 107 | ||
586 | 108 | 108 | ||
587 | 109 | class DNSZoneInfo: | ||
588 | 110 | """Information about a DNS zone""" | ||
589 | 111 | |||
590 | 112 | def __init__(self, subnetwork, zone_name, target_path=None): | ||
591 | 113 | """ | ||
592 | 114 | :param subnetwork: IPNetwork that this zone (chunk) is for. None | ||
593 | 115 | for forward zones. | ||
594 | 116 | :param zone_name: Fully-qualified zone name | ||
595 | 117 | :param target_path: Optional, can be used to override the target path. | ||
596 | 118 | """ | ||
597 | 119 | self.subnetwork = subnetwork | ||
598 | 120 | self.zone_name = zone_name | ||
599 | 121 | if target_path is None: | ||
600 | 122 | self.target_path = compose_config_path('zone.%s' % zone_name) | ||
601 | 123 | else: | ||
602 | 124 | self.target_path = target_path | ||
603 | 125 | |||
604 | 126 | |||
605 | 109 | class DNSZoneConfigBase: | 127 | class DNSZoneConfigBase: |
606 | 110 | """Base class for zone writers.""" | 128 | """Base class for zone writers.""" |
607 | 111 | 129 | ||
608 | @@ -113,17 +131,17 @@ | |||
609 | 113 | 131 | ||
610 | 114 | template_file_name = 'zone.template' | 132 | template_file_name = 'zone.template' |
611 | 115 | 133 | ||
613 | 116 | def __init__(self, domain, zone_name, serial=None): | 134 | def __init__(self, domain, zone_info, serial=None): |
614 | 117 | """ | 135 | """ |
615 | 118 | :param domain: The domain name of the forward zone. | 136 | :param domain: The domain name of the forward zone. |
617 | 119 | :param zone_name: Fully-qualified zone name. | 137 | :param zone_info: list of DNSZoneInfo entries. |
618 | 120 | :param serial: The serial to use in the zone file. This must increment | 138 | :param serial: The serial to use in the zone file. This must increment |
619 | 121 | on each change. | 139 | on each change. |
620 | 122 | """ | 140 | """ |
621 | 123 | self.domain = domain | 141 | self.domain = domain |
622 | 124 | self.zone_name = zone_name | ||
623 | 125 | self.serial = serial | 142 | self.serial = serial |
625 | 126 | self.target_path = compose_config_path('zone.%s' % self.zone_name) | 143 | self.zone_info = zone_info |
626 | 144 | self.target_base = compose_config_path('zone') | ||
627 | 127 | 145 | ||
628 | 128 | def make_parameters(self): | 146 | def make_parameters(self): |
629 | 129 | """Return a dict of the common template parameters.""" | 147 | """Return a dict of the common template parameters.""" |
630 | @@ -142,9 +160,12 @@ | |||
631 | 142 | support a resolution of one second, and so this method may set an | 160 | support a resolution of one second, and so this method may set an |
632 | 143 | unexpected modification time in order to maintain that property. | 161 | unexpected modification time in order to maintain that property. |
633 | 144 | """ | 162 | """ |
637 | 145 | content = render_dns_template(cls.template_file_name, *parameters) | 163 | if not isinstance(output_file, list): |
638 | 146 | with report_missing_config_dir(): | 164 | output_file = [output_file] |
639 | 147 | incremental_write(content, output_file, mode=0644) | 165 | for outfile in output_file: |
640 | 166 | content = render_dns_template(cls.template_file_name, *parameters) | ||
641 | 167 | with report_missing_config_dir(): | ||
642 | 168 | incremental_write(content, outfile, mode=0644) | ||
643 | 148 | 169 | ||
644 | 149 | 170 | ||
645 | 150 | class DNSForwardZoneConfig(DNSZoneConfigBase): | 171 | class DNSForwardZoneConfig(DNSZoneConfigBase): |
646 | @@ -175,7 +196,7 @@ | |||
647 | 175 | self._dynamic_ranges = kwargs.pop('dynamic_ranges', []) | 196 | self._dynamic_ranges = kwargs.pop('dynamic_ranges', []) |
648 | 176 | self._srv_mapping = kwargs.pop('srv_mapping', []) | 197 | self._srv_mapping = kwargs.pop('srv_mapping', []) |
649 | 177 | super(DNSForwardZoneConfig, self).__init__( | 198 | super(DNSForwardZoneConfig, self).__init__( |
651 | 178 | domain, zone_name=domain, **kwargs) | 199 | domain, zone_info=[DNSZoneInfo(None, domain)], **kwargs) |
652 | 179 | 200 | ||
653 | 180 | @classmethod | 201 | @classmethod |
654 | 181 | def get_mapping(cls, mapping, domain, dns_ip): | 202 | def get_mapping(cls, mapping, domain, dns_ip): |
655 | @@ -255,7 +276,7 @@ | |||
656 | 255 | return [] | 276 | return [] |
657 | 256 | 277 | ||
658 | 257 | generate_directives = set() | 278 | generate_directives = set() |
660 | 258 | subnets, prefix, _ = get_details_for_ip_range(dynamic_range) | 279 | subnets, prefix, rdns_suffix = get_details_for_ip_range(dynamic_range) |
661 | 259 | for subnet in subnets: | 280 | for subnet in subnets: |
662 | 260 | iterator = "%d-%d" % ( | 281 | iterator = "%d-%d" % ( |
663 | 261 | (subnet.first & 0x000000ff), | 282 | (subnet.first & 0x000000ff), |
664 | @@ -279,27 +300,28 @@ | |||
665 | 279 | def write_config(self): | 300 | def write_config(self): |
666 | 280 | """Write the zone file.""" | 301 | """Write the zone file.""" |
667 | 281 | # Create GENERATE directives for IPv4 ranges. | 302 | # Create GENERATE directives for IPv4 ranges. |
689 | 282 | generate_directives = list( | 303 | for zi in self.zone_info: |
690 | 283 | chain.from_iterable( | 304 | generate_directives = list( |
691 | 284 | self.get_GENERATE_directives(dynamic_range) | 305 | chain.from_iterable( |
692 | 285 | for dynamic_range in self._dynamic_ranges | 306 | self.get_GENERATE_directives(dynamic_range) |
693 | 286 | if dynamic_range.version == 4 | 307 | for dynamic_range in self._dynamic_ranges |
694 | 287 | )) | 308 | if dynamic_range.version == 4 |
695 | 288 | self.write_zone_file( | 309 | )) |
696 | 289 | self.target_path, self.make_parameters(), | 310 | self.write_zone_file( |
697 | 290 | { | 311 | zi.target_path, self.make_parameters(), |
698 | 291 | 'mappings': { | 312 | { |
699 | 292 | 'SRV': self.get_srv_mapping( | 313 | 'mappings': { |
700 | 293 | self._srv_mapping), | 314 | 'SRV': self.get_srv_mapping( |
701 | 294 | 'A': self.get_A_mapping( | 315 | self._srv_mapping), |
702 | 295 | self._mapping, self.domain, self._dns_ip), | 316 | 'A': self.get_A_mapping( |
703 | 296 | 'AAAA': self.get_AAAA_mapping( | 317 | self._mapping, self.domain, self._dns_ip), |
704 | 297 | self._mapping, self.domain, self._dns_ip), | 318 | 'AAAA': self.get_AAAA_mapping( |
705 | 298 | }, | 319 | self._mapping, self.domain, self._dns_ip), |
706 | 299 | 'generate_directives': { | 320 | }, |
707 | 300 | 'A': generate_directives, | 321 | 'generate_directives': { |
708 | 301 | } | 322 | 'A': generate_directives, |
709 | 302 | }) | 323 | } |
710 | 324 | }) | ||
711 | 303 | 325 | ||
712 | 304 | 326 | ||
713 | 305 | class DNSReverseZoneConfig(DNSZoneConfigBase): | 327 | class DNSReverseZoneConfig(DNSZoneConfigBase): |
714 | @@ -325,32 +347,88 @@ | |||
715 | 325 | self._mapping = kwargs.pop('mapping', {}) | 347 | self._mapping = kwargs.pop('mapping', {}) |
716 | 326 | self._network = kwargs.pop("network", None) | 348 | self._network = kwargs.pop("network", None) |
717 | 327 | self._dynamic_ranges = kwargs.pop('dynamic_ranges', []) | 349 | self._dynamic_ranges = kwargs.pop('dynamic_ranges', []) |
719 | 328 | zone_name = self.compose_zone_name(self._network) | 350 | zone_info = self.compose_zone_info(self._network) |
720 | 329 | super(DNSReverseZoneConfig, self).__init__( | 351 | super(DNSReverseZoneConfig, self).__init__( |
722 | 330 | domain, zone_name=zone_name, **kwargs) | 352 | domain, zone_info=zone_info, **kwargs) |
723 | 331 | 353 | ||
724 | 332 | @classmethod | 354 | @classmethod |
727 | 333 | def compose_zone_name(cls, network): | 355 | def compose_zone_info(cls, network): |
728 | 334 | """Return the name of the reverse zone.""" | 356 | """Return the names of the reverse zones.""" |
729 | 335 | # Generate the name of the reverse zone file: | 357 | # Generate the name of the reverse zone file: |
730 | 336 | # Use netaddr's reverse_dns() to get the reverse IP name | 358 | # Use netaddr's reverse_dns() to get the reverse IP name |
731 | 337 | # of the first IP address in the network and then drop the first | 359 | # of the first IP address in the network and then drop the first |
732 | 338 | # octets of that name (i.e. drop the octets that will be specified in | 360 | # octets of that name (i.e. drop the octets that will be specified in |
733 | 339 | # the zone file). | 361 | # the zone file). |
734 | 362 | # returns a list of (IPNetwork, zone_name, zonefile_path) tuples | ||
735 | 363 | info = [] | ||
736 | 340 | first = IPAddress(network.first) | 364 | first = IPAddress(network.first) |
737 | 365 | last = IPAddress(network.last) | ||
738 | 341 | if first.version == 6: | 366 | if first.version == 6: |
739 | 342 | # IPv6. | 367 | # IPv6. |
743 | 343 | # Use float division and ceil to cope with network sizes that | 368 | # 2001:89ab::/19 yields 8.1.0.0.2.ip6.arpa, and the full list |
744 | 344 | # are not divisible by 4. | 369 | # is 8.1.0.0.2.ip6.arpa, 9.1.0.0.2.ip6.arpa |
745 | 345 | rest_limit = int(math.ceil((128 - network.prefixlen) / 4.)) | 370 | # The ipv6 reverse dns form is 32 elements of 1 hex digit each. |
746 | 371 | # How many elements of the reverse DNS name to we throw away? | ||
747 | 372 | # Prefixlen of 0-3 gives us 1, 4-7 gives us 2, etc. | ||
748 | 373 | # While this seems wrong, we always _add_ a base label back in, | ||
749 | 374 | # so it's correct. | ||
750 | 375 | rest_limit = (132 - network.prefixlen) / 4 | ||
751 | 376 | # What is the prefix for each inner subnet (It will be the next | ||
752 | 377 | # smaller multiple of 4.) If it's the smallest one, then RFC2317 | ||
753 | 378 | # tells us that we're adding an extra blob to the front of the | ||
754 | 379 | # reverse zone name, and we want the entire prefixlen. | ||
755 | 380 | subnet_prefix = (network.prefixlen + 3) / 4 * 4 | ||
756 | 381 | if subnet_prefix == 128: | ||
757 | 382 | subnet_prefix = network.prefixlen | ||
758 | 383 | # How big is the step between subnets? Again, special case for | ||
759 | 384 | # extra small subnets. | ||
760 | 385 | step = 1 << ((128 - network.prefixlen) / 4 * 4) | ||
761 | 386 | if step < 16: | ||
762 | 387 | step = 16 | ||
763 | 388 | # Grab the base (hex) and trailing labels for our reverse zone. | ||
764 | 389 | split_zone = first.reverse_dns.split('.') | ||
765 | 390 | zone_rest = ".".join(split_zone[rest_limit:-1]) | ||
766 | 391 | base = int(split_zone[rest_limit - 1], 16) | ||
767 | 346 | else: | 392 | else: |
768 | 347 | # IPv4. | 393 | # IPv4. |
775 | 348 | # Use float division and ceil to cope with splits not done on | 394 | # The logic here is the same as for IPv6, but with 8 instead of 4. |
776 | 349 | # octets boundaries. | 395 | rest_limit = (40 - network.prefixlen) / 8 |
777 | 350 | rest_limit = int(math.ceil((32 - network.prefixlen) / 8.)) | 396 | subnet_prefix = (network.prefixlen + 7) / 8 * 8 |
778 | 351 | reverse_name = first.reverse_dns.split('.', rest_limit)[-1] | 397 | if subnet_prefix == 32: |
779 | 352 | # Strip off trailing '.'. | 398 | subnet_prefix = network.prefixlen |
780 | 353 | return reverse_name[:-1] | 399 | step = 1 << ((32 - network.prefixlen) / 8 * 8) |
781 | 400 | if step < 256: | ||
782 | 401 | step = 256 | ||
783 | 402 | # Grab the base (decimal) and trailing labels for our reverse | ||
784 | 403 | # zone. | ||
785 | 404 | split_zone = first.reverse_dns.split('.') | ||
786 | 405 | zone_rest = ".".join(split_zone[rest_limit:-1]) | ||
787 | 406 | base = int(split_zone[rest_limit - 1]) | ||
788 | 407 | while first <= last: | ||
789 | 408 | # Rest_limit has bounds of 1..labelcount+1 (5 or 33). | ||
790 | 409 | # If we're stripping any elements, then we just want base.name. | ||
791 | 410 | if rest_limit > 1: | ||
792 | 411 | if first.version == 6: | ||
793 | 412 | new_zone = "%x.%s" % (base, zone_rest) | ||
794 | 413 | else: | ||
795 | 414 | new_zone = "%d.%s" % (base, zone_rest) | ||
796 | 415 | # We didn't actually strip any elemnts, so base goes back with | ||
797 | 416 | # the prefixlen attached. | ||
798 | 417 | elif first.version == 6: | ||
799 | 418 | new_zone = "%x-%d.%s" % (base, network.prefixlen, zone_rest) | ||
800 | 419 | else: | ||
801 | 420 | new_zone = "%d-%d.%s" % (base, network.prefixlen, zone_rest) | ||
802 | 421 | info.append(DNSZoneInfo( | ||
803 | 422 | IPNetwork("%s/%d" % (first, subnet_prefix)), | ||
804 | 423 | new_zone)) | ||
805 | 424 | base += 1 | ||
806 | 425 | try: | ||
807 | 426 | first += step | ||
808 | 427 | except IndexError: | ||
809 | 428 | # IndexError occurs when we go from 255.255.255.255 to | ||
810 | 429 | # 0.0.0.0. If we hit that, we're all fine and done. | ||
811 | 430 | break | ||
812 | 431 | return info | ||
813 | 354 | 432 | ||
814 | 355 | @classmethod | 433 | @classmethod |
815 | 356 | def get_PTR_mapping(cls, mapping, domain, network): | 434 | def get_PTR_mapping(cls, mapping, domain, network): |
816 | @@ -366,7 +444,7 @@ | |||
817 | 366 | :param mapping: A hostname:ip-addresses mapping for all known hosts in | 444 | :param mapping: A hostname:ip-addresses mapping for all known hosts in |
818 | 367 | the reverse zone. | 445 | the reverse zone. |
819 | 368 | :param domain: Zone's domain name. | 446 | :param domain: Zone's domain name. |
821 | 369 | :param network: Zone's network. | 447 | :param network: DNS Zone's network. |
822 | 370 | :type network: :class:`netaddr.IPNetwork` | 448 | :type network: :class:`netaddr.IPNetwork` |
823 | 371 | """ | 449 | """ |
824 | 372 | return ( | 450 | return ( |
825 | @@ -380,7 +458,7 @@ | |||
826 | 380 | ) | 458 | ) |
827 | 381 | 459 | ||
828 | 382 | @classmethod | 460 | @classmethod |
830 | 383 | def get_GENERATE_directives(cls, dynamic_range, domain): | 461 | def get_GENERATE_directives(cls, dynamic_range, domain, zone_info): |
831 | 384 | """Return the GENERATE directives for the reverse zone of a network.""" | 462 | """Return the GENERATE directives for the reverse zone of a network.""" |
832 | 385 | slash_16 = IPNetwork("%s/16" % IPAddress(dynamic_range.first)) | 463 | slash_16 = IPNetwork("%s/16" % IPAddress(dynamic_range.first)) |
833 | 386 | if (dynamic_range.size > 256 ** 2 or | 464 | if (dynamic_range.size > 256 ** 2 or |
834 | @@ -391,19 +469,29 @@ | |||
835 | 391 | return [] | 469 | return [] |
836 | 392 | 470 | ||
837 | 393 | generate_directives = set() | 471 | generate_directives = set() |
838 | 472 | # The largest subnet returned is a /24. | ||
839 | 394 | subnets, prefix, rdns_suffix = get_details_for_ip_range(dynamic_range) | 473 | subnets, prefix, rdns_suffix = get_details_for_ip_range(dynamic_range) |
840 | 395 | for subnet in subnets: | 474 | for subnet in subnets: |
852 | 396 | iterator = "%d-%d" % ( | 475 | if (IPAddress(subnet.first) in zone_info.subnetwork): |
853 | 397 | (subnet.first & 0x000000ff), | 476 | iterator = "%d-%d" % ( |
854 | 398 | (subnet.last & 0x000000ff)) | 477 | (subnet.first & 0x000000ff), |
855 | 399 | hostname = "%s-%d-$" % ( | 478 | (subnet.last & 0x000000ff)) |
856 | 400 | prefix.replace('.', '-'), | 479 | hostname = "%s-%d-$" % ( |
857 | 401 | (subnet.first & 0x0000ff00) >> 8) | 480 | prefix.replace('.', '-'), |
858 | 402 | rdns = "$.%d.%s" % ( | 481 | (subnet.first & 0x0000ff00) >> 8) |
859 | 403 | (subnet.first & 0x0000ff00) >> 8, | 482 | # If we're at least a /24, then fully specify the name, |
860 | 404 | rdns_suffix) | 483 | # rather than trying to figure out how much of the name |
861 | 405 | generate_directives.add( | 484 | # is in the zone name. |
862 | 406 | (iterator, rdns, "%s.%s." % (hostname, domain))) | 485 | if zone_info.subnetwork.prefixlen <= 24: |
863 | 486 | rdns = "$.%d.%s" % ( | ||
864 | 487 | (subnet.first & 0x0000ff00) >> 8, | ||
865 | 488 | rdns_suffix) | ||
866 | 489 | else: | ||
867 | 490 | # Let the zone declaration provide the suffix. | ||
868 | 491 | # rather than trying to calculate it. | ||
869 | 492 | rdns = "$" | ||
870 | 493 | generate_directives.add( | ||
871 | 494 | (iterator, rdns, "%s.%s." % (hostname, domain))) | ||
872 | 407 | 495 | ||
873 | 408 | return sorted( | 496 | return sorted( |
874 | 409 | generate_directives, key=lambda directive: directive[2]) | 497 | generate_directives, key=lambda directive: directive[2]) |
875 | @@ -411,21 +499,25 @@ | |||
876 | 411 | def write_config(self): | 499 | def write_config(self): |
877 | 412 | """Write the zone file.""" | 500 | """Write the zone file.""" |
878 | 413 | # Create GENERATE directives for IPv4 ranges. | 501 | # Create GENERATE directives for IPv4 ranges. |
894 | 414 | generate_directives = list( | 502 | for zi in self.zone_info: |
895 | 415 | chain.from_iterable( | 503 | generate_directives = list( |
896 | 416 | self.get_GENERATE_directives(dynamic_range, self.domain) | 504 | chain.from_iterable( |
897 | 417 | for dynamic_range in self._dynamic_ranges | 505 | self.get_GENERATE_directives( |
898 | 418 | if dynamic_range.version == 4 | 506 | dynamic_range, |
899 | 419 | )) | 507 | self.domain, |
900 | 420 | self.write_zone_file( | 508 | zi) |
901 | 421 | self.target_path, self.make_parameters(), | 509 | for dynamic_range in self._dynamic_ranges |
902 | 422 | { | 510 | if dynamic_range.version == 4 |
903 | 423 | 'mappings': { | 511 | )) |
904 | 424 | 'PTR': self.get_PTR_mapping( | 512 | self.write_zone_file( |
905 | 425 | self._mapping, self.domain, self._network), | 513 | zi.target_path, self.make_parameters(), |
906 | 426 | }, | 514 | { |
907 | 427 | 'generate_directives': { | 515 | 'mappings': { |
908 | 428 | 'PTR': generate_directives, | 516 | 'PTR': self.get_PTR_mapping( |
909 | 517 | self._mapping, self.domain, zi.subnetwork), | ||
910 | 518 | }, | ||
911 | 519 | 'generate_directives': { | ||
912 | 520 | 'PTR': generate_directives, | ||
913 | 521 | } | ||
914 | 429 | } | 522 | } |
917 | 430 | } | 523 | ) |
916 | 431 | ) |
general overview: DNSZoneConfigBa se.{zone_ name,target_ path} get replaced with zone_info: a list of 1-128 DNSZoneInfo elements (which contain the subnet and reverse zone name, with target_path being derived as needed.)
The zones that we are authoritative for are limited to only the reverse zones for the subnets we
own.