Merge ~blr/maas:compose-kvm-networking into maas:master
- Git
- lp:~blr/maas
- compose-kvm-networking
- Merge into master
Status: | Merged |
---|---|
Approved by: | Kit Randel |
Approved revision: | 3d947a4636340a391ab3b4d9079a7906349f8cd0 |
Merge reported by: | MAAS Lander |
Merged at revision: | not available |
Proposed branch: | ~blr/maas:compose-kvm-networking |
Merge into: | maas:master |
Diff against target: |
532 lines (+344/-12) 4 files modified
src/maasserver/static/js/angular/controllers/pod_details.js (+75/-7) src/maasserver/static/js/angular/controllers/tests/test_pod_details.js (+138/-5) src/maasserver/static/partials/pod-details.html (+87/-0) src/maasserver/static/scss/_tables.scss (+44/-0) |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Mike Pontillo (community) | Approve | ||
Newell Jensen (community) | Approve | ||
Review via email: mp+354846@code.launchpad.net |
Commit message
Add KVM pod interfaces by subnet.
Description of the change
The first in a series of branches to implement this feature. To come in future branches: spaces, multicolumn dropdown (as per Martin's designs), PXE indicator, manual IP allocation.
screenshot: https:/
Mike Pontillo (mpontillo) wrote : | # |
This looks like a great start! I was looking at this branch a little bit last week, but I just re-tested the latest version (thanks for the fixes).
About the default interface names: en* is more of an Apple convention (likely originating from BSD). Linux users would probably be more at home if we use "eth*" for interface names.
MAAS Lander (maas-lander) wrote : | # |
LANDING
-b compose-
STATUS: FAILED BUILD
LOG: http://
- 3d947a4... by Kit Randel
-
Testfix.
Preview Diff
1 | diff --git a/src/maasserver/static/js/angular/controllers/pod_details.js b/src/maasserver/static/js/angular/controllers/pod_details.js |
2 | index 385d4a3..961c49d 100644 |
3 | --- a/src/maasserver/static/js/angular/controllers/pod_details.js |
4 | +++ b/src/maasserver/static/js/angular/controllers/pod_details.js |
5 | @@ -8,12 +8,15 @@ angular.module('MAAS').controller('PodDetailsController', [ |
6 | '$scope', '$rootScope', '$location', '$routeParams', |
7 | 'PodsManager', 'GeneralManager', 'UsersManager', 'DomainsManager', |
8 | 'ZonesManager', 'MachinesManager', 'ManagerHelperService', 'ErrorService', |
9 | - 'ResourcePoolsManager', 'ValidationService', |
10 | + 'ResourcePoolsManager', 'SubnetsManager', 'VLANsManager', 'FabricsManager', |
11 | + 'ValidationService', |
12 | + |
13 | function( |
14 | $scope, $rootScope, $location, $routeParams, |
15 | PodsManager, GeneralManager, UsersManager, DomainsManager, |
16 | ZonesManager, MachinesManager, ManagerHelperService, ErrorService, |
17 | - ResourcePoolsManager, ValidationService) { |
18 | + ResourcePoolsManager, SubnetsManager, VLANsManager, FabricsManager, |
19 | + ValidationService) { |
20 | |
21 | // Set title and page. |
22 | $rootScope.title = "Loading..."; |
23 | @@ -42,6 +45,9 @@ angular.module('MAAS').controller('PodDetailsController', [ |
24 | inProgress: false, |
25 | error: null |
26 | }; |
27 | + $scope.defaultInterface = { |
28 | + name: 'default' |
29 | + }; |
30 | $scope.compose = { |
31 | action: { |
32 | name: 'compose', |
33 | @@ -55,9 +61,11 @@ angular.module('MAAS').controller('PodDetailsController', [ |
34 | tags: [], |
35 | pool: {}, |
36 | boot: true |
37 | - }] |
38 | + }], |
39 | + interfaces: [$scope.defaultInterface] |
40 | } |
41 | }; |
42 | + $scope.subnets = SubnetsManager.getItems(); |
43 | $scope.power_types = GeneralManager.getData("power_types"); |
44 | $scope.domains = DomainsManager.getItems(); |
45 | $scope.zones = ZonesManager.getItems(); |
46 | @@ -284,6 +292,18 @@ angular.module('MAAS').controller('PodDetailsController', [ |
47 | storage.push(constraint); |
48 | }); |
49 | params.storage = storage.join(','); |
50 | + |
51 | + // Create the interface constraint. |
52 | + // <interface-name>:<key>=<value>[,<key>=<value>];... |
53 | + var interfaces = []; |
54 | + angular.forEach($scope.compose.obj.interfaces, function(iface) { |
55 | + if (iface.subnet) { |
56 | + var row = `${iface.name}:subnet=${iface.subnet.cidr}` |
57 | + interfaces.push(row); |
58 | + } |
59 | + }); |
60 | + params.interfaces = interfaces.join(';'); |
61 | + |
62 | return params; |
63 | }; |
64 | |
65 | @@ -307,7 +327,8 @@ angular.module('MAAS').controller('PodDetailsController', [ |
66 | tags: [], |
67 | pool: {}, |
68 | boot: true |
69 | - }] |
70 | + }], |
71 | + interfaces: [$scope.defaultInterface] |
72 | }; |
73 | $scope.action.option = null; |
74 | }; |
75 | @@ -339,7 +360,6 @@ angular.module('MAAS').controller('PodDetailsController', [ |
76 | // Get the default pod pool |
77 | $scope.getDefaultStoragePool = function () { |
78 | var defaultPool = {}; |
79 | - console.log($scope); |
80 | if($scope.pod.storage_pools && $scope.pod.default_storage_pool) { |
81 | defaultPool = $scope.pod.storage_pools.filter( |
82 | pool => pool.id == $scope.pod.default_storage_pool |
83 | @@ -356,6 +376,53 @@ angular.module('MAAS').controller('PodDetailsController', [ |
84 | } |
85 | }; |
86 | |
87 | + // Add interfaces. |
88 | + $scope.composeAddInterface = function() { |
89 | + |
90 | + // Set displayName for subnets |
91 | + angular.forEach($scope.subnets, function(subnet, idx) { |
92 | + if (subnet.name === subnet.cidr) { |
93 | + $scope.subnets[idx].displayName = subnet.cidr; |
94 | + } else { |
95 | + let name = `${subnet.cidr} (${subnet.name})`; |
96 | + $scope.subnets[idx].displayName = name; |
97 | + } |
98 | + }); |
99 | + |
100 | + // Remove default auto-assigned interface when |
101 | + // adding custom interfaces |
102 | + let defaultIdx = $scope.compose.obj.interfaces.indexOf( |
103 | + $scope.defaultInterface); |
104 | + if (defaultIdx >= 0) { |
105 | + $scope.compose.obj.interfaces.splice(defaultIdx, 1); |
106 | + } |
107 | + var iface = { |
108 | + name: `eth${$scope.compose.obj.interfaces.length}` |
109 | + }; |
110 | + $scope.compose.obj.interfaces.push(iface); |
111 | + }; |
112 | + |
113 | + $scope.setFabricAndVlan = function(iface) { |
114 | + const idx = $scope.compose.obj.interfaces.indexOf(iface); |
115 | + const vlan = VLANsManager.getItemFromList(iface.subnet.vlan); |
116 | + $scope.compose.obj.interfaces[idx].vlan = vlan; |
117 | + $scope.compose.obj.interfaces[idx].fabric = |
118 | + FabricsManager.getItemFromList(vlan.fabric); |
119 | + } |
120 | + |
121 | + // Remove an interface from interfaces config. |
122 | + $scope.composeRemoveInterface = function(iface) { |
123 | + var idx = $scope.compose.obj.interfaces.indexOf(iface); |
124 | + if(idx >= 0) { |
125 | + $scope.compose.obj.interfaces.splice(idx, 1); |
126 | + } |
127 | + |
128 | + // Re-add default interface if all custom interfaces removed |
129 | + if ($scope.compose.obj.interfaces.length == 0) { |
130 | + $scope.compose.obj.interfaces.push($scope.defaultInterface); |
131 | + } |
132 | + }; |
133 | + |
134 | // Start watching key fields. |
135 | $scope.startWatching = function() { |
136 | $scope.$watch("pod.name", function() { |
137 | @@ -396,7 +463,8 @@ angular.module('MAAS').controller('PodDetailsController', [ |
138 | ManagerHelperService.loadManagers($scope, [ |
139 | PodsManager, GeneralManager, UsersManager, |
140 | DomainsManager, ZonesManager, MachinesManager, |
141 | - ResourcePoolsManager]).then(function() { |
142 | + ResourcePoolsManager, SubnetsManager, VLANsManager, |
143 | + FabricsManager]).then(function() { |
144 | |
145 | // Possibly redirected from another controller that already had |
146 | // this pod set to active. Only call setActiveItem if not already |
147 | @@ -424,4 +492,4 @@ angular.module('MAAS').controller('PodDetailsController', [ |
148 | }); |
149 | } |
150 | }); |
151 | - }]); |
152 | \ No newline at end of file |
153 | + }]); |
154 | diff --git a/src/maasserver/static/js/angular/controllers/tests/test_pod_details.js b/src/maasserver/static/js/angular/controllers/tests/test_pod_details.js |
155 | index 2fde19b..0e1df71 100644 |
156 | --- a/src/maasserver/static/js/angular/controllers/tests/test_pod_details.js |
157 | +++ b/src/maasserver/static/js/angular/controllers/tests/test_pod_details.js |
158 | @@ -36,6 +36,7 @@ describe("PodDetailsController", function() { |
159 | // Load the required managers. |
160 | var PodsManager, UsersManager, GeneralManager, DomainsManager; |
161 | var ZonesManager, ManagerHelperService, ErrorService; |
162 | + var SubnetsManager, VLANsManager, FabricsManager; |
163 | var ResourcePoolsManager; |
164 | beforeEach(inject(function($injector) { |
165 | PodsManager = $injector.get("PodsManager"); |
166 | @@ -46,6 +47,9 @@ describe("PodDetailsController", function() { |
167 | MachinesManager = $injector.get("MachinesManager"); |
168 | ManagerHelperService = $injector.get("ManagerHelperService"); |
169 | ErrorService = $injector.get("ErrorService"); |
170 | + SubnetsManager = $injector.get("SubnetsManager"); |
171 | + VLANsManager = $injector.get("VLANsManager"); |
172 | + FabricsManager = $injector.get("FabricsManager"); |
173 | ResourcePoolsManager = $injector.get("ResourcePoolsManager"); |
174 | })); |
175 | |
176 | @@ -110,6 +114,9 @@ describe("PodDetailsController", function() { |
177 | MachinesManager: MachinesManager, |
178 | ManagerHelperService: ManagerHelperService, |
179 | ErrorService: ErrorService, |
180 | + SubnetsManager: SubnetsManager, |
181 | + VLANsManager: VLANsManager, |
182 | + FabricsManager: FabricsManager, |
183 | ResourcePoolsManager: ResourcePoolsManager |
184 | }); |
185 | |
186 | @@ -159,18 +166,23 @@ describe("PodDetailsController", function() { |
187 | tags: [], |
188 | pool: {}, |
189 | boot: true |
190 | + }], |
191 | + interfaces: [{ |
192 | + name: 'default' |
193 | }] |
194 | } |
195 | }); |
196 | expect($scope.power_types).toBe(GeneralManager.getData('power_types')); |
197 | expect($scope.domains).toBe(DomainsManager.getItems()); |
198 | expect($scope.zones).toBe(ZonesManager.getItems()); |
199 | + expect($scope.subnets).toBe(SubnetsManager.getItems()); |
200 | expect($scope.pools).toBe(ResourcePoolsManager.getItems()); |
201 | expect($scope.editing).toBe(false); |
202 | }); |
203 | |
204 | it("calls loadManagers with PodsManager, UsersManager, GeneralManager, \ |
205 | - DomainsManager, ZonesManager, MachinesManager", function() { |
206 | + DomainsManager, ZonesManager, SubnetsManager, VLANsManager, \ |
207 | + FabricsManager, MachinesManager", function() { |
208 | var controller = makeController(); |
209 | expect(ManagerHelperService.loadManagers).toHaveBeenCalledWith( |
210 | $scope, |
211 | @@ -181,7 +193,10 @@ describe("PodDetailsController", function() { |
212 | DomainsManager, |
213 | ZonesManager, |
214 | MachinesManager, |
215 | - ResourcePoolsManager |
216 | + ResourcePoolsManager, |
217 | + SubnetsManager, |
218 | + VLANsManager, |
219 | + FabricsManager |
220 | ]); |
221 | }); |
222 | |
223 | @@ -603,7 +618,8 @@ describe("PodDetailsController", function() { |
224 | $scope.pod.type = 'rsd'; |
225 | expect($scope.composePreProcess({})).toEqual({ |
226 | id: $scope.pod.id, |
227 | - storage: '0:8(local)' |
228 | + storage: '0:8(local)', |
229 | + interfaces: '' |
230 | }); |
231 | }); |
232 | |
233 | @@ -644,7 +660,27 @@ describe("PodDetailsController", function() { |
234 | id: $scope.pod.id, |
235 | storage: ( |
236 | '0:50(local,happy,days),' + |
237 | - '1:20(iscsi,one,two),2:60(local,other)') |
238 | + '1:20(iscsi,one,two),2:60(local,other)'), |
239 | + interfaces: '' |
240 | + }); |
241 | + }); |
242 | + |
243 | + it("sets interface based on compose.obj.iterfaces", function() { |
244 | + var controller = makeControllerResolveSetActiveItem(); |
245 | + $scope.compose.obj.interfaces = [ |
246 | + { |
247 | + name: 'eth0', |
248 | + subnet: {cidr: '172.16.4.0/24'} |
249 | + }, |
250 | + { |
251 | + name: 'eth1', |
252 | + subnet: {cidr: '192.168.1.0/24'} |
253 | + } |
254 | + ]; |
255 | + expect($scope.composePreProcess({})).toEqual({ |
256 | + id: $scope.pod.id, |
257 | + storage: '0:8()', |
258 | + interfaces: 'eth0:subnet=172.16.4.0/24;eth1:subnet=192.168.1.0/24' |
259 | }); |
260 | }); |
261 | |
262 | @@ -691,7 +727,56 @@ describe("PodDetailsController", function() { |
263 | id: $scope.pod.id, |
264 | storage: ( |
265 | '0:50(pool2,happy,days),' + |
266 | - '1:20(pool1,one,two),2:60(pool3,other)') |
267 | + '1:20(pool1,one,two),2:60(pool3,other)'), |
268 | + interfaces: '' |
269 | + }); |
270 | + }); |
271 | + |
272 | + it("sets virsh storage based on compose.obj.storage", function() { |
273 | + var controller = makeControllerResolveSetActiveItem(); |
274 | + $scope.pod.type = 'virsh'; |
275 | + $scope.compose.obj.storage = [ |
276 | + { |
277 | + size: 20, |
278 | + pool: { |
279 | + name: 'pool1' |
280 | + }, |
281 | + tags: [{ |
282 | + text: 'one' |
283 | + }, { |
284 | + text: 'two' |
285 | + }], |
286 | + boot: false |
287 | + }, |
288 | + { |
289 | + size: 50, |
290 | + pool: { |
291 | + name: 'pool2' |
292 | + }, |
293 | + tags: [{ |
294 | + text: 'happy' |
295 | + }, { |
296 | + text: 'days' |
297 | + }], |
298 | + boot: true |
299 | + }, |
300 | + { |
301 | + size: 60, |
302 | + pool: { |
303 | + name: 'pool3' |
304 | + }, |
305 | + tags: [{ |
306 | + text: 'other' |
307 | + }], |
308 | + boot: false |
309 | + } |
310 | + ]; |
311 | + expect($scope.composePreProcess({})).toEqual({ |
312 | + id: $scope.pod.id, |
313 | + storage: ( |
314 | + '0:50(pool2,happy,days),' + |
315 | + '1:20(pool1,one,two),2:60(pool3,other)'), |
316 | + interfaces: '' |
317 | }); |
318 | }); |
319 | }); |
320 | @@ -712,6 +797,9 @@ describe("PodDetailsController", function() { |
321 | tags: [], |
322 | pool: {}, |
323 | boot: true |
324 | + }], |
325 | + interfaces: [{ |
326 | + name: 'default' |
327 | }] |
328 | }); |
329 | expect($scope.action.option).toBeNull(); |
330 | @@ -776,4 +864,49 @@ describe("PodDetailsController", function() { |
331 | expect($scope.compose.obj.storage.indexOf(deleteStorage)).toBe(-1); |
332 | }); |
333 | }); |
334 | + |
335 | + describe("composeAddInterface", function() { |
336 | + |
337 | + it("adds a new interface item and removes the default", function() { |
338 | + var controller = makeControllerResolveSetActiveItem(); |
339 | + expect($scope.compose.obj.interfaces.length).toBe(1); |
340 | + expect($scope.compose.obj.interfaces[0]).toEqual({ |
341 | + name: 'default' |
342 | + }); |
343 | + $scope.composeAddInterface(); |
344 | + expect($scope.compose.obj.interfaces.length).toBe(1); |
345 | + expect($scope.compose.obj.interfaces[0]).toEqual({ |
346 | + name: 'eth0' |
347 | + }); |
348 | + }); |
349 | + |
350 | + it("increments the default interface name", function() { |
351 | + var controller = makeControllerResolveSetActiveItem(); |
352 | + $scope.composeAddInterface(); |
353 | + $scope.composeAddInterface(); |
354 | + expect($scope.compose.obj.interfaces[0]).toEqual({ |
355 | + name: 'eth0' |
356 | + }); |
357 | + expect($scope.compose.obj.interfaces[1]).toEqual({ |
358 | + name: 'eth1' |
359 | + }); |
360 | + }); |
361 | + }); |
362 | + |
363 | + describe("composeRemoveInterface", function() { |
364 | + |
365 | + it("removes interface from interfaces table", function() { |
366 | + var controller = makeControllerResolveSetActiveItem(); |
367 | + $scope.composeAddInterface(); |
368 | + $scope.composeAddInterface(); |
369 | + $scope.composeAddInterface(); |
370 | + var deletedIface = $scope.compose.obj.interfaces[3]; |
371 | + $scope.composeRemoveInterface(deletedIface); |
372 | + |
373 | + expect($scope.compose.obj.interfaces.indexOf( |
374 | + deletedIface)).toBe(-1); |
375 | + }); |
376 | + }); |
377 | + |
378 | + |
379 | }); |
380 | diff --git a/src/maasserver/static/partials/pod-details.html b/src/maasserver/static/partials/pod-details.html |
381 | index 1c6bad0..00ac64b 100644 |
382 | --- a/src/maasserver/static/partials/pod-details.html |
383 | +++ b/src/maasserver/static/partials/pod-details.html |
384 | @@ -63,6 +63,93 @@ |
385 | </div> |
386 | <div class="row"> |
387 | <div class="col-12"> |
388 | + <h3 class="p-heading--four">Interfaces</h3> |
389 | + <table class="p-table-expanding p-table--pod-networking-config"> |
390 | + <thead> |
391 | + <tr class="p-table__row"> |
392 | + <th></th> |
393 | + <th>Name</th> |
394 | + <th>IP Address</th> |
395 | + <th>Subnet</th> |
396 | + <th>Fabric</th> |
397 | + <th>VLAN</th> |
398 | + </tr> |
399 | + </thead> |
400 | + <tbody> |
401 | + <tr class="p-table__row" data-ng-repeat="iface in compose.obj.interfaces"> |
402 | + <td> |
403 | + <div data-ng-if="iface.name !== 'default'"> |
404 | + <button class="p-button--base" data-ng-click="composeRemoveInterface(iface)"> |
405 | + <i class="p-icon--close"></i> |
406 | + <span class="u-off-screen">Remove</span> |
407 | + </button> |
408 | + </div> |
409 | + </td> |
410 | + <td> |
411 | + <div ng-switch on="iface.name"> |
412 | + <div ng-switch-when="default"> |
413 | + default |
414 | + </div> |
415 | + <div ng-switch-default> |
416 | + <div class="form__group-input"> |
417 | + <input type="text" placeholder="Enter name" data-ng-model="iface.name"> |
418 | + </div> |
419 | + </div> |
420 | + </div> |
421 | + </td> |
422 | + <td> |
423 | + <div ng-switch on="iface.name"> |
424 | + <div ng-switch-when="default"> |
425 | + <span>Created by hypervisor at compose time.</span> |
426 | + </div> |
427 | + <div ng-switch-default> |
428 | + <div class="form__group-input"> |
429 | + <span>auto-assigned</span> |
430 | + </div> |
431 | + </div> |
432 | + </div> |
433 | + </td> |
434 | + <td> |
435 | + <div data-ng-if="iface.name !== 'default'" class="form__group-input"> |
436 | + <select |
437 | + data-ng-model="iface.subnet" |
438 | + data-ng-options="subnet as subnet.displayName for subnet in subnets track by subnet.id" |
439 | + data-ng-change="setFabricAndVlan(iface)"> |
440 | + <option value="" disabled selected>Select a subnet</option> |
441 | + </select> |
442 | + </div> |
443 | + </td> |
444 | + <td> |
445 | + <div data-ng-if="iface.name !== 'default'"> |
446 | + {$ iface.fabric.name $} |
447 | + </div> |
448 | + </td> |
449 | + <td> |
450 | + <div data-ng-if="iface.name !== 'default'"> |
451 | + <div ng-switch on="iface.vlan.vid"> |
452 | + <div ng-switch-when="0">untagged</div> |
453 | + <div ng-switch-default> |
454 | + <span>{$ iface.vlan.vid $}</span> |
455 | + <span data-ng-if="iface.vlan.name">({$ iface.vlan.name $})</span> |
456 | + </div> |
457 | + </div> |
458 | + </td> |
459 | + </tr> |
460 | + <tr> |
461 | + <td> |
462 | + <button class="p-button--base" data-ng-click="composeAddInterface()"> |
463 | + <i class="p-icon--plus"></i> |
464 | + <span class="u-off-screen">Add another interface</span> |
465 | + </button> |
466 | + <span ng-if="compose.obj.interfaces[0].name == 'default'">Define (optional)</span> |
467 | + </td> |
468 | + </tr> |
469 | + </tbody> |
470 | + </table> |
471 | + </div> |
472 | + </div> |
473 | + <div class="row"> |
474 | + <div class="col-12"> |
475 | <h3 class="p-heading--four">Storage configuration</h3> |
476 | <table class="p-table-expanding p-table--pod-storage-config"> |
477 | <thead> |
478 | diff --git a/src/maasserver/static/scss/_tables.scss b/src/maasserver/static/scss/_tables.scss |
479 | index 0f21734..227b24a 100644 |
480 | --- a/src/maasserver/static/scss/_tables.scss |
481 | +++ b/src/maasserver/static/scss/_tables.scss |
482 | @@ -92,6 +92,50 @@ |
483 | } |
484 | } |
485 | |
486 | +.p-table--pod-networking-config { |
487 | + input { |
488 | + // overriding min-width to make inputs fit within container |
489 | + min-width: auto; |
490 | + margin-bottom: 0; |
491 | + } |
492 | + |
493 | + select { |
494 | + margin-bottom: 0; |
495 | + } |
496 | + |
497 | + @media (min-width: $breakpoint-small) { |
498 | + margin-bottom: 0; |
499 | + |
500 | + .p-table__row { |
501 | + th, td { |
502 | + &:nth-child(1) { |
503 | + width: 5%; |
504 | + } |
505 | + |
506 | + &:nth-child(2) { |
507 | + width: 10%; |
508 | + } |
509 | + |
510 | + &:nth-child(3) { |
511 | + width: 25%; |
512 | + } |
513 | + |
514 | + &:nth-child(4) { |
515 | + width: 25%; |
516 | + } |
517 | + |
518 | + &:nth-child(5) { |
519 | + width: 25%; |
520 | + } |
521 | + |
522 | + &:nth-child(6) { |
523 | + width: 10%; |
524 | + } |
525 | + } |
526 | + } |
527 | + } |
528 | + } |
529 | + |
530 | .p-table--devices { |
531 | .p-table__row { |
532 | .p-table__cell { |
+1 code wise but not sure if this matches what design has agreed upon. Setting to approved but would like someone else on the MAAS team to test and someone from UI team to approve before this gets landed.
One inline comment below, might be lint related?