Status: | Rejected |
---|---|
Rejected by: | LaMont Jones |
Proposed branch: | lp:~lamont/maas/dns-1.9 |
Merge into: | lp:maas/1.9 |
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-1.9 |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
MAAS Committers | Pending | ||
Review via email: mp+281879@code.launchpad.net |
Commit message
Correctly manage reverse DNS: do not manage a /16 because the user added a /22 network.
Description of the change
Correctly manage reverse DNS: do not manage a /16 because the user added a /22 network.
Andres Rodriguez (andreserl) wrote : | # |
LaMont Jones (lamont) wrote : | # |
That was before a maas 1.8 user reported #1531868, wherein maas tries to reload DNS for changes, and fails utterly to do so, because the named.conf is invalid.
I think that this bugfix warrants reconsideration since it's breaking maas 1.8 users in the field.
Andres Rodriguez (andreserl) wrote : | # |
Ok, let's discuss this next week.
Thanks!
On Thu, Jan 14, 2016 at 2:00 AM, LaMont Jones <email address hidden>
wrote:
> That was before a maas 1.8 user reported #1531868, wherein maas tries to
> reload DNS for changes, and fails utterly to do so, because the named.conf
> is invalid.
>
> I think that this bugfix warrants reconsideration since it's breaking maas
> 1.8 users in the field.
> --
> https:/
> Your team MAAS Committers is requested to review the proposed merge of
> lp:~lamont/maas/dns-1.9 into lp:maas/1.9.
>
--
Andres Rodriguez
Engineering Manager, MAAS
Canonical USA, Inc.
- 4520. By LaMont Jones
-
Merge from maas/1.9.
Mike Pontillo (mpontillo) wrote : | # |
Does this still need review, or is it WIP? (I was under the impression that this code has changed significantly in trunk, and would need to be ported back to 1.9 in order for it to be usable there. Is that correct?)
Unmerged revisions
- 4520. By LaMont Jones
-
Merge from maas/1.9.
- 4519. By LaMont Jones
-
Cleanup errors introduced in doing review feedback
- 4518. By LaMont Jones
-
Review feedback
- 4517. By LaMont Jones
-
For reverse zones, get_GENERATE_
directives needs to follow rfc2317. - 4516. By LaMont Jones
-
Correctly cast subnet.first to avoid errors with trusty's netaddr.
- 4515. By LaMont Jones
-
Merge DNS fixes from trunk
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 2016-02-17 21:43:45 +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 2016-02-17 21:43:45 +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 2016-02-17 21:43:45 +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 2016-02-17 21:43:45 +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 2016-02-17 21:43:45 +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 2016-02-17 21:43:45 +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 2016-02-17 21:43:45 +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 | ) |
I thought that we agarres that since this was a change of behavior we were
going to defer this?
On Thursday, January 7, 2016, LaMont Jones <email address hidden>
wrote:
> LaMont Jones has proposed merging lp:~lamont/maas/dns-1.9 into lp:maas/1.9. /bugs.launchpad .net/maas/ +bug/1356012 /bugs.launchpad .net/maas/ +bug/1531868 /code.launchpad .net/~lamont/ maas/dns- 1.9/+merge/ 281879
>
> Commit message:
> Correctly manage reverse DNS: do not manage a /16 because the user added a
> /22 network.
>
> Requested reviews:
> MAAS Committers (maas-committers)
> Related bugs:
> Bug #1356012 in MAAS: "maas incorrectly overmanages DNS reverse zones"
> https:/
> Bug #1531868 in MAAS: "Add network fails for multiple bigger-than-/24
> networks in a single /16"
> https:/
>
> For more details, see:
> https:/
>
> Correctly manage reverse DNS: do not manage a /16 because the user added a
> /22 network.
> --
> Your team MAAS Committers is requested to review the proposed merge of
> lp:~lamont/maas/dns-1.9 into lp:maas/1.9.
>
--
Andres Rodriguez (RoAkSoAx)
Ubuntu Server Developer
MSc. Telecom & Networking
Systems Engineer