Merge lp:~blake-rouse/maas/fix-1510118 into lp:~maas-committers/maas/trunk

Proposed by Blake Rouse
Status: Merged
Approved by: Blake Rouse
Approved revision: no longer in the source branch.
Merged at revision: 4485
Proposed branch: lp:~blake-rouse/maas/fix-1510118
Merge into: lp:~maas-committers/maas/trunk
Diff against target: 1008 lines (+484/-81)
7 files modified
src/maasserver/static/js/angular/controllers/node_details_storage.js (+100/-20)
src/maasserver/static/js/angular/controllers/tests/test_node_details_storage.js (+232/-36)
src/maasserver/static/js/angular/factories/nodes.js (+11/-0)
src/maasserver/static/js/angular/factories/tests/test_nodes.js (+19/-0)
src/maasserver/static/partials/node-details.html (+57/-25)
src/maasserver/websockets/handlers/node.py (+40/-0)
src/maasserver/websockets/handlers/tests/test_node.py (+25/-0)
To merge this branch: bzr merge lp:~blake-rouse/maas/fix-1510118
Reviewer Review Type Date Requested Status
Andres Rodriguez (community) Approve
Review via email: mp+277156@code.launchpad.net

Commit message

Allow deleting items from the storage filesystem section. Allow deleting formatted available items in the storage available section. Show which disk is the boot disk. Allow the boot disk to easily be changed.

To post a comment you must log in.
Revision history for this message
Andres Rodriguez (andreserl) wrote :

