Merge lp:~blake-rouse/maas/create-bcache-ui into lp:~maas-committers/maas/trunk
- create-bcache-ui
- Merge into trunk
Proposed by
Blake Rouse
Status: | Merged |
---|---|
Approved by: | Blake Rouse |
Approved revision: | no longer in the source branch. |
Merged at revision: | 4393 |
Proposed branch: | lp:~blake-rouse/maas/create-bcache-ui |
Merge into: | lp:~maas-committers/maas/trunk |
Diff against target: |
2577 lines (+1829/-210) 9 files modified
src/maasserver/models/blockdevice.py (+26/-14) src/maasserver/static/js/angular/controllers/node_details_storage.js (+400/-20) src/maasserver/static/js/angular/controllers/tests/test_node_details_networking.js (+3/-3) src/maasserver/static/js/angular/controllers/tests/test_node_details_storage.js (+803/-5) src/maasserver/static/js/angular/factories/nodes.js (+37/-0) src/maasserver/static/js/angular/factories/tests/test_nodes.js (+86/-0) src/maasserver/static/partials/node-details.html (+153/-162) src/maasserver/websockets/handlers/node.py (+105/-1) src/maasserver/websockets/handlers/tests/test_node.py (+216/-5) |
To merge this branch: | bzr merge lp:~blake-rouse/maas/create-bcache-ui |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Andres Rodriguez (community) | Approve | ||
Review via email: mp+275090@code.launchpad.net |
Commit message
Add the ability to create and delete cache sets. Add the ability to create a bcache device by selecting an available disk, its cache set, and its caching mode. Include cache sets in the JSON result over the websocket. Render the cache sets in their own section under filesystems.
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/models/blockdevice.py' |
2 | --- src/maasserver/models/blockdevice.py 2015-10-19 17:56:35 +0000 |
3 | +++ src/maasserver/models/blockdevice.py 2015-10-21 17:51:26 +0000 |
4 | @@ -232,24 +232,12 @@ |
5 | @property |
6 | def used_size(self): |
7 | """Return the used size on the block device.""" |
8 | - filesystem = self.get_effective_filesystem() |
9 | - if filesystem is not None: |
10 | - return self.size |
11 | - partitiontable = self.get_partitiontable() |
12 | - if partitiontable is not None: |
13 | - return partitiontable.get_used_size() |
14 | - return 0 |
15 | + return self.get_used_size() |
16 | |
17 | @property |
18 | def available_size(self): |
19 | """Return the available size on the block device.""" |
20 | - filesystem = self.get_effective_filesystem() |
21 | - if filesystem is not None: |
22 | - return 0 |
23 | - partitiontable = self.get_partitiontable() |
24 | - if partitiontable is not None: |
25 | - return partitiontable.get_available_size() |
26 | - return self.size |
27 | + return self.get_available_size() |
28 | |
29 | @property |
30 | def used_for(self): |
31 | @@ -261,6 +249,30 @@ |
32 | size=human_readable_bytes(self.size), |
33 | node=self.node) |
34 | |
35 | + def get_block_size(self): |
36 | + """Return the block size for the block device.""" |
37 | + return self.block_size |
38 | + |
39 | + def get_used_size(self): |
40 | + """Return the used size on the block device.""" |
41 | + filesystem = self.get_effective_filesystem() |
42 | + if filesystem is not None: |
43 | + return self.size |
44 | + partitiontable = self.get_partitiontable() |
45 | + if partitiontable is not None: |
46 | + return partitiontable.get_used_size() |
47 | + return 0 |
48 | + |
49 | + def get_available_size(self): |
50 | + """Return the available size on the block device.""" |
51 | + filesystem = self.get_effective_filesystem() |
52 | + if filesystem is not None: |
53 | + return 0 |
54 | + partitiontable = self.get_partitiontable() |
55 | + if partitiontable is not None: |
56 | + return partitiontable.get_available_size() |
57 | + return self.size |
58 | + |
59 | def delete(self): |
60 | """Delete the block device. |
61 | |
62 | |
63 | === modified file 'src/maasserver/static/js/angular/controllers/node_details_storage.js' |
64 | --- src/maasserver/static/js/angular/controllers/node_details_storage.js 2015-10-19 20:12:24 +0000 |
65 | +++ src/maasserver/static/js/angular/controllers/node_details_storage.js 2015-10-21 17:51:26 +0000 |
66 | @@ -4,6 +4,44 @@ |
67 | * MAAS Node Storage Controller |
68 | */ |
69 | |
70 | + |
71 | +// Filter that is specific to the NodeStorageController. Remove the available |
72 | +// disks from the list if being used in the availableNew. |
73 | +angular.module('MAAS').filter('removeAvailableByNew', function() { |
74 | + return function(disks, availableNew) { |
75 | + if(!angular.isObject(availableNew) || ( |
76 | + !angular.isObject(availableNew.device) && |
77 | + !angular.isArray(availableNew.devices))) { |
78 | + return disks; |
79 | + } |
80 | + |
81 | + var filtered = []; |
82 | + var single = true; |
83 | + if(angular.isArray(availableNew.devices)) { |
84 | + single = false; |
85 | + } |
86 | + angular.forEach(disks, function(disk) { |
87 | + if(single) { |
88 | + if(disk !== availableNew.device) { |
89 | + filtered.push(disk); |
90 | + } |
91 | + } else { |
92 | + var i, found = false; |
93 | + for(i = 0; i < availableNew.devices.length; i++) { |
94 | + if(disk === availableNew.devices[i]) { |
95 | + found = true; |
96 | + break; |
97 | + } |
98 | + } |
99 | + if(!found) { |
100 | + filtered.push(disk); |
101 | + } |
102 | + } |
103 | + }); |
104 | + return filtered; |
105 | + }; |
106 | +}); |
107 | + |
108 | angular.module('MAAS').controller('NodeStorageController', [ |
109 | '$scope', 'NodesManager', 'ConverterService', |
110 | function($scope, NodesManager, ConverterService) { |
111 | @@ -19,7 +57,8 @@ |
112 | UNFORMAT: "unformat", |
113 | DELETE: "delete", |
114 | FORMAT_AND_MOUNT: "format-mount", |
115 | - PARTITION: "partition" |
116 | + PARTITION: "partition", |
117 | + BCACHE: "bcache" |
118 | }; |
119 | |
120 | $scope.editing = false; |
121 | @@ -30,10 +69,15 @@ |
122 | $scope.filesystemsMap = {}; |
123 | $scope.filesystemMode = SELECTION_MODE.NONE; |
124 | $scope.filesystemAllSelected = false; |
125 | + $scope.cachesets = []; |
126 | + $scope.cachesetsMap = {}; |
127 | + $scope.cachesetsMode = SELECTION_MODE.NONE; |
128 | + $scope.cachesetsAllSelected = false; |
129 | $scope.available = []; |
130 | $scope.availableMap = {}; |
131 | $scope.availableMode = SELECTION_MODE.NONE; |
132 | $scope.availableAllSelected = false; |
133 | + $scope.availableNew = {}; |
134 | $scope.used = []; |
135 | |
136 | // Give $parent which is the NodeDetailsController access to this scope |
137 | @@ -62,7 +106,9 @@ |
138 | |
139 | // Return True if the item is in use. |
140 | function isInUse(item) { |
141 | - if(angular.isObject(item.filesystem)) { |
142 | + if(item.type === "cache-set") { |
143 | + return true; |
144 | + } else if(angular.isObject(item.filesystem)) { |
145 | if(item.filesystem.is_format_fstype && |
146 | angular.isString(item.filesystem.mount_point) && |
147 | item.filesystem.mount_point !== "") { |
148 | @@ -84,6 +130,19 @@ |
149 | return tags; |
150 | } |
151 | |
152 | + // Return a unique key that will never change. |
153 | + function getUniqueKey(disk) { |
154 | + if(disk.type === "cache-set") { |
155 | + return "cache-set-" + disk.cache_set_id; |
156 | + } else { |
157 | + var key = disk.type + "-" + disk.block_id; |
158 | + if(angular.isNumber(disk.partition_id)) { |
159 | + key += "-" + disk.partition_id; |
160 | + } |
161 | + return key; |
162 | + } |
163 | + } |
164 | + |
165 | // Update the list of filesystems. Only filesystems with a mount point |
166 | // set go here. If no mount point is set, it goes in available. |
167 | function updateFilesystems() { |
168 | @@ -92,6 +151,7 @@ |
169 | angular.forEach($scope.node.disks, function(disk) { |
170 | if(hasMountedFilesystem(disk)) { |
171 | filesystems.push({ |
172 | + "type": "filesystem", |
173 | "name": disk.name, |
174 | "size_human": disk.size_human, |
175 | "fstype": disk.filesystem.fstype, |
176 | @@ -103,6 +163,7 @@ |
177 | angular.forEach(disk.partitions, function(partition) { |
178 | if(hasMountedFilesystem(partition)) { |
179 | filesystems.push({ |
180 | + "type": "filesystem", |
181 | "name": partition.name, |
182 | "size_human": partition.size_human, |
183 | "fstype": partition.filesystem.fstype, |
184 | @@ -117,7 +178,8 @@ |
185 | // Update the selected filesystems with the currently selected |
186 | // filesystems. |
187 | angular.forEach(filesystems, function(filesystem) { |
188 | - var oldFilesystem = $scope.filesystemsMap[filesystem.name]; |
189 | + var key = getUniqueKey(filesystem); |
190 | + var oldFilesystem = $scope.filesystemsMap[key]; |
191 | if(angular.isObject(oldFilesystem)) { |
192 | filesystem.$selected = oldFilesystem.$selected; |
193 | } else { |
194 | @@ -129,13 +191,52 @@ |
195 | $scope.filesystems = filesystems; |
196 | $scope.filesystemsMap = {}; |
197 | angular.forEach(filesystems, function(filesystem) { |
198 | - $scope.filesystemsMap[filesystem.name] = filesystem; |
199 | + $scope.filesystemsMap[getUniqueKey(filesystem)] = filesystem; |
200 | }); |
201 | |
202 | // Update the selection mode. |
203 | $scope.updateFilesystemSelection(false); |
204 | } |
205 | |
206 | + // Update the list of cache sets. |
207 | + function updateCacheSets() { |
208 | + // Create the new list of cache sets. |
209 | + var cachesets = []; |
210 | + angular.forEach($scope.node.disks, function(disk) { |
211 | + if(disk.type === "cache-set") { |
212 | + cachesets.push({ |
213 | + "type": "cache-set", |
214 | + "name": disk.name, |
215 | + "size_human": disk.size_human, |
216 | + "cache_set_id": disk.id, |
217 | + "used_by": disk.used_for |
218 | + }); |
219 | + } |
220 | + }); |
221 | + |
222 | + // Update the selected cache sets with the currently selected |
223 | + // cache sets. |
224 | + angular.forEach(cachesets, function(cacheset) { |
225 | + var key = getUniqueKey(cacheset); |
226 | + var oldCacheSet = $scope.cachesetsMap[key]; |
227 | + if(angular.isObject(oldCacheSet)) { |
228 | + cacheset.$selected = oldCacheSet.$selected; |
229 | + } else { |
230 | + cacheset.$selected = false; |
231 | + } |
232 | + }); |
233 | + |
234 | + // Update the cachesets and cachesetsMap on the scope. |
235 | + $scope.cachesets = cachesets; |
236 | + $scope.cachesetsMap = {}; |
237 | + angular.forEach(cachesets, function(cacheset) { |
238 | + $scope.cachesetsMap[getUniqueKey(cacheset)] = cacheset; |
239 | + }); |
240 | + |
241 | + // Update the selection mode. |
242 | + $scope.updateCacheSetsSelection(false); |
243 | + } |
244 | + |
245 | // Update list of all available disks. |
246 | function updateAvailable() { |
247 | var available = []; |
248 | @@ -192,7 +293,8 @@ |
249 | // available disks. Also copy the $options so they are not lost |
250 | // for the current action. |
251 | angular.forEach(available, function(disk) { |
252 | - var oldDisk = $scope.availableMap[disk.name]; |
253 | + var key = getUniqueKey(disk); |
254 | + var oldDisk = $scope.availableMap[key]; |
255 | if(angular.isObject(oldDisk)) { |
256 | disk.$selected = oldDisk.$selected; |
257 | disk.$options = oldDisk.$options; |
258 | @@ -206,9 +308,31 @@ |
259 | $scope.available = available; |
260 | $scope.availableMap = {}; |
261 | angular.forEach(available, function(disk) { |
262 | - $scope.availableMap[disk.name] = disk; |
263 | + $scope.availableMap[getUniqueKey(disk)] = disk; |
264 | }); |
265 | |
266 | + // Update device or devices on the availableNew object to be |
267 | + // there new objects. |
268 | + if(angular.isObject($scope.availableNew)) { |
269 | + // Update device. |
270 | + if(angular.isObject($scope.availableNew.device)) { |
271 | + var key = getUniqueKey($scope.availableNew.device); |
272 | + $scope.availableNew.device = $scope.availableMap[key]; |
273 | + // Update devices. |
274 | + } else if(angular.isArray($scope.availableNew.devices)) { |
275 | + var newDevices = []; |
276 | + angular.forEach( |
277 | + $scope.availableNew.devices, function(device) { |
278 | + var key = getUniqueKey(device); |
279 | + var newDevice = $scope.availableMap[key]; |
280 | + if(angular.isObject(newDevice)) { |
281 | + newDevices.push(newDevice); |
282 | + } |
283 | + }); |
284 | + $scope.availableNew.devices = newDevices; |
285 | + } |
286 | + } |
287 | + |
288 | // Update the selection mode. |
289 | $scope.updateAvailableSelection(false); |
290 | } |
291 | @@ -217,18 +341,22 @@ |
292 | function updateUsed() { |
293 | var used = []; |
294 | angular.forEach($scope.node.disks, function(disk) { |
295 | - if(isInUse(disk)) { |
296 | - used.push({ |
297 | + if(isInUse(disk) && disk.type !== "cache-set") { |
298 | + var data = { |
299 | "name": disk.name, |
300 | "type": disk.type, |
301 | "model": disk.model, |
302 | "serial": disk.serial, |
303 | "tags": getTags(disk), |
304 | "used_for": disk.used_for |
305 | - }); |
306 | + }; |
307 | + if(disk.type === "virtual") { |
308 | + data.parent_type = disk.parent.type; |
309 | + } |
310 | + used.push(data); |
311 | } |
312 | angular.forEach(disk.partitions, function(partition) { |
313 | - if(isInUse(partition)) { |
314 | + if(isInUse(partition) && partition.type !== "cache-set") { |
315 | used.push({ |
316 | "name": partition.name, |
317 | "type": "partition", |
318 | @@ -248,6 +376,7 @@ |
319 | if(angular.isArray($scope.node.disks)) { |
320 | $scope.has_disks = $scope.node.disks.length > 0; |
321 | updateFilesystems(); |
322 | + updateCacheSets(); |
323 | updateAvailable(); |
324 | updateUsed(); |
325 | } else { |
326 | @@ -256,10 +385,15 @@ |
327 | $scope.filesystemsMap = {}; |
328 | $scope.filesystemMode = SELECTION_MODE.NONE; |
329 | $scope.filesystemAllSelected = false; |
330 | + $scope.cachesets = []; |
331 | + $scope.cachesetsMap = {}; |
332 | + $scope.cachesetsMode = SELECTION_MODE.NONE; |
333 | + $scope.cachesetsAllSelected = false; |
334 | $scope.available = []; |
335 | $scope.availableMap = {}; |
336 | $scope.availableMode = SELECTION_MODE.NONE; |
337 | $scope.availableAllSelected = false; |
338 | + $scope.availableNew = {}; |
339 | $scope.used = []; |
340 | } |
341 | } |
342 | @@ -282,18 +416,33 @@ |
343 | return pattern.test(string); |
344 | } |
345 | |
346 | - // Convert the string to a byte. |
347 | - function convertToBytes(string, units) { |
348 | - var value = parseFloat(string); |
349 | - if(units === "mb") { |
350 | - return value * 1000 * 1000; |
351 | - } else if(units === "gb") { |
352 | - return value * 1000 * 1000 * 1000; |
353 | - } else if(units === "tb") { |
354 | - return value * 1000 * 1000 * 1000 * 1000; |
355 | + // Extract the index from the bcache name. |
356 | + function getBcacheIndexFromName(name) { |
357 | + var pattern = /^bcache([0-9]+)$/; |
358 | + var match = pattern.exec(name); |
359 | + if(angular.isArray(match) && match.length === 2) { |
360 | + return parseInt(match[1], 10); |
361 | } |
362 | } |
363 | |
364 | + // Get the next bcache device name. |
365 | + function getNextBcacheName() { |
366 | + var idx = -1; |
367 | + angular.forEach($scope.node.disks, function(disk) { |
368 | + var bcacheIdx = getBcacheIndexFromName(disk.name); |
369 | + if(angular.isNumber(bcacheIdx)) { |
370 | + idx = Math.max(idx, bcacheIdx); |
371 | + } |
372 | + angular.forEach(disk.partitions, function(partition) { |
373 | + bcacheIdx = getBcacheIndexFromName(partition.name); |
374 | + if(angular.isNumber(bcacheIdx)) { |
375 | + idx = Math.max(idx, bcacheIdx); |
376 | + } |
377 | + }); |
378 | + }); |
379 | + return "bcache" + (idx + 1); |
380 | + } |
381 | + |
382 | // Called by $parent when the node has been loaded. |
383 | $scope.nodeLoaded = function() { |
384 | $scope.$watch("node.disks", updateDisks); |
385 | @@ -413,6 +562,10 @@ |
386 | |
387 | // Return the device type for the disk. |
388 | $scope.getDeviceType = function(disk) { |
389 | + if(angular.isUndefined(disk)) { |
390 | + return ""; |
391 | + } |
392 | + |
393 | if(disk.type === "virtual") { |
394 | if(disk.parent_type === "lvm-vg") { |
395 | return "Logical Volume"; |
396 | @@ -551,6 +704,7 @@ |
397 | // Cancel the current available operation. |
398 | $scope.availableCancel = function() { |
399 | $scope.updateAvailableSelection(true); |
400 | + $scope.availableNew = {}; |
401 | }; |
402 | |
403 | // Enter unformat mode. |
404 | @@ -653,7 +807,7 @@ |
405 | // Return true if the disk can be deleted. |
406 | $scope.canDelete = function(disk) { |
407 | if(!disk.has_partitions && ( |
408 | - !angular.isString(disk.fstype) || disk.fstype === "")) { |
409 | + !angular.isString(disk.fstype) || disk.fstype === "")) { |
410 | return true; |
411 | } else { |
412 | return false; |
413 | @@ -777,6 +931,7 @@ |
414 | } |
415 | }; |
416 | |
417 | + // Confirm the partition creation. |
418 | $scope.availableConfirmPartition = function(disk) { |
419 | // Do nothing if not valid. |
420 | if($scope.isAddPartitionSizeInvalid(disk)) { |
421 | @@ -820,6 +975,231 @@ |
422 | $scope.updateAvailableSelection(true); |
423 | }; |
424 | |
425 | + // Return array of selected cache sets. |
426 | + $scope.getSelectedCacheSets = function() { |
427 | + var cachesets = []; |
428 | + angular.forEach($scope.cachesets, function(cacheset) { |
429 | + if(cacheset.$selected) { |
430 | + cachesets.push(cacheset); |
431 | + } |
432 | + }); |
433 | + return cachesets; |
434 | + }; |
435 | + |
436 | + // Update the currect mode for the cache sets section and the all |
437 | + // selected value. |
438 | + $scope.updateCacheSetsSelection = function(force) { |
439 | + if(angular.isUndefined(force)) { |
440 | + force = false; |
441 | + } |
442 | + var cachesets = $scope.getSelectedCacheSets(); |
443 | + if(cachesets.length === 0) { |
444 | + $scope.cachesetsMode = SELECTION_MODE.NONE; |
445 | + } else if(cachesets.length === 1 && force) { |
446 | + $scope.cachesetsMode = SELECTION_MODE.SINGLE; |
447 | + } else if(force) { |
448 | + $scope.cachesetsMode = SELECTION_MODE.MUTLI; |
449 | + } |
450 | + |
451 | + if($scope.cachesets.length === 0) { |
452 | + $scope.cachesetsAllSelected = false; |
453 | + } else if(cachesets.length === $scope.cachesets.length) { |
454 | + $scope.cachesetsAllSelected = true; |
455 | + } else { |
456 | + $scope.cachesetsAllSelected = false; |
457 | + } |
458 | + }; |
459 | + |
460 | + // Toggle the selection of the filesystem. |
461 | + $scope.toggleCacheSetSelect = function(cacheset) { |
462 | + cacheset.$selected = !cacheset.$selected; |
463 | + $scope.updateCacheSetsSelection(true); |
464 | + }; |
465 | + |
466 | + // Toggle the selection of all filesystems. |
467 | + $scope.toggleCacheSetAllSelect = function() { |
468 | + angular.forEach($scope.cachesets, function(cacheset) { |
469 | + if($scope.cachesetsAllSelected) { |
470 | + cacheset.$selected = false; |
471 | + } else { |
472 | + cacheset.$selected = true; |
473 | + } |
474 | + }); |
475 | + $scope.updateCacheSetsSelection(true); |
476 | + }; |
477 | + |
478 | + // Return true if checkboxes in the cache sets section should be |
479 | + // disabled. |
480 | + $scope.isCacheSetsDisabled = function() { |
481 | + return ( |
482 | + $scope.cachesetsMode !== SELECTION_MODE.NONE && |
483 | + $scope.cachesetsMode !== SELECTION_MODE.SINGLE && |
484 | + $scope.cachesetsMode !== SELECTION_MODE.MUTLI); |
485 | + }; |
486 | + |
487 | + // Cancel the current cache set operation. |
488 | + $scope.cacheSetCancel = function() { |
489 | + $scope.updateCacheSetsSelection(true); |
490 | + }; |
491 | + |
492 | + // Can delete the cache set. |
493 | + $scope.canDeleteCacheSet = function(cacheset) { |
494 | + return cacheset.used_by === ""; |
495 | + }; |
496 | + |
497 | + // Enter delete mode. |
498 | + $scope.cacheSetDelete = function() { |
499 | + $scope.cachesetsMode = SELECTION_MODE.DELETE; |
500 | + }; |
501 | + |
502 | + // Quickly enter delete by selecting the cache set first. |
503 | + $scope.quickCacheSetDelete = function(cacheset) { |
504 | + deselectAll($scope.cachesets); |
505 | + cacheset.$selected = true; |
506 | + $scope.updateCacheSetsSelection(true); |
507 | + $scope.cacheSetDelete(); |
508 | + }; |
509 | + |
510 | + // Confirm the delete action for cache set. |
511 | + $scope.cacheSetConfirmDelete = function(cacheset) { |
512 | + NodesManager.deleteCacheSet( |
513 | + $scope.node, cacheset.cache_set_id); |
514 | + |
515 | + var idx = $scope.cachesets.indexOf(cacheset); |
516 | + $scope.cachesets.splice(idx, 1); |
517 | + $scope.updateCacheSetsSelection(); |
518 | + }; |
519 | + |
520 | + // Return true if a cache set can be created. |
521 | + $scope.canCreateCacheSet = function() { |
522 | + if($scope.isAvailableDisabled()) { |
523 | + return false; |
524 | + } |
525 | + |
526 | + var selected = $scope.getSelectedAvailable(); |
527 | + if(selected.length === 1) { |
528 | + return !$scope.hasUnmountedFilesystem(selected[0]); |
529 | + } |
530 | + return false; |
531 | + }; |
532 | + |
533 | + // Called to create a cache set. |
534 | + $scope.createCacheSet = function() { |
535 | + if(!$scope.canCreateCacheSet()) { |
536 | + return; |
537 | + } |
538 | + |
539 | + // Create cache set. |
540 | + var disk = $scope.getSelectedAvailable()[0]; |
541 | + NodesManager.createCacheSet( |
542 | + $scope.node, disk.block_id, disk.partition_id); |
543 | + |
544 | + // Remove from available. |
545 | + var idx = $scope.available.indexOf(disk); |
546 | + $scope.available.splice(idx, 1); |
547 | + }; |
548 | + |
549 | + // Return true if a bcache can be created. |
550 | + $scope.canCreateBcache = function() { |
551 | + if($scope.isAvailableDisabled()) { |
552 | + return false; |
553 | + } |
554 | + |
555 | + var selected = $scope.getSelectedAvailable(); |
556 | + if(selected.length === 1) { |
557 | + var allowed = !$scope.hasUnmountedFilesystem(selected[0]); |
558 | + return allowed && $scope.cachesets.length > 0; |
559 | + } |
560 | + return false; |
561 | + }; |
562 | + |
563 | + // Enter bcache mode. |
564 | + $scope.createBcache = function() { |
565 | + if(!$scope.canCreateBcache()) { |
566 | + return; |
567 | + } |
568 | + $scope.availableMode = SELECTION_MODE.BCACHE; |
569 | + $scope.availableNew = { |
570 | + name: getNextBcacheName(), |
571 | + device: $scope.getSelectedAvailable()[0], |
572 | + cacheset: $scope.cachesets[0], |
573 | + cacheMode: "writeback", |
574 | + fstype: null, |
575 | + mountPoint: "" |
576 | + }; |
577 | + }; |
578 | + |
579 | + // Return true when the name of the new disk is invalid. |
580 | + $scope.isNewDiskNameInvalid = function() { |
581 | + if(!angular.isObject($scope.node) || |
582 | + !angular.isArray($scope.node.disks)) { |
583 | + return true; |
584 | + } |
585 | + |
586 | + if($scope.availableNew.name === "") { |
587 | + return true; |
588 | + } else { |
589 | + var i, j; |
590 | + for(i = 0; i < $scope.node.disks.length; i++) { |
591 | + var disk = $scope.node.disks[i]; |
592 | + if($scope.availableNew.name === disk.name) { |
593 | + return true; |
594 | + } |
595 | + if(angular.isArray(disk.partitions)) { |
596 | + for(j = 0; j < disk.partitions.length; j++) { |
597 | + var partition = disk.partitions[j]; |
598 | + if($scope.availableNew.name === partition.name) { |
599 | + return true; |
600 | + } |
601 | + } |
602 | + } |
603 | + } |
604 | + } |
605 | + return false; |
606 | + }; |
607 | + |
608 | + // Return true if bcache can be saved. |
609 | + $scope.createBcacheCanSave = function() { |
610 | + return ( |
611 | + !$scope.isNewDiskNameInvalid() && |
612 | + !$scope.isMountPointInvalid($scope.availableNew.mountPoint)); |
613 | + }; |
614 | + |
615 | + // Confirm and create the bcache device. |
616 | + $scope.availableConfirmCreateBcache = function() { |
617 | + if(!$scope.createBcacheCanSave()) { |
618 | + return; |
619 | + } |
620 | + |
621 | + // Create the bcache. |
622 | + var params = { |
623 | + name: $scope.availableNew.name, |
624 | + cache_set: $scope.availableNew.cacheset.cache_set_id, |
625 | + cache_mode: $scope.availableNew.cacheMode |
626 | + }; |
627 | + if($scope.availableNew.device.type === "partition") { |
628 | + params.partition_id = $scope.availableNew.device.partition_id; |
629 | + } else { |
630 | + params.block_id = $scope.availableNew.device.block_id; |
631 | + } |
632 | + if(angular.isString($scope.availableNew.fstype) && |
633 | + $scope.availableNew.fstype !== "") { |
634 | + params.fstype = $scope.availableNew.fstype; |
635 | + if($scope.availableNew.mountPoint !== "") { |
636 | + params.mount_point = $scope.availableNew.mountPoint; |
637 | + } |
638 | + } |
639 | + NodesManager.createBcache($scope.node, params); |
640 | + |
641 | + // Remove device from available. |
642 | + var idx = $scope.available.indexOf($scope.availableNew.device); |
643 | + $scope.available.splice(idx, 1); |
644 | + $scope.availableNew = {}; |
645 | + |
646 | + // Update the selection. |
647 | + $scope.updateAvailableSelection(true); |
648 | + }; |
649 | + |
650 | // Called to enter tag editing mode |
651 | $scope.editTags = function() { |
652 | if($scope.$parent.canEdit() && !$scope.editing) { |
653 | |
654 | === modified file 'src/maasserver/static/js/angular/controllers/tests/test_node_details_networking.js' |
655 | --- src/maasserver/static/js/angular/controllers/tests/test_node_details_networking.js 2015-10-19 20:12:24 +0000 |
656 | +++ src/maasserver/static/js/angular/controllers/tests/test_node_details_networking.js 2015-10-21 17:51:26 +0000 |
657 | @@ -2687,7 +2687,7 @@ |
658 | type: "alias" |
659 | }; |
660 | var nic2 = { |
661 | - id: makeInteger(0, 100), |
662 | + id: makeInteger(101, 200), |
663 | link_id: makeInteger(0, 100), |
664 | type: "alias" |
665 | }; |
666 | @@ -2711,7 +2711,7 @@ |
667 | vlan: {} |
668 | }; |
669 | var nic2 = { |
670 | - id: makeInteger(0, 100), |
671 | + id: makeInteger(101, 200), |
672 | link_id: makeInteger(0, 100), |
673 | type: "physical", |
674 | vlan: {} |
675 | @@ -2737,7 +2737,7 @@ |
676 | vlan: vlan |
677 | }; |
678 | var nic2 = { |
679 | - id: makeInteger(0, 100), |
680 | + id: makeInteger(101, 200), |
681 | link_id: makeInteger(0, 100), |
682 | type: "physical", |
683 | vlan: vlan |
684 | |
685 | === modified file 'src/maasserver/static/js/angular/controllers/tests/test_node_details_storage.js' |
686 | --- src/maasserver/static/js/angular/controllers/tests/test_node_details_storage.js 2015-10-19 20:12:24 +0000 |
687 | +++ src/maasserver/static/js/angular/controllers/tests/test_node_details_storage.js 2015-10-21 17:51:26 +0000 |
688 | @@ -4,6 +4,79 @@ |
689 | * Unit tests for NodeStorageController. |
690 | */ |
691 | |
692 | +describe("removeAvailableByNew", function() { |
693 | + |
694 | + // Load the MAAS module. |
695 | + beforeEach(module("MAAS")); |
696 | + |
697 | + // Load the removeAvailableByNew. |
698 | + var removeAvailableByNew; |
699 | + beforeEach(inject(function($filter) { |
700 | + removeAvailableByNew = $filter("removeAvailableByNew"); |
701 | + })); |
702 | + |
703 | + it("returns disks if undefined availableNew", function() { |
704 | + var i, disk, disks = []; |
705 | + for(i = 0; i < 3; i++) { |
706 | + disk = { |
707 | + id: i |
708 | + }; |
709 | + disks.push(disk); |
710 | + } |
711 | + expect(removeAvailableByNew(disks)).toBe(disks); |
712 | + }); |
713 | + |
714 | + it("returns disks if undefined device(s) in availableNew", function() { |
715 | + var i, disk, disks = []; |
716 | + for(i = 0; i < 3; i++) { |
717 | + disk = { |
718 | + id: i |
719 | + }; |
720 | + disks.push(disk); |
721 | + } |
722 | + var availableNew = {}; |
723 | + expect(removeAvailableByNew(disks, availableNew)).toBe(disks); |
724 | + }); |
725 | + |
726 | + it("removes availableNew.device from disks", function() { |
727 | + var i, disk, disks = []; |
728 | + for(i = 0; i < 3; i++) { |
729 | + disk = { |
730 | + id: i |
731 | + }; |
732 | + disks.push(disk); |
733 | + } |
734 | + |
735 | + var availableNew = { |
736 | + device: disks[0] |
737 | + }; |
738 | + var expectedDisks = angular.copy(disks); |
739 | + expectedDisks.splice(0, 1); |
740 | + |
741 | + expect(removeAvailableByNew(disks, availableNew)).toEqual( |
742 | + expectedDisks); |
743 | + }); |
744 | + |
745 | + it("removes availableNew.devices from disks", function() { |
746 | + var i, disk, disks = []; |
747 | + for(i = 0; i < 6; i++) { |
748 | + disk = { |
749 | + id: i |
750 | + }; |
751 | + disks.push(disk); |
752 | + } |
753 | + |
754 | + var availableNew = { |
755 | + devices: [disks[0], disks[1]] |
756 | + }; |
757 | + var expectedDisks = angular.copy(disks); |
758 | + expectedDisks.splice(0, 2); |
759 | + |
760 | + expect(removeAvailableByNew(disks, availableNew)).toEqual( |
761 | + expectedDisks); |
762 | + }); |
763 | +}); |
764 | + |
765 | describe("NodeStorageController", function() { |
766 | // Load the MAAS module. |
767 | beforeEach(module("MAAS")); |
768 | @@ -154,6 +227,25 @@ |
769 | used_for: "ext4 formatted filesystem mounted at /mnt." |
770 | } |
771 | ] |
772 | + }, |
773 | + { |
774 | + // Disk that is a cache set. |
775 | + id: 4, |
776 | + name: "cache0", |
777 | + model: "", |
778 | + serial: "", |
779 | + tags: [], |
780 | + type: "cache-set", |
781 | + size: Math.pow(1024, 4), |
782 | + size_human: "1024 GB", |
783 | + available_size: 0, |
784 | + available_size_human: "0 GB", |
785 | + used_size: Math.pow(1024, 4), |
786 | + used_size_human: "1024 GB", |
787 | + partition_table_type: null, |
788 | + used_for: "", |
789 | + filesystem: null, |
790 | + partitions: null |
791 | } |
792 | ]; |
793 | } |
794 | @@ -171,6 +263,10 @@ |
795 | expect($scope.availableMap).toEqual({}); |
796 | expect($scope.availableMode).toBeNull(); |
797 | expect($scope.availableAllSelected).toBe(false); |
798 | + expect($scope.cachesets).toEqual([]); |
799 | + expect($scope.cachesetsMap).toEqual({}); |
800 | + expect($scope.cachesetsMode).toBeNull(); |
801 | + expect($scope.cachesetsAllSelected).toBe(false); |
802 | expect($scope.used).toEqual([]); |
803 | }); |
804 | |
805 | @@ -195,6 +291,7 @@ |
806 | |
807 | var filesystems = [ |
808 | { |
809 | + type: "filesystem", |
810 | name: disks[2].name, |
811 | size_human: disks[2].size_human, |
812 | fstype: disks[2].filesystem.fstype, |
813 | @@ -204,6 +301,7 @@ |
814 | $selected: false |
815 | }, |
816 | { |
817 | + type: "filesystem", |
818 | name: disks[3].partitions[1].name, |
819 | size_human: disks[3].partitions[1].size_human, |
820 | fstype: disks[3].partitions[1].filesystem.fstype, |
821 | @@ -213,6 +311,16 @@ |
822 | $selected: false |
823 | } |
824 | ]; |
825 | + var cachesets = [ |
826 | + { |
827 | + type: "cache-set", |
828 | + name: disks[4].name, |
829 | + size_human: disks[4].size_human, |
830 | + cache_set_id: disks[4].id, |
831 | + used_by: disks[4].used_for, |
832 | + $selected: false |
833 | + } |
834 | + ]; |
835 | var available = [ |
836 | { |
837 | name: disks[0].name, |
838 | @@ -297,6 +405,7 @@ |
839 | $rootScope.$digest(); |
840 | expect($scope.has_disks).toEqual(true); |
841 | expect($scope.filesystems).toEqual(filesystems); |
842 | + expect($scope.cachesets).toEqual(cachesets); |
843 | expect($scope.available).toEqual(available); |
844 | expect($scope.used).toEqual(used); |
845 | }); |
846 | @@ -306,14 +415,17 @@ |
847 | var disks = makeDisks(); |
848 | node.disks = disks; |
849 | |
850 | - // Load the filesystems, available, and used once. |
851 | + // Load the filesystems, cachesets, available, and used once. |
852 | $scope.nodeLoaded(); |
853 | $rootScope.$digest(); |
854 | |
855 | - // Set all filesystems and available to selected. |
856 | + // Set all filesystems, cachesets, and available to selected. |
857 | angular.forEach($scope.filesystems, function(filesystem) { |
858 | filesystem.$selected = true; |
859 | }); |
860 | + angular.forEach($scope.cachesets, function(cacheset) { |
861 | + cacheset.$selected = true; |
862 | + }); |
863 | angular.forEach($scope.available, function(disk) { |
864 | disk.$selected = true; |
865 | }); |
866 | @@ -324,18 +436,21 @@ |
867 | options.push(disk.$options); |
868 | }); |
869 | |
870 | - // Force the disks to change so the filesystems, available, and used |
871 | - // are reloaded. |
872 | + // Force the disks to change so the filesystems, cachesets, available, |
873 | + // and used are reloaded. |
874 | var firstFilesystem = $scope.filesystems[0]; |
875 | node.disks = angular.copy(node.disks); |
876 | $rootScope.$digest(); |
877 | expect($scope.filesystems[0]).not.toBe(firstFilesystem); |
878 | expect($scope.filesystems[0]).toEqual(firstFilesystem); |
879 | |
880 | - // All filesystems and available should be selected. |
881 | + // All filesystems, cachesets and available should be selected. |
882 | angular.forEach($scope.filesystems, function(filesystem) { |
883 | expect(filesystem.$selected).toBe(true); |
884 | }); |
885 | + angular.forEach($scope.cachesets, function(cacheset) { |
886 | + expect(cacheset.$selected).toBe(true); |
887 | + }); |
888 | angular.forEach($scope.available, function(disk) { |
889 | expect(disk.$selected).toBe(true); |
890 | }); |
891 | @@ -346,6 +461,51 @@ |
892 | }); |
893 | }); |
894 | |
895 | + it("availableNew.device object is updated", function() { |
896 | + var controller = makeController(); |
897 | + var disks = makeDisks(); |
898 | + node.disks = disks; |
899 | + |
900 | + // Load the filesystems, cachesets, available, and used once. |
901 | + $scope.nodeLoaded(); |
902 | + $rootScope.$digest(); |
903 | + |
904 | + // Set availableNew.device to a disk from available. |
905 | + var disk = $scope.available[0]; |
906 | + $scope.availableNew.device = disk; |
907 | + |
908 | + // Force the update. The device should be the same value but |
909 | + // a new object. |
910 | + node.disks = angular.copy(node.disks); |
911 | + $rootScope.$digest(); |
912 | + expect($scope.availableNew.device).toEqual(disk); |
913 | + expect($scope.availableNew.device).not.toBe(disk); |
914 | + }); |
915 | + |
916 | + it("availableNew.devices array is updated", function() { |
917 | + var controller = makeController(); |
918 | + var disks = makeDisks(); |
919 | + node.disks = disks; |
920 | + |
921 | + // Load the filesystems, cachesets, available, and used once. |
922 | + $scope.nodeLoaded(); |
923 | + $rootScope.$digest(); |
924 | + |
925 | + // Set availableNew.device to a disk from available. |
926 | + var disk0 = $scope.available[0]; |
927 | + var disk1 = $scope.available[1]; |
928 | + $scope.availableNew.devices = [disk0, disk1]; |
929 | + |
930 | + // Force the update. The devices should be the same values but |
931 | + // a new objects. |
932 | + node.disks = angular.copy(node.disks); |
933 | + $rootScope.$digest(); |
934 | + expect($scope.availableNew.devices[0]).toEqual(disk0); |
935 | + expect($scope.availableNew.devices[0]).not.toBe(disk0); |
936 | + expect($scope.availableNew.devices[1]).toEqual(disk1); |
937 | + expect($scope.availableNew.devices[1]).not.toBe(disk1); |
938 | + }); |
939 | + |
940 | describe("getSelectedFilesystems", function() { |
941 | |
942 | it("returns selected filesystems", function() { |
943 | @@ -1834,6 +1994,644 @@ |
944 | }); |
945 | }); |
946 | |
947 | + describe("getSelectedCacheSets", function() { |
948 | + |
949 | + it("returns selected cachesets", function() { |
950 | + var controller = makeController(); |
951 | + var cachesets = [ |
952 | + { $selected: true }, |
953 | + { $selected: true }, |
954 | + { $selected: false }, |
955 | + { $selected: false } |
956 | + ]; |
957 | + $scope.cachesets = cachesets; |
958 | + expect($scope.getSelectedCacheSets()).toEqual( |
959 | + [cachesets[0], cachesets[1]]); |
960 | + }); |
961 | + }); |
962 | + |
963 | + describe("updateCacheSetsSelection", function() { |
964 | + |
965 | + it("sets cachesetsMode to NONE when none selected", function() { |
966 | + var controller = makeController(); |
967 | + spyOn($scope, "getSelectedCacheSets").and.returnValue([]); |
968 | + $scope.cachesetsMode = "other"; |
969 | + |
970 | + $scope.updateCacheSetsSelection(); |
971 | + |
972 | + expect($scope.cachesetsMode).toBeNull(); |
973 | + }); |
974 | + |
975 | + it("doesn't sets cachesetsMode to SINGLE when not force", function() { |
976 | + var controller = makeController(); |
977 | + spyOn($scope, "getSelectedCacheSets").and.returnValue([{}]); |
978 | + $scope.cachesetsMode = "other"; |
979 | + |
980 | + $scope.updateCacheSetsSelection(); |
981 | + |
982 | + expect($scope.cachesetsMode).toBe("other"); |
983 | + }); |
984 | + |
985 | + it("sets cachesetsMode to SINGLE when force", function() { |
986 | + var controller = makeController(); |
987 | + spyOn($scope, "getSelectedCacheSets").and.returnValue([{}]); |
988 | + $scope.cachesetsMode = "other"; |
989 | + |
990 | + $scope.updateCacheSetsSelection(true); |
991 | + |
992 | + expect($scope.cachesetsMode).toBe("single"); |
993 | + }); |
994 | + |
995 | + it("doesn't sets cachesetsMode to MUTLI when not force", function() { |
996 | + var controller = makeController(); |
997 | + spyOn($scope, "getSelectedCacheSets").and.returnValue([{}, {}]); |
998 | + $scope.cachesetsMode = "other"; |
999 | + |
1000 | + $scope.updateCacheSetsSelection(); |
1001 | + |
1002 | + expect($scope.cachesetsMode).toBe("other"); |
1003 | + }); |
1004 | + |
1005 | + it("sets cachesetsMode to MULTI when force", function() { |
1006 | + var controller = makeController(); |
1007 | + spyOn($scope, "getSelectedCacheSets").and.returnValue([{}, {}]); |
1008 | + $scope.cachesetsMode = "other"; |
1009 | + |
1010 | + $scope.updateCacheSetsSelection(true); |
1011 | + |
1012 | + expect($scope.cachesetsMode).toBe("multi"); |
1013 | + }); |
1014 | + |
1015 | + it("sets cachesetsAllSelected to false when none selected", |
1016 | + function() { |
1017 | + var controller = makeController(); |
1018 | + spyOn($scope, "getSelectedCacheSets").and.returnValue([]); |
1019 | + $scope.cachesetsAllSelected = true; |
1020 | + |
1021 | + $scope.updateCacheSetsSelection(); |
1022 | + |
1023 | + expect($scope.cachesetsAllSelected).toBe(false); |
1024 | + }); |
1025 | + |
1026 | + it("sets cachesetsAllSelected to false when not all selected", |
1027 | + function() { |
1028 | + var controller = makeController(); |
1029 | + $scope.cachesets = [{}, {}]; |
1030 | + spyOn($scope, "getSelectedCacheSets").and.returnValue([{}]); |
1031 | + $scope.cachesetsAllSelected = true; |
1032 | + |
1033 | + $scope.updateCacheSetsSelection(); |
1034 | + |
1035 | + expect($scope.cachesetsAllSelected).toBe(false); |
1036 | + }); |
1037 | + |
1038 | + it("sets cachesetsAllSelected to true when all selected", |
1039 | + function() { |
1040 | + var controller = makeController(); |
1041 | + $scope.cachesets = [{}, {}]; |
1042 | + spyOn($scope, "getSelectedCacheSets").and.returnValue( |
1043 | + [{}, {}]); |
1044 | + $scope.cachesetsAllSelected = false; |
1045 | + |
1046 | + $scope.updateCacheSetsSelection(); |
1047 | + |
1048 | + expect($scope.cachesetsAllSelected).toBe(true); |
1049 | + }); |
1050 | + }); |
1051 | + |
1052 | + describe("toggleCacheSetSelect", function() { |
1053 | + |
1054 | + it("inverts $selected", function() { |
1055 | + var controller = makeController(); |
1056 | + var cacheset = { $selected: true }; |
1057 | + spyOn($scope, "updateCacheSetsSelection"); |
1058 | + |
1059 | + $scope.toggleCacheSetSelect(cacheset); |
1060 | + |
1061 | + expect(cacheset.$selected).toBe(false); |
1062 | + $scope.toggleCacheSetSelect(cacheset); |
1063 | + expect(cacheset.$selected).toBe(true); |
1064 | + expect($scope.updateCacheSetsSelection).toHaveBeenCalledWith( |
1065 | + true); |
1066 | + }); |
1067 | + }); |
1068 | + |
1069 | + describe("toggleCacheSetAllSelect", function() { |
1070 | + |
1071 | + it("sets all to true if not all selected", function() { |
1072 | + var controller = makeController(); |
1073 | + var cachesets = [{ $selected: true }, { $selected: false }]; |
1074 | + $scope.cachesets = cachesets; |
1075 | + $scope.cachesetsAllSelected = false; |
1076 | + spyOn($scope, "updateCacheSetsSelection"); |
1077 | + |
1078 | + $scope.toggleCacheSetAllSelect(); |
1079 | + |
1080 | + expect(cachesets[0].$selected).toBe(true); |
1081 | + expect(cachesets[1].$selected).toBe(true); |
1082 | + expect($scope.updateCacheSetsSelection).toHaveBeenCalledWith( |
1083 | + true); |
1084 | + }); |
1085 | + |
1086 | + it("sets all to false if all selected", function() { |
1087 | + var controller = makeController(); |
1088 | + var cachesets = [{ $selected: true }, { $selected: true }]; |
1089 | + $scope.cachesets = cachesets; |
1090 | + $scope.cachesetsAllSelected = true; |
1091 | + spyOn($scope, "updateCacheSetsSelection"); |
1092 | + |
1093 | + $scope.toggleCacheSetAllSelect(); |
1094 | + |
1095 | + expect(cachesets[0].$selected).toBe(false); |
1096 | + expect(cachesets[1].$selected).toBe(false); |
1097 | + expect($scope.updateCacheSetsSelection).toHaveBeenCalledWith( |
1098 | + true); |
1099 | + }); |
1100 | + }); |
1101 | + |
1102 | + describe("isCacheSetsDisabled", function() { |
1103 | + |
1104 | + it("returns false for NONE", function() { |
1105 | + var controller = makeController(); |
1106 | + $scope.cachesetsMode = null; |
1107 | + |
1108 | + expect($scope.isCacheSetsDisabled()).toBe(false); |
1109 | + }); |
1110 | + |
1111 | + it("returns false for SINGLE", function() { |
1112 | + var controller = makeController(); |
1113 | + $scope.cachesetsMode = "single"; |
1114 | + |
1115 | + expect($scope.isCacheSetsDisabled()).toBe(false); |
1116 | + }); |
1117 | + |
1118 | + it("returns false for MULTI", function() { |
1119 | + var controller = makeController(); |
1120 | + $scope.cachesetsMode = "multi"; |
1121 | + |
1122 | + expect($scope.isCacheSetsDisabled()).toBe(false); |
1123 | + }); |
1124 | + |
1125 | + it("returns true for DELETE", function() { |
1126 | + var controller = makeController(); |
1127 | + $scope.cachesetsMode = "delete"; |
1128 | + |
1129 | + expect($scope.isCacheSetsDisabled()).toBe(true); |
1130 | + }); |
1131 | + }); |
1132 | + |
1133 | + describe("cacheSetCancel", function() { |
1134 | + |
1135 | + it("calls updateCacheSetsSelection with force true", function() { |
1136 | + var controller = makeController(); |
1137 | + spyOn($scope, "updateCacheSetsSelection"); |
1138 | + |
1139 | + $scope.cacheSetCancel(); |
1140 | + |
1141 | + expect($scope.updateCacheSetsSelection).toHaveBeenCalledWith( |
1142 | + true); |
1143 | + }); |
1144 | + }); |
1145 | + |
1146 | + describe("canDeleteCacheSet", function() { |
1147 | + |
1148 | + it("returns true when not being used", function() { |
1149 | + var controller = makeController(); |
1150 | + var cacheset = { used_by: "" }; |
1151 | + |
1152 | + expect($scope.canDeleteCacheSet(cacheset)).toBe(true); |
1153 | + }); |
1154 | + |
1155 | + it("returns false when being used", function() { |
1156 | + var controller = makeController(); |
1157 | + var cacheset = { used_by: "bcache0" }; |
1158 | + |
1159 | + expect($scope.canDeleteCacheSet(cacheset)).toBe(false); |
1160 | + }); |
1161 | + }); |
1162 | + |
1163 | + describe("cacheSetDelete", function() { |
1164 | + |
1165 | + it("sets cachesetsMode to DELETE", function() { |
1166 | + var controller = makeController(); |
1167 | + $scope.cachesetsMode = "other"; |
1168 | + |
1169 | + $scope.cacheSetDelete(); |
1170 | + |
1171 | + expect($scope.cachesetsMode).toBe("delete"); |
1172 | + }); |
1173 | + }); |
1174 | + |
1175 | + describe("quickCacheSetDelete", function() { |
1176 | + |
1177 | + it("selects cacheset and calls cacheSetDelete", function() { |
1178 | + var controller = makeController(); |
1179 | + var cachesets = [{ $selected: true }, { $selected: false }]; |
1180 | + $scope.cachesets = cachesets; |
1181 | + spyOn($scope, "updateCacheSetsSelection"); |
1182 | + spyOn($scope, "cacheSetDelete"); |
1183 | + |
1184 | + $scope.quickCacheSetDelete(cachesets[1]); |
1185 | + |
1186 | + expect(cachesets[0].$selected).toBe(false); |
1187 | + expect(cachesets[1].$selected).toBe(true); |
1188 | + expect($scope.updateCacheSetsSelection).toHaveBeenCalledWith( |
1189 | + true); |
1190 | + expect($scope.cacheSetDelete).toHaveBeenCalled(); |
1191 | + }); |
1192 | + }); |
1193 | + |
1194 | + describe("cacheSetConfirmDelete", function() { |
1195 | + |
1196 | + it("calls NodesManager.deleteCacheSet and removes from list", |
1197 | + function() { |
1198 | + var controller = makeController(); |
1199 | + var cacheset = { |
1200 | + cache_set_id: makeInteger(0, 100) |
1201 | + }; |
1202 | + $scope.cachesets = [cacheset]; |
1203 | + spyOn(NodesManager, "deleteCacheSet"); |
1204 | + spyOn($scope, "updateCacheSetsSelection"); |
1205 | + |
1206 | + $scope.cacheSetConfirmDelete(cacheset); |
1207 | + |
1208 | + expect(NodesManager.deleteCacheSet).toHaveBeenCalledWith( |
1209 | + node, cacheset.cache_set_id); |
1210 | + expect($scope.cachesets).toEqual([]); |
1211 | + expect($scope.updateCacheSetsSelection).toHaveBeenCalledWith(); |
1212 | + }); |
1213 | + }); |
1214 | + |
1215 | + describe("canCreateCacheSet", function() { |
1216 | + |
1217 | + it("returns false if isAvailableDisabled returns true", function() { |
1218 | + var controller = makeController(); |
1219 | + spyOn($scope, "isAvailableDisabled").and.returnValue(true); |
1220 | + |
1221 | + expect($scope.canCreateCacheSet()).toBe(false); |
1222 | + }); |
1223 | + |
1224 | + it("returns false if two selected", function() { |
1225 | + var controller = makeController(); |
1226 | + spyOn($scope, "isAvailableDisabled").and.returnValue(false); |
1227 | + $scope.available = [ { $selected: true }, { $selected: true }]; |
1228 | + |
1229 | + expect($scope.canCreateCacheSet()).toBe(false); |
1230 | + }); |
1231 | + |
1232 | + it("returns false if selected has fstype", function() { |
1233 | + var controller = makeController(); |
1234 | + spyOn($scope, "isAvailableDisabled").and.returnValue(false); |
1235 | + $scope.available = [ |
1236 | + { |
1237 | + fstype: "ext4", |
1238 | + $selected: true |
1239 | + } |
1240 | + ]; |
1241 | + |
1242 | + expect($scope.canCreateCacheSet()).toBe(false); |
1243 | + }); |
1244 | + |
1245 | + it("returns true if selected has no fstype", function() { |
1246 | + var controller = makeController(); |
1247 | + spyOn($scope, "isAvailableDisabled").and.returnValue(false); |
1248 | + $scope.available = [ |
1249 | + { |
1250 | + fstype: null, |
1251 | + $selected: true |
1252 | + } |
1253 | + ]; |
1254 | + |
1255 | + expect($scope.canCreateCacheSet()).toBe(true); |
1256 | + }); |
1257 | + }); |
1258 | + |
1259 | + describe("createCacheSet", function() { |
1260 | + |
1261 | + it("does nothing if canCreateCacheSet returns false", function() { |
1262 | + var controller = makeController(); |
1263 | + var disk = { |
1264 | + block_id: makeInteger(0, 100), |
1265 | + partition_id: makeInteger(0, 100), |
1266 | + $selected: true |
1267 | + }; |
1268 | + $scope.available = [disk]; |
1269 | + spyOn($scope, "canCreateCacheSet").and.returnValue(false); |
1270 | + spyOn(NodesManager, "createCacheSet"); |
1271 | + |
1272 | + $scope.createCacheSet(); |
1273 | + expect(NodesManager.createCacheSet).not.toHaveBeenCalled(); |
1274 | + }); |
1275 | + |
1276 | + it("calls NodesManager.createCacheSet and removes from available", |
1277 | + function() { |
1278 | + var controller = makeController(); |
1279 | + var disk = { |
1280 | + block_id: makeInteger(0, 100), |
1281 | + partition_id: makeInteger(0, 100), |
1282 | + $selected: true |
1283 | + }; |
1284 | + $scope.available = [disk]; |
1285 | + spyOn($scope, "canCreateCacheSet").and.returnValue(true); |
1286 | + spyOn(NodesManager, "createCacheSet"); |
1287 | + |
1288 | + $scope.createCacheSet(); |
1289 | + expect(NodesManager.createCacheSet).toHaveBeenCalledWith( |
1290 | + node, disk.block_id, disk.partition_id); |
1291 | + expect($scope.available).toEqual([]); |
1292 | + }); |
1293 | + }); |
1294 | + |
1295 | + describe("canCreateBcache", function() { |
1296 | + |
1297 | + it("returns false when isAvailableDisabled is true", function() { |
1298 | + var controller = makeController(); |
1299 | + spyOn($scope, "isAvailableDisabled").and.returnValue(true); |
1300 | + |
1301 | + expect($scope.canCreateBcache()).toBe(false); |
1302 | + }); |
1303 | + |
1304 | + it("returns false if two selected", function() { |
1305 | + var controller = makeController(); |
1306 | + spyOn($scope, "isAvailableDisabled").and.returnValue(false); |
1307 | + $scope.available = [ { $selected: true }, { $selected: true }]; |
1308 | + |
1309 | + expect($scope.canCreateBcache()).toBe(false); |
1310 | + }); |
1311 | + |
1312 | + it("returns false if selected has fstype", function() { |
1313 | + var controller = makeController(); |
1314 | + spyOn($scope, "isAvailableDisabled").and.returnValue(false); |
1315 | + $scope.available = [ |
1316 | + { |
1317 | + fstype: "ext4", |
1318 | + $selected: true |
1319 | + } |
1320 | + ]; |
1321 | + $scope.cachesets = [{}]; |
1322 | + |
1323 | + expect($scope.canCreateBcache()).toBe(false); |
1324 | + }); |
1325 | + |
1326 | + it("returns false if selected has no fstype but not cachesets ", |
1327 | + function() { |
1328 | + var controller = makeController(); |
1329 | + spyOn($scope, "isAvailableDisabled").and.returnValue(false); |
1330 | + $scope.available = [ |
1331 | + { |
1332 | + fstype: null, |
1333 | + $selected: true |
1334 | + } |
1335 | + ]; |
1336 | + $scope.cachesets = []; |
1337 | + |
1338 | + expect($scope.canCreateBcache()).toBe(false); |
1339 | + }); |
1340 | + |
1341 | + it("returns true if selected has no fstype but has cachesets ", |
1342 | + function() { |
1343 | + var controller = makeController(); |
1344 | + spyOn($scope, "isAvailableDisabled").and.returnValue(false); |
1345 | + $scope.available = [ |
1346 | + { |
1347 | + fstype: null, |
1348 | + $selected: true |
1349 | + } |
1350 | + ]; |
1351 | + $scope.cachesets = [{}]; |
1352 | + |
1353 | + expect($scope.canCreateBcache()).toBe(true); |
1354 | + }); |
1355 | + }); |
1356 | + |
1357 | + describe("createBcache", function() { |
1358 | + |
1359 | + it("does nothing if canCreateBcache returns false", function() { |
1360 | + var controller = makeController(); |
1361 | + $scope.availableMode = "other"; |
1362 | + spyOn($scope, "canCreateBcache").and.returnValue(false); |
1363 | + |
1364 | + $scope.createBcache(); |
1365 | + expect($scope.availableMode).toBe("other"); |
1366 | + }); |
1367 | + |
1368 | + it("sets availableMode and availableNew", function() { |
1369 | + var controller = makeController(); |
1370 | + $scope.availableMode = "other"; |
1371 | + spyOn($scope, "canCreateBcache").and.returnValue(true); |
1372 | + |
1373 | + // Add bcache name to create a name after that index. |
1374 | + var otherBcache = { |
1375 | + name: "bcache4" |
1376 | + }; |
1377 | + node.disks = [otherBcache]; |
1378 | + |
1379 | + // Will be set as the device. |
1380 | + var disk = { |
1381 | + $selected: true |
1382 | + }; |
1383 | + $scope.available = [disk]; |
1384 | + |
1385 | + // Will be set as the cacheset. |
1386 | + var cacheset = {}; |
1387 | + $scope.cachesets = [cacheset]; |
1388 | + |
1389 | + $scope.createBcache(); |
1390 | + expect($scope.availableMode).toBe("bcache"); |
1391 | + expect($scope.availableNew).toEqual({ |
1392 | + name: "bcache5", |
1393 | + device: disk, |
1394 | + cacheset: cacheset, |
1395 | + cacheMode: "writeback", |
1396 | + fstype: null, |
1397 | + mountPoint: "" |
1398 | + }); |
1399 | + expect($scope.availableNew.device).toBe(disk); |
1400 | + expect($scope.availableNew.cacheset).toBe(cacheset); |
1401 | + }); |
1402 | + }); |
1403 | + |
1404 | + describe("isNewDiskNameInvalid", function() { |
1405 | + |
1406 | + it("returns true if blank name", function() { |
1407 | + var controller = makeController(); |
1408 | + $scope.node.disks = []; |
1409 | + $scope.availableNew.name = ""; |
1410 | + |
1411 | + expect($scope.isNewDiskNameInvalid()).toBe(true); |
1412 | + }); |
1413 | + |
1414 | + it("returns true if name used by disk", function() { |
1415 | + var controller = makeController(); |
1416 | + var name = makeName("disk"); |
1417 | + $scope.node.disks = [{ |
1418 | + name: name |
1419 | + }]; |
1420 | + $scope.availableNew.name = name; |
1421 | + |
1422 | + expect($scope.isNewDiskNameInvalid()).toBe(true); |
1423 | + }); |
1424 | + |
1425 | + it("returns true if name used by partition", function() { |
1426 | + var controller = makeController(); |
1427 | + var name = makeName("disk"); |
1428 | + $scope.node.disks = [{ |
1429 | + name: makeName("other"), |
1430 | + partitions: [ |
1431 | + { |
1432 | + name: name |
1433 | + } |
1434 | + ] |
1435 | + }]; |
1436 | + $scope.availableNew.name = name; |
1437 | + |
1438 | + expect($scope.isNewDiskNameInvalid()).toBe(true); |
1439 | + }); |
1440 | + |
1441 | + it("returns false if the name is not already used", function() { |
1442 | + var controller = makeController(); |
1443 | + var name = makeName("disk"); |
1444 | + $scope.node.disks = [{ |
1445 | + name: makeName("other"), |
1446 | + partitions: [ |
1447 | + { |
1448 | + name: makeName("part") |
1449 | + } |
1450 | + ] |
1451 | + }]; |
1452 | + $scope.availableNew.name = name; |
1453 | + |
1454 | + expect($scope.isNewDiskNameInvalid()).toBe(false); |
1455 | + }); |
1456 | + }); |
1457 | + |
1458 | + describe("createBcacheCanSave", function() { |
1459 | + |
1460 | + it("returns false if isNewDiskNameInvalid returns true", function() { |
1461 | + var controller = makeController(); |
1462 | + $scope.availableNew.mountPoint = "/"; |
1463 | + spyOn($scope, "isNewDiskNameInvalid").and.returnValue(true); |
1464 | + |
1465 | + expect($scope.createBcacheCanSave()).toBe(false); |
1466 | + }); |
1467 | + |
1468 | + it("returns false if isMountPointInvalid returns true", function() { |
1469 | + var controller = makeController(); |
1470 | + $scope.availableNew.mountPoint = "not/absolute"; |
1471 | + spyOn($scope, "isNewDiskNameInvalid").and.returnValue(false); |
1472 | + |
1473 | + expect($scope.createBcacheCanSave()).toBe(false); |
1474 | + }); |
1475 | + |
1476 | + it("returns true if both return false", function() { |
1477 | + var controller = makeController(); |
1478 | + $scope.availableNew.mountPoint = "/"; |
1479 | + spyOn($scope, "isNewDiskNameInvalid").and.returnValue(false); |
1480 | + |
1481 | + expect($scope.createBcacheCanSave()).toBe(true); |
1482 | + }); |
1483 | + }); |
1484 | + |
1485 | + describe("availableConfirmCreateBcache", function() { |
1486 | + |
1487 | + it("does nothing if createBcacheCanSave returns false", function() { |
1488 | + var controller = makeController(); |
1489 | + spyOn($scope, "createBcacheCanSave").and.returnValue(false); |
1490 | + var availableNew = { |
1491 | + name: makeName("bcache"), |
1492 | + cacheset: { |
1493 | + cache_set_id: makeInteger(0, 100) |
1494 | + }, |
1495 | + cacheMode: "writearound", |
1496 | + device: { |
1497 | + type: "partition", |
1498 | + partition_id: makeInteger(0, 100) |
1499 | + }, |
1500 | + fstype: null, |
1501 | + mountPoint: "" |
1502 | + }; |
1503 | + $scope.availableNew = availableNew; |
1504 | + spyOn(NodesManager, "createBcache"); |
1505 | + |
1506 | + $scope.availableConfirmCreateBcache(); |
1507 | + expect(NodesManager.createBcache).not.toHaveBeenCalled(); |
1508 | + }); |
1509 | + |
1510 | + it("calls NodesManager.createBcache for partition", function() { |
1511 | + var controller = makeController(); |
1512 | + spyOn($scope, "createBcacheCanSave").and.returnValue(true); |
1513 | + var device = { |
1514 | + type: "partition", |
1515 | + partition_id: makeInteger(0, 100), |
1516 | + $selected: true |
1517 | + }; |
1518 | + var availableNew = { |
1519 | + name: makeName("bcache"), |
1520 | + cacheset: { |
1521 | + cache_set_id: makeInteger(0, 100) |
1522 | + }, |
1523 | + cacheMode: "writearound", |
1524 | + device: device, |
1525 | + fstype: "ext4", |
1526 | + mountPoint: "/" |
1527 | + }; |
1528 | + $scope.available = [device]; |
1529 | + $scope.availableNew = availableNew; |
1530 | + spyOn(NodesManager, "createBcache"); |
1531 | + spyOn($scope, "updateAvailableSelection"); |
1532 | + |
1533 | + $scope.availableConfirmCreateBcache(); |
1534 | + expect(NodesManager.createBcache).toHaveBeenCalledWith( |
1535 | + node, { |
1536 | + name: availableNew.name, |
1537 | + cache_set: availableNew.cacheset.cache_set_id, |
1538 | + cache_mode: "writearound", |
1539 | + partition_id: device.partition_id, |
1540 | + fstype: "ext4", |
1541 | + mount_point: "/" |
1542 | + }); |
1543 | + expect($scope.available).toEqual([]); |
1544 | + expect($scope.updateAvailableSelection).toHaveBeenCalledWith( |
1545 | + true); |
1546 | + }); |
1547 | + |
1548 | + it("calls NodesManager.createBcache for block device", function() { |
1549 | + var controller = makeController(); |
1550 | + spyOn($scope, "createBcacheCanSave").and.returnValue(true); |
1551 | + var device = { |
1552 | + type: "physical", |
1553 | + block_id: makeInteger(0, 100), |
1554 | + $selected: true |
1555 | + }; |
1556 | + var availableNew = { |
1557 | + name: makeName("bcache"), |
1558 | + cacheset: { |
1559 | + cache_set_id: makeInteger(0, 100) |
1560 | + }, |
1561 | + cacheMode: "writearound", |
1562 | + device: device, |
1563 | + fstype: null, |
1564 | + mountPoint: "/" |
1565 | + }; |
1566 | + $scope.available = [device]; |
1567 | + $scope.availableNew = availableNew; |
1568 | + spyOn(NodesManager, "createBcache"); |
1569 | + spyOn($scope, "updateAvailableSelection"); |
1570 | + |
1571 | + $scope.availableConfirmCreateBcache(); |
1572 | + expect(NodesManager.createBcache).toHaveBeenCalledWith( |
1573 | + node, { |
1574 | + name: availableNew.name, |
1575 | + cache_set: availableNew.cacheset.cache_set_id, |
1576 | + cache_mode: "writearound", |
1577 | + block_id: device.block_id |
1578 | + }); |
1579 | + expect($scope.available).toEqual([]); |
1580 | + expect($scope.updateAvailableSelection).toHaveBeenCalledWith( |
1581 | + true); |
1582 | + }); |
1583 | + }); |
1584 | + |
1585 | describe("editTags", function() { |
1586 | |
1587 | it("doesnt sets editing to true if cannot edit", function() { |
1588 | |
1589 | === modified file 'src/maasserver/static/js/angular/factories/nodes.js' |
1590 | --- src/maasserver/static/js/angular/factories/nodes.js 2015-10-19 17:56:35 +0000 |
1591 | +++ src/maasserver/static/js/angular/factories/nodes.js 2015-10-21 17:51:26 +0000 |
1592 | @@ -211,6 +211,19 @@ |
1593 | return RegionConnection.callMethod(method, params); |
1594 | }; |
1595 | |
1596 | + // Delete a cache set. |
1597 | + NodesManager.prototype.deleteCacheSet = function( |
1598 | + node, cache_set_id) { |
1599 | + var self = this; |
1600 | + var method = this._handler + ".delete_cache_set"; |
1601 | + var params = { |
1602 | + system_id: node.system_id, |
1603 | + cache_set_id: cache_set_id |
1604 | + }; |
1605 | + return RegionConnection.callMethod(method, params); |
1606 | + }; |
1607 | + |
1608 | + // Create a new partition. |
1609 | NodesManager.prototype.createPartition = function( |
1610 | node, block_id, size) { |
1611 | var self = this; |
1612 | @@ -223,5 +236,29 @@ |
1613 | return RegionConnection.callMethod(method, params); |
1614 | }; |
1615 | |
1616 | + // Create a new cache set. |
1617 | + NodesManager.prototype.createCacheSet = function( |
1618 | + node, block_id, partition_id) { |
1619 | + var self = this; |
1620 | + var method = this._handler + ".create_cache_set"; |
1621 | + var params = { |
1622 | + system_id: node.system_id, |
1623 | + block_id: block_id, |
1624 | + partition_id: partition_id |
1625 | + }; |
1626 | + return RegionConnection.callMethod(method, params); |
1627 | + }; |
1628 | + |
1629 | + // Create a new bcache device. |
1630 | + NodesManager.prototype.createBcache = function( |
1631 | + node, params) { |
1632 | + if(!angular.isObject(params)) { |
1633 | + params = {}; |
1634 | + } |
1635 | + params.system_id = node.system_id; |
1636 | + return RegionConnection.callMethod( |
1637 | + "node.create_bcache", params); |
1638 | + }; |
1639 | + |
1640 | return new NodesManager(); |
1641 | }]); |
1642 | |
1643 | === modified file 'src/maasserver/static/js/angular/factories/tests/test_nodes.js' |
1644 | --- src/maasserver/static/js/angular/factories/tests/test_nodes.js 2015-10-19 20:12:24 +0000 |
1645 | +++ src/maasserver/static/js/angular/factories/tests/test_nodes.js 2015-10-21 17:51:26 +0000 |
1646 | @@ -476,6 +476,27 @@ |
1647 | }); |
1648 | }); |
1649 | |
1650 | + describe("deleteCacheSet", function() { |
1651 | + |
1652 | + it("calls node.delete_cache_set with correct params", |
1653 | + function(done) { |
1654 | + var node = makeNode(), cache_set_id = makeInteger(0, 100); |
1655 | + webSocket.returnData.push(makeFakeResponse("deleted")); |
1656 | + NodesManager.deleteCacheSet(node, cache_set_id).then( |
1657 | + function() { |
1658 | + var sentObject = angular.fromJson( |
1659 | + webSocket.sentData[0]); |
1660 | + expect(sentObject.method).toBe( |
1661 | + "node.delete_cache_set"); |
1662 | + expect(sentObject.params.system_id).toBe( |
1663 | + node.system_id); |
1664 | + expect(sentObject.params.cache_set_id).toBe( |
1665 | + cache_set_id); |
1666 | + done(); |
1667 | + }); |
1668 | + }); |
1669 | + }); |
1670 | + |
1671 | describe("createPartition", function() { |
1672 | |
1673 | it("calls node.create_partition with correct params", |
1674 | @@ -499,4 +520,69 @@ |
1675 | }); |
1676 | }); |
1677 | }); |
1678 | + |
1679 | + describe("createCacheSet", function() { |
1680 | + |
1681 | + it("calls node.create_cache_set", function(done) { |
1682 | + var fakeNode = makeNode(); |
1683 | + webSocket.returnData.push(makeFakeResponse(null)); |
1684 | + NodesManager.createCacheSet( |
1685 | + fakeNode, "", "").then(function() { |
1686 | + var sentObject = angular.fromJson(webSocket.sentData[0]); |
1687 | + expect(sentObject.method).toBe("node.create_cache_set"); |
1688 | + done(); |
1689 | + }); |
1690 | + }); |
1691 | + |
1692 | + it("calls node.create_cache_set with params", function(done) { |
1693 | + var fakeNode = makeNode(); |
1694 | + var block_id = makeName("block_id"); |
1695 | + var partition_id = makeName("block_id"); |
1696 | + webSocket.returnData.push(makeFakeResponse(null)); |
1697 | + NodesManager.createCacheSet( |
1698 | + fakeNode, block_id, partition_id).then( |
1699 | + function() { |
1700 | + var sentObject = angular.fromJson(webSocket.sentData[0]); |
1701 | + expect(sentObject.method).toBe("node.create_cache_set"); |
1702 | + expect(sentObject.params.system_id).toBe(fakeNode.system_id); |
1703 | + expect(sentObject.params.block_id).toBe(block_id); |
1704 | + expect(sentObject.params.partition_id).toBe(partition_id); |
1705 | + done(); |
1706 | + }); |
1707 | + }); |
1708 | + }); |
1709 | + |
1710 | + describe("createBcache", function() { |
1711 | + |
1712 | + it("calls node.create_bcache", function(done) { |
1713 | + var fakeNode = makeNode(); |
1714 | + webSocket.returnData.push(makeFakeResponse(null)); |
1715 | + NodesManager.createBcache( |
1716 | + fakeNode, {}).then(function() { |
1717 | + var sentObject = angular.fromJson(webSocket.sentData[0]); |
1718 | + expect(sentObject.method).toBe("node.create_bcache"); |
1719 | + done(); |
1720 | + }); |
1721 | + }); |
1722 | + |
1723 | + it("calls node.create_bcache with params", function(done) { |
1724 | + var fakeNode = makeNode(); |
1725 | + var params = { |
1726 | + block_id: makeName("block_id"), |
1727 | + partition_id: makeName("block_id") |
1728 | + }; |
1729 | + webSocket.returnData.push(makeFakeResponse(null)); |
1730 | + NodesManager.createBcache( |
1731 | + fakeNode, params).then( |
1732 | + function() { |
1733 | + var sentObject = angular.fromJson(webSocket.sentData[0]); |
1734 | + expect(sentObject.method).toBe("node.create_bcache"); |
1735 | + expect(sentObject.params.system_id).toBe(fakeNode.system_id); |
1736 | + expect(sentObject.params.block_id).toBe(params.block_id); |
1737 | + expect(sentObject.params.partition_id).toBe( |
1738 | + params.partition_id); |
1739 | + done(); |
1740 | + }); |
1741 | + }); |
1742 | + }); |
1743 | }); |
1744 | |
1745 | === modified file 'src/maasserver/static/partials/node-details.html' |
1746 | --- src/maasserver/static/partials/node-details.html 2015-10-21 12:36:21 +0000 |
1747 | +++ src/maasserver/static/partials/node-details.html 2015-10-21 17:51:26 +0000 |
1748 | @@ -106,7 +106,7 @@ |
1749 | |
1750 | <div class="page-header__feedback ng-hide" data-ng-show="hasActionPowerError(actionOption.name)"> |
1751 | <p class="page-header__feedback-message info"> |
1752 | - Node cannot be {$ actionOption.sentence $}, because power control software for the |
1753 | + Node cannot be {$ actionOption.sentence $}, because power control software for the |
1754 | node is missing from its cluster controller. To proceed, install the |
1755 | {$ getPowerErrors() $} on the {$ node.nodegroup.cluster_name $} cluster. |
1756 | </p> |
1757 | @@ -681,6 +681,70 @@ |
1758 | </main> |
1759 | </section> |
1760 | </div> |
1761 | + <div class="twelve-col padding-bottom" data-ng-show="cachesets.length"> |
1762 | + <h3>Available cache sets</h3> |
1763 | + <section class="table"> |
1764 | + <header class="table__head"> |
1765 | + <div class="table__row"> |
1766 | + <div class="table__header table__column--3"> |
1767 | + <input class="checkbox" type="checkbox" id="cachesets-check-all" |
1768 | + data-ng-checked="cachesetsAllSelected" |
1769 | + data-ng-click="toggleCacheSetAllSelect()" |
1770 | + data-ng-disabled="isCacheSetsDisabled()" /> |
1771 | + <label class="checkbox-label" for="cachesets-check-all"></label> |
1772 | + </div> |
1773 | + <div class="table__header table__column--15">Name</div> |
1774 | + <div class="table__header table__column--27">Size</div> |
1775 | + <div class="table__header table__column--55">Used by</div> |
1776 | + </div> |
1777 | + |
1778 | + </header> |
1779 | + <main class="table__body"> |
1780 | + <div data-ng-repeat="cacheset in cachesets"> |
1781 | + <div class="table__row" |
1782 | + data-ng-class="{ active: cacheset.$selected }"> |
1783 | + <div class="table__data table__column--3"> |
1784 | + <input class="checkbox" type="checkbox" id="{$ cacheset.name $}" |
1785 | + data-ng-checked="cacheset.$selected" |
1786 | + data-ng-click="toggleCacheSetSelect(cacheset)" |
1787 | + data-ng-disabled="isCacheSetsDisabled()" /> |
1788 | + <label class="checkbox-label" for="{$ cacheset.name $}" ></label> |
1789 | + </div> |
1790 | + <div class="table__data table__column--15">{$ cacheset.name $}</div> |
1791 | + <div class="table__data table__column--27">{$ cacheset.size_human $}</div> |
1792 | + <div class="table__data table__column--45">{$ cacheset.used_by $}</div> |
1793 | + <div class="table__data table__column--10"> |
1794 | + <div class="table__controls"> |
1795 | + <a class="icon delete" |
1796 | + data-ng-show="canDeleteCacheSet(cacheset)" |
1797 | + data-ng-click="quickCacheSetDelete(cacheset)">Remove</a> |
1798 | + </div> |
1799 | + </div> |
1800 | + <div class="table__dropdown"> |
1801 | + <div class="table__row table__dropdown-row"> |
1802 | + <div class="ng-hide" data-ng-show="cachesetsMode === 'single' && canDeleteCacheSet(cacheset)"> |
1803 | + <div class="table__data left"> |
1804 | + <a class="link-cta-ubuntu text-button" |
1805 | + data-ng-show="canDeleteCacheSet(cacheset)" |
1806 | + data-ng-click="cacheSetDelete()">Remove</a> |
1807 | + </div> |
1808 | + </div> |
1809 | + <div class="ng-hide" data-ng-show="cachesetsMode === 'delete'"> |
1810 | + <div class="table__data left margin-top--five"> |
1811 | + <p><span class="icon warning margin-right--ten"></span> Are you sure you want to delete this cache set?</p> |
1812 | + </div> |
1813 | + <div class="table__data right"> |
1814 | + <a class="link-cta-ubuntu text-button" data-ng-click="cacheSetCancel()">Cancel</a> |
1815 | + <button class="cta-ubuntu" data-ng-click="cacheSetConfirmDelete(cacheset)">Remove</button> |
1816 | + </div> |
1817 | + </div> |
1818 | + </div> |
1819 | + </div> |
1820 | + </div> |
1821 | + </div> |
1822 | + </main> |
1823 | + </section> |
1824 | + </div> |
1825 | <div class="twelve-col padding-bottom"> |
1826 | <h3>Available disks and partitions</h3> |
1827 | <section class="table margin-bottom"> |
1828 | @@ -712,7 +776,7 @@ |
1829 | No available disks or partitions. |
1830 | </div> |
1831 | </div> |
1832 | - <div data-ng-repeat="item in available"> |
1833 | + <div data-ng-repeat="item in available | removeAvailableByNew:availableNew"> |
1834 | <div class="table__row" |
1835 | data-ng-class="{ active: item.$selected }"> |
1836 | <div class="table__data table__column--3"> |
1837 | @@ -753,8 +817,8 @@ |
1838 | data-ng-show="canAddPartition(item)" |
1839 | data-ng-click="availableQuickPartition(item)">Partition</a> |
1840 | <a class="icon delete" |
1841 | - data-ng-show="canDelete(item)" |
1842 | - data-ng-click="availableQuickDelete(item)">Delete</a> |
1843 | + data-ng-show="hasUnmountedFilesystem(item) || canDelete(item)" |
1844 | + data-ng-click="availableQuickDelete(item)">Remove</a> |
1845 | </div> |
1846 | </div> |
1847 | <div class="table__dropdown"> |
1848 | @@ -806,10 +870,10 @@ |
1849 | <div class="table__row table__dropdown-row"> |
1850 | <div class="ng-hide" data-ng-show="availableMode === 'single'"> |
1851 | <div class="table__data left"> |
1852 | - <button class="cta-ubuntu" |
1853 | + <button class="cta-ubuntu secondary" |
1854 | data-ng-show="canFormatAndMount(item)" |
1855 | data-ng-click="availableFormatAndMount(item)">{$ getFormatAndMountButtonText (item) $}</button> |
1856 | - <button class="cta-ubuntu" |
1857 | + <button class="cta-ubuntu secondary" |
1858 | data-ng-show="canAddPartition(item)" |
1859 | data-ng-click="availablePartiton(item)">{$ getPartitionButtonText(item) $}</button> |
1860 | <a class="link-cta-ubuntu text-button" |
1861 | @@ -858,13 +922,91 @@ |
1862 | </div> |
1863 | </div> |
1864 | </div> |
1865 | + <div class="table__row active" data-ng-show="availableMode === 'bcache'"> |
1866 | + <div class="table__data table__column--3"> |
1867 | + <input class="checkbox" type="checkbox" id="bcache-create" disabled="disabled" checked /> |
1868 | + <label class="checkbox-label" for="bcache-create"></label> |
1869 | + </div> |
1870 | + <div class="table__data table__column--15"> |
1871 | + <input type="text" class="table__input" |
1872 | + data-ng-class="{ invalid: isNewDiskNameInvalid() }" |
1873 | + data-ng-model="availableNew.name"> |
1874 | + </div> |
1875 | + <div class="table__data table__column--15">{$ availableNew.device.size_human $}</div> |
1876 | + <div class="table__data table__column--15">Bcache</div> |
1877 | + <div class="table__data table__column--45"></div> |
1878 | + <div class="table__data table__column--7"></div> |
1879 | + <div class="table__dropdown"> |
1880 | + <div class="table__row table__dropdown-row padding-bottom--ten"> |
1881 | + <div class="table__data table__column--3"></div> |
1882 | + <div class="table__data table__column--15"> |
1883 | + <label for="type">Cache set</label> |
1884 | + <select name="cache-set" id="cache-set" |
1885 | + data-ng-model="availableNew.cacheset" |
1886 | + data-ng-options="cacheset as cacheset.name for cacheset in cachesets"> |
1887 | + </select> |
1888 | + </div> |
1889 | + <div class="table__data table__column--15"> |
1890 | + <label for="type">Cache mode</label> |
1891 | + <select name="cache-mode" id="cache-mode" |
1892 | + data-ng-model="availableNew.cacheMode"> |
1893 | + <option value="writeback">writeback</option> |
1894 | + <option value="writethrough">writethrough</option> |
1895 | + <option value="writearound">writearound</option> |
1896 | + </select> |
1897 | + </div> |
1898 | + <div class="table__data table__column--15"></div> |
1899 | + <div class="table__data table__column--15"> |
1900 | + <label for="type">File system</label> |
1901 | + <select name="filesys" |
1902 | + data-ng-model="availableNew.fstype" |
1903 | + data-ng-options="fs.key as fs.key for fs in node.supported_filesystems"> |
1904 | + <option value="">Unformatted</option> |
1905 | + </select> |
1906 | + </div> |
1907 | + <div class="table__data table__column--30"> |
1908 | + <label for="mountpoint">Mount point</label> |
1909 | + <input type="text" id="mountpoint" |
1910 | + ng-model="availableNew.mountPoint" |
1911 | + ng-class="{ invalid: isMountPointInvalid(availableNew.mountPoint) }"> |
1912 | + </div> |
1913 | + <div class="table__data table__column--7"></div> |
1914 | + </div> |
1915 | + <div class="table__row table__dropdown-row table__dropdown-row--head"> |
1916 | + <div class="table__header table__column--5"></div> |
1917 | + <div class="table__header table__column--15">Name</div> |
1918 | + <div class="table__header table__column--15">Size</div> |
1919 | + <div class="table__header table__column--15">Device type</div> |
1920 | + <div class="table__header table__column--40"></div> |
1921 | + </div> |
1922 | + <div class="table__row table__dropdown-row no-border"> |
1923 | + <div class="table__data no-padding-top table__column--5"></div> |
1924 | + <div class="table__data no-padding-top table__column--15">{$ availableNew.device.name $}</div> |
1925 | + <div class="table__data no-padding-top table__column--15">{$ availableNew.device.size_human $}</div> |
1926 | + <div class="table__data no-padding-top table__column--15">{$ getDeviceType(availableNew.device) $}</div> |
1927 | + <div class="table__data no-padding-top table__column--40"></div> |
1928 | + </div> |
1929 | + <div class="table__row table__dropdown-row"> |
1930 | + <div class="table__data right"> |
1931 | + <a class="link-cta-ubuntu text-button" |
1932 | + data-ng-click="availableCancel()">Cancel</a> |
1933 | + <button class="cta-ubuntu" |
1934 | + data-ng-disabled="!createBcacheCanSave()" |
1935 | + data-ng-click="availableConfirmCreateBcache()">Create Bcache</button> |
1936 | + </div> |
1937 | + </div> |
1938 | + </div> |
1939 | + </div> |
1940 | </main> |
1941 | </section> |
1942 | - <div class="ng-hide"> |
1943 | - <a class="link-cta-ubuntu secondary margin-right">Create RAID</a> |
1944 | - <a class="link-cta-ubuntu secondary margin-right">Create BCache</a> |
1945 | - <a class="link-cta-ubuntu secondary">Create LVM</a> |
1946 | - </div> |
1947 | + <a class="link-cta-ubuntu secondary margin-right ng-hide">Create RAID</a> |
1948 | + <a class="link-cta-ubuntu secondary margin-right ng-hide">Create LVM</a> |
1949 | + <a class="link-cta-ubuntu secondary margin-right" |
1950 | + data-ng-disabled="!canCreateCacheSet()" |
1951 | + data-ng-click="createCacheSet()">Create Cache Set</a> |
1952 | + <a class="link-cta-ubuntu secondary" |
1953 | + data-ng-disabled="!canCreateBcache()" |
1954 | + data-ng-click="createBcache()">Create Bcache</a> |
1955 | </div> |
1956 | <div class="twelve-col padding-bottom"> |
1957 | <h3>Used disks and partitions</h3> |
1958 | @@ -904,68 +1046,6 @@ |
1959 | |
1960 | <!-- |
1961 | |
1962 | - // XXX ltrager 2015-09-16 - Mock static storage, to be transformed into |
1963 | - // something more angular friendly |
1964 | - |
1965 | - // Available disks & Block devices (CREATE PARTITION ROW) |
1966 | - |
1967 | - // Markup for a row when create a partition has been clicked. All |
1968 | - // Form elements creating the partition will be in table__dropdown |
1969 | - // and the original table__row has a class of active applied |
1970 | - |
1971 | - <div class="table__row active"> |
1972 | - <div class="table__data table__column--3"> |
1973 | - <input class="checkbox" type="checkbox" id="sdb" /> |
1974 | - <label class="checkbox-label" for="sdb"></label> |
1975 | - </div> |
1976 | - <div class="table__data table__column--15"> |
1977 | - <input type="text" value="sdb" class="table__input"> |
1978 | - </div> |
1979 | - <div class="table__data table__column--15">8.5 G</div> |
1980 | - <div class="table__data table__column--15">Physical</div> |
1981 | - <div class="table__data table__column--15"> |
1982 | - fat32 <a class="table__controls table__controls--secondary margin-left--ten">Mount</a> |
1983 | - </div> |
1984 | - <div class="table__data table__column--15">QEMU HARDDISK</div> |
1985 | - <div class="table__data table__column--15 ng-hide">937368252</div> |
1986 | - <div class="table__data table__column--15"> |
1987 | - <div class="table__tags">rotary, storage, test, hp, machine<span class="table__controls table__controls--secondary">, <a>edit</a></span></div> |
1988 | - </div> |
1989 | - <div class="table__data table__column--7"> |
1990 | - <div class="table__controls"> |
1991 | - <a class="icon partition">Partition</a> |
1992 | - <a class="icon delete">Delete</a> |
1993 | - </div> |
1994 | - </div> |
1995 | - <div class="table__dropdown"> |
1996 | - <div class="table__row table__dropdown-row padding-bottom--ten"> |
1997 | - <div class="table__data table__column--3"></div> |
1998 | - <div class="table__data table__column--15"> |
1999 | - <label for="filesys">Filesystem</label> |
2000 | - <select name="filesys" id="filesys"> |
2001 | - <option value="fat32">fat32</option> |
2002 | - </select> |
2003 | - </div> |
2004 | - <div class="table__data table__column--30"> |
2005 | - <label for="mountpoint">Mountpoint</label> |
2006 | - <input type="text" id="mountpoint"> |
2007 | - </div> |
2008 | - <div class="table__data table__column--52"> |
2009 | - </div> |
2010 | - </div> |
2011 | - <div class="table__row table__dropdown-row"> |
2012 | - <div class="table__data right"> |
2013 | - <a class="link-cta-ubuntu text-button">Cancel</a> |
2014 | - <button class="cta-ubuntu">Mount</button> |
2015 | - </div> |
2016 | - </div> |
2017 | - </div> |
2018 | - </div> |
2019 | - |
2020 | - --> |
2021 | - |
2022 | - <!-- |
2023 | - |
2024 | // Creating a Volume Group |
2025 | |
2026 | // First state of creating a volume group. This row shows once you have |
2027 | @@ -1236,95 +1316,6 @@ |
2028 | </div> |
2029 | |
2030 | --> |
2031 | - |
2032 | - <!-- |
2033 | - |
2034 | - // Create Bchache |
2035 | - |
2036 | - <div class="table__row active"> |
2037 | - <div class="table__data table__column--3"> |
2038 | - <input class="checkbox" type="checkbox" id="check-all" /> |
2039 | - <label class="checkbox-label" for="check-all"></label> |
2040 | - </div> |
2041 | - <div class="table__data table__column--15"> |
2042 | - <input type="text" value="BChache0" class="table__input"> |
2043 | - </div> |
2044 | - <div class="table__data table__column--15">34 G</div> |
2045 | - <div class="table__data table__column--15">BChache</div> |
2046 | - <div class="table__data table__column--45"></div> |
2047 | - <div class="table__data table__column--7"> |
2048 | - <div class="table__controls"> |
2049 | - <a class="icon partition">Partition</a> |
2050 | - <a class="icon delete">Delete</a> |
2051 | - </div> |
2052 | - </div> |
2053 | - <div class="table__dropdown"> |
2054 | - <div class="table__row table__dropdown-row padding-bottom--ten"> |
2055 | - <div class="table__data table__column--3"> |
2056 | - </div> |
2057 | - <div class="table__data table__column--15"> |
2058 | - <label for="type">BCache setting</label> |
2059 | - <select name="type" id="type"> |
2060 | - <option value="raid5">Setting</option> |
2061 | - </select> |
2062 | - </div> |
2063 | - <div class="table__data table__column--30"></div> |
2064 | - <div class="table__data table__column--15"> |
2065 | - <label for="type">File system</label> |
2066 | - <select name="type" id="type"> |
2067 | - <option value="raid5">fat32</option> |
2068 | - </select> |
2069 | - </div> |
2070 | - <div class="table__data table__column--30"> |
2071 | - <label for="mtPoint">Mount point</label> |
2072 | - <input type="text"> |
2073 | - </div> |
2074 | - <div class="table__data table__column--7"></div> |
2075 | - </div> |
2076 | - <div class="table__row table__dropdown-row table__dropdown-row--head"> |
2077 | - <div class="table__header table__column--5"></div> |
2078 | - <div class="table__header table__column--15">Name</div> |
2079 | - <div class="table__header table__column--15">Size</div> |
2080 | - <div class="table__header table__column--15">Device type</div> |
2081 | - <div class="table__header table__column--10">Caching</div> |
2082 | - <div class="table__header table__column--15">Backing</div> |
2083 | - <div class="table__header table__column--25"></div> |
2084 | - </div> |
2085 | - <div class="table__row table__dropdown-row no-border"> |
2086 | - <div class="table__data no-padding-top table__column--5"></div> |
2087 | - <div class="table__data no-padding-top table__column--15">sda</div> |
2088 | - <div class="table__data no-padding-top table__column--15">8.5 G</div> |
2089 | - <div class="table__data no-padding-top table__column--15">Physical</div> |
2090 | - <div class="table__data no-padding-top table__column--10"> |
2091 | - <input type="radio" name="active" value="active"> |
2092 | - </div> |
2093 | - <div class="table__data no-padding-top table__column--15"> |
2094 | - <input type="radio" name="active" value="spare"> |
2095 | - </div> |
2096 | - <div class="table__data no-padding-top table__column--25"></div> |
2097 | - </div> |
2098 | - <div class="table__row table__dropdown-row no-border"> |
2099 | - <div class="table__data no-padding-top table__column--5"></div> |
2100 | - <div class="table__data no-padding-top table__column--15">sdb</div> |
2101 | - <div class="table__data no-padding-top table__column--15">8.5 G</div> |
2102 | - <div class="table__data no-padding-top table__column--15">Physical</div> |
2103 | - <div class="table__data no-padding-top table__column--10"> |
2104 | - <input type="radio" name="active" value="active"> |
2105 | - </div> |
2106 | - <div class="table__data no-padding-top table__column--15"> |
2107 | - <input type="radio" name="active" value="spare" selected> |
2108 | - </div> |
2109 | - <div class="table__data no-padding-top table__column--25"></div> |
2110 | - </div> |
2111 | - <div class="table__row table__dropdown-row"> |
2112 | - <div class="table__data right"> |
2113 | - <a href="" class="link-cta-ubuntu text-button">Cancel</a> |
2114 | - <button class="cta-ubuntu">Save</button> |
2115 | - </div> |
2116 | - </div> |
2117 | - </div> |
2118 | - </div> |
2119 | - --> |
2120 | </form> |
2121 | </div> |
2122 | </div> |
2123 | |
2124 | === modified file 'src/maasserver/websockets/handlers/node.py' |
2125 | --- src/maasserver/websockets/handlers/node.py 2015-10-19 17:56:35 +0000 |
2126 | +++ src/maasserver/websockets/handlers/node.py 2015-10-21 17:51:26 +0000 |
2127 | @@ -33,6 +33,8 @@ |
2128 | from maasserver.forms import ( |
2129 | AddPartitionForm, |
2130 | AdminNodeWithMACAddressesForm, |
2131 | + CreateBcacheForm, |
2132 | + CreateCacheSetForm, |
2133 | FormatBlockDeviceForm, |
2134 | FormatPartitionForm, |
2135 | MountBlockDeviceForm, |
2136 | @@ -45,6 +47,7 @@ |
2137 | ) |
2138 | from maasserver.forms_interface_link import InterfaceLinkForm |
2139 | from maasserver.models.blockdevice import BlockDevice |
2140 | +from maasserver.models.cacheset import CacheSet |
2141 | from maasserver.models.config import Config |
2142 | from maasserver.models.event import Event |
2143 | from maasserver.models.filesystemgroup import VolumeGroup |
2144 | @@ -134,7 +137,10 @@ |
2145 | 'delete_disk', |
2146 | 'delete_partition', |
2147 | 'delete_volume_group', |
2148 | + 'delete_cache_set', |
2149 | 'create_partition', |
2150 | + 'create_cache_set', |
2151 | + 'create_bcache', |
2152 | ] |
2153 | form = AdminNodeWithMACAddressesForm |
2154 | exclude = [ |
2155 | @@ -277,6 +283,9 @@ |
2156 | data["disks"] = data["disks"] + [ |
2157 | self.dehydrate_volume_group(volume_group) |
2158 | for volume_group in VolumeGroup.objects.filter_by_node(obj) |
2159 | + ] + [ |
2160 | + self.dehydrate_cache_set(cache_set) |
2161 | + for cache_set in CacheSet.objects.get_cache_sets_for_node(obj) |
2162 | ] |
2163 | data["disks"] = sorted(data["disks"], key=itemgetter("name")) |
2164 | data["supported_filesystems"] = [ |
2165 | @@ -373,7 +382,7 @@ |
2166 | return data |
2167 | |
2168 | def dehydrate_volume_group(self, volume_group): |
2169 | - """Return `BlockDevice` formatted for JSON encoding.""" |
2170 | + """Return `VolumeGroup` formatted for JSON encoding.""" |
2171 | size = volume_group.get_size() |
2172 | available_size = volume_group.get_lvm_free_space() |
2173 | used_size = volume_group.get_lvm_allocated_size() |
2174 | @@ -398,6 +407,36 @@ |
2175 | "partitions": None, |
2176 | } |
2177 | |
2178 | + def dehydrate_cache_set(self, cache_set): |
2179 | + """Return `CacheSet` formatted for JSON encoding.""" |
2180 | + device = cache_set.get_device() |
2181 | + used_size = device.get_used_size() |
2182 | + available_size = device.get_available_size() |
2183 | + bcache_devices = sorted([ |
2184 | + bcache.name |
2185 | + for bcache in cache_set.filesystemgroup_set.all() |
2186 | + ]) |
2187 | + return { |
2188 | + "id": cache_set.id, |
2189 | + "name": cache_set.name, |
2190 | + "tags": [], |
2191 | + "type": "cache-set", |
2192 | + "path": "", |
2193 | + "size": device.size, |
2194 | + "size_human": human_readable_bytes(device.size), |
2195 | + "used_size": used_size, |
2196 | + "used_size_human": human_readable_bytes(used_size), |
2197 | + "available_size": available_size, |
2198 | + "available_size_human": human_readable_bytes(available_size), |
2199 | + "block_size": device.get_block_size(), |
2200 | + "model": "", |
2201 | + "serial": "", |
2202 | + "partition_table_type": "", |
2203 | + "used_for": ", ".join(bcache_devices), |
2204 | + "filesystem": None, |
2205 | + "partitions": None, |
2206 | + } |
2207 | + |
2208 | def dehydrate_partitions(self, partition_table): |
2209 | """Return `PartitionTable` formatted for JSON encoding.""" |
2210 | if partition_table is None: |
2211 | @@ -762,6 +801,19 @@ |
2212 | raise VolumeGroup.DoesNotExist() |
2213 | volume_group.delete() |
2214 | |
2215 | + def delete_cache_set(self, params): |
2216 | + # Only admin users can perform delete. |
2217 | + if not self.user.is_superuser: |
2218 | + raise HandlerPermissionError() |
2219 | + |
2220 | + node = self.get_object(params) |
2221 | + cache_set_id = params.get('cache_set_id') |
2222 | + if cache_set_id is not None: |
2223 | + cache_set = CacheSet.objects.get(id=cache_set_id) |
2224 | + if cache_set.get_node() != node: |
2225 | + raise CacheSet.DoesNotExist() |
2226 | + cache_set.delete() |
2227 | + |
2228 | def create_partition(self, params): |
2229 | """Create a partition.""" |
2230 | node = self.get_object(params) |
2231 | @@ -775,6 +827,58 @@ |
2232 | else: |
2233 | form.save() |
2234 | |
2235 | + def create_cache_set(self, params): |
2236 | + """Create a cache set.""" |
2237 | + node = self.get_object(params) |
2238 | + block_id = params.get('block_id') |
2239 | + partition_id = params.get('partition_id') |
2240 | + |
2241 | + data = {} |
2242 | + if partition_id is not None: |
2243 | + data["cache_partition"] = partition_id |
2244 | + elif block_id is not None: |
2245 | + data["cache_device"] = block_id |
2246 | + else: |
2247 | + raise HandlerError( |
2248 | + "Either block_id or partition_id is required.") |
2249 | + |
2250 | + form = CreateCacheSetForm(node=node, data=data) |
2251 | + if not form.is_valid(): |
2252 | + raise HandlerError(form.errors) |
2253 | + else: |
2254 | + form.save() |
2255 | + |
2256 | + def create_bcache(self, params): |
2257 | + """Create a bcache.""" |
2258 | + node = self.get_object(params) |
2259 | + block_id = params.get('block_id') |
2260 | + partition_id = params.get('partition_id') |
2261 | + |
2262 | + data = { |
2263 | + "name": params["name"], |
2264 | + "cache_set": params["cache_set"], |
2265 | + "cache_mode": params["cache_mode"], |
2266 | + } |
2267 | + |
2268 | + if partition_id is not None: |
2269 | + data["backing_partition"] = partition_id |
2270 | + elif block_id is not None: |
2271 | + data["backing_device"] = block_id |
2272 | + else: |
2273 | + raise HandlerError( |
2274 | + "Either block_id or partition_id is required.") |
2275 | + |
2276 | + form = CreateBcacheForm(node=node, data=data) |
2277 | + if not form.is_valid(): |
2278 | + raise HandlerError(form.errors) |
2279 | + else: |
2280 | + bcache = form.save() |
2281 | + |
2282 | + if 'fstype' in params: |
2283 | + self.update_blockdevice_filesystem( |
2284 | + node, bcache.virtual_device.id, |
2285 | + params.get("fstype"), params.get("mount_point")) |
2286 | + |
2287 | def action(self, params): |
2288 | """Perform the action on the object.""" |
2289 | obj = self.get_object(params) |
2290 | |
2291 | === modified file 'src/maasserver/websockets/handlers/tests/test_node.py' |
2292 | --- src/maasserver/websockets/handlers/tests/test_node.py 2015-10-19 20:12:24 +0000 |
2293 | +++ src/maasserver/websockets/handlers/tests/test_node.py 2015-10-21 17:51:26 +0000 |
2294 | @@ -18,16 +18,17 @@ |
2295 | from operator import itemgetter |
2296 | import random |
2297 | import re |
2298 | -from unittest import skip |
2299 | |
2300 | from django.contrib.auth.models import User |
2301 | from django.core.exceptions import ValidationError |
2302 | from lxml import etree |
2303 | from maasserver.enum import ( |
2304 | BOND_MODE, |
2305 | + CACHE_MODE_TYPE, |
2306 | FILESYSTEM_FORMAT_TYPE_CHOICES, |
2307 | FILESYSTEM_FORMAT_TYPE_CHOICES_DICT, |
2308 | FILESYSTEM_GROUP_TYPE, |
2309 | + FILESYSTEM_TYPE, |
2310 | INTERFACE_LINK_TYPE, |
2311 | INTERFACE_TYPE, |
2312 | IPADDRESS_TYPE, |
2313 | @@ -37,8 +38,12 @@ |
2314 | from maasserver.forms import AdminNodeWithMACAddressesForm |
2315 | from maasserver.models import interface as interface_module |
2316 | from maasserver.models.blockdevice import BlockDevice |
2317 | +from maasserver.models.cacheset import CacheSet |
2318 | from maasserver.models.config import Config |
2319 | -from maasserver.models.filesystemgroup import VolumeGroup |
2320 | +from maasserver.models.filesystemgroup import ( |
2321 | + Bcache, |
2322 | + VolumeGroup, |
2323 | +) |
2324 | from maasserver.models.interface import Interface |
2325 | from maasserver.models.nodeprobeddetails import get_single_probed_details |
2326 | from maasserver.models.partition import Partition |
2327 | @@ -123,6 +128,9 @@ |
2328 | disks = disks + [ |
2329 | handler.dehydrate_volume_group(volume_group) |
2330 | for volume_group in VolumeGroup.objects.filter_by_node(node) |
2331 | + ] + [ |
2332 | + handler.dehydrate_cache_set(cache_set) |
2333 | + for cache_set in CacheSet.objects.get_cache_sets_for_node(node) |
2334 | ] |
2335 | disks = sorted(disks, key=itemgetter("name")) |
2336 | data = { |
2337 | @@ -450,20 +458,62 @@ |
2338 | "partitions": None, |
2339 | }, handler.dehydrate_volume_group(volume_group)) |
2340 | |
2341 | + def test_dehydrate_cache_set(self): |
2342 | + owner = factory.make_User() |
2343 | + node = factory.make_Node(owner=owner) |
2344 | + handler = NodeHandler(owner, {}) |
2345 | + cache_set = factory.make_CacheSet(node=node) |
2346 | + backings = [] |
2347 | + for _ in range(3): |
2348 | + backing = factory.make_PhysicalBlockDevice(node=node) |
2349 | + fs = factory.make_Filesystem( |
2350 | + block_device=backing, fstype=FILESYSTEM_TYPE.BCACHE_BACKING) |
2351 | + backings.append( |
2352 | + factory.make_FilesystemGroup( |
2353 | + group_type=FILESYSTEM_GROUP_TYPE.BCACHE, |
2354 | + filesystems=[fs], cache_set=cache_set)) |
2355 | + self.assertEquals({ |
2356 | + "id": cache_set.id, |
2357 | + "name": cache_set.name, |
2358 | + "tags": [], |
2359 | + "type": "cache-set", |
2360 | + "path": "", |
2361 | + "size": cache_set.get_device().size, |
2362 | + "size_human": human_readable_bytes( |
2363 | + cache_set.get_device().size), |
2364 | + "used_size": cache_set.get_device().get_used_size(), |
2365 | + "used_size_human": human_readable_bytes( |
2366 | + cache_set.get_device().get_used_size()), |
2367 | + "available_size": cache_set.get_device().get_available_size(), |
2368 | + "available_size_human": human_readable_bytes( |
2369 | + cache_set.get_device().get_available_size()), |
2370 | + "block_size": cache_set.get_device().get_block_size(), |
2371 | + "model": "", |
2372 | + "serial": "", |
2373 | + "partition_table_type": "", |
2374 | + "used_for": ", ".join(sorted([ |
2375 | + backing_device.name |
2376 | + for backing_device in backings |
2377 | + ])), |
2378 | + "filesystem": None, |
2379 | + "partitions": None, |
2380 | + }, handler.dehydrate_cache_set(cache_set)) |
2381 | + |
2382 | def test_dehydrate_partitions_returns_None(self): |
2383 | owner = factory.make_User() |
2384 | handler = NodeHandler(owner, {}) |
2385 | self.assertIsNone(handler.dehydrate_partitions(None)) |
2386 | |
2387 | - @skip("XXX: GavinPanella 2015-10-01 bug=1501753: Fails spuriously") |
2388 | def test_dehydrate_partitions_returns_list_of_partitions(self): |
2389 | owner = factory.make_User() |
2390 | node = factory.make_Node(owner=owner) |
2391 | handler = NodeHandler(owner, {}) |
2392 | - blockdevice = factory.make_PhysicalBlockDevice(node=node) |
2393 | + blockdevice = factory.make_PhysicalBlockDevice( |
2394 | + node=node, size=10 * 1024 ** 3, block_size=512) |
2395 | partition_table = factory.make_PartitionTable(block_device=blockdevice) |
2396 | partitions = [ |
2397 | - factory.make_Partition(partition_table=partition_table) |
2398 | + factory.make_Partition( |
2399 | + partition_table=partition_table, size=1 * 1024 ** 3) |
2400 | for _ in range(3) |
2401 | ] |
2402 | expected = [] |
2403 | @@ -1193,6 +1243,18 @@ |
2404 | }) |
2405 | self.assertIsNone(reload_object(volume_group)) |
2406 | |
2407 | + def test_delete_cache_set(self): |
2408 | + user = factory.make_admin() |
2409 | + handler = NodeHandler(user, {}) |
2410 | + architecture = make_usable_architecture(self) |
2411 | + node = factory.make_Node(interface=True, architecture=architecture) |
2412 | + cache_set = factory.make_CacheSet(node=node) |
2413 | + handler.delete_cache_set({ |
2414 | + 'system_id': node.system_id, |
2415 | + 'cache_set_id': cache_set.id, |
2416 | + }) |
2417 | + self.assertIsNone(reload_object(cache_set)) |
2418 | + |
2419 | def test_create_partition(self): |
2420 | user = factory.make_admin() |
2421 | handler = NodeHandler(user, {}) |
2422 | @@ -1211,6 +1273,155 @@ |
2423 | human_readable_bytes(size), |
2424 | human_readable_bytes(Partition.objects.first().size)) |
2425 | |
2426 | + def test_create_cache_set_for_partition(self): |
2427 | + user = factory.make_admin() |
2428 | + handler = NodeHandler(user, {}) |
2429 | + architecture = make_usable_architecture(self) |
2430 | + node = factory.make_Node(interface=True, architecture=architecture) |
2431 | + partition = factory.make_Partition(node=node) |
2432 | + handler.create_cache_set({ |
2433 | + 'system_id': node.system_id, |
2434 | + 'partition_id': partition.id |
2435 | + }) |
2436 | + cache_set = CacheSet.objects.get_cache_sets_for_node(node).first() |
2437 | + self.assertIsNotNone(cache_set) |
2438 | + self.assertEquals(partition, cache_set.get_filesystem().partition) |
2439 | + |
2440 | + def test_create_cache_set_for_block_device(self): |
2441 | + user = factory.make_admin() |
2442 | + handler = NodeHandler(user, {}) |
2443 | + architecture = make_usable_architecture(self) |
2444 | + node = factory.make_Node(interface=True, architecture=architecture) |
2445 | + block_device = factory.make_PhysicalBlockDevice(node=node) |
2446 | + handler.create_cache_set({ |
2447 | + 'system_id': node.system_id, |
2448 | + 'block_id': block_device.id |
2449 | + }) |
2450 | + cache_set = CacheSet.objects.get_cache_sets_for_node(node).first() |
2451 | + self.assertIsNotNone(cache_set) |
2452 | + self.assertEquals( |
2453 | + block_device.id, cache_set.get_filesystem().block_device.id) |
2454 | + |
2455 | + def test_create_bcache_for_partition(self): |
2456 | + user = factory.make_admin() |
2457 | + handler = NodeHandler(user, {}) |
2458 | + architecture = make_usable_architecture(self) |
2459 | + node = factory.make_Node(interface=True, architecture=architecture) |
2460 | + partition = factory.make_Partition(node=node) |
2461 | + name = factory.make_name("bcache") |
2462 | + cache_set = factory.make_CacheSet(node=node) |
2463 | + cache_mode = factory.pick_enum(CACHE_MODE_TYPE) |
2464 | + handler.create_bcache({ |
2465 | + 'system_id': node.system_id, |
2466 | + 'partition_id': partition.id, |
2467 | + 'block_id': partition.partition_table.block_device.id, |
2468 | + 'name': name, |
2469 | + 'cache_set': cache_set.id, |
2470 | + 'cache_mode': cache_mode, |
2471 | + }) |
2472 | + bcache = Bcache.objects.filter_by_node(node).first() |
2473 | + self.assertIsNotNone(bcache) |
2474 | + self.assertEquals(name, bcache.name) |
2475 | + self.assertEquals(cache_set, bcache.cache_set) |
2476 | + self.assertEquals(cache_mode, bcache.cache_mode) |
2477 | + self.assertEquals( |
2478 | + partition, bcache.get_bcache_backing_filesystem().partition) |
2479 | + |
2480 | + def test_create_bcache_for_partition_with_filesystem(self): |
2481 | + user = factory.make_admin() |
2482 | + handler = NodeHandler(user, {}) |
2483 | + architecture = make_usable_architecture(self) |
2484 | + node = factory.make_Node(interface=True, architecture=architecture) |
2485 | + partition = factory.make_Partition(node=node) |
2486 | + name = factory.make_name("bcache") |
2487 | + cache_set = factory.make_CacheSet(node=node) |
2488 | + cache_mode = factory.pick_enum(CACHE_MODE_TYPE) |
2489 | + fstype = factory.pick_choice(FILESYSTEM_FORMAT_TYPE_CHOICES) |
2490 | + mount_point = factory.make_absolute_path() |
2491 | + handler.create_bcache({ |
2492 | + 'system_id': node.system_id, |
2493 | + 'partition_id': partition.id, |
2494 | + 'block_id': partition.partition_table.block_device.id, |
2495 | + 'name': name, |
2496 | + 'cache_set': cache_set.id, |
2497 | + 'cache_mode': cache_mode, |
2498 | + 'fstype': fstype, |
2499 | + 'mount_point': mount_point, |
2500 | + }) |
2501 | + bcache = Bcache.objects.filter_by_node(node).first() |
2502 | + self.assertIsNotNone(bcache) |
2503 | + self.assertEquals(name, bcache.name) |
2504 | + self.assertEquals(cache_set, bcache.cache_set) |
2505 | + self.assertEquals(cache_mode, bcache.cache_mode) |
2506 | + self.assertEquals( |
2507 | + partition, bcache.get_bcache_backing_filesystem().partition) |
2508 | + self.assertEquals( |
2509 | + fstype, |
2510 | + bcache.virtual_device.get_effective_filesystem().fstype) |
2511 | + self.assertEquals( |
2512 | + mount_point, |
2513 | + bcache.virtual_device.get_effective_filesystem().mount_point) |
2514 | + |
2515 | + def test_create_bcache_for_block_device(self): |
2516 | + user = factory.make_admin() |
2517 | + handler = NodeHandler(user, {}) |
2518 | + architecture = make_usable_architecture(self) |
2519 | + node = factory.make_Node(interface=True, architecture=architecture) |
2520 | + block_device = factory.make_PhysicalBlockDevice(node=node) |
2521 | + name = factory.make_name("bcache") |
2522 | + cache_set = factory.make_CacheSet(node=node) |
2523 | + cache_mode = factory.pick_enum(CACHE_MODE_TYPE) |
2524 | + handler.create_bcache({ |
2525 | + 'system_id': node.system_id, |
2526 | + 'block_id': block_device.id, |
2527 | + 'name': name, |
2528 | + 'cache_set': cache_set.id, |
2529 | + 'cache_mode': cache_mode, |
2530 | + }) |
2531 | + bcache = Bcache.objects.filter_by_node(node).first() |
2532 | + self.assertIsNotNone(bcache) |
2533 | + self.assertEquals(name, bcache.name) |
2534 | + self.assertEquals(cache_set, bcache.cache_set) |
2535 | + self.assertEquals(cache_mode, bcache.cache_mode) |
2536 | + self.assertEquals( |
2537 | + block_device.id, |
2538 | + bcache.get_bcache_backing_filesystem().block_device.id) |
2539 | + |
2540 | + def test_create_bcache_for_block_device_with_filesystem(self): |
2541 | + user = factory.make_admin() |
2542 | + handler = NodeHandler(user, {}) |
2543 | + architecture = make_usable_architecture(self) |
2544 | + node = factory.make_Node(interface=True, architecture=architecture) |
2545 | + block_device = factory.make_PhysicalBlockDevice(node=node) |
2546 | + name = factory.make_name("bcache") |
2547 | + cache_set = factory.make_CacheSet(node=node) |
2548 | + cache_mode = factory.pick_enum(CACHE_MODE_TYPE) |
2549 | + fstype = factory.pick_choice(FILESYSTEM_FORMAT_TYPE_CHOICES) |
2550 | + mount_point = factory.make_absolute_path() |
2551 | + handler.create_bcache({ |
2552 | + 'system_id': node.system_id, |
2553 | + 'block_id': block_device.id, |
2554 | + 'name': name, |
2555 | + 'cache_set': cache_set.id, |
2556 | + 'cache_mode': cache_mode, |
2557 | + 'fstype': fstype, |
2558 | + 'mount_point': mount_point, |
2559 | + }) |
2560 | + bcache = Bcache.objects.filter_by_node(node).first() |
2561 | + self.assertIsNotNone(bcache) |
2562 | + self.assertEquals(name, bcache.name) |
2563 | + self.assertEquals(cache_set, bcache.cache_set) |
2564 | + self.assertEquals(cache_mode, bcache.cache_mode) |
2565 | + self.assertEquals( |
2566 | + block_device.id, |
2567 | + bcache.get_bcache_backing_filesystem().block_device.id) |
2568 | + self.assertEquals( |
2569 | + fstype, |
2570 | + bcache.virtual_device.get_effective_filesystem().fstype) |
2571 | + self.assertEquals( |
2572 | + mount_point, |
2573 | + bcache.virtual_device.get_effective_filesystem().mount_point) |
2574 | + |
2575 | def test_update_raise_HandlerError_if_tag_has_definition(self): |
2576 | user = factory.make_admin() |
2577 | handler = NodeHandler(user, {}) |
lgtm!