Merge ~blr/maas:compose-kvm-networking into maas:master

Proposed by Kit Randel
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)
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://drive.google.com/file/d/11Ls--bgeQc2dZ1jx394SclBkCEjwP0f8/view

To post a comment you must log in.
Revision history for this message
Newell Jensen (newell-jensen) wrote :

+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?

review: Approve
Revision history for this message
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.

review: Approve
Revision history for this message
MAAS Lander (maas-lander) wrote :
~blr/maas:compose-kvm-networking updated
3d947a4... by Kit Randel

Testfix.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/src/maasserver/static/js/angular/controllers/pod_details.js b/src/maasserver/static/js/angular/controllers/pod_details.js
2index 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+ }]);
154diff --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
155index 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 });
380diff --git a/src/maasserver/static/partials/pod-details.html b/src/maasserver/static/partials/pod-details.html
381index 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>
478diff --git a/src/maasserver/static/scss/_tables.scss b/src/maasserver/static/scss/_tables.scss
479index 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 {

Subscribers

People subscribed via source and target branches