Merge lp:~blake-rouse/maas/new-images-page into lp:~maas-committers/maas/trunk
- new-images-page
- Merge into trunk
Status: | Merged | ||||
---|---|---|---|---|---|
Approved by: | Blake Rouse | ||||
Approved revision: | no longer in the source branch. | ||||
Merged at revision: | 5352 | ||||
Proposed branch: | lp:~blake-rouse/maas/new-images-page | ||||
Merge into: | lp:~maas-committers/maas/trunk | ||||
Diff against target: |
3897 lines (+3284/-108) 15 files modified
src/maasserver/bootresources.py (+48/-14) src/maasserver/static/js/angular/controllers/images.js (+11/-0) src/maasserver/static/js/angular/directives/boot_images.js (+744/-0) src/maasserver/static/js/angular/directives/tests/test_boot_images.js (+1454/-0) src/maasserver/static/js/angular/factories/bootresources.js (+179/-0) src/maasserver/static/js/angular/factories/tests/test_bootresources.js (+195/-0) src/maasserver/static/js/angular/maas.js (+5/-0) src/maasserver/static/partials/boot-images.html (+265/-0) src/maasserver/static/partials/images.html (+7/-0) src/maasserver/templates/maasserver/base.html (+1/-1) src/maasserver/templates/maasserver/index.html (+1/-1) src/maasserver/tests/test_bootresources.py (+46/-4) src/maasserver/views/combo.py (+3/-0) src/maasserver/websockets/handlers/bootresource.py (+226/-66) src/maasserver/websockets/handlers/tests/test_bootresource.py (+99/-22) |
||||
To merge this branch: | bzr merge lp:~blake-rouse/maas/new-images-page | ||||
Related bugs: |
|
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Andres Rodriguez (community) | Approve | ||
Review via email: mp+305612@code.launchpad.net |
Commit message
Add new maas-boot-images directive and new angular based images page.
The maas-boot-images directive allows the same code to be used both on the images page as well as the first user journey.
Description of the change
This is a whole re-implementation and the new design has a lot more interaction than the previous design, so that is why it is so large. Its also javascript, ;-)
Lee Trager (ltrager) wrote : | # |
Blake Rouse (blake-rouse) wrote : | # |
> Looks really good so far. I was able to merge lp:~ltrager/maas/use_bootloaders
> into your branch to test it with the bootloaders.
Thanks for looking at it.
>
> Couple of things I noticed
> 1. The bootloaders still show up. In my branch I filter them with
> BootSourceCache
Ah thats much better. I cannot do that until your branches make it into trunk so I will leave them as so.
> 2. The other images section lists the images twice, onces with checkboxes,
> onces without. See http://
This is correct. You select the images you want then save that selection and they will be imported. At the moment yours look wierd and is confusing since you see the bootloaders. With the change above it will make more since.
> 3. I started with MAAS using the V3 test stream. If I click on the maas.io
> source the boot source metadata is immediately reloaded. It looks like the
> region then tries to download the maas.io images(they're at an older version
> than mine). If I switch back to my custom stream 16.10 is marked 'Queued for
> deletion.' I see there is a 'connect' button. I think the user should have to
> click connect before things change. This should prevent downloading the image
> again if the source is accidentally changed.
I think I understand what you are describing and I have cleaned this up. It doesn't actually start downloading. You have to click save selection, for it to be saved.
> 4. Sometimes when the page loads its blank or I only see other images.
> Refreshing fixes this.
Yeah I have fixed this issue.
> 5. When I click connect with a custom stream I briefly(only shows for about a
> second) get an error message saying 'Select atleast one 14.04+ LTS release and
> architecture' even though I already have 16.04 installed.
What you have installed doesn't matter since your on a new stream. You have to make selections for that new source.
Andres Rodriguez (andreserl) wrote : | # |
I haven't done a full reivew but just have a couple of comments inlien.
Andres Rodriguez (andreserl) : | # |
Blake Rouse (blake-rouse) : | # |
Blake Rouse (blake-rouse) wrote : | # |
Fixed and pushed.
Andres Rodriguez (andreserl) wrote : | # |
I've done testing and filed a couple bugs that cannot be fixed right now but bugs are filed for them. Other than that it looks good to me.
MAAS Lander (maas-lander) wrote : | # |
The attempt to merge lp:~blake-rouse/maas/new-images-page into lp:maas failed. Below is the output from the failed tests.
Get:1 http://
Hit:2 http://
Get:3 http://
Hit:4 http://
Get:5 http://
Get:6 http://
Fetched 901 kB in 0s (2,077 kB/s)
Reading package lists...
sudo DEBIAN_
--no-
Reading package lists...
Building dependency tree...
Reading state information...
archdetect-deb is already the newest version (1.117ubuntu2).
authbind is already the newest version (2.1.1+nmu1).
avahi-utils is already the newest version (0.6.32~
build-essential is already the newest version (12.1ubuntu2).
debhelper is already the newest version (9.20160115ubun
distro-info is already the newest version (0.14build1).
freeipmi-tools is already the newest version (1.4.11-1ubuntu1).
git is already the newest version (1:2.7.4-0ubuntu1).
libjs-angularjs is already the newest version (1.2.28-1ubuntu2).
libjs-jquery is already the newest version (1.11.3+dfsg-4).
libjs-yui3-full is already the newest version (3.5.1-1ubuntu3).
libjs-yui3-min is already the newest version (3.5.1-1ubuntu3).
make is already the newest version (4.1-6).
postgresql is already the newest version (9.5+173).
...
MAAS Lander (maas-lander) wrote : | # |
The attempt to merge lp:~blake-rouse/maas/new-images-page into lp:maas failed. Below is the output from the failed tests.
Hit:1 http://
Hit:2 http://
Hit:3 http://
Get:4 http://
Fetched 94.5 kB in 0s (208 kB/s)
Reading package lists...
sudo DEBIAN_
--no-
Reading package lists...
Building dependency tree...
Reading state information...
archdetect-deb is already the newest version (1.117ubuntu2).
authbind is already the newest version (2.1.1+nmu1).
avahi-utils is already the newest version (0.6.32~
build-essential is already the newest version (12.1ubuntu2).
debhelper is already the newest version (9.20160115ubun
distro-info is already the newest version (0.14build1).
freeipmi-tools is already the newest version (1.4.11-1ubuntu1).
git is already the newest version (1:2.7.4-0ubuntu1).
libjs-angularjs is already the newest version (1.2.28-1ubuntu2).
libjs-jquery is already the newest version (1.11.3+dfsg-4).
libjs-yui3-full is already the newest version (3.5.1-1ubuntu3).
libjs-yui3-min is already the newest version (3.5.1-1ubuntu3).
make is already the newest version (4.1-6).
postgresql is already the newest version (9.5+173).
pxelinux is already the newest version (3:6.03+
python-formencode is already the newest version (1.3.0-0ubuntu5).
python-lxml is already the newest version (3.5.0-1build1).
python-netaddr is already the newest ver...
Preview Diff
1 | === modified file 'src/maasserver/bootresources.py' |
2 | --- src/maasserver/bootresources.py 2016-08-31 06:26:21 +0000 |
3 | +++ src/maasserver/bootresources.py 2016-09-14 21:01:21 +0000 |
4 | @@ -102,11 +102,13 @@ |
5 | from twisted.application.internet import TimerService |
6 | from twisted.internet import reactor |
7 | from twisted.internet.defer import ( |
8 | + Deferred, |
9 | DeferredList, |
10 | inlineCallbacks, |
11 | ) |
12 | from twisted.protocols.amp import UnhandledCommand |
13 | from twisted.python import log |
14 | +from twisted.python.failure import Failure |
15 | |
16 | |
17 | maaslog = get_maas_logger("bootresources") |
18 | @@ -799,11 +801,15 @@ |
19 | "Deleting empty resource %s.", resource) |
20 | resource.delete() |
21 | |
22 | - def finalize(self): |
23 | + def finalize(self, notify=None): |
24 | """Perform the finalization of data into the database. |
25 | |
26 | This will remove the un-needed `BootResource`'s and write the |
27 | file data into the large object store. |
28 | + |
29 | + :param notify: Instance of `Deferred` that is called when all the |
30 | + metadata has been downloaded and the image data download has been |
31 | + started. |
32 | """ |
33 | # XXX blake_r 2014-10-30 bug=1387133: A scenario can occur where insert |
34 | # never gets called by the writer, causing this method to delete all |
35 | @@ -818,12 +824,20 @@ |
36 | len(self._content_to_finalize)) |
37 | if (self._resources_to_delete == self._init_resources_to_delete and |
38 | len(self._content_to_finalize) == 0): |
39 | - maaslog.error( |
40 | + error_msg = ( |
41 | "Finalization of imported images skipped, " |
42 | - "or all %s synced images would be deleted.", |
43 | - self._resources_to_delete) |
44 | + "or all %s synced images would be deleted." % ( |
45 | + self._resources_to_delete)) |
46 | + maaslog.error(error_msg) |
47 | + if notify is not None: |
48 | + failure = Failure(Exception(error_msg)) |
49 | + reactor.callLater(0, notify.errback, failure) |
50 | return |
51 | self.resource_cleaner() |
52 | + # Callback the notify before starting the download of the actual data |
53 | + # for the images. |
54 | + if notify is not None: |
55 | + reactor.callLater(0, notify.callback, None) |
56 | self.perform_write() |
57 | self.resource_set_cleaner() |
58 | |
59 | @@ -905,7 +919,8 @@ |
60 | writer.sync(reader, rpath) |
61 | |
62 | |
63 | -def download_all_boot_resources(sources, product_mapping, store=None): |
64 | +def download_all_boot_resources( |
65 | + sources, product_mapping, store=None, notify=None): |
66 | """Downloads all of the boot resources from the sources. |
67 | |
68 | Boot resources are stored into the `BootResource` model. |
69 | @@ -914,6 +929,8 @@ |
70 | which we should download. |
71 | :param product_mapping: A `ProductMapping` describing the resources to be |
72 | downloaded. |
73 | + :param notify: Instance of `Deferred` that is called when all the metadata |
74 | + has been downloaded and the image data download has been started. |
75 | """ |
76 | maaslog.debug("Initializing BootResourceStore.") |
77 | if store is None: |
78 | @@ -925,7 +942,7 @@ |
79 | source['url'], store, product_mapping, |
80 | keyring_file=source.get('keyring')) |
81 | maaslog.debug("Finalizing BootResourceStore.") |
82 | - store.finalize() |
83 | + store.finalize(notify=notify) |
84 | |
85 | |
86 | def set_global_default_releases(): |
87 | @@ -959,19 +976,22 @@ |
88 | |
89 | |
90 | @asynchronous(timeout=FOREVER) |
91 | -def _import_resources(): |
92 | +def _import_resources(notify=None): |
93 | """Import boot resources. |
94 | |
95 | Pulls the sources from `BootSource`. This only starts the process if |
96 | some SYNCED `BootResource` already exist. |
97 | |
98 | This MUST be called from outside of a database transaction. |
99 | + |
100 | + :param notify: Instance of `Deferred` that is called when all the metadata |
101 | + has been downloaded and the image data download has been started. |
102 | """ |
103 | # Avoid circular import. |
104 | from maasserver.clusterrpc.boot_images import RackControllersImporter |
105 | |
106 | # Sync boot resources into the region. |
107 | - d = deferToDatabase(_import_resources_with_lock) |
108 | + d = deferToDatabase(_import_resources_with_lock, notify=notify) |
109 | |
110 | def cb_import(_): |
111 | d = deferToDatabase(RackControllersImporter.new) |
112 | @@ -988,7 +1008,7 @@ |
113 | @synchronous |
114 | @with_connection |
115 | @synchronised(locks.import_images.TRY) # TRY is important; read docstring. |
116 | -def _import_resources_with_lock(): |
117 | +def _import_resources_with_lock(notify=None): |
118 | """Import boot resources once the `import_images` lock is held. |
119 | |
120 | This should *not* be called in a transaction; it will manage transactions |
121 | @@ -999,11 +1019,19 @@ |
122 | :raise DatabaseLockNotHeld: If the `import_images` lock cannot be acquired |
123 | at the beginning of this method. This happens quickly because the lock |
124 | is acquired using a TRY variant; see `dblocks`. |
125 | + |
126 | + :param notify: Instance of `Deferred` that is called when all the metadata |
127 | + has been downloaded and the image data download has been started. |
128 | """ |
129 | assert not in_transaction(), ( |
130 | "_import_resources_with_lock() must not be called within a " |
131 | "preexisting transaction; it manages its own.") |
132 | |
133 | + # Make sure that notify is a `Deferred` that has not beed called. |
134 | + if notify is not None: |
135 | + assert isinstance(notify, Deferred), "Notify should be a `Deferred`" |
136 | + assert not notify.called, "Notify should not have already been called." |
137 | + |
138 | # Make sure that maas user's GNUPG home directory exists. This is |
139 | # needed for importing of boot resources, which occurs on the region |
140 | # as well as the clusters. |
141 | @@ -1033,20 +1061,23 @@ |
142 | return |
143 | product_mapping = map_products(image_descriptions) |
144 | |
145 | - download_all_boot_resources(sources, product_mapping) |
146 | + download_all_boot_resources(sources, product_mapping, notify=notify) |
147 | set_global_default_releases() |
148 | maaslog.info( |
149 | "Finished importing of boot images from %d source(s).", |
150 | len(sources)) |
151 | |
152 | |
153 | -def _import_resources_in_thread(): |
154 | +def _import_resources_in_thread(notify=None): |
155 | """Import boot resources in a thread managed by Twisted. |
156 | |
157 | Errors are logged. The returned `Deferred` will never errback so it's safe |
158 | to use in a `TimerService`, for example. |
159 | + |
160 | + :param notify: Instance of `Deferred` that is called when all the metadata |
161 | + has been downloaded and the image data download has been started. |
162 | """ |
163 | - d = deferToDatabase(_import_resources) |
164 | + d = deferToDatabase(_import_resources, notify=notify) |
165 | d.addErrback(_handle_import_failures) |
166 | return d |
167 | |
168 | @@ -1062,14 +1093,17 @@ |
169 | log.err(failure, "Importing boot resources failed.") |
170 | |
171 | |
172 | -def import_resources(): |
173 | +def import_resources(notify=None): |
174 | """Starts the importing of boot resources. |
175 | |
176 | Note: This function returns immediately. It only starts the process, it |
177 | doesn't wait for it to be finished, as it can take several minutes to |
178 | complete. |
179 | + |
180 | + :param notify: Instance of `Deferred` that is called when all the metadata |
181 | + has been downloaded and the image data download has been started. |
182 | """ |
183 | - reactor.callFromThread(_import_resources_in_thread) |
184 | + reactor.callFromThread(_import_resources_in_thread, notify=notify) |
185 | |
186 | |
187 | def is_import_resources_running(): |
188 | |
189 | === added file 'src/maasserver/static/js/angular/controllers/images.js' |
190 | --- src/maasserver/static/js/angular/controllers/images.js 1970-01-01 00:00:00 +0000 |
191 | +++ src/maasserver/static/js/angular/controllers/images.js 2016-09-14 21:01:21 +0000 |
192 | @@ -0,0 +1,11 @@ |
193 | +/* Copyright 2016 Canonical Ltd. This software is licensed under the |
194 | + * GNU Affero General Public License version 3 (see the file LICENSE). |
195 | + * |
196 | + * MAAS Images Controller |
197 | + */ |
198 | + |
199 | +angular.module('MAAS').controller('ImagesController', [ |
200 | + '$rootScope', function($rootScope) { |
201 | + $rootScope.page = "images"; |
202 | + $rootScope.title = "Boot Images"; |
203 | + }]); |
204 | |
205 | === added file 'src/maasserver/static/js/angular/directives/boot_images.js' |
206 | --- src/maasserver/static/js/angular/directives/boot_images.js 1970-01-01 00:00:00 +0000 |
207 | +++ src/maasserver/static/js/angular/directives/boot_images.js 2016-09-14 21:01:21 +0000 |
208 | @@ -0,0 +1,744 @@ |
209 | +/* Copyright 2016 Canonical Ltd. This software is licensed under the |
210 | + * GNU Affero General Public License version 3 (see the file LICENSE). |
211 | + * |
212 | + * Boot images directive. |
213 | +*/ |
214 | + |
215 | +angular.module('MAAS').directive('maasBootImagesStatus', [ |
216 | + 'BootResourcesManager', |
217 | + function(BootResourcesManager) { |
218 | + return { |
219 | + restrict: "E", |
220 | + scope: {}, |
221 | + template: [ |
222 | + '<p class="page-header__status" ', |
223 | + 'data-ng-if="data.region_import_running">', |
224 | + '<span class="u-text--loading u-margin--left-small">', |
225 | + '<i class="icon icon--loading u-animation--spin ', |
226 | + 'u-margin--right-tiny"></i>', |
227 | + 'Step 1/2: Region controller importing', |
228 | + '</span>', |
229 | + '</p>', |
230 | + '<p class="page-header__status" ', |
231 | + 'data-ng-if="data.rack_import_running">', |
232 | + '<span class="u-text--loading u-margin--left-small">', |
233 | + '<i class="icon icon--loading u-animation--spin ', |
234 | + 'u-margin--right-tiny"></i>', |
235 | + 'Step 2/2: Rack controller(s) importing', |
236 | + '</span>', |
237 | + '</p>' |
238 | + ].join(''), |
239 | + controller: function($scope, $rootScope, $element, $document) { |
240 | + // This controller doesn't start the polling. The |
241 | + // `maasBootImages` controller should be used on the page to |
242 | + // start the polling. This just presents a nice status spinner |
243 | + // when the polling is enabled. |
244 | + $scope.data = BootResourcesManager.getData(); |
245 | + } |
246 | + }; |
247 | + }]); |
248 | + |
249 | +angular.module('MAAS').directive('maasBootImages', [ |
250 | + '$filter', 'BootResourcesManager', 'UsersManager', 'ManagerHelperService', |
251 | + function($filter, BootResourcesManager, |
252 | + UsersManager, ManagerHelperService) { |
253 | + return { |
254 | + restrict: "E", |
255 | + scope: { |
256 | + design: "=?" |
257 | + }, |
258 | + templateUrl: ( |
259 | + 'static/partials/boot-images.html?v=' + ( |
260 | + MAAS_config.files_version)), |
261 | + controller: function($scope, $rootScope, $element, $document) { |
262 | + $scope.loading = true; |
263 | + $scope.saving = false; |
264 | + $scope.design = $scope.design || 'page'; |
265 | + $scope.bootResources = BootResourcesManager.getData(); |
266 | + $scope.ubuntuImages = []; |
267 | + $scope.source = { |
268 | + isNew: false, |
269 | + tooMany: false, |
270 | + showingAdvanced: false, |
271 | + connecting: false, |
272 | + errorMessage: "", |
273 | + source_type: 'maas.io', |
274 | + url: '', |
275 | + keyring_filename: '', |
276 | + keyring_data: '', |
277 | + releases: [], |
278 | + arches: [], |
279 | + selections: { |
280 | + changed: false, |
281 | + releases: [], |
282 | + arches: [] |
283 | + } |
284 | + }; |
285 | + $scope.otherImages = []; |
286 | + $scope.other = { |
287 | + changed: false, |
288 | + images: [] |
289 | + }; |
290 | + $scope.generatedImages = []; |
291 | + $scope.customImages = []; |
292 | + |
293 | + // Return true if the authenticated user is super user. |
294 | + $scope.isSuperUser = function() { |
295 | + return UsersManager.isSuperUser(); |
296 | + }; |
297 | + |
298 | + // Return the overall title icon. |
299 | + $scope.getTitleIcon = function() { |
300 | + if($scope.bootResources.resources.length === 0) { |
301 | + return 'icon--error'; |
302 | + } else { |
303 | + return 'icon--success'; |
304 | + } |
305 | + }; |
306 | + |
307 | + // Return true if the mirror path section should be shown. |
308 | + $scope.showMirrorPath = function() { |
309 | + if($scope.source.source_type === 'custom') { |
310 | + return true; |
311 | + } else { |
312 | + return false; |
313 | + } |
314 | + }; |
315 | + |
316 | + // Return true if the advanced options are shown. |
317 | + $scope.isShowingAdvancedOptions = function() { |
318 | + return $scope.source.showingAdvanced; |
319 | + }; |
320 | + |
321 | + // Toggle showing the advanced options. |
322 | + $scope.toggleAdvancedOptions = function() { |
323 | + $scope.source.showingAdvanced = ( |
324 | + !$scope.source.showingAdvanced); |
325 | + }; |
326 | + |
327 | + // Return true if both keyring options are set. |
328 | + $scope.bothKeyringOptionsSet = function() { |
329 | + return ( |
330 | + $scope.source.keyring_filename.length > 0 && |
331 | + $scope.source.keyring_data.length > 0); |
332 | + }; |
333 | + |
334 | + // Return true when the connect button for the mirror path |
335 | + // should be shown. |
336 | + $scope.showConnectButton = function() { |
337 | + return $scope.source.isNew; |
338 | + }; |
339 | + |
340 | + // Called when the source radio chanaged. |
341 | + $scope.sourceChanged = function() { |
342 | + var currentSources = $scope.bootResources.ubuntu.sources; |
343 | + if(currentSources.length === 0) { |
344 | + $scope.source.isNew = true; |
345 | + } else { |
346 | + var prevNew = $scope.source.isNew; |
347 | + $scope.source.isNew = ( |
348 | + $scope.source.source_type !== |
349 | + currentSources[0].source_type); |
350 | + if($scope.source.source_type === 'custom') { |
351 | + $scope.source.isNew = $scope.source.isNew || ( |
352 | + $scope.source.url !== |
353 | + currentSources[0].url |
354 | + ); |
355 | + } |
356 | + if(prevNew && !$scope.source.isNew) { |
357 | + // No longer a new source set url and keyring to |
358 | + // original. |
359 | + $scope.source.url = currentSources[0].url; |
360 | + $scope.source.keyring_filename = ( |
361 | + currentSources[0].keyring_filename); |
362 | + $scope.source.keyring_data = ( |
363 | + currentSources[0].keyring_data); |
364 | + } |
365 | + $scope.source.releases = []; |
366 | + $scope.source.arches = []; |
367 | + $scope.source.selections = { |
368 | + changed: false, |
369 | + releases: [], |
370 | + arches: [] |
371 | + }; |
372 | + } |
373 | + $scope.updateSource(); |
374 | + $scope.regenerateUbuntuImages(); |
375 | + |
376 | + // When the source is new and its maas.io automatically |
377 | + // fetch the source information. |
378 | + if($scope.source.isNew && |
379 | + $scope.source.source_type === 'maas.io') { |
380 | + $scope.connect(); |
381 | + } |
382 | + }; |
383 | + |
384 | + // Return true when the connect button should be disabled. |
385 | + $scope.isConnectButtonDisabled = function() { |
386 | + if($scope.source.source_type === 'maas.io') { |
387 | + return false; |
388 | + } else { |
389 | + return ($scope.bothKeyringOptionsSet() || |
390 | + $scope.source.url.length === 0 || |
391 | + $scope.source.connecting); |
392 | + } |
393 | + }; |
394 | + |
395 | + // Return the source parameters. |
396 | + $scope.getSourceParams = function() { |
397 | + if($scope.source.source_type === 'maas.io') { |
398 | + return { |
399 | + source_type: 'maas.io' |
400 | + }; |
401 | + } else { |
402 | + return { |
403 | + source_type: $scope.source.source_type, |
404 | + url: $scope.source.url, |
405 | + keyring_filename: $scope.source.keyring_filename, |
406 | + keyring_data: $scope.source.keyring_data |
407 | + }; |
408 | + } |
409 | + }; |
410 | + |
411 | + // Select the default images that should be selected. Current |
412 | + // defaults are '16.04 LTS' and 'amd64'. |
413 | + $scope.selectDefaults = function() { |
414 | + angular.forEach($scope.source.releases, function(release) { |
415 | + if(release.name === "xenial") { |
416 | + $scope.source.selections.releases.push(release); |
417 | + } |
418 | + }); |
419 | + angular.forEach($scope.source.arches, function(arch) { |
420 | + if(arch.name === "amd64") { |
421 | + $scope.source.selections.arches.push(arch); |
422 | + } |
423 | + }); |
424 | + }; |
425 | + |
426 | + // Connected to the simplestreams endpoint. This only gets the |
427 | + // information from the endpoint it does not save it into the |
428 | + // database. |
429 | + $scope.connect = function() { |
430 | + if($scope.isConnectButtonDisabled()) { |
431 | + return; |
432 | + } |
433 | + |
434 | + var source = $scope.getSourceParams(); |
435 | + $scope.source.connecting = true; |
436 | + $scope.source.releases = []; |
437 | + $scope.source.arches = []; |
438 | + $scope.source.selections.changed = true; |
439 | + $scope.source.selections.releases = []; |
440 | + $scope.source.selections.arches = []; |
441 | + $scope.regenerateUbuntuImages(); |
442 | + BootResourcesManager.fetch(source).then(function(data) { |
443 | + $scope.source.connecting = false; |
444 | + data = angular.fromJson(data); |
445 | + $scope.source.releases = data.releases; |
446 | + $scope.source.arches = data.arches; |
447 | + $scope.selectDefaults(); |
448 | + $scope.regenerateUbuntuImages(); |
449 | + }, function(error) { |
450 | + $scope.source.connecting = false; |
451 | + $scope.source.errorMessage = error; |
452 | + }); |
453 | + }; |
454 | + |
455 | + // Return true if the connect block should be shown. |
456 | + $scope.showConnectBlock = function() { |
457 | + return $scope.source.tooMany || ( |
458 | + $scope.source.isNew && !$scope.showSelections()); |
459 | + }; |
460 | + |
461 | + // Return true if the release and architecture selection |
462 | + // should be shown. |
463 | + $scope.showSelections = function() { |
464 | + return ( |
465 | + $scope.source.releases.length > 0 && |
466 | + $scope.source.arches.length > 0); |
467 | + }; |
468 | + |
469 | + // Return the Ubuntu LTS releases. |
470 | + $scope.getUbuntuLTSReleases = function() { |
471 | + var releases = $scope.bootResources.ubuntu.releases; |
472 | + if($scope.source.isNew) { |
473 | + releases = $scope.source.releases; |
474 | + } |
475 | + var filtered = []; |
476 | + angular.forEach(releases, function(release) { |
477 | + if(release.title.indexOf('LTS') !== -1) { |
478 | + filtered.push(release); |
479 | + } |
480 | + }); |
481 | + return filtered; |
482 | + }; |
483 | + |
484 | + // Return the Ubuntu non-LTS releases. |
485 | + $scope.getUbuntuNonLTSReleases = function() { |
486 | + var releases = $scope.bootResources.ubuntu.releases; |
487 | + if($scope.source.isNew) { |
488 | + releases = $scope.source.releases; |
489 | + } |
490 | + var filtered = []; |
491 | + angular.forEach(releases, function(release) { |
492 | + if(release.title.indexOf('LTS') === -1) { |
493 | + filtered.push(release); |
494 | + } |
495 | + }); |
496 | + return filtered; |
497 | + }; |
498 | + |
499 | + // Return the available architectures. |
500 | + $scope.getArchitectures = function() { |
501 | + var arches = $scope.bootResources.ubuntu.arches; |
502 | + if($scope.source.isNew) { |
503 | + arches = $scope.source.arches; |
504 | + } |
505 | + return arches; |
506 | + }; |
507 | + |
508 | + // Return true if the source has this selected. |
509 | + $scope.isSelected = function(type, obj) { |
510 | + return $scope.source.selections[type].indexOf(obj) >= 0; |
511 | + }; |
512 | + |
513 | + // Toggle the selection of the release or the architecture. |
514 | + $scope.toggleSelection = function(type, obj) { |
515 | + var idx = $scope.source.selections[type].indexOf(obj); |
516 | + if(idx === -1) { |
517 | + $scope.source.selections[type].push(obj); |
518 | + } else { |
519 | + $scope.source.selections[type].splice(idx, 1); |
520 | + } |
521 | + $scope.source.selections.changed = true; |
522 | + $scope.regenerateUbuntuImages(); |
523 | + }; |
524 | + |
525 | + // Return true if the images table should be shown. |
526 | + $scope.showImagesTable = function() { |
527 | + if($scope.ubuntuImages.length > 0) { |
528 | + return true; |
529 | + } else { |
530 | + // Show the images table source has |
531 | + // releases and architectures. |
532 | + return ( |
533 | + $scope.source.arches.length > 0 && |
534 | + $scope.source.releases.length > 0); |
535 | + } |
536 | + }; |
537 | + |
538 | + // Regenerates the Ubuntu images list for the directive. |
539 | + $scope.regenerateUbuntuImages = function() { |
540 | + var getResource = function() { return null; }; |
541 | + var resources = $scope.bootResources.resources.filter( |
542 | + function(resource) { |
543 | + var name_split = resource.name.split('/'); |
544 | + var resource_os = name_split[0]; |
545 | + return ( |
546 | + resource.rtype === 0 && |
547 | + resource_os === 'ubuntu'); |
548 | + }); |
549 | + if(!$scope.source.isNew) { |
550 | + getResource = function(release, arch) { |
551 | + var i; |
552 | + for(i = 0; i < resources.length; i++) { |
553 | + // Only care about Ubuntu images. |
554 | + var resource = (resources[i]); |
555 | + var name_split = resource.name.split('/'); |
556 | + var resource_release = name_split[1]; |
557 | + if(resource_release === release && |
558 | + resource.arch === arch) { |
559 | + resources.splice(i, 1); |
560 | + return resource; |
561 | + } |
562 | + } |
563 | + return null; |
564 | + }; |
565 | + } |
566 | + |
567 | + // Create the images based on the selections. |
568 | + $scope.ubuntuImages.length = 0; |
569 | + angular.forEach($scope.source.selections.releases, |
570 | + function(release) { |
571 | + angular.forEach($scope.source.selections.arches, |
572 | + function(arch) { |
573 | + var image = { |
574 | + icon: 'icon--status-queued', |
575 | + title: release.title, |
576 | + arch: arch.title, |
577 | + size: '-', |
578 | + status: 'Queued for download', |
579 | + beingDeleted: false |
580 | + }; |
581 | + var resource = getResource( |
582 | + release.name, arch.name); |
583 | + if(angular.isObject(resource)) { |
584 | + image.icon = ( |
585 | + 'icon--status-' + resource.icon); |
586 | + image.size = resource.size; |
587 | + image.status = resource.status; |
588 | + if(resource.downloading) { |
589 | + image.icon += ' u-animation--pulse'; |
590 | + } |
591 | + } |
592 | + $scope.ubuntuImages.push(image); |
593 | + }); |
594 | + }); |
595 | + |
596 | + // If not a new source and images remain in resources, then |
597 | + // those are set to be deleted. |
598 | + if(!$scope.source.isNew) { |
599 | + angular.forEach(resources, function(resource) { |
600 | + var image = { |
601 | + icon: 'icon--status-failed', |
602 | + title: resource.title, |
603 | + arch: resource.arch, |
604 | + size: resource.size, |
605 | + status: 'Queued for deletion', |
606 | + beingDeleted: true |
607 | + }; |
608 | + $scope.ubuntuImages.push(image); |
609 | + }); |
610 | + } |
611 | + }; |
612 | + |
613 | + // Regenerates the other images list for the directive. |
614 | + $scope.regenerateOtherImages = function() { |
615 | + var isOther = function(resource) { |
616 | + var name_split = resource.name.split('/'); |
617 | + var resource_os = name_split[0]; |
618 | + return ( |
619 | + resource.rtype === 0 && |
620 | + resource_os !== 'ubuntu' && |
621 | + resource_os !== 'custom'); |
622 | + }; |
623 | + var resources = ( |
624 | + $scope.bootResources.resources.filter(isOther)); |
625 | + var getResource = function(release, arch) { |
626 | + var i; |
627 | + for(i = 0; i < resources.length; i++) { |
628 | + // Only care about other images. Removing custom, |
629 | + // bootloaders, and Ubuntu images. |
630 | + var resource = (resources[i]); |
631 | + var name_split = resource.name.split('/'); |
632 | + var resource_release = name_split[1]; |
633 | + if(resource_release === release && |
634 | + resource.arch === arch) { |
635 | + resources.splice(i, 1); |
636 | + return resource; |
637 | + } |
638 | + } |
639 | + return null; |
640 | + }; |
641 | + |
642 | + // Create the images based on the selections. |
643 | + $scope.otherImages.length = 0; |
644 | + angular.forEach($scope.other.images, |
645 | + function(otherImage) { |
646 | + if(otherImage.checked) { |
647 | + var name_split = otherImage.name.split('/'); |
648 | + var image = { |
649 | + icon: 'icon--status-queued', |
650 | + title: otherImage.title, |
651 | + arch: name_split[1], |
652 | + size: '-', |
653 | + status: 'Queued for download', |
654 | + beingDeleted: false |
655 | + }; |
656 | + var resource = getResource( |
657 | + name_split[3], name_split[1]); |
658 | + if(angular.isObject(resource)) { |
659 | + image.icon = ( |
660 | + 'icon--status-' + resource.icon); |
661 | + image.size = resource.size; |
662 | + image.status = resource.status; |
663 | + if(resource.downloading) { |
664 | + image.icon += ' u-animation--pulse'; |
665 | + } |
666 | + } |
667 | + $scope.otherImages.push(image); |
668 | + } |
669 | + }); |
670 | + |
671 | + // If not a new source and images remain in resources, then |
672 | + // those are set to be deleted. |
673 | + angular.forEach(resources, function(resource) { |
674 | + var image = { |
675 | + icon: 'icon--status-failed', |
676 | + title: resource.title, |
677 | + arch: resource.arch, |
678 | + size: resource.size, |
679 | + status: 'Queued for deletion', |
680 | + beingDeleted: true |
681 | + }; |
682 | + $scope.otherImages.push(image); |
683 | + }); |
684 | + }; |
685 | + |
686 | + // Helper for basic image generation. |
687 | + $scope._regenerateImages = function(rtype, images) { |
688 | + // Create the generated images list. |
689 | + images.length = 0; |
690 | + angular.forEach($scope.bootResources.resources, |
691 | + function(resource) { |
692 | + if(resource.rtype === rtype) { |
693 | + var image = { |
694 | + icon: 'icon--status-' + resource.icon, |
695 | + title: resource.title, |
696 | + arch: resource.arch, |
697 | + size: resource.size, |
698 | + status: resource.status |
699 | + }; |
700 | + if(resource.downloading) { |
701 | + image.icon += ' u-animation--pulse'; |
702 | + } |
703 | + images.push(image); |
704 | + } |
705 | + }); |
706 | + }; |
707 | + |
708 | + // Regenerates the generated images list for the directive. |
709 | + $scope.regenerateGeneratedImages = function() { |
710 | + $scope._regenerateImages(1, $scope.generatedImages); |
711 | + }; |
712 | + |
713 | + // Regenerates the custom images list for the directive. |
714 | + $scope.regenerateCustomImages = function() { |
715 | + $scope._regenerateImages(2, $scope.customImages); |
716 | + }; |
717 | + |
718 | + // Returns true if at least one LTS is selected. |
719 | + $scope.ltsIsSelected = function() { |
720 | + var i; |
721 | + for(i = 0; i < $scope.ubuntuImages.length; i++) { |
722 | + // Must have LTS in the title and not being deleted. |
723 | + if(!$scope.ubuntuImages[i].beingDeleted && |
724 | + $scope.ubuntuImages[i].title.indexOf('LTS') >= 0) { |
725 | + // Must be greater than Ubuntu series 14. |
726 | + var series = parseInt( |
727 | + $scope.ubuntuImages[i].title.split('.')[0], 10); |
728 | + if(series >= 14) { |
729 | + return true; |
730 | + } |
731 | + } |
732 | + } |
733 | + return false; |
734 | + }; |
735 | + |
736 | + // Return true if the stop import button should be shown. |
737 | + $scope.showStopImportButton = function() { |
738 | + return $scope.bootResources.region_import_running; |
739 | + }; |
740 | + |
741 | + // Return true if should show save selection button, this |
742 | + // doesn't mean it can actually be clicked. |
743 | + $scope.showSaveSelection = function() { |
744 | + return $scope.showImagesTable(); |
745 | + }; |
746 | + |
747 | + // Return true if can save the current selection. |
748 | + $scope.canSaveSelection = function() { |
749 | + return !$scope.saving && $scope.ltsIsSelected(); |
750 | + }; |
751 | + |
752 | + // Return the text for the save selection button. |
753 | + $scope.getSaveSelectionText = function() { |
754 | + if($scope.saving) { |
755 | + return "Saving..."; |
756 | + } else { |
757 | + return "Save selection"; |
758 | + } |
759 | + }; |
760 | + |
761 | + // Save the selections into boot selections. |
762 | + $scope.saveSelection = function() { |
763 | + if(!$scope.canSaveSelection()) { |
764 | + return; |
765 | + } |
766 | + |
767 | + var params = $scope.getSourceParams(); |
768 | + params.releases = ( |
769 | + $scope.source.selections.releases.map(function(obj) { |
770 | + return obj.name; |
771 | + })); |
772 | + params.arches = ( |
773 | + $scope.source.selections.arches.map(function(obj) { |
774 | + return obj.name; |
775 | + })); |
776 | + $scope.saving = true; |
777 | + BootResourcesManager.saveUbuntu(params).then(function() { |
778 | + $scope.saving = false; |
779 | + $scope.source.isNew = false; |
780 | + $scope.source.selections.changed = false; |
781 | + $scope.updateSource(); |
782 | + }); |
783 | + }; |
784 | + |
785 | + // Re-create an array with the new objects using the old |
786 | + // selected array. |
787 | + $scope.getNewSelections = function(newObjs, oldSelections) { |
788 | + var newSelections = []; |
789 | + angular.forEach(newObjs, function(obj) { |
790 | + angular.forEach(oldSelections, function(selection) { |
791 | + if(obj.name === selection.name) { |
792 | + newSelections.push(obj); |
793 | + } |
794 | + }); |
795 | + }); |
796 | + return newSelections; |
797 | + }; |
798 | + |
799 | + // Update the source information. |
800 | + $scope.updateSource = function() { |
801 | + // Do not update if the source is new. |
802 | + if($scope.source.isNew) { |
803 | + return; |
804 | + } |
805 | + |
806 | + var source_len = $scope.bootResources.ubuntu.sources.length; |
807 | + if(source_len === 0) { |
808 | + $scope.source.isNew = true; |
809 | + $scope.source.source_type = 'custom'; |
810 | + $scope.source.errorMessage = ( |
811 | + "Currently no source exists."); |
812 | + } else if(source_len === 1) { |
813 | + var source = $scope.bootResources.ubuntu.sources[0]; |
814 | + $scope.source.source_type = source.source_type; |
815 | + if(source.source_type === "maas.io") { |
816 | + $scope.source.url = ""; |
817 | + $scope.source.keyring_filename = ""; |
818 | + $scope.source.keyring_data = ""; |
819 | + } else { |
820 | + $scope.source.url = source.url; |
821 | + $scope.source.keyring_filename = ( |
822 | + source.keyring_filename); |
823 | + $scope.source.keyring_data = source.keyring_data; |
824 | + } |
825 | + $scope.source.releases = ( |
826 | + $scope.bootResources.ubuntu.releases); |
827 | + $scope.source.arches = ( |
828 | + $scope.bootResources.ubuntu.arches); |
829 | + if(!$scope.source.selections.changed) { |
830 | + // User didn't make a change update to the |
831 | + // current selections server side. |
832 | + $scope.source.selections.releases = ( |
833 | + $scope.source.releases.filter(function(obj) { |
834 | + return obj.checked; |
835 | + })); |
836 | + $scope.source.selections.arches = ( |
837 | + $scope.source.arches.filter(function(obj) { |
838 | + return obj.checked; |
839 | + })); |
840 | + } else { |
841 | + // Update the objects to be the new objects, with |
842 | + // the same selections. |
843 | + $scope.source.selections.releases = ( |
844 | + $scope.getNewSelections( |
845 | + $scope.source.releases, |
846 | + $scope.source.selections.releases)); |
847 | + $scope.source.selections.arches = ( |
848 | + $scope.getNewSelections( |
849 | + $scope.source.arches, |
850 | + $scope.source.selections.arches)); |
851 | + } |
852 | + $scope.regenerateUbuntuImages(); |
853 | + } else { |
854 | + // Having more than one source prevents modification |
855 | + // of the sources. |
856 | + $scope.source.tooMany = true; |
857 | + $scope.source.releases = ( |
858 | + $scope.bootResources.ubuntu.releases); |
859 | + $scope.source.arches = ( |
860 | + $scope.bootResources.ubuntu.arches); |
861 | + $scope.source.selections.releases = ( |
862 | + $scope.source.releases.filter(function(obj) { |
863 | + return obj.checked; |
864 | + })); |
865 | + $scope.source.selections.arches = ( |
866 | + $scope.source.arches.filter(function(obj) { |
867 | + return obj.checked; |
868 | + })); |
869 | + $scope.source.errorMessage = ( |
870 | + "More than one image source exists. UI does not " + |
871 | + "support modification of sources when more than " + |
872 | + "one has been defined. Used the API to adjust " + |
873 | + "your sources."); |
874 | + $scope.regenerateUbuntuImages(); |
875 | + } |
876 | + }; |
877 | + |
878 | + // Toggle the selection of other images. |
879 | + $scope.toggleOtherSelection = function(image) { |
880 | + $scope.other.changed = true; |
881 | + image.checked = !image.checked; |
882 | + $scope.regenerateOtherImages(); |
883 | + }; |
884 | + |
885 | + // Save the other image selections into boot selections. |
886 | + $scope.saveOtherSelection = function() { |
887 | + var params = { |
888 | + images: $scope.other.images.filter(function(image) { |
889 | + return image.checked; |
890 | + }).map(function(image) { |
891 | + return image.name; |
892 | + }) |
893 | + }; |
894 | + $scope.saving = true; |
895 | + BootResourcesManager.saveOther(params).then(function() { |
896 | + $scope.saving = false; |
897 | + }); |
898 | + }; |
899 | + |
900 | + // Start polling now that the directive is viewable and ensure |
901 | + // the UserManager is loaded. |
902 | + var ready = 2; |
903 | + BootResourcesManager.startPolling().then(function() { |
904 | + ready -= 1; |
905 | + if(ready === 0) { |
906 | + $scope.loading = false; |
907 | + } |
908 | + }); |
909 | + ManagerHelperService.loadManager(UsersManager).then(function() { |
910 | + ready -= 1; |
911 | + if(ready === 0) { |
912 | + $scope.loading = false; |
913 | + } |
914 | + }); |
915 | + |
916 | + // Update the source information with the ubuntu source |
917 | + // information changes. |
918 | + $scope.$watch("bootResources.ubuntu", function() { |
919 | + if(!angular.isObject($scope.bootResources.ubuntu)) { |
920 | + return; |
921 | + } |
922 | + $scope.updateSource(); |
923 | + }); |
924 | + |
925 | + // Regenerate the images array when the resources change. |
926 | + $scope.$watch("bootResources.resources", function() { |
927 | + if(!angular.isArray($scope.bootResources.resources)) { |
928 | + return; |
929 | + } |
930 | + $scope.regenerateUbuntuImages(); |
931 | + $scope.regenerateOtherImages(); |
932 | + $scope.regenerateGeneratedImages(); |
933 | + $scope.regenerateCustomImages(); |
934 | + }); |
935 | + |
936 | + $scope.$watch("bootResources.other_images", function() { |
937 | + if(!angular.isArray($scope.bootResources.other_images)) { |
938 | + return; |
939 | + } |
940 | + if(!$scope.other.changed) { |
941 | + $scope.other.images = $scope.bootResources.other_images; |
942 | + } |
943 | + $scope.regenerateOtherImages(); |
944 | + }); |
945 | + |
946 | + // Stop polling when the directive is destroyed. |
947 | + $scope.$on('$destroy', function() { |
948 | + BootResourcesManager.stopPolling(); |
949 | + }); |
950 | + } |
951 | + }; |
952 | + }]); |
953 | |
954 | === added file 'src/maasserver/static/js/angular/directives/tests/test_boot_images.js' |
955 | --- src/maasserver/static/js/angular/directives/tests/test_boot_images.js 1970-01-01 00:00:00 +0000 |
956 | +++ src/maasserver/static/js/angular/directives/tests/test_boot_images.js 2016-09-14 21:01:21 +0000 |
957 | @@ -0,0 +1,1454 @@ |
958 | +/* Copyright 2016 Canonical Ltd. This software is licensed under the |
959 | + * GNU Affero General Public License version 3 (see the file LICENSE). |
960 | + * |
961 | + * Unit tests for boot images directive. |
962 | + */ |
963 | + |
964 | +describe("maasBootImages", function() { |
965 | + |
966 | + // Load the MAAS module. |
967 | + beforeEach(module("MAAS")); |
968 | + |
969 | + // Preload the $templateCache with empty contents. We only test the |
970 | + // controller of the directive, not the template. |
971 | + var $q, $templateCache; |
972 | + beforeEach(inject(function($injector) { |
973 | + $q = $injector.get('$q'); |
974 | + $templateCache = $injector.get('$templateCache'); |
975 | + $templateCache.put("static/partials/boot-images.html?v=undefined", ''); |
976 | + })); |
977 | + |
978 | + // Load the required managers. |
979 | + var BootResourcesManager, UsersManager, ManagerHelperService; |
980 | + beforeEach(inject(function($injector) { |
981 | + BootResourcesManager = $injector.get('BootResourcesManager'); |
982 | + UsersManager = $injector.get('UsersManager'); |
983 | + ManagerHelperService = $injector.get('ManagerHelperService'); |
984 | + })); |
985 | + |
986 | + // Create a new scope before each test. |
987 | + var $scope; |
988 | + beforeEach(inject(function($rootScope) { |
989 | + $scope = $rootScope.$new(); |
990 | + })); |
991 | + |
992 | + // Return the compiled directive with the items from the scope. |
993 | + function compileDirective(design) { |
994 | + if(angular.isUndefined(design)) { |
995 | + design = ""; |
996 | + } |
997 | + var directive; |
998 | + var html = [ |
999 | + '<div>', |
1000 | + '<maas-boot-images design="' + design + '"></maas-boot-images>', |
1001 | + '</div>' |
1002 | + ].join(''); |
1003 | + |
1004 | + // Compile the directive. |
1005 | + inject(function($compile) { |
1006 | + directive = $compile(html)($scope); |
1007 | + }); |
1008 | + |
1009 | + // Perform the digest cycle to finish the compile. |
1010 | + $scope.$digest(); |
1011 | + return directive.find("maas-boot-images"); |
1012 | + } |
1013 | + |
1014 | + it("sets initial variables", function() { |
1015 | + var directive = compileDirective(); |
1016 | + var scope = directive.isolateScope(); |
1017 | + expect(scope.loading).toBe(true); |
1018 | + expect(scope.saving).toBe(false); |
1019 | + expect(scope.design).toBe('page'); |
1020 | + expect(scope.bootResources).toBe(BootResourcesManager.getData()); |
1021 | + expect(scope.ubuntuImages).toEqual([]); |
1022 | + expect(scope.source).toEqual({ |
1023 | + isNew: false, |
1024 | + tooMany: false, |
1025 | + showingAdvanced: false, |
1026 | + connecting: false, |
1027 | + errorMessage: "", |
1028 | + source_type: "maas.io", |
1029 | + url: '', |
1030 | + keyring_filename: '', |
1031 | + keyring_data: '', |
1032 | + releases: [], |
1033 | + arches: [], |
1034 | + selections: { |
1035 | + changed: false, |
1036 | + releases: [], |
1037 | + arches: [] |
1038 | + } |
1039 | + }); |
1040 | + expect(scope.otherImages).toEqual([]); |
1041 | + expect(scope.other).toEqual({ |
1042 | + changed: false, |
1043 | + images: [] |
1044 | + }); |
1045 | + expect(scope.generatedImages).toEqual([]); |
1046 | + expect(scope.customImages).toEqual([]); |
1047 | + }); |
1048 | + |
1049 | + it("clears loading once polling and user manager loaded", function() { |
1050 | + var pollingDefer = $q.defer(); |
1051 | + spyOn(BootResourcesManager, "startPolling").and.returnValue( |
1052 | + pollingDefer.promise); |
1053 | + var managerDefer = $q.defer(); |
1054 | + spyOn(ManagerHelperService, "loadManager").and.returnValue( |
1055 | + managerDefer.promise); |
1056 | + |
1057 | + var directive = compileDirective(); |
1058 | + var scope = directive.isolateScope(); |
1059 | + |
1060 | + pollingDefer.resolve(); |
1061 | + $scope.$digest(); |
1062 | + managerDefer.resolve(); |
1063 | + $scope.$digest(); |
1064 | + expect(scope.loading).toBe(false); |
1065 | + }); |
1066 | + |
1067 | + it("calls updateSource when ubuntu changes", function() { |
1068 | + var directive = compileDirective(); |
1069 | + var scope = directive.isolateScope(); |
1070 | + spyOn(scope, "updateSource"); |
1071 | + scope.bootResources.ubuntu = {}; |
1072 | + $scope.$digest(); |
1073 | + |
1074 | + expect(scope.updateSource).toHaveBeenCalled(); |
1075 | + }); |
1076 | + |
1077 | + it("calls all regenerates when resources changes", function() { |
1078 | + var directive = compileDirective(); |
1079 | + var scope = directive.isolateScope(); |
1080 | + spyOn(scope, "regenerateUbuntuImages"); |
1081 | + spyOn(scope, "regenerateOtherImages"); |
1082 | + spyOn(scope, "regenerateGeneratedImages"); |
1083 | + spyOn(scope, "regenerateCustomImages"); |
1084 | + scope.bootResources.resources = []; |
1085 | + $scope.$digest(); |
1086 | + |
1087 | + expect(scope.regenerateUbuntuImages).toHaveBeenCalled(); |
1088 | + expect(scope.regenerateOtherImages).toHaveBeenCalled(); |
1089 | + expect(scope.regenerateGeneratedImages).toHaveBeenCalled(); |
1090 | + expect(scope.regenerateCustomImages).toHaveBeenCalled(); |
1091 | + }); |
1092 | + |
1093 | + it("sets other.images when other_images change", function() { |
1094 | + var directive = compileDirective(); |
1095 | + var scope = directive.isolateScope(); |
1096 | + spyOn(scope, "regenerateOtherImages"); |
1097 | + var sentinel = []; |
1098 | + scope.bootResources.other_images = sentinel; |
1099 | + $scope.$digest(); |
1100 | + |
1101 | + expect(scope.other.images).toBe(sentinel); |
1102 | + expect(scope.regenerateOtherImages).toHaveBeenCalled(); |
1103 | + }); |
1104 | + |
1105 | + it("doesnt sets other.images when other changed", function() { |
1106 | + var directive = compileDirective(); |
1107 | + var scope = directive.isolateScope(); |
1108 | + spyOn(scope, "regenerateOtherImages"); |
1109 | + var sentinel = []; |
1110 | + scope.bootResources.other_images = sentinel; |
1111 | + scope.other.changed = true; |
1112 | + $scope.$digest(); |
1113 | + |
1114 | + expect(scope.other.images).not.toBe(sentinel); |
1115 | + expect(scope.regenerateOtherImages).toHaveBeenCalled(); |
1116 | + }); |
1117 | + |
1118 | + it("stops polling when scope is destroyed", function() { |
1119 | + var directive = compileDirective(); |
1120 | + spyOn(BootResourcesManager, "stopPolling"); |
1121 | + directive.scope().$destroy(); |
1122 | + expect(BootResourcesManager.stopPolling).toHaveBeenCalled(); |
1123 | + }); |
1124 | + |
1125 | + describe("isSuperUser", function() { |
1126 | + |
1127 | + it("returns UsersManager.isSuperUser", function() { |
1128 | + var directive = compileDirective(); |
1129 | + var scope = directive.isolateScope(); |
1130 | + |
1131 | + var sentinel = {}; |
1132 | + spyOn(UsersManager, "isSuperUser").and.returnValue(sentinel); |
1133 | + expect(scope.isSuperUser()).toBe(sentinel); |
1134 | + }); |
1135 | + }); |
1136 | + |
1137 | + describe("getTitleIcon", function() { |
1138 | + |
1139 | + it("returns error when no resources", function() { |
1140 | + var directive = compileDirective(); |
1141 | + var scope = directive.isolateScope(); |
1142 | + BootResourcesManager._data.resources = []; |
1143 | + expect(scope.getTitleIcon()).toBe('icon--error'); |
1144 | + }); |
1145 | + |
1146 | + it("returns success when resources", function() { |
1147 | + var directive = compileDirective(); |
1148 | + var scope = directive.isolateScope(); |
1149 | + BootResourcesManager._data.resources = [{}]; |
1150 | + expect(scope.getTitleIcon()).toBe('icon--success'); |
1151 | + }); |
1152 | + }); |
1153 | + |
1154 | + describe("showMirrorPath", function() { |
1155 | + |
1156 | + it("returns true when custom", function() { |
1157 | + var directive = compileDirective(); |
1158 | + var scope = directive.isolateScope(); |
1159 | + scope.source.source_type = 'custom'; |
1160 | + expect(scope.showMirrorPath()).toBe(true); |
1161 | + }); |
1162 | + |
1163 | + it("returns false when maas.io", function() { |
1164 | + var directive = compileDirective(); |
1165 | + var scope = directive.isolateScope(); |
1166 | + scope.source.source_type = 'maas.io'; |
1167 | + expect(scope.showMirrorPath()).toBe(false); |
1168 | + }); |
1169 | + }); |
1170 | + |
1171 | + describe("isShowingAdvancedOptions", function() { |
1172 | + |
1173 | + it("returns showingAdvanced", function() { |
1174 | + var directive = compileDirective(); |
1175 | + var scope = directive.isolateScope(); |
1176 | + var sentinel = {}; |
1177 | + scope.source.showingAdvanced = sentinel; |
1178 | + expect(scope.isShowingAdvancedOptions()).toBe(sentinel); |
1179 | + }); |
1180 | + }); |
1181 | + |
1182 | + describe("toggleAdvancedOptions", function() { |
1183 | + |
1184 | + it("inverts showingAdvanced", function() { |
1185 | + var directive = compileDirective(); |
1186 | + var scope = directive.isolateScope(); |
1187 | + scope.source.showingAdvanced = false; |
1188 | + scope.toggleAdvancedOptions(); |
1189 | + expect(scope.source.showingAdvanced).toBe(true); |
1190 | + scope.toggleAdvancedOptions(); |
1191 | + expect(scope.source.showingAdvanced).toBe(false); |
1192 | + }); |
1193 | + }); |
1194 | + |
1195 | + describe("bothKeyringOptionsSet", function() { |
1196 | + |
1197 | + it("returns false if neither set", function() { |
1198 | + var directive = compileDirective(); |
1199 | + var scope = directive.isolateScope(); |
1200 | + expect(scope.bothKeyringOptionsSet()).toBe(false); |
1201 | + }); |
1202 | + |
1203 | + it("returns false if keyring_filename set", function() { |
1204 | + var directive = compileDirective(); |
1205 | + var scope = directive.isolateScope(); |
1206 | + scope.source.keyring_filename = makeName("file"); |
1207 | + expect(scope.bothKeyringOptionsSet()).toBe(false); |
1208 | + }); |
1209 | + |
1210 | + it("returns false if keyring_data set", function() { |
1211 | + var directive = compileDirective(); |
1212 | + var scope = directive.isolateScope(); |
1213 | + scope.source.keyring_data = makeName("data"); |
1214 | + expect(scope.bothKeyringOptionsSet()).toBe(false); |
1215 | + }); |
1216 | + |
1217 | + it("returns true if both set set", function() { |
1218 | + var directive = compileDirective(); |
1219 | + var scope = directive.isolateScope(); |
1220 | + scope.source.keyring_filename = makeName("file"); |
1221 | + scope.source.keyring_data = makeName("data"); |
1222 | + expect(scope.bothKeyringOptionsSet()).toBe(true); |
1223 | + }); |
1224 | + }); |
1225 | + |
1226 | + describe("showConnectButton", function() { |
1227 | + |
1228 | + it("returns isNew", function() { |
1229 | + var directive = compileDirective(); |
1230 | + var scope = directive.isolateScope(); |
1231 | + var sentinel = {}; |
1232 | + scope.source.isNew = sentinel; |
1233 | + expect(scope.showConnectButton()).toBe(sentinel); |
1234 | + }); |
1235 | + }); |
1236 | + |
1237 | + describe("sourceChanged", function() { |
1238 | + |
1239 | + it("sets isNew if no sources", function() { |
1240 | + var directive = compileDirective(); |
1241 | + var scope = directive.isolateScope(); |
1242 | + scope.bootResources = { |
1243 | + resources: [], |
1244 | + ubuntu: { |
1245 | + sources: [] |
1246 | + } |
1247 | + }; |
1248 | + spyOn(scope, "connect"); |
1249 | + scope.sourceChanged(); |
1250 | + expect(scope.source.isNew).toBe(true); |
1251 | + }); |
1252 | + |
1253 | + it("calls connect when no sources", function() { |
1254 | + var directive = compileDirective(); |
1255 | + var scope = directive.isolateScope(); |
1256 | + scope.bootResources = { |
1257 | + resources: [], |
1258 | + ubuntu: { |
1259 | + sources: [] |
1260 | + } |
1261 | + }; |
1262 | + spyOn(scope, "connect"); |
1263 | + scope.sourceChanged(); |
1264 | + expect(scope.connect).toHaveBeenCalled(); |
1265 | + }); |
1266 | + |
1267 | + it("calls connect when no sources", function() { |
1268 | + var directive = compileDirective(); |
1269 | + var scope = directive.isolateScope(); |
1270 | + scope.bootResources = { |
1271 | + resources: [], |
1272 | + ubuntu: { |
1273 | + sources: [] |
1274 | + } |
1275 | + }; |
1276 | + spyOn(scope, "connect"); |
1277 | + scope.sourceChanged(); |
1278 | + expect(scope.connect).toHaveBeenCalled(); |
1279 | + }); |
1280 | + |
1281 | + it("calls updateSource and regenerateUbuntuImages", function() { |
1282 | + var directive = compileDirective(); |
1283 | + var scope = directive.isolateScope(); |
1284 | + scope.bootResources = { |
1285 | + resources: [], |
1286 | + ubuntu: { |
1287 | + sources: [] |
1288 | + } |
1289 | + }; |
1290 | + spyOn(scope, "connect"); |
1291 | + spyOn(scope, "updateSource"); |
1292 | + spyOn(scope, "regenerateUbuntuImages"); |
1293 | + scope.sourceChanged(); |
1294 | + expect(scope.updateSource).toHaveBeenCalled(); |
1295 | + expect(scope.regenerateUbuntuImages).toHaveBeenCalled(); |
1296 | + }); |
1297 | + |
1298 | + it("changing to maas.io clears options and selections", function() { |
1299 | + var directive = compileDirective(); |
1300 | + var scope = directive.isolateScope(); |
1301 | + scope.bootResources = { |
1302 | + resources: [], |
1303 | + ubuntu: { |
1304 | + sources: [{ |
1305 | + source_type: 'custom' |
1306 | + }] |
1307 | + } |
1308 | + }; |
1309 | + scope.source.isNew = false; |
1310 | + scope.source.source_type = 'maas.io'; |
1311 | + scope.source.releases = [{}, {}]; |
1312 | + scope.source.arches = [{}, {}]; |
1313 | + scope.source.selections = { |
1314 | + changed: true, |
1315 | + releases: [{}], |
1316 | + arches: [{}] |
1317 | + }; |
1318 | + spyOn(scope, "connect"); |
1319 | + scope.sourceChanged(); |
1320 | + expect(scope.source.releases).toEqual([]); |
1321 | + expect(scope.source.arches).toEqual([]); |
1322 | + expect(scope.source.selections).toEqual({ |
1323 | + changed: false, |
1324 | + releases: [], |
1325 | + arches: [] |
1326 | + }); |
1327 | + }); |
1328 | + |
1329 | + it("changing to custom clears options and selections", function() { |
1330 | + var directive = compileDirective(); |
1331 | + var scope = directive.isolateScope(); |
1332 | + scope.bootResources = { |
1333 | + resources: [], |
1334 | + ubuntu: { |
1335 | + sources: [{ |
1336 | + source_type: 'maas.io' |
1337 | + }] |
1338 | + } |
1339 | + }; |
1340 | + scope.source.isNew = false; |
1341 | + scope.source.source_type = 'custom'; |
1342 | + scope.source.releases = [{}, {}]; |
1343 | + scope.source.arches = [{}, {}]; |
1344 | + scope.source.selections = { |
1345 | + changed: true, |
1346 | + releases: [{}], |
1347 | + arches: [{}] |
1348 | + }; |
1349 | + spyOn(scope, "connect"); |
1350 | + scope.sourceChanged(); |
1351 | + expect(scope.source.releases).toEqual([]); |
1352 | + expect(scope.source.arches).toEqual([]); |
1353 | + expect(scope.source.selections).toEqual({ |
1354 | + changed: false, |
1355 | + releases: [], |
1356 | + arches: [] |
1357 | + }); |
1358 | + }); |
1359 | + |
1360 | + it("changing to custom url clears options and selections", function() { |
1361 | + var directive = compileDirective(); |
1362 | + var scope = directive.isolateScope(); |
1363 | + scope.bootResources = { |
1364 | + resources: [], |
1365 | + ubuntu: { |
1366 | + sources: [{ |
1367 | + source_type: 'custom', |
1368 | + url: '' |
1369 | + }] |
1370 | + } |
1371 | + }; |
1372 | + scope.source.isNew = false; |
1373 | + scope.source.source_type = 'custom'; |
1374 | + scope.source.url = makeName('url'); |
1375 | + scope.source.releases = [{}, {}]; |
1376 | + scope.source.arches = [{}, {}]; |
1377 | + scope.source.selections = { |
1378 | + changed: true, |
1379 | + releases: [{}], |
1380 | + arches: [{}] |
1381 | + }; |
1382 | + spyOn(scope, "connect"); |
1383 | + scope.sourceChanged(); |
1384 | + expect(scope.source.releases).toEqual([]); |
1385 | + expect(scope.source.arches).toEqual([]); |
1386 | + expect(scope.source.selections).toEqual({ |
1387 | + changed: false, |
1388 | + releases: [], |
1389 | + arches: [] |
1390 | + }); |
1391 | + }); |
1392 | + }); |
1393 | + |
1394 | + describe("isConnectButtonDisabled", function() { |
1395 | + |
1396 | + it("never disabled when maas.io", function() { |
1397 | + var directive = compileDirective(); |
1398 | + var scope = directive.isolateScope(); |
1399 | + scope.source.source_type = 'maas.io'; |
1400 | + expect(scope.isConnectButtonDisabled()).toBe(false); |
1401 | + }); |
1402 | + |
1403 | + it("disabled when custom and both keyrings are set", function() { |
1404 | + var directive = compileDirective(); |
1405 | + var scope = directive.isolateScope(); |
1406 | + scope.source.source_type = 'custom'; |
1407 | + spyOn(scope, "bothKeyringOptionsSet").and.returnValue(true); |
1408 | + expect(scope.isConnectButtonDisabled()).toBe(true); |
1409 | + }); |
1410 | + |
1411 | + it("disabled when custom and no url", function() { |
1412 | + var directive = compileDirective(); |
1413 | + var scope = directive.isolateScope(); |
1414 | + scope.source.source_type = 'custom'; |
1415 | + scope.source.url = ""; |
1416 | + expect(scope.isConnectButtonDisabled()).toBe(true); |
1417 | + }); |
1418 | + |
1419 | + it("disabled when custom connecting", function() { |
1420 | + var directive = compileDirective(); |
1421 | + var scope = directive.isolateScope(); |
1422 | + scope.source.source_type = 'custom'; |
1423 | + scope.source.url = makeName("url"); |
1424 | + scope.source.connecting = true; |
1425 | + expect(scope.isConnectButtonDisabled()).toBe(true); |
1426 | + }); |
1427 | + |
1428 | + it("enabled when custom and not connecting", function() { |
1429 | + var directive = compileDirective(); |
1430 | + var scope = directive.isolateScope(); |
1431 | + scope.source.source_type = 'custom'; |
1432 | + scope.source.url = makeName("url"); |
1433 | + scope.source.connecting = false; |
1434 | + expect(scope.isConnectButtonDisabled()).toBe(false); |
1435 | + }); |
1436 | + }); |
1437 | + |
1438 | + describe("getSourceParams", function() { |
1439 | + |
1440 | + it("maas.io only returns source_type", function() { |
1441 | + var directive = compileDirective(); |
1442 | + var scope = directive.isolateScope(); |
1443 | + scope.source.source_type = 'maas.io'; |
1444 | + expect(scope.getSourceParams()).toEqual({ |
1445 | + source_type: 'maas.io' |
1446 | + }); |
1447 | + }); |
1448 | + |
1449 | + it("custom returns all fields", function() { |
1450 | + var directive = compileDirective(); |
1451 | + var scope = directive.isolateScope(); |
1452 | + scope.source.source_type = 'custom'; |
1453 | + scope.source.url = makeName("url"); |
1454 | + scope.source.keyring_filename = makeName("keyring_filename"); |
1455 | + scope.source.keyring_data = makeName("keyring_data"); |
1456 | + expect(scope.getSourceParams()).toEqual({ |
1457 | + source_type: 'custom', |
1458 | + url: scope.source.url, |
1459 | + keyring_filename: scope.source.keyring_filename, |
1460 | + keyring_data: scope.source.keyring_data |
1461 | + }); |
1462 | + }); |
1463 | + }); |
1464 | + |
1465 | + describe("selectDefaults", function() { |
1466 | + |
1467 | + it("selects xenial and amd64", function() { |
1468 | + var directive = compileDirective(); |
1469 | + var scope = directive.isolateScope(); |
1470 | + var xenial = { |
1471 | + name: 'xenial' |
1472 | + }; |
1473 | + var amd64 = { |
1474 | + name: 'amd64' |
1475 | + }; |
1476 | + scope.source.releases = [xenial]; |
1477 | + scope.source.arches = [amd64]; |
1478 | + scope.selectDefaults(); |
1479 | + |
1480 | + expect(scope.source.selections.releases).toEqual([xenial]); |
1481 | + expect(scope.source.selections.arches).toEqual([amd64]); |
1482 | + }); |
1483 | + }); |
1484 | + |
1485 | + describe("connect", function() { |
1486 | + |
1487 | + it("does nothing if disabled", function() { |
1488 | + var directive = compileDirective(); |
1489 | + var scope = directive.isolateScope(); |
1490 | + spyOn(scope, "isConnectButtonDisabled").and.returnValue(true); |
1491 | + spyOn(BootResourcesManager, "fetch"); |
1492 | + scope.connect(); |
1493 | + expect(BootResourcesManager.fetch).not.toHaveBeenCalled(); |
1494 | + }); |
1495 | + |
1496 | + it("toggles connecting and sets options and defaults", function() { |
1497 | + var directive = compileDirective(); |
1498 | + var scope = directive.isolateScope(); |
1499 | + spyOn(scope, "isConnectButtonDisabled").and.returnValue(false); |
1500 | + |
1501 | + // Mock the fetch call. |
1502 | + var defer = $q.defer(); |
1503 | + spyOn(BootResourcesManager, "fetch").and.returnValue(defer.promise); |
1504 | + spyOn(scope, "regenerateUbuntuImages"); |
1505 | + scope.connect(); |
1506 | + expect(scope.source.connecting).toBe(true); |
1507 | + |
1508 | + // Call connect and fake the resolve with mock data. |
1509 | + spyOn(scope, "selectDefaults"); |
1510 | + var release = makeName("release"); |
1511 | + var arch = makeName("arch"); |
1512 | + var data = angular.toJson({ |
1513 | + releases: [{ |
1514 | + name: release |
1515 | + }], |
1516 | + arches: [{ |
1517 | + name: arch |
1518 | + }] |
1519 | + }); |
1520 | + defer.resolve(data); |
1521 | + $scope.$digest(); |
1522 | + |
1523 | + expect(scope.source.connecting).toBe(false); |
1524 | + expect(scope.source.releases).toEqual([{ |
1525 | + name: release |
1526 | + }]); |
1527 | + expect(scope.source.arches).toEqual([{ |
1528 | + name: arch |
1529 | + }]); |
1530 | + expect(scope.selectDefaults).toHaveBeenCalled(); |
1531 | + expect(scope.regenerateUbuntuImages).toHaveBeenCalled(); |
1532 | + }); |
1533 | + |
1534 | + it("toggles connecting and sets error", function() { |
1535 | + var directive = compileDirective(); |
1536 | + var scope = directive.isolateScope(); |
1537 | + spyOn(scope, "isConnectButtonDisabled").and.returnValue(false); |
1538 | + |
1539 | + // Mock the fetch call. |
1540 | + var defer = $q.defer(); |
1541 | + spyOn(BootResourcesManager, "fetch").and.returnValue(defer.promise); |
1542 | + spyOn(scope, "regenerateUbuntuImages"); |
1543 | + scope.connect(); |
1544 | + expect(scope.source.connecting).toBe(true); |
1545 | + |
1546 | + // Call connect and fake the reject. |
1547 | + var error = makeName("error"); |
1548 | + defer.reject(error); |
1549 | + $scope.$digest(); |
1550 | + |
1551 | + expect(scope.source.connecting).toBe(false); |
1552 | + expect(scope.source.errorMessage).toBe(error); |
1553 | + }); |
1554 | + }); |
1555 | + |
1556 | + describe("showConnectBlock", function() { |
1557 | + |
1558 | + it("true if tooMany sources", function() { |
1559 | + var directive = compileDirective(); |
1560 | + var scope = directive.isolateScope(); |
1561 | + scope.source.tooMany = true; |
1562 | + expect(scope.showConnectBlock()).toBe(true); |
1563 | + }); |
1564 | + |
1565 | + it("true if new and not showing selections", function() { |
1566 | + var directive = compileDirective(); |
1567 | + var scope = directive.isolateScope(); |
1568 | + scope.source.isNew = true; |
1569 | + spyOn(scope, "showSelections").and.returnValue(false); |
1570 | + expect(scope.showConnectBlock()).toBe(true); |
1571 | + }); |
1572 | + |
1573 | + it("false if new and showing selections", function() { |
1574 | + var directive = compileDirective(); |
1575 | + var scope = directive.isolateScope(); |
1576 | + scope.source.isNew = true; |
1577 | + spyOn(scope, "showSelections").and.returnValue(true); |
1578 | + expect(scope.showConnectBlock()).toBe(false); |
1579 | + }); |
1580 | + |
1581 | + it("false if not new", function() { |
1582 | + var directive = compileDirective(); |
1583 | + var scope = directive.isolateScope(); |
1584 | + scope.source.isNew = false; |
1585 | + spyOn(scope, "showSelections").and.returnValue(false); |
1586 | + expect(scope.showConnectBlock()).toBe(false); |
1587 | + }); |
1588 | + }); |
1589 | + |
1590 | + describe("showSelections", function() { |
1591 | + |
1592 | + it("true releases and arches exist", function() { |
1593 | + var directive = compileDirective(); |
1594 | + var scope = directive.isolateScope(); |
1595 | + scope.source.releases = [{}]; |
1596 | + scope.source.arches = [{}]; |
1597 | + expect(scope.showSelections()).toBe(true); |
1598 | + }); |
1599 | + |
1600 | + it("false if only releases", function() { |
1601 | + var directive = compileDirective(); |
1602 | + var scope = directive.isolateScope(); |
1603 | + scope.source.releases = [{}]; |
1604 | + scope.source.arches = []; |
1605 | + expect(scope.showSelections()).toBe(false); |
1606 | + }); |
1607 | + |
1608 | + it("false if only arches", function() { |
1609 | + var directive = compileDirective(); |
1610 | + var scope = directive.isolateScope(); |
1611 | + scope.source.releases = []; |
1612 | + scope.source.arches = [{}]; |
1613 | + expect(scope.showSelections()).toBe(false); |
1614 | + }); |
1615 | + }); |
1616 | + |
1617 | + describe("getUbuntuLTSReleases", function() { |
1618 | + |
1619 | + it("filters bootResources to LTS", function() { |
1620 | + var directive = compileDirective(); |
1621 | + var scope = directive.isolateScope(); |
1622 | + var lts = { |
1623 | + title: '16.04 LTS' |
1624 | + }; |
1625 | + var nonLTS = { |
1626 | + title: '16.10' |
1627 | + }; |
1628 | + scope.bootResources = { |
1629 | + ubuntu: { |
1630 | + releases: [lts, nonLTS] |
1631 | + } |
1632 | + }; |
1633 | + expect(scope.getUbuntuLTSReleases()).toEqual([lts]); |
1634 | + }); |
1635 | + |
1636 | + it("filters new sources to LTS", function() { |
1637 | + var directive = compileDirective(); |
1638 | + var scope = directive.isolateScope(); |
1639 | + var lts = { |
1640 | + title: '16.04 LTS' |
1641 | + }; |
1642 | + var nonLTS = { |
1643 | + title: '16.10' |
1644 | + }; |
1645 | + scope.bootResources = { |
1646 | + ubuntu: { |
1647 | + releases: [] |
1648 | + } |
1649 | + }; |
1650 | + scope.source.isNew = true; |
1651 | + scope.source.releases = [lts, nonLTS]; |
1652 | + expect(scope.getUbuntuLTSReleases()).toEqual([lts]); |
1653 | + }); |
1654 | + }); |
1655 | + |
1656 | + describe("getUbuntuNonLTSReleases", function() { |
1657 | + |
1658 | + it("filters bootResources to non-LTS", function() { |
1659 | + var directive = compileDirective(); |
1660 | + var scope = directive.isolateScope(); |
1661 | + var lts = { |
1662 | + title: '16.04 LTS' |
1663 | + }; |
1664 | + var nonLTS = { |
1665 | + title: '16.10' |
1666 | + }; |
1667 | + scope.bootResources = { |
1668 | + ubuntu: { |
1669 | + releases: [lts, nonLTS] |
1670 | + } |
1671 | + }; |
1672 | + expect(scope.getUbuntuNonLTSReleases()).toEqual([nonLTS]); |
1673 | + }); |
1674 | + |
1675 | + it("filters new sources to non-LTS", function() { |
1676 | + var directive = compileDirective(); |
1677 | + var scope = directive.isolateScope(); |
1678 | + var lts = { |
1679 | + title: '16.04 LTS' |
1680 | + }; |
1681 | + var nonLTS = { |
1682 | + title: '16.10' |
1683 | + }; |
1684 | + scope.bootResources = { |
1685 | + ubuntu: { |
1686 | + releases: [] |
1687 | + } |
1688 | + }; |
1689 | + scope.source.isNew = true; |
1690 | + scope.source.releases = [lts, nonLTS]; |
1691 | + expect(scope.getUbuntuNonLTSReleases()).toEqual([nonLTS]); |
1692 | + }); |
1693 | + }); |
1694 | + |
1695 | + describe("getArchitectures", function() { |
1696 | + |
1697 | + it("returns bootResources arches", function() { |
1698 | + var directive = compileDirective(); |
1699 | + var scope = directive.isolateScope(); |
1700 | + var arch1 = {}; |
1701 | + var arch2 = {}; |
1702 | + scope.bootResources = { |
1703 | + ubuntu: { |
1704 | + arches: [arch1, arch2] |
1705 | + } |
1706 | + }; |
1707 | + expect(scope.getArchitectures()).toEqual([arch1, arch2]); |
1708 | + }); |
1709 | + |
1710 | + it("returns new sources arches", function() { |
1711 | + var directive = compileDirective(); |
1712 | + var scope = directive.isolateScope(); |
1713 | + var arch1 = {}; |
1714 | + var arch2 = {}; |
1715 | + scope.bootResources = { |
1716 | + ubuntu: { |
1717 | + arches: [] |
1718 | + } |
1719 | + }; |
1720 | + scope.source.isNew = true; |
1721 | + scope.source.arches = [arch1, arch2]; |
1722 | + expect(scope.getArchitectures()).toEqual([arch1, arch2]); |
1723 | + }); |
1724 | + }); |
1725 | + |
1726 | + describe("isSelected", function() { |
1727 | + |
1728 | + it("returns true if in release selections", function() { |
1729 | + var directive = compileDirective(); |
1730 | + var scope = directive.isolateScope(); |
1731 | + var obj = {}; |
1732 | + scope.source.selections.releases = [obj]; |
1733 | + expect(scope.isSelected('releases', obj)).toBe(true); |
1734 | + }); |
1735 | + |
1736 | + it("returns false if not in release selections", function() { |
1737 | + var directive = compileDirective(); |
1738 | + var scope = directive.isolateScope(); |
1739 | + var obj = {}; |
1740 | + scope.source.selections.releases = [obj]; |
1741 | + expect(scope.isSelected('releases', {})).toBe(false); |
1742 | + }); |
1743 | + |
1744 | + it("returns true if in arch selections", function() { |
1745 | + var directive = compileDirective(); |
1746 | + var scope = directive.isolateScope(); |
1747 | + var obj = {}; |
1748 | + scope.source.selections.arches = [obj]; |
1749 | + expect(scope.isSelected('arches', obj)).toBe(true); |
1750 | + }); |
1751 | + |
1752 | + it("returns false if not in arch selections", function() { |
1753 | + var directive = compileDirective(); |
1754 | + var scope = directive.isolateScope(); |
1755 | + var obj = {}; |
1756 | + scope.source.selections.arches = [obj]; |
1757 | + expect(scope.isSelected('arches', {})).toBe(false); |
1758 | + }); |
1759 | + }); |
1760 | + |
1761 | + describe("toggleSelection", function() { |
1762 | + |
1763 | + it("selects the obj for releases", function() { |
1764 | + var directive = compileDirective(); |
1765 | + var scope = directive.isolateScope(); |
1766 | + var obj = {}; |
1767 | + spyOn(scope, "regenerateUbuntuImages"); |
1768 | + scope.toggleSelection('releases', obj); |
1769 | + expect(scope.source.selections.changed).toBe(true); |
1770 | + expect(scope.source.selections.releases).toEqual([obj]); |
1771 | + expect(scope.regenerateUbuntuImages).toHaveBeenCalled(); |
1772 | + }); |
1773 | + |
1774 | + it("deselects the obj for releases", function() { |
1775 | + var directive = compileDirective(); |
1776 | + var scope = directive.isolateScope(); |
1777 | + var obj = {}; |
1778 | + scope.source.selections.releases = [obj]; |
1779 | + spyOn(scope, "regenerateUbuntuImages"); |
1780 | + scope.toggleSelection('releases', obj); |
1781 | + expect(scope.source.selections.changed).toBe(true); |
1782 | + expect(scope.source.selections.releases).toEqual([]); |
1783 | + expect(scope.regenerateUbuntuImages).toHaveBeenCalled(); |
1784 | + }); |
1785 | + |
1786 | + it("selects the obj for arches", function() { |
1787 | + var directive = compileDirective(); |
1788 | + var scope = directive.isolateScope(); |
1789 | + var obj = {}; |
1790 | + spyOn(scope, "regenerateUbuntuImages"); |
1791 | + scope.toggleSelection('arches', obj); |
1792 | + expect(scope.source.selections.changed).toBe(true); |
1793 | + expect(scope.source.selections.arches).toEqual([obj]); |
1794 | + expect(scope.regenerateUbuntuImages).toHaveBeenCalled(); |
1795 | + }); |
1796 | + |
1797 | + it("deselects the obj for arches", function() { |
1798 | + var directive = compileDirective(); |
1799 | + var scope = directive.isolateScope(); |
1800 | + var obj = {}; |
1801 | + scope.source.selections.arches = [obj]; |
1802 | + spyOn(scope, "regenerateUbuntuImages"); |
1803 | + scope.toggleSelection('arches', obj); |
1804 | + expect(scope.source.selections.changed).toBe(true); |
1805 | + expect(scope.source.selections.arches).toEqual([]); |
1806 | + expect(scope.regenerateUbuntuImages).toHaveBeenCalled(); |
1807 | + }); |
1808 | + }); |
1809 | + |
1810 | + describe("showImagesTable", function() { |
1811 | + |
1812 | + it("returns true if ubuntuImages exist", function() { |
1813 | + var directive = compileDirective(); |
1814 | + var scope = directive.isolateScope(); |
1815 | + scope.ubuntuImages = [{}]; |
1816 | + expect(scope.showImagesTable()).toBe(true); |
1817 | + }); |
1818 | + |
1819 | + it("returns true source has arches and releases", function() { |
1820 | + var directive = compileDirective(); |
1821 | + var scope = directive.isolateScope(); |
1822 | + scope.ubuntuImages = []; |
1823 | + scope.source.arches = [{}]; |
1824 | + scope.source.releases = [{}]; |
1825 | + expect(scope.showImagesTable()).toBe(true); |
1826 | + }); |
1827 | + |
1828 | + it("returns false no images and no source info", function() { |
1829 | + var directive = compileDirective(); |
1830 | + var scope = directive.isolateScope(); |
1831 | + scope.ubuntuImages = []; |
1832 | + scope.source.arches = []; |
1833 | + scope.source.releases = []; |
1834 | + expect(scope.showImagesTable()).toBe(false); |
1835 | + }); |
1836 | + }); |
1837 | + |
1838 | + describe("regenerateUbuntuImages", function() { |
1839 | + |
1840 | + it("builds images based on selections", function() { |
1841 | + var directive = compileDirective(); |
1842 | + var scope = directive.isolateScope(); |
1843 | + scope.bootResources.resources = []; |
1844 | + var release = { |
1845 | + name: makeName("release"), |
1846 | + title: makeName("releaseTitle") |
1847 | + }; |
1848 | + var arch = { |
1849 | + name: makeName("arch"), |
1850 | + title: makeName("archTitle") |
1851 | + }; |
1852 | + scope.source.selections.releases = [release]; |
1853 | + scope.source.selections.arches = [arch]; |
1854 | + scope.regenerateUbuntuImages(); |
1855 | + expect(scope.ubuntuImages).toEqual([{ |
1856 | + icon: 'icon--status-queued', |
1857 | + title: release.title, |
1858 | + arch: arch.title, |
1859 | + size: '-', |
1860 | + status: 'Queued for download', |
1861 | + beingDeleted: false |
1862 | + }]); |
1863 | + }); |
1864 | + |
1865 | + it("builds images based on selection and resource", function() { |
1866 | + var directive = compileDirective(); |
1867 | + var scope = directive.isolateScope(); |
1868 | + var release = { |
1869 | + name: makeName("release"), |
1870 | + title: makeName("releaseTitle") |
1871 | + }; |
1872 | + var arch = { |
1873 | + name: makeName("arch"), |
1874 | + title: makeName("archTitle") |
1875 | + }; |
1876 | + var icon = makeName("icon"); |
1877 | + var size = makeName("size"); |
1878 | + var status = makeName("status"); |
1879 | + scope.bootResources.resources = [{ |
1880 | + rtype: 0, |
1881 | + name: 'ubuntu/' + release.name, |
1882 | + arch: arch.name, |
1883 | + icon: icon, |
1884 | + size: size, |
1885 | + status: status, |
1886 | + downloading: true |
1887 | + }]; |
1888 | + scope.source.selections.releases = [release]; |
1889 | + scope.source.selections.arches = [arch]; |
1890 | + scope.regenerateUbuntuImages(); |
1891 | + expect(scope.ubuntuImages).toEqual([{ |
1892 | + icon: 'icon--status-' + icon + ' u-animation--pulse', |
1893 | + title: release.title, |
1894 | + arch: arch.title, |
1895 | + size: size, |
1896 | + status: status, |
1897 | + beingDeleted: false |
1898 | + }]); |
1899 | + }); |
1900 | + |
1901 | + it("marks resource as being deleted", function() { |
1902 | + var directive = compileDirective(); |
1903 | + var scope = directive.isolateScope(); |
1904 | + var release = { |
1905 | + name: makeName("release"), |
1906 | + title: makeName("releaseTitle") |
1907 | + }; |
1908 | + var arch = { |
1909 | + name: makeName("arch"), |
1910 | + title: makeName("archTitle") |
1911 | + }; |
1912 | + var icon = makeName("icon"); |
1913 | + var size = makeName("size"); |
1914 | + var status = makeName("status"); |
1915 | + scope.bootResources.resources = [{ |
1916 | + rtype: 0, |
1917 | + name: 'ubuntu/' + release.name, |
1918 | + title: release.title, |
1919 | + arch: arch.name, |
1920 | + icon: icon, |
1921 | + size: size, |
1922 | + status: status, |
1923 | + downloading: true |
1924 | + }]; |
1925 | + scope.source.selections.releases = []; |
1926 | + scope.source.selections.arches = []; |
1927 | + scope.regenerateUbuntuImages(); |
1928 | + expect(scope.ubuntuImages).toEqual([{ |
1929 | + icon: 'icon--status-failed', |
1930 | + title: release.title, |
1931 | + arch: arch.name, |
1932 | + size: size, |
1933 | + status: 'Queued for deletion', |
1934 | + beingDeleted: true |
1935 | + }]); |
1936 | + }); |
1937 | + }); |
1938 | + |
1939 | + describe("regenerateOtherImages", function() { |
1940 | + |
1941 | + it("builds images based on selections", function() { |
1942 | + var directive = compileDirective(); |
1943 | + var scope = directive.isolateScope(); |
1944 | + scope.bootResources.resources = []; |
1945 | + var os = makeName("os"); |
1946 | + var release = makeName("release"); |
1947 | + var arch = makeName("arch"); |
1948 | + var name = os + '/' + arch + '/generic/' + release; |
1949 | + var image = { |
1950 | + name: name, |
1951 | + title: makeName("title"), |
1952 | + checked: true |
1953 | + }; |
1954 | + scope.other.images = [image]; |
1955 | + scope.regenerateOtherImages(); |
1956 | + expect(scope.otherImages).toEqual([{ |
1957 | + icon: 'icon--status-queued', |
1958 | + title: image.title, |
1959 | + arch: arch, |
1960 | + size: '-', |
1961 | + status: 'Queued for download', |
1962 | + beingDeleted: false |
1963 | + }]); |
1964 | + }); |
1965 | + |
1966 | + it("builds images based on selection and resource", function() { |
1967 | + var directive = compileDirective(); |
1968 | + var scope = directive.isolateScope(); |
1969 | + var os = makeName("os"); |
1970 | + var release = makeName("release"); |
1971 | + var arch = makeName("arch"); |
1972 | + var name = os + '/' + arch + '/generic/' + release; |
1973 | + var image = { |
1974 | + name: name, |
1975 | + title: makeName("title"), |
1976 | + checked: true |
1977 | + }; |
1978 | + var icon = makeName("icon"); |
1979 | + var size = makeName("size"); |
1980 | + var status = makeName("status"); |
1981 | + scope.bootResources.resources = [{ |
1982 | + rtype: 0, |
1983 | + name: os + '/' + release, |
1984 | + arch: arch, |
1985 | + icon: icon, |
1986 | + size: size, |
1987 | + status: status, |
1988 | + downloading: true |
1989 | + }]; |
1990 | + scope.other.images = [image]; |
1991 | + scope.regenerateOtherImages(); |
1992 | + expect(scope.otherImages).toEqual([{ |
1993 | + icon: 'icon--status-' + icon + ' u-animation--pulse', |
1994 | + title: image.title, |
1995 | + arch: arch, |
1996 | + size: size, |
1997 | + status: status, |
1998 | + beingDeleted: false |
1999 | + }]); |
2000 | + }); |
2001 | + |
2002 | + it("marks resource as being deleted", function() { |
2003 | + var directive = compileDirective(); |
2004 | + var scope = directive.isolateScope(); |
2005 | + var os = makeName("os"); |
2006 | + var release = makeName("release"); |
2007 | + var arch = makeName("arch"); |
2008 | + var name = os + '/' + arch + '/generic/' + release; |
2009 | + var image = { |
2010 | + name: name, |
2011 | + title: makeName("title"), |
2012 | + checked: false |
2013 | + }; |
2014 | + var icon = makeName("icon"); |
2015 | + var size = makeName("size"); |
2016 | + var status = makeName("status"); |
2017 | + scope.bootResources.resources = [{ |
2018 | + rtype: 0, |
2019 | + name: os + '/' + release, |
2020 | + title: image.title, |
2021 | + arch: arch, |
2022 | + icon: icon, |
2023 | + size: size, |
2024 | + status: status, |
2025 | + downloading: true |
2026 | + }]; |
2027 | + scope.other.images = [image]; |
2028 | + scope.regenerateOtherImages(); |
2029 | + expect(scope.otherImages).toEqual([{ |
2030 | + icon: 'icon--status-failed', |
2031 | + title: image.title, |
2032 | + arch: arch, |
2033 | + size: size, |
2034 | + status: 'Queued for deletion', |
2035 | + beingDeleted: true |
2036 | + }]); |
2037 | + }); |
2038 | + }); |
2039 | + |
2040 | + describe("regenerateGeneratedImages", function() { |
2041 | + |
2042 | + it("builds images based on resource", function() { |
2043 | + var directive = compileDirective(); |
2044 | + var scope = directive.isolateScope(); |
2045 | + var icon = makeName("icon"); |
2046 | + var title = makeName("title"); |
2047 | + var arch = makeName("arch"); |
2048 | + var size = makeName("size"); |
2049 | + var status = makeName("status"); |
2050 | + scope.bootResources.resources = [{ |
2051 | + rtype: 1, |
2052 | + icon: icon, |
2053 | + title: title, |
2054 | + arch: arch, |
2055 | + size: size, |
2056 | + status: status, |
2057 | + downloading: true |
2058 | + }]; |
2059 | + scope.regenerateGeneratedImages(); |
2060 | + expect(scope.generatedImages).toEqual([{ |
2061 | + icon: 'icon--status-' + icon + ' u-animation--pulse', |
2062 | + title: title, |
2063 | + arch: arch, |
2064 | + size: size, |
2065 | + status: status |
2066 | + }]); |
2067 | + }); |
2068 | + }); |
2069 | + |
2070 | + describe("regenerateCustomImages", function() { |
2071 | + |
2072 | + it("builds images based on resource", function() { |
2073 | + var directive = compileDirective(); |
2074 | + var scope = directive.isolateScope(); |
2075 | + var icon = makeName("icon"); |
2076 | + var title = makeName("title"); |
2077 | + var arch = makeName("arch"); |
2078 | + var size = makeName("size"); |
2079 | + var status = makeName("status"); |
2080 | + scope.bootResources.resources = [{ |
2081 | + rtype: 2, |
2082 | + icon: icon, |
2083 | + title: title, |
2084 | + arch: arch, |
2085 | + size: size, |
2086 | + status: status, |
2087 | + downloading: true |
2088 | + }]; |
2089 | + scope.regenerateCustomImages(); |
2090 | + expect(scope.customImages).toEqual([{ |
2091 | + icon: 'icon--status-' + icon + ' u-animation--pulse', |
2092 | + title: title, |
2093 | + arch: arch, |
2094 | + size: size, |
2095 | + status: status |
2096 | + }]); |
2097 | + }); |
2098 | + }); |
2099 | + |
2100 | + describe("ltsIsSelected", function() { |
2101 | + |
2102 | + it("returns true if LTS is selected", function() { |
2103 | + var directive = compileDirective(); |
2104 | + var scope = directive.isolateScope(); |
2105 | + var image = { |
2106 | + title: '16.04 LTS', |
2107 | + beingDeleted: false |
2108 | + }; |
2109 | + scope.ubuntuImages = [image]; |
2110 | + expect(scope.ltsIsSelected()).toBe(true); |
2111 | + }); |
2112 | + |
2113 | + it("returns true if 14 series LTS is selected", function() { |
2114 | + var directive = compileDirective(); |
2115 | + var scope = directive.isolateScope(); |
2116 | + var image = { |
2117 | + title: '14.04 LTS', |
2118 | + beingDeleted: false |
2119 | + }; |
2120 | + scope.ubuntuImages = [image]; |
2121 | + expect(scope.ltsIsSelected()).toBe(true); |
2122 | + }); |
2123 | + |
2124 | + it("returns false if LTS is being deleted", function() { |
2125 | + var directive = compileDirective(); |
2126 | + var scope = directive.isolateScope(); |
2127 | + var image = { |
2128 | + title: '16.04 LTS', |
2129 | + beingDeleted: true |
2130 | + }; |
2131 | + scope.ubuntuImages = [image]; |
2132 | + expect(scope.ltsIsSelected()).toBe(false); |
2133 | + }); |
2134 | + |
2135 | + it("returns false if less than 14 series", function() { |
2136 | + var directive = compileDirective(); |
2137 | + var scope = directive.isolateScope(); |
2138 | + var image = { |
2139 | + title: '12.04 LTS', |
2140 | + beingDeleted: false |
2141 | + }; |
2142 | + scope.ubuntuImages = [image]; |
2143 | + expect(scope.ltsIsSelected()).toBe(false); |
2144 | + }); |
2145 | + }); |
2146 | + |
2147 | + describe("showStopImportButton", function() { |
2148 | + |
2149 | + it("returns region_import_running", function() { |
2150 | + var directive = compileDirective(); |
2151 | + var scope = directive.isolateScope(); |
2152 | + var sentinel = {}; |
2153 | + scope.bootResources.region_import_running = sentinel; |
2154 | + expect(scope.showStopImportButton()).toBe(sentinel); |
2155 | + }); |
2156 | + }); |
2157 | + |
2158 | + describe("showSaveSelection", function() { |
2159 | + |
2160 | + it("returns showImagesTable", function() { |
2161 | + var directive = compileDirective(); |
2162 | + var scope = directive.isolateScope(); |
2163 | + var sentinel = {}; |
2164 | + spyOn(scope, "showImagesTable").and.returnValue(sentinel); |
2165 | + expect(scope.showSaveSelection()).toBe(sentinel); |
2166 | + }); |
2167 | + }); |
2168 | + |
2169 | + describe("canSaveSelection", function() { |
2170 | + |
2171 | + it("returns false if saving", function() { |
2172 | + var directive = compileDirective(); |
2173 | + var scope = directive.isolateScope(); |
2174 | + scope.saving = true; |
2175 | + expect(scope.canSaveSelection()).toBe(false); |
2176 | + }); |
2177 | + |
2178 | + it("returns false if not lts selected", function() { |
2179 | + var directive = compileDirective(); |
2180 | + var scope = directive.isolateScope(); |
2181 | + scope.saving = false; |
2182 | + spyOn(scope, "ltsIsSelected").and.returnValue(false); |
2183 | + expect(scope.canSaveSelection()).toBe(false); |
2184 | + }); |
2185 | + |
2186 | + it("returns true if lts selected and not saving", function() { |
2187 | + var directive = compileDirective(); |
2188 | + var scope = directive.isolateScope(); |
2189 | + scope.saving = false; |
2190 | + spyOn(scope, "ltsIsSelected").and.returnValue(true); |
2191 | + expect(scope.canSaveSelection()).toBe(true); |
2192 | + }); |
2193 | + }); |
2194 | + |
2195 | + describe("getSaveSelectionText", function() { |
2196 | + |
2197 | + it("returns 'Saving...' when saving", function() { |
2198 | + var directive = compileDirective(); |
2199 | + var scope = directive.isolateScope(); |
2200 | + scope.saving = true; |
2201 | + expect(scope.getSaveSelectionText()).toBe('Saving...'); |
2202 | + }); |
2203 | + |
2204 | + it("returns 'Save selection' when not saving", function() { |
2205 | + var directive = compileDirective(); |
2206 | + var scope = directive.isolateScope(); |
2207 | + scope.saving = false; |
2208 | + expect(scope.getSaveSelectionText()).toBe('Save selection'); |
2209 | + }); |
2210 | + }); |
2211 | + |
2212 | + describe("saveSelection", function() { |
2213 | + |
2214 | + it("passes selected releases and arches", function() { |
2215 | + var directive = compileDirective(); |
2216 | + var scope = directive.isolateScope(); |
2217 | + var defer = $q.defer(); |
2218 | + spyOn(BootResourcesManager, "saveUbuntu").and.returnValue( |
2219 | + defer.promise); |
2220 | + spyOn(scope, "canSaveSelection").and.returnValue(true); |
2221 | + |
2222 | + var release = makeName("release"); |
2223 | + scope.source.selections.releases = [{ |
2224 | + name: release |
2225 | + }]; |
2226 | + var arch = makeName("arch"); |
2227 | + scope.source.selections.arches = [{ |
2228 | + name: arch |
2229 | + }]; |
2230 | + scope.saveSelection(); |
2231 | + |
2232 | + expect(scope.saving).toBe(true); |
2233 | + expect(BootResourcesManager.saveUbuntu).toHaveBeenCalledWith({ |
2234 | + source_type: 'maas.io', |
2235 | + releases: [release], |
2236 | + arches: [arch] |
2237 | + }); |
2238 | + }); |
2239 | + |
2240 | + it("clears saving and calls updateSource", function() { |
2241 | + var directive = compileDirective(); |
2242 | + var scope = directive.isolateScope(); |
2243 | + var defer = $q.defer(); |
2244 | + spyOn(BootResourcesManager, "saveUbuntu").and.returnValue( |
2245 | + defer.promise); |
2246 | + spyOn(scope, "canSaveSelection").and.returnValue(true); |
2247 | + |
2248 | + var release = makeName("release"); |
2249 | + scope.source.selections.releases = [{ |
2250 | + name: release |
2251 | + }]; |
2252 | + var arch = makeName("arch"); |
2253 | + scope.source.selections.arches = [{ |
2254 | + name: arch |
2255 | + }]; |
2256 | + scope.source.isNew = true; |
2257 | + scope.source.selections.changed = true; |
2258 | + spyOn(scope, "updateSource"); |
2259 | + scope.saveSelection(); |
2260 | + |
2261 | + expect(scope.saving).toBe(true); |
2262 | + defer.resolve(); |
2263 | + $scope.$digest(); |
2264 | + expect(scope.saving).toBe(false); |
2265 | + expect(scope.source.isNew).toBe(false); |
2266 | + expect(scope.source.selections.changed).toBe(false); |
2267 | + expect(scope.updateSource).toHaveBeenCalled(); |
2268 | + }); |
2269 | + }); |
2270 | + |
2271 | + describe("updateSource", function() { |
2272 | + |
2273 | + it("sets to new and custom when no source", function() { |
2274 | + var directive = compileDirective(); |
2275 | + var scope = directive.isolateScope(); |
2276 | + scope.bootResources.ubuntu = { |
2277 | + sources: [] |
2278 | + }; |
2279 | + scope.updateSource(); |
2280 | + expect(scope.source.isNew).toBe(true); |
2281 | + expect(scope.source.source_type).toBe('custom'); |
2282 | + expect(scope.source.errorMessage).toBe( |
2283 | + 'Currently no source exists.'); |
2284 | + |
2285 | + }); |
2286 | + |
2287 | + it("sets releases and arches and selections when source", function() { |
2288 | + var directive = compileDirective(); |
2289 | + var scope = directive.isolateScope(); |
2290 | + var source = { |
2291 | + source_type: 'custom', |
2292 | + url: makeName('url'), |
2293 | + keyring_filename: makeName('keyring_filename'), |
2294 | + keyring_data: makeName('keyring_data') |
2295 | + }; |
2296 | + var release = { |
2297 | + name: makeName("release"), |
2298 | + checked: false |
2299 | + }; |
2300 | + var releaseChecked = { |
2301 | + name: makeName("release"), |
2302 | + checked: true |
2303 | + }; |
2304 | + var arch = { |
2305 | + name: makeName("arch"), |
2306 | + checked: false |
2307 | + }; |
2308 | + var archChecked = { |
2309 | + name: makeName("arch"), |
2310 | + checked: true |
2311 | + }; |
2312 | + scope.bootResources.ubuntu = { |
2313 | + sources: [source], |
2314 | + releases: [release, releaseChecked], |
2315 | + arches: [arch, archChecked] |
2316 | + }; |
2317 | + spyOn(scope, "regenerateUbuntuImages"); |
2318 | + scope.updateSource(); |
2319 | + expect(scope.source.isNew).toBe(false); |
2320 | + expect(scope.source.source_type).toBe('custom'); |
2321 | + expect(scope.source.url).toBe(source.url); |
2322 | + expect(scope.source.keyring_filename).toBe(source.keyring_filename); |
2323 | + expect(scope.source.keyring_data).toBe(source.keyring_data); |
2324 | + expect(scope.source.releases).toBe( |
2325 | + scope.bootResources.ubuntu.releases); |
2326 | + expect(scope.source.arches).toBe( |
2327 | + scope.bootResources.ubuntu.arches); |
2328 | + expect(scope.source.selections.releases).toEqual([releaseChecked]); |
2329 | + expect(scope.source.selections.arches).toEqual([archChecked]); |
2330 | + expect(scope.regenerateUbuntuImages).toHaveBeenCalled(); |
2331 | + }); |
2332 | + |
2333 | + it("sets tooMany when multiple sources", function() { |
2334 | + var directive = compileDirective(); |
2335 | + var scope = directive.isolateScope(); |
2336 | + var release = { |
2337 | + name: makeName("release"), |
2338 | + checked: false |
2339 | + }; |
2340 | + var releaseChecked = { |
2341 | + name: makeName("release"), |
2342 | + checked: true |
2343 | + }; |
2344 | + var arch = { |
2345 | + name: makeName("arch"), |
2346 | + checked: false |
2347 | + }; |
2348 | + var archChecked = { |
2349 | + name: makeName("arch"), |
2350 | + checked: true |
2351 | + }; |
2352 | + scope.bootResources.ubuntu = { |
2353 | + sources: [{}, {}], |
2354 | + releases: [release, releaseChecked], |
2355 | + arches: [arch, archChecked] |
2356 | + }; |
2357 | + spyOn(scope, "regenerateUbuntuImages"); |
2358 | + scope.updateSource(); |
2359 | + expect(scope.source.isNew).toBe(false); |
2360 | + expect(scope.source.tooMany).toBe(true); |
2361 | + expect(scope.source.releases).toBe( |
2362 | + scope.bootResources.ubuntu.releases); |
2363 | + expect(scope.source.arches).toBe( |
2364 | + scope.bootResources.ubuntu.arches); |
2365 | + expect(scope.source.selections.releases).toEqual([releaseChecked]); |
2366 | + expect(scope.source.selections.arches).toEqual([archChecked]); |
2367 | + expect(scope.regenerateUbuntuImages).toHaveBeenCalled(); |
2368 | + }); |
2369 | + }); |
2370 | + |
2371 | + describe("toggleOtherSelection", function() { |
2372 | + |
2373 | + it("toggles checked and sets changed", function() { |
2374 | + var directive = compileDirective(); |
2375 | + var scope = directive.isolateScope(); |
2376 | + var image = { |
2377 | + checked: true |
2378 | + }; |
2379 | + spyOn(scope, "regenerateOtherImages"); |
2380 | + scope.toggleOtherSelection(image); |
2381 | + expect(scope.other.changed).toBe(true); |
2382 | + expect(image.checked).toBe(false); |
2383 | + expect(scope.regenerateOtherImages).toHaveBeenCalled(); |
2384 | + }); |
2385 | + }); |
2386 | + |
2387 | + describe("saveOtherSelection", function() { |
2388 | + |
2389 | + it("passes correct params and toggles saving", function() { |
2390 | + var directive = compileDirective(); |
2391 | + var scope = directive.isolateScope(); |
2392 | + var image = { |
2393 | + name: makeName("name"), |
2394 | + checked: true |
2395 | + }; |
2396 | + scope.other.images = [image]; |
2397 | + var defer = $q.defer(); |
2398 | + spyOn(BootResourcesManager, "saveOther").and.returnValue( |
2399 | + defer.promise); |
2400 | + scope.saveOtherSelection(); |
2401 | + |
2402 | + expect(scope.saving).toBe(true); |
2403 | + expect(BootResourcesManager.saveOther).toHaveBeenCalledWith({ |
2404 | + images: [image.name] |
2405 | + }); |
2406 | + defer.resolve(); |
2407 | + $scope.$digest(); |
2408 | + expect(scope.saving).toBe(false); |
2409 | + }); |
2410 | + }); |
2411 | +}); |
2412 | |
2413 | === added file 'src/maasserver/static/js/angular/factories/bootresources.js' |
2414 | --- src/maasserver/static/js/angular/factories/bootresources.js 1970-01-01 00:00:00 +0000 |
2415 | +++ src/maasserver/static/js/angular/factories/bootresources.js 2016-09-14 21:01:21 +0000 |
2416 | @@ -0,0 +1,179 @@ |
2417 | +/* Copyright 2016 Canonical Ltd. This software is licensed under the |
2418 | + * GNU Affero General Public License version 3 (see the file LICENSE). |
2419 | + * |
2420 | + * MAAS BootResource Manager |
2421 | + * |
2422 | + * Manager for the boot resources. This manager is unique from all the other |
2423 | + * managers because it uses polling instead of having the region push the |
2424 | + * information. |
2425 | + * |
2426 | + * Why is it polling? |
2427 | + * The boot resource information is split between the region controller and |
2428 | + * all rack controllers. The region controller does not cache any information |
2429 | + * about a rack controllers images it contacts the rack as its source of truth. |
2430 | + * This means that the client needs to use polling so the region controller |
2431 | + * can ask each rack controller what is the status of your images. |
2432 | + */ |
2433 | + |
2434 | +angular.module('MAAS').factory( |
2435 | + 'BootResourcesManager', |
2436 | + ['$q', '$timeout', 'RegionConnection', 'ErrorService', |
2437 | + function($q, $timeout, RegionConnection, ErrorService) { |
2438 | + |
2439 | + // Constructor |
2440 | + function BootResourcesManager() { |
2441 | + // Set true once been loaded the first time. |
2442 | + this._loaded = false; |
2443 | + |
2444 | + // Holds the data recieved from polling. |
2445 | + this._data = {}; |
2446 | + |
2447 | + // Set to true when polling has been enabled. |
2448 | + this._polling = false; |
2449 | + |
2450 | + // The next promise for the polling interval. |
2451 | + this._nextPromise = null; |
2452 | + |
2453 | + // Amount of time in milliseconds the manager should wait to poll |
2454 | + // for new data. |
2455 | + this._pollTimeout = 10000; |
2456 | + |
2457 | + // Amount of time in milliseconds the manager should wait to poll |
2458 | + // for new data when an error occurs. |
2459 | + this._pollErrorTimeout = 500; |
2460 | + |
2461 | + // Amount of time in milliseconds the manager should wait to poll |
2462 | + // for new data when the retrieved data is empty. |
2463 | + this._pollEmptyTimeout = 3000; |
2464 | + } |
2465 | + |
2466 | + // Return the data. |
2467 | + BootResourcesManager.prototype.getData = function() { |
2468 | + return this._data; |
2469 | + }; |
2470 | + |
2471 | + // Return true when data has been loaded. |
2472 | + BootResourcesManager.prototype.isLoaded = function() { |
2473 | + return this._loaded; |
2474 | + }; |
2475 | + |
2476 | + // Returns true when currently polling. |
2477 | + BootResourcesManager.prototype.isPolling = function() { |
2478 | + return this._polling; |
2479 | + }; |
2480 | + |
2481 | + // Starts the polling for data. |
2482 | + BootResourcesManager.prototype.startPolling = function() { |
2483 | + if(!this._polling) { |
2484 | + this._polling = true; |
2485 | + return this._poll(); |
2486 | + } else { |
2487 | + return this._nextPromise; |
2488 | + } |
2489 | + }; |
2490 | + |
2491 | + // Stops the polling for data. |
2492 | + BootResourcesManager.prototype.stopPolling = function() { |
2493 | + this._polling = false; |
2494 | + if(angular.isObject(this._nextPromise)) { |
2495 | + $timeout.cancel(this._nextPromise); |
2496 | + this._nextPromise = null; |
2497 | + } |
2498 | + }; |
2499 | + |
2500 | + // Load the data from the region. |
2501 | + BootResourcesManager.prototype._loadData = function(raiseError) { |
2502 | + raiseError = raiseError || false; |
2503 | + var self = this; |
2504 | + return RegionConnection.callMethod("bootresource.poll").then( |
2505 | + function(newData) { |
2506 | + angular.copy(angular.fromJson(newData), self._data); |
2507 | + self._loaded = true; |
2508 | + return self._data; |
2509 | + }, function(error) { |
2510 | + if(raiseError) { |
2511 | + ErrorService.raiseError(error); |
2512 | + } |
2513 | + }); |
2514 | + }; |
2515 | + |
2516 | + // Registers the next polling attempt. |
2517 | + BootResourcesManager.prototype._pollAgain = function(timeout) { |
2518 | + var self = this; |
2519 | + this._nextPromise = $timeout(function() { |
2520 | + self._poll(); |
2521 | + }, timeout); |
2522 | + return this._nextPromise; |
2523 | + }; |
2524 | + |
2525 | + // Polls for the data from the region. |
2526 | + BootResourcesManager.prototype._poll = function() { |
2527 | + var self = this; |
2528 | + |
2529 | + // Can only poll if connected. |
2530 | + if(!RegionConnection.isConnected()) { |
2531 | + return this._pollAgain(this._pollErrorTimeout); |
2532 | + } |
2533 | + |
2534 | + return this._loadData(false).then(function(newData) { |
2535 | + var pollTimeout = self._pollTimeout; |
2536 | + if(!angular.isObject(newData) || |
2537 | + newData.connection_error || |
2538 | + !angular.isArray(newData.resources) || |
2539 | + newData.resources.length === 0) { |
2540 | + pollTimeout = self._pollEmptyTimeout; |
2541 | + } |
2542 | + self._pollAgain(pollTimeout); |
2543 | + return newData; |
2544 | + }, function(error) { |
2545 | + // Don't raise the error, just log it and try again. |
2546 | + console.log(error); |
2547 | + self._pollAgain(self._pollErrorTimeout); |
2548 | + }); |
2549 | + }; |
2550 | + |
2551 | + // Loads the resources. This implemented so the ManagerHelperService |
2552 | + // can work on this manager just like all the rest. |
2553 | + BootResourcesManager.prototype.loadItems = function() { |
2554 | + var defer = $q.defer(); |
2555 | + this._loadData(true).then(function() { |
2556 | + defer.resolve(); |
2557 | + }); |
2558 | + return defer.promise; |
2559 | + }; |
2560 | + |
2561 | + // Does nothing. This implemented so the ManagerHelperService |
2562 | + // can work on this manager just like all the rest. |
2563 | + BootResourcesManager.prototype.enableAutoReload = function() { }; |
2564 | + |
2565 | + // Save the ubuntu options and start the import process. |
2566 | + BootResourcesManager.prototype.saveUbuntu = function(params) { |
2567 | + var self = this; |
2568 | + return RegionConnection.callMethod( |
2569 | + "bootresource.save_ubuntu", params).then( |
2570 | + function(newData) { |
2571 | + angular.copy(angular.fromJson(newData), self._data); |
2572 | + self._loaded = true; |
2573 | + return self._data; |
2574 | + }); |
2575 | + }; |
2576 | + |
2577 | + // Save the other images and start the import process. |
2578 | + BootResourcesManager.prototype.saveOther = function(params) { |
2579 | + var self = this; |
2580 | + return RegionConnection.callMethod( |
2581 | + "bootresource.save_other", params).then( |
2582 | + function(newData) { |
2583 | + angular.copy(angular.fromJson(newData), self._data); |
2584 | + self._loaded = true; |
2585 | + return self._data; |
2586 | + }); |
2587 | + }; |
2588 | + |
2589 | + // Fetch the releases and arches from the provided source. |
2590 | + BootResourcesManager.prototype.fetch = function(source) { |
2591 | + return RegionConnection.callMethod("bootresource.fetch", source); |
2592 | + }; |
2593 | + |
2594 | + return new BootResourcesManager(); |
2595 | + }]); |
2596 | |
2597 | === added file 'src/maasserver/static/js/angular/factories/tests/test_bootresources.js' |
2598 | --- src/maasserver/static/js/angular/factories/tests/test_bootresources.js 1970-01-01 00:00:00 +0000 |
2599 | +++ src/maasserver/static/js/angular/factories/tests/test_bootresources.js 2016-09-14 21:01:21 +0000 |
2600 | @@ -0,0 +1,195 @@ |
2601 | +/* Copyright 2016 Canonical Ltd. This software is licensed under the |
2602 | + * GNU Affero General Public License version 3 (see the file LICENSE). |
2603 | + * |
2604 | + * Unit tests for BootResourcesManager. |
2605 | + */ |
2606 | + |
2607 | + |
2608 | +describe("BootResourcesManager", function() { |
2609 | + |
2610 | + // Load the MAAS module. |
2611 | + beforeEach(module("MAAS")); |
2612 | + |
2613 | + // Grab the needed angular pieces. |
2614 | + var $rootScope, $timeout, $q; |
2615 | + beforeEach(inject(function($injector) { |
2616 | + $rootScope = $injector.get("$rootScope"); |
2617 | + $timeout = $injector.get("$timeout"); |
2618 | + $q = $injector.get("$q"); |
2619 | + })); |
2620 | + |
2621 | + // Load the needed services. |
2622 | + var BootResourcesManager, RegionConnection, ErrorService, webSocket; |
2623 | + beforeEach(inject(function($injector) { |
2624 | + BootResourcesManager = $injector.get("BootResourcesManager"); |
2625 | + RegionConnection = $injector.get("RegionConnection"); |
2626 | + ErrorService = $injector.get("ErrorService"); |
2627 | + |
2628 | + // Mock buildSocket so an actual connection is not made. |
2629 | + webSocket = new MockWebSocket(); |
2630 | + spyOn(RegionConnection, "buildSocket").and.returnValue(webSocket); |
2631 | + })); |
2632 | + |
2633 | + it("sets initial values", function() { |
2634 | + expect(BootResourcesManager._loaded).toBe(false); |
2635 | + expect(BootResourcesManager._data).toEqual({}); |
2636 | + expect(BootResourcesManager._polling).toBe(false); |
2637 | + expect(BootResourcesManager._nextPromise).toBeNull(); |
2638 | + expect(BootResourcesManager._pollTimeout).toBe(10000); |
2639 | + expect(BootResourcesManager._pollErrorTimeout).toBe(500); |
2640 | + expect(BootResourcesManager._pollEmptyTimeout).toBe(3000); |
2641 | + }); |
2642 | + |
2643 | + describe("getData", function() { |
2644 | + |
2645 | + it("returns _data", function() { |
2646 | + expect(BootResourcesManager.getData()).toBe( |
2647 | + BootResourcesManager._data); |
2648 | + }); |
2649 | + }); |
2650 | + |
2651 | + describe("isLoaded", function() { |
2652 | + |
2653 | + it("returns _loaded", function() { |
2654 | + var sentinel = {}; |
2655 | + BootResourcesManager._loaded = sentinel; |
2656 | + expect(BootResourcesManager.isLoaded()).toBe(sentinel); |
2657 | + }); |
2658 | + }); |
2659 | + |
2660 | + describe("isPolling", function() { |
2661 | + |
2662 | + it("returns _polling", function() { |
2663 | + var sentinel = {}; |
2664 | + BootResourcesManager._polling = sentinel; |
2665 | + expect(BootResourcesManager.isPolling()).toBe(sentinel); |
2666 | + }); |
2667 | + }); |
2668 | + |
2669 | + describe("startPolling", function() { |
2670 | + |
2671 | + it("calls _poll and sets polling", function() { |
2672 | + var sentinel = {}; |
2673 | + spyOn(BootResourcesManager, "_poll").and.returnValue(sentinel); |
2674 | + expect(BootResourcesManager.startPolling()).toBe(sentinel); |
2675 | + expect(BootResourcesManager._polling).toBe(true); |
2676 | + }); |
2677 | + |
2678 | + it("returns _nextPromise if already polling", function() { |
2679 | + var sentinel = {}; |
2680 | + BootResourcesManager._polling = true; |
2681 | + BootResourcesManager._nextPromise = sentinel; |
2682 | + spyOn(BootResourcesManager, "_poll"); |
2683 | + expect(BootResourcesManager.startPolling()).toBe(sentinel); |
2684 | + expect(BootResourcesManager._poll).not.toHaveBeenCalled(); |
2685 | + }); |
2686 | + }); |
2687 | + |
2688 | + describe("stopPolling", function() { |
2689 | + |
2690 | + it("clears _polling and cancels _nextPromise", function() { |
2691 | + var sentinel = {}; |
2692 | + BootResourcesManager._polling = true; |
2693 | + BootResourcesManager._nextPromise = sentinel; |
2694 | + spyOn($timeout, "cancel"); |
2695 | + BootResourcesManager.stopPolling(); |
2696 | + expect($timeout.cancel).toHaveBeenCalledWith(sentinel); |
2697 | + expect(BootResourcesManager._nextPromise).toBeNull(); |
2698 | + expect(BootResourcesManager._polling).toBe(false); |
2699 | + }); |
2700 | + }); |
2701 | + |
2702 | + describe("_loadData", function() { |
2703 | + |
2704 | + it("calls bootresource.poll and sets _data", function(done) { |
2705 | + var data = BootResourcesManager._data; |
2706 | + var defer = $q.defer(); |
2707 | + spyOn(RegionConnection, "callMethod").and.returnValue( |
2708 | + defer.promise); |
2709 | + |
2710 | + var newData = { |
2711 | + key: makeName("value") |
2712 | + }; |
2713 | + BootResourcesManager._loadData().then(function(passedData) { |
2714 | + expect(BootResourcesManager._loaded).toBe(true); |
2715 | + expect(BootResourcesManager._data).toBe(data); |
2716 | + expect(BootResourcesManager._data).toBe(passedData); |
2717 | + expect(BootResourcesManager._data).toEqual(newData); |
2718 | + done(); |
2719 | + }); |
2720 | + |
2721 | + expect(RegionConnection.callMethod).toHaveBeenCalledWith( |
2722 | + "bootresource.poll"); |
2723 | + defer.resolve(angular.toJson(newData)); |
2724 | + $rootScope.$digest(); |
2725 | + }); |
2726 | + }); |
2727 | + |
2728 | + describe("saveUbuntu", function() { |
2729 | + |
2730 | + it("calls bootresource.save_ubuntu and sets _data", function(done) { |
2731 | + var data = BootResourcesManager._data; |
2732 | + var defer = $q.defer(); |
2733 | + spyOn(RegionConnection, "callMethod").and.returnValue( |
2734 | + defer.promise); |
2735 | + |
2736 | + var newData = { |
2737 | + key: makeName("value") |
2738 | + }; |
2739 | + var sentinel = {}; |
2740 | + BootResourcesManager.saveUbuntu(sentinel).then(function(pData) { |
2741 | + expect(BootResourcesManager._loaded).toBe(true); |
2742 | + expect(BootResourcesManager._data).toBe(data); |
2743 | + expect(BootResourcesManager._data).toBe(pData); |
2744 | + expect(BootResourcesManager._data).toEqual(newData); |
2745 | + done(); |
2746 | + }); |
2747 | + |
2748 | + expect(RegionConnection.callMethod).toHaveBeenCalledWith( |
2749 | + "bootresource.save_ubuntu", sentinel); |
2750 | + defer.resolve(angular.toJson(newData)); |
2751 | + $rootScope.$digest(); |
2752 | + }); |
2753 | + }); |
2754 | + |
2755 | + describe("saveOther", function() { |
2756 | + |
2757 | + it("calls bootresource.save_other and sets _data", function(done) { |
2758 | + var data = BootResourcesManager._data; |
2759 | + var defer = $q.defer(); |
2760 | + spyOn(RegionConnection, "callMethod").and.returnValue( |
2761 | + defer.promise); |
2762 | + |
2763 | + var newData = { |
2764 | + key: makeName("value") |
2765 | + }; |
2766 | + var sentinel = {}; |
2767 | + BootResourcesManager.saveOther(sentinel).then(function(pData) { |
2768 | + expect(BootResourcesManager._loaded).toBe(true); |
2769 | + expect(BootResourcesManager._data).toBe(data); |
2770 | + expect(BootResourcesManager._data).toBe(pData); |
2771 | + expect(BootResourcesManager._data).toEqual(newData); |
2772 | + done(); |
2773 | + }); |
2774 | + |
2775 | + expect(RegionConnection.callMethod).toHaveBeenCalledWith( |
2776 | + "bootresource.save_other", sentinel); |
2777 | + defer.resolve(angular.toJson(newData)); |
2778 | + $rootScope.$digest(); |
2779 | + }); |
2780 | + }); |
2781 | + |
2782 | + describe("fetch", function() { |
2783 | + |
2784 | + it("calls bootresource.fetch", function() { |
2785 | + var returnSentinel = {}; |
2786 | + var sourceSentinel = {}; |
2787 | + spyOn(RegionConnection, "callMethod").and.returnValue( |
2788 | + returnSentinel); |
2789 | + expect(BootResourcesManager.fetch(sourceSentinel)).toBe( |
2790 | + returnSentinel); |
2791 | + expect(RegionConnection.callMethod).toHaveBeenCalledWith( |
2792 | + "bootresource.fetch", sourceSentinel); |
2793 | + }); |
2794 | + }); |
2795 | +}); |
2796 | |
2797 | === modified file 'src/maasserver/static/js/angular/maas.js' |
2798 | --- src/maasserver/static/js/angular/maas.js 2016-07-06 20:09:20 +0000 |
2799 | +++ src/maasserver/static/js/angular/maas.js 2016-09-14 21:01:21 +0000 |
2800 | @@ -54,6 +54,11 @@ |
2801 | 'static/partials/node-details.html'), |
2802 | controller: 'NodeDetailsController' |
2803 | }). |
2804 | + when('/images', { |
2805 | + templateUrl: versionedPath( |
2806 | + 'static/partials/images.html'), |
2807 | + controller: 'ImagesController' |
2808 | + }). |
2809 | when('/domains', { |
2810 | templateUrl: versionedPath( |
2811 | 'static/partials/domains-list.html'), |
2812 | |
2813 | === added file 'src/maasserver/static/partials/boot-images.html' |
2814 | --- src/maasserver/static/partials/boot-images.html 1970-01-01 00:00:00 +0000 |
2815 | +++ src/maasserver/static/partials/boot-images.html 2016-09-14 21:01:21 +0000 |
2816 | @@ -0,0 +1,265 @@ |
2817 | + |
2818 | +<div data-ng-class="{ 'action-card': design === 'action-card', 'row u-padding--top-large': design === 'page' }" data-ng-if="!loading"> |
2819 | + <div data-ng-class="{ 'wrapper--inner': design === 'page' }"> |
2820 | + <h2 data-ng-class="{ 'action-card__title': design === 'action-card' }"> |
2821 | + <i data-ng-if="design === 'action-card'" class="icon icon--large {$ getTitleIcon() $}"></i> Ubuntu |
2822 | + </h2> |
2823 | + <div data-ng-class="{ 'action-card__controls': design === 'action-card' }"> |
2824 | + <p class="u-margin--top-small u-margin--bottom-small" data-ng-if="isSuperUser() && !source.tooMany"> |
2825 | + Select images and architecture to be imported and kept in sync daily. Images will be available for deploying to machines managed by MAAS. |
2826 | + </p> |
2827 | + <form class="form"> |
2828 | + <div data-ng-if="isSuperUser() && !source.tooMany"> |
2829 | + <h3>Choose source</h3> |
2830 | + <div class="form__group"> |
2831 | + <input type="radio" ng-model="source.source_type" id="source_maas" value="maas.io" data-ng-change="sourceChanged()"> |
2832 | + <label for="source_maas" class="u-margin--right">maas.io</label> |
2833 | + <input type="radio" ng-model="source.source_type" id="source_custom" value="custom" data-ng-change="sourceChanged()"> |
2834 | + <label for="source_custom">Custom</label> |
2835 | + </div> |
2836 | + <div data-ng-if="showMirrorPath()"> |
2837 | + <h3>Mirror URL</h3> |
2838 | + <p>Add the URL you want to use to select your images from.</p> |
2839 | + <div class="u-width--half u-display--inline-block u-float--left"> |
2840 | + <div class="form__group"> |
2841 | + <input type="text" name="mirrorUrl" placeholder="e.g. http:// or file://" |
2842 | + data-ng-model="source.url" data-ng-change="sourceChanged()"> |
2843 | + </div> |
2844 | + <div data-ng-if="isShowingAdvancedOptions()"> |
2845 | + <div class="form__group"> |
2846 | + <label for="keyring_filename" class="form__group--label">Path to the keyring to validate the mirror path.</label> |
2847 | + <input type="text" name="keyring_filename" placeholder="e.g. /usr/share/keyrings/ubuntu-cloudimage-keyring.gpg" |
2848 | + data-ng-model="source.keyring_filename" |
2849 | + data-ng-class="{ 'has-error': bothKeyringOptionsSet() }"> |
2850 | + <ul class="form__error" data-ng-if="bothKeyringOptionsSet()"> |
2851 | + <li class="form__error-item">Cannot set both keyring path and keyring contents.</li> |
2852 | + </ul> |
2853 | + </div> |
2854 | + <div class="form__group"> |
2855 | + <label for="keyring_data" class="form__group--label">Contents of the keyring to validate the mirror path.</label> |
2856 | + <textarea name="keyring_data" placeholder="Contents of GPG key" |
2857 | + data-ng-model="source.keyring_data" |
2858 | + data-ng-class="{ 'has-error': bothKeyringOptionsSet() }"></textarea> |
2859 | + <ul class="form__error" data-ng-if="bothKeyringOptionsSet()"> |
2860 | + <li class="form__error-item">Cannot set both keyring path and keyring contents.</li> |
2861 | + </ul> |
2862 | + </div> |
2863 | + </div> |
2864 | + </div> |
2865 | + <div class="u-width--half u-display--inline-block u-float--left"> |
2866 | + <div class="u-display--inline-block u-float--left u-margin--top-tiny u-margin--left"> |
2867 | + <i class="icon" data-ng-class="{ 'icon--close': !isShowingAdvancedOptions(), 'icon--open': isShowingAdvancedOptions() }"></i> |
2868 | + <a data-ng-click="toggleAdvancedOptions()" data-ng-if="!isShowingAdvancedOptions()">Show advanced options</a> |
2869 | + <a data-ng-click="toggleAdvancedOptions()" data-ng-if="isShowingAdvancedOptions()">Hide advanced options</a> |
2870 | + </div> |
2871 | + <button class="button--secondary button--inline u-float--right" |
2872 | + data-ng-if="showConnectButton()" |
2873 | + data-ng-disabled="isConnectButtonDisabled()" |
2874 | + data-ng-click="connect()">Connect</button> |
2875 | + <div class="u-clear"></div> |
2876 | + </div> |
2877 | + <div class="u-clear"></div> |
2878 | + </div> |
2879 | + </div> |
2880 | + <div data-ng-if="showConnectBlock()"> |
2881 | + <div class="u-clear"></div> |
2882 | + <div class="u-margin--top-large u-margin--bottom-large u-padding--top-large u-padding--bottom-large"> |
2883 | + <p class="text-center" data-ng-if="!source.connecting && source.errorMessage"> |
2884 | + <i class="icon icon--warning"></i> {$ source.errorMessage $} |
2885 | + <a data-ng-if="source.source_type === 'maas.io'" data-ng-click="connect()">Retry</a> |
2886 | + </p> |
2887 | + <p class="text-center" data-ng-if="source.connecting"> |
2888 | + <i class="icon icon--loading u-animation--spin"></i> Connecting |
2889 | + </p> |
2890 | + </div> |
2891 | + </div> |
2892 | + <div data-ng-if="isSuperUser() && !source.tooMany && showSelections()"> |
2893 | + <div class="u-width--half u-display--inline-block u-float--left"> |
2894 | + <h3>Images</h3> |
2895 | + <div class="u-width--half u-display--inline-block u-float--left"> |
2896 | + <div class="form__group" data-ng-repeat="release in getUbuntuLTSReleases() | orderBy:'-title'"> |
2897 | + <input type="checkbox" id="{$ release.name $}" |
2898 | + data-ng-checked="isSelected('releases', release)" |
2899 | + data-ng-click="toggleSelection('releases', release)"> |
2900 | + <label for="{$ release.name $}">{$ release.title $}</label> |
2901 | + </div> |
2902 | + </div> |
2903 | + <div class="u-width--half u-display--inline-block u-float--left"> |
2904 | + <div class="form__group" data-ng-repeat="release in getUbuntuNonLTSReleases() | orderBy:'-title'"> |
2905 | + <input type="checkbox" id="{$ release.name $}" |
2906 | + data-ng-checked="isSelected('releases', release)" |
2907 | + data-ng-click="toggleSelection('releases', release)"> |
2908 | + <label for="{$ release.name $}">{$ release.title $}</label> |
2909 | + </div> |
2910 | + </div> |
2911 | + <div class="u-clear"></div> |
2912 | + </div> |
2913 | + <div class="u-width--half u-display--inline-block u-float--left"> |
2914 | + <h3>Architectures</h3> |
2915 | + <div class="form__group" data-ng-repeat="arch in getArchitectures() | orderBy:'title'"> |
2916 | + <input type="checkbox" id="{$ arch.name $}" |
2917 | + data-ng-checked="isSelected('arches', arch)" |
2918 | + data-ng-click="toggleSelection('arches', arch)"> |
2919 | + <label for="{$ arch.name $}">{$ arch.title $}</label> |
2920 | + </div> |
2921 | + </div> |
2922 | + <div class="u-clear"></div> |
2923 | + </div> |
2924 | + <section class="table u-margin--top" data-ng-if="showImagesTable()"> |
2925 | + <header class="table__head"> |
2926 | + <div class="table__row"> |
2927 | + <div class="table__header table-col--25">Release</div> |
2928 | + <div class="table__header table-col--15">Architecture</div> |
2929 | + <div class="table__header table-col--20">Size</div> |
2930 | + <div class="table__header table-col--30">Importing status</div> |
2931 | + </div> |
2932 | + </header> |
2933 | + <main class="table__body"> |
2934 | + <div class="table__row table--error" data-ng-if="!ltsIsSelected()"> |
2935 | + <div class="table__data table-col--100"> |
2936 | + <i class="icon icon--error"></i> Select at least one 14.04+ LTS release and one architecture. |
2937 | + </div> |
2938 | + </div> |
2939 | + <div class="table__row" data-ng-repeat="image in ubuntuImages | orderBy:['-title', 'arch']"> |
2940 | + <div class="table__data table-col--2"> |
2941 | + <i class="icon {$ image.icon $}"></i> |
2942 | + </div> |
2943 | + <div class="table__data table-col--23"> |
2944 | + {$ image.title $} |
2945 | + </div> |
2946 | + <div class="table__data table-col--15">{$ image.arch $}</div> |
2947 | + <div class="table__data table-col--20">{$ image.size $}</div> |
2948 | + <div class="table__data table-col--30">{$ image.status $}</div> |
2949 | + </div> |
2950 | + </main> |
2951 | + </section> |
2952 | + <div class="twelve-col" data-ng-if="!source.tooMany"> |
2953 | + <div class="u-float--right"> |
2954 | + <!-- Not supported yet. |
2955 | + <a class="button--base button--inline" data-ng-if="showStopImportButton()">Stop import</a> |
2956 | + --> |
2957 | + <button class="button--secondary button--inline" |
2958 | + data-ng-if="isSuperUser() && showSaveSelection()" |
2959 | + data-ng-disabled="!canSaveSelection()" |
2960 | + data-ng-click="saveSelection()">{$ getSaveSelectionText() $}</button> |
2961 | + </div> |
2962 | + </div> |
2963 | + </form> |
2964 | + </div> |
2965 | + </div> |
2966 | +</div> |
2967 | +<div class="row" data-ng-if="design === 'page' && other.images.length && !source.isNew"> |
2968 | + <div class="wrapper--inner"> |
2969 | + <h2>Other Images</h2> |
2970 | + <div data-ng-if="isSuperUser() && !source.tooMany"> |
2971 | + <span class="u-margin--right" data-ng-repeat="image in other.images | orderBy:['-title']"> |
2972 | + <input type="checkbox" id="{$ image.name $}" |
2973 | + data-ng-checked="image.checked" |
2974 | + data-ng-click="toggleOtherSelection(image)"> |
2975 | + <label for="{$ image.name $}">{$ image.title $}</label> |
2976 | + <span> |
2977 | + </div> |
2978 | + <section class="table u-margin--top"> |
2979 | + <header class="table__head"> |
2980 | + <div class="table__row"> |
2981 | + <div class="table__header table-col--25">Release</div> |
2982 | + <div class="table__header table-col--15">Architecture</div> |
2983 | + <div class="table__header table-col--20">Size</div> |
2984 | + <div class="table__header table-col--30">Importing status</div> |
2985 | + </div> |
2986 | + </header> |
2987 | + <main class="table__body"> |
2988 | + <div class="table__row" data-ng-if="!otherImages.length"> |
2989 | + <div class="table__data table-col--100"> |
2990 | + No images have been selected for syncing. |
2991 | + </div> |
2992 | + </div> |
2993 | + <div class="table__row" data-ng-repeat="image in otherImages | orderBy:['-title', 'arch']"> |
2994 | + <div class="table__data table-col--2"> |
2995 | + <i class="icon {$ image.icon $}"></i> |
2996 | + </div> |
2997 | + <div class="table__data table-col--23"> |
2998 | + {$ image.title $} |
2999 | + </div> |
3000 | + <div class="table__data table-col--15">{$ image.arch $}</div> |
3001 | + <div class="table__data table-col--20">{$ image.size $}</div> |
3002 | + <div class="table__data table-col--30">{$ image.status $}</div> |
3003 | + </div> |
3004 | + </main> |
3005 | + </section> |
3006 | + <div class="twelve-col" data-ng-if="!source.tooMany"> |
3007 | + <div class="u-float--right"> |
3008 | + <button class="button--secondary button--inline" |
3009 | + data-ng-if="isSuperUser()" |
3010 | + data-ng-disabled="saving" |
3011 | + data-ng-click="saveOtherSelection()">{$ getSaveSelectionText() $}</button> |
3012 | + </div> |
3013 | + </div> |
3014 | + </div> |
3015 | +</div> |
3016 | +<div class="row" data-ng-if="design === 'page' && generatedImages.length"> |
3017 | + <div class="wrapper--inner"> |
3018 | + <h2>Generated Images</h2> |
3019 | + <section class="table u-margin--top"> |
3020 | + <header class="table__head"> |
3021 | + <div class="table__row"> |
3022 | + <div class="table__header table-col--25">Release</div> |
3023 | + <div class="table__header table-col--15">Architecture</div> |
3024 | + <div class="table__header table-col--20">Size</div> |
3025 | + <div class="table__header table-col--30">Importing status</div> |
3026 | + </div> |
3027 | + </header> |
3028 | + <main class="table__body"> |
3029 | + <div class="table__row" data-ng-if="!generatedImages.length"> |
3030 | + <div class="table__data table-col--100"> |
3031 | + No images have been uploaded. |
3032 | + </div> |
3033 | + </div> |
3034 | + <div class="table__row" data-ng-repeat="image in generatedImages | orderBy:['-title', 'arch']"> |
3035 | + <div class="table__data table-col--2"> |
3036 | + <i class="icon {$ image.icon $}"></i> |
3037 | + </div> |
3038 | + <div class="table__data table-col--23"> |
3039 | + {$ image.title $} |
3040 | + </div> |
3041 | + <div class="table__data table-col--15">{$ image.arch $}</div> |
3042 | + <div class="table__data table-col--20">{$ image.size $}</div> |
3043 | + <div class="table__data table-col--30">{$ image.status $}</div> |
3044 | + </div> |
3045 | + </main> |
3046 | + </section> |
3047 | + </div> |
3048 | +</div> |
3049 | +<div class="row" data-ng-if="design === 'page' && customImages.length"> |
3050 | + <div class="wrapper--inner"> |
3051 | + <h2>Custom Images</h2> |
3052 | + <section class="table u-margin--top"> |
3053 | + <header class="table__head"> |
3054 | + <div class="table__row"> |
3055 | + <div class="table__header table-col--25">Release</div> |
3056 | + <div class="table__header table-col--15">Architecture</div> |
3057 | + <div class="table__header table-col--20">Size</div> |
3058 | + <div class="table__header table-col--30">Importing status</div> |
3059 | + </div> |
3060 | + </header> |
3061 | + <main class="table__body"> |
3062 | + <div class="table__row" data-ng-if="!customImages.length"> |
3063 | + <div class="table__data table-col--100"> |
3064 | + No images have been uploaded. |
3065 | + </div> |
3066 | + </div> |
3067 | + <div class="table__row" data-ng-repeat="image in customImages | orderBy:['-title', 'arch']"> |
3068 | + <div class="table__data table-col--2"> |
3069 | + <i class="icon {$ image.icon $}"></i> |
3070 | + </div> |
3071 | + <div class="table__data table-col--23"> |
3072 | + {$ image.title $} |
3073 | + </div> |
3074 | + <div class="table__data table-col--15">{$ image.arch $}</div> |
3075 | + <div class="table__data table-col--20">{$ image.size $}</div> |
3076 | + <div class="table__data table-col--30">{$ image.status $}</div> |
3077 | + </div> |
3078 | + </main> |
3079 | + </section> |
3080 | + </div> |
3081 | +</div> |
3082 | |
3083 | === added file 'src/maasserver/static/partials/images.html' |
3084 | --- src/maasserver/static/partials/images.html 1970-01-01 00:00:00 +0000 |
3085 | +++ src/maasserver/static/partials/images.html 2016-09-14 21:01:21 +0000 |
3086 | @@ -0,0 +1,7 @@ |
3087 | +<header class="page-header margin-bottom"> |
3088 | + <div class="wrapper--inner"> |
3089 | + <h1 class="page-header__title">Boot Images</h1> |
3090 | + <maas-boot-images-status></maas-boot-images-status> |
3091 | + </div> |
3092 | +</header> |
3093 | +<maas-boot-images></maas-boot-images> |
3094 | |
3095 | === modified file 'src/maasserver/templates/maasserver/base.html' |
3096 | --- src/maasserver/templates/maasserver/base.html 2016-09-06 15:44:03 +0000 |
3097 | +++ src/maasserver/templates/maasserver/base.html 2016-09-14 21:01:21 +0000 |
3098 | @@ -115,7 +115,7 @@ |
3099 | <a class="{% block nav-active-node-list %}{% endblock %}" href="{% url 'index' %}#/nodes">Nodes</a> |
3100 | </li> |
3101 | <li> |
3102 | - <a class="{% block nav-active-images %}{% endblock %}" href="{% url 'images' %}">Images</a> |
3103 | + <a class="{% block nav-active-images %}{% endblock %}" href="{% url 'index' %}#/images">Images</a> |
3104 | </li> |
3105 | <li> |
3106 | <a class="{% block nav-active-domains-list %}{% endblock %}" href="{% url 'index' %}#/domains">DNS</a> |
3107 | |
3108 | === modified file 'src/maasserver/templates/maasserver/index.html' |
3109 | --- src/maasserver/templates/maasserver/index.html 2016-09-06 15:59:15 +0000 |
3110 | +++ src/maasserver/templates/maasserver/index.html 2016-09-14 21:01:21 +0000 |
3111 | @@ -92,7 +92,7 @@ |
3112 | <a data-ng-class="{ active: page === 'nodes' }" href="#/nodes">Nodes</a> |
3113 | </li> |
3114 | <li> |
3115 | - <a data-ng-class="{ active: page === 'images' }" href="{% url 'images' %}">Images</a> |
3116 | + <a data-ng-class="{ active: page === 'images' }" href="#/images">Images</a> |
3117 | </li> |
3118 | <li> |
3119 | <a data-ng-class="{ active: page === 'domains' }" href="#/domains">DNS</a> |
3120 | |
3121 | === modified file 'src/maasserver/tests/test_bootresources.py' |
3122 | --- src/maasserver/tests/test_bootresources.py 2016-08-31 06:26:21 +0000 |
3123 | +++ src/maasserver/tests/test_bootresources.py 2016-09-14 21:01:21 +0000 |
3124 | @@ -83,7 +83,9 @@ |
3125 | get_one, |
3126 | post_commit_hooks, |
3127 | reload_object, |
3128 | + transactional, |
3129 | ) |
3130 | +from maasserver.utils.threads import deferToDatabase |
3131 | from maasserver.utils.version import get_maas_version_ui |
3132 | from maastesting.matchers import ( |
3133 | MockCalledOnce, |
3134 | @@ -112,6 +114,7 @@ |
3135 | ) |
3136 | from twisted.application.internet import TimerService |
3137 | from twisted.internet.defer import ( |
3138 | + Deferred, |
3139 | fail, |
3140 | succeed, |
3141 | ) |
3142 | @@ -1149,6 +1152,44 @@ |
3143 | written_data = stream.read() |
3144 | self.assertEqual(content, written_data) |
3145 | |
3146 | + @asynchronous(timeout=1) |
3147 | + def test_finalize_calls_notify_errback(self): |
3148 | + |
3149 | + @transactional |
3150 | + def create_store(testcase): |
3151 | + factory.make_BootResource(rtype=BOOT_RESOURCE_TYPE.SYNCED) |
3152 | + store = BootResourceStore() |
3153 | + testcase.patch(store, 'resource_cleaner') |
3154 | + testcase.patch(store, 'perform_write') |
3155 | + testcase.patch(store, 'resource_set_cleaner') |
3156 | + return store |
3157 | + |
3158 | + notify = Deferred() |
3159 | + d = deferToDatabase(create_store, self) |
3160 | + d.addCallback(lambda store: store.finalize(notify=notify)) |
3161 | + d.addCallback(lambda _: notify) |
3162 | + d.addErrback(lambda failure: failure.trap(Exception)) |
3163 | + return d |
3164 | + |
3165 | + @asynchronous(timeout=1) |
3166 | + def test_finalize_calls_notify_callback(self): |
3167 | + |
3168 | + @transactional |
3169 | + def create_store(testcase): |
3170 | + factory.make_BootResource(rtype=BOOT_RESOURCE_TYPE.SYNCED) |
3171 | + store = BootResourceStore() |
3172 | + store._content_to_finalize = [sentinel.content] |
3173 | + testcase.patch(store, 'resource_cleaner') |
3174 | + testcase.patch(store, 'perform_write') |
3175 | + testcase.patch(store, 'resource_set_cleaner') |
3176 | + return store |
3177 | + |
3178 | + notify = Deferred() |
3179 | + d = deferToDatabase(create_store, self) |
3180 | + d.addCallback(lambda store: store.finalize(notify=notify)) |
3181 | + d.addCallback(lambda _: notify) |
3182 | + return d |
3183 | + |
3184 | |
3185 | class TestSetGlobalDefaultReleases(MAASServerTestCase): |
3186 | |
3187 | @@ -1300,7 +1341,7 @@ |
3188 | sources=[], product_mapping=product_mapping, store=store) |
3189 | self.assertThat( |
3190 | fake_finalize, |
3191 | - MockCalledOnceWith()) |
3192 | + MockCalledOnceWith(notify=None)) |
3193 | |
3194 | def test__import_resources_exits_early_if_lock_held(self): |
3195 | set_simplestreams_env = self.patch_autospec( |
3196 | @@ -1362,7 +1403,8 @@ |
3197 | MockCalledOnceWith(descriptions)) |
3198 | self.expectThat( |
3199 | download_all_boot_resources, |
3200 | - MockCalledOnceWith([sentinel.source], sentinel.mapping)) |
3201 | + MockCalledOnceWith( |
3202 | + [sentinel.source], sentinel.mapping, notify=None)) |
3203 | self.expectThat( |
3204 | set_global_default_releases, |
3205 | MockCalledOnceWith()) |
3206 | @@ -1417,14 +1459,14 @@ |
3207 | bootresources._import_resources_in_thread() |
3208 | self.assertThat( |
3209 | deferToDatabase, MockCalledOnceWith( |
3210 | - bootresources._import_resources)) |
3211 | + bootresources._import_resources, notify=None)) |
3212 | |
3213 | def tests__defaults_force_to_False(self): |
3214 | deferToDatabase = self.patch(bootresources, "deferToDatabase") |
3215 | bootresources._import_resources_in_thread() |
3216 | self.assertThat( |
3217 | deferToDatabase, MockCalledOnceWith( |
3218 | - bootresources._import_resources)) |
3219 | + bootresources._import_resources, notify=None)) |
3220 | |
3221 | def test__logs_errors_and_does_not_errback(self): |
3222 | logger = self.useFixture(TwistedLoggerFixture()) |
3223 | |
3224 | === modified file 'src/maasserver/views/combo.py' |
3225 | --- src/maasserver/views/combo.py 2016-08-19 10:22:29 +0000 |
3226 | +++ src/maasserver/views/combo.py 2016-09-14 21:01:21 +0000 |
3227 | @@ -51,6 +51,7 @@ |
3228 | "content_type": "text/javascript; charset=UTF-8", |
3229 | "files": [ |
3230 | "js/angular/maas.js", |
3231 | + "js/angular/factories/bootresources.js", |
3232 | "js/angular/factories/dhcpsnippets.js", |
3233 | "js/angular/factories/packagerepositories.js", |
3234 | "js/angular/factories/region.js", |
3235 | @@ -80,6 +81,7 @@ |
3236 | "js/angular/services/converter.js", |
3237 | "js/angular/services/json.js", |
3238 | "js/angular/directives/accordion.js", |
3239 | + "js/angular/directives/boot_images.js", |
3240 | "js/angular/directives/call_to_action.js", |
3241 | "js/angular/directives/code_lines.js", |
3242 | "js/angular/directives/controller_image_status.js", |
3243 | @@ -107,6 +109,7 @@ |
3244 | "js/angular/controllers/add_hardware.js", |
3245 | "js/angular/controllers/add_device.js", |
3246 | "js/angular/controllers/add_domain.js", |
3247 | + "js/angular/controllers/images.js", |
3248 | "js/angular/controllers/node_details.js", |
3249 | "js/angular/controllers/node_details_networking.js", |
3250 | "js/angular/controllers/node_details_storage.js", |
3251 | |
3252 | === modified file 'src/maasserver/websockets/handlers/bootresource.py' |
3253 | --- src/maasserver/websockets/handlers/bootresource.py 2016-09-02 13:16:06 +0000 |
3254 | +++ src/maasserver/websockets/handlers/bootresource.py 2016-09-14 21:01:21 +0000 |
3255 | @@ -38,6 +38,8 @@ |
3256 | Node, |
3257 | ) |
3258 | from maasserver.utils.converters import human_readable_bytes |
3259 | +from maasserver.utils.orm import transactional |
3260 | +from maasserver.utils.threads import deferToDatabase |
3261 | from maasserver.utils.version import get_maas_version_ui |
3262 | from maasserver.websockets.base import ( |
3263 | Handler, |
3264 | @@ -52,6 +54,13 @@ |
3265 | ) |
3266 | from provisioningserver.import_images.keyrings import write_all_keyrings |
3267 | from provisioningserver.utils.fs import tempdir |
3268 | +from provisioningserver.utils.twisted import ( |
3269 | + asynchronous, |
3270 | + callOut, |
3271 | + FOREVER, |
3272 | +) |
3273 | +from twisted.internet.defer import Deferred |
3274 | +from twisted.python import log |
3275 | |
3276 | |
3277 | def get_distro_series_info_row(series): |
3278 | @@ -94,7 +103,7 @@ |
3279 | 'source_type': source_type, |
3280 | 'url': source.url, |
3281 | 'keyring_filename': source.keyring_filename, |
3282 | - 'keyring_data': source.keyring_data.decode('ascii'), |
3283 | + 'keyring_data': bytes(source.keyring_data).decode("ascii"), |
3284 | }) |
3285 | return sources |
3286 | |
3287 | @@ -157,6 +166,94 @@ |
3288 | }) |
3289 | return arches |
3290 | |
3291 | + def check_if_image_matches_resource(self, resource, image): |
3292 | + """Return True if the resource matches the image.""" |
3293 | + os, series = resource.name.split('/') |
3294 | + arch, subarch = resource.split_arch() |
3295 | + if os != image.os or series != image.release or arch != image.arch: |
3296 | + return False |
3297 | + if not resource.supports_subarch(subarch): |
3298 | + return False |
3299 | + return True |
3300 | + |
3301 | + def get_matching_resource_for_image(self, resources, image): |
3302 | + """Return True if the image matches one of the resources.""" |
3303 | + for resource in resources: |
3304 | + if self.check_if_image_matches_resource(resource, image): |
3305 | + return resource |
3306 | + return None |
3307 | + |
3308 | + def get_other_synced_resources(self): |
3309 | + """Return all synced resources that are not Ubuntu.""" |
3310 | + resources = list(BootResource.objects.filter( |
3311 | + rtype=BOOT_RESOURCE_TYPE.SYNCED).exclude( |
3312 | + name__startswith='ubuntu/').order_by('-name', 'architecture')) |
3313 | + for resource in resources: |
3314 | + self.add_resource_template_attributes(resource) |
3315 | + return resources |
3316 | + |
3317 | + def add_resource_template_attributes(self, resource): |
3318 | + """Adds helper attributes to the resource.""" |
3319 | + resource.title = self.get_resource_title(resource) |
3320 | + resource.arch, resource.subarch = resource.split_arch() |
3321 | + resource.number_of_nodes = self.get_number_of_nodes_deployed_for( |
3322 | + resource) |
3323 | + resource_set = resource.get_latest_set() |
3324 | + if resource_set is None: |
3325 | + resource.size = human_readable_bytes(0) |
3326 | + resource.last_update = resource.updated |
3327 | + resource.complete = False |
3328 | + resource.status = "Queued for download" |
3329 | + resource.downloading = False |
3330 | + else: |
3331 | + resource.size = human_readable_bytes(resource_set.total_size) |
3332 | + resource.last_update = resource_set.updated |
3333 | + resource.complete = resource_set.complete |
3334 | + if not resource.complete: |
3335 | + progress = resource_set.progress |
3336 | + if progress > 0: |
3337 | + resource.status = "Downloading %3.0f%%" % progress |
3338 | + resource.downloading = True |
3339 | + resource.icon = 'in-progress' |
3340 | + else: |
3341 | + resource.status = "Queued for download" |
3342 | + resource.downloading = False |
3343 | + resource.icon = 'queued' |
3344 | + else: |
3345 | + # See if the resource also exists on all the clusters. |
3346 | + if resource in self.rack_resources: |
3347 | + resource.status = "Synced" |
3348 | + resource.downloading = False |
3349 | + resource.icon = 'succeeded' |
3350 | + else: |
3351 | + resource.complete = False |
3352 | + if self.racks_syncing: |
3353 | + resource.status = "Syncing to rack controller(s)" |
3354 | + resource.downloading = True |
3355 | + resource.icon = 'in-progress' |
3356 | + else: |
3357 | + resource.status = ( |
3358 | + "Waiting for rack controller(s) to sync") |
3359 | + resource.downloading = False |
3360 | + resource.icon = 'waiting' |
3361 | + |
3362 | + def format_other_images(self): |
3363 | + """Return formatted other images for selection.""" |
3364 | + resources = self.get_other_synced_resources() |
3365 | + images = [] |
3366 | + for image in BootSourceCache.objects.exclude(os='ubuntu'): |
3367 | + resource = self.get_matching_resource_for_image(resources, image) |
3368 | + title = get_os_release_title(image.os, image.release) |
3369 | + if title is None: |
3370 | + title = '%s/%s' % (image.os, image.release) |
3371 | + images.append({ |
3372 | + 'name': '%s/%s/%s/%s' % ( |
3373 | + image.os, image.arch, image.subarch, image.release), |
3374 | + 'title': title, |
3375 | + 'checked': True if resource else False, |
3376 | + }) |
3377 | + return images |
3378 | + |
3379 | def node_has_architecture_for_resource(self, node, resource): |
3380 | """Return True if node is the same architecture as resource.""" |
3381 | arch, _ = resource.split_arch() |
3382 | @@ -302,24 +399,29 @@ |
3383 | if progress > 0: |
3384 | resource.status = "Downloading %3.0f%%" % progress |
3385 | resource.downloading = True |
3386 | + resource.icon = 'in-progress' |
3387 | else: |
3388 | resource.status = "Queued for download" |
3389 | resource.downloading = False |
3390 | + resource.icon = 'queued' |
3391 | else: |
3392 | # See if all the resources exist on all the racks. |
3393 | rack_has_resources = any( |
3394 | res in group for res in self.rack_resources) |
3395 | if rack_has_resources: |
3396 | - resource.status = "Complete" |
3397 | + resource.status = "Synced" |
3398 | resource.downloading = False |
3399 | + resource.icon = 'succeeded' |
3400 | else: |
3401 | resource.complete = False |
3402 | if self.racks_syncing: |
3403 | resource.status = "Syncing to rack controller(s)" |
3404 | resource.downloading = True |
3405 | + resource.icon = 'in-progress' |
3406 | else: |
3407 | resource.status = "Waiting for rack controller(s) to sync" |
3408 | resource.downloading = False |
3409 | + resource.icon = 'waiting' |
3410 | return resource |
3411 | |
3412 | def combine_resources(self, resources): |
3413 | @@ -380,7 +482,7 @@ |
3414 | rtype=resource.rtype, name=resource.name, |
3415 | title=resource.title, arch=resource.arch, size=resource.size, |
3416 | complete=resource.complete, status=resource.status, |
3417 | - downloading=resource.downloading, |
3418 | + icon=resource.icon, downloading=resource.downloading, |
3419 | numberOfNodes=resource.number_of_nodes, |
3420 | lastUpdate=resource.last_update.strftime( |
3421 | "%a, %d %b. %Y %H:%M:%S") |
3422 | @@ -396,92 +498,150 @@ |
3423 | region_import_running=is_import_resources_running(), |
3424 | rack_import_running=self.racks_syncing, |
3425 | resources=json_resources, |
3426 | - ubuntu=json_ubuntu) |
3427 | + ubuntu=json_ubuntu, |
3428 | + other_images=self.format_other_images()) |
3429 | return json.dumps(data) |
3430 | |
3431 | + def get_bootsource(self, params, from_db=False): |
3432 | + source_type = params.get('source_type', 'custom') |
3433 | + if source_type == 'maas.io': |
3434 | + url = DEFAULT_IMAGES_URL |
3435 | + keyring_filename = DEFAULT_KEYRINGS_PATH |
3436 | + keyring_data = b'' |
3437 | + elif source_type == 'custom': |
3438 | + url = params['url'] |
3439 | + keyring_filename = params.get('keyring_filename', '') |
3440 | + keyring_data = params.get('keyring_data', '').encode('utf-8') |
3441 | + if keyring_filename == '' and keyring_data == b'': |
3442 | + keyring_filename = DEFAULT_KEYRINGS_PATH |
3443 | + else: |
3444 | + raise HandlerError('Unknown source_type: %s' % source_type) |
3445 | + |
3446 | + if from_db: |
3447 | + source, created = BootSource.objects.get_or_create( |
3448 | + url=url, |
3449 | + defaults={ |
3450 | + 'keyring_filename': keyring_filename, |
3451 | + 'keyring_data': keyring_data}) |
3452 | + if not created: |
3453 | + source.keyring_filename = keyring_filename |
3454 | + source.keyring_data = keyring_data |
3455 | + source.save() |
3456 | + else: |
3457 | + # This was a new source, make sure its the only source in the |
3458 | + # database. This is because the UI only supports handling one |
3459 | + # source at a time. |
3460 | + BootSource.objects.exclude(id=source.id).delete() |
3461 | + return source |
3462 | + else: |
3463 | + return BootSource( |
3464 | + url=url, |
3465 | + keyring_filename=keyring_filename, |
3466 | + keyring_data=keyring_data, |
3467 | + ) |
3468 | + |
3469 | + @asynchronous(timeout=FOREVER) |
3470 | def save_ubuntu(self, params): |
3471 | """Called to save the Ubuntu section of the websocket.""" |
3472 | # Must be administrator. |
3473 | assert self.user.is_superuser, "Permission denied." |
3474 | |
3475 | - os = 'ubuntu' |
3476 | - releases = params['releases'] |
3477 | - arches = params['arches'] |
3478 | - boot_source = BootSource.objects.first() |
3479 | - |
3480 | - # Remove all selections, that are not of release. |
3481 | - BootSourceSelection.objects.filter( |
3482 | - boot_source=boot_source, os=os).exclude( |
3483 | - release__in=releases).delete() |
3484 | - |
3485 | - if len(releases) > 0: |
3486 | - # Create or update the selections. |
3487 | - for release in releases: |
3488 | + @transactional |
3489 | + def update_source(params): |
3490 | + os = 'ubuntu' |
3491 | + releases = params['releases'] |
3492 | + arches = params['arches'] |
3493 | + boot_source = self.get_bootsource(params, from_db=True) |
3494 | + |
3495 | + # Remove all selections, that are not of release. |
3496 | + BootSourceSelection.objects.filter( |
3497 | + boot_source=boot_source, os=os).exclude( |
3498 | + release__in=releases).delete() |
3499 | + |
3500 | + if len(releases) > 0: |
3501 | + # Create or update the selections. |
3502 | + for release in releases: |
3503 | + selection, _ = BootSourceSelection.objects.get_or_create( |
3504 | + boot_source=boot_source, os=os, release=release) |
3505 | + selection.arches = arches |
3506 | + selection.subarches = ["*"] |
3507 | + selection.labels = ["*"] |
3508 | + selection.save() |
3509 | + else: |
3510 | + # Create a selection that will cause nothing to be downloaded, |
3511 | + # since no releases are selected. |
3512 | selection, _ = BootSourceSelection.objects.get_or_create( |
3513 | - boot_source=boot_source, os=os, release=release) |
3514 | + boot_source=boot_source, os=os, release="") |
3515 | selection.arches = arches |
3516 | selection.subarches = ["*"] |
3517 | selection.labels = ["*"] |
3518 | selection.save() |
3519 | - else: |
3520 | - # Create a selection that will cause nothing to be downloaded, |
3521 | - # since no releases are selected. |
3522 | - selection, _ = BootSourceSelection.objects.get_or_create( |
3523 | - boot_source=boot_source, os=os, release="") |
3524 | - selection.arches = arches |
3525 | - selection.subarches = ["*"] |
3526 | - selection.labels = ["*"] |
3527 | - selection.save() |
3528 | - |
3529 | - # Start the import process, now that the selections have changed. |
3530 | - import_resources() |
3531 | - return self.poll({}) |
3532 | - |
3533 | + |
3534 | + notify = Deferred() |
3535 | + d = deferToDatabase(update_source, params) |
3536 | + d.addCallback(callOut, import_resources, notify=notify) |
3537 | + d.addCallback(lambda _: notify) |
3538 | + d.addCallback(lambda _: deferToDatabase(transactional(self.poll), {})) |
3539 | + d.addErrback( |
3540 | + log.err, |
3541 | + "Failed to start the image import. Unable to save the Ubuntu " |
3542 | + "image(s) source information.") |
3543 | + return d |
3544 | + |
3545 | + @asynchronous(timeout=FOREVER) |
3546 | def save_other(self, params): |
3547 | """Update `BootSourceSelection`'s to only include the selected |
3548 | images.""" |
3549 | # Must be administrator. |
3550 | assert self.user.is_superuser, "Permission denied." |
3551 | - # Remove all selections that are not Ubuntu. |
3552 | - BootSourceSelection.objects.exclude(os='ubuntu').delete() |
3553 | - |
3554 | - # Break down the images into os/release with multiple arches. |
3555 | - selections = defaultdict(list) |
3556 | - for image in params['images']: |
3557 | - os, arch, _, release = image.split('/', 4) |
3558 | - name = '%s/%s' % (os, release) |
3559 | - selections[name].append(arch) |
3560 | - |
3561 | - # Create each selection for the source. |
3562 | - for name, arches in selections.items(): |
3563 | - os, release = name.split('/') |
3564 | - cache = BootSourceCache.objects.filter( |
3565 | - os=os, arch=arch, release=release).first() |
3566 | - if cache is None: |
3567 | - # It is possible the cache changed while waiting for the user |
3568 | - # to perform an action. Ignore the selection as its no longer |
3569 | - # available. |
3570 | - continue |
3571 | - # Create the selection for the source. |
3572 | - BootSourceSelection.objects.create( |
3573 | - boot_source=cache.boot_source, |
3574 | - os=os, release=release, |
3575 | - arches=arches, subarches=["*"], labels=["*"]) |
3576 | - |
3577 | - # Start the import process, now that the selections have changed. |
3578 | - import_resources() |
3579 | - return self.poll({}) |
3580 | + |
3581 | + @transactional |
3582 | + def update_selections(params): |
3583 | + # Remove all selections that are not Ubuntu. |
3584 | + BootSourceSelection.objects.exclude(os='ubuntu').delete() |
3585 | + |
3586 | + # Break down the images into os/release with multiple arches. |
3587 | + selections = defaultdict(list) |
3588 | + for image in params['images']: |
3589 | + os, arch, _, release = image.split('/', 4) |
3590 | + name = '%s/%s' % (os, release) |
3591 | + selections[name].append(arch) |
3592 | + |
3593 | + # Create each selection for the source. |
3594 | + for name, arches in selections.items(): |
3595 | + os, release = name.split('/') |
3596 | + cache = BootSourceCache.objects.filter( |
3597 | + os=os, arch=arch, release=release).first() |
3598 | + if cache is None: |
3599 | + # It is possible the cache changed while waiting for the |
3600 | + # user to perform an action. Ignore the selection as its |
3601 | + # no longer available. |
3602 | + continue |
3603 | + # Create the selection for the source. |
3604 | + BootSourceSelection.objects.create( |
3605 | + boot_source=cache.boot_source, |
3606 | + os=os, release=release, |
3607 | + arches=arches, subarches=["*"], labels=["*"]) |
3608 | + |
3609 | + notify = Deferred() |
3610 | + d = deferToDatabase(update_selections, params) |
3611 | + d.addCallback(callOut, import_resources, notify=notify) |
3612 | + d.addCallback(lambda _: notify) |
3613 | + d.addCallback(lambda _: deferToDatabase(transactional(self.poll), {})) |
3614 | + d.addErrback( |
3615 | + log.err, |
3616 | + "Failed to start the image import. Unable to save the non-Ubuntu " |
3617 | + "image(s) source information") |
3618 | + return d |
3619 | |
3620 | def fetch(self, params): |
3621 | """Fetch the releases and the arches from the provided source.""" |
3622 | # Must be administrator. |
3623 | assert self.user.is_superuser, "Permission denied." |
3624 | # Build a source, but its not saved into the database. |
3625 | - source = BootSource( |
3626 | - url=params['url'], |
3627 | - keyring_filename=params.get('keyring_filename', ''), |
3628 | - keyring_data=params.get('keyring_data', '').encode('utf-8'), |
3629 | - ).to_dict_without_selections() |
3630 | + source = self.get_bootsource( |
3631 | + params, from_db=False).to_dict_without_selections() |
3632 | + |
3633 | # FIXME: This modifies the environment of the entire process, which is |
3634 | # Not Cool. We should integrate with simplestreams in a more |
3635 | # Pythonic manner. |
3636 | |
3637 | === modified file 'src/maasserver/websockets/handlers/tests/test_bootresource.py' |
3638 | --- src/maasserver/websockets/handlers/tests/test_bootresource.py 2016-09-01 20:45:56 +0000 |
3639 | +++ src/maasserver/websockets/handlers/tests/test_bootresource.py 2016-09-14 21:01:21 +0000 |
3640 | @@ -21,7 +21,10 @@ |
3641 | ) |
3642 | from maasserver.models.signals import bootsources |
3643 | from maasserver.testing.factory import factory |
3644 | -from maasserver.testing.testcase import MAASServerTestCase |
3645 | +from maasserver.testing.testcase import ( |
3646 | + MAASServerTestCase, |
3647 | + MAASTransactionServerTestCase, |
3648 | +) |
3649 | from maasserver.utils.converters import human_readable_bytes |
3650 | from maasserver.utils.orm import ( |
3651 | get_one, |
3652 | @@ -50,6 +53,7 @@ |
3653 | ContainsAll, |
3654 | HasLength, |
3655 | ) |
3656 | +from twisted.internet import reactor |
3657 | |
3658 | |
3659 | class PatchOSInfoMixin: |
3660 | @@ -74,6 +78,25 @@ |
3661 | self.addCleanup(bootsources.signals.enable) |
3662 | bootsources.signals.disable() |
3663 | |
3664 | + def make_other_resource(self, os=None, arch=None, subarch=None, |
3665 | + release=None): |
3666 | + if os is None: |
3667 | + os = factory.make_name('os') |
3668 | + if arch is None: |
3669 | + arch = factory.make_name('arch') |
3670 | + if subarch is None: |
3671 | + subarch = factory.make_name('subarch') |
3672 | + if release is None: |
3673 | + release = factory.make_name('release') |
3674 | + name = '%s/%s' % (os, release) |
3675 | + architecture = '%s/%s' % (arch, subarch) |
3676 | + resource = factory.make_BootResource( |
3677 | + rtype=BOOT_RESOURCE_TYPE.SYNCED, |
3678 | + name=name, architecture=architecture) |
3679 | + resource_set = factory.make_BootResourceSet(resource) |
3680 | + factory.make_boot_resource_file_with_content(resource_set) |
3681 | + return resource |
3682 | + |
3683 | def test__returns_connection_error_True(self): |
3684 | owner = factory.make_admin() |
3685 | handler = BootResourceHandler(owner, {}) |
3686 | @@ -237,7 +260,7 @@ |
3687 | json_resource, |
3688 | ContainsAll([ |
3689 | 'id', 'rtype', 'name', 'title', 'arch', 'size', |
3690 | - 'complete', 'status', 'downloading', |
3691 | + 'complete', 'status', 'icon', 'downloading', |
3692 | 'numberOfNodes', 'lastUpdate'])) |
3693 | |
3694 | def test_returns_ubuntu_release_version_name(self): |
3695 | @@ -431,6 +454,7 @@ |
3696 | json_obj = json.loads(response) |
3697 | json_resource = json_obj['resources'][0] |
3698 | self.assertEqual("Downloading 50%", json_resource['status']) |
3699 | + self.assertEqual("in-progress", json_resource['icon']) |
3700 | |
3701 | def test_combined_subarch_resource_shows_queued_if_no_progress(self): |
3702 | owner = factory.make_admin() |
3703 | @@ -449,6 +473,7 @@ |
3704 | json_obj = json.loads(response) |
3705 | json_resource = json_obj['resources'][0] |
3706 | self.assertEqual("Queued for download", json_resource['status']) |
3707 | + self.assertEqual("queued", json_resource['icon']) |
3708 | |
3709 | def test_combined_subarch_resource_shows_complete_status(self): |
3710 | owner = factory.make_admin() |
3711 | @@ -468,7 +493,8 @@ |
3712 | response = handler.poll({}) |
3713 | json_obj = json.loads(response) |
3714 | json_resource = json_obj['resources'][0] |
3715 | - self.assertEqual("Complete", json_resource['status']) |
3716 | + self.assertEqual("Synced", json_resource['status']) |
3717 | + self.assertEqual("succeeded", json_resource['icon']) |
3718 | |
3719 | def test_combined_subarch_resource_shows_waiting_for_cluster_to_sync(self): |
3720 | owner = factory.make_admin() |
3721 | @@ -488,6 +514,7 @@ |
3722 | json_resource = json_obj['resources'][0] |
3723 | self.assertEqual( |
3724 | "Waiting for rack controller(s) to sync", json_resource['status']) |
3725 | + self.assertEqual("waiting", json_resource['icon']) |
3726 | |
3727 | def test_combined_subarch_resource_shows_clusters_syncing(self): |
3728 | owner = factory.make_admin() |
3729 | @@ -509,9 +536,42 @@ |
3730 | json_resource = json_obj['resources'][0] |
3731 | self.assertEqual( |
3732 | "Syncing to rack controller(s)", json_resource['status']) |
3733 | - |
3734 | - |
3735 | -class TestBootResourceSaveUbuntu(MAASServerTestCase, PatchOSInfoMixin): |
3736 | + self.assertEqual("in-progress", json_resource['icon']) |
3737 | + |
3738 | + def test_other_images_returns_images_from_cache(self): |
3739 | + owner = factory.make_admin() |
3740 | + handler = BootResourceHandler(owner, {}) |
3741 | + cache = factory.make_BootSourceCache() |
3742 | + response = handler.poll({}) |
3743 | + json_obj = json.loads(response) |
3744 | + other_images = json_obj['other_images'] |
3745 | + self.assertEquals([{ |
3746 | + 'name': '%s/%s/%s/%s' % ( |
3747 | + cache.os, cache.arch, cache.subarch, cache.release), |
3748 | + 'title': '%s/%s' % (cache.os, cache.release), |
3749 | + 'checked': False, |
3750 | + }], other_images) |
3751 | + |
3752 | + def test_other_images_returns_image_checked_when_synced(self): |
3753 | + owner = factory.make_admin() |
3754 | + handler = BootResourceHandler(owner, {}) |
3755 | + cache = factory.make_BootSourceCache() |
3756 | + self.make_other_resource( |
3757 | + os=cache.os, arch=cache.arch, |
3758 | + subarch=cache.subarch, release=cache.release) |
3759 | + response = handler.poll({}) |
3760 | + json_obj = json.loads(response) |
3761 | + other_images = json_obj['other_images'] |
3762 | + self.assertEquals([{ |
3763 | + 'name': '%s/%s/%s/%s' % ( |
3764 | + cache.os, cache.arch, cache.subarch, cache.release), |
3765 | + 'title': '%s/%s' % (cache.os, cache.release), |
3766 | + 'checked': True, |
3767 | + }], other_images) |
3768 | + |
3769 | + |
3770 | +class TestBootResourceSaveUbuntu( |
3771 | + MAASTransactionServerTestCase, PatchOSInfoMixin): |
3772 | |
3773 | def setUp(self): |
3774 | super(TestBootResourceSaveUbuntu, self).setUp() |
3775 | @@ -519,6 +579,12 @@ |
3776 | self.addCleanup(bootsources.signals.enable) |
3777 | bootsources.signals.disable() |
3778 | |
3779 | + def patch_import_resources(self): |
3780 | + mock_import = self.patch(bootresource, 'import_resources') |
3781 | + mock_import.side_effect = ( |
3782 | + lambda notify: reactor.callLater(0, notify.callback, None)) |
3783 | + return mock_import |
3784 | + |
3785 | def test_asserts_is_admin(self): |
3786 | owner = factory.make_User() |
3787 | handler = BootResourceHandler(owner, {}) |
3788 | @@ -529,17 +595,19 @@ |
3789 | handler = BootResourceHandler(owner, {}) |
3790 | sources = [factory.make_BootSource()] |
3791 | self.patch_get_os_info_from_boot_sources(sources) |
3792 | - mock_import = self.patch(bootresource, 'import_resources') |
3793 | - handler.save_ubuntu({'releases': [], 'arches': []}) |
3794 | - self.assertThat(mock_import, MockCalledOnceWith()) |
3795 | + mock_import = self.patch_import_resources() |
3796 | + handler.save_ubuntu( |
3797 | + {'url': sources[0].url, 'releases': [], 'arches': []}) |
3798 | + self.assertThat(mock_import, MockCalledOnceWith(notify=ANY)) |
3799 | |
3800 | def test_sets_empty_selections(self): |
3801 | owner = factory.make_admin() |
3802 | handler = BootResourceHandler(owner, {}) |
3803 | source = factory.make_BootSource() |
3804 | self.patch_get_os_info_from_boot_sources([source]) |
3805 | - self.patch(bootresource, 'import_resources') |
3806 | - handler.save_ubuntu({'releases': [], 'arches': []}) |
3807 | + self.patch_import_resources() |
3808 | + handler.save_ubuntu( |
3809 | + {'url': source.url, 'releases': [], 'arches': []}) |
3810 | |
3811 | selections = BootSourceSelection.objects.filter(boot_source=source) |
3812 | self.assertThat(selections, HasLength(1)) |
3813 | @@ -555,8 +623,9 @@ |
3814 | source = factory.make_BootSource() |
3815 | releases = [factory.make_name('release') for _ in range(3)] |
3816 | self.patch_get_os_info_from_boot_sources([source]) |
3817 | - self.patch(bootresource, 'import_resources') |
3818 | - handler.save_ubuntu({'releases': releases, 'arches': []}) |
3819 | + self.patch_import_resources() |
3820 | + handler.save_ubuntu( |
3821 | + {'url': source.url, 'releases': releases, 'arches': []}) |
3822 | |
3823 | selections = BootSourceSelection.objects.filter(boot_source=source) |
3824 | self.assertThat(selections, HasLength(len(releases))) |
3825 | @@ -571,8 +640,9 @@ |
3826 | releases = [factory.make_name('release') for _ in range(3)] |
3827 | arches = [factory.make_name('arches') for _ in range(3)] |
3828 | self.patch_get_os_info_from_boot_sources([source]) |
3829 | - self.patch(bootresource, 'import_resources') |
3830 | - handler.save_ubuntu({'releases': releases, 'arches': arches}) |
3831 | + self.patch_import_resources() |
3832 | + handler.save_ubuntu( |
3833 | + {'url': source.url, 'releases': releases, 'arches': arches}) |
3834 | |
3835 | selections = BootSourceSelection.objects.filter(boot_source=source) |
3836 | self.assertThat(selections, HasLength(len(releases))) |
3837 | @@ -591,13 +661,14 @@ |
3838 | keep_selection = BootSourceSelection.objects.create( |
3839 | boot_source=source, os='ubuntu', release=release) |
3840 | self.patch_get_os_info_from_boot_sources([source]) |
3841 | - self.patch(bootresource, 'import_resources') |
3842 | - handler.save_ubuntu({'releases': [release], 'arches': []}) |
3843 | + self.patch_import_resources() |
3844 | + handler.save_ubuntu( |
3845 | + {'url': source.url, 'releases': [release], 'arches': []}) |
3846 | self.assertIsNone(reload_object(delete_selection)) |
3847 | self.assertIsNotNone(reload_object(keep_selection)) |
3848 | |
3849 | |
3850 | -class TestBootResourceSaveOther(MAASServerTestCase): |
3851 | +class TestBootResourceSaveOther(MAASTransactionServerTestCase): |
3852 | |
3853 | def setUp(self): |
3854 | super(TestBootResourceSaveOther, self).setUp() |
3855 | @@ -624,6 +695,12 @@ |
3856 | factory.make_boot_resource_file_with_content(resource_set) |
3857 | return resource |
3858 | |
3859 | + def patch_import_resources(self): |
3860 | + mock_import = self.patch(bootresource, 'import_resources') |
3861 | + mock_import.side_effect = ( |
3862 | + lambda notify: reactor.callLater(0, notify.callback, None)) |
3863 | + return mock_import |
3864 | + |
3865 | def test_asserts_is_admin(self): |
3866 | owner = factory.make_User() |
3867 | handler = BootResourceHandler(owner, {}) |
3868 | @@ -637,7 +714,7 @@ |
3869 | boot_source=source, os='ubuntu') |
3870 | other_selection = BootSourceSelection.objects.create( |
3871 | boot_source=source, os=factory.make_name('os')) |
3872 | - self.patch(bootresource, 'import_resources') |
3873 | + self.patch_import_resources() |
3874 | handler.save_other({'images': []}) |
3875 | self.assertIsNotNone(reload_object(ubuntu_selection)) |
3876 | self.assertIsNone(reload_object(other_selection)) |
3877 | @@ -654,7 +731,7 @@ |
3878 | factory.make_BootSourceCache( |
3879 | boot_source=source, os=os, release=release, arch=arch) |
3880 | images.append('%s/%s/subarch/%s' % (os, arch, release)) |
3881 | - self.patch(bootresource, 'import_resources') |
3882 | + self.patch_import_resources() |
3883 | handler.save_other({'images': images}) |
3884 | |
3885 | selection = get_one(BootSourceSelection.objects.filter( |
3886 | @@ -665,9 +742,9 @@ |
3887 | def test_calls_import_resources(self): |
3888 | owner = factory.make_admin() |
3889 | handler = BootResourceHandler(owner, {}) |
3890 | - mock_import = self.patch(bootresource, 'import_resources') |
3891 | + mock_import = self.patch_import_resources() |
3892 | handler.save_other({'images': []}) |
3893 | - self.assertThat(mock_import, MockCalledOnceWith()) |
3894 | + self.assertThat(mock_import, MockCalledOnceWith(notify=ANY)) |
3895 | |
3896 | |
3897 | class TestBootResourceFetch(MAASServerTestCase): |
Looks really good so far. I was able to merge lp:~ltrager/maas/use_bootloaders into your branch to test it with the bootloaders.
Couple of things I noticed .objects. filter( bootloader_ type=None) i.imgur. com/EJOjhpg. png
1. The bootloaders still show up. In my branch I filter them with BootSourceCache
2. The other images section lists the images twice, onces with checkboxes, onces without. See http://
3. I started with MAAS using the V3 test stream. If I click on the maas.io source the boot source metadata is immediately reloaded. It looks like the region then tries to download the maas.io images(they're at an older version than mine). If I switch back to my custom stream 16.10 is marked 'Queued for deletion.' I see there is a 'connect' button. I think the user should have to click connect before things change. This should prevent downloading the image again if the source is accidentally changed.
4. Sometimes when the page loads its blank or I only see other images. Refreshing fixes this.
5. When I click connect with a custom stream I briefly(only shows for about a second) get an error message saying 'Select atleast one 14.04+ LTS release and architecture' even though I already have 16.04 installed.