Merge lp:~mpontillo/maas/suggested-range-1562198 into lp:~maas-committers/maas/trunk
- suggested-range-1562198
- Merge into trunk
Status: | Merged |
---|---|
Approved by: | Mike Pontillo |
Approved revision: | no longer in the source branch. |
Merged at revision: | 4911 |
Proposed branch: | lp:~mpontillo/maas/suggested-range-1562198 |
Merge into: | lp:~maas-committers/maas/trunk |
Diff against target: |
790 lines (+423/-83) 12 files modified
src/maasserver/api/subnets.py (+8/-1) src/maasserver/api/tests/test_subnets.py (+19/-4) src/maasserver/models/subnet.py (+12/-4) src/maasserver/static/js/angular/controllers/tests/test_vlan_details.js (+57/-56) src/maasserver/static/js/angular/controllers/vlan_details.js (+25/-6) src/maasserver/static/partials/vlan-details.html (+3/-3) src/maasserver/testing/factory.py (+3/-2) src/maasserver/websockets/handlers/subnet.py (+2/-1) src/maasserver/websockets/handlers/tests/test_subnet.py (+2/-1) src/maastesting/factory.py (+4/-2) src/provisioningserver/utils/network.py (+163/-3) src/provisioningserver/utils/tests/test_network.py (+125/-0) |
To merge this branch: | bzr merge lp:~mpontillo/maas/suggested-range-1562198 |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Blake Rouse (community) | Approve | ||
Review via email: mp+291699@code.launchpad.net |
Commit message
Add suggested values for gateway and dynamic range to IP range calculations.
* Calculate a good guess for a practical size for the dynamic range.
* Present default ranges to the user when providing DHCP.
* Make sure the placeholder matches the initial values of the suggestions.
Description of the change
Mike Pontillo (mpontillo) wrote : | # |
Thanks. Going to try to land this now; wish me luck. ;-)
MAAS Lander (maas-lander) wrote : | # |
The attempt to merge lp:~mpontillo/maas/suggested-range-1562198 into lp:maas failed. Below is the output from the failed tests.
Hit:1 http://
Get:2 http://
Hit:3 http://
Hit:4 http://
Get:5 http://
Get:6 http://
Get:7 http://
Fetched 9,850 kB in 1s (4,989 kB/s)
Reading package lists...
sudo DEBIAN_
--no-
Reading package lists...
Building dependency tree...
Reading state information...
apache2 is already the newest version (2.4.18-2ubuntu2).
archdetect-deb is already the newest version (1.117ubuntu1).
authbind is already the newest version (2.1.1+nmu1).
bash is already the newest version (4.3-14ubuntu1).
bind9 is already the newest version (1:9.10.
bind9utils is already the newest version (1:9.10.
build-essential is already the newest version (12.1ubuntu2).
curl is already the newest version (7.47.0-1ubuntu2).
debhelper is already the newest version (9.20160115ubun
distro-info is already the newest version (0.14build1).
dnsutils is already the newest version (1:9.10.
firefox is already the newest version (45.0.1+
freeipmi-tools is already the newest version (1.4.11-1ubuntu1).
git is already the newest version (1:...
MAAS Lander (maas-lander) wrote : | # |
The attempt to merge lp:~mpontillo/maas/suggested-range-1562198 into lp:maas failed. Below is the output from the failed tests.
Get:1 http://
Hit:2 http://
Hit:3 http://
Hit:4 http://
Get:5 http://
Get:6 http://
Get:7 http://
Get:8 http://
Get:9 http://
Get:10 http://
Fetched 22.5 MB in 4s (4,841 kB/s)
Reading package lists...
sudo DEBIAN_
--no-
Reading package lists...
Building dependency tree...
Reading state information...
apache2 is already the newest version (2.4.18-2ubuntu2).
archdetect-deb is already the newest version (1.117ubuntu1).
authbind is already the newest version (2.1.1+nmu1).
bash is already the newest version (4.3-14ubuntu1).
bind9 is already the newest version (1:9.10.
bind9utils is already the newest version (1:9.10.
build-essential is already the newest version (12.1ubuntu2).
curl is already the newest version (7.47.0-1ubuntu2).
debhelper is already the newest version (...
MAAS Lander (maas-lander) wrote : | # |
The attempt to merge lp:~mpontillo/maas/suggested-range-1562198 into lp:maas failed. Below is the output from the failed tests.
Hit:1 http://
Hit:2 http://
Hit:3 http://
Hit:4 http://
Reading package lists...
sudo DEBIAN_
--no-
Reading package lists...
Building dependency tree...
Reading state information...
apache2 is already the newest version (2.4.18-2ubuntu2).
archdetect-deb is already the newest version (1.117ubuntu1).
authbind is already the newest version (2.1.1+nmu1).
bash is already the newest version (4.3-14ubuntu1).
bind9 is already the newest version (1:9.10.
bind9utils is already the newest version (1:9.10.
build-essential is already the newest version (12.1ubuntu2).
curl is already the newest version (7.47.0-1ubuntu2).
debhelper is already the newest version (9.20160115ubun
distro-info is already the newest version (0.14build1).
dnsutils is already the newest version (1:9.10.
firefox is already the newest version (45.0.1+
freeipmi-tools is already the newest version (1.4.11-1ubuntu1).
git is already the newest version (1:2.7.4-0ubuntu1).
isc-dhcp-common is already the newest version (4.3.3-5ubuntu11).
libjs-angularjs is already the newest version (1.2.28-1ubuntu2).
libjs-jquery is already the newest version (1.11.3+dfsg-4).
libjs-yui3-full is already the newest version (3.5.1-1ubuntu3).
libjs-yui3-min is already the newest version (3.5.1-1ubuntu3).
libpq-dev...
Mike Pontillo (mpontillo) wrote : | # |
Random failure in:
maasserver.
Trying again.
Preview Diff
1 | === modified file 'src/maasserver/api/subnets.py' |
2 | --- src/maasserver/api/subnets.py 2016-03-28 13:54:47 +0000 |
3 | +++ src/maasserver/api/subnets.py 2016-04-13 03:03:16 +0000 |
4 | @@ -185,6 +185,8 @@ |
5 | Optional arguments: |
6 | include_ranges: if True, includes detailed information |
7 | about the usage of this range. |
8 | + include_suggestions: if True, includes the suggested gateway and |
9 | + dynamic range for this subnet, if it were to be configured. |
10 | |
11 | Returns 404 if the subnet is not found. |
12 | """ |
13 | @@ -192,9 +194,14 @@ |
14 | subnet_id, request.user, NODE_PERMISSION.VIEW) |
15 | include_ranges = get_optional_param( |
16 | request.GET, 'include_ranges', default=False, validator=StringBool) |
17 | + include_suggestions = get_optional_param( |
18 | + request.GET, 'include_suggestions', default=False, |
19 | + validator=StringBool) |
20 | full_iprange = subnet.get_iprange_usage() |
21 | statistics = IPRangeStatistics(full_iprange) |
22 | - return statistics.render_json(include_ranges=include_ranges) |
23 | + return statistics.render_json( |
24 | + include_ranges=include_ranges, |
25 | + include_suggestions=include_suggestions) |
26 | |
27 | @operation(idempotent=True) |
28 | def ip_addresses(self, request, subnet_id): |
29 | |
30 | === modified file 'src/maasserver/api/tests/test_subnets.py' |
31 | --- src/maasserver/api/tests/test_subnets.py 2016-04-11 09:55:54 +0000 |
32 | +++ src/maasserver/api/tests/test_subnets.py 2016-04-13 03:03:16 +0000 |
33 | @@ -29,8 +29,10 @@ |
34 | IPRangeStatistics, |
35 | ) |
36 | from testtools.matchers import ( |
37 | + Contains, |
38 | ContainsDict, |
39 | Equals, |
40 | + HasLength, |
41 | ) |
42 | |
43 | |
44 | @@ -323,8 +325,8 @@ |
45 | |
46 | class TestSubnetReservedIPRangesAPI(APITestCase): |
47 | |
48 | - def test__returns_empty_list_for_empty_subnet(self): |
49 | - subnet = factory.make_Subnet(dns_servers=[], gateway_ip='') |
50 | + def test__returns_empty_list_for_empty_ipv4_subnet(self): |
51 | + subnet = factory.make_Subnet(version=4, dns_servers=[], gateway_ip='') |
52 | response = self.client.get( |
53 | get_subnet_uri(subnet), |
54 | {'op': 'reserved_ip_ranges'}) |
55 | @@ -334,6 +336,19 @@ |
56 | result = json.loads(response.content.decode(settings.DEFAULT_CHARSET)) |
57 | self.assertThat(result, Equals([])) |
58 | |
59 | + def test__returns_reserved_anycast_for_empty_ipv6_subnet(self): |
60 | + subnet = factory.make_Subnet(version=6, dns_servers=[], gateway_ip='') |
61 | + response = self.client.get( |
62 | + get_subnet_uri(subnet), |
63 | + {'op': 'reserved_ip_ranges'}) |
64 | + self.assertEqual( |
65 | + http.client.OK, response.status_code, |
66 | + explain_unexpected_response(http.client.OK, response)) |
67 | + result = json.loads(response.content.decode(settings.DEFAULT_CHARSET)) |
68 | + self.assertThat(result, HasLength(1)) |
69 | + self.assertThat(result[0]["num_addresses"], Equals(1)) |
70 | + self.assertThat(result[0]["purpose"], Contains("rfc-4291-2.6.1")) |
71 | + |
72 | def test__accounts_for_reserved_ip_address(self): |
73 | subnet = factory.make_Subnet(dns_servers=[], gateway_ip='') |
74 | ip = factory.pick_ip_in_network(subnet.get_ipnetwork()) |
75 | @@ -346,13 +361,13 @@ |
76 | http.client.OK, response.status_code, |
77 | explain_unexpected_response(http.client.OK, response)) |
78 | result = json.loads(response.content.decode(settings.DEFAULT_CHARSET)) |
79 | - self.assertThat(result, Equals([ |
80 | + self.assertThat(result, Contains( |
81 | { |
82 | "start": ip, |
83 | "end": ip, |
84 | "purpose": ["assigned-ip"], |
85 | "num_addresses": 1, |
86 | - }])) |
87 | + })) |
88 | |
89 | |
90 | class TestSubnetUnreservedIPRangesAPI(APITestCase): |
91 | |
92 | === modified file 'src/maasserver/models/subnet.py' |
93 | --- src/maasserver/models/subnet.py 2016-04-11 09:39:42 +0000 |
94 | +++ src/maasserver/models/subnet.py 2016-04-13 03:03:16 +0000 |
95 | @@ -414,10 +414,10 @@ |
96 | """ |
97 | ranges = set() |
98 | network = self.get_ipnetwork() |
99 | - if network.version == 6 and network.prefixlen == 64: |
100 | + if network.version == 6: |
101 | # For most IPv6 networks, automatically reserve the range: |
102 | - # ::0 - ::ffff:ffff |
103 | - # We expect the administrator will be using ::0 through ::ffff. |
104 | + # ::1 - ::ffff:ffff |
105 | + # We expect the administrator will be using ::1 through ::ffff. |
106 | # We plan to reserve ::1:0 through ::ffff:ffff for use by MAAS, |
107 | # so that we can allocate addresses in the form: |
108 | # ::<node>:<child> |
109 | @@ -425,8 +425,16 @@ |
110 | # *outside* both ranges, so that they won't conflict with addresses |
111 | # reserved from this scheme in the future. |
112 | first = str(IPAddress(network.first)) |
113 | + first_plus_one = str(IPAddress(network.first + 1)) |
114 | second = str(IPAddress(network.first + 0xFFFFFFFF)) |
115 | - ranges |= {make_iprange(first, second, purpose="reserved")} |
116 | + if network.prefixlen == 64: |
117 | + ranges |= {make_iprange( |
118 | + first_plus_one, second, purpose="reserved")} |
119 | + # Reserve the subnet router anycast address, except for /127 and |
120 | + # /128 networks. (See RFC 6164, and RFC 4291 section 2.6.1.) |
121 | + if network.prefixlen < 127: |
122 | + ranges |= {make_iprange( |
123 | + first, first, purpose="rfc-4291-2.6.1")} |
124 | ipnetwork = self.get_ipnetwork() |
125 | if not ranges_only: |
126 | assigned_ip_addresses = self.get_staticipaddresses_in_use() |
127 | |
128 | === modified file 'src/maasserver/static/js/angular/controllers/tests/test_vlan_details.js' |
129 | --- src/maasserver/static/js/angular/controllers/tests/test_vlan_details.js 2016-03-28 13:54:47 +0000 |
130 | +++ src/maasserver/static/js/angular/controllers/tests/test_vlan_details.js 2016-04-13 03:03:16 +0000 |
131 | @@ -451,75 +451,76 @@ |
132 | }); |
133 | |
134 | |
135 | - it("prepares provideDHCPAction on actionOptionChanged", function() { |
136 | - var controller = makeControllerResolveSetActiveItem(); |
137 | - controller.actionOption = controller.PROVIDE_DHCP_ACTION; |
138 | - controller.actionOptionChanged(); |
139 | - expect(controller.provideDHCPAction).toEqual({ |
140 | - subnet: subnet.id, |
141 | - primaryRack: "p1", |
142 | - secondaryRack: "p2", |
143 | - maxIPs: 0, |
144 | - startIP: '', |
145 | - endIP: '', |
146 | - gatewayIP: '', |
147 | - needsGatewayIP: false, |
148 | - subnetMissingGatewayIP: true, |
149 | - needsDynamicRange: true |
150 | - }); |
151 | - }); |
152 | - |
153 | - it("provideDHCPAction skips dynamic range if already present", function() { |
154 | - var controller = makeControllerResolveSetActiveItem(); |
155 | - controller.subnets[0].statistics.ranges = [{purpose: ["dynamic"]}]; |
156 | - controller.actionOption = controller.PROVIDE_DHCP_ACTION; |
157 | - controller.actionOptionChanged(); |
158 | - expect(controller.provideDHCPAction).toEqual({ |
159 | - subnet: subnet.id, |
160 | - primaryRack: "p1", |
161 | - secondaryRack: "p2", |
162 | - maxIPs: 0, |
163 | - startIP: null, |
164 | - endIP: null, |
165 | - gatewayIP: '', |
166 | - needsGatewayIP: false, |
167 | + it("prepares provideDHCPAction on actionOptionChanged " + |
168 | + "and populates suggested gateway", function() { |
169 | + var controller = makeControllerResolveSetActiveItem(); |
170 | + controller.subnets[0].gateway_ip = null; |
171 | + controller.subnets[0].statistics = { |
172 | + suggested_gateway: "192.168.0.1" |
173 | + }; |
174 | + controller.updateSubnet(); |
175 | + controller.actionOption = controller.PROVIDE_DHCP_ACTION; |
176 | + controller.actionOptionChanged(); |
177 | + expect(controller.provideDHCPAction).toEqual({ |
178 | + subnet: subnet.id, |
179 | + primaryRack: "p1", |
180 | + secondaryRack: "p2", |
181 | + maxIPs: 0, |
182 | + startIP: "", |
183 | + startPlaceholder: "(no available IPs)", |
184 | + endIP: "", |
185 | + endPlaceholder: "(no available IPs)", |
186 | + gatewayIP: '192.168.0.1', |
187 | + gatewayPlaceholder: '192.168.0.1', |
188 | + needsGatewayIP: true, |
189 | subnetMissingGatewayIP: true, |
190 | needsDynamicRange: false |
191 | }); |
192 | }); |
193 | |
194 | + it("provideDHCPAction uses suggested dynamic range", function() { |
195 | + var controller = makeControllerResolveSetActiveItem(); |
196 | + controller.subnets[0].statistics = { |
197 | + num_addresses: 26, |
198 | + suggested_dynamic_range: { |
199 | + num_addresses: 26, |
200 | + start: "192.168.0.200", |
201 | + end: "192.168.0.225" |
202 | + }, |
203 | + first_address: "192.168.0.1" |
204 | + }; |
205 | + controller.updateSubnet(); |
206 | + controller.actionOption = controller.PROVIDE_DHCP_ACTION; |
207 | + controller.actionOptionChanged(); |
208 | + expect(controller.provideDHCPAction).toEqual({ |
209 | + subnet: subnet.id, |
210 | + primaryRack: "p1", |
211 | + secondaryRack: "p2", |
212 | + maxIPs: 26, |
213 | + startIP: "192.168.0.200", |
214 | + endIP: "192.168.0.225", |
215 | + startPlaceholder: "192.168.0.200", |
216 | + endPlaceholder: "192.168.0.225", |
217 | + gatewayIP: '', |
218 | + gatewayPlaceholder: '', |
219 | + needsGatewayIP: false, |
220 | + subnetMissingGatewayIP: true, |
221 | + needsDynamicRange: true |
222 | + }); |
223 | + }); |
224 | + |
225 | it("prevents selection of a duplicate rack controller", function() { |
226 | var controller = makeControllerResolveSetActiveItem(); |
227 | controller.actionOption = controller.PROVIDE_DHCP_ACTION; |
228 | controller.actionOptionChanged(); |
229 | controller.provideDHCPAction.primaryRack = "p2"; |
230 | controller.updatePrimaryRack(); |
231 | - expect(controller.provideDHCPAction).toEqual({ |
232 | - subnet: subnet.id, |
233 | - primaryRack: "p2", |
234 | - secondaryRack: null, |
235 | - maxIPs: 0, |
236 | - startIP: '', |
237 | - endIP: '', |
238 | - gatewayIP: '', |
239 | - needsGatewayIP: false, |
240 | - subnetMissingGatewayIP: true, |
241 | - needsDynamicRange: true |
242 | - }); |
243 | + expect(controller.provideDHCPAction.primaryRack).toEqual("p2"); |
244 | + expect(controller.provideDHCPAction.secondaryRack).toBe(null); |
245 | controller.provideDHCPAction.secondaryRack = "p2"; |
246 | controller.updateSecondaryRack(); |
247 | - expect(controller.provideDHCPAction).toEqual({ |
248 | - subnet: subnet.id, |
249 | - primaryRack: null, |
250 | - secondaryRack: null, |
251 | - maxIPs: 0, |
252 | - startIP: '', |
253 | - endIP: '', |
254 | - gatewayIP: '', |
255 | - needsGatewayIP: false, |
256 | - subnetMissingGatewayIP: true, |
257 | - needsDynamicRange: true |
258 | - }); |
259 | + expect(controller.provideDHCPAction.primaryRack).toBe(null); |
260 | + expect(controller.provideDHCPAction.secondaryRack).toBe(null); |
261 | }); |
262 | |
263 | describe("filterPrimaryRack", function() { |
264 | |
265 | === modified file 'src/maasserver/static/js/angular/controllers/vlan_details.js' |
266 | --- src/maasserver/static/js/angular/controllers/vlan_details.js 2016-03-28 13:54:47 +0000 |
267 | +++ src/maasserver/static/js/angular/controllers/vlan_details.js 2016-04-13 03:03:16 +0000 |
268 | @@ -102,8 +102,11 @@ |
269 | for (i = 0; i < vm.relatedSubnets.length; i++) { |
270 | subnet = vm.relatedSubnets[i].subnet; |
271 | // If any related subnet already has a dynamic range, we |
272 | - // cannot prompt the user to enter one here. |
273 | - if (SubnetsManager.hasDynamicRange(subnet)) { |
274 | + // cannot prompt the user to enter one here. If a |
275 | + // suggestion does not exist, a range does not exist |
276 | + // already. |
277 | + var iprange = subnet.statistics.suggested_dynamic_range; |
278 | + if (!angular.isObject(iprange)) { |
279 | // If there is already a dynamic range on one of the |
280 | // subnets, it's the "subnet of least surprise" if |
281 | // the user is choosing to reconfigure their rack |
282 | @@ -186,20 +189,36 @@ |
283 | vm.updateSubnet = function() { |
284 | var dhcp = vm.provideDHCPAction; |
285 | var subnet = SubnetsManager.getItemFromList(dhcp.subnet); |
286 | - if(angular.isObject(subnet) && dhcp.needsDynamicRange === true) { |
287 | - var iprange = SubnetsManager.getLargestRange(subnet); |
288 | - if(iprange.num_addresses > 0) { |
289 | + if(angular.isObject(subnet)) { |
290 | + var suggested_gateway = null; |
291 | + var iprange = null; |
292 | + if(angular.isObject(subnet.statistics)) { |
293 | + suggested_gateway = subnet.statistics.suggested_gateway; |
294 | + iprange = subnet.statistics.suggested_dynamic_range; |
295 | + } |
296 | + if(angular.isObject(iprange) && iprange.num_addresses > 0) { |
297 | dhcp.maxIPs = iprange.num_addresses; |
298 | dhcp.startIP = iprange.start; |
299 | dhcp.endIP = iprange.end; |
300 | - dhcp.gatewayIP = iprange.start; |
301 | + dhcp.startPlaceholder = iprange.start; |
302 | + dhcp.endPlaceholder = iprange.end; |
303 | } else { |
304 | // Need to add a dynamic range, but according to our data, |
305 | // there is no room on the subnet for a dynamic range. |
306 | dhcp.maxIPs = 0; |
307 | dhcp.startIP = ""; |
308 | dhcp.endIP = ""; |
309 | + dhcp.startPlaceholder = "(no available IPs)"; |
310 | + dhcp.endPlaceholder = "(no available IPs)"; |
311 | + } |
312 | + if(angular.isString(suggested_gateway)) { |
313 | + dhcp.gatewayIP = suggested_gateway; |
314 | + dhcp.gatewayPlaceholder = suggested_gateway; |
315 | + } else { |
316 | + // This means the subnet already has a gateway, so don't |
317 | + // bother populating it. |
318 | dhcp.gatewayIP = ""; |
319 | + dhcp.gatewayPlaceholder = ""; |
320 | } |
321 | } else { |
322 | // Don't need to add a dynamic range, so ensure these fields |
323 | |
324 | === modified file 'src/maasserver/static/partials/vlan-details.html' |
325 | --- src/maasserver/static/partials/vlan-details.html 2016-04-11 13:52:21 +0000 |
326 | +++ src/maasserver/static/partials/vlan-details.html 2016-04-13 03:03:16 +0000 |
327 | @@ -78,7 +78,7 @@ |
328 | name="start-ip" |
329 | size="39" |
330 | class="three-col" |
331 | - placeholder="192.168.0.1" |
332 | + data-ng-placeholder="vlanDetails.provideDHCPAction.startPlaceholder" |
333 | data-ng-model="vlanDetails.provideDHCPAction.startIP" |
334 | data-ng-disabled="!vlanDetails.provideDHCPAction.subnet" |
335 | data-ng-change="vlanDetails.updateStartIP()"> |
336 | @@ -89,7 +89,7 @@ |
337 | name="end-ip" |
338 | size="39" |
339 | class="three-col" |
340 | - placeholder="192.168.0.10" |
341 | + data-ng-placeholder="vlanDetails.provideDHCPAction.endPlaceholder" |
342 | data-ng-model="vlanDetails.provideDHCPAction.endIP" |
343 | data-ng-disabled="!vlanDetails.provideDHCPAction.subnet" |
344 | data-ng-change="vlanDetails.updateEndIP()"> |
345 | @@ -101,7 +101,7 @@ |
346 | name="gateway-ip" |
347 | size="39" |
348 | class="three-col" |
349 | - placeholder="0.0.0.0" |
350 | + data-ng-placeholder="vlanDetails.provideDHCPAction.gatewayPlaceholder" |
351 | data-ng-model="vlanDetails.provideDHCPAction.gatewayIP" |
352 | data-ng-disabled="!vlanDetails.provideDHCPAction.subnet" |
353 | data-ng-change="vlanDetails.updatendIP()"> |
354 | |
355 | === modified file 'src/maasserver/testing/factory.py' |
356 | --- src/maasserver/testing/factory.py 2016-04-11 20:42:12 +0000 |
357 | +++ src/maasserver/testing/factory.py 2016-04-13 03:03:16 +0000 |
358 | @@ -743,7 +743,7 @@ |
359 | |
360 | def make_Subnet(self, name=None, vlan=None, space=None, cidr=None, |
361 | gateway_ip=None, dns_servers=None, host_bits=None, |
362 | - fabric=None, vid=None, dhcp_on=False, |
363 | + fabric=None, vid=None, dhcp_on=False, version=None, |
364 | rdns_mode=RDNS_MODE.DEFAULT, allow_proxy=True): |
365 | if name is None: |
366 | name = factory.make_name('name') |
367 | @@ -753,7 +753,8 @@ |
368 | space = factory.make_Space() |
369 | network = None |
370 | if cidr is None: |
371 | - network = factory.make_ip4_or_6_network(host_bits=host_bits) |
372 | + network = factory.make_ip4_or_6_network( |
373 | + version=version, host_bits=host_bits) |
374 | cidr = str(network.cidr) |
375 | if gateway_ip is None: |
376 | network = IPNetwork(cidr) if network is None else network |
377 | |
378 | === modified file 'src/maasserver/websockets/handlers/subnet.py' |
379 | --- src/maasserver/websockets/handlers/subnet.py 2016-03-28 13:54:47 +0000 |
380 | +++ src/maasserver/websockets/handlers/subnet.py 2016-04-13 03:03:16 +0000 |
381 | @@ -39,7 +39,8 @@ |
382 | def dehydrate(self, subnet, data, for_list=False): |
383 | full_range = subnet.get_iprange_usage() |
384 | metadata = IPRangeStatistics(full_range) |
385 | - data['statistics'] = metadata.render_json(include_ranges=True) |
386 | + data['statistics'] = metadata.render_json( |
387 | + include_ranges=True, include_suggestions=True) |
388 | data['version'] = IPNetwork(subnet.cidr).version |
389 | if not for_list: |
390 | data["ip_addresses"] = subnet.render_json_for_related_ips( |
391 | |
392 | === modified file 'src/maasserver/websockets/handlers/tests/test_subnet.py' |
393 | --- src/maasserver/websockets/handlers/tests/test_subnet.py 2016-03-28 13:54:47 +0000 |
394 | +++ src/maasserver/websockets/handlers/tests/test_subnet.py 2016-04-13 03:03:16 +0000 |
395 | @@ -33,7 +33,8 @@ |
396 | } |
397 | full_range = subnet.get_iprange_usage() |
398 | metadata = IPRangeStatistics(full_range) |
399 | - data['statistics'] = metadata.render_json(include_ranges=True) |
400 | + data['statistics'] = metadata.render_json( |
401 | + include_ranges=True, include_suggestions=True) |
402 | data['version'] = IPNetwork(subnet.cidr).version |
403 | if not for_list: |
404 | data["ip_addresses"] = subnet.render_json_for_related_ips( |
405 | |
406 | === modified file 'src/maastesting/factory.py' |
407 | --- src/maastesting/factory.py 2016-03-28 13:54:47 +0000 |
408 | +++ src/maastesting/factory.py 2016-04-13 03:03:16 +0000 |
409 | @@ -273,10 +273,12 @@ |
410 | slash=slash, but_not=but_not, disjoint_from=disjoint_from, |
411 | random_address_factory=self.make_ipv6_address) |
412 | |
413 | - def make_ip4_or_6_network(self, host_bits=None): |
414 | + def make_ip4_or_6_network(self, version=None, host_bits=None): |
415 | """Generate a random IPv4 or IPv6 network.""" |
416 | slash = None |
417 | - if random.randint(0, 1) == 0: |
418 | + if version is None: |
419 | + version = random.choice([4, 6]) |
420 | + if version == 4: |
421 | if host_bits is not None: |
422 | slash = 32 - host_bits |
423 | return self.make_ipv4_network(slash=slash) |
424 | |
425 | === modified file 'src/provisioningserver/utils/network.py' |
426 | --- src/provisioningserver/utils/network.py 2016-03-28 13:54:47 +0000 |
427 | +++ src/provisioningserver/utils/network.py 2016-04-13 03:03:16 +0000 |
428 | @@ -54,6 +54,14 @@ |
429 | ] |
430 | |
431 | |
432 | +class IPRANGE_TYPE: |
433 | + """Well-known purpose types for IP ranges.""" |
434 | + UNUSED = 'unused' |
435 | + GATEWAY_IP = 'gateway-ip' |
436 | + DYNAMIC = 'dynamic' |
437 | + PROPOSED_DYNAMIC = 'proposed-dynamic' |
438 | + |
439 | + |
440 | class MAASIPRange(IPRange): |
441 | """IPRange object whose default end address is the start address if not |
442 | specified. Capable of storing a string to indicate the purpose of |
443 | @@ -144,17 +152,103 @@ |
444 | to cover every possible IP address present in the desired range.""" |
445 | def __init__(self, full_maasipset): |
446 | self.ranges = full_maasipset |
447 | + self.first_address_value = self.ranges.first |
448 | + self.last_address_value = self.ranges.last |
449 | + self.ip_version = IPAddress(self.ranges.last).version |
450 | + self.first_address = str(IPAddress(self.first_address_value)) |
451 | + self.last_address = str(IPAddress(self.last_address_value)) |
452 | self.num_available = 0 |
453 | self.num_unavailable = 0 |
454 | self.largest_available = 0 |
455 | + self.suggested_gateway = None |
456 | + self.suggested_dynamic_range = None |
457 | for range in full_maasipset.ranges: |
458 | - if 'unused' in range.purpose: |
459 | + if IPRANGE_TYPE.UNUSED in range.purpose: |
460 | self.num_available += range.num_addresses |
461 | if range.num_addresses > self.largest_available: |
462 | self.largest_available = range.num_addresses |
463 | else: |
464 | self.num_unavailable += range.num_addresses |
465 | self.total_addresses = self.num_available + self.num_unavailable |
466 | + if not self.ranges.includes_purpose(IPRANGE_TYPE.GATEWAY_IP): |
467 | + self.suggested_gateway = self.get_recommended_gateway() |
468 | + if not self.ranges.includes_purpose(IPRANGE_TYPE.DYNAMIC): |
469 | + self.suggested_dynamic_range = self.get_recommended_dynamic_range() |
470 | + |
471 | + def get_recommended_gateway(self): |
472 | + """Returns a suggested gateway for the set of ranges in `self.ranges`. |
473 | + Will attempt to choose the first IP address available, then the last IP |
474 | + address available, then the first IP address in the first unused range, |
475 | + in that order of preference. |
476 | + |
477 | + Must be called after the range usage has been calculated. |
478 | + """ |
479 | + suggested_gateway = None |
480 | + first_address = self.first_address_value |
481 | + last_address = self.last_address_value |
482 | + if self.ip_version == 6: |
483 | + # For IPv6 addresses, always return the subnet-router anycast |
484 | + # address. (See RFC 4291 section 2.6.1 for more information.) |
485 | + return str(IPAddress(first_address - 1)) |
486 | + if self.ranges.is_unused(first_address): |
487 | + suggested_gateway = str(IPAddress(first_address)) |
488 | + elif self.ranges.is_unused(last_address): |
489 | + suggested_gateway = str(IPAddress(last_address)) |
490 | + else: |
491 | + first_unused = self.ranges.get_first_unused_ip() |
492 | + if first_unused is not None: |
493 | + suggested_gateway = str(IPAddress(first_unused)) |
494 | + return suggested_gateway |
495 | + |
496 | + def get_recommended_dynamic_range(self): |
497 | + """Returns a recommended dynamic range for the set of ranges in |
498 | + `self.ranges`, or None if one could not be found. |
499 | + |
500 | + Must be called after the recommended gateway is selected, the |
501 | + range usage has been calculated, and the number of total and available |
502 | + addresses have been determined. |
503 | + """ |
504 | + largest_unused = self.ranges.get_largest_unused_block() |
505 | + if largest_unused is None: |
506 | + return None |
507 | + if self.suggested_gateway is not None and largest_unused.size == 1: |
508 | + # Can't suggest a range if we're also suggesting the only available |
509 | + # IP address as the gateway. |
510 | + return None |
511 | + candidate = MAASIPRange( |
512 | + largest_unused.first, largest_unused.last, |
513 | + purpose=IPRANGE_TYPE.PROPOSED_DYNAMIC) |
514 | + # Adjust the largest unused block if it contains the suggested gateway. |
515 | + if self.suggested_gateway is not None: |
516 | + gateway_value = IPAddress(self.suggested_gateway).value |
517 | + if gateway_value in candidate: |
518 | + # The suggested gateway is going to be either the first |
519 | + # or the last IP address in the range. |
520 | + if gateway_value == candidate.first: |
521 | + candidate = MAASIPRange( |
522 | + candidate.first + 1, candidate.last, |
523 | + purpose=IPRANGE_TYPE.PROPOSED_DYNAMIC) |
524 | + else: |
525 | + # Must be the last address. |
526 | + candidate = MAASIPRange( |
527 | + candidate.first, candidate.last - 1, |
528 | + purpose=IPRANGE_TYPE.PROPOSED_DYNAMIC) |
529 | + if candidate is not None: |
530 | + one_fourth_range = self.total_addresses >> 2 |
531 | + half_remaining_space = self.num_available >> 1 |
532 | + if candidate.size > one_fourth_range: |
533 | + # Prevent the proposed range from taking up too much available |
534 | + # space in the subnet. |
535 | + candidate = MAASIPRange( |
536 | + candidate.last - one_fourth_range, candidate.last, |
537 | + purpose=IPRANGE_TYPE.PROPOSED_DYNAMIC) |
538 | + elif candidate.size >= half_remaining_space: |
539 | + # Prevent the proposed range from taking up the remainder of |
540 | + # the available IP addresses. (take at most half.) |
541 | + candidate = MAASIPRange( |
542 | + candidate.last - half_remaining_space + 1, candidate.last, |
543 | + purpose=IPRANGE_TYPE.PROPOSED_DYNAMIC) |
544 | + return candidate |
545 | |
546 | @property |
547 | def available_percentage(self): |
548 | @@ -180,7 +274,7 @@ |
549 | :return:unicode""" |
550 | return "{0:.0%}".format(self.usage_percentage) |
551 | |
552 | - def render_json(self, include_ranges=False): |
553 | + def render_json(self, include_ranges=False, include_suggestions=False): |
554 | """Returns a representation of the statistics suitable for rendering |
555 | into JSON format.""" |
556 | data = { |
557 | @@ -191,9 +285,17 @@ |
558 | "usage": self.usage_percentage, |
559 | "usage_string": self.usage_percentage_string, |
560 | "available_string": self.available_percentage_string, |
561 | + "first_address": self.first_address, |
562 | + "last_address": self.last_address, |
563 | + "ip_version": self.ip_version |
564 | } |
565 | if include_ranges: |
566 | data["ranges"] = self.ranges.render_json() |
567 | + if include_suggestions: |
568 | + data["suggested_gateway"] = self.suggested_gateway |
569 | + data["suggested_dynamic_range"] = ( |
570 | + self.suggested_dynamic_range.render_json() |
571 | + ) |
572 | return data |
573 | |
574 | |
575 | @@ -224,6 +326,64 @@ |
576 | return item |
577 | return None |
578 | |
579 | + @property |
580 | + def first(self): |
581 | + """Returns the first IP address in this set.""" |
582 | + return self.ranges[0].first |
583 | + |
584 | + @property |
585 | + def last(self): |
586 | + """Returns the last IP address in this set.""" |
587 | + return self.ranges[-1].last |
588 | + |
589 | + def ip_has_purpose(self, ip, purpose): |
590 | + """Returns True if the specified IP address has the specified purpose |
591 | + in this set; False otherwise. |
592 | + """ |
593 | + range = self.find(ip) |
594 | + if range is None: |
595 | + raise ValueError( |
596 | + "IP address %s does not exist in range (%s-%s)." % ( |
597 | + ip, self.first, self.last)) |
598 | + return purpose in range.purpose |
599 | + |
600 | + def is_unused(self, ip): |
601 | + """Returns True if the specified IP address (which must be within the |
602 | + ranges in this set) is unused; False otherwise.""" |
603 | + return self.ip_has_purpose(ip, IPRANGE_TYPE.UNUSED) |
604 | + |
605 | + def includes_purpose(self, purpose): |
606 | + """Returns True if the specified purpose is found inside any of the |
607 | + ranges in this set, otherwise returns False""" |
608 | + for item in self.ranges: |
609 | + if purpose in item.purpose: |
610 | + return True |
611 | + return False |
612 | + |
613 | + def get_first_unused_ip(self): |
614 | + """Returns the integer value of the first unused IP address in the set. |
615 | + """ |
616 | + for item in self.ranges: |
617 | + if IPRANGE_TYPE.UNUSED in item.purpose: |
618 | + return item.first |
619 | + return None |
620 | + |
621 | + def get_largest_unused_block(self): |
622 | + """Find the largest unused block of addresses in this set.""" |
623 | + class NullIPRange: |
624 | + """Throwaway class to represent an empty IP range.""" |
625 | + def __init__(self): |
626 | + self.size = 0 |
627 | + |
628 | + largest = NullIPRange() |
629 | + for item in self.ranges: |
630 | + if IPRANGE_TYPE.UNUSED in item.purpose: |
631 | + if item.size >= largest.size: |
632 | + largest = item |
633 | + if largest.size == 0: |
634 | + return None |
635 | + return largest |
636 | + |
637 | def render_json(self, *args, **kwargs): |
638 | return [ |
639 | iprange.render_json(*args, **kwargs) |
640 | @@ -236,7 +396,7 @@ |
641 | def __contains__(self, item): |
642 | return bool(self.find(item)) |
643 | |
644 | - def get_unused_ranges(self, outer_range, comment="unused"): |
645 | + def get_unused_ranges(self, outer_range, comment=IPRANGE_TYPE.UNUSED): |
646 | """Calculates and returns a list of unused IP ranges, based on |
647 | the supplied range of desired addresses. |
648 | |
649 | |
650 | === modified file 'src/provisioningserver/utils/tests/test_network.py' |
651 | --- src/provisioningserver/utils/tests/test_network.py 2016-03-28 13:54:47 +0000 |
652 | +++ src/provisioningserver/utils/tests/test_network.py 2016-04-13 03:03:16 +0000 |
653 | @@ -51,6 +51,7 @@ |
654 | from testtools.matchers import ( |
655 | Contains, |
656 | Equals, |
657 | + HasLength, |
658 | Is, |
659 | MatchesDict, |
660 | MatchesSetwise, |
661 | @@ -788,6 +789,130 @@ |
662 | self.assertThat(json['available_string'], Equals("100%")) |
663 | self.assertThat(json, Not(Contains("ranges"))) |
664 | |
665 | + def test__suggests_subnet_anycast_address_for_ipv6(self): |
666 | + s = MAASIPSet([]) |
667 | + u = s.get_full_range('2001:db8::/64') |
668 | + stats = IPRangeStatistics(u) |
669 | + self.assertThat(stats.suggested_gateway, Equals("2001:db8::")) |
670 | + |
671 | + def test__suggests_first_ip_as_default_gateway_if_available(self): |
672 | + s = MAASIPSet(['10.0.0.2', '10.0.0.4', '10.0.0.6', '10.0.0.8']) |
673 | + u = s.get_full_range('10.0.0.0/24') |
674 | + stats = IPRangeStatistics(u) |
675 | + self.assertThat(stats.suggested_gateway, Equals("10.0.0.1")) |
676 | + |
677 | + def test__suggests_last_ip_as_default_gateway_if_needed(self): |
678 | + s = MAASIPSet(['10.0.0.1', '10.0.0.4', '10.0.0.6', '10.0.0.8']) |
679 | + u = s.get_full_range('10.0.0.0/24') |
680 | + stats = IPRangeStatistics(u) |
681 | + self.assertThat(stats.suggested_gateway, Equals("10.0.0.254")) |
682 | + |
683 | + def test__suggests_first_available_ip_as_default_gateway_if_needed(self): |
684 | + s = MAASIPSet(['10.0.0.1', '10.0.0.4', '10.0.0.6', '10.0.0.254']) |
685 | + u = s.get_full_range('10.0.0.0/24') |
686 | + stats = IPRangeStatistics(u) |
687 | + self.assertThat(stats.suggested_gateway, Equals("10.0.0.2")) |
688 | + |
689 | + def test__suggests_no_gateway_if_range_full(self): |
690 | + s = MAASIPSet(['10.0.0.1']) |
691 | + u = s.get_full_range('10.0.0.1/32') |
692 | + stats = IPRangeStatistics(u) |
693 | + self.assertThat(stats.suggested_gateway, Is(None)) |
694 | + |
695 | + def test__suggests_upper_one_fourth_range_for_dynamic_by_default(self): |
696 | + s = MAASIPSet([]) |
697 | + u = s.get_full_range('10.0.0.0/24') |
698 | + stats = IPRangeStatistics(u) |
699 | + self.assertThat(stats.suggested_gateway, Equals("10.0.0.1")) |
700 | + self.assertThat(stats.suggested_dynamic_range, HasLength(64)) |
701 | + self.assertThat(stats.suggested_dynamic_range, Contains("10.0.0.191")) |
702 | + self.assertThat(stats.suggested_dynamic_range, Contains("10.0.0.254")) |
703 | + self.assertThat( |
704 | + stats.suggested_dynamic_range, Not(Contains("10.0.0.255"))) |
705 | + self.assertThat( |
706 | + stats.suggested_dynamic_range, Not(Contains("10.0.0.190"))) |
707 | + |
708 | + def test__suggests_half_available_if_available_less_than_one_fourth(self): |
709 | + s = MAASIPSet([MAASIPRange("10.0.0.2", "10.0.0.205")]) |
710 | + u = s.get_full_range('10.0.0.0/24') |
711 | + stats = IPRangeStatistics(u) |
712 | + self.assertThat(stats.suggested_gateway, Equals("10.0.0.1")) |
713 | + self.assertThat(stats.num_available, Equals(50)) |
714 | + self.assertThat(stats.suggested_dynamic_range, HasLength(25)) |
715 | + self.assertThat(stats.suggested_dynamic_range, Contains("10.0.0.230")) |
716 | + self.assertThat(stats.suggested_dynamic_range, Contains("10.0.0.254")) |
717 | + self.assertThat( |
718 | + stats.suggested_dynamic_range, Not(Contains("10.0.0.255"))) |
719 | + self.assertThat( |
720 | + stats.suggested_dynamic_range, Not(Contains("10.0.0.229"))) |
721 | + |
722 | + def test__suggested_range_excludes_suggested_gateway(self): |
723 | + s = MAASIPSet([MAASIPRange("10.0.0.1", "10.0.0.204")]) |
724 | + u = s.get_full_range('10.0.0.0/24') |
725 | + stats = IPRangeStatistics(u) |
726 | + self.assertThat(stats.suggested_gateway, Equals("10.0.0.254")) |
727 | + self.assertThat(stats.num_available, Equals(50)) |
728 | + self.assertThat(stats.suggested_dynamic_range, HasLength(25)) |
729 | + self.assertThat(stats.suggested_dynamic_range, Contains("10.0.0.229")) |
730 | + self.assertThat(stats.suggested_dynamic_range, Contains("10.0.0.253")) |
731 | + self.assertThat( |
732 | + stats.suggested_dynamic_range, Not(Contains("10.0.0.255"))) |
733 | + self.assertThat( |
734 | + stats.suggested_dynamic_range, Not(Contains("10.0.0.228"))) |
735 | + |
736 | + def test__suggested_range_excludes_suggested_gateway_when_gw_first(self): |
737 | + s = MAASIPSet([MAASIPRange("10.0.0.1", "10.0.0.203"), "10.0.0.254"]) |
738 | + u = s.get_full_range('10.0.0.0/24') |
739 | + stats = IPRangeStatistics(u) |
740 | + self.assertThat(stats.suggested_gateway, Equals("10.0.0.204")) |
741 | + self.assertThat(stats.num_available, Equals(50)) |
742 | + self.assertThat(stats.suggested_dynamic_range, HasLength(25)) |
743 | + self.assertThat(stats.suggested_dynamic_range, Contains("10.0.0.229")) |
744 | + self.assertThat(stats.suggested_dynamic_range, Contains("10.0.0.253")) |
745 | + self.assertThat( |
746 | + stats.suggested_dynamic_range, Not(Contains("10.0.0.255"))) |
747 | + self.assertThat( |
748 | + stats.suggested_dynamic_range, Not(Contains("10.0.0.228"))) |
749 | + |
750 | + def test__suggests_upper_one_fourth_range_for_ipv6(self): |
751 | + s = MAASIPSet([]) |
752 | + u = s.get_full_range('2001:db8::/64') |
753 | + stats = IPRangeStatistics(u) |
754 | + self.assertThat(stats.suggested_gateway, Equals("2001:db8::")) |
755 | + self.assertThat( |
756 | + stats.suggested_dynamic_range, HasLength((2 ** 64) >> 2)) |
757 | + self.assertThat( |
758 | + stats.suggested_dynamic_range, Contains( |
759 | + "2001:db8:0:0:c000::")) |
760 | + self.assertThat( |
761 | + stats.suggested_dynamic_range, Contains( |
762 | + "2001:db8::ffff:ffff:ffff:ffff")) |
763 | + self.assertThat( |
764 | + stats.suggested_dynamic_range, Not(Contains("2001:db8::1"))) |
765 | + self.assertThat( |
766 | + stats.suggested_dynamic_range, Not(Contains( |
767 | + "2001:db8::bfff:ffff:ffff:ffff"))) |
768 | + |
769 | + def test__suggests_half_available_for_ipv6(self): |
770 | + s = MAASIPSet([MAASIPRange( |
771 | + "2001:db8::1", "2001:db8::ffff:ffff:ffff:ff00")]) |
772 | + u = s.get_full_range('2001:db8::/64') |
773 | + stats = IPRangeStatistics(u) |
774 | + self.assertThat(stats.suggested_gateway, Equals("2001:db8::")) |
775 | + self.assertThat(stats.num_available, Equals(255)) |
776 | + self.assertThat(stats.suggested_dynamic_range, HasLength(127)) |
777 | + self.assertThat( |
778 | + stats.suggested_dynamic_range, Contains( |
779 | + "2001:db8::ffff:ffff:ffff:ff81")) |
780 | + self.assertThat( |
781 | + stats.suggested_dynamic_range, Contains( |
782 | + "2001:db8::ffff:ffff:ffff:ffff")) |
783 | + self.assertThat( |
784 | + stats.suggested_dynamic_range, Not(Contains("2001:db8::1"))) |
785 | + self.assertThat( |
786 | + stats.suggested_dynamic_range, Not(Contains( |
787 | + "2001:db8::ffff:ffff:ffff:ff80"))) |
788 | + |
789 | |
790 | class TestParseInteger(MAASTestCase): |
791 |
Looks really good. Only issue is you should be used data-ng-placeholder instead of placing a template into the placeholder attribute. data-ng-placeholder is a directive that MAAS adds into angular.