I haven't tested this, but code wise couldn't spot anything!

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'src/maasserver/static/js/angular/controllers/node_details_storage.js'
2--- src/maasserver/static/js/angular/controllers/node_details_storage.js 2015-11-09 22:23:42 +0000
3+++ src/maasserver/static/js/angular/controllers/node_details_storage.js 2015-11-10 17:38:20 +0000
4@@ -201,15 +201,21 @@
5 var filesystems = [];
6 angular.forEach($scope.node.disks, function(disk) {
7 if(hasMountedFilesystem(disk)) {
8- filesystems.push({
9+ var data = {
10 "type": "filesystem",
11 "name": disk.name,
12 "size_human": disk.size_human,
13 "fstype": disk.filesystem.fstype,
14 "mount_point": disk.filesystem.mount_point,
15 "block_id": disk.id,
16- "partition_id": null
17- });
18+ "partition_id": null,
19+ "original_type": disk.type,
20+ "original": disk
21+ };
22+ if(disk.type === "virtual") {
23+ disk.parent_type = disk.parent.type;
24+ }
25+ filesystems.push(data);
26 }
27 angular.forEach(disk.partitions, function(partition) {
28 if(hasMountedFilesystem(partition)) {
29@@ -220,7 +226,9 @@
30 "fstype": partition.filesystem.fstype,
31 "mount_point": partition.filesystem.mount_point,
32 "block_id": disk.id,
33- "partition_id": partition.id
34+ "partition_id": partition.id,
35+ "original_type": "partition",
36+ "original": partition
37 });
38 }
39 });
40@@ -312,6 +320,7 @@
41 "block_id": disk.id,
42 "partition_id": null,
43 "has_partitions": has_partitions,
44+ "is_boot": disk.is_boot,
45 "original": disk
46 };
47 if(disk.type === "virtual") {
48@@ -337,6 +346,7 @@
49 "block_id": disk.id,
50 "partition_id": partition.id,
51 "has_partitions": false,
52+ "is_boot": false,
53 "original": partition
54 });
55 }
56@@ -402,7 +412,8 @@
57 "model": disk.model,
58 "serial": disk.serial,
59 "tags": getTags(disk),
60- "used_for": disk.used_for
61+ "used_for": disk.used_for,
62+ "is_boot": disk.is_boot
63 };
64 if(disk.type === "virtual") {
65 data.parent_type = disk.parent.type;
66@@ -417,7 +428,8 @@
67 "model": "",
68 "serial": "",
69 "tags": [],
70- "used_for": partition.used_for
71+ "used_for": partition.used_for,
72+ "is_boot": false
73 });
74 }
75 });
76@@ -539,6 +551,37 @@
77 $scope.$watch("node.disks", updateDisks);
78 };
79
80+ // Return true if the item can be a boot disk.
81+ $scope.isBootDiskDisabled = function(item, section) {
82+ if(item.type !== "physical") {
83+ return true;
84+ }
85+
86+ // If the disk is in the used section and does not have any
87+ // partitions then it cannot be a boot disk. Boot disk either
88+ // require that it be unused or that some partitions exists
89+ // on the disk. This is because the boot disk has to have a
90+ // partition table header.
91+ if(section === "used") {
92+ return !item.has_partitions;
93+ }
94+ return false;
95+ };
96+
97+ // Called to change the disk to a boot disk.
98+ $scope.setAsBootDisk = function(item) {
99+ // Do nothing if already the boot disk.
100+ if(item.is_boot) {
101+ return;
102+ }
103+ // Do nothing if disabled.
104+ if($scope.isBootDiskDisabled(item)) {
105+ return;
106+ }
107+
108+ NodesManager.setBootDisk($scope.node, item.block_id);
109+ };
110+
111 // Return array of selected filesystems.
112 $scope.getSelectedFilesystems = function() {
113 var filesystems = [];
114@@ -632,6 +675,36 @@
115 $scope.updateFilesystemSelection();
116 };
117
118+ // Enter delete mode.
119+ $scope.filesystemDelete = function() {
120+ $scope.filesystemMode = SELECTION_MODE.DELETE;
121+ };
122+
123+ // Quickly enter delete by selecting the filesystem first.
124+ $scope.quickFilesystemDelete = function(filesystem) {
125+ deselectAll($scope.filesystems);
126+ filesystem.$selected = true;
127+ $scope.updateFilesystemSelection(true);
128+ $scope.filesystemDelete();
129+ };
130+
131+ // Confirm the delete action for filesystem.
132+ $scope.filesystemConfirmDelete = function(filesystem) {
133+ if(filesystem.original_type === "partition") {
134+ // Delete the partition.
135+ NodesManager.deletePartition(
136+ $scope.node, filesystem.original.id);
137+ } else {
138+ // Delete the disk.
139+ NodesManager.deleteDisk(
140+ $scope.node, filesystem.original.id);
141+ }
142+
143+ var idx = $scope.filesystems.indexOf(filesystem);
144+ $scope.filesystems.splice(idx, 1);
145+ $scope.updateFilesystemSelection();
146+ };
147+
148 // Return true if the disk has an unmouted filesystem.
149 $scope.hasUnmountedFilesystem = function(disk) {
150 if(angular.isString(disk.fstype) && disk.fstype !== "") {
151@@ -850,7 +923,7 @@
152 };
153
154 // Enter unformat mode.
155- $scope.availableUnformat = function(disk) {
156+ $scope.availableUnformat = function() {
157 $scope.availableMode = SELECTION_MODE.UNFORMAT;
158 };
159
160@@ -949,35 +1022,42 @@
161 } else if(disk.type === "lvm-vg") {
162 return disk.original.used_size === 0;
163 } else {
164- if(!disk.has_partitions && (
165- !angular.isString(disk.fstype) || disk.fstype === "")) {
166- return true;
167- } else {
168- return false;
169- }
170+ return !disk.has_partitions;
171 }
172 };
173
174+ // Enter unformat mode.
175+ $scope.availableUnformat = function() {
176+ $scope.availableMode = SELECTION_MODE.UNFORMAT;
177+ };
178+
179+ // Quickly enter unformat mode.
180+ $scope.availableQuickUnformat = function(disk) {
181+ deselectAll($scope.available);
182+ disk.$selected = true;
183+ $scope.updateAvailableSelection(true);
184+ $scope.availableUnformat();
185+ };
186+
187 // Enter delete mode.
188 $scope.availableDelete = function() {
189 $scope.availableMode = SELECTION_MODE.DELETE;
190 };
191
192- // Quickly enter delete mode. If the disk has a filesystem it will
193- // enter unformat mode.
194+ // Quickly enter delete mode.
195 $scope.availableQuickDelete = function(disk) {
196 deselectAll($scope.available);
197 disk.$selected = true;
198 $scope.updateAvailableSelection(true);
199- if($scope.hasUnmountedFilesystem(disk)) {
200- $scope.availableUnformat(disk);
201- } else {
202- $scope.availableDelete();
203- }
204+ $scope.availableDelete();
205 };
206
207 // Return the text for remove confirmation message.
208 $scope.getRemoveTypeText = function(disk) {
209+ if(disk.type === "filesystem") {
210+ disk = disk.original;
211+ }
212+
213 if(disk.type === "physical") {
214 return "physical disk";
215 } else if(disk.type === "partition") {
216
217=== modified file 'src/maasserver/static/js/angular/controllers/tests/test_node_details_storage.js'
218--- src/maasserver/static/js/angular/controllers/tests/test_node_details_storage.js 2015-11-09 22:27:48 +0000
219+++ src/maasserver/static/js/angular/controllers/tests/test_node_details_storage.js 2015-11-10 17:38:20 +0000
220@@ -127,6 +127,7 @@
221 {
222 // Blank disk
223 id: 0,
224+ is_boot: true,
225 name: makeName("name"),
226 model: makeName("model"),
227 serial: makeName("serial"),
228@@ -146,6 +147,7 @@
229 {
230 // Disk with filesystem, no mount point
231 id: 1,
232+ is_boot: false,
233 name: makeName("name"),
234 model: makeName("model"),
235 serial: makeName("serial"),
236@@ -169,6 +171,7 @@
237 {
238 // Disk with mounted filesystem
239 id: 2,
240+ is_boot: false,
241 name: makeName("name"),
242 model: makeName("model"),
243 serial: makeName("serial"),
244@@ -192,6 +195,7 @@
245 {
246 // Partitioned disk, one partition free one used
247 id: 3,
248+ is_boot: false,
249 name: makeName("name"),
250 model: makeName("model"),
251 serial: makeName("serial"),
252@@ -231,6 +235,7 @@
253 {
254 // Disk that is a cache set.
255 id: 4,
256+ is_boot: false,
257 name: "cache0",
258 model: "",
259 serial: "",
260@@ -297,6 +302,8 @@
261 mount_point: disks[2].filesystem.mount_point,
262 block_id: disks[2].id,
263 partition_id: null,
264+ original_type: disks[2].type,
265+ original: disks[2],
266 $selected: false
267 },
268 {
269@@ -307,6 +314,8 @@
270 mount_point: disks[3].partitions[1].filesystem.mount_point,
271 block_id: disks[3].id,
272 partition_id: disks[3].partitions[1].id,
273+ original_type: "partition",
274+ original: disks[3].partitions[1],
275 $selected: false
276 }
277 ];
278@@ -323,6 +332,7 @@
279 var available = [
280 {
281 name: disks[0].name,
282+ is_boot: disks[0].is_boot,
283 size_human: disks[0].size_human,
284 available_size_human: disks[0].available_size_human,
285 used_size_human: disks[0].used_size_human,
286@@ -341,6 +351,7 @@
287 },
288 {
289 name: disks[1].name,
290+ is_boot: disks[1].is_boot,
291 size_human: disks[1].size_human,
292 available_size_human: disks[1].available_size_human,
293 used_size_human: disks[1].used_size_human,
294@@ -359,6 +370,7 @@
295 },
296 {
297 name: disks[3].partitions[0].name,
298+ is_boot: false,
299 size_human: disks[3].partitions[0].size_human,
300 available_size_human: (
301 disks[3].partitions[0].available_size_human),
302@@ -380,6 +392,7 @@
303 var used = [
304 {
305 name: disks[2].name,
306+ is_boot: disks[2].is_boot,
307 type: disks[2].type,
308 model: disks[2].model,
309 serial: disks[2].serial,
310@@ -388,6 +401,7 @@
311 },
312 {
313 name: disks[3].name,
314+ is_boot: disks[3].is_boot,
315 type: disks[3].type,
316 model: disks[3].model,
317 serial: disks[3].serial,
318@@ -396,6 +410,7 @@
319 },
320 {
321 name: disks[3].partitions[1].name,
322+ is_boot: false,
323 type: "partition",
324 model: "",
325 serial: "",
326@@ -509,6 +524,74 @@
327 expect($scope.availableNew.devices[1]).not.toBe(disk1);
328 });
329
330+ describe("isBootDiskDisabled", function() {
331+
332+ it("returns true if not physical", function() {
333+ var controller = makeController();
334+ var disk = { type: "virtual" };
335+
336+ expect($scope.isBootDiskDisabled(disk, "available")).toBe(true);
337+ });
338+
339+ it("returns false if in available", function() {
340+ var controller = makeController();
341+ var disk = { type: "physical" };
342+
343+ expect($scope.isBootDiskDisabled(disk, "available")).toBe(false);
344+ });
345+
346+ it("returns true when used and no partitions", function() {
347+ var controller = makeController();
348+ var disk = { type: "physical", has_partitions: false };
349+
350+ expect($scope.isBootDiskDisabled(disk, "used")).toBe(true);
351+ });
352+
353+ it("returns false when used and partitions", function() {
354+ var controller = makeController();
355+ var disk = { type: "physical", has_partitions: true };
356+
357+ expect($scope.isBootDiskDisabled(disk, "used")).toBe(false);
358+ });
359+ });
360+
361+ describe("setAsBootDisk", function() {
362+
363+ it("does nothing if already boot disk", function() {
364+ var controller = makeController();
365+ var disk = { is_boot: true };
366+ spyOn(NodesManager, "setBootDisk");
367+ spyOn($scope, "isBootDiskDisabled").and.returnValue(false);
368+
369+ $scope.setAsBootDisk(disk);
370+
371+ expect(NodesManager.setBootDisk).not.toHaveBeenCalled();
372+ });
373+
374+ it("does nothing if set boot disk disabled", function() {
375+ var controller = makeController();
376+ var disk = { is_boot: false };
377+ spyOn(NodesManager, "setBootDisk");
378+ spyOn($scope, "isBootDiskDisabled").and.returnValue(true);
379+
380+ $scope.setAsBootDisk(disk);
381+
382+ expect(NodesManager.setBootDisk).not.toHaveBeenCalled();
383+ });
384+
385+ it("calls NodesManager.setBootDisk", function() {
386+ var controller = makeController();
387+ var disk = { block_id: makeInteger(0, 100), is_boot: false };
388+ spyOn(NodesManager, "setBootDisk");
389+ spyOn($scope, "isBootDiskDisabled").and.returnValue(false);
390+
391+ $scope.setAsBootDisk(disk);
392+
393+ expect(NodesManager.setBootDisk).toHaveBeenCalledWith(
394+ node, disk.block_id);
395+ });
396+ });
397+
398 describe("getSelectedFilesystems", function() {
399
400 it("returns selected filesystems", function() {
401@@ -789,6 +872,78 @@
402 });
403 });
404
405+ describe("filesystemDelete", function() {
406+
407+ it("sets filesystemMode to DELETE", function() {
408+ var controller = makeController();
409+ $scope.filesystemMode = "other";
410+
411+ $scope.filesystemDelete();
412+
413+ expect($scope.filesystemMode).toBe("delete");
414+ });
415+ });
416+
417+ describe("quickFilesystemDelete", function() {
418+
419+ it("selects filesystem and calls filesystemDelete", function() {
420+ var controller = makeController();
421+ var filesystems = [{ $selected: true }, { $selected: false }];
422+ $scope.filesystems = filesystems;
423+ spyOn($scope, "updateFilesystemSelection");
424+ spyOn($scope, "filesystemDelete");
425+
426+ $scope.quickFilesystemDelete(filesystems[1]);
427+
428+ expect(filesystems[0].$selected).toBe(false);
429+ expect(filesystems[1].$selected).toBe(true);
430+ expect($scope.updateFilesystemSelection).toHaveBeenCalledWith(
431+ true);
432+ expect($scope.filesystemDelete).toHaveBeenCalled();
433+ });
434+ });
435+
436+ describe("filesystemConfirmDelete", function() {
437+
438+ it("calls NodesManager.deletePartition for partition", function() {
439+ var controller = makeController();
440+ var filesystem = {
441+ original_type: "partition",
442+ original: {
443+ id: makeInteger(0, 100)
444+ }
445+ };
446+ $scope.filesystems = [filesystem];
447+ spyOn(NodesManager, "deletePartition");
448+ spyOn($scope, "updateFilesystemSelection");
449+
450+ $scope.filesystemConfirmDelete(filesystem);
451+ expect(NodesManager.deletePartition).toHaveBeenCalledWith(
452+ node, filesystem.original.id);
453+ expect($scope.filesystems).toEqual([]);
454+ expect($scope.updateFilesystemSelection).toHaveBeenCalledWith();
455+ });
456+
457+ it("calls NodesManager.deleteDisk for disk", function() {
458+ var controller = makeController();
459+ var filesystem = {
460+ original_type: "physical",
461+ original: {
462+ id: makeInteger(0, 100)
463+ }
464+ };
465+ $scope.filesystems = [filesystem];
466+ spyOn(NodesManager, "deleteDisk");
467+ spyOn($scope, "updateFilesystemSelection");
468+
469+ $scope.filesystemConfirmDelete(filesystem);
470+ expect(NodesManager.deleteDisk).toHaveBeenCalledWith(
471+ node, filesystem.original.id);
472+ expect($scope.filesystems).toEqual([]);
473+ expect($scope.updateFilesystemSelection).toHaveBeenCalledWith();
474+ });
475+ });
476+
477 describe("hasUnmountedFilesystem", function() {
478
479 it("returns false if no fstype", function() {
480@@ -1871,6 +2026,15 @@
481 expect($scope.canDelete(disk)).toBe(true);
482 });
483
484+ it("returns true if fstype is not empty", function() {
485+ var controller = makeController();
486+ var disk = { fstype: "ext4" };
487+ $scope.isSuperUser = function() { return true; };
488+ spyOn($scope, "isAllStorageDisabled").and.returnValue(false);
489+
490+ expect($scope.canDelete(disk)).toBe(true);
491+ });
492+
493 it("returns false if has_partitions is true", function() {
494 var controller = makeController();
495 var disk = { fstype: "", has_partitions: true };
496@@ -1879,15 +2043,58 @@
497
498 expect($scope.canDelete(disk)).toBe(false);
499 });
500-
501- it("returns false if fstype is not empty", function() {
502- var controller = makeController();
503- var disk = { fstype: "ext4" };
504- $scope.isSuperUser = function() { return true; };
505- spyOn($scope, "isAllStorageDisabled").and.returnValue(false);
506-
507- expect($scope.canDelete(disk)).toBe(false);
508- });
509+ });
510+
511+ describe("availableUnformat", function() {
512+
513+ it("sets availableMode to UNFORMAT", function() {
514+ var controller = makeController();
515+ $scope.availableMode = "other";
516+
517+ $scope.availableUnformat();
518+
519+ expect($scope.availableMode).toBe("unformat");
520+ });
521+ });
522+
523+ describe("availableQuickUnformat", function() {
524+
525+ it("selects disks and deselects others", function() {
526+ var controller = makeController();
527+ var available = [{ $selected: false }, { $selected: true }];
528+ $scope.available = available;
529+ spyOn($scope, "updateAvailableSelection");
530+ spyOn($scope, "availableUnformat");
531+
532+ $scope.availableQuickUnformat(available[0]);
533+
534+ expect(available[0].$selected).toBe(true);
535+ expect(available[1].$selected).toBe(false);
536+ });
537+
538+ it("calls updateAvailableSelection with force true", function() {
539+ var controller = makeController();
540+ var available = [{ $selected: false }, { $selected: true }];
541+ spyOn($scope, "updateAvailableSelection");
542+ spyOn($scope, "availableUnformat");
543+
544+ $scope.availableQuickUnformat(available[0]);
545+
546+ expect($scope.updateAvailableSelection).toHaveBeenCalledWith(
547+ true);
548+ });
549+
550+ it("calls availableUnformat",
551+ function() {
552+ var controller = makeController();
553+ var available = [{ $selected: false }, { $selected: true }];
554+ spyOn($scope, "updateAvailableSelection");
555+ spyOn($scope, "availableUnformat");
556+
557+ $scope.availableQuickUnformat(available[0]);
558+
559+ expect($scope.availableUnformat).toHaveBeenCalledWith();
560+ });
561 });
562
563 describe("availableDelete", function() {
564@@ -1909,7 +2116,6 @@
565 var available = [{ $selected: false }, { $selected: true }];
566 $scope.available = available;
567 spyOn($scope, "updateAvailableSelection");
568- spyOn($scope, "availableUnformat");
569 spyOn($scope, "availableDelete");
570
571 $scope.availableQuickDelete(available[0]);
572@@ -1922,7 +2128,6 @@
573 var controller = makeController();
574 var available = [{ $selected: false }, { $selected: true }];
575 spyOn($scope, "updateAvailableSelection");
576- spyOn($scope, "availableUnformat");
577 spyOn($scope, "availableDelete");
578
579 $scope.availableQuickDelete(available[0]);
580@@ -1931,40 +2136,31 @@
581 true);
582 });
583
584- it("calls availableUnformat when hasUnmountedFilesystem returns true",
585- function() {
586- var controller = makeController();
587- var available = [{ $selected: false }, { $selected: true }];
588- spyOn($scope, "updateAvailableSelection");
589- spyOn($scope, "availableUnformat");
590- spyOn($scope, "availableDelete");
591- spyOn($scope, "hasUnmountedFilesystem").and.returnValue(true);
592-
593- $scope.availableQuickDelete(available[0]);
594-
595- expect($scope.availableUnformat).toHaveBeenCalledWith(
596- available[0]);
597- expect($scope.availableDelete).not.toHaveBeenCalled();
598- });
599-
600- it("calls availableDelete when hasUnmountedFilesystem returns true",
601- function() {
602- var controller = makeController();
603- var available = [{ $selected: false }, { $selected: true }];
604- spyOn($scope, "updateAvailableSelection");
605- spyOn($scope, "availableUnformat");
606- spyOn($scope, "availableDelete");
607- spyOn($scope, "hasUnmountedFilesystem").and.returnValue(false);
608+ it("calls availableDelete",
609+ function() {
610+ var controller = makeController();
611+ var available = [{ $selected: false }, { $selected: true }];
612+ spyOn($scope, "updateAvailableSelection");
613+ spyOn($scope, "availableDelete");
614
615 $scope.availableQuickDelete(available[0]);
616
617 expect($scope.availableDelete).toHaveBeenCalledWith();
618- expect($scope.availableUnformat).not.toHaveBeenCalled();
619 });
620 });
621
622 describe("getRemoveTypeText", function() {
623
624+ it("returns 'physical disk' for physical on filesystem", function() {
625+ var controller = makeController();
626+ expect($scope.getRemoveTypeText({
627+ type: "filesystem",
628+ original: {
629+ type: "physical"
630+ }
631+ })).toBe("physical disk");
632+ });
633+
634 it("returns 'physical disk' for physical", function() {
635 var controller = makeController();
636 expect($scope.getRemoveTypeText({
637
638=== modified file 'src/maasserver/static/js/angular/factories/nodes.js'
639--- src/maasserver/static/js/angular/factories/nodes.js 2015-11-10 15:35:12 +0000
640+++ src/maasserver/static/js/angular/factories/nodes.js 2015-11-10 17:38:20 +0000
641@@ -323,5 +323,16 @@
642 "node.update_disk", params);
643 };
644
645+ // Set disk as the boot disk.
646+ NodesManager.prototype.setBootDisk = function(
647+ node, block_id) {
648+ var params = {
649+ system_id: node.system_id,
650+ block_id: block_id
651+ };
652+ return RegionConnection.callMethod(
653+ "node.set_boot_disk", params);
654+ };
655+
656 return new NodesManager();
657 }]);
658
659=== modified file 'src/maasserver/static/js/angular/factories/tests/test_nodes.js'
660--- src/maasserver/static/js/angular/factories/tests/test_nodes.js 2015-11-10 15:35:12 +0000
661+++ src/maasserver/static/js/angular/factories/tests/test_nodes.js 2015-11-10 17:38:20 +0000
662@@ -761,4 +761,23 @@
663 });
664 });
665 });
666+
667+ describe("setBootDisk", function() {
668+
669+ it("calls node.set_boot_disk", function(done) {
670+ var fakeNode = makeNode();
671+ var block_id = makeInteger(0, 100);
672+ webSocket.returnData.push(makeFakeResponse(null));
673+ NodesManager.setBootDisk(
674+ fakeNode, block_id).then(
675+ function() {
676+ var sentObject = angular.fromJson(webSocket.sentData[0]);
677+ expect(sentObject.method).toBe("node.set_boot_disk");
678+ expect(sentObject.params.system_id).toBe(fakeNode.system_id);
679+ expect(sentObject.params.block_id).toBe(
680+ block_id);
681+ done();
682+ });
683+ });
684+ });
685 });
686
687=== modified file 'src/maasserver/static/partials/node-details.html'
688--- src/maasserver/static/partials/node-details.html 2015-11-10 15:35:12 +0000
689+++ src/maasserver/static/partials/node-details.html 2015-11-10 17:38:20 +0000
690@@ -771,7 +771,7 @@
691 </ul>
692 </div>
693 <div class="twelve-col padding-bottom no-margin">
694- <h3>File System</h3>
695+ <h3>File systems</h3>
696 <section class="table">
697 <header class="table__head">
698 <div class="table__row">
699@@ -816,23 +816,35 @@
700 <div class="table__data table__column--10">
701 <div class="table__controls">
702 <a class="icon unmount tooltip" data-tooltip="Unmount" data-ng-click="quickFilesystemUnmount(filesystem)">Unmount</a>
703+ <a class="icon delete tooltip" data-tooltip="Remove" data-ng-click="quickFilesystemDelete(filesystem)">Remove</a>
704 </div>
705 </div>
706 <div class="table__dropdown">
707 <div class="table__row table__dropdown-row" data-ng-class="{ active: filesystemMode !== null && filesystemMode !== 'multi' }">
708 <div class="ng-hide" data-ng-show="filesystemMode === 'single'">
709 <div class="table__data left">
710- <a class="link-cta-ubuntu text-button"
711- data-ng-click="filesystemUnmount()">Unmount</a>
712+ <button class="cta-ubuntu secondary margin-right"
713+ data-ng-click="filesystemUnmount()">Unmount</button>
714+ <a class="link-cta-ubuntu text-button"
715+ data-ng-click="filesystemDelete()">Remove {$ getRemoveTypeText(filesystem) $}</a>
716 </div>
717 </div>
718 <div class="ng-hide" data-ng-show="filesystemMode === 'unmount'">
719 <div class="table__data left margin-top--five">
720- <p><span class="icon warning margin-right--ten"></span> Are you sure you want to unmount this filesystem?</p>
721- </div>
722- <div class="table__data right">
723- <a class="link-cta-ubuntu text-button" data-ng-click="filesystemCancel()">Cancel</a>
724- <button class="cta-ubuntu" data-ng-click="filesystemConfirmUnmount(filesystem)">Unmount</button>
725+ <p><span class="icon warning margin-right--ten"></span> Are you sure you want to unmount this filesystem?</p>
726+ </div>
727+ <div class="table__data right">
728+ <a class="link-cta-ubuntu text-button" data-ng-click="filesystemCancel()">Cancel</a>
729+ <button class="cta-ubuntu" data-ng-click="filesystemConfirmUnmount(filesystem)">Unmount</button>
730+ </div>
731+ </div>
732+ <div class="ng-hide" data-ng-show="filesystemMode === 'delete'">
733+ <div class="table__data left margin-top--five">
734+ <p><span class="icon warning margin-right--ten"></span> Are you sure you want to remove this {$ getRemoveTypeText(filesystem) $}?</p>
735+ </div>
736+ <div class="table__data right">
737+ <a class="link-cta-ubuntu text-button" data-ng-click="filesystemCancel()">Cancel</a>
738+ <button class="cta-ubuntu" data-ng-click="filesystemConfirmDelete(filesystem)">Remove</button>
739 </div>
740 </div>
741 </div>
742@@ -889,18 +901,18 @@
743 <div class="table__row table__dropdown-row" data-ng-class="{ active: cachesetsMode !== null && cachesetsMode !== 'multi' }">
744 <div data-ng-show="cachesetsMode === 'single' && canDeleteCacheSet(cacheset)">
745 <div class="table__data left">
746- <a class="link-cta-ubuntu text-button"
747+ <a class="link-cta-ubuntu text-button"
748 data-ng-show="canDeleteCacheSet(cacheset)"
749 data-ng-click="cacheSetDelete()">Remove</a>
750 </div>
751 </div>
752 <div class="ng-hide" data-ng-show="cachesetsMode === 'delete'">
753 <div class="table__data left margin-top--five">
754- <p><span class="icon warning margin-right--ten"></span> Are you sure you want to delete this cache set?</p>
755+ <p><span class="icon warning margin-right--ten"></span> Are you sure you want to delete this cache set?</p>
756 </div>
757 <div class="table__data right">
758- <a class="link-cta-ubuntu text-button" data-ng-click="cacheSetCancel()">Cancel</a>
759- <button class="cta-ubuntu" data-ng-click="cacheSetConfirmDelete(cacheset)">Remove</button>
760+ <a class="link-cta-ubuntu text-button" data-ng-click="cacheSetCancel()">Cancel</a>
761+ <button class="cta-ubuntu" data-ng-click="cacheSetConfirmDelete(cacheset)">Remove</button>
762 </div>
763 </div>
764 </div>
765@@ -924,13 +936,14 @@
766 <label class="checkbox-label" for="available-check-all"></label>
767 </span>
768 </div>
769- <div class="table__header table__column--20">
770+ <div class="table__header table__column--17">
771 <a data-ng-click="column = 'name'" data-ng-class="{active: column === 'name'}">Name</a>
772 <span class="divide"></span>
773 <a data-ng-click="column = 'model'" data-ng-class="{active: column === 'model'}">Model</a>
774 <span class="divide"></span>
775 <a data-ng-click="column = 'serial'" data-ng-class="{active: column === 'serial'}">Serial</a>
776 </div>
777+ <div class="table__header table__column--3">Boot</div>
778 <div class="table__header table__column--15">Size</div>
779 <div class="table__header table__column--15">Device Type</div>
780 <div class="table__header table__column--15">File system</div>
781@@ -956,7 +969,7 @@
782 <label class="checkbox-label" for="{$ item.name $}" ></label>
783 </span>
784 </div>
785- <div class="table__data table__column--20" data-ng-show="column === 'name'">
786+ <div class="table__data table__column--17" data-ng-show="column === 'name'">
787 <input type="text" class="table__input"
788 data-ng-model="item.name"
789 data-maas-enter-blur
790@@ -965,8 +978,15 @@
791 data-ng-disabled="item.type === 'partition' || isAllStorageDisabled() || !isSuperUser()"
792 data-ng-change="nameHasChanged(item)">
793 </div>
794- <div class="table__data table__column--20 ng-hide" data-ng-show="column === 'model'">{$ item.model $}</div>
795- <div class="table__data table__column--20 ng-hide" data-ng-show="column === 'serial'">{$ item.serial $}</div>
796+ <div class="table__data table__column--17 ng-hide" data-ng-show="column === 'model'">{$ item.model $}</div>
797+ <div class="table__data table__column--17 ng-hide" data-ng-show="column === 'serial'">{$ item.serial $}</div>
798+ <div class="table__data table__column--3">
799+ <input class="align-center" type="radio"
800+ data-ng-click="setAsBootDisk(item)"
801+ data-ng-checked="item.is_boot"
802+ data-ng-if="item.type === 'physical'"
803+ data-ng-disabled="isBootDiskDisabled(item, 'available')">
804+ </div>
805 <div class="table__data table__column--15">{$ item.size_human $} <span class="table__label ng-hide" data-ng-show="showFreeSpace(item)">Free: {$ item.available_size_human $}</span></div>
806 <div class="table__data table__column--15">{$ getDeviceType(item) $}</div>
807 <div class="table__data table__column--15">
808@@ -995,9 +1015,13 @@
809 data-tooltip="Partition"
810 data-ng-show="canAddPartition(item)"
811 data-ng-click="availableQuickPartition(item)">Partition</a>
812- <a class="icon delete tooltip" data-tooltip="Remove"
813- data-tooltip="Delete"
814- data-ng-show="hasUnmountedFilesystem(item) || canDelete(item)"
815+ <a class="icon unmount tooltip ng-hide"
816+ data-tooltip="Unformat"
817+ data-ng-show="hasUnmountedFilesystem(item)"
818+ data-ng-click="availableQuickUnformat(item)">Unformat</a>
819+ <a class="icon delete tooltip ng-hide"
820+ data-tooltip="Remove"
821+ data-ng-show="canDelete(item)"
822 data-ng-click="availableQuickDelete(item)">Remove</a>
823 </div>
824 </div>
825@@ -1156,9 +1180,9 @@
826 <button class="cta-ubuntu secondary margin-right"
827 data-ng-show="canAddLogicalVolume(item)"
828 data-ng-click="availableLogicalVolume(item)">Add logical volume</button>
829- <a class="link-cta-ubuntu text-button margin-right"
830+ <button class="cta-ubuntu secondary margin-right"
831 data-ng-show="hasUnmountedFilesystem(item)"
832- data-ng-click="availableUnformat()">Unformat</a>
833+ data-ng-click="availableUnformat()">Unformat</button>
834 <a class="link-cta-ubuntu text-button"
835 data-ng-show="canDelete(item)"
836 data-ng-click="availableDelete()">Remove {$ getRemoveTypeText(item) $}</a>
837@@ -1402,13 +1426,14 @@
838 <header class="table__head">
839 <div class="table__row">
840 <div class="table__header table__column--3"></div>
841- <div class="table__header table__column--20">
842+ <div class="table__header table__column--17">
843 <a data-ng-click="column = 'name'" data-ng-class="{active: column === 'name'}">Name</a>
844 <span class="divide"></span>
845 <a data-ng-click="column = 'model'" data-ng-class="{active: column === 'model'}">Model</a>
846 <span class="divide"></span>
847 <a data-ng-click="column = 'serial'" data-ng-class="{active: column === 'serial'}">Serial</a>
848 </div>
849+ <div class="table__header table__column--3">Boot</div>
850 <div class="table__header table__column--15">Device type</div>
851 <div class="table__header table__column--52">Used for</div>
852 </div>
853@@ -1422,9 +1447,16 @@
854 <div data-ng-repeat="item in used">
855 <div class="table__row details__used">
856 <div class="table__data table__column--3"></div>
857- <div class="table__data table__column--20" data-ng-show="column === 'name'">{$ item.name $}</div>
858- <div class="table__data table__column--20" data-ng-show="column === 'model'">{$ item.model $}</div>
859- <div class="table__data table__column--20" data-ng-show="column === 'serial'">{$ item.serial $}</div>
860+ <div class="table__data table__column--17" data-ng-show="column === 'name'">{$ item.name $}</div>
861+ <div class="table__data table__column--17" data-ng-show="column === 'model'">{$ item.model $}</div>
862+ <div class="table__data table__column--17" data-ng-show="column === 'serial'">{$ item.serial $}</div>
863+ <div class="table__data table__column--3">
864+ <input class="align-center" type="radio"
865+ data-ng-click="setAsBootDisk(item)"
866+ data-ng-checked="item.is_boot"
867+ data-ng-if="item.type === 'physical'"
868+ data-ng-disabled="isBootDiskDisabled(item, 'used')">
869+ </div>
870 <div class="table__data table__column--15">{$ getDeviceType(item) $}</div>
871 <div class="table__data table__column--52">{$ item.used_for $}</div>
872 </div>
873
874=== modified file 'src/maasserver/websockets/handlers/node.py'
875--- src/maasserver/websockets/handlers/node.py 2015-11-10 15:35:12 +0000
876+++ src/maasserver/websockets/handlers/node.py 2015-11-10 17:38:20 +0000
877@@ -156,6 +156,7 @@
878 'create_raid',
879 'create_volume_group',
880 'create_logical_volume',
881+ 'set_boot_disk',
882 ]
883 form = AdminNodeWithMACAddressesForm
884 exclude = [
885@@ -904,6 +905,10 @@
886
887 def create_partition(self, params):
888 """Create a partition."""
889+ # Only admin users can perform delete.
890+ if not self.user.is_superuser:
891+ raise HandlerPermissionError()
892+
893 node = self.get_object(params)
894 disk_obj = BlockDevice.objects.get(id=params['block_id'], node=node)
895 form = AddPartitionForm(
896@@ -922,6 +927,10 @@
897
898 def create_cache_set(self, params):
899 """Create a cache set."""
900+ # Only admin users can perform delete.
901+ if not self.user.is_superuser:
902+ raise HandlerPermissionError()
903+
904 node = self.get_object(params)
905 block_id = params.get('block_id')
906 partition_id = params.get('partition_id')
907@@ -943,6 +952,10 @@
908
909 def create_bcache(self, params):
910 """Create a bcache."""
911+ # Only admin users can perform delete.
912+ if not self.user.is_superuser:
913+ raise HandlerPermissionError()
914+
915 node = self.get_object(params)
916 block_id = params.get('block_id')
917 partition_id = params.get('partition_id')
918@@ -974,6 +987,10 @@
919
920 def create_raid(self, params):
921 """Create a RAID."""
922+ # Only admin users can perform delete.
923+ if not self.user.is_superuser:
924+ raise HandlerPermissionError()
925+
926 node = self.get_object(params)
927 form = CreateRaidForm(node=node, data=params)
928 if not form.is_valid():
929@@ -988,6 +1005,10 @@
930
931 def create_volume_group(self, params):
932 """Create a volume group."""
933+ # Only admin users can perform delete.
934+ if not self.user.is_superuser:
935+ raise HandlerPermissionError()
936+
937 node = self.get_object(params)
938 form = CreateVolumeGroupForm(node=node, data=params)
939 if not form.is_valid():
940@@ -997,6 +1018,10 @@
941
942 def create_logical_volume(self, params):
943 """Create a logical volume."""
944+ # Only admin users can perform delete.
945+ if not self.user.is_superuser:
946+ raise HandlerPermissionError()
947+
948 node = self.get_object(params)
949 volume_group = VolumeGroup.objects.get(id=params['volume_group_id'])
950 if volume_group.get_node() != node:
951@@ -1016,6 +1041,21 @@
952 node, logical_volume.id,
953 params.get("fstype"), params.get("mount_point"))
954
955+ def set_boot_disk(self, params):
956+ """Set the disk as the boot disk."""
957+ # Only admin users can perform delete.
958+ if not self.user.is_superuser:
959+ raise HandlerPermissionError()
960+
961+ node = self.get_object(params)
962+ device = BlockDevice.objects.get(
963+ id=params['block_id'], node=node).actual_instance
964+ if device.type != 'physical':
965+ raise HandlerError(
966+ "Only a physical disk can be set as the boot disk.")
967+ node.boot_disk = device
968+ node.save()
969+
970 def action(self, params):
971 """Perform the action on the object."""
972 obj = self.get_object(params)
973
974=== modified file 'src/maasserver/websockets/handlers/tests/test_node.py'
975--- src/maasserver/websockets/handlers/tests/test_node.py 2015-11-10 15:35:12 +0000
976+++ src/maasserver/websockets/handlers/tests/test_node.py 2015-11-10 17:38:20 +0000
977@@ -1784,6 +1784,31 @@
978 mount_point,
979 logical_volume.get_effective_filesystem().mount_point)
980
981+ def test_set_boot_disk(self):
982+ user = factory.make_admin()
983+ handler = NodeHandler(user, {})
984+ architecture = make_usable_architecture(self)
985+ node = factory.make_Node(interface=True, architecture=architecture)
986+ boot_disk = factory.make_PhysicalBlockDevice(node=node)
987+ handler.set_boot_disk({
988+ 'system_id': node.system_id,
989+ 'block_id': boot_disk.id,
990+ })
991+ self.assertEquals(boot_disk.id, reload_object(node).get_boot_disk().id)
992+
993+ def test_set_boot_disk_raises_error_for_none_physical(self):
994+ user = factory.make_admin()
995+ handler = NodeHandler(user, {})
996+ architecture = make_usable_architecture(self)
997+ node = factory.make_Node(interface=True, architecture=architecture)
998+ boot_disk = factory.make_VirtualBlockDevice(node=node)
999+ error = self.assertRaises(HandlerError, handler.set_boot_disk, {
1000+ 'system_id': node.system_id,
1001+ 'block_id': boot_disk.id,
1002+ })
1003+ self.assertEquals(
1004+ error.message, "Only a physical disk can be set as the boot disk.")
1005+
1006 def test_update_raise_HandlerError_if_tag_has_definition(self):
1007 user = factory.make_admin()
1008 handler = NodeHandler(user, {})