Merge lp:~blake-rouse/maas/fix-1378527-1.7 into lp:maas/1.7

Proposed by Blake Rouse
Status: Merged
Approved by: Raphaël Badin
Approved revision: no longer in the source branch.
Merged at revision: 3263
Proposed branch: lp:~blake-rouse/maas/fix-1378527-1.7
Merge into: lp:maas/1.7
Diff against target: 530 lines (+386/-24)
7 files modified
src/maasserver/models/bootresource.py (+21/-12)
src/maasserver/models/bootresourceset.py (+1/-1)
src/maasserver/models/tests/test_bootresource.py (+28/-0)
src/maasserver/models/tests/test_bootresourceset.py (+3/-3)
src/maasserver/views/images.py (+139/-3)
src/maasserver/views/tests/test_images.py (+191/-0)
src/provisioningserver/import_images/download_descriptions.py (+3/-5)
To merge this branch: bzr merge lp:~blake-rouse/maas/fix-1378527-1.7
Reviewer Review Type Date Requested Status
Christian Reis (community) Needs Information
Gavin Panella (community) Abstain
Andres Rodriguez (community) Approve
Raphaël Badin (community) Approve
Review via email: mp+238641@code.launchpad.net

Commit message

Fixes the images page to not show multiple resources for HWE enablement resources. Fixes the incorrect progress of downloading resources.

To post a comment you must log in.
Revision history for this message
Christian Reis (kiko) wrote :

Ugh, this is a big change.

Revision history for this message
Raphaël Badin (rvb) wrote :

Not done with the review yet but here is a first set of comments…

Revision history for this message
Raphaël Badin (rvb) :
review: Approve
Revision history for this message
Christian Reis (kiko) wrote :

Can someone independently verify trunk before we land this and/or cut the RC for Utopic?

Revision history for this message
Andres Rodriguez (andreserl) wrote :

Tested... no regressions. Seems to fix the issues.

review: Approve
Revision history for this message
Gavin Panella (allenap) wrote :

I've skimmed over this, so no vote. I don't think I can add anything to what Raphaël has written.

review: Abstain
Revision history for this message
Christian Reis (kiko) wrote :

How can this have not regressed locally and then died when on the branch?

review: Needs Information

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'src/maasserver/models/bootresource.py'
2--- src/maasserver/models/bootresource.py 2014-10-10 10:24:21 +0000
3+++ src/maasserver/models/bootresource.py 2014-10-17 01:59:55 +0000
4@@ -186,19 +186,28 @@
5 else:
6 rtypes = [BOOT_RESOURCE_TYPE.UPLOADED]
7 name = image['release']
8- resource = resources.filter(
9+ matching_resources = resources.filter(
10 rtype__in=rtypes, name=name,
11- architecture__startswith=image['architecture']).first()
12- if resource is None:
13- continue
14- if not resource.supports_subarch(image['subarchitecture']):
15- continue
16- resource_set = resource.get_latest_complete_set()
17- if resource_set is None:
18- continue
19- if resource_set.label != image['label']:
20- continue
21- matched_resources.add(resource)
22+ architecture__startswith=image['architecture'])
23+ for resource in matching_resources:
24+ if resource is None:
25+ # This shouldn't happen at all, but just to be sure.
26+ continue
27+ if not resource.supports_subarch(image['subarchitecture']):
28+ # This matching resource doesn't support the images
29+ # subarchitecture, so its not a matching resource.
30+ continue
31+ resource_set = resource.get_latest_complete_set()
32+ if resource_set is None:
33+ # Possible that the import just started, and there is no
34+ # set. Making it not a matching resource, as it cannot
35+ # exist on the cluster unless it has a set.
36+ continue
37+ if resource_set.label != image['label']:
38+ # The label is different so the cluster has a different
39+ # version of this set.
40+ continue
41+ matched_resources.add(resource)
42 return list(matched_resources)
43
44 def boot_images_are_in_sync(self, images):
45
46=== modified file 'src/maasserver/models/bootresourceset.py'
47--- src/maasserver/models/bootresourceset.py 2014-08-31 02:47:48 +0000
48+++ src/maasserver/models/bootresourceset.py 2014-10-17 01:59:55 +0000
49@@ -130,7 +130,7 @@
50 if size <= 0:
51 # Handle division by zero
52 return 0
53- return self.total_size / float(size)
54+ return 100.0 * size / float(self.total_size)
55
56 @property
57 def complete(self):
58
59=== modified file 'src/maasserver/models/tests/test_bootresource.py'
60--- src/maasserver/models/tests/test_bootresource.py 2014-10-10 10:59:28 +0000
61+++ src/maasserver/models/tests/test_bootresource.py 2014-10-17 01:59:55 +0000
62@@ -363,6 +363,34 @@
63 [resource],
64 BootResource.objects.get_resources_matching_boot_images(images))
65
66+ def test__returns_multiple_resource_for_hwe_resources(self):
67+ os = factory.make_name('os')
68+ series = factory.make_name('series')
69+ name = '%s/%s' % (os, series)
70+ arch = factory.make_name('arch')
71+ subarches = [factory.make_name('hwe') for _ in range(3)]
72+ resources = [
73+ factory.make_usable_boot_resource(
74+ rtype=BOOT_RESOURCE_TYPE.SYNCED,
75+ name=name, architecture='%s/%s' % (arch, subarch))
76+ for subarch in subarches
77+ ]
78+ images = []
79+ for resource in resources:
80+ label = resource.get_latest_complete_set().label
81+ purposes = [factory.make_name('purpose') for _ in range(3)]
82+ arch, subarch = resource.split_arch()
83+ images.extend([
84+ make_rpc_boot_image(
85+ osystem=os, release=series,
86+ architecture=arch, subarchitecture=subarch,
87+ label=label, purpose=purpose)
88+ for purpose in purposes
89+ ])
90+ self.assertItemsEqual(
91+ resources,
92+ BootResource.objects.get_resources_matching_boot_images(images))
93+
94 def test__returns_resource_for_generated_resource(self):
95 resource = factory.make_usable_boot_resource(
96 rtype=BOOT_RESOURCE_TYPE.GENERATED)
97
98=== modified file 'src/maasserver/models/tests/test_bootresourceset.py'
99--- src/maasserver/models/tests/test_bootresourceset.py 2014-09-10 16:20:31 +0000
100+++ src/maasserver/models/tests/test_bootresourceset.py 2014-10-17 01:59:55 +0000
101@@ -132,7 +132,7 @@
102 resource_set, largefile, filename=filetype, filetype=filetype)
103 self.assertEqual(0, resource_set.progress)
104
105- def test_progress_increases_from_0_to_1(self):
106+ def test_progress_increases_from_0_to_100(self):
107 resource = factory.make_BootResource()
108 resource_set = factory.make_BootResourceSet(resource)
109 filetype = BOOT_RESOURCE_FILE_TYPE.ROOT_IMAGE
110@@ -149,7 +149,7 @@
111 stream.write(b"a")
112 current_size += 1
113 self.assertAlmostEqual(
114- total_size / float(current_size),
115+ 100.0 * current_size / float(total_size),
116 resource_set.progress)
117
118 def test_progress_accumulates_all_files(self):
119@@ -174,7 +174,7 @@
120 content=content, size=total_size)
121 factory.make_BootResourceFile(
122 resource_set, largefile, filename=filetype, filetype=filetype)
123- progress = final_total_size / float(final_size)
124+ progress = 100.0 * final_size / float(final_total_size)
125 self.assertAlmostEqual(progress, resource_set.progress)
126
127 def test_complete_returns_false_for_no_files(self):
128
129=== modified file 'src/maasserver/views/images.py'
130--- src/maasserver/views/images.py 2014-10-02 18:55:37 +0000
131+++ src/maasserver/views/images.py 2014-10-17 01:59:55 +0000
132@@ -53,6 +53,7 @@
133 BootSourceCache,
134 BootSourceSelection,
135 Config,
136+ LargeFile,
137 Node,
138 )
139 from maasserver.views import HelpfulDeleteView
140@@ -460,14 +461,149 @@
141 self.add_resource_template_attributes(resource)
142 return resources
143
144+ def is_hwe_resource(self, resource):
145+ """Return True if the resource is an Ubuntu HWE resource."""
146+ if resource.rtype != BOOT_RESOURCE_TYPE.SYNCED:
147+ return False
148+ if not resource.name.startswith('ubuntu/'):
149+ return False
150+ arch, subarch = resource.split_arch()
151+ return subarch.startswith('hwe-')
152+
153+ def pick_latest_datetime(self, time, other_time):
154+ """Return the datetime that is the latest."""
155+ if time is None:
156+ return other_time
157+ return max([time, other_time])
158+
159+ def calculate_unique_size_for_resources(self, resources):
160+ """Return size of all unique largefiles for the given resources."""
161+ shas = set()
162+ size = 0
163+ for resource in resources:
164+ resource_set = resource.get_latest_set()
165+ if resource_set is None:
166+ continue
167+ for rfile in resource_set.files.all():
168+ try:
169+ largefile = rfile.largefile
170+ except LargeFile.DoesNotExist:
171+ continue
172+ if largefile.sha256 not in shas:
173+ size += largefile.total_size
174+ shas.add(largefile.sha256)
175+ return size
176+
177+ def are_all_resources_complete(self, resources):
178+ """Return the complete status for all the given resources."""
179+ for resource in resources:
180+ resource_set = resource.get_latest_set()
181+ if resource_set is None:
182+ return False
183+ if not resource_set.complete:
184+ return False
185+ return True
186+
187+ def get_last_update_for_resources(self, resources):
188+ """Return the latest updated time for all resources."""
189+ last_update = None
190+ for resource in resources:
191+ last_update = self.pick_latest_datetime(
192+ last_update, resource.updated)
193+ resource_set = resource.get_latest_set()
194+ if resource_set is not None:
195+ last_update = self.pick_latest_datetime(
196+ last_update, resource_set.updated)
197+ return last_update
198+
199+ def get_number_of_nodes_for_resources(self, resources):
200+ """Return the number of nodes used by all resources."""
201+ return sum([
202+ self.get_number_of_nodes_deployed_for(resource)
203+ for resource in resources])
204+
205+ def get_progress_for_resources(self, resources):
206+ """Return the overall progress for all resources."""
207+ size = 0
208+ total_size = 0
209+ for resource in resources:
210+ resource_set = resource.get_latest_set()
211+ if resource_set is not None:
212+ size += resource_set.size
213+ total_size += resource_set.total_size
214+ if size <= 0:
215+ # Handle division by zero
216+ return 0
217+ return 100.0 * (size / float(total_size))
218+
219+ def hwes_to_resource(self, hwes):
220+ """Convert the list of hwes into one resource to be used in the UI."""
221+ # Calculate all of the values using all of the hwe resources for
222+ # this combination of resources.
223+ last_update = self.get_last_update_for_resources(hwes)
224+ unique_size = self.calculate_unique_size_for_resources(hwes)
225+ number_of_nodes = self.get_number_of_nodes_for_resources(hwes)
226+ complete = self.are_all_resources_complete(hwes)
227+ progress = self.get_progress_for_resources(hwes)
228+
229+ # Set the computed attributes on the first resource as that will
230+ # be the only one returned to the UI.
231+ resource = hwes[0]
232+ resource.arch, resource.subarch = resource.split_arch()
233+ resource.title = self.get_resource_title(resource)
234+ resource.complete = complete
235+ resource.size = format_size(unique_size)
236+ resource.last_update = last_update
237+ resource.number_of_nodes = number_of_nodes
238+ resource.complete = complete
239+ if not complete:
240+ if progress > 0:
241+ resource.status = "Downloading %3.0f%%" % progress
242+ resource.downloading = True
243+ else:
244+ resource.status = "Queued for download"
245+ resource.downloading = False
246+ else:
247+ # See if all the hwe resources exist on all the clusters.
248+ cluster_has_hwes = any(
249+ hwe in hwes for hwe in self.cluster_resources)
250+ if cluster_has_hwes:
251+ resource.status = "Complete"
252+ resource.downloading = False
253+ else:
254+ resource.complete = False
255+ if self.clusters_syncing:
256+ resource.status = "Syncing to clusters"
257+ resource.downloading = True
258+ else:
259+ resource.status = "Waiting for clusters to sync"
260+ resource.downloading = False
261+ return resource
262+
263+ def combine_hwe_resources(self, resources):
264+ """Return a list of resources removing the duplicate hwe resources."""
265+ none_hwe_resources = []
266+ hwe_resources = defaultdict(list)
267+ for resource in resources:
268+ if not self.is_hwe_resource(resource):
269+ self.add_resource_template_attributes(resource)
270+ none_hwe_resources.append(resource)
271+ else:
272+ arch = resource.split_arch()[0]
273+ key = '%s/%s' % (resource.name, arch)
274+ hwe_resources[key].append(resource)
275+ combined_hwes = [
276+ self.hwes_to_resource(hwes)
277+ for _, hwes in hwe_resources.items()
278+ ]
279+ return none_hwe_resources + combined_hwes
280+
281 def ajax(self, request, *args, **kwargs):
282 """Return all resources in a json object.
283
284 This is used by the image model list on the client side to update
285 the status of images."""
286- resources = list(BootResource.objects.all())
287- for resource in resources:
288- self.add_resource_template_attributes(resource)
289+ resources = self.combine_hwe_resources(BootResource.objects.all())
290 json_resources = [
291 dict(
292 id=resource.id,
293
294=== modified file 'src/maasserver/views/tests/test_images.py'
295--- src/maasserver/views/tests/test_images.py 2014-10-02 18:55:37 +0000
296+++ src/maasserver/views/tests/test_images.py 2014-10-17 01:59:55 +0000
297@@ -25,6 +25,7 @@
298 NODE_STATUS,
299 )
300 from maasserver.models import (
301+ BootResource,
302 BootSourceCache,
303 BootSourceSelection,
304 Config,
305@@ -37,6 +38,7 @@
306 )
307 from maasserver.testing.testcase import MAASServerTestCase
308 from maasserver.views import images as images_view
309+from maasserver.views.images import format_size
310 from maastesting.matchers import (
311 MockCalledOnceWith,
312 MockCalledWith,
313@@ -688,6 +690,195 @@
314 json_resource = json_obj['resources'][0]
315 self.assertEqual(number_of_nodes, json_resource['numberOfNodes'])
316
317+ def test_combines_hwe_resources_into_one_resource(self):
318+ self.client_log_in()
319+ name = 'ubuntu/%s' % factory.make_name('series')
320+ arch = factory.make_name('arch')
321+ subarches = [factory.make_name('hwe') for _ in range(3)]
322+ for subarch in subarches:
323+ factory.make_usable_boot_resource(
324+ rtype=BOOT_RESOURCE_TYPE.SYNCED,
325+ name=name, architecture='%s/%s' % (arch, subarch))
326+ response = self.get_images_ajax()
327+ json_obj = json.loads(response.content)
328+ self.assertEqual(
329+ 1, len(json_obj['resources']),
330+ 'More than one resource was returned.')
331+
332+ def test_combined_hwe_resource_calculates_unique_size(self):
333+ self.client_log_in()
334+ name = 'ubuntu/%s' % factory.make_name('series')
335+ arch = factory.make_name('arch')
336+ subarches = [factory.make_name('hwe') for _ in range(3)]
337+ largefile = factory.make_LargeFile()
338+ for subarch in subarches:
339+ resource = factory.make_BootResource(
340+ rtype=BOOT_RESOURCE_TYPE.SYNCED,
341+ name=name, architecture='%s/%s' % (arch, subarch))
342+ resource_set = factory.make_BootResourceSet(resource)
343+ factory.make_BootResourceFile(resource_set, largefile)
344+ response = self.get_images_ajax()
345+ json_obj = json.loads(response.content)
346+ json_resource = json_obj['resources'][0]
347+ self.assertEqual(
348+ format_size(largefile.total_size), json_resource['size'])
349+
350+ def test_combined_hwe_resource_calculates_number_of_nodes_deployed(self):
351+ self.client_log_in()
352+ osystem = 'ubuntu'
353+ series = factory.make_name('series')
354+ name = '%s/%s' % (osystem, series)
355+ arch = factory.make_name('arch')
356+ subarches = [factory.make_name('hwe') for _ in range(3)]
357+ for subarch in subarches:
358+ factory.make_usable_boot_resource(
359+ rtype=BOOT_RESOURCE_TYPE.SYNCED,
360+ name=name, architecture='%s/%s' % (arch, subarch))
361+
362+ number_of_nodes = random.randint(1, 4)
363+ for _ in range(number_of_nodes):
364+ subarch = random.choice(subarches)
365+ node_architecture = '%s/%s' % (arch, subarch)
366+ factory.make_Node(
367+ status=NODE_STATUS.DEPLOYED,
368+ osystem=osystem, distro_series=series,
369+ architecture=node_architecture)
370+
371+ response = self.get_images_ajax()
372+ json_obj = json.loads(response.content)
373+ json_resource = json_obj['resources'][0]
374+ self.assertEqual(number_of_nodes, json_resource['numberOfNodes'])
375+
376+ def test_combined_hwe_resource_calculates_complete_True(self):
377+ self.client_log_in()
378+ name = 'ubuntu/%s' % factory.make_name('series')
379+ arch = factory.make_name('arch')
380+ subarches = [factory.make_name('hwe') for _ in range(3)]
381+ resources = [
382+ factory.make_usable_boot_resource(
383+ rtype=BOOT_RESOURCE_TYPE.SYNCED,
384+ name=name, architecture='%s/%s' % (arch, subarch))
385+ for subarch in subarches
386+ ]
387+ self.patch(
388+ BootResource.objects,
389+ 'get_resources_matching_boot_images').return_value = resources
390+ response = self.get_images_ajax()
391+ json_obj = json.loads(response.content)
392+ json_resource = json_obj['resources'][0]
393+ self.assertTrue(json_resource['complete'])
394+
395+ def test_combined_hwe_resource_calculates_complete_False(self):
396+ self.client_log_in()
397+ name = 'ubuntu/%s' % factory.make_name('series')
398+ arch = factory.make_name('arch')
399+ subarches = [factory.make_name('hwe') for _ in range(3)]
400+ incomplete_subarch = subarches.pop()
401+ factory.make_BootResource(
402+ rtype=BOOT_RESOURCE_TYPE.SYNCED,
403+ name=name, architecture='%s/%s' % (arch, incomplete_subarch))
404+ for subarch in subarches:
405+ factory.make_usable_boot_resource(
406+ rtype=BOOT_RESOURCE_TYPE.SYNCED,
407+ name=name, architecture='%s/%s' % (arch, subarch))
408+ response = self.get_images_ajax()
409+ json_obj = json.loads(response.content)
410+ json_resource = json_obj['resources'][0]
411+ self.assertFalse(json_resource['complete'])
412+
413+ def test_combined_hwe_resource_calculates_progress(self):
414+ self.client_log_in()
415+ name = 'ubuntu/%s' % factory.make_name('series')
416+ arch = factory.make_name('arch')
417+ subarches = [factory.make_name('hwe') for _ in range(3)]
418+ largefile = factory.make_LargeFile()
419+ largefile.total_size = largefile.total_size * 2
420+ largefile.save()
421+ for subarch in subarches:
422+ resource = factory.make_BootResource(
423+ rtype=BOOT_RESOURCE_TYPE.SYNCED,
424+ name=name, architecture='%s/%s' % (arch, subarch))
425+ resource_set = factory.make_BootResourceSet(resource)
426+ factory.make_BootResourceFile(resource_set, largefile)
427+ response = self.get_images_ajax()
428+ json_obj = json.loads(response.content)
429+ json_resource = json_obj['resources'][0]
430+ self.assertEqual("Downloading 50%", json_resource['status'])
431+
432+ def test_combined_hwe_resource_shows_queued_if_no_progress(self):
433+ self.client_log_in()
434+ name = 'ubuntu/%s' % factory.make_name('series')
435+ arch = factory.make_name('arch')
436+ subarches = [factory.make_name('hwe') for _ in range(3)]
437+ largefile = factory.make_LargeFile(content="")
438+ for subarch in subarches:
439+ resource = factory.make_BootResource(
440+ rtype=BOOT_RESOURCE_TYPE.SYNCED,
441+ name=name, architecture='%s/%s' % (arch, subarch))
442+ resource_set = factory.make_BootResourceSet(resource)
443+ factory.make_BootResourceFile(resource_set, largefile)
444+ response = self.get_images_ajax()
445+ json_obj = json.loads(response.content)
446+ json_resource = json_obj['resources'][0]
447+ self.assertEqual("Queued for download", json_resource['status'])
448+
449+ def test_combined_hwe_resource_shows_complete_status(self):
450+ self.client_log_in()
451+ name = 'ubuntu/%s' % factory.make_name('series')
452+ arch = factory.make_name('arch')
453+ subarches = [factory.make_name('hwe') for _ in range(3)]
454+ resources = [
455+ factory.make_usable_boot_resource(
456+ rtype=BOOT_RESOURCE_TYPE.SYNCED,
457+ name=name, architecture='%s/%s' % (arch, subarch))
458+ for subarch in subarches
459+ ]
460+ self.patch(
461+ BootResource.objects,
462+ 'get_resources_matching_boot_images').return_value = resources
463+ response = self.get_images_ajax()
464+ json_obj = json.loads(response.content)
465+ json_resource = json_obj['resources'][0]
466+ self.assertEqual("Complete", json_resource['status'])
467+
468+ def test_combined_hwe_resource_shows_waiting_for_cluster_to_sync(self):
469+ self.client_log_in()
470+ name = 'ubuntu/%s' % factory.make_name('series')
471+ arch = factory.make_name('arch')
472+ subarches = [factory.make_name('hwe') for _ in range(3)]
473+ for subarch in subarches:
474+ factory.make_usable_boot_resource(
475+ rtype=BOOT_RESOURCE_TYPE.SYNCED,
476+ name=name, architecture='%s/%s' % (arch, subarch))
477+ self.patch(
478+ BootResource.objects,
479+ 'get_resources_matching_boot_images').return_value = []
480+ response = self.get_images_ajax()
481+ json_obj = json.loads(response.content)
482+ json_resource = json_obj['resources'][0]
483+ self.assertEqual(
484+ "Waiting for clusters to sync", json_resource['status'])
485+
486+ def test_combined_hwe_resource_shows_clusters_syncing(self):
487+ self.client_log_in()
488+ name = 'ubuntu/%s' % factory.make_name('series')
489+ arch = factory.make_name('arch')
490+ subarches = [factory.make_name('hwe') for _ in range(3)]
491+ for subarch in subarches:
492+ factory.make_usable_boot_resource(
493+ rtype=BOOT_RESOURCE_TYPE.SYNCED,
494+ name=name, architecture='%s/%s' % (arch, subarch))
495+ self.patch(
496+ BootResource.objects,
497+ 'get_resources_matching_boot_images').return_value = []
498+ self.patch(
499+ images_view, 'is_import_boot_images_running').return_value = True
500+ response = self.get_images_ajax()
501+ json_obj = json.loads(response.content)
502+ json_resource = json_obj['resources'][0]
503+ self.assertEqual(
504+ "Syncing to clusters", json_resource['status'])
505+
506
507 class TestImageDelete(MAASServerTestCase):
508
509
510=== modified file 'src/provisioningserver/import_images/download_descriptions.py'
511--- src/provisioningserver/import_images/download_descriptions.py 2014-09-30 20:54:32 +0000
512+++ src/provisioningserver/import_images/download_descriptions.py 2014-10-17 01:59:55 +0000
513@@ -78,14 +78,12 @@
514 """Overridable from `BasicMirrorWriter`."""
515 item = products_exdata(src, pedigree)
516 os = get_os_from_product(item)
517- arch, subarches = item['arch'], item['subarches']
518+ arch, subarch = item['arch'], item['subarch']
519 release = item['release']
520 label = item['label']
521- base_image = ImageSpec(os, arch, None, release, label)
522+ base_image = ImageSpec(os, arch, subarch, release, label)
523 compact_item = clean_up_repo_item(item)
524- for subarch in subarches.split(','):
525- self.boot_images_dict.setdefault(
526- base_image._replace(subarch=subarch), compact_item)
527+ self.boot_images_dict.setdefault(base_image, compact_item)
528
529
530 def value_passes_filter_list(filter_list, property_value):

Subscribers

People subscribed via source and target branches

to all changes: