Merge lp:~gmb/maas/restrict-dynamic-range-to-slash-16-bug-1382190 into lp:~maas-committers/maas/trunk

Proposed by Graham Binns
Status: Merged
Approved by: Graham Binns
Approved revision: no longer in the source branch.
Merged at revision: 3315
Proposed branch: lp:~gmb/maas/restrict-dynamic-range-to-slash-16-bug-1382190
Merge into: lp:~maas-committers/maas/trunk
Diff against target: 479 lines (+235/-67)
6 files modified
src/maasserver/forms.py (+48/-1)
src/maasserver/tests/test_forms_nodegroupinterface.py (+58/-0)
src/provisioningserver/dns/tests/test_zoneconfig.py (+29/-46)
src/provisioningserver/dns/zoneconfig.py (+19/-20)
src/provisioningserver/utils/network.py (+27/-0)
src/provisioningserver/utils/tests/test_network.py (+54/-0)
To merge this branch: bzr merge lp:~gmb/maas/restrict-dynamic-range-to-slash-16-bug-1382190
Reviewer Review Type Date Requested Status
Julian Edwards (community) Approve
Review via email: mp+240061@code.launchpad.net

Commit message

Restrict the dynamic range on node group interfaces to /16 or smaller for IPv4 networks. Allow any size of dynamic range for IPv6 networks.

This is to allow us to generate DNS mappings for all the addresses in the v4 networks. We need to restrict the size of the network so as to keep the size of the zone files reasonable.

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

Potential problem highlighted below.

review: Needs Information
Revision history for this message
Graham Binns (gmb) wrote :

On 30 October 2014 02:35, Julian Edwards <email address hidden> wrote:
>> + if ip_range.size > 256 ** 2:
>> + raise ValidationError(ERROR_MESSAGE_DYNAMIC_RANGE_TOO_LARGE)
>> +
>
> This check assumes that the IP values will be on exact boundaries. You could straddle more than a /16 and it won't notice, even though the IP range is under 65536 addresses.

How? I don't see the assumption. For example:

In [12]: netaddr.IPRange('10.0.0.55', '10.1.0.55').size > 256 ** 2
Out[12]: True

Am I missing something?

> I am not familiar with your GENERATE branch so I don't know if this is going to break it or not, but I'm raising the point for your consideration.

Well, if you tried to put an oversize network through it, you'll get
an AssertionError. Actually, I think that should change so that
creating the GENERATEs becomes a no-op in that case (otherwise, how do
we handle networks > /16 that people already have — say if they've
been testing with the 1.7 betas), and I'll take care of that as a
drive-by in this branch.

Revision history for this message
Graham Binns (gmb) wrote :

On 30 October 2014 02:35, Julian Edwards <email address hidden> wrote:
> Review: Needs Information
>
> Potential problem highlighted below.
>
> Diff comments:
>
>> === modified file 'src/maasserver/forms.py'
>> --- src/maasserver/forms.py 2014-10-22 19:49:03 +0000
>> +++ src/maasserver/forms.py 2014-10-29 21:06:23 +0000
>> @@ -46,6 +46,7 @@
>> "ZoneForm",
>> ]
>>
>> +from netaddr import IPRange
>> import collections
>> import json
>> import pipes
>> @@ -1305,6 +1306,36 @@
>> "in that range.")
>>
>>
>> +ERROR_MESSAGE_DYNAMIC_RANGE_TOO_LARGE = (
>> + "Dynamic address range cannot be larger than a /16 (65536 addresses) ")
>> +
>> +
>> +def validate_new_dynamic_range_size(instance, ip_range_low, ip_range_high):
>> + """Check that a ip address range is of a manageable size.
>> +
>> + :raises ValidationError: If the ip range is larger than a /16
>> + network.
>> + """
>> + # Return early if the instance is not already managed, it currently
>> + # has no static IP range, or the static IP range hasn't changed.
>
> s/static/dynamic/

Gah! Fixed.

>> +
>> def test_calls_get_duplicate_fqdns_when_appropriate(self):
>> # Check for duplicate FQDNs if the NodeGroupInterface has a
>
> You need a happy-path test for an IPv4 range as well, I think.

You're right, I do. Added.

Revision history for this message
Julian Edwards (julian-edwards) wrote :

Approved as discussed on the hangout.

review: Approve
Revision history for this message
Julian Edwards (julian-edwards) wrote :

On Thursday 30 Oct 2014 07:46:26 you wrote:
> On 30 October 2014 02:35, Julian Edwards <email address hidden>
wrote:
> >> + if ip_range.size > 256 ** 2:
> >> + raise ValidationError(ERROR_MESSAGE_DYNAMIC_RANGE_TOO_LARGE)
> >> +
> >
> > This check assumes that the IP values will be on exact boundaries. You
> > could straddle more than a /16 and it won't notice, even though the IP
> > range is under 65536 addresses.
> How? I don't see the assumption. For example:
>
> In [12]: netaddr.IPRange('10.0.0.55', '10.1.0.55').size > 256 ** 2
> Out[12]: True
>
> Am I missing something?

Yes :)

>
> > I am not familiar with your GENERATE branch so I don't know if this is
> > going to break it or not, but I'm raising the point for your
> > consideration.
> Well, if you tried to put an oversize network through it, you'll get
> an AssertionError. Actually, I think that should change so that
> creating the GENERATEs becomes a no-op in that case (otherwise, how do
> we handle networks > /16 that people already have — say if they've
> been testing with the 1.7 betas), and I'll take care of that as a
> drive-by in this branch.

<gmb> bigjools: So, you found a nice, interesting edge case… The GENERATE
stuff expects everythign to be in the same /16, so we need to enforce that
(fixing it is horrid, and if users want a dynamic range to span two /16s then
fuck ‘em, frankly). It doesn’t blow up… it but for 10.0.0.55 - 10.1.0.54, say,
you’ll get GENERATEs for 10.0.0.0 - 10.0.255.55. Which is surprising.

Revision history for this message
MAAS Lander (maas-lander) wrote :
Download full text (23.9 KiB)

The attempt to merge lp:~gmb/maas/restrict-dynamic-range-to-slash-16-bug-1382190 into lp:maas failed. Below is the output from the failed tests.

Ign http://security.ubuntu.com trusty-security InRelease
Ign http://nova.clouds.archive.ubuntu.com trusty InRelease
Get:1 http://security.ubuntu.com trusty-security Release.gpg [933 B]
Ign http://nova.clouds.archive.ubuntu.com trusty-updates InRelease
Hit http://nova.clouds.archive.ubuntu.com trusty Release.gpg
Get:2 http://security.ubuntu.com trusty-security Release [59.7 kB]
Get:3 http://nova.clouds.archive.ubuntu.com trusty-updates Release.gpg [933 B]
Hit http://nova.clouds.archive.ubuntu.com trusty Release
Get:4 http://nova.clouds.archive.ubuntu.com trusty-updates Release [59.7 kB]
Hit http://nova.clouds.archive.ubuntu.com trusty/main Sources
Hit http://nova.clouds.archive.ubuntu.com trusty/universe Sources
Get:5 http://security.ubuntu.com trusty-security/main Sources [48.3 kB]
Hit http://nova.clouds.archive.ubuntu.com trusty/main amd64 Packages
Hit http://nova.clouds.archive.ubuntu.com trusty/universe amd64 Packages
Hit http://nova.clouds.archive.ubuntu.com trusty/main Translation-en
Hit http://nova.clouds.archive.ubuntu.com trusty/universe Translation-en
Get:6 http://security.ubuntu.com trusty-security/universe Sources [11.2 kB]
Get:7 http://security.ubuntu.com trusty-security/main amd64 Packages [151 kB]
Get:8 http://security.ubuntu.com trusty-security/universe amd64 Packages [50.4 kB]
Hit http://security.ubuntu.com trusty-security/main Translation-en
Hit http://security.ubuntu.com trusty-security/universe Translation-en
Ign http://nova.clouds.archive.ubuntu.com trusty/main Translation-en_US
Ign http://nova.clouds.archive.ubuntu.com trusty/universe Translation-en_US
Get:9 http://nova.clouds.archive.ubuntu.com trusty-updates/main Sources [134 kB]
Get:10 http://nova.clouds.archive.ubuntu.com trusty-updates/universe Sources [89.3 kB]
Get:11 http://nova.clouds.archive.ubuntu.com trusty-updates/main amd64 Packages [352 kB]
Get:12 http://nova.clouds.archive.ubuntu.com trusty-updates/universe amd64 Packages [216 kB]
Get:13 http://nova.clouds.archive.ubuntu.com trusty-updates/main Translation-en [159 kB]
Hit http://nova.clouds.archive.ubuntu.com trusty-updates/universe Translation-en
Fetched 1,332 kB in 2s (458 kB/s)
Reading package lists...
sudo DEBIAN_FRONTEND=noninteractive apt-get -y \
     --no-install-recommends install apache2 authbind bind9 bind9utils build-essential bzr-builddeb curl daemontools debhelper dh-apport distro-info dnsutils firefox freeipmi-tools gjs ipython isc-dhcp-common libjs-raphael libjs-yui3-full libjs-yui3-min libpq-dev make pep8 postgresql pyflakes python-amqplib python-bzrlib python-celery python-convoy python-crochet python-cssselect python-curtin python-dev python-distro-info python-django python-django-piston python-django-south python-djorm-ext-pgarray python-docutils python-extras python-fixtures python-flake8 python-formencode python-hivex python-httplib2 python-jinja2 python-jsonschema python-lockfile python-lxml python-mimeparse python-mock python-netaddr python-netifaces python-nose python-oauth python-oops python-oops-amqp python-oops-datedir-repo python-oops-...

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'src/maasserver/forms.py'
2--- src/maasserver/forms.py 2014-10-22 19:49:03 +0000
3+++ src/maasserver/forms.py 2014-10-30 11:08:14 +0000
4@@ -152,6 +152,8 @@
5 from metadataserver.models import CommissioningScript
6 from netaddr import (
7 IPAddress,
8+ IPNetwork,
9+ IPRange,
10 valid_ipv6,
11 )
12 from provisioningserver.logger import get_maas_logger
13@@ -160,7 +162,10 @@
14 NoConnectionsAvailable,
15 NoSuchOperatingSystem,
16 )
17-from provisioningserver.utils.network import make_network
18+from provisioningserver.utils.network import (
19+ ip_range_within_network,
20+ make_network,
21+ )
22
23
24 maaslog = get_maas_logger()
25@@ -1305,6 +1310,39 @@
26 "in that range.")
27
28
29+ERROR_MESSAGE_DYNAMIC_RANGE_SPANS_SLASH_16S = (
30+ "All addresses in the dynamic range must be within the same /16 "
31+ "network.")
32+
33+
34+def validate_new_dynamic_range_size(instance, ip_range_low, ip_range_high):
35+ """Check that a ip address range is of a manageable size.
36+
37+ :raises ValidationError: If the ip range is larger than a /16
38+ network.
39+ """
40+ # Return early if the instance is not already managed, its dynamic
41+ # IP range hasn't changed, or the new values are blank.
42+ if not instance.is_managed:
43+ return True
44+ # Deliberately vague check to allow for empty strings.
45+ if (not ip_range_low and not ip_range_high):
46+ return True
47+ if (ip_range_low == instance.ip_range_low and
48+ ip_range_high == instance.ip_range_high):
49+ return True
50+ ip_range = IPRange(ip_range_low, ip_range_high)
51+
52+ # Allow any size of dynamic range for v6 networks, but limit v4
53+ # networks to /16s.
54+ if ip_range.version == 6:
55+ return True
56+
57+ slash_16_network = IPNetwork("%s/16" % IPAddress(ip_range.first))
58+ if not ip_range_within_network(ip_range, slash_16_network):
59+ raise ValidationError(ERROR_MESSAGE_DYNAMIC_RANGE_SPANS_SLASH_16S)
60+
61+
62 def validate_new_static_ip_ranges(instance, static_ip_range_low,
63 static_ip_range_high):
64 """Check that new static IP ranges don't exclude allocated addresses.
65@@ -1552,6 +1590,15 @@
66 netmask = 'ffff:ffff:ffff:ffff::'
67 cleaned_data['subnet_mask'] = unicode(netmask)
68
69+ dynamic_range_low = cleaned_data.get('ip_range_low')
70+ dynamic_range_high = cleaned_data.get('ip_range_high')
71+ try:
72+ validate_new_dynamic_range_size(
73+ self.instance, dynamic_range_low, dynamic_range_high)
74+ except forms.ValidationError as exception:
75+ set_form_error(self, 'ip_range_low', exception.message)
76+ set_form_error(self, 'ip_range_high', exception.message)
77+
78 static_ip_range_low = cleaned_data.get('static_ip_range_low')
79 static_ip_range_high = cleaned_data.get('static_ip_range_high')
80 try:
81
82=== modified file 'src/maasserver/tests/test_forms_nodegroupinterface.py'
83--- src/maasserver/tests/test_forms_nodegroupinterface.py 2014-10-10 04:55:03 +0000
84+++ src/maasserver/tests/test_forms_nodegroupinterface.py 2014-10-30 11:08:14 +0000
85@@ -21,6 +21,7 @@
86 NODEGROUPINTERFACE_MANAGEMENT,
87 )
88 from maasserver.forms import (
89+ ERROR_MESSAGE_DYNAMIC_RANGE_SPANS_SLASH_16S,
90 ERROR_MESSAGE_STATIC_RANGE_IN_USE,
91 NodeGroupInterfaceForm,
92 )
93@@ -221,6 +222,63 @@
94 [ERROR_MESSAGE_STATIC_RANGE_IN_USE],
95 form._errors['static_ip_range_high'])
96
97+ def test_rejects_ipv4_dynamic_ranges_across_multiple_slash_16s(self):
98+ # Even if a dynamic range is < 65536 addresses, it can't cross
99+ # two /16 networks.
100+ network = IPNetwork("10.1.0.0/8")
101+ nodegroup = factory.make_NodeGroup(
102+ status=NODEGROUP_STATUS.ACCEPTED,
103+ management=NODEGROUPINTERFACE_MANAGEMENT.DHCP_AND_DNS,
104+ network=network, static_ip_range_low=None,
105+ static_ip_range_high=None)
106+ [interface] = nodegroup.get_managed_interfaces()
107+ form = NodeGroupInterfaceForm(
108+ data={
109+ 'ip_range_low': '10.1.255.255',
110+ 'ip_range_high': '10.2.0.1',
111+ },
112+ instance=interface)
113+ self.assertFalse(form.is_valid())
114+ self.assertEqual(
115+ [ERROR_MESSAGE_DYNAMIC_RANGE_SPANS_SLASH_16S],
116+ form._errors['ip_range_low'])
117+ self.assertEqual(
118+ [ERROR_MESSAGE_DYNAMIC_RANGE_SPANS_SLASH_16S],
119+ form._errors['ip_range_low'])
120+
121+ def test_allows_sane_ipv4_dynamic_range_size(self):
122+ network = IPNetwork("10.1.0.0/8")
123+ nodegroup = factory.make_NodeGroup(
124+ status=NODEGROUP_STATUS.ACCEPTED,
125+ management=NODEGROUPINTERFACE_MANAGEMENT.DHCP_AND_DNS,
126+ network=network, static_ip_range_low=None,
127+ static_ip_range_high=None)
128+ [interface] = nodegroup.get_managed_interfaces()
129+ form = NodeGroupInterfaceForm(
130+ data={
131+ 'ip_range_low': '10.0.0.1',
132+ 'ip_range_high': '10.0.1.255',
133+ },
134+ instance=interface)
135+ self.assertTrue(form.is_valid())
136+
137+ def test_allows_any_size_ipv6_dynamic_range(self):
138+ network = factory.make_ipv6_network(slash=64)
139+ nodegroup = factory.make_NodeGroup(
140+ status=NODEGROUP_STATUS.ACCEPTED,
141+ management=NODEGROUPINTERFACE_MANAGEMENT.DHCP_AND_DNS,
142+ network=network)
143+ [interface] = nodegroup.get_managed_interfaces()
144+ form = NodeGroupInterfaceForm(
145+ data={
146+ 'ip_range_low': IPAddress(network.first).format(),
147+ 'ip_range_high': IPAddress(network.last).format(),
148+ 'static_ip_range_low': '',
149+ 'static_ip_range_high': '',
150+ },
151+ instance=interface)
152+ self.assertTrue(form.is_valid())
153+
154 def test_calls_get_duplicate_fqdns_when_appropriate(self):
155 # Check for duplicate FQDNs if the NodeGroupInterface has a
156 # NodeGroup and is managing DNS.
157
158=== modified file 'src/provisioningserver/dns/tests/test_zoneconfig.py'
159--- src/provisioningserver/dns/tests/test_zoneconfig.py 2014-10-29 19:30:18 +0000
160+++ src/provisioningserver/dns/tests/test_zoneconfig.py 2014-10-30 11:08:14 +0000
161@@ -37,7 +37,6 @@
162 from provisioningserver.dns.zoneconfig import (
163 DNSForwardZoneConfig,
164 DNSReverseZoneConfig,
165- intersect_iprange,
166 )
167 from testtools.matchers import (
168 Contains,
169@@ -443,9 +442,9 @@
170 # The other tests in this TestCase rely on
171 # get_expected_generate_directives(), which is quite dense. Here
172 # we test get_GENERATE_directives() explicitly.
173- ip_range = IPRange('192.168.0.1', '192.168.2.128')
174+ ip_range = IPRange('192.168.0.55', '192.168.2.128')
175 expected_directives = [
176- ("1-255", "$.0.168.192.in-addr.arpa.", "192-168-0-$.domain."),
177+ ("55-255", "$.0.168.192.in-addr.arpa.", "192-168-0-$.domain."),
178 ("0-255", "$.1.168.192.in-addr.arpa.", "192-168-1-$.domain."),
179 ("0-128", "$.2.168.192.in-addr.arpa.", "192-168-2-$.domain."),
180 ]
181@@ -545,15 +544,21 @@
182 self.expectThat(directives, HasLength(2))
183 self.assertItemsEqual(expected_generate_directives, directives)
184
185- def test_rejects_network_larger_than_slash_16(self):
186+ def test_ignores_network_larger_than_slash_16(self):
187 network = IPNetwork("%s/15" % factory.make_ipv4_address())
188- error = self.assertRaises(
189- AssertionError, DNSReverseZoneConfig.get_GENERATE_directives,
190- network, None)
191 self.assertEqual(
192- "Cannot generate reverse zone mapping for any network larger "
193- "than /16.",
194- unicode(error))
195+ [],
196+ DNSReverseZoneConfig.get_GENERATE_directives(
197+ network, factory.make_string()))
198+
199+ def test_ignores_networks_that_span_slash_16s(self):
200+ # If the upper and lower bounds of a range span two /16 networks
201+ # (but contain between them no more than 65536 addresses),
202+ # get_GENERATE_directives() will return early
203+ ip_range = IPRange('10.0.0.55', '10.1.0.54')
204+ directives = DNSReverseZoneConfig.get_GENERATE_directives(
205+ ip_range, factory.make_string())
206+ self.assertEqual([], directives)
207
208 def test_sorts_output_by_hostname(self):
209 network = IPNetwork("10.0.0.1/23")
210@@ -579,9 +584,9 @@
211 # The other tests in this TestCase rely on
212 # get_expected_generate_directives(), which is quite dense. Here
213 # we test get_GENERATE_directives() explicitly.
214- ip_range = IPRange('192.168.0.1', '192.168.2.128')
215+ ip_range = IPRange('192.168.0.55', '192.168.2.128')
216 expected_directives = [
217- ("1-255", "192-168-0-$", "192.168.0.$"),
218+ ("55-255", "192-168-0-$", "192.168.0.$"),
219 ("0-255", "192-168-1-$", "192.168.1.$"),
220 ("0-128", "192-168-2-$", "192.168.2.$"),
221 ]
222@@ -665,15 +670,20 @@
223 network)
224 self.assertIsEqual(network.size / 256, len(directives))
225
226- def test_rejects_network_larger_than_slash_16(self):
227+ def test_ignores_network_larger_than_slash_16(self):
228 network = IPNetwork("%s/15" % factory.make_ipv4_address())
229- error = self.assertRaises(
230- AssertionError, DNSForwardZoneConfig.get_GENERATE_directives,
231- network)
232 self.assertEqual(
233- "Cannot generate reverse zone mapping for any network larger "
234- "than /16.",
235- unicode(error))
236+ [],
237+ DNSForwardZoneConfig.get_GENERATE_directives(network))
238+
239+ def test_ignores_networks_that_span_slash_16s(self):
240+ # If the upper and lower bounds of a range span two /16 networks
241+ # (but contain between them no more than 65536 addresses),
242+ # get_GENERATE_directives() will return early
243+ ip_range = IPRange('10.0.0.55', '10.1.0.54')
244+ directives = DNSForwardZoneConfig.get_GENERATE_directives(
245+ ip_range)
246+ self.assertEqual([], directives)
247
248 def test_sorts_output(self):
249 network = IPNetwork("10.0.0.0/23")
250@@ -690,30 +700,3 @@
251 self.expectThat(
252 directives[1], Equals(
253 ("0-255", expected_hostname % "1", expected_address % "1")))
254-
255-
256-class TestIntersectIPRange(MAASTestCase):
257- """Tests for `intersect_iprange()`."""
258-
259- def test_finds_intersection_between_two_ranges(self):
260- range_1 = IPRange('10.0.0.1', '10.0.0.255')
261- range_2 = IPRange('10.0.0.128', '10.0.0.200')
262- intersect = intersect_iprange(range_1, range_2)
263- self.expectThat(
264- IPAddress(intersect.first), Equals(IPAddress('10.0.0.128')))
265- self.expectThat(
266- IPAddress(intersect.last), Equals(IPAddress('10.0.0.200')))
267-
268- def test_ignores_non_intersecting_ranges(self):
269- range_1 = IPRange('10.0.0.1', '10.0.0.255')
270- range_2 = IPRange('10.0.1.128', '10.0.1.200')
271- self.assertIsNone(intersect_iprange(range_1, range_2))
272-
273- def test_finds_partial_intersection(self):
274- range_1 = IPRange('10.0.0.1', '10.0.0.128')
275- range_2 = IPRange('10.0.0.64', '10.0.0.200')
276- intersect = intersect_iprange(range_1, range_2)
277- self.expectThat(
278- IPAddress(intersect.first), Equals(IPAddress('10.0.0.64')))
279- self.expectThat(
280- IPAddress(intersect.last), Equals(IPAddress('10.0.0.128')))
281
282=== modified file 'src/provisioningserver/dns/zoneconfig.py'
283--- src/provisioningserver/dns/zoneconfig.py 2014-10-29 19:26:07 +0000
284+++ src/provisioningserver/dns/zoneconfig.py 2014-10-30 11:08:14 +0000
285@@ -25,7 +25,7 @@
286
287 from netaddr import (
288 IPAddress,
289- IPRange,
290+ IPNetwork,
291 spanning_cidr,
292 )
293 from netaddr.core import AddrFormatError
294@@ -35,19 +35,10 @@
295 report_missing_config_dir,
296 )
297 from provisioningserver.utils.fs import incremental_write
298-
299-
300-def intersect_iprange(network, iprange):
301- """Return the intersection between two IPNetworks or IPRanges.
302-
303- IPSet is notoriously inefficient so we intersect ourselves here.
304- """
305- if network.last >= iprange.first and network.first <= iprange.last:
306- first = max(network.first, iprange.first)
307- last = min(network.last, iprange.last)
308- return IPRange(first, last)
309- else:
310- return None
311+from provisioningserver.utils.network import (
312+ intersect_iprange,
313+ ip_range_within_network,
314+ )
315
316
317 def get_fqdn_or_ip_address(target):
318@@ -255,9 +246,13 @@
319 def get_GENERATE_directives(cls, dynamic_range):
320 """Return the GENERATE directives for the forward zone of a network.
321 """
322- assert dynamic_range.size <= 256 ** 2, (
323- "Cannot generate reverse zone mapping for any network larger than "
324- "/16.")
325+ slash_16 = IPNetwork("%s/16" % IPAddress(dynamic_range.first))
326+ if (dynamic_range.size > 256 ** 2 or
327+ not ip_range_within_network(dynamic_range, slash_16)):
328+ # We can't issue a sane set of $GENERATEs for any network
329+ # larger than a /16, or for one that spans two /16s, so we
330+ # don't try.
331+ return []
332
333 generate_directives = set()
334 subnets, prefix, _ = get_details_for_ip_range(dynamic_range)
335@@ -387,9 +382,13 @@
336 @classmethod
337 def get_GENERATE_directives(cls, dynamic_range, domain):
338 """Return the GENERATE directives for the reverse zone of a network."""
339- assert dynamic_range.size <= 256 ** 2, (
340- "Cannot generate reverse zone mapping for any network larger than "
341- "/16.")
342+ slash_16 = IPNetwork("%s/16" % IPAddress(dynamic_range.first))
343+ if (dynamic_range.size > 256 ** 2 or
344+ not ip_range_within_network(dynamic_range, slash_16)):
345+ # We can't issue a sane set of $GENERATEs for any network
346+ # larger than a /16, or for one that spans two /16s, so we
347+ # don't try.
348+ return []
349
350 generate_directives = set()
351 subnets, prefix, rdns_suffix = get_details_for_ip_range(dynamic_range)
352
353=== modified file 'src/provisioningserver/utils/network.py'
354--- src/provisioningserver/utils/network.py 2014-09-10 16:20:31 +0000
355+++ src/provisioningserver/utils/network.py 2014-10-30 11:08:14 +0000
356@@ -20,6 +20,8 @@
357 'get_all_interface_addresses',
358 'make_network',
359 'resolve_hostname',
360+ 'intersect_iprange',
361+ 'ip_range_within_network',
362 ]
363
364
365@@ -35,6 +37,7 @@
366 from netaddr import (
367 IPAddress,
368 IPNetwork,
369+ IPRange,
370 )
371 import netifaces
372 from provisioningserver.utils.shell import call_and_check
373@@ -196,3 +199,27 @@
374 IPAddress(sockaddr[0])
375 for family, socktype, proto, canonname, sockaddr in address_info
376 }
377+
378+
379+def intersect_iprange(network, iprange):
380+ """Return the intersection between two IPNetworks or IPRanges.
381+
382+ IPSet is notoriously inefficient so we intersect ourselves here.
383+ """
384+ if network.last >= iprange.first and network.first <= iprange.last:
385+ first = max(network.first, iprange.first)
386+ last = min(network.last, iprange.last)
387+ return IPRange(first, last)
388+ else:
389+ return None
390+
391+
392+def ip_range_within_network(ip_range, network):
393+ """Check that the whole of a given IP range is within a given network."""
394+ # Make sure that ip_range is an IPRange and not an IPNetwork,
395+ # otherwise this won't work.
396+ if isinstance(ip_range, IPNetwork):
397+ ip_range = IPRange(
398+ IPAddress(network.first), IPAddress(network.last))
399+ return all([
400+ intersect_iprange(cidr, network) for cidr in ip_range.cidrs()])
401
402=== modified file 'src/provisioningserver/utils/tests/test_network.py'
403--- src/provisioningserver/utils/tests/test_network.py 2014-09-19 10:10:49 +0000
404+++ src/provisioningserver/utils/tests/test_network.py 2014-10-30 11:08:14 +0000
405@@ -28,6 +28,7 @@
406 from netaddr import (
407 IPAddress,
408 IPNetwork,
409+ IPRange,
410 )
411 import netifaces
412 from netifaces import (
413@@ -43,9 +44,12 @@
414 find_mac_via_arp,
415 get_all_addresses_for_interface,
416 get_all_interface_addresses,
417+ intersect_iprange,
418+ ip_range_within_network,
419 make_network,
420 resolve_hostname,
421 )
422+from testtools.matchers import Equals
423
424
425 class TestMakeNetwork(MAASTestCase):
426@@ -433,3 +437,53 @@
427 self.assertRaises(
428 KeyError,
429 resolve_hostname, factory.make_hostname(), 4)
430+
431+
432+class TestIntersectIPRange(MAASTestCase):
433+ """Tests for `intersect_iprange()`."""
434+
435+ def test_finds_intersection_between_two_ranges(self):
436+ range_1 = IPRange('10.0.0.1', '10.0.0.255')
437+ range_2 = IPRange('10.0.0.128', '10.0.0.200')
438+ intersect = intersect_iprange(range_1, range_2)
439+ self.expectThat(
440+ IPAddress(intersect.first), Equals(IPAddress('10.0.0.128')))
441+ self.expectThat(
442+ IPAddress(intersect.last), Equals(IPAddress('10.0.0.200')))
443+
444+ def test_ignores_non_intersecting_ranges(self):
445+ range_1 = IPRange('10.0.0.1', '10.0.0.255')
446+ range_2 = IPRange('10.0.1.128', '10.0.1.200')
447+ self.assertIsNone(intersect_iprange(range_1, range_2))
448+
449+ def test_finds_partial_intersection(self):
450+ range_1 = IPRange('10.0.0.1', '10.0.0.128')
451+ range_2 = IPRange('10.0.0.64', '10.0.0.200')
452+ intersect = intersect_iprange(range_1, range_2)
453+ self.expectThat(
454+ IPAddress(intersect.first), Equals(IPAddress('10.0.0.64')))
455+ self.expectThat(
456+ IPAddress(intersect.last), Equals(IPAddress('10.0.0.128')))
457+
458+
459+class TestIPRangeWithinNetwork(MAASTestCase):
460+
461+ def test_returns_true_when_ip_range_is_within_network(self):
462+ ip_range = IPRange('10.0.0.55', '10.0.255.55')
463+ ip_network = IPNetwork('10.0.0.0/16')
464+ self.assertTrue(ip_range_within_network(ip_range, ip_network))
465+
466+ def test_returns_false_when_ip_range_is_within_network(self):
467+ ip_range = IPRange('192.0.0.55', '192.0.255.55')
468+ ip_network = IPNetwork('10.0.0.0/16')
469+ self.assertFalse(ip_range_within_network(ip_range, ip_network))
470+
471+ def test_returns_false_when_ip_range_is_partially_within_network(self):
472+ ip_range = IPRange('10.0.0.55', '10.1.0.55')
473+ ip_network = IPNetwork('10.0.0.0/16')
474+ self.assertFalse(ip_range_within_network(ip_range, ip_network))
475+
476+ def test_works_with_two_ip_networks(self):
477+ network_1 = IPNetwork('10.0.0.0/16')
478+ network_2 = IPNetwork('10.0.0.0/24')
479+ self.assertTrue(ip_range_within_network(network_2, network_1))