Browse Source

Adds pagination to Glance API and tables.

Fixes bug 981252.

Change-Id: Ib1fa6136947a23521dcdbf6d0d7ae783a6e0fae7
Gabriel Hurley 7 years ago
parent
commit
259d3d573f

+ 20
- 4
horizon/api/glance.py View File

@@ -23,6 +23,8 @@ from __future__ import absolute_import
23 23
 import logging
24 24
 import urlparse
25 25
 
26
+from django.conf import settings
27
+
26 28
 from glanceclient.v1 import client as glance_client
27 29
 
28 30
 from horizon.api.base import url_for
@@ -51,16 +53,30 @@ def image_get(request, image_id):
51 53
     return glanceclient(request).images.get(image_id)
52 54
 
53 55
 
54
-def image_list_detailed(request, filters=None):
56
+def image_list_detailed(request, marker=None, filters=None):
55 57
     filters = filters or {}
56
-    return glanceclient(request).images.list(filters=filters)
58
+    limit = getattr(settings, 'API_RESULT_LIMIT', 1000)
59
+    images = glanceclient(request).images.list(limit=limit + 1,
60
+                                               marker=marker,
61
+                                               filters=filters)
62
+    if(len(images) > limit):
63
+        return (images[0:-1], True)
64
+    else:
65
+        return (images, False)
57 66
 
58 67
 
59 68
 def image_update(request, image_id, **kwargs):
60 69
     return glanceclient(request).images.update(image_id, **kwargs)
61 70
 
62 71
 
63
-def snapshot_list_detailed(request, extra_filters=None):
72
+def snapshot_list_detailed(request, marker=None, extra_filters=None):
64 73
     filters = {'property-image_type': 'snapshot'}
65 74
     filters.update(extra_filters or {})
66
-    return glanceclient(request).images.list(filters=filters)
75
+    limit = getattr(settings, 'API_RESULT_LIMIT', 1000)
76
+    images = glanceclient(request).images.list(limit=limit + 1,
77
+                                               marker=marker,
78
+                                               filters=filters)
79
+    if(len(images) > limit):
80
+        return (images[0:-1], True)
81
+    else:
82
+        return (images, False)

+ 0
- 1
horizon/dashboards/nova/containers/tables.py View File

@@ -17,7 +17,6 @@
17 17
 import logging
18 18
 
19 19
 from cloudfiles.errors import ContainerNotEmpty
20
-from django import shortcuts
21 20
 from django.contrib import messages
22 21
 from django.core.urlresolvers import reverse
23 22
 from django.template.defaultfilters import filesizeformat

+ 1
- 0
horizon/dashboards/nova/images_and_snapshots/images/tables.py View File

@@ -102,3 +102,4 @@ class ImagesTable(tables.DataTable):
102 102
         verbose_name = _("Images")
103 103
         table_actions = (DeleteImage,)
104 104
         row_actions = (LaunchImage, EditImage, DeleteImage)
105
+        pagination_param = "image_marker"

+ 1
- 0
horizon/dashboards/nova/images_and_snapshots/snapshots/tables.py View File

@@ -54,3 +54,4 @@ class SnapshotsTable(ImagesTable):
54 54
         verbose_name = _("Instance Snapshots")
55 55
         table_actions = (DeleteSnapshot,)
56 56
         row_actions = (LaunchSnapshot, EditImage, DeleteSnapshot)
57
+        pagination_param = "snapshot_marker"

+ 16
- 12
horizon/dashboards/nova/images_and_snapshots/tests.py View File

@@ -40,8 +40,10 @@ class ImagesAndSnapshotsTests(test.TestCase):
40 40
         self.mox.StubOutWithMock(api, 'volume_snapshot_list')
41 41
         api.volume_snapshot_list(IsA(http.HttpRequest)) \
42 42
                                 .AndReturn(self.volumes.list())
43
-        api.image_list_detailed(IsA(http.HttpRequest)).AndReturn(images)
44
-        api.snapshot_list_detailed(IsA(http.HttpRequest)).AndReturn(snapshots)
43
+        api.image_list_detailed(IsA(http.HttpRequest),
44
+                                marker=None).AndReturn([images, False])
45
+        api.snapshot_list_detailed(IsA(http.HttpRequest),
46
+                                marker=None).AndReturn([snapshots, False])
45 47
         self.mox.ReplayAll()
46 48
 
47 49
         res = self.client.get(INDEX_URL)
@@ -58,9 +60,10 @@ class ImagesAndSnapshotsTests(test.TestCase):
58 60
         self.mox.StubOutWithMock(api, 'volume_snapshot_list')
59 61
         api.volume_snapshot_list(IsA(http.HttpRequest)) \
60 62
                                 .AndReturn(self.volumes.list())
61
-        api.image_list_detailed(IsA(http.HttpRequest)).AndReturn([])
62
-        api.snapshot_list_detailed(IsA(http.HttpRequest)) \
63
-                                   .AndReturn(self.snapshots.list())
63
+        api.image_list_detailed(IsA(http.HttpRequest),
64
+                                marker=None).AndReturn([(), False])
65
+        api.snapshot_list_detailed(IsA(http.HttpRequest), marker=None) \
66
+                                .AndReturn([self.snapshots.list(), False])
64 67
         self.mox.ReplayAll()
65 68
 
66 69
         res = self.client.get(INDEX_URL)
@@ -72,10 +75,10 @@ class ImagesAndSnapshotsTests(test.TestCase):
72 75
         self.mox.StubOutWithMock(api, 'volume_snapshot_list')
73 76
         api.volume_snapshot_list(IsA(http.HttpRequest)) \
74 77
                                 .AndReturn(self.volumes.list())
75
-        api.image_list_detailed(IsA(http.HttpRequest)) \
76
-                                .AndRaise(self.exceptions.glance)
77
-        api.snapshot_list_detailed(IsA(http.HttpRequest)) \
78
-                                   .AndReturn(self.snapshots.list())
78
+        api.image_list_detailed(IsA(http.HttpRequest),
79
+                                marker=None).AndRaise(self.exceptions.glance)
80
+        api.snapshot_list_detailed(IsA(http.HttpRequest), marker=None) \
81
+                                .AndReturn([self.snapshots.list(), False])
79 82
         self.mox.ReplayAll()
80 83
 
81 84
         res = self.client.get(INDEX_URL)
@@ -102,9 +105,10 @@ class ImagesAndSnapshotsTests(test.TestCase):
102 105
         self.mox.StubOutWithMock(api, 'volume_snapshot_list')
103 106
         api.volume_snapshot_list(IsA(http.HttpRequest)) \
104 107
                                 .AndReturn(self.volumes.list())
105
-        api.image_list_detailed(IsA(http.HttpRequest)).AndReturn(images)
106
-        api.snapshot_list_detailed(IsA(http.HttpRequest)).\
107
-                AndReturn(new_snapshots)
108
+        api.image_list_detailed(IsA(http.HttpRequest),
109
+                                marker=None).AndReturn([images, False])
110
+        api.snapshot_list_detailed(IsA(http.HttpRequest), marker=None) \
111
+                                .AndReturn([new_snapshots, False])
108 112
         self.mox.ReplayAll()
109 113
 
110 114
         res = self.client.get(INDEX_URL)

+ 15
- 5
horizon/dashboards/nova/images_and_snapshots/views.py View File

@@ -42,9 +42,16 @@ class IndexView(tables.MultiTableView):
42 42
     table_classes = (ImagesTable, SnapshotsTable, VolumeSnapshotsTable)
43 43
     template_name = 'nova/images_and_snapshots/index.html'
44 44
 
45
+    def has_more_data(self, table):
46
+        return getattr(self, "_more_%s" % table.name, False)
47
+
45 48
     def get_images_data(self):
49
+        marker = self.request.GET.get(ImagesTable._meta.pagination_param, None)
46 50
         try:
47
-            all_images = api.image_list_detailed(self.request)
51
+            # FIXME(gabriel): The paging is going to be strange here due to
52
+            # our filtering after the fact.
53
+            all_images, _more_images = api.image_list_detailed(self.request,
54
+                                                                marker=marker)
48 55
             images = [im for im in all_images
49 56
                       if im.container_format not in ['aki', 'ari'] and
50 57
                       im.properties.get("image_type", '') != "snapshot"]
@@ -54,12 +61,15 @@ class IndexView(tables.MultiTableView):
54 61
         return images
55 62
 
56 63
     def get_snapshots_data(self):
64
+        req = self.request
65
+        marker = req.GET.get(SnapshotsTable._meta.pagination_param, None)
57 66
         try:
58
-            snapshots = api.snapshot_list_detailed(self.request)
67
+            snaps, self._more_snapshots = api.snapshot_list_detailed(req,
68
+                                                                marker=marker)
59 69
         except:
60
-            snapshots = []
61
-            exceptions.handle(self.request, _("Unable to retrieve snapshots."))
62
-        return snapshots
70
+            snaps = []
71
+            exceptions.handle(req, _("Unable to retrieve snapshots."))
72
+        return snaps
63 73
 
64 74
     def get_volume_snapshots_data(self):
65 75
         try:

+ 17
- 15
horizon/dashboards/nova/instances_and_volumes/instances/tests.py View File

@@ -329,8 +329,10 @@ class InstanceViewTests(test.TestCase):
329 329
                             server.id,
330 330
                             "snapshot1")
331 331
         api.server_get(IsA(http.HttpRequest), server.id).AndReturn(server)
332
-        api.snapshot_list_detailed(IsA(http.HttpRequest)).AndReturn([])
333
-        api.image_list_detailed(IsA(http.HttpRequest)).AndReturn([])
332
+        api.snapshot_list_detailed(IsA(http.HttpRequest),
333
+                                marker=None).AndReturn([[], False])
334
+        api.image_list_detailed(IsA(http.HttpRequest),
335
+                                marker=None).AndReturn([[], False])
334 336
         api.volume_snapshot_list(IsA(http.HttpRequest)).AndReturn([])
335 337
 
336 338
         api.volume_list(IsA(http.HttpRequest)).AndReturn(self.volumes.list())
@@ -414,7 +416,6 @@ class InstanceViewTests(test.TestCase):
414 416
         self.assertRedirectsNoFollow(res, INDEX_URL)
415 417
 
416 418
     def test_launch_get(self):
417
-        image = self.images.first()
418 419
         quota_usages = self.quota_usages.first()
419 420
 
420 421
         self.mox.StubOutWithMock(api.glance, 'image_list_detailed')
@@ -432,10 +433,10 @@ class InstanceViewTests(test.TestCase):
432 433
                                 .AndReturn(self.volumes.list())
433 434
         api.glance.image_list_detailed(IsA(http.HttpRequest),
434 435
                                        filters={'is_public': True}) \
435
-                  .AndReturn(self.images.list())
436
+                  .AndReturn([self.images.list(), False])
436 437
         api.glance.image_list_detailed(IsA(http.HttpRequest),
437 438
                             filters={'property-owner_id': self.tenant.id}) \
438
-                  .AndReturn([])
439
+                  .AndReturn([[], False])
439 440
         api.nova.tenant_quota_usages(IsA(http.HttpRequest)) \
440 441
                 .AndReturn(quota_usages)
441 442
         api.nova.flavor_list(IsA(http.HttpRequest)) \
@@ -489,10 +490,10 @@ class InstanceViewTests(test.TestCase):
489 490
                 .AndReturn(self.security_groups.list())
490 491
         api.glance.image_list_detailed(IsA(http.HttpRequest),
491 492
                                        filters={'is_public': True}) \
492
-                  .AndReturn(self.images.list())
493
+                  .AndReturn([self.images.list(), False])
493 494
         api.glance.image_list_detailed(IsA(http.HttpRequest),
494 495
                             filters={'property-owner_id': self.tenant.id}) \
495
-                  .AndReturn([])
496
+                  .AndReturn([[], False])
496 497
         api.nova.volume_list(IsA(http.HttpRequest)) \
497 498
                 .AndReturn(self.volumes.list())
498 499
         api.nova.volume_snapshot_list(IsA(http.HttpRequest)).AndReturn([])
@@ -541,10 +542,10 @@ class InstanceViewTests(test.TestCase):
541 542
                 .AndReturn(self.volumes.list())
542 543
         api.glance.image_list_detailed(IsA(http.HttpRequest),
543 544
                                        filters={'is_public': True}) \
544
-                  .AndReturn(self.images.list())
545
+                  .AndReturn([self.images.list(), False])
545 546
         api.glance.image_list_detailed(IsA(http.HttpRequest),
546 547
                             filters={'property-owner_id': self.tenant.id}) \
547
-                  .AndReturn([])
548
+                  .AndReturn([[], False])
548 549
         api.nova.tenant_quota_usages(IsA(http.HttpRequest)) \
549 550
                 .AndReturn(self.quota_usages.first())
550 551
         api.nova.flavor_list(IsA(http.HttpRequest)) \
@@ -586,10 +587,10 @@ class InstanceViewTests(test.TestCase):
586 587
                                 .AndReturn(self.security_groups.list())
587 588
         api.glance.image_list_detailed(IsA(http.HttpRequest),
588 589
                                        filters={'is_public': True}) \
589
-                  .AndReturn(self.images.list())
590
+                  .AndReturn([self.images.list(), False])
590 591
         api.glance.image_list_detailed(IsA(http.HttpRequest),
591 592
                             filters={'property-owner_id': self.tenant.id}) \
592
-                  .AndReturn([])
593
+                  .AndReturn([[], False])
593 594
         api.nova.volume_list(IgnoreArg()).AndReturn(self.volumes.list())
594 595
         api.nova.server_create(IsA(http.HttpRequest),
595 596
                                server.name,
@@ -637,8 +638,6 @@ class InstanceViewTests(test.TestCase):
637 638
         self.mox.StubOutWithMock(api.nova, 'volume_snapshot_list')
638 639
         self.mox.StubOutWithMock(api.nova, 'tenant_quota_usages')
639 640
 
640
-        api.nova.flavor_list(IsA(http.HttpRequest)) \
641
-                .AndReturn(self.flavors.list())
642 641
         api.nova.flavor_list(IsA(http.HttpRequest)) \
643 642
                 .AndReturn(self.flavors.list())
644 643
         api.nova.keypair_list(IsA(http.HttpRequest)) \
@@ -647,13 +646,16 @@ class InstanceViewTests(test.TestCase):
647 646
                 .AndReturn(self.security_groups.list())
648 647
         api.glance.image_list_detailed(IsA(http.HttpRequest),
649 648
                                        filters={'is_public': True}) \
650
-                  .AndReturn(self.images.list())
649
+                  .AndReturn([self.images.list(), False])
651 650
         api.glance.image_list_detailed(IsA(http.HttpRequest),
652 651
                             filters={'property-owner_id': self.tenant.id}) \
653
-                  .AndReturn([])
652
+                  .AndReturn([[], False])
654 653
         api.nova.volume_list(IsA(http.HttpRequest)) \
655 654
                 .AndReturn(self.volumes.list())
656 655
         api.nova.volume_snapshot_list(IsA(http.HttpRequest)).AndReturn([])
656
+
657
+        api.nova.flavor_list(IsA(http.HttpRequest)) \
658
+                .AndReturn(self.flavors.list())
657 659
         api.nova.tenant_quota_usages(IsA(http.HttpRequest)) \
658 660
                 .AndReturn(self.quota_usages.first())
659 661
         self.mox.ReplayAll()

+ 6
- 6
horizon/dashboards/nova/instances_and_volumes/instances/workflows.py View File

@@ -204,7 +204,7 @@ class SetInstanceDetailsAction(workflows.Action):
204 204
         project_id = context.get('project_id', None)
205 205
         if not hasattr(self, "_public_images"):
206 206
             public = {"is_public": True}
207
-            public_images = api.glance.image_list_detailed(request,
207
+            public_images, _more = api.glance.image_list_detailed(request,
208 208
                                                            filters=public)
209 209
             self._public_images = public_images
210 210
 
@@ -214,7 +214,7 @@ class SetInstanceDetailsAction(workflows.Action):
214 214
 
215 215
         if not hasattr(self, "_images_for_%s" % project_id):
216 216
             owner = {"property-owner_id": project_id}
217
-            owned_images = api.glance.image_list_detailed(request,
217
+            owned_images, _more = api.glance.image_list_detailed(request,
218 218
                                                           filters=owner)
219 219
             setattr(self, "_images_for_%s" % project_id, owned_images)
220 220
 
@@ -223,12 +223,12 @@ class SetInstanceDetailsAction(workflows.Action):
223 223
 
224 224
         # Remove duplicate images.
225 225
         image_ids = []
226
+        final_images = []
226 227
         for image in images:
227 228
             if image.id not in image_ids:
228 229
                 image_ids.append(image.id)
229
-            else:
230
-                images.remove(image)
231
-        return [image for image in images
230
+                final_images.append(image)
231
+        return [image for image in final_images
232 232
                 if image.container_format not in ('aki', 'ari')]
233 233
 
234 234
     def populate_image_id_choices(self, request, context):
@@ -250,7 +250,7 @@ class SetInstanceDetailsAction(workflows.Action):
250 250
         if choices:
251 251
             choices.insert(0, ("", _("Select Instance Snapshot")))
252 252
         else:
253
-            choices.insert(0, ("", _("No images available.")))
253
+            choices.insert(0, ("", _("No snapshots available.")))
254 254
         return choices
255 255
 
256 256
     def populate_flavor_choices(self, request, context):

+ 8
- 1
horizon/dashboards/syspanel/images/views.py View File

@@ -37,11 +37,18 @@ class IndexView(tables.DataTableView):
37 37
     table_class = AdminImagesTable
38 38
     template_name = 'syspanel/images/index.html'
39 39
 
40
+    def has_more_data(self, table):
41
+        return self._more
42
+
40 43
     def get_data(self):
41 44
         images = []
45
+        marker = self.request.GET.get(AdminImagesTable._meta.pagination_param,
46
+                                      None)
42 47
         try:
43
-            images = api.image_list_detailed(self.request)
48
+            images, self._more = api.image_list_detailed(self.request,
49
+                                                         marker=marker)
44 50
         except:
51
+            self._more = False
45 52
             msg = _('Unable to retrieve image list.')
46 53
             exceptions.handle(self.request, msg)
47 54
         return images

+ 12
- 0
horizon/tables/base.py View File

@@ -563,6 +563,13 @@ class DataTableOptions(object):
563 563
         The name of the context variable which will contain the table when
564 564
         it is rendered. Defaults to ``"table"``.
565 565
 
566
+    .. attribute:: pagination_param
567
+
568
+        The name of the query string parameter which will be used when
569
+        paginating this table. When using multiple tables in a single
570
+        view this will need to be changed to differentiate between the
571
+        tables. Default: ``"marker"``.
572
+
566 573
     .. attribute:: status_columns
567 574
 
568 575
         A list or tuple of column names which represents the "state"
@@ -597,6 +604,7 @@ class DataTableOptions(object):
597 604
         self.row_actions = getattr(options, 'row_actions', [])
598 605
         self.row_class = getattr(options, 'row_class', Row)
599 606
         self.column_class = getattr(options, 'column_class', Column)
607
+        self.pagination_param = getattr(options, 'pagination_param', 'marker')
600 608
 
601 609
         # Set self.filter if we have any FilterActions
602 610
         filter_actions = [action for action in self.table_actions if
@@ -1043,6 +1051,10 @@ class DataTable(object):
1043 1051
         """
1044 1052
         return http.urlquote_plus(self.get_object_id(self.data[-1]))
1045 1053
 
1054
+    def get_pagination_string(self):
1055
+        """ Returns the query parameter string to paginate this table. """
1056
+        return "=".join([self._meta.pagination_param, self.get_marker()])
1057
+
1046 1058
     def calculate_row_status(self, statuses):
1047 1059
         """
1048 1060
         Returns a boolean value determining the overall row status

+ 1
- 1
horizon/templates/horizon/common/_data_table.html View File

@@ -31,7 +31,7 @@
31 31
           <span>{% blocktrans count counter=rows|length %}Displaying {{ counter }} item{% plural %}Displaying {{ counter }} items{% endblocktrans %}</span>
32 32
           {% if table.has_more_data %}
33 33
           <span class="spacer">|</span>
34
-          <a href="?marker={{ table.get_marker }}">More&nbsp;&raquo;</a>
34
+          <a href="?{{ table.get_pagination_string }}">More&nbsp;&raquo;</a>
35 35
           {% endif %}
36 36
         </td>
37 37
       </td>

+ 6
- 1
horizon/tests/api_tests/glance_tests.py View File

@@ -18,6 +18,8 @@
18 18
 #    License for the specific language governing permissions and limitations
19 19
 #    under the License.
20 20
 
21
+from django.conf import settings
22
+
21 23
 from horizon import api
22 24
 from horizon import test
23 25
 
@@ -26,10 +28,13 @@ class GlanceApiTests(test.APITestCase):
26 28
     def test_snapshot_list_detailed(self):
27 29
         images = self.images.list()
28 30
         filters = {'property-image_type': 'snapshot'}
31
+        limit = getattr(settings, 'API_RESULT_LIMIT', 1000)
29 32
 
30 33
         glanceclient = self.stub_glanceclient()
31 34
         glanceclient.images = self.mox.CreateMockAnything()
32
-        glanceclient.images.list(filters=filters).AndReturn(images)
35
+        glanceclient.images.list(filters=filters,
36
+                                 limit=limit + 1,
37
+                                 marker=None).AndReturn(images)
33 38
         self.mox.ReplayAll()
34 39
 
35 40
         # No assertions are necessary. Verification is handled by mox.

Loading…
Cancel
Save