Merge lp:~trapnine/maas/fix-1564971 into lp:~maas-committers/maas/trunk
- fix-1564971
- Merge into trunk
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 |
Related bugs: |
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.
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)?
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).
Jeffrey C Jones (trapnine) wrote : | # |
Per sprint discussion, will move login to model clean().
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.
Preview Diff
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, |
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?