Merge lp:~blake-rouse/maas/fix-1510118 into lp:~maas-committers/maas/trunk
- fix-1510118
- Merge into 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 |
Related bugs: |
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.
Description of the change
To post a comment you must log in.
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, {}) |
I haven't tested this, but code wise couldn't spot anything!