Merge lp:~trapnine/maas/fix-1564971 into lp:~maas-committers/maas/trunk

Proposed by Jeffrey C Jones
Status: Merged
Approved by: Jeffrey C Jones
Approved revision: no longer in the source branch.
Merged at revision: 4898
Proposed branch: lp:~trapnine/maas/fix-1564971
Merge into: lp:~maas-committers/maas/trunk
Diff against target: 895 lines (+545/-67)
8 files modified
src/maasserver/api/tests/test_ipranges.py (+5/-5)
src/maasserver/api/tests/test_subnets.py (+32/-8)
src/maasserver/models/iprange.py (+71/-2)
src/maasserver/models/subnet.py (+32/-24)
src/maasserver/models/tests/test_iprange.py (+389/-17)
src/maasserver/models/tests/test_staticipaddress.py (+1/-1)
src/maasserver/testing/factory.py (+4/-1)
src/maasserver/tests/test_dhcp.py (+11/-9)
To merge this branch: bzr merge lp:~trapnine/maas/fix-1564971
Reviewer Review Type Date Requested Status
Mike Pontillo (community) Approve
Review via email: mp+291229@code.launchpad.net

Commit message

When creating a new IPRange, make sure no other conflicting ranges or IP addresses are in use.

Description of the change

When creating a new IPRange, make sure no other conflicting ranges or IP addresses are in use.

To post a comment you must log in.
Revision history for this message
Mike Pontillo (mpontillo) wrote :

This looks good in general to me. A few comments below.

I just have one question: I see that you are doing the validation in a signal handler. Normally we would put it in the model or in the form. Was there any particular reason it was done this way? (Is there already a precedent in MAAS code somewhere? I have to admit I haven't done much with signals.)

Did you do any manual testing with the API for this change?

review: Needs Information
Revision history for this message
Jeffrey C Jones (trapnine) wrote :

> This looks good in general to me. A few comments below.
>
> I just have one question: I see that you are doing the validation in a signal
> handler. Normally we would put it in the model or in the form. Was there any
> particular reason it was done this way? (Is there already a precedent in MAAS
> code somewhere? I have to admit I haven't done much with signals.)
>
> Did you do any manual testing with the API for this change?

I've actually be directed towards signals and away from complex clean() override processing in the past, most notably for the BMC validation work. There are lots of examples in the signals package this new signal handler lives in. This is better than a form clean() because it will be run for any IPRange model save(), not just a form save().

In this case, it was 'almost' necessary because I actually delete the to-be-saved instance, then do some testing, then recreate it. I was not sure I could get away with that in a model's actual save() override (maybe it'd be OK). Also, signals make it easy to store pre-modification state in the post-init signal, then read them in the post-save signal to see what was changed. Basically, after all the other normal form and model checking has been done, this will test for range conflicts.

I did not manually test the API for the change, just the unit tests. This was written as a 'make model save() self-protect against dupes and overlapped ranges' perspective, not a 'fix the API or UI' perspective. Is there a gotcha I'm missing in the API (like maybe it doesn't error to the API cleanly when an Exception is thrown from the model)?

Revision history for this message
Mike Pontillo (mpontillo) wrote :

Sounds good.

Yeah, I asked if you tested the API because I was curious if the errors propagate correctly.

Given your explanation, I'm good with this branch (assuming you address the comments I mentioned).

review: Approve
Revision history for this message
Jeffrey C Jones (trapnine) wrote :

Per sprint discussion, will move login to model clean().

Revision history for this message
Mike Pontillo (mpontillo) wrote :

Looks good; I see some minor issues that I won't block you on, mostly regarding docstrings and comments. (see comments below.) Also, the major function in this branch is a little long and could stand to be refactored to reduce its complexity, IMO.

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'src/maasserver/api/tests/test_ipranges.py'
2--- src/maasserver/api/tests/test_ipranges.py 2016-02-26 18:39:26 +0000
3+++ src/maasserver/api/tests/test_ipranges.py 2016-04-11 09:56:14 +0000
4@@ -40,7 +40,7 @@
5
6 def test_read(self):
7 subnet = factory.make_Subnet(cidr="10.0.0.0/24")
8- factory.make_IPRange(subnet, '10.0.0.1', '10.0.0.10')
9+ factory.make_IPRange(subnet, '10.0.0.2', '10.0.0.10')
10 factory.make_IPRange(subnet, '10.0.0.11', '10.0.0.20')
11 factory.make_IPRange(subnet, '10.0.0.21', '10.0.0.30')
12 uri = get_ipranges_uri()
13@@ -97,14 +97,14 @@
14
15 def test_handler_path(self):
16 subnet = factory.make_Subnet(cidr="10.0.0.0/24")
17- iprange = factory.make_IPRange(subnet, '10.0.0.1', '10.0.0.10')
18+ iprange = factory.make_IPRange(subnet, '10.0.0.2', '10.0.0.10')
19 self.assertEqual(
20 '/api/2.0/ipranges/%s/' % iprange.id,
21 get_iprange_uri(iprange))
22
23 def test_read(self):
24 subnet = factory.make_Subnet(cidr="10.0.0.0/24")
25- iprange = factory.make_IPRange(subnet, '10.0.0.1', '10.0.0.10')
26+ iprange = factory.make_IPRange(subnet, '10.0.0.2', '10.0.0.10')
27 factory.make_IPRange(subnet, '10.0.0.11', '10.0.0.20')
28 factory.make_IPRange(subnet, '10.0.0.21', '10.0.0.30')
29 uri = get_iprange_uri(iprange)
30@@ -131,7 +131,7 @@
31
32 def test_update(self):
33 subnet = factory.make_Subnet(cidr="10.0.0.0/24")
34- iprange = factory.make_IPRange(subnet, '10.0.0.1', '10.0.0.10')
35+ iprange = factory.make_IPRange(subnet, '10.0.0.2', '10.0.0.10')
36 uri = get_iprange_uri(iprange)
37 comment = factory.make_name("comment")
38 response = self.client.put(uri, {
39@@ -147,7 +147,7 @@
40
41 def test_delete_deletes_iprange(self):
42 subnet = factory.make_Subnet(cidr="10.0.0.0/24")
43- iprange = factory.make_IPRange(subnet, '10.0.0.1', '10.0.0.10')
44+ iprange = factory.make_IPRange(subnet, '10.0.0.2', '10.0.0.10')
45 uri = get_iprange_uri(iprange)
46 response = self.client.delete(uri)
47 self.assertEqual(
48
49=== modified file 'src/maasserver/api/tests/test_subnets.py'
50--- src/maasserver/api/tests/test_subnets.py 2016-03-28 13:54:47 +0000
51+++ src/maasserver/api/tests/test_subnets.py 2016-04-11 09:56:14 +0000
52@@ -385,14 +385,11 @@
53 "num_addresses": expected_addresses,
54 }]))
55
56- def test__returns_empty_list_for_full_subnet(self):
57- subnet = factory.make_Subnet()
58- network = subnet.get_ipnetwork()
59- first_address = inet_ntop(network.first + 1)
60- if network.version == 6:
61- last_address = inet_ntop(network.last)
62- else:
63- last_address = inet_ntop(network.last - 1)
64+ def _unreserved_ip_ranges_empty(self, subnet, first_address, last_address):
65+ """ Used by the succeeding three tests to prevent duplicating the
66+ boilerplate that creates the requested range, then makes sure the
67+ unreserved_ip_ranges API call successfully returns an empty list.
68+ """
69 factory.make_IPRange(
70 subnet, first_address, last_address,
71 type=IPRANGE_TYPE.DYNAMIC)
72@@ -406,6 +403,33 @@
73 self.assertThat(
74 result, Equals([]), str(subnet.get_ipranges_in_use()))
75
76+ def test__returns_empty_list_for_full_ipv4_subnet(self):
77+ network = factory.make_ipv4_network()
78+ subnet = factory.make_Subnet(cidr=str(network.cidr), dns_servers=[])
79+ network = subnet.get_ipnetwork()
80+ first_address = inet_ntop(network.first + 2) # Skip gateway, network.
81+ last_address = inet_ntop(network.last - 1) # Skip broadcast.
82+ self._unreserved_ip_ranges_empty(subnet, first_address, last_address)
83+
84+ def test__returns_empty_list_for_full_ipv6_subnet(self):
85+ network = factory.make_ipv6_network()
86+ subnet = factory.make_Subnet(cidr=str(network.cidr), dns_servers=[])
87+ network = subnet.get_ipnetwork()
88+ first_address = inet_ntop(network.first + 2) # Skip gateway, network.
89+ last_address = inet_ntop(network.last)
90+ self._unreserved_ip_ranges_empty(subnet, first_address, last_address)
91+
92+ # Slash-64 ipv6 subnets get a special range put in them - test separately.
93+ def test__returns_empty_list_for_full_ipv6_slash_64_subnet(self):
94+ network = factory.make_ipv6_network(slash=64)
95+ subnet = factory.make_Subnet(cidr=str(network.cidr), dns_servers=[])
96+ network = subnet.get_ipnetwork()
97+ strip64 = (network.first >> 8) << 8
98+ # Skip the automatically reserved range on /64's.
99+ first_address = inet_ntop(strip64 + 0x100000000)
100+ last_address = inet_ntop(network.last)
101+ self._unreserved_ip_ranges_empty(subnet, first_address, last_address)
102+
103 def test__accounts_for_reserved_ip_address(self):
104 subnet = factory.make_ipv4_Subnet_with_IPRanges(
105 with_dynamic_range=False, dns_servers=[], with_router=False)
106
107=== modified file 'src/maasserver/models/iprange.py'
108--- src/maasserver/models/iprange.py 2016-03-28 13:54:47 +0000
109+++ src/maasserver/models/iprange.py 2016-04-11 09:56:14 +0000
110@@ -19,11 +19,17 @@
111 PROTECT,
112 QuerySet,
113 )
114-from maasserver.enum import IPRANGE_TYPE_CHOICES
115+from maasserver.enum import (
116+ IPRANGE_TYPE,
117+ IPRANGE_TYPE_CHOICES,
118+)
119 from maasserver.fields import MAASIPAddressField
120 from maasserver.models.cleansave import CleanSave
121 from maasserver.models.timestampedmodel import TimestampedModel
122-from maasserver.utils.orm import MAASQueriesMixin
123+from maasserver.utils.orm import (
124+ MAASQueriesMixin,
125+ transactional,
126+)
127 import netaddr
128 from netaddr import (
129 AddrFormatError,
130@@ -155,6 +161,7 @@
131 if end_ip not in cidr:
132 raise ValidationError(
133 "End IP address must be within subnet: %s." % cidr)
134+ self.clean_prevent_dupes_and_overlaps()
135
136 @property
137 def netaddr_iprange(self):
138@@ -166,3 +173,65 @@
139 # APIs in previous MAAS releases used '-' in range types.
140 purpose = purpose.replace('_', '-')
141 return make_iprange(self.start_ip, self.end_ip, purpose=purpose)
142+
143+ @transactional
144+ def clean_prevent_dupes_and_overlaps(self):
145+
146+ # A range overlap/conflict could be due to any of these fields.
147+ def fail(message, fields=['start_ip', 'end_ip', 'type']):
148+ for field in fields:
149+ validation_errors[field] = [message]
150+ raise ValidationError(validation_errors)
151+
152+ """Make sure the new or updated range isn't going to cause a conflict.
153+ If it will, raise ValidationError.
154+ """
155+ # If model is incomplete, save() will fail, so don't bother checking.
156+ if self.subnet_id is None or self.start_ip is None or (
157+ self.end_ip is None) or self.type is None:
158+ return
159+
160+ # The _state.adding flag is False if this instance exists in the DB.
161+ # See https://docs.djangoproject.com/en/1.9/ref/models/instances/.
162+ if not self._state.adding:
163+ orig = IPRange.objects.get(pk=self.pk)
164+ if orig.type == self.type and (
165+ orig.start_ip == self.start_ip) and (
166+ orig.end_ip == self.end_ip):
167+ # Range not materially modified, no range dupe check required.
168+ return
169+ # Pre-existing range moved: remove existing, check, then re-create.
170+ if IPRange.objects.filter(pk=self.id).exists():
171+ self_id = self.id
172+ # Delete will be rolled back if imminent range checks raise.
173+ self.delete()
174+ # Simulate update by setting the ID back to what it was.
175+ self.id = self_id
176+
177+ # Reserved ranges can overlap allocated IPs but not other ranges.
178+ # Dynamic ranges cannot overlap anything (no ranges or IPs).
179+ if self.type == IPRANGE_TYPE.RESERVED:
180+ unused = self.subnet.get_ipranges_available_for_reserved_range()
181+ else:
182+ unused = self.subnet.get_ipranges_available_for_dynamic_range()
183+
184+ validation_errors = {}
185+ if len(unused) == 0:
186+ fail("There is no room for any %s ranges on this subnet." % (
187+ self.type))
188+
189+ # Find unused range for start_ip
190+ for range in unused:
191+ if IPAddress(self.start_ip) in range:
192+ if IPAddress(self.end_ip) in range:
193+ # Success, start and end IP are in an unused range.
194+ return
195+ else:
196+ message = ("Requested %s range conflicts with "
197+ "an existing ") % (self.type)
198+ if self.type == IPRANGE_TYPE.RESERVED:
199+ fail(message + "range.")
200+ else:
201+ fail(message + "IP address or range.")
202+ fail("No %s range can be created at requested start IP." % self.type,
203+ ['start_ip', 'type'])
204
205=== modified file 'src/maasserver/models/subnet.py'
206--- src/maasserver/models/subnet.py 2016-04-08 12:03:57 +0000
207+++ src/maasserver/models/subnet.py 2016-04-11 09:56:14 +0000
208@@ -8,7 +8,6 @@
209 'Subnet',
210 ]
211
212-
213 from django.contrib.postgres.fields import ArrayField
214 from django.core.exceptions import (
215 PermissionDenied,
216@@ -407,7 +406,7 @@
217 for ip in self.staticipaddress_set.all()
218 if ip.ip)
219
220- def get_ipranges_in_use(self, exclude_addresses=[]):
221+ def get_ipranges_in_use(self, exclude_addresses=[], ranges_only=False):
222 """Returns a `MAASIPSet` of `MAASIPRange` objects which are currently
223 in use on this `Subnet`.
224
225@@ -429,37 +428,46 @@
226 second = str(IPAddress(network.first + 0xFFFFFFFF))
227 ranges |= {make_iprange(first, second, purpose="reserved")}
228 ipnetwork = self.get_ipnetwork()
229- assigned_ip_addresses = self.get_staticipaddresses_in_use()
230- if (self.gateway_ip is not None and self.gateway_ip != '' and
231- self.gateway_ip in ipnetwork):
232- ranges |= {make_iprange(self.gateway_ip, purpose="gateway-ip")}
233- if self.dns_servers is not None:
234- ranges |= set(
235- make_iprange(server, purpose="dns-server")
236- for server in self.dns_servers
237- if server in ipnetwork
238- )
239- ranges |= set(
240- make_iprange(ip, purpose="assigned-ip")
241- for ip in assigned_ip_addresses
242- if ip in ipnetwork
243- )
244- ranges |= set(
245- make_iprange(address, purpose="excluded")
246- for address in exclude_addresses
247- if address in network
248- )
249+ if not ranges_only:
250+ assigned_ip_addresses = self.get_staticipaddresses_in_use()
251+ if (self.gateway_ip is not None and self.gateway_ip != '' and
252+ self.gateway_ip in ipnetwork):
253+ ranges |= {make_iprange(self.gateway_ip, purpose="gateway-ip")}
254+ if self.dns_servers is not None:
255+ ranges |= set(
256+ make_iprange(server, purpose="dns-server")
257+ for server in self.dns_servers
258+ if server in ipnetwork
259+ )
260+ ranges |= set(
261+ make_iprange(ip, purpose="assigned-ip")
262+ for ip in assigned_ip_addresses
263+ if ip in ipnetwork
264+ )
265+ ranges |= set(
266+ make_iprange(address, purpose="excluded")
267+ for address in exclude_addresses
268+ if address in network
269+ )
270 ranges |= self.get_reserved_maasipset()
271 ranges |= self.get_dynamic_maasipset()
272 return MAASIPSet(ranges)
273
274- def get_ipranges_not_in_use(self, exclude_addresses=[]):
275+ def get_ipranges_available_for_reserved_range(self):
276+ return self.get_ipranges_not_in_use(ranges_only=True)
277+
278+ def get_ipranges_available_for_dynamic_range(self):
279+ return self.get_ipranges_not_in_use()
280+
281+ def get_ipranges_not_in_use(self, exclude_addresses=[], ranges_only=False):
282 """Returns a `MAASIPSet` of ranges which are currently free on this
283 `Subnet`.
284
285 :param exclude_addresses: An iterable of addresses not to use.
286 """
287- ranges = self.get_ipranges_in_use(exclude_addresses=exclude_addresses)
288+ ranges = self.get_ipranges_in_use(
289+ exclude_addresses=exclude_addresses,
290+ ranges_only=ranges_only)
291 unused = ranges.get_unused_ranges(self.get_ipnetwork())
292 return unused
293
294
295=== modified file 'src/maasserver/models/tests/test_iprange.py'
296--- src/maasserver/models/tests/test_iprange.py 2016-03-28 13:54:47 +0000
297+++ src/maasserver/models/tests/test_iprange.py 2016-04-11 09:56:14 +0000
298@@ -6,26 +6,37 @@
299 __all__ = []
300
301 from django.core.exceptions import ValidationError
302-from maasserver.enum import IPRANGE_TYPE
303+from maasserver.enum import (
304+ IPADDRESS_TYPE,
305+ IPRANGE_TYPE,
306+)
307 from maasserver.models import IPRange
308 from maasserver.testing.factory import factory
309 from maasserver.testing.testcase import MAASServerTestCase
310+from maasserver.utils.orm import reload_object
311 from testtools import ExpectedException
312
313
314+def make_plain_subnet():
315+ return factory.make_Subnet(
316+ cidr='192.168.0.0/24',
317+ gateway_ip='192.168.0.1',
318+ dns_servers=[])
319+
320+
321 class IPRangeTest(MAASServerTestCase):
322
323 def test__create(self):
324- subnet = factory.make_Subnet(cidr='192.168.0.0/24')
325+ subnet = make_plain_subnet()
326 iprange = IPRange(
327- start_ip='192.168.0.1', end_ip='192.168.0.254',
328+ start_ip='192.168.0.2', end_ip='192.168.0.254',
329 type=IPRANGE_TYPE.RESERVED, user=factory.make_User(),
330 comment="The quick brown fox jumps over the lazy dog.",
331 subnet=subnet)
332 iprange.save()
333
334 def test__requires_valid_ip_addresses(self):
335- subnet = factory.make_Subnet(cidr='192.168.0.0/24')
336+ subnet = make_plain_subnet()
337 iprange = IPRange(
338 start_ip='x192.x168.x0.x1', end_ip='y192.y168.y0.y254',
339 type=IPRANGE_TYPE.RESERVED, user=factory.make_User(),
340@@ -35,7 +46,7 @@
341 iprange.save()
342
343 def test__requires_start_ip_address(self):
344- subnet = factory.make_Subnet(cidr='192.168.0.0/24')
345+ subnet = make_plain_subnet()
346 iprange = IPRange(
347 start_ip='192.168.0.1', type=IPRANGE_TYPE.RESERVED,
348 user=factory.make_User(), subnet=subnet,
349@@ -44,7 +55,7 @@
350 iprange.save()
351
352 def test__requires_end_ip_address(self):
353- subnet = factory.make_Subnet(cidr='192.168.0.0/24')
354+ subnet = make_plain_subnet()
355 iprange = IPRange(
356 end_ip='192.168.0.1', type=IPRANGE_TYPE.RESERVED,
357 user=factory.make_User(), subnet=subnet,
358@@ -53,7 +64,7 @@
359 iprange.save()
360
361 def test__requires_matching_address_family(self):
362- subnet = factory.make_Subnet(cidr='192.168.0.0/24')
363+ subnet = make_plain_subnet()
364 iprange = IPRange(
365 start_ip='192.168.0.1', end_ip='2001:db8::1',
366 type=IPRANGE_TYPE.RESERVED,
367@@ -71,7 +82,7 @@
368 iprange.save()
369
370 def test__requires_start_ip_and_end_ip(self):
371- subnet = factory.make_Subnet(cidr='192.168.0.0/24')
372+ subnet = make_plain_subnet()
373 iprange = IPRange(
374 subnet=subnet, type=IPRANGE_TYPE.RESERVED,
375 user=factory.make_User(),
376@@ -80,7 +91,7 @@
377 iprange.save()
378
379 def test__requires_start_ip_and_end_ip_to_be_within_subnet(self):
380- subnet = factory.make_Subnet(cidr='192.168.0.0/24')
381+ subnet = make_plain_subnet()
382 iprange = IPRange(
383 start_ip='192.168.1.1', end_ip='192.168.1.254', subnet=subnet,
384 type=IPRANGE_TYPE.RESERVED, user=factory.make_User(),
385@@ -90,7 +101,7 @@
386 iprange.save()
387
388 def test__requires_start_ip_to_be_within_subnet(self):
389- subnet = factory.make_Subnet(cidr='192.168.0.0/24')
390+ subnet = make_plain_subnet()
391 iprange = IPRange(
392 start_ip='19.168.0.1', end_ip='192.168.0.254', subnet=subnet,
393 type=IPRANGE_TYPE.DYNAMIC, user=factory.make_User(),
394@@ -100,7 +111,7 @@
395 iprange.save()
396
397 def test__requires_end_ip_to_be_within_subnet(self):
398- subnet = factory.make_Subnet(cidr='192.168.0.0/24')
399+ subnet = make_plain_subnet()
400 iprange = IPRange(
401 start_ip='192.168.0.1', end_ip='193.168.0.254',
402 subnet=subnet, type=IPRANGE_TYPE.DYNAMIC,
403@@ -111,7 +122,7 @@
404 iprange.save()
405
406 def test__requires_end_ip_to_be_greater_or_equal_to_start_ip(self):
407- subnet = factory.make_Subnet(cidr='192.168.0.0/24')
408+ subnet = make_plain_subnet()
409 iprange = IPRange(
410 start_ip='192.168.0.2', end_ip='192.168.0.1',
411 user=factory.make_User(), subnet=subnet,
412@@ -122,7 +133,7 @@
413 iprange.save()
414
415 def test__requires_type(self):
416- subnet = factory.make_Subnet(cidr='192.168.0.0/24')
417+ subnet = make_plain_subnet()
418 iprange = IPRange(
419 start_ip='192.168.0.1', end_ip='192.168.0.254',
420 user=factory.make_User(), subnet=subnet,
421@@ -131,16 +142,377 @@
422 iprange.save()
423
424 def test__user_optional(self):
425- subnet = factory.make_Subnet(cidr='192.168.0.0/24')
426+ subnet = make_plain_subnet()
427 iprange = IPRange(
428- start_ip='192.168.0.1', end_ip='192.168.0.254',
429+ start_ip='192.168.0.2', end_ip='192.168.0.254',
430 type=IPRANGE_TYPE.DYNAMIC, subnet=subnet,
431 comment="The quick brown owl jumps over the lazy alligator.")
432 iprange.save()
433
434 def test__comment_optional(self):
435- subnet = factory.make_Subnet(cidr='192.168.0.0/24')
436+ subnet = make_plain_subnet()
437 iprange = IPRange(
438- start_ip='192.168.0.1', end_ip='192.168.0.254', subnet=subnet,
439+ start_ip='192.168.0.2', end_ip='192.168.0.254', subnet=subnet,
440 type=IPRANGE_TYPE.RESERVED, user=factory.make_User())
441 iprange.save()
442+
443+
444+class TestIPRangeSavePreventsOverlapping(MAASServerTestCase):
445+
446+ no_fit = ".*No %s range can be created at requested start IP."
447+ dynamic_no_fit = no_fit % IPRANGE_TYPE.DYNAMIC
448+ reserved_no_fit = no_fit % IPRANGE_TYPE.RESERVED
449+
450+ overlaps = ".*Requested %s range conflicts with an existing %srange.*"
451+ dynamic_overlaps = overlaps % (IPRANGE_TYPE.DYNAMIC, "IP address or ")
452+ reserved_overlaps = overlaps % (IPRANGE_TYPE.RESERVED, "")
453+
454+ no_room = ".*There is no room for any %s ranges on this subnet.*"
455+ dynamic_no_room = no_room % IPRANGE_TYPE.DYNAMIC
456+ reserved_no_room = no_room % IPRANGE_TYPE.RESERVED
457+
458+ def test__no_save_duplicate_ipranges(self):
459+ subnet = make_plain_subnet()
460+ IPRange(
461+ subnet=subnet,
462+ type=IPRANGE_TYPE.DYNAMIC,
463+ start_ip="192.168.0.100",
464+ end_ip="192.168.0.150",
465+ ).save()
466+ # Make the same range again, should fail to save.
467+ iprange = IPRange(
468+ subnet=subnet,
469+ type=IPRANGE_TYPE.DYNAMIC,
470+ start_ip="192.168.0.100",
471+ end_ip="192.168.0.150",
472+ )
473+ with ExpectedException(ValidationError, self.dynamic_no_fit):
474+ iprange.save()
475+
476+ def test__no_save_range_overlap_begin(self):
477+ subnet = make_plain_subnet()
478+ IPRange(
479+ subnet=subnet,
480+ type=IPRANGE_TYPE.DYNAMIC,
481+ start_ip="192.168.0.100",
482+ end_ip="192.168.0.150",
483+ ).save()
484+ # Make an overlapping range across start_ip, should fail to save.
485+ iprange = IPRange(
486+ subnet=subnet,
487+ type=IPRANGE_TYPE.DYNAMIC,
488+ start_ip="192.168.0.90",
489+ end_ip="192.168.0.100",
490+ )
491+ with ExpectedException(ValidationError, self.dynamic_overlaps):
492+ iprange.save()
493+ # Try as reserved range.
494+ iprange.type = IPRANGE_TYPE.RESERVED
495+ with ExpectedException(ValidationError, self.reserved_overlaps):
496+ iprange.save()
497+
498+ def test__no_save_range_overlap_end(self):
499+ subnet = make_plain_subnet()
500+ IPRange(
501+ subnet=subnet,
502+ type=IPRANGE_TYPE.DYNAMIC,
503+ start_ip="192.168.0.100",
504+ end_ip="192.168.0.150",
505+ ).save()
506+ # Make an overlapping range across end_ip, should fail to save.
507+ iprange = IPRange(
508+ subnet=subnet,
509+ type=IPRANGE_TYPE.DYNAMIC,
510+ start_ip="192.168.0.140",
511+ end_ip="192.168.0.160",
512+ )
513+ with ExpectedException(ValidationError, self.dynamic_no_fit):
514+ iprange.save()
515+
516+ def test__no_save_range_within_ranges(self):
517+ subnet = make_plain_subnet()
518+ IPRange(
519+ subnet=subnet,
520+ type=IPRANGE_TYPE.DYNAMIC,
521+ start_ip="192.168.0.100",
522+ end_ip="192.168.0.150",
523+ ).save()
524+ # Make a contained range, should not save.
525+ iprange = IPRange(
526+ subnet=subnet,
527+ type=IPRANGE_TYPE.DYNAMIC,
528+ start_ip="192.168.0.110",
529+ end_ip="192.168.0.140",
530+ )
531+ with ExpectedException(ValidationError, self.dynamic_no_fit):
532+ iprange.save()
533+
534+ def test__no_save_range_spanning_existing_range(self):
535+ subnet = make_plain_subnet()
536+ IPRange(
537+ subnet=subnet,
538+ type=IPRANGE_TYPE.DYNAMIC,
539+ start_ip="192.168.0.100",
540+ end_ip="192.168.0.150",
541+ ).save()
542+ # Make a contained range, should not save.
543+ iprange = IPRange(
544+ subnet=subnet,
545+ type=IPRANGE_TYPE.DYNAMIC,
546+ start_ip="192.168.0.10",
547+ end_ip="192.168.0.240",
548+ )
549+ with ExpectedException(ValidationError, self.dynamic_overlaps):
550+ iprange.save()
551+
552+ def test__no_save_range_within_existing_range(self):
553+ subnet = make_plain_subnet()
554+ IPRange(
555+ subnet=subnet,
556+ type=IPRANGE_TYPE.DYNAMIC,
557+ start_ip="192.168.0.100",
558+ end_ip="192.168.0.150",
559+ ).save()
560+ # Make a contained range, should not save.
561+ iprange = IPRange(
562+ subnet=subnet,
563+ type=IPRANGE_TYPE.DYNAMIC,
564+ start_ip="192.168.0.110",
565+ end_ip="192.168.0.140",
566+ )
567+ with ExpectedException(ValidationError, self.dynamic_no_fit):
568+ iprange.save()
569+
570+ def test__no_save_range_within_existing_reserved_range(self):
571+ subnet = make_plain_subnet()
572+ IPRange(
573+ subnet=subnet,
574+ type=IPRANGE_TYPE.RESERVED,
575+ start_ip="192.168.0.100",
576+ end_ip="192.168.0.150",
577+ ).save()
578+ # Make a contained range, should not save.
579+ iprange = IPRange(
580+ subnet=subnet,
581+ type=IPRANGE_TYPE.DYNAMIC,
582+ start_ip="192.168.0.110",
583+ end_ip="192.168.0.140",
584+ )
585+ with ExpectedException(ValidationError, self.dynamic_no_fit):
586+ iprange.save()
587+
588+ def test__no_save_when_no_ranges_available(self):
589+ subnet = make_plain_subnet()
590+ # Reserve the whole subnet, except gateway.
591+ IPRange(
592+ subnet=subnet,
593+ type=IPRANGE_TYPE.RESERVED,
594+ start_ip="192.168.0.2",
595+ end_ip="192.168.0.254",
596+ ).save()
597+ # Try to make dynamic range at gateway (anywhere, actually) = no room!
598+ iprange = IPRange(
599+ subnet=subnet,
600+ type=IPRANGE_TYPE.DYNAMIC,
601+ start_ip="192.168.0.1",
602+ end_ip="192.168.0.1",
603+ )
604+ with ExpectedException(ValidationError, self.dynamic_no_room):
605+ iprange.save()
606+ # We CAN reserve the gateway addr.
607+ IPRange(
608+ subnet=subnet,
609+ type=IPRANGE_TYPE.RESERVED,
610+ start_ip="192.168.0.1",
611+ end_ip="192.168.0.1",
612+ ).save()
613+ # But now it's full - trying to save any reserved = no room!
614+ iprange = IPRange(
615+ subnet=subnet,
616+ type=IPRANGE_TYPE.RESERVED,
617+ start_ip="192.168.0.25",
618+ end_ip="192.168.0.35",
619+ )
620+ with ExpectedException(ValidationError, self.reserved_no_room):
621+ iprange.save()
622+
623+ def test__modify_existing_performs_validation(self):
624+ subnet = make_plain_subnet()
625+ IPRange(
626+ subnet=subnet,
627+ type=IPRANGE_TYPE.DYNAMIC,
628+ start_ip="192.168.0.100",
629+ end_ip="192.168.0.150",
630+ ).save()
631+ iprange = IPRange(
632+ subnet=subnet,
633+ type=IPRANGE_TYPE.DYNAMIC,
634+ start_ip="192.168.0.151",
635+ end_ip="192.168.0.200",
636+ )
637+ iprange.save()
638+ # Make sure safe modification works.
639+ iprange.start_ip = "192.168.0.210"
640+ iprange.end_ip = "192.168.0.250"
641+ iprange.save()
642+ # Modify again, but conflict with first range this time.
643+ instance_id = iprange.id
644+ iprange.start_ip = "192.168.0.110"
645+ iprange.end_ip = "192.168.0.140"
646+ with ExpectedException(ValidationError, self.dynamic_no_fit):
647+ iprange.save()
648+ # Make sure original range isn't deleted after failure to modify.
649+ iprange = reload_object(iprange)
650+ self.assertEqual(iprange.id, instance_id)
651+
652+ def test__dynamic_range_cant_overlap_gateway_ip(self):
653+ subnet = make_plain_subnet()
654+ iprange = IPRange(
655+ subnet=subnet,
656+ type=IPRANGE_TYPE.DYNAMIC,
657+ start_ip="192.168.0.2",
658+ end_ip="192.168.0.5",
659+ )
660+ iprange.save()
661+ # A DYNAMIC range cannot overlap the gateway IP.
662+ iprange.start_ip = "192.168.0.1"
663+ with ExpectedException(ValidationError, self.dynamic_no_fit):
664+ iprange.save()
665+
666+ def test__reserved_range_can_overlap_gateway_ip(self):
667+ subnet = make_plain_subnet()
668+ iprange = IPRange(
669+ subnet=subnet,
670+ type=IPRANGE_TYPE.RESERVED,
671+ start_ip="192.168.0.2",
672+ end_ip="192.168.0.5",
673+ )
674+ iprange.save()
675+ # A RESERVED range can overlap the gateway IP.
676+ iprange.start_ip = "192.168.0.1"
677+ iprange.save()
678+
679+ def test__reserved_range_cannot_overlap_dynamic_ranges(self):
680+ subnet = factory.make_Subnet(
681+ cidr='192.168.0.0/24',
682+ gateway_ip='192.168.0.1',
683+ dns_servers=['192.168.0.50', '192.168.0.200'])
684+ IPRange(
685+ subnet=subnet,
686+ type=IPRANGE_TYPE.DYNAMIC,
687+ start_ip="192.168.0.2",
688+ end_ip="192.168.0.49",
689+ ).save()
690+ iprange = IPRange(
691+ subnet=subnet,
692+ type=IPRANGE_TYPE.RESERVED,
693+ start_ip="192.168.0.25",
694+ end_ip="192.168.0.30",
695+ )
696+ with ExpectedException(ValidationError, self.reserved_no_fit):
697+ iprange.save()
698+
699+ def test__reserved_range_cannot_overlap_reserved_ranges(self):
700+ subnet = factory.make_Subnet(
701+ cidr='192.168.0.0/24',
702+ gateway_ip='192.168.0.1',
703+ dns_servers=['192.168.0.50', '192.168.0.200'])
704+ IPRange(
705+ subnet=subnet,
706+ type=IPRANGE_TYPE.RESERVED,
707+ start_ip="192.168.0.1",
708+ end_ip="192.168.0.250",
709+ ).save()
710+ iprange = IPRange(
711+ subnet=subnet,
712+ type=IPRANGE_TYPE.RESERVED,
713+ start_ip="192.168.0.250",
714+ end_ip="192.168.0.254",
715+ )
716+ with ExpectedException(ValidationError, self.reserved_no_fit):
717+ iprange.save()
718+
719+ def test__dynamic_range_cannot_overlap_static_ip(self):
720+ subnet = make_plain_subnet()
721+ factory.make_StaticIPAddress(
722+ alloc_type=IPADDRESS_TYPE.USER_RESERVED, subnet=subnet)
723+ iprange = IPRange(
724+ subnet=subnet,
725+ type=IPRANGE_TYPE.DYNAMIC,
726+ start_ip="192.168.0.2",
727+ end_ip="192.168.0.254",
728+ )
729+ with ExpectedException(ValidationError, self.dynamic_overlaps):
730+ iprange.save()
731+
732+ def test__reserved_range_can_overlap_static_ip(self):
733+ subnet = make_plain_subnet()
734+ factory.make_StaticIPAddress(
735+ alloc_type=IPADDRESS_TYPE.USER_RESERVED, subnet=subnet)
736+ iprange = IPRange(
737+ subnet=subnet,
738+ type=IPRANGE_TYPE.RESERVED,
739+ start_ip="192.168.0.1",
740+ end_ip="192.168.0.254",
741+ )
742+ iprange.save()
743+
744+ def test__dynamic_range_cannot_overlap_dns_servers(self):
745+ subnet = factory.make_Subnet(
746+ cidr='192.168.0.0/24',
747+ gateway_ip='192.168.0.1',
748+ dns_servers=['192.168.0.50', '192.168.0.200'])
749+ iprange = IPRange(
750+ subnet=subnet,
751+ type=IPRANGE_TYPE.DYNAMIC,
752+ start_ip="192.168.0.1",
753+ end_ip="192.168.0.254",
754+ )
755+ with ExpectedException(ValidationError, self.dynamic_no_fit):
756+ iprange.save()
757+
758+ def test__reserved_range_can_overlap_dns_servers(self):
759+ subnet = factory.make_Subnet(
760+ cidr='192.168.0.0/24',
761+ gateway_ip='192.168.0.1',
762+ dns_servers=['192.168.0.50', '192.168.0.200'])
763+ iprange = IPRange(
764+ subnet=subnet,
765+ type=IPRANGE_TYPE.RESERVED,
766+ start_ip="192.168.0.1",
767+ end_ip="192.168.0.254",
768+ )
769+ iprange.save()
770+
771+ def test__change_reserved_to_dynamic(self):
772+ subnet = make_plain_subnet()
773+ iprange = IPRange(
774+ subnet=subnet,
775+ type=IPRANGE_TYPE.RESERVED,
776+ start_ip="192.168.0.1",
777+ end_ip="192.168.0.5",
778+ )
779+ # Reserved should save OK overlapping gateway IP.
780+ iprange.save()
781+
782+ # Dynamic should not save overlapping gateway IP.
783+ iprange.type = IPRANGE_TYPE.DYNAMIC
784+ with ExpectedException(ValidationError, self.dynamic_no_fit):
785+ iprange.save()
786+ # Fix start_ip and now it should save.
787+ iprange.start_ip = "192.168.0.2"
788+ iprange.save()
789+
790+ def test__change_dynamic_to_reserved(self):
791+ subnet = make_plain_subnet()
792+ iprange = IPRange(
793+ subnet=subnet,
794+ type=IPRANGE_TYPE.DYNAMIC,
795+ start_ip="192.168.0.2",
796+ end_ip="192.168.0.5",
797+ )
798+ iprange.save()
799+ # Reserved should save OK overlapping gateway IP.
800+ iprange.type = IPRANGE_TYPE.RESERVED
801+ iprange.start_ip = "192.168.0.1"
802+ iprange.save()
803
804=== modified file 'src/maasserver/models/tests/test_staticipaddress.py'
805--- src/maasserver/models/tests/test_staticipaddress.py 2016-03-28 13:54:47 +0000
806+++ src/maasserver/models/tests/test_staticipaddress.py 2016-04-11 09:56:14 +0000
807@@ -322,7 +322,7 @@
808 with transaction.atomic():
809 subnet = factory.make_Subnet(cidr=network)
810 factory.make_IPRange(
811- subnet, '192.168.230.0', '192.168.230.255',
812+ subnet, '192.168.230.1', '192.168.230.254',
813 type=IPRANGE_TYPE.RESERVED)
814 with transaction.atomic():
815 e = self.assertRaises(
816
817=== modified file 'src/maasserver/testing/factory.py'
818--- src/maasserver/testing/factory.py 2016-04-07 18:58:38 +0000
819+++ src/maasserver/testing/factory.py 2016-04-11 09:56:14 +0000
820@@ -118,6 +118,7 @@
821 IPNetwork,
822 )
823 from provisioningserver.utils.enum import map_enum
824+from provisioningserver.utils.network import inet_ntop
825
826 # We have a limited number of public keys:
827 # src/maasserver/tests/data/test_rsa{0, 1, 2, 3, 4}.pub
828@@ -750,11 +751,13 @@
829 vlan = factory.make_VLAN(fabric=fabric, vid=vid, dhcp_on=dhcp_on)
830 if space is None:
831 space = factory.make_Space()
832+ network = None
833 if cidr is None:
834 network = factory.make_ip4_or_6_network(host_bits=host_bits)
835 cidr = str(network.cidr)
836 if gateway_ip is None:
837- gateway_ip = factory.pick_ip_in_network(IPNetwork(cidr))
838+ network = IPNetwork(cidr) if network is None else network
839+ gateway_ip = inet_ntop(network.first + 1)
840 if dns_servers is None:
841 dns_servers = [
842 self.make_ip_address() for _ in range(random.randint(1, 3))]
843
844=== modified file 'src/maasserver/tests/test_dhcp.py'
845--- src/maasserver/tests/test_dhcp.py 2016-04-01 21:14:24 +0000
846+++ src/maasserver/tests/test_dhcp.py 2016-04-11 09:56:14 +0000
847@@ -1220,8 +1220,8 @@
848 ha_subnet = factory.make_Subnet(
849 vlan=ha_vlan, cidr="fd38:c341:27da:c831::/64")
850 factory.make_IPRange(
851- ha_subnet, "fd38:c341:27da:c831::0001:0000",
852- "fd38:c341:27da:c831::FFFF:0000")
853+ ha_subnet, "fd38:c341:27da:c831:0:1::",
854+ "fd38:c341:27da:c831:0:1:ffff:0")
855 factory.make_Interface(
856 INTERFACE_TYPE.PHYSICAL, node=primary_rack, vlan=ha_vlan)
857 secondary_interface = factory.make_Interface(
858@@ -1251,8 +1251,8 @@
859 vlan=ha_vlan, cidr="fd38:c341:27da:c831::/64")
860 ha_network = ha_subnet.get_ipnetwork()
861 factory.make_IPRange(
862- ha_subnet, "fd38:c341:27da:c831::0001:0000",
863- "fd38:c341:27da:c831::FFFF:0000")
864+ ha_subnet, "fd38:c341:27da:c831:0:1::",
865+ "fd38:c341:27da:c831:0:1:ffff:0")
866 ha_dhcp_snippets = [
867 factory.make_DHCPSnippet(subnet=ha_subnet, enabled=True)
868 for _ in range(3)]
869@@ -1375,10 +1375,12 @@
870 subnet_v4 = factory.make_ipv4_Subnet_with_IPRanges(
871 vlan=vlan, unmanaged=(not dhcp_on))
872 subnet_v6 = factory.make_Subnet(
873- vlan=vlan, cidr="fd38:c341:27da:c831::/64")
874+ vlan=vlan, cidr="fd38:c341:27da:c831::/64",
875+ gateway_ip="fd38:c341:27da:c831::1",
876+ dns_servers=[])
877 factory.make_IPRange(
878- subnet_v6, "fd38:c341:27da:c831::0001:0000",
879- "fd38:c341:27da:c831::FFFF:0000")
880+ subnet_v6, "fd38:c341:27da:c831:0:1::",
881+ "fd38:c341:27da:c831:0:1:ffff:0")
882
883 factory.make_StaticIPAddress(
884 alloc_type=IPADDRESS_TYPE.AUTO, subnet=subnet_v4,
885@@ -1596,8 +1598,8 @@
886 subnet_v6 = factory.make_Subnet(
887 vlan=vlan, cidr="fd38:c341:27da:c831::/64")
888 factory.make_IPRange(
889- subnet_v6, "fd38:c341:27da:c831::0001:0000",
890- "fd38:c341:27da:c831::FFFF:0000")
891+ subnet_v6, "fd38:c341:27da:c831:0:1::",
892+ "fd38:c341:27da:c831:0:1:ffff:0")
893
894 factory.make_StaticIPAddress(
895 alloc_type=IPADDRESS_TYPE.AUTO, subnet=subnet_v4,