Merge lp:~gmb/maas/restrict-dynamic-range-to-slash-16-bug-1382190 into lp:~maas-committers/maas/trunk
- restrict-dynamic-range-to-slash-16-bug-1382190
- Merge into trunk
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 | ||||
Related bugs: |
|
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.
Description of the change
Julian Edwards (julian-edwards) wrote : | # |
Graham Binns (gmb) wrote : | # |
On 30 October 2014 02:35, Julian Edwards <email address hidden> wrote:
>> + if ip_range.size > 256 ** 2:
>> + raise ValidationError
>> +
>
> 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.
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.
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
>> --- src/maasserver/
>> +++ src/maasserver/
>> @@ -46,6 +46,7 @@
>> "ZoneForm",
>> ]
>>
>> +from netaddr import IPRange
>> import collections
>> import json
>> import pipes
>> @@ -1305,6 +1306,36 @@
>> "in that range.")
>>
>>
>> +ERROR_
>> + "Dynamic address range cannot be larger than a /16 (65536 addresses) ")
>> +
>> +
>> +def validate_
>> + """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_
>> # 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.
Julian Edwards (julian-edwards) wrote : | # |
Approved as discussed on the hangout.
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
> >> +
> >
> > 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.
> 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.
MAAS Lander (maas-lander) wrote : | # |
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://
Ign http://
Get:1 http://
Ign http://
Hit http://
Get:2 http://
Get:3 http://
Hit http://
Get:4 http://
Hit http://
Hit http://
Get:5 http://
Hit http://
Hit http://
Hit http://
Hit http://
Get:6 http://
Get:7 http://
Get:8 http://
Hit http://
Hit http://
Ign http://
Ign http://
Get:9 http://
Get:10 http://
Get:11 http://
Get:12 http://
Get:13 http://
Hit http://
Fetched 1,332 kB in 2s (458 kB/s)
Reading package lists...
sudo DEBIAN_
--
Preview Diff
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)) |
Potential problem highlighted below.