Merge lp:~mpontillo/maas/unmanaged-subnets into lp:~maas-committers/maas/trunk
- unmanaged-subnets
- Merge into trunk
Proposed by
Mike Pontillo
Status: | Rejected |
---|---|
Rejected by: | Mike Pontillo |
Proposed branch: | lp:~mpontillo/maas/unmanaged-subnets |
Merge into: | lp:~maas-committers/maas/trunk |
Diff against target: |
634 lines (+308/-52) 7 files modified
src/maasserver/migrations/builtin/maasserver/0027_replace_static_range_with_admin_reserved_ranges.py (+1/-1) src/maasserver/migrations/builtin/maasserver/0094_add_unmanaged_subnets.py (+19/-0) src/maasserver/models/iprange.py (+3/-2) src/maasserver/models/subnet.py (+70/-31) src/maasserver/models/tests/test_subnet.py (+56/-8) src/maasserver/testing/factory.py (+3/-2) src/provisioningserver/utils/network.py (+156/-8) |
To merge this branch: | bzr merge lp:~mpontillo/maas/unmanaged-subnets |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
MAAS Maintainers | Pending | ||
Review via email: mp+312076@code.launchpad.net |
Commit message
WIP
Description of the change
To post a comment you must log in.
Revision history for this message
MAAS Lander (maas-lander) wrote : | # |
Unmerged revisions
- 5573. By Mike Pontillo
-
Unmanaged subnets WIP.
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | === modified file 'src/maasserver/migrations/builtin/maasserver/0027_replace_static_range_with_admin_reserved_ranges.py' |
2 | --- src/maasserver/migrations/builtin/maasserver/0027_replace_static_range_with_admin_reserved_ranges.py 2016-05-11 19:01:48 +0000 |
3 | +++ src/maasserver/migrations/builtin/maasserver/0027_replace_static_range_with_admin_reserved_ranges.py 2016-11-29 18:04:20 +0000 |
4 | @@ -37,7 +37,7 @@ |
5 | IPRange, subnet, ranges, created_time, range_description): |
6 | unreserved_range_set = MAASIPSet(ranges) |
7 | unreserved_ranges = unreserved_range_set.get_unused_ranges( |
8 | - subnet.cidr, comment="reserved") |
9 | + subnet.cidr, purpose="reserved") |
10 | for iprange in unreserved_ranges: |
11 | start_ip = str(IPAddress(iprange.first)) |
12 | end_ip = str(IPAddress(iprange.last)) |
13 | |
14 | === added file 'src/maasserver/migrations/builtin/maasserver/0094_add_unmanaged_subnets.py' |
15 | --- src/maasserver/migrations/builtin/maasserver/0094_add_unmanaged_subnets.py 1970-01-01 00:00:00 +0000 |
16 | +++ src/maasserver/migrations/builtin/maasserver/0094_add_unmanaged_subnets.py 2016-11-29 18:04:20 +0000 |
17 | @@ -0,0 +1,19 @@ |
18 | +# -*- coding: utf-8 -*- |
19 | +from __future__ import unicode_literals |
20 | + |
21 | +from django.db import migrations, models |
22 | + |
23 | + |
24 | +class Migration(migrations.Migration): |
25 | + |
26 | + dependencies = [ |
27 | + ('maasserver', '0093_add_rdns_model'), |
28 | + ] |
29 | + |
30 | + operations = [ |
31 | + migrations.AddField( |
32 | + model_name='subnet', |
33 | + name='managed', |
34 | + field=models.BooleanField(default=True), |
35 | + ), |
36 | + ] |
37 | |
38 | === modified file 'src/maasserver/models/iprange.py' |
39 | --- src/maasserver/models/iprange.py 2016-10-19 19:20:24 +0000 |
40 | +++ src/maasserver/models/iprange.py 2016-11-29 18:04:20 +0000 |
41 | @@ -171,8 +171,9 @@ |
42 | def netaddr_iprange(self): |
43 | return netaddr.IPRange(self.start_ip, self.end_ip) |
44 | |
45 | - def get_MAASIPRange(self): |
46 | - purpose = self.type |
47 | + def get_MAASIPRange(self, purpose=None): |
48 | + if purpose is None: |
49 | + purpose = self.type |
50 | # Using '-' instead of '_' is just for consistency. |
51 | # APIs in previous MAAS releases used '-' in range types. |
52 | purpose = purpose.replace('_', '-') |
53 | |
54 | === modified file 'src/maasserver/models/subnet.py' |
55 | --- src/maasserver/models/subnet.py 2016-10-18 16:48:13 +0000 |
56 | +++ src/maasserver/models/subnet.py 2016-11-29 18:04:20 +0000 |
57 | @@ -65,6 +65,7 @@ |
58 | MaybeIPAddress, |
59 | parse_integer, |
60 | ) |
61 | +from provisioningserver.utils.network import IPRANGE_TYPE as MAASIPRANGE_TYPE |
62 | |
63 | |
64 | maaslog = get_maas_logger("subnet") |
65 | @@ -386,6 +387,9 @@ |
66 | active_discovery = BooleanField( |
67 | editable=True, blank=False, null=False, default=False) |
68 | |
69 | + managed = BooleanField( |
70 | + editable=True, blank=False, null=False, default=True) |
71 | + |
72 | @property |
73 | def label(self): |
74 | """Returns a human-friendly label for this subnet.""" |
75 | @@ -457,8 +461,8 @@ |
76 | |
77 | def _get_ranges_for_allocated_ips( |
78 | self, ipnetwork: IPNetwork, ignore_discovered_ips: bool) -> set: |
79 | - """Returns a set of MAASIPRange objects created from the set of allocated |
80 | - StaticIPAddress objects. |
81 | + """Returns a set of MAASIPRange objects created from the set of |
82 | + allocated StaticIPAddress objects. |
83 | """ |
84 | # Note, the original implementation used .exclude() to filter, |
85 | # but we'll filter at runtime so that prefetch_related in the |
86 | @@ -472,10 +476,49 @@ |
87 | ranges.add(make_iprange(ip, purpose="assigned-ip")) |
88 | return ranges |
89 | |
90 | + def _add_subnet_metadata(self, ranges, exclude_addresses, |
91 | + ignore_discovered_ips): |
92 | + """Adds subnet metadata to the specified range set. |
93 | + |
94 | + For IP addresses within the subnet which are either a default gateway |
95 | + or are a gateway specified by a static route, adds the 'gateway-ip' |
96 | + purpose to the set of ranges. |
97 | + |
98 | + For IP addresses on the subnet which are also designated DNS servers |
99 | + on the subnet, adds IP addresses with the `dns-server` purpose |
100 | + to the set of ranges. |
101 | + |
102 | + For IP addresses specified in the `exclude_addresses` parameter, |
103 | + adds IP addresses with the `excluded` purpose to the set of ranges. |
104 | + """ |
105 | + ipnetwork = self.get_ipnetwork() |
106 | + if (self.gateway_ip is not None and self.gateway_ip != '' and |
107 | + self.gateway_ip in ipnetwork): |
108 | + ranges |= {make_iprange(self.gateway_ip, purpose="gateway-ip")} |
109 | + if self.dns_servers is not None: |
110 | + ranges |= set( |
111 | + make_iprange(server, purpose="dns-server") |
112 | + for server in self.dns_servers |
113 | + if server in ipnetwork |
114 | + ) |
115 | + for static_route in StaticRoute.objects.filter(source=self): |
116 | + ranges |= { |
117 | + make_iprange( |
118 | + static_route.gateway_ip, purpose="gateway-ip")} |
119 | + ranges |= self._get_ranges_for_allocated_ips( |
120 | + ipnetwork, ignore_discovered_ips) |
121 | + ranges |= set( |
122 | + make_iprange(address, purpose="excluded") |
123 | + for address in exclude_addresses |
124 | + if address in ipnetwork |
125 | + ) |
126 | + return ranges |
127 | + |
128 | def get_ipranges_in_use( |
129 | self, exclude_addresses: IPAddressExcludeList=None, |
130 | ranges_only: bool=False, |
131 | - ignore_discovered_ips: bool=False) -> MAASIPSet: |
132 | + ignore_discovered_ips: bool=False, |
133 | + for_reserved_allocation: bool=False) -> MAASIPSet: |
134 | """Returns a `MAASIPSet` of `MAASIPRange` objects which are currently |
135 | in use on this `Subnet`. |
136 | |
137 | @@ -483,6 +526,8 @@ |
138 | :param ignore_discovered_ips: DISCOVERED addresses are not "in use". |
139 | :param ranges_only: if True, filters out gateway IPs, static routes, |
140 | DNS servers, and `exclude_addresses`. |
141 | + :param for_reserved_allocation: if True, filters out reserved IP |
142 | + ranges, so that users can use them for allocation. |
143 | """ |
144 | if exclude_addresses is None: |
145 | exclude_addresses = [] |
146 | @@ -499,9 +544,9 @@ |
147 | # *outside* both ranges, so that they won't conflict with addresses |
148 | # reserved from this scheme in the future. |
149 | first = str(IPAddress(network.first)) |
150 | - first_plus_one = str(IPAddress(network.first + 1)) |
151 | - second = str(IPAddress(network.first + 0xFFFFFFFF)) |
152 | if network.prefixlen == 64: |
153 | + first_plus_one = str(IPAddress(network.first + 1)) |
154 | + second = str(IPAddress(network.first + 0xFFFFFFFF)) |
155 | ranges |= {make_iprange( |
156 | first_plus_one, second, purpose="reserved")} |
157 | # Reserve the subnet router anycast address, except for /127 and |
158 | @@ -509,29 +554,11 @@ |
159 | if network.prefixlen < 127: |
160 | ranges |= {make_iprange( |
161 | first, first, purpose="rfc-4291-2.6.1")} |
162 | - ipnetwork = self.get_ipnetwork() |
163 | if not ranges_only: |
164 | - if (self.gateway_ip is not None and self.gateway_ip != '' and |
165 | - self.gateway_ip in ipnetwork): |
166 | - ranges |= {make_iprange(self.gateway_ip, purpose="gateway-ip")} |
167 | - if self.dns_servers is not None: |
168 | - ranges |= set( |
169 | - make_iprange(server, purpose="dns-server") |
170 | - for server in self.dns_servers |
171 | - if server in ipnetwork |
172 | - ) |
173 | - for static_route in StaticRoute.objects.filter(source=self): |
174 | - ranges |= { |
175 | - make_iprange( |
176 | - static_route.gateway_ip, purpose="gateway-ip")} |
177 | - ranges |= self._get_ranges_for_allocated_ips( |
178 | - ipnetwork, ignore_discovered_ips) |
179 | - ranges |= set( |
180 | - make_iprange(address, purpose="excluded") |
181 | - for address in exclude_addresses |
182 | - if address in network |
183 | - ) |
184 | - ranges |= self.get_reserved_maasipset() |
185 | + ranges |= self._add_subnet_metadata( |
186 | + ranges, exclude_addresses, ignore_discovered_ips) |
187 | + if not for_reserved_allocation: |
188 | + ranges |= self.get_reserved_maasipset() |
189 | ranges |= self.get_dynamic_maasipset() |
190 | return MAASIPSet(ranges) |
191 | |
192 | @@ -554,12 +581,24 @@ |
193 | """ |
194 | if exclude_addresses is None: |
195 | exclude_addresses = [] |
196 | + for_reserved_allocation = self.managed is False and not ranges_only |
197 | ranges = self.get_ipranges_in_use( |
198 | exclude_addresses=exclude_addresses, |
199 | ranges_only=ranges_only, |
200 | - ignore_discovered_ips=ignore_discovered_ips) |
201 | + ignore_discovered_ips=ignore_discovered_ips, |
202 | + for_reserved_allocation=for_reserved_allocation |
203 | + ) |
204 | + if for_reserved_allocation: |
205 | + # For unmanaged networks, we must reserve everything NOT in a |
206 | + # reserved range with the 'unmanaged' type. |
207 | + reserved = self.get_reserved_maasipset() |
208 | + unmanaged = reserved.get_unused_ranges( |
209 | + self.get_ipnetwork(), purpose=MAASIPRANGE_TYPE.UNMANAGED) |
210 | + ranges |= unmanaged |
211 | if with_neighbours: |
212 | ranges |= self.get_maasipset_for_neighbours() |
213 | + from pprint import pprint |
214 | + pprint(ranges) |
215 | unused = ranges.get_unused_ranges(self.get_ipnetwork()) |
216 | return unused |
217 | |
218 | @@ -574,7 +613,7 @@ |
219 | # IP addresses should already be covered by get_ipranges_in_use(). |
220 | neighbours = Discovery.objects.filter(subnet=self).by_unknown_ip() |
221 | neighbour_set = { |
222 | - make_iprange(neighbour.ip, purpose="neighbour") |
223 | + make_iprange(neighbour.ip, purpose=MAASIPRANGE_TYPE.NEIGHBOUR) |
224 | for neighbour in neighbours |
225 | } |
226 | return MAASIPSet(neighbour_set) |
227 | @@ -717,9 +756,9 @@ |
228 | "%s is within the dynamic range from %s to %s" % ( |
229 | ip, IPAddress(iprange.first), IPAddress(iprange.last))) |
230 | |
231 | - def get_reserved_maasipset(self): |
232 | + def get_reserved_maasipset(self, purpose=None): |
233 | reserved_ranges = MAASIPSet( |
234 | - iprange.get_MAASIPRange() |
235 | + iprange.get_MAASIPRange(purpose=purpose) |
236 | for iprange in self.get_reserved_ranges() |
237 | ) |
238 | return reserved_ranges |
239 | |
240 | === modified file 'src/maasserver/models/tests/test_subnet.py' |
241 | --- src/maasserver/models/tests/test_subnet.py 2016-11-09 08:14:00 +0000 |
242 | +++ src/maasserver/models/tests/test_subnet.py 2016-11-29 18:04:20 +0000 |
243 | @@ -2,6 +2,7 @@ |
244 | # GNU Affero General Public License version 3 (see the file LICENSE). |
245 | |
246 | """Tests for the Subnet model.""" |
247 | +from maasserver.utils.orm import reload_object |
248 | |
249 | __all__ = [] |
250 | |
251 | @@ -898,6 +899,11 @@ |
252 | |
253 | class TestSubnetGetNextIPForAllocation(MAASServerTestCase): |
254 | |
255 | + scenarios = ( |
256 | + ("managed", {'managed': True}), |
257 | + # ("unmanaged", {'managed': False}), |
258 | + ) |
259 | + |
260 | def setUp(self): |
261 | register_view("maasserver_discovery") |
262 | return super().setUp() |
263 | @@ -906,7 +912,7 @@ |
264 | # Note: 10.0.0.0/30 --> 10.0.0.1 and 10.0.0.0.2 are usable. |
265 | subnet = factory.make_Subnet( |
266 | cidr="10.0.0.0/30", gateway_ip="10.0.0.1", |
267 | - dns_servers=["10.0.0.2"]) |
268 | + dns_servers=["10.0.0.2"], managed=self.managed) |
269 | with ExpectedException( |
270 | StaticIPAddressExhaustion, |
271 | "No more IPs available in subnet: 10.0.0.0/30."): |
272 | @@ -915,35 +921,65 @@ |
273 | def test__allocates_next_free_address(self): |
274 | # Note: 10.0.0.0/30 --> 10.0.0.1 and 10.0.0.0.2 are usable. |
275 | subnet = factory.make_Subnet( |
276 | - cidr="10.0.0.0/30", gateway_ip=None, dns_servers=None) |
277 | + cidr="10.0.0.0/30", gateway_ip=None, dns_servers=None, |
278 | + managed=self.managed) |
279 | + if not self.managed: |
280 | + factory.make_IPRange( |
281 | + subnet, start_ip='10.0.0.1', end_ip='10.0.0.2', |
282 | + type=IPRANGE_TYPE.RESERVED) |
283 | + subnet = reload_object(subnet) |
284 | ip = subnet.get_next_ip_for_allocation() |
285 | self.assertThat(ip, Equals("10.0.0.1")) |
286 | |
287 | def test__avoids_gateway_ip(self): |
288 | # Note: 10.0.0.0/30 --> 10.0.0.1 and 10.0.0.0.2 are usable. |
289 | subnet = factory.make_Subnet( |
290 | - cidr="10.0.0.0/30", gateway_ip="10.0.0.1", dns_servers=None) |
291 | + cidr="10.0.0.0/30", gateway_ip="10.0.0.1", dns_servers=None, |
292 | + managed=self.managed) |
293 | + if not self.managed: |
294 | + factory.make_IPRange( |
295 | + subnet, start_ip='10.0.0.1', end_ip='10.0.0.2', |
296 | + type=IPRANGE_TYPE.RESERVED) |
297 | + subnet = reload_object(subnet) |
298 | ip = subnet.get_next_ip_for_allocation() |
299 | self.assertThat(ip, Equals("10.0.0.2")) |
300 | |
301 | def test__avoids_excluded_addresses(self): |
302 | # Note: 10.0.0.0/30 --> 10.0.0.1 and 10.0.0.0.2 are usable. |
303 | subnet = factory.make_Subnet( |
304 | - cidr="10.0.0.0/30", gateway_ip=None, dns_servers=None) |
305 | + cidr="10.0.0.0/30", gateway_ip=None, dns_servers=None, |
306 | + managed=self.managed) |
307 | + if not self.managed: |
308 | + factory.make_IPRange( |
309 | + subnet, start_ip='10.0.0.1', end_ip='10.0.0.2', |
310 | + type=IPRANGE_TYPE.RESERVED) |
311 | + subnet = reload_object(subnet) |
312 | ip = subnet.get_next_ip_for_allocation(exclude_addresses=["10.0.0.1"]) |
313 | self.assertThat(ip, Equals("10.0.0.2")) |
314 | |
315 | def test__avoids_dns_servers(self): |
316 | # Note: 10.0.0.0/30 --> 10.0.0.1 and 10.0.0.0.2 are usable. |
317 | subnet = factory.make_Subnet( |
318 | - cidr="10.0.0.0/30", gateway_ip=None, dns_servers=["10.0.0.1"]) |
319 | + cidr="10.0.0.0/30", gateway_ip=None, dns_servers=["10.0.0.1"], |
320 | + managed=self.managed) |
321 | + if not self.managed: |
322 | + factory.make_IPRange( |
323 | + subnet, start_ip='10.0.0.1', end_ip='10.0.0.2', |
324 | + type=IPRANGE_TYPE.RESERVED) |
325 | + subnet = reload_object(subnet) |
326 | ip = subnet.get_next_ip_for_allocation() |
327 | self.assertThat(ip, Equals("10.0.0.2")) |
328 | |
329 | def test__avoids_observed_neighbours(self): |
330 | # Note: 10.0.0.0/30 --> 10.0.0.1 and 10.0.0.0.2 are usable. |
331 | subnet = factory.make_Subnet( |
332 | - cidr="10.0.0.0/30", gateway_ip=None, dns_servers=None) |
333 | + cidr="10.0.0.0/30", gateway_ip=None, dns_servers=None, |
334 | + managed=self.managed) |
335 | + if not self.managed: |
336 | + factory.make_IPRange( |
337 | + subnet, start_ip='10.0.0.1', end_ip='10.0.0.2', |
338 | + type=IPRANGE_TYPE.RESERVED) |
339 | + subnet = reload_object(subnet) |
340 | rackif = factory.make_Interface(vlan=subnet.vlan) |
341 | factory.make_Discovery(ip="10.0.0.1", interface=rackif) |
342 | ip = subnet.get_next_ip_for_allocation() |
343 | @@ -952,7 +988,13 @@ |
344 | def test__logs_if_suggests_previously_observed_neighbour(self): |
345 | # Note: 10.0.0.0/30 --> 10.0.0.1 and 10.0.0.0.2 are usable. |
346 | subnet = factory.make_Subnet( |
347 | - cidr="10.0.0.0/30", gateway_ip=None, dns_servers=None) |
348 | + cidr="10.0.0.0/30", gateway_ip=None, dns_servers=None, |
349 | + managed=self.managed) |
350 | + if not self.managed: |
351 | + factory.make_IPRange( |
352 | + subnet, start_ip='10.0.0.1', end_ip='10.0.0.2', |
353 | + type=IPRANGE_TYPE.RESERVED) |
354 | + subnet = reload_object(subnet) |
355 | rackif = factory.make_Interface(vlan=subnet.vlan) |
356 | now = datetime.now() |
357 | yesterday = now - timedelta(days=1) |
358 | @@ -970,7 +1012,13 @@ |
359 | def test__uses_smallest_free_range_when_not_considering_neighbours(self): |
360 | # Note: 10.0.0.0/29 --> 10.0.0.1 through 10.0.0.0.6 are usable. |
361 | subnet = factory.make_Subnet( |
362 | - cidr="10.0.0.0/29", gateway_ip=None, dns_servers=None) |
363 | + cidr="10.0.0.0/29", gateway_ip=None, dns_servers=None, |
364 | + managed=self.managed) |
365 | + if not self.managed: |
366 | + factory.make_IPRange( |
367 | + subnet, start_ip='10.0.0.1', end_ip='10.0.0.6', |
368 | + type=IPRANGE_TYPE.RESERVED) |
369 | + subnet = reload_object(subnet) |
370 | # With .4 in use, the free ranges are {1, 2, 3}, {5, 6}. So MAAS should |
371 | # select 10.0.0.5, since that is the first address in the smallest |
372 | # available range. |
373 | |
374 | === modified file 'src/maasserver/testing/factory.py' |
375 | --- src/maasserver/testing/factory.py 2016-11-11 17:21:57 +0000 |
376 | +++ src/maasserver/testing/factory.py 2016-11-29 18:04:20 +0000 |
377 | @@ -848,7 +848,8 @@ |
378 | def make_Subnet(self, name=None, vlan=None, space=None, cidr=None, |
379 | gateway_ip=RANDOM, dns_servers=None, host_bits=None, |
380 | fabric=None, vid=None, dhcp_on=False, version=None, |
381 | - rdns_mode=RDNS_MODE.DEFAULT, allow_proxy=True): |
382 | + rdns_mode=RDNS_MODE.DEFAULT, allow_proxy=True, |
383 | + managed=True): |
384 | if name is None: |
385 | name = factory.make_name('name') |
386 | if vlan is None: |
387 | @@ -869,7 +870,7 @@ |
388 | subnet = Subnet( |
389 | name=name, vlan=vlan, cidr=cidr, gateway_ip=gateway_ip, |
390 | space=space, dns_servers=dns_servers, rdns_mode=rdns_mode, |
391 | - allow_proxy=allow_proxy) |
392 | + allow_proxy=allow_proxy, managed=managed) |
393 | subnet.save() |
394 | return subnet |
395 | |
396 | |
397 | === modified file 'src/provisioningserver/utils/network.py' |
398 | --- src/provisioningserver/utils/network.py 2016-11-03 15:39:17 +0000 |
399 | +++ src/provisioningserver/utils/network.py 2016-11-29 18:04:20 +0000 |
400 | @@ -35,6 +35,7 @@ |
401 | import struct |
402 | from typing import ( |
403 | Iterable, |
404 | + Tuple, |
405 | List, |
406 | Optional, |
407 | TypeVar, |
408 | @@ -100,6 +101,8 @@ |
409 | GATEWAY_IP = 'gateway-ip' |
410 | DYNAMIC = 'dynamic' |
411 | PROPOSED_DYNAMIC = 'proposed-dynamic' |
412 | + UNMANAGED = 'unmanaged' |
413 | + NEIGHBOUR = 'neighbour' |
414 | |
415 | |
416 | class MAASIPRange(IPRange): |
417 | @@ -152,31 +155,173 @@ |
418 | return json |
419 | |
420 | |
421 | -def _combine_overlapping_maasipranges( |
422 | +def get_iprange_intersection(a: MAASIPRange, b: MAASIPRange): |
423 | + """Given two overlapping MAASIPRange objects, return the best possible |
424 | + combination of the two without any information loss. |
425 | + """ |
426 | + # This ensures r0's start index is less than or equal to r1's start index. |
427 | + r0, r1 = sorted([a, b]) |
428 | + if r0.first == r1.first and r0.last == r1.last: |
429 | + # Easy case: exact match. |
430 | + # +------------+ |
431 | + # | r0 | |
432 | + # +------------+ |
433 | + # +------------+ |
434 | + # | r1 | |
435 | + # + +------------+ |
436 | + # ================ |
437 | + # +------------+ |
438 | + # | x | |
439 | + # +------------+ |
440 | + # purpose(x) = r0 | r1 |
441 | + return ( |
442 | + MAASIPRange(r0.first, r0.last, purpose=r0.purpose | r1. purpose), |
443 | + ) |
444 | + elif r0.first == r1.first and r0.last > r1.last: |
445 | + # Note: if the start index is equal, larger ranges always sort before |
446 | + # smaller ranges. |
447 | + # +------------+ |
448 | + # | r0 | |
449 | + # +------------+ |
450 | + # +--------+ |
451 | + # | r1 | |
452 | + # + +--------+ |
453 | + # ================ |
454 | + # +---+--------+ |
455 | + # | x | y | |
456 | + # +---+--------+ |
457 | + # purpose(x) = r0 | r1 |
458 | + # purpose(y) = r0 |
459 | + return ( |
460 | + MAASIPRange(r0.first, r1.last, purpose=r0.purpose | r1.purpose), |
461 | + MAASIPRange(r1.last + 1, r0.last, purpose=r0.purpose), |
462 | + ) |
463 | + elif r0.first < r1.first and r0.last == r1.last: |
464 | + # +------------+ |
465 | + # | r0 | |
466 | + # +------------+ |
467 | + # +--------+ |
468 | + # | r1 | |
469 | + # + +--------+ |
470 | + # ================ |
471 | + # +---+--------+ |
472 | + # | x | y | |
473 | + # +---+--------+ |
474 | + # purpose(x) = r0 |
475 | + # purpose(y) = r0 | r1 |
476 | + return ( |
477 | + MAASIPRange(r0.first, r1.first - 1, purpose=r0.purpose), |
478 | + MAASIPRange(r1.first, r0.last, purpose=r0.purpose | r1.purpose), |
479 | + ) |
480 | + elif r0.first < r1.first and r0.last < r1.last: |
481 | + # +--------+ |
482 | + # | r0 | |
483 | + # +--------+ |
484 | + # +--------+ |
485 | + # | r1 | |
486 | + # + +--------+ |
487 | + # ================ |
488 | + # +---+----+---+ |
489 | + # | x | y | z | |
490 | + # +---+----+---+ |
491 | + # purpose(x) = r0 |
492 | + # purpose(y) = r0 | r1 |
493 | + # purpose(z) = r1 |
494 | + return ( |
495 | + MAASIPRange(r0.first, r1.first - 1, purpose=r0.purpose), |
496 | + MAASIPRange( |
497 | + r1.first, r0.last, purpose=r0.purpose | r1.purpose), |
498 | + MAASIPRange(r0.last + 1, r1.last, purpose=r1.purpose), |
499 | + ) |
500 | + elif r0.first < r1.first and r0.last > r1.last: |
501 | + # +------------+ |
502 | + # | r0 | |
503 | + # +------------+ |
504 | + # +----+ |
505 | + # | r1 | |
506 | + # + +----+ |
507 | + # ================ |
508 | + # +---+----+---+ |
509 | + # | x | y | z | |
510 | + # +---+----+---+ |
511 | + # x = r0 |
512 | + # y = r0 | r1 |
513 | + # z = r0 |
514 | + return ( |
515 | + MAASIPRange(r0.first, r1.first - 1, purpose=r0.purpose), |
516 | + MAASIPRange( |
517 | + r1.first, r1.last, purpose=r0.purpose | r1.purpose), |
518 | + MAASIPRange(r1.last + 1, r0.last, purpose=r0.purpose), |
519 | + ) |
520 | + else: |
521 | + raise ValueError( |
522 | + "Unable to combine IP ranges: (%s, %s); do they actually overlap?" |
523 | + % (a, b)) |
524 | + |
525 | + |
526 | +def _split_overlapping_ipranges_for_mismatched_purposes( |
527 | ranges: Iterable[MAASIPRange]) -> List[MAASIPRange]: |
528 | """Returns the specified ranges after combining any overlapping ranges. |
529 | |
530 | Given a sorted list of `MAASIPRange` objects, returns a new (sorted) |
531 | list where any adjacent overlapping ranges have been combined into a single |
532 | range. |
533 | + |
534 | + Must be run after combining overlapping IP ranges (if purpose matches). |
535 | """ |
536 | new_ranges = [] |
537 | previous_min = None |
538 | previous_max = None |
539 | + previous_purpose = None |
540 | for item in ranges: |
541 | if previous_min is not None and previous_max is not None: |
542 | # Check for an overlapping range. |
543 | min_overlaps = previous_min <= item.first <= previous_max |
544 | max_overlaps = previous_min <= item.last <= previous_max |
545 | - if min_overlaps or max_overlaps: |
546 | + if previous_purpose != item.purpose and ( |
547 | + min_overlaps or max_overlaps): |
548 | + # Replace the previous range with one or more ranges. |
549 | previous = new_ranges.pop() |
550 | + ranges = get_iprange_intersection(previous, item) |
551 | + new_ranges.extend(ranges) |
552 | + item = new_ranges[-1] |
553 | + else: |
554 | + new_ranges.append(item) |
555 | + previous_min = item.first |
556 | + previous_max = item.last |
557 | + previous_purpose = item.purpose |
558 | + return new_ranges |
559 | + |
560 | + |
561 | +def _combine_adjacent_maasipranges( |
562 | + ranges: Iterable[MAASIPRange]) -> List[MAASIPRange]: |
563 | + """Returns the specified ranges after combining any overlapping ranges. |
564 | + |
565 | + Given a sorted list of `MAASIPRange` objects, returns a new (sorted) |
566 | + list where any adjacent overlapping ranges have been combined into a single |
567 | + range. |
568 | + """ |
569 | + new_ranges = [] |
570 | + previous_min = None |
571 | + previous_max = None |
572 | + previous_purpose = None |
573 | + for item in ranges: |
574 | + if previous_min is not None and previous_max is not None: |
575 | + # Check for an overlapping range. |
576 | + min_overlaps = previous_min <= item.first <= previous_max |
577 | + max_overlaps = previous_min <= item.last <= previous_max |
578 | + if item.purpose == previous_purpose and ( |
579 | + min_overlaps or max_overlaps): |
580 | + # Replace the previous range with a new, combined range. |
581 | + new_ranges.pop() |
582 | item = make_iprange( |
583 | min(item.first, previous_min), |
584 | max(item.last, previous_max), |
585 | - previous.purpose | item.purpose) |
586 | + item.purpose) |
587 | + new_ranges.append(item) |
588 | previous_min = item.first |
589 | previous_max = item.last |
590 | - new_ranges.append(item) |
591 | + previous_purpose = item.purpose |
592 | return new_ranges |
593 | |
594 | |
595 | @@ -398,8 +543,11 @@ |
596 | (3) Combining adjacent ranges with an identical purpose. |
597 | """ |
598 | self.ranges = _normalize_ipranges(self.ranges) |
599 | - self.ranges = _combine_overlapping_maasipranges(self.ranges) |
600 | self.ranges = _coalesce_adjacent_purposes(self.ranges) |
601 | + self.ranges = _split_overlapping_ipranges_for_mismatched_purposes( |
602 | + self.ranges) |
603 | + self.ranges = _combine_adjacent_maasipranges( |
604 | + self.ranges) |
605 | |
606 | def __ior__(self, other): |
607 | """Return self |= other.""" |
608 | @@ -521,7 +669,7 @@ |
609 | |
610 | def get_unused_ranges( |
611 | self, outer_range: OuterRange, |
612 | - comment=IPRANGE_TYPE.UNUSED) -> 'MAASIPSet': |
613 | + purpose=IPRANGE_TYPE.UNUSED) -> 'MAASIPSet': |
614 | """Calculates and returns a list of unused IP ranges, based on |
615 | the supplied range of desired addresses. |
616 | |
617 | @@ -557,7 +705,7 @@ |
618 | # range. |
619 | if candidate_end - candidate_start >= 0: |
620 | unused_ranges.append( |
621 | - make_iprange(candidate_start, candidate_end, comment)) |
622 | + make_iprange(candidate_start, candidate_end, purpose)) |
623 | candidate_start = used_range.last + 1 |
624 | # Skip the broadcast address, if this is an IPv4 network |
625 | if type(outer_range) == IPNetwork: |
626 | @@ -572,7 +720,7 @@ |
627 | # of the range we're checking against. |
628 | if candidate_end - candidate_start >= 0: |
629 | unused_ranges.append( |
630 | - make_iprange(candidate_start, candidate_end, comment)) |
631 | + make_iprange(candidate_start, candidate_end, purpose)) |
632 | return MAASIPSet(unused_ranges) |
633 | |
634 | def get_full_range(self, outer_range): |
Transitioned to Git.
lp:maas has now moved from Bzr to Git.
Please propose your branches with Launchpad using Git.
git clone https:/ /git.launchpad. net/maas