Merge lp:~gmb/maas/backport-generate-directives-to-1.7 into lp:maas/1.7
- backport-generate-directives-to-1.7
- Merge into 1.7
Proposed by
Graham Binns
Status: | Merged | ||||
---|---|---|---|---|---|
Approved by: | Graham Binns | ||||
Approved revision: | no longer in the source branch. | ||||
Merged at revision: | 3286 | ||||
Proposed branch: | lp:~gmb/maas/backport-generate-directives-to-1.7 | ||||
Merge into: | lp:maas/1.7 | ||||
Diff against target: |
802 lines (+551/-20) 6 files modified
etc/maas/templates/dns/zone.template (+6/-0) src/maasserver/dns/tests/test_config.py (+4/-4) src/maasserver/dns/tests/test_zonegenerator.py (+20/-3) src/maasserver/dns/zonegenerator.py (+26/-7) src/provisioningserver/dns/tests/test_zoneconfig.py (+353/-4) src/provisioningserver/dns/zoneconfig.py (+142/-2) |
||||
To merge this branch: | bzr merge lp:~gmb/maas/backport-generate-directives-to-1.7 | ||||
Related bugs: |
|
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Raphaël Badin (community) | Approve | ||
Julian Edwards (community) | Approve | ||
Review via email: mp+240064@code.launchpad.net |
Commit message
Using $GENERATE directives, generate DNS A and PTR records for all addresses in each managed NodeGroupInterf
Previously, hosts with addresses in the dynamic range were not resolvable through DNS, which caused problems with some Juju charms.
Backported from r3308 lp:maas.
Description of the change
To post a comment you must log in.
Revision history for this message
Raphaël Badin (rvb) wrote : | # |
Approve. But let's look into the edge cases Julian described this morning during the standup.
review:
Approve
Revision history for this message
Graham Binns (gmb) wrote : | # |
On 30 October 2014 09:32, Raphaël Badin <email address hidden> wrote:
> Approve. But let's look into the edge cases Julian described this morning during the standup.
Agreed. I'll merge the fixes for those, and the UI restrictions, into
this backport — unless you'd rather I did it separately.
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | === modified file 'etc/maas/templates/dns/zone.template' |
2 | --- etc/maas/templates/dns/zone.template 2012-10-04 15:59:56 +0000 |
3 | +++ etc/maas/templates/dns/zone.template 2014-10-29 22:31:20 +0000 |
4 | @@ -12,6 +12,12 @@ |
5 | ) |
6 | |
7 | IN NS {{domain}}. |
8 | +{{for type, directive in generate_directives.items()}} |
9 | +{{for iterator_values, rdns, hostname in directive}} |
10 | +$GENERATE {{iterator_values}} {{rdns}} IN {{type}} {{hostname}} |
11 | +{{endfor}} |
12 | +{{endfor}} |
13 | + |
14 | {{for type, mapping in mappings.items()}} |
15 | {{for item_from, item_to in mapping}} |
16 | {{item_from}} IN {{type}} {{item_to}} |
17 | |
18 | === modified file 'src/maasserver/dns/tests/test_config.py' |
19 | --- src/maasserver/dns/tests/test_config.py 2014-10-15 11:22:22 +0000 |
20 | +++ src/maasserver/dns/tests/test_config.py 2014-10-29 22:31:20 +0000 |
21 | @@ -165,15 +165,15 @@ |
22 | # A forward lookup on the hostname returns the IP address. |
23 | fqdn = "%s.%s" % (hostname, domain) |
24 | forward_lookup_result = self.dig_resolve(fqdn, version=version) |
25 | - self.assertEqual( |
26 | - [ip], forward_lookup_result, |
27 | + self.expectThat( |
28 | + forward_lookup_result, Contains(ip), |
29 | "Failed to resolve '%s' (results: '%s')." % ( |
30 | fqdn, ','.join(forward_lookup_result))) |
31 | # A reverse lookup on the IP address returns the hostname. |
32 | reverse_lookup_result = self.dig_reverse_resolve( |
33 | ip, version=version) |
34 | - self.assertEqual( |
35 | - ["%s." % fqdn], reverse_lookup_result, |
36 | + self.expectThat( |
37 | + reverse_lookup_result, Contains("%s." % fqdn), |
38 | "Failed to reverse resolve '%s' (results: '%s')." % ( |
39 | fqdn, ','.join(reverse_lookup_result))) |
40 | |
41 | |
42 | === modified file 'src/maasserver/dns/tests/test_zonegenerator.py' |
43 | --- src/maasserver/dns/tests/test_zonegenerator.py 2014-09-15 14:28:28 +0000 |
44 | +++ src/maasserver/dns/tests/test_zonegenerator.py 2014-10-29 22:31:20 +0000 |
45 | @@ -393,14 +393,28 @@ |
46 | [interface] = nodegroup.get_managed_interfaces() |
47 | networks_dict = ZoneGenerator._get_networks() |
48 | retrieved_interface = networks_dict[nodegroup] |
49 | - self.assertEqual([interface.network], retrieved_interface) |
50 | + self.assertEqual( |
51 | + [ |
52 | + ( |
53 | + interface.network, |
54 | + (interface.ip_range_low, interface.ip_range_high) |
55 | + ) |
56 | + ], |
57 | + retrieved_interface) |
58 | |
59 | def test_get_networks_returns_multiple_networks(self): |
60 | nodegroups = [self.make_node_group() for _ in range(3)] |
61 | networks_dict = ZoneGenerator._get_networks() |
62 | for nodegroup in nodegroups: |
63 | [interface] = nodegroup.get_managed_interfaces() |
64 | - self.assertEqual([interface.network], networks_dict[nodegroup]) |
65 | + self.assertEqual( |
66 | + [ |
67 | + ( |
68 | + interface.network, |
69 | + (interface.ip_range_low, interface.ip_range_high), |
70 | + ), |
71 | + ], |
72 | + networks_dict[nodegroup]) |
73 | |
74 | def test_get_networks_returns_managed_networks(self): |
75 | nodegroups = [ |
76 | @@ -415,7 +429,10 @@ |
77 | self.assertEqual( |
78 | { |
79 | nodegroup: [ |
80 | - interface.network |
81 | + ( |
82 | + interface.network, |
83 | + (interface.ip_range_low, interface.ip_range_high), |
84 | + ) |
85 | for interface in nodegroup.get_managed_interfaces() |
86 | ] |
87 | for nodegroup in nodegroups |
88 | |
89 | === modified file 'src/maasserver/dns/zonegenerator.py' |
90 | --- src/maasserver/dns/zonegenerator.py 2014-08-28 12:00:15 +0000 |
91 | +++ src/maasserver/dns/zonegenerator.py 2014-10-29 22:31:20 +0000 |
92 | @@ -29,7 +29,10 @@ |
93 | from maasserver.models.config import Config |
94 | from maasserver.models.nodegroup import NodeGroup |
95 | from maasserver.server_address import get_maas_facing_server_address |
96 | -from netaddr import IPAddress |
97 | +from netaddr import ( |
98 | + IPAddress, |
99 | + IPRange, |
100 | + ) |
101 | from provisioningserver.dns.zoneconfig import ( |
102 | DNSForwardZoneConfig, |
103 | DNSReverseZoneConfig, |
104 | @@ -180,9 +183,18 @@ |
105 | |
106 | @staticmethod |
107 | def _get_networks(): |
108 | - """Return a lazily evaluated nodegroup:network dict.""" |
109 | - return lazydict(lambda ng: [ |
110 | - interface.network for interface in ng.get_managed_interfaces()]) |
111 | + """Return a lazily evaluated nodegroup:network_details dict. |
112 | + |
113 | + network_details takes the form of a tuple of (network, |
114 | + (network.ip_range_low, network.ip_range_high)). |
115 | + """ |
116 | + |
117 | + def get_network(nodegroup): |
118 | + return [ |
119 | + (iface.network, (iface.ip_range_low, iface.ip_range_high)) |
120 | + for iface in nodegroup.get_managed_interfaces() |
121 | + ] |
122 | + return lazydict(get_network) |
123 | |
124 | @staticmethod |
125 | def _get_srv_mappings(): |
126 | @@ -209,6 +221,12 @@ |
127 | forward_nodegroups = sorted(nodegroups, key=get_domain) |
128 | for domain, nodegroups in groupby(forward_nodegroups, get_domain): |
129 | nodegroups = list(nodegroups) |
130 | + dynamic_ranges = [ |
131 | + interface.get_dynamic_ip_range() |
132 | + for nodegroup in nodegroups |
133 | + for interface in nodegroup.get_managed_interfaces() |
134 | + ] |
135 | + |
136 | # A forward zone encompassing all nodes in the same domain. |
137 | yield DNSForwardZoneConfig( |
138 | domain, serial=serial, dns_ip=dns_ip, |
139 | @@ -217,7 +235,8 @@ |
140 | for nodegroup in nodegroups |
141 | for hostname, ip in mappings[nodegroup].items() |
142 | }, |
143 | - srv_mapping=set(srv_mappings) |
144 | + srv_mapping=set(srv_mappings), |
145 | + dynamic_ranges=dynamic_ranges, |
146 | ) |
147 | |
148 | @staticmethod |
149 | @@ -226,11 +245,11 @@ |
150 | get_domain = lambda nodegroup: nodegroup.name |
151 | reverse_nodegroups = sorted(nodegroups, key=networks.get) |
152 | for nodegroup in reverse_nodegroups: |
153 | - for network in networks[nodegroup]: |
154 | + for network, dynamic_range in networks[nodegroup]: |
155 | mapping = mappings[nodegroup] |
156 | yield DNSReverseZoneConfig( |
157 | get_domain(nodegroup), serial=serial, mapping=mapping, |
158 | - network=network |
159 | + network=network, dynamic_ranges=[IPRange(*dynamic_range)] |
160 | ) |
161 | |
162 | def __iter__(self): |
163 | |
164 | === modified file 'src/provisioningserver/dns/tests/test_zoneconfig.py' |
165 | --- src/provisioningserver/dns/tests/test_zoneconfig.py 2014-09-23 21:43:27 +0000 |
166 | +++ src/provisioningserver/dns/tests/test_zoneconfig.py 2014-10-29 22:31:20 +0000 |
167 | @@ -22,10 +22,12 @@ |
168 | import random |
169 | |
170 | from maastesting.factory import factory |
171 | +from maastesting.matchers import MockNotCalled |
172 | from maastesting.testcase import MAASTestCase |
173 | from netaddr import ( |
174 | IPAddress, |
175 | IPNetwork, |
176 | + IPRange, |
177 | ) |
178 | from provisioningserver.dns.config import ( |
179 | get_dns_config_dir, |
180 | @@ -35,11 +37,14 @@ |
181 | from provisioningserver.dns.zoneconfig import ( |
182 | DNSForwardZoneConfig, |
183 | DNSReverseZoneConfig, |
184 | + intersect_iprange, |
185 | ) |
186 | from testtools.matchers import ( |
187 | Contains, |
188 | ContainsAll, |
189 | + Equals, |
190 | FileContains, |
191 | + HasLength, |
192 | IsInstance, |
193 | MatchesAll, |
194 | MatchesStructure, |
195 | @@ -187,10 +192,13 @@ |
196 | ipv4_hostname: [ipv4_ip], |
197 | ipv6_hostname: [ipv6_ip], |
198 | } |
199 | + expected_generate_directives = ( |
200 | + DNSForwardZoneConfig.get_GENERATE_directives(network)) |
201 | srv = self.make_srv_record() |
202 | dns_zone_config = DNSForwardZoneConfig( |
203 | domain, serial=random.randint(1, 100), |
204 | - mapping=mapping, dns_ip=dns_ip, srv_mapping=[srv]) |
205 | + mapping=mapping, dns_ip=dns_ip, srv_mapping=[srv], |
206 | + dynamic_ranges=[IPRange(network.first, network.last)]) |
207 | dns_zone_config.write_config() |
208 | self.assertThat( |
209 | os.path.join(target_dir, 'zone.%s' % domain), |
210 | @@ -201,6 +209,12 @@ |
211 | srv.service, self.get_srv_item_output(srv)), |
212 | '%s IN A %s' % (ipv4_hostname, ipv4_ip), |
213 | '%s IN AAAA %s' % (ipv6_hostname, ipv6_ip), |
214 | + ] + |
215 | + [ |
216 | + '$GENERATE %s %s IN A %s' % ( |
217 | + iterator_values, reverse_dns, hostname) |
218 | + for iterator_values, reverse_dns, hostname in |
219 | + expected_generate_directives |
220 | ] |
221 | ) |
222 | ) |
223 | @@ -222,6 +236,31 @@ |
224 | '%s. IN A %s' % (dns_zone_config.domain, dns_ip), |
225 | ]))) |
226 | |
227 | + def test_ignores_generate_directives_for_v6_dynamic_ranges(self): |
228 | + patch_dns_config_path(self) |
229 | + domain = factory.make_string() |
230 | + network = factory.make_ipv4_network() |
231 | + dns_ip = factory.pick_ip_in_network(network) |
232 | + ipv4_hostname = factory.make_name('host') |
233 | + ipv4_ip = factory.pick_ip_in_network(network) |
234 | + ipv6_hostname = factory.make_name('host') |
235 | + ipv6_ip = factory.make_ipv6_address() |
236 | + ipv6_network = factory.make_ipv6_network() |
237 | + dynamic_range = IPRange(ipv6_network.first, ipv6_network.last) |
238 | + mapping = { |
239 | + ipv4_hostname: [ipv4_ip], |
240 | + ipv6_hostname: [ipv6_ip], |
241 | + } |
242 | + srv = self.make_srv_record() |
243 | + dns_zone_config = DNSForwardZoneConfig( |
244 | + domain, serial=random.randint(1, 100), |
245 | + mapping=mapping, dns_ip=dns_ip, srv_mapping=[srv], |
246 | + dynamic_ranges=[dynamic_range]) |
247 | + get_generate_directives = self.patch( |
248 | + dns_zone_config, 'get_GENERATE_directives') |
249 | + dns_zone_config.write_config() |
250 | + self.assertThat(get_generate_directives, MockNotCalled()) |
251 | + |
252 | def test_config_file_is_world_readable(self): |
253 | patch_dns_config_path(self) |
254 | dns_zone_config = DNSForwardZoneConfig( |
255 | @@ -350,16 +389,43 @@ |
256 | target_dir = patch_dns_config_path(self) |
257 | domain = factory.make_string() |
258 | network = IPNetwork('192.168.0.1/22') |
259 | + dynamic_network = IPNetwork('192.168.0.1/28') |
260 | dns_zone_config = DNSReverseZoneConfig( |
261 | - domain, serial=random.randint(1, 100), network=network) |
262 | + domain, serial=random.randint(1, 100), network=network, |
263 | + dynamic_ranges=[ |
264 | + IPRange(dynamic_network.first, dynamic_network.last)]) |
265 | dns_zone_config.write_config() |
266 | reverse_file_name = 'zone.168.192.in-addr.arpa' |
267 | - expected = Contains( |
268 | - 'IN NS %s' % domain) |
269 | + expected_generate_directives = dns_zone_config.get_GENERATE_directives( |
270 | + dynamic_network, domain) |
271 | + expected = ContainsAll( |
272 | + [ |
273 | + 'IN NS %s' % domain |
274 | + ] + |
275 | + [ |
276 | + '$GENERATE %s %s IN PTR %s' % ( |
277 | + iterator_values, reverse_dns, hostname) |
278 | + for iterator_values, reverse_dns, hostname in |
279 | + expected_generate_directives |
280 | + ]) |
281 | self.assertThat( |
282 | os.path.join(target_dir, reverse_file_name), |
283 | FileContains(matcher=expected)) |
284 | |
285 | + def test_ignores_generate_directives_for_v6_dynamic_ranges(self): |
286 | + patch_dns_config_path(self) |
287 | + domain = factory.make_string() |
288 | + network = IPNetwork('192.168.0.1/22') |
289 | + dynamic_network = IPNetwork("%s/64" % factory.make_ipv6_address()) |
290 | + dns_zone_config = DNSReverseZoneConfig( |
291 | + domain, serial=random.randint(1, 100), network=network, |
292 | + dynamic_ranges=[ |
293 | + IPRange(dynamic_network.first, dynamic_network.last)]) |
294 | + get_generate_directives = self.patch( |
295 | + dns_zone_config, 'get_GENERATE_directives') |
296 | + dns_zone_config.write_config() |
297 | + self.assertThat(get_generate_directives, MockNotCalled()) |
298 | + |
299 | def test_reverse_config_file_is_world_readable(self): |
300 | patch_dns_config_path(self) |
301 | dns_zone_config = DNSReverseZoneConfig( |
302 | @@ -368,3 +434,286 @@ |
303 | dns_zone_config.write_config() |
304 | filepath = FilePath(dns_zone_config.target_path) |
305 | self.assertTrue(filepath.getPermissions().other.read) |
306 | + |
307 | + |
308 | +class TestDNSReverseZoneConfig_GetGenerateDirectives(MAASTestCase): |
309 | + """Tests for `DNSReverseZoneConfig.get_GENERATE_directives()`.""" |
310 | + |
311 | + def test_excplicitly(self): |
312 | + # The other tests in this TestCase rely on |
313 | + # get_expected_generate_directives(), which is quite dense. Here |
314 | + # we test get_GENERATE_directives() explicitly. |
315 | + ip_range = IPRange('192.168.0.1', '192.168.2.128') |
316 | + expected_directives = [ |
317 | + ("1-255", "$.0.168.192.in-addr.arpa.", "192-168-0-$.domain."), |
318 | + ("0-255", "$.1.168.192.in-addr.arpa.", "192-168-1-$.domain."), |
319 | + ("0-128", "$.2.168.192.in-addr.arpa.", "192-168-2-$.domain."), |
320 | + ] |
321 | + self.assertItemsEqual( |
322 | + expected_directives, |
323 | + DNSReverseZoneConfig.get_GENERATE_directives( |
324 | + ip_range, domain="domain")) |
325 | + |
326 | + def get_expected_generate_directives(self, network, domain): |
327 | + ip_parts = network.network.format().split('.') |
328 | + relevant_ip_parts = ip_parts[:-2] |
329 | + |
330 | + first_address = IPAddress(network.first).format() |
331 | + first_address_parts = first_address.split(".") |
332 | + |
333 | + if network.size < 256: |
334 | + last_address = IPAddress(network.last).format() |
335 | + iterator_low = int(first_address_parts[-1]) |
336 | + iterator_high = last_address.split('.')[-1] |
337 | + else: |
338 | + iterator_low = 0 |
339 | + iterator_high = 255 |
340 | + |
341 | + second_octet_offset = int(first_address_parts[-2]) |
342 | + expected_generate_directives = [] |
343 | + directives_needed = network.size / 256 |
344 | + |
345 | + if directives_needed == 0: |
346 | + directives_needed = 1 |
347 | + for num in range(directives_needed): |
348 | + expected_address_base = "%s-%s" % tuple(relevant_ip_parts) |
349 | + expected_address = "%s-%s-$" % ( |
350 | + expected_address_base, num + second_octet_offset) |
351 | + relevant_ip_parts.reverse() |
352 | + expected_rdns_base = ( |
353 | + "%s.%s.in-addr.arpa." % tuple(relevant_ip_parts)) |
354 | + expected_rdns_template = "$.%s.%s" % ( |
355 | + num + second_octet_offset, expected_rdns_base) |
356 | + expected_generate_directives.append( |
357 | + ( |
358 | + "%s-%s" % (iterator_low, iterator_high), |
359 | + expected_rdns_template, |
360 | + "%s.%s." % (expected_address, domain) |
361 | + )) |
362 | + relevant_ip_parts.reverse() |
363 | + return expected_generate_directives |
364 | + |
365 | + def test_returns_single_entry_for_slash_24_network(self): |
366 | + network = IPNetwork("%s/24" % factory.make_ipv4_address()) |
367 | + domain = factory.make_string() |
368 | + expected_generate_directives = self.get_expected_generate_directives( |
369 | + network, domain) |
370 | + directives = DNSReverseZoneConfig.get_GENERATE_directives( |
371 | + network, domain) |
372 | + self.expectThat(directives, HasLength(1)) |
373 | + self.assertItemsEqual(expected_generate_directives, directives) |
374 | + |
375 | + def test_returns_single_entry_for_tiny_network(self): |
376 | + network = IPNetwork("%s/28" % factory.make_ipv4_address()) |
377 | + domain = factory.make_string() |
378 | + |
379 | + expected_generate_directives = self.get_expected_generate_directives( |
380 | + network, domain) |
381 | + directives = DNSReverseZoneConfig.get_GENERATE_directives( |
382 | + network, domain) |
383 | + self.expectThat(directives, HasLength(1)) |
384 | + self.assertItemsEqual(expected_generate_directives, directives) |
385 | + |
386 | + def test_returns_single_entry_for_weird_small_range(self): |
387 | + ip_range = IPRange('10.0.0.1', '10.0.0.255') |
388 | + domain = factory.make_string() |
389 | + directives = DNSReverseZoneConfig.get_GENERATE_directives( |
390 | + ip_range, domain) |
391 | + self.expectThat(directives, HasLength(1)) |
392 | + |
393 | + def test_dtrt_for_larger_networks(self): |
394 | + # For every other network size that we're not explicitly |
395 | + # testing here, |
396 | + # DNSReverseZoneConfig.get_GENERATE_directives() will return |
397 | + # one GENERATE directive for every 255 addresses in the network. |
398 | + for prefixlen in range(23, 17): |
399 | + network = IPNetwork( |
400 | + "%s/%s" % (factory.make_ipv4_address(), prefixlen)) |
401 | + domain = factory.make_string() |
402 | + directives = DNSReverseZoneConfig.get_GENERATE_directives( |
403 | + network, domain) |
404 | + self.expectThat(directives, HasLength(network.size / 256)) |
405 | + |
406 | + def test_returns_two_entries_for_slash_23_network(self): |
407 | + network = IPNetwork(factory.make_ipv4_network(slash=23)) |
408 | + domain = factory.make_string() |
409 | + |
410 | + expected_generate_directives = self.get_expected_generate_directives( |
411 | + network, domain) |
412 | + directives = DNSReverseZoneConfig.get_GENERATE_directives( |
413 | + network, domain) |
414 | + self.expectThat(directives, HasLength(2)) |
415 | + self.assertItemsEqual(expected_generate_directives, directives) |
416 | + |
417 | + def test_rejects_network_larger_than_slash_16(self): |
418 | + network = IPNetwork("%s/15" % factory.make_ipv4_address()) |
419 | + error = self.assertRaises( |
420 | + AssertionError, DNSReverseZoneConfig.get_GENERATE_directives, |
421 | + network, None) |
422 | + self.assertEqual( |
423 | + "Cannot generate reverse zone mapping for any network larger " |
424 | + "than /16.", |
425 | + unicode(error)) |
426 | + |
427 | + def test_sorts_output_by_hostname(self): |
428 | + network = IPNetwork("10.0.0.1/23") |
429 | + domain = factory.make_string() |
430 | + |
431 | + expected_hostname = "10-0-%s-$." + domain + "." |
432 | + expected_rdns = "$.%s.0.10.in-addr.arpa." |
433 | + |
434 | + directives = list(DNSReverseZoneConfig.get_GENERATE_directives( |
435 | + network, domain)) |
436 | + self.expectThat( |
437 | + directives[0], Equals( |
438 | + ("0-255", expected_rdns % "0", expected_hostname % "0"))) |
439 | + self.expectThat( |
440 | + directives[1], Equals( |
441 | + ("0-255", expected_rdns % "1", expected_hostname % "1"))) |
442 | + |
443 | + |
444 | +class TestDNSForwardZoneConfig_GetGenerateDirectives(MAASTestCase): |
445 | + """Tests for `DNSForwardZoneConfig.get_GENERATE_directives()`.""" |
446 | + |
447 | + def test_excplicitly(self): |
448 | + # The other tests in this TestCase rely on |
449 | + # get_expected_generate_directives(), which is quite dense. Here |
450 | + # we test get_GENERATE_directives() explicitly. |
451 | + ip_range = IPRange('192.168.0.1', '192.168.2.128') |
452 | + expected_directives = [ |
453 | + ("1-255", "192-168-0-$", "192.168.0.$"), |
454 | + ("0-255", "192-168-1-$", "192.168.1.$"), |
455 | + ("0-128", "192-168-2-$", "192.168.2.$"), |
456 | + ] |
457 | + self.assertItemsEqual( |
458 | + expected_directives, |
459 | + DNSForwardZoneConfig.get_GENERATE_directives(ip_range)) |
460 | + |
461 | + def get_expected_generate_directives(self, network): |
462 | + ip_parts = network.network.format().split('.') |
463 | + ip_parts[-1] = "$" |
464 | + expected_hostname = "%s" % "-".join(ip_parts) |
465 | + expected_address = ".".join(ip_parts) |
466 | + |
467 | + first_address = IPAddress(network.first).format() |
468 | + first_address_parts = first_address.split(".") |
469 | + last_address = IPAddress(network.last).format() |
470 | + last_address_parts = last_address.split(".") |
471 | + |
472 | + if network.size < 256: |
473 | + iterator_low = int(first_address_parts[-1]) |
474 | + if iterator_low == 0: |
475 | + iterator_low = 1 |
476 | + iterator_high = last_address_parts[-1] |
477 | + else: |
478 | + iterator_low = 0 |
479 | + iterator_high = 255 |
480 | + |
481 | + expected_iterator_values = "%s-%s" % (iterator_low, iterator_high) |
482 | + |
483 | + directives_needed = network.size / 256 |
484 | + if directives_needed == 0: |
485 | + directives_needed = 1 |
486 | + expected_directives = [] |
487 | + for num in range(directives_needed): |
488 | + ip_parts[-2] = unicode(num + int(ip_parts[-2])) |
489 | + expected_address = ".".join(ip_parts) |
490 | + expected_hostname = "%s" % "-".join(ip_parts) |
491 | + expected_directives.append( |
492 | + ( |
493 | + expected_iterator_values, |
494 | + expected_hostname, |
495 | + expected_address |
496 | + )) |
497 | + return expected_directives |
498 | + |
499 | + def test_returns_single_entry_for_slash_24_network(self): |
500 | + network = IPNetwork("%s/24" % factory.make_ipv4_address()) |
501 | + expected_directives = self.get_expected_generate_directives(network) |
502 | + directives = DNSForwardZoneConfig.get_GENERATE_directives( |
503 | + network) |
504 | + self.expectThat(directives, HasLength(1)) |
505 | + self.assertItemsEqual(expected_directives, directives) |
506 | + |
507 | + def test_returns_single_entry_for_tiny_network(self): |
508 | + network = IPNetwork("%s/31" % factory.make_ipv4_address()) |
509 | + |
510 | + expected_directives = self.get_expected_generate_directives(network) |
511 | + directives = DNSForwardZoneConfig.get_GENERATE_directives( |
512 | + network) |
513 | + self.assertEqual(1, len(expected_directives)) |
514 | + self.assertItemsEqual(expected_directives, directives) |
515 | + |
516 | + def test_returns_two_entries_for_slash_23_network(self): |
517 | + network = IPNetwork("%s/23" % factory.make_ipv4_address()) |
518 | + |
519 | + expected_directives = self.get_expected_generate_directives(network) |
520 | + directives = DNSForwardZoneConfig.get_GENERATE_directives( |
521 | + network) |
522 | + self.assertEqual(2, len(expected_directives)) |
523 | + self.assertItemsEqual(expected_directives, directives) |
524 | + |
525 | + def test_dtrt_for_larger_networks(self): |
526 | + # For every other network size that we're not explicitly |
527 | + # testing here, |
528 | + # DNSForwardZoneConfig.get_GENERATE_directives() will return |
529 | + # one GENERATE directive for every 255 addresses in the network. |
530 | + for prefixlen in range(23, 16): |
531 | + network = IPNetwork( |
532 | + "%s/%s" % (factory.make_ipv4_address(), prefixlen)) |
533 | + directives = DNSForwardZoneConfig.get_GENERATE_directives( |
534 | + network) |
535 | + self.assertIsEqual(network.size / 256, len(directives)) |
536 | + |
537 | + def test_rejects_network_larger_than_slash_16(self): |
538 | + network = IPNetwork("%s/15" % factory.make_ipv4_address()) |
539 | + error = self.assertRaises( |
540 | + AssertionError, DNSForwardZoneConfig.get_GENERATE_directives, |
541 | + network) |
542 | + self.assertEqual( |
543 | + "Cannot generate reverse zone mapping for any network larger " |
544 | + "than /16.", |
545 | + unicode(error)) |
546 | + |
547 | + def test_sorts_output(self): |
548 | + network = IPNetwork("10.0.0.0/23") |
549 | + |
550 | + expected_hostname = "10-0-%s-$" |
551 | + expected_address = "10.0.%s.$" |
552 | + |
553 | + directives = list(DNSForwardZoneConfig.get_GENERATE_directives( |
554 | + network)) |
555 | + self.expectThat(len(directives), Equals(2)) |
556 | + self.expectThat( |
557 | + directives[0], Equals( |
558 | + ("0-255", expected_hostname % "0", expected_address % "0"))) |
559 | + self.expectThat( |
560 | + directives[1], Equals( |
561 | + ("0-255", expected_hostname % "1", expected_address % "1"))) |
562 | + |
563 | + |
564 | +class TestIntersectIPRange(MAASTestCase): |
565 | + """Tests for `intersect_iprange()`.""" |
566 | + |
567 | + def test_finds_intersection_between_two_ranges(self): |
568 | + range_1 = IPRange('10.0.0.1', '10.0.0.255') |
569 | + range_2 = IPRange('10.0.0.128', '10.0.0.200') |
570 | + intersect = intersect_iprange(range_1, range_2) |
571 | + self.expectThat( |
572 | + IPAddress(intersect.first), Equals(IPAddress('10.0.0.128'))) |
573 | + self.expectThat( |
574 | + IPAddress(intersect.last), Equals(IPAddress('10.0.0.200'))) |
575 | + |
576 | + def test_ignores_non_intersecting_ranges(self): |
577 | + range_1 = IPRange('10.0.0.1', '10.0.0.255') |
578 | + range_2 = IPRange('10.0.1.128', '10.0.1.200') |
579 | + self.assertIsNone(intersect_iprange(range_1, range_2)) |
580 | + |
581 | + def test_finds_partial_intersection(self): |
582 | + range_1 = IPRange('10.0.0.1', '10.0.0.128') |
583 | + range_2 = IPRange('10.0.0.64', '10.0.0.200') |
584 | + intersect = intersect_iprange(range_1, range_2) |
585 | + self.expectThat( |
586 | + IPAddress(intersect.first), Equals(IPAddress('10.0.0.64'))) |
587 | + self.expectThat( |
588 | + IPAddress(intersect.last), Equals(IPAddress('10.0.0.128'))) |
589 | |
590 | === modified file 'src/provisioningserver/dns/zoneconfig.py' |
591 | --- src/provisioningserver/dns/zoneconfig.py 2014-08-15 11:10:09 +0000 |
592 | +++ src/provisioningserver/dns/zoneconfig.py 2014-10-29 22:31:20 +0000 |
593 | @@ -23,7 +23,11 @@ |
594 | from itertools import chain |
595 | import math |
596 | |
597 | -from netaddr import IPAddress |
598 | +from netaddr import ( |
599 | + IPAddress, |
600 | + IPRange, |
601 | + spanning_cidr, |
602 | + ) |
603 | from netaddr.core import AddrFormatError |
604 | from provisioningserver.dns.config import ( |
605 | compose_config_path, |
606 | @@ -33,6 +37,19 @@ |
607 | from provisioningserver.utils.fs import incremental_write |
608 | |
609 | |
610 | +def intersect_iprange(network, iprange): |
611 | + """Return the intersection between two IPNetworks or IPRanges. |
612 | + |
613 | + IPSet is notoriously inefficient so we intersect ourselves here. |
614 | + """ |
615 | + if network.last >= iprange.first and network.first <= iprange.last: |
616 | + first = max(network.first, iprange.first) |
617 | + last = min(network.last, iprange.last) |
618 | + return IPRange(first, last) |
619 | + else: |
620 | + return None |
621 | + |
622 | + |
623 | def get_fqdn_or_ip_address(target): |
624 | """Returns the ip address is target is a valid ip address, otherwise |
625 | returns the target with appended '.' if missing.""" |
626 | @@ -52,6 +69,52 @@ |
627 | yield hostname, ip |
628 | |
629 | |
630 | +def get_details_for_ip_range(ip_range): |
631 | + """For a given IPRange, return all subnets, a useable prefix and the |
632 | + reverse DNS suffix calculated from that IP range. |
633 | + |
634 | + :return: A tuple of: |
635 | + All subnets of /24 (or smaller if there is no /24 subnet to be |
636 | + found) in `ip_range`. |
637 | + A prefix made from the first two octets in the range. |
638 | + A RDNS suffix calculated from the first two octets in the range. |
639 | + """ |
640 | + # Calculate a spanning network for the range above. There are |
641 | + # 256 /24 networks in a /16, so that's the most /24s we're going |
642 | + # to have to deal with; this matters later on when we iterate |
643 | + # through the /24s within this network. |
644 | + cidr = spanning_cidr(ip_range) |
645 | + subnets = cidr.subnet(max(24, cidr.prefixlen)) |
646 | + |
647 | + # Split the spanning network into /24 subnets, then see if they fall |
648 | + # entirely within the original network range, partially, or not at |
649 | + # all. |
650 | + intersecting_subnets = [] |
651 | + for subnet in subnets: |
652 | + intersect = intersect_iprange(subnet, ip_range) |
653 | + if intersect is None: |
654 | + # The subnet does not fall within the original network. |
655 | + pass |
656 | + else: |
657 | + # The subnet falls partially within the original network, so print |
658 | + # out a $GENERATE expression for a subset of the /24. |
659 | + intersecting_subnets.append(intersect) |
660 | + |
661 | + octet_one = (cidr.value & 0xff000000) >> 24 |
662 | + octet_two = (cidr.value & 0x00ff0000) >> 16 |
663 | + |
664 | + # The first two octets of the network range formatted in the |
665 | + # usual dotted-quad style. We can precalculate the start of any IP |
666 | + # address in the range because we're only ever dealing with /16 |
667 | + # networks and smaller. |
668 | + prefix = "%d.%d" % (octet_one, octet_two) |
669 | + |
670 | + # Similarly, we can calculate what the reverse DNS suffix is going |
671 | + # to look like. |
672 | + rdns_suffix = "%d.%d.in-addr.arpa." % (octet_two, octet_one) |
673 | + return intersecting_subnets, prefix, rdns_suffix |
674 | + |
675 | + |
676 | class DNSZoneConfigBase: |
677 | """Base class for zone writers.""" |
678 | |
679 | @@ -117,7 +180,8 @@ |
680 | """ |
681 | self._dns_ip = kwargs.pop('dns_ip', None) |
682 | self._mapping = kwargs.pop('mapping', {}) |
683 | - self._network = None |
684 | + self._network = kwargs.pop('network', None) |
685 | + self._dynamic_ranges = kwargs.pop('dynamic_ranges', []) |
686 | self._srv_mapping = kwargs.pop('srv_mapping', []) |
687 | super(DNSForwardZoneConfig, self).__init__( |
688 | domain, zone_name=domain, **kwargs) |
689 | @@ -187,8 +251,45 @@ |
690 | target) |
691 | yield (record.service, item) |
692 | |
693 | + @classmethod |
694 | + def get_GENERATE_directives(cls, dynamic_range): |
695 | + """Return the GENERATE directives for the forward zone of a network. |
696 | + """ |
697 | + assert dynamic_range.size <= 256 ** 2, ( |
698 | + "Cannot generate reverse zone mapping for any network larger than " |
699 | + "/16.") |
700 | + |
701 | + generate_directives = set() |
702 | + subnets, prefix, _ = get_details_for_ip_range(dynamic_range) |
703 | + for subnet in subnets: |
704 | + iterator = "%d-%d" % ( |
705 | + (subnet.first & 0x000000ff), |
706 | + (subnet.last & 0x000000ff)) |
707 | + |
708 | + hostname = "%s-%d-$" % ( |
709 | + prefix.replace('.', '-'), |
710 | + # Calculate what the third quad (i.e. 10.0.X.1) value should |
711 | + # be for this subnet. |
712 | + (subnet.first & 0x0000ff00) >> 8, |
713 | + ) |
714 | + |
715 | + ip_address = "%s.%d.$" % ( |
716 | + prefix, |
717 | + (subnet.first & 0x0000ff00) >> 8) |
718 | + generate_directives.add((iterator, hostname, ip_address)) |
719 | + |
720 | + return sorted( |
721 | + generate_directives, key=lambda directive: directive[2]) |
722 | + |
723 | def write_config(self): |
724 | """Write the zone file.""" |
725 | + # Create GENERATE directives for IPv4 ranges. |
726 | + generate_directives = list( |
727 | + chain.from_iterable( |
728 | + self.get_GENERATE_directives(dynamic_range) |
729 | + for dynamic_range in self._dynamic_ranges |
730 | + if dynamic_range.version == 4 |
731 | + )) |
732 | self.write_zone_file( |
733 | self.target_path, self.make_parameters(), |
734 | { |
735 | @@ -200,6 +301,9 @@ |
736 | 'AAAA': self.get_AAAA_mapping( |
737 | self._mapping, self.domain, self._dns_ip), |
738 | }, |
739 | + 'generate_directives': { |
740 | + 'A': generate_directives, |
741 | + } |
742 | }) |
743 | |
744 | |
745 | @@ -225,6 +329,7 @@ |
746 | """ |
747 | self._mapping = kwargs.pop('mapping', {}) |
748 | self._network = kwargs.pop("network", None) |
749 | + self._dynamic_ranges = kwargs.pop('dynamic_ranges', []) |
750 | zone_name = self.compose_zone_name(self._network) |
751 | super(DNSReverseZoneConfig, self).__init__( |
752 | domain, zone_name=zone_name, **kwargs) |
753 | @@ -279,8 +384,40 @@ |
754 | if IPAddress(ip) in network |
755 | ) |
756 | |
757 | + @classmethod |
758 | + def get_GENERATE_directives(cls, dynamic_range, domain): |
759 | + """Return the GENERATE directives for the reverse zone of a network.""" |
760 | + assert dynamic_range.size <= 256 ** 2, ( |
761 | + "Cannot generate reverse zone mapping for any network larger than " |
762 | + "/16.") |
763 | + |
764 | + generate_directives = set() |
765 | + subnets, prefix, rdns_suffix = get_details_for_ip_range(dynamic_range) |
766 | + for subnet in subnets: |
767 | + iterator = "%d-%d" % ( |
768 | + (subnet.first & 0x000000ff), |
769 | + (subnet.last & 0x000000ff)) |
770 | + hostname = "%s-%d-$" % ( |
771 | + prefix.replace('.', '-'), |
772 | + (subnet.first & 0x0000ff00) >> 8) |
773 | + rdns = "$.%d.%s" % ( |
774 | + (subnet.first & 0x0000ff00) >> 8, |
775 | + rdns_suffix) |
776 | + generate_directives.add( |
777 | + (iterator, rdns, "%s.%s." % (hostname, domain))) |
778 | + |
779 | + return sorted( |
780 | + generate_directives, key=lambda directive: directive[2]) |
781 | + |
782 | def write_config(self): |
783 | """Write the zone file.""" |
784 | + # Create GENERATE directives for IPv4 ranges. |
785 | + generate_directives = list( |
786 | + chain.from_iterable( |
787 | + self.get_GENERATE_directives(dynamic_range, self.domain) |
788 | + for dynamic_range in self._dynamic_ranges |
789 | + if dynamic_range.version == 4 |
790 | + )) |
791 | self.write_zone_file( |
792 | self.target_path, self.make_parameters(), |
793 | { |
794 | @@ -288,5 +425,8 @@ |
795 | 'PTR': self.get_PTR_mapping( |
796 | self._mapping, self.domain, self._network), |
797 | }, |
798 | + 'generate_directives': { |
799 | + 'PTR': generate_directives, |
800 | + } |
801 | } |
802 | ) |
Approve straight backport