Merge lp:~gmb/maas/backport-generate-directives-to-1.7 into lp:maas/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
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 NodeGroupInterface's dynamic range, as long as that dynamic range uses IPv4.

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.

To post a comment you must log in.
Revision history for this message
Julian Edwards (julian-edwards) wrote :

Approve straight backport

review: Approve
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 )

Subscribers

People subscribed via source and target branches

to all changes: