Add pagination support to the volume page

Cinder v2 supports pagination.
Added to the volumes table for both admin and project.
Also fix the Cinder REST API to handle pagination (like Glance
REST API).

To test: set 'Items Per Page' in the UI Settings page to a low number.

Co-Authored-By: Cindy Lu <clu@us.ibm.com>
Co-Authored-By: Timur Sufiev <tsufiev@mirantis.com>

Change-Id: Ib1772d6e6214dc96a09ce32fb4d9f9fb79d161f0
Closes-Bug: #1316793
This commit is contained in:
Mingyan Bao 2014-09-02 15:35:17 -04:00 committed by Timur Sufiev
parent 31b92c7a3b
commit 091d351553
10 changed files with 543 additions and 53 deletions

View File

@ -30,6 +30,7 @@ from cinderclient import exceptions as cinder_exception
from cinderclient.v2.contrib import list_extensions as cinder_list_extensions from cinderclient.v2.contrib import list_extensions as cinder_list_extensions
from horizon import exceptions from horizon import exceptions
from horizon.utils import functions as utils
from horizon.utils.memoized import memoized # noqa from horizon.utils.memoized import memoized # noqa
from openstack_dashboard.api import base from openstack_dashboard.api import base
@ -194,25 +195,59 @@ def version_get():
return api_version['version'] return api_version['version']
def volume_list(request, search_opts=None): def volume_list(request, search_opts=None, marker=None, sort_dir="desc"):
volumes, _, __ = volume_list_paged(
request, search_opts=search_opts, marker=marker, paginate=False,
sort_dir=sort_dir)
return volumes
def volume_list_paged(request, search_opts=None, marker=None, paginate=False,
sort_dir="desc"):
"""To see all volumes in the cloud as an admin you can pass in a special """To see all volumes in the cloud as an admin you can pass in a special
search option: {'all_tenants': 1} search option: {'all_tenants': 1}
""" """
has_more_data = False
has_prev_data = False
volumes = []
c_client = cinderclient(request) c_client = cinderclient(request)
if c_client is None: if c_client is None:
return [] return volumes, has_more_data, has_prev_data
# build a dictionary of volume_id -> transfer # build a dictionary of volume_id -> transfer
transfers = {t.volume_id: t transfers = {t.volume_id: t
for t in transfer_list(request, search_opts=search_opts)} for t in transfer_list(request, search_opts=search_opts)}
volumes = [] if VERSIONS.active > 1 and paginate:
for v in c_client.volumes.list(search_opts=search_opts): page_size = utils.get_page_size(request)
v.transfer = transfers.get(v.id) # sort_key and sort_dir deprecated in kilo, use sort
volumes.append(Volume(v)) # if pagination is true, we use a single sort parameter
# by default, it is "created_at"
sort = 'created_at:' + sort_dir
for v in c_client.volumes.list(search_opts=search_opts,
limit=page_size + 1,
marker=marker,
sort=sort):
v.transfer = transfers.get(v.id)
volumes.append(Volume(v))
if len(volumes) > page_size:
has_more_data = True
volumes.pop()
if marker is not None:
has_prev_data = True
# first page condition when reached via prev back
elif sort_dir == 'asc' and marker is not None:
has_more_data = True
# last page condition
elif marker is not None:
has_prev_data = True
else:
for v in c_client.volumes.list(search_opts=search_opts):
v.transfer = transfers.get(v.id)
volumes.append(Volume(v))
return volumes return volumes, has_more_data, has_prev_data
def volume_get(request, volume_id): def volume_get(request, volume_id):

View File

@ -22,6 +22,9 @@ from openstack_dashboard.api.rest import utils as rest_utils
from openstack_dashboard.api.rest import urls from openstack_dashboard.api.rest import urls
CLIENT_KEYWORDS = {'marker', 'sort_dir', 'paginate'}
@urls.register @urls.register
class Volumes(generic.View): class Volumes(generic.View):
"""API for cinder volumes. """API for cinder volumes.
@ -33,26 +36,43 @@ class Volumes(generic.View):
"""Get a detailed list of volumes associated with the current user's """Get a detailed list of volumes associated with the current user's
project. project.
Example GET:
http://localhost/api/cinder/volumes?paginate=true&sort_dir=asc #flake8: noqa
If invoked as an admin, you may set the GET parameter "all_projects" If invoked as an admin, you may set the GET parameter "all_projects"
to 'true'. to 'true' to return details for all projects.
The following get parameters may be passed in the GET The following get parameters may be passed in the GET
:param search_opts: include options such as name, status, bootable :param search_opts: includes options such as name, status, bootable
:param paginate: If true will perform pagination based on settings.
:param marker: Specifies the namespace of the last-seen image.
The typical pattern of limit and marker is to make an
initial limited request and then to use the last
namespace from the response as the marker parameter
in a subsequent limited request. With paginate, limit
is automatically set.
:param sort_dir: The sort direction ('asc' or 'desc').
The listing result is an object with property "items". The listing result is an object with property "items".
""" """
# TODO(clu_): when v2 pagination stuff in Cinder API merges
# (https://review.openstack.org/#/c/118450), handle here accordingly
if request.GET.get('all_projects') == 'true': if request.GET.get('all_projects') == 'true':
result = api.cinder.volume_list(request, {'all_tenants': 1}) result, has_more, has_prev = api.cinder.volume_list_paged(
else:
result = api.cinder.volume_list(
request, request,
search_opts=rest_utils.parse_filters_kwargs(request)[0] {'all_tenants': 1}
) )
return {'items': [u.to_dict() for u in result]} else:
search_opts, kwargs = rest_utils.parse_filters_kwargs(request, CLIENT_KEYWORDS)
result, has_more, has_prev = api.cinder.volume_list_paged(
request,
search_opts=search_opts, **kwargs
)
return {
'items': [u.to_dict() for u in result],
'has_more_data': has_more,
'has_prev_data': has_prev
}
@rest_utils.ajax(data_required=True) @rest_utils.ajax(data_required=True)
def post(self, request): def post(self, request):

View File

@ -61,6 +61,12 @@ class VolumeTab(tabs.TableTab, volumes_tabs.VolumeTableMixIn):
return volumes return volumes
def has_prev_data(self, table):
return self._has_prev_data
def has_more_data(self, table):
return self._has_more_data
class VolumeTypesTab(tabs.TableTab, volumes_tabs.VolumeTableMixIn): class VolumeTypesTab(tabs.TableTab, volumes_tabs.VolumeTableMixIn):
table_classes = (volume_types_tables.VolumeTypesTable, table_classes = (volume_types_tables.VolumeTypesTable,

View File

@ -12,24 +12,34 @@
# License for the specific language governing permissions and limitations # License for the specific language governing permissions and limitations
# under the License. # under the License.
from django.conf import settings
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from django import http from django import http
from django.test.utils import override_settings
from mox3.mox import IsA # noqa from mox3.mox import IsA # noqa
from openstack_dashboard import api from openstack_dashboard import api
from openstack_dashboard.api import cinder from openstack_dashboard.api import cinder
from openstack_dashboard.api import keystone from openstack_dashboard.api import keystone
from openstack_dashboard.dashboards.project.volumes.volumes \
import tables as volume_tables
from openstack_dashboard.test import helpers as test from openstack_dashboard.test import helpers as test
INDEX_URL = reverse('horizon:admin:volumes:index')
class VolumeTests(test.BaseAdminViewTests): class VolumeTests(test.BaseAdminViewTests):
@test.create_stubs({api.nova: ('server_list',), @test.create_stubs({api.nova: ('server_list',),
cinder: ('volume_list', cinder: ('volume_list_paged',
'volume_snapshot_list'), 'volume_snapshot_list'),
keystone: ('tenant_list',)}) keystone: ('tenant_list',)})
def test_index(self): def test_index(self):
cinder.volume_list(IsA(http.HttpRequest), search_opts={ cinder.volume_list_paged(IsA(http.HttpRequest), sort_dir="desc",
'all_tenants': True}).AndReturn(self.cinder_volumes.list()) marker=None, paginate=True,
search_opts={'all_tenants': True})\
.AndReturn([self.cinder_volumes.list(), False, False])
cinder.volume_snapshot_list(IsA(http.HttpRequest), search_opts={ cinder.volume_snapshot_list(IsA(http.HttpRequest), search_opts={
'all_tenants': True}).AndReturn([]) 'all_tenants': True}).AndReturn([])
api.nova.server_list(IsA(http.HttpRequest), search_opts={ api.nova.server_list(IsA(http.HttpRequest), search_opts={
@ -39,12 +49,100 @@ class VolumeTests(test.BaseAdminViewTests):
.AndReturn([self.tenants.list(), False]) .AndReturn([self.tenants.list(), False])
self.mox.ReplayAll() self.mox.ReplayAll()
res = self.client.get(reverse('horizon:admin:volumes:index')) res = self.client.get(INDEX_URL)
self.assertTemplateUsed(res, 'admin/volumes/index.html') self.assertTemplateUsed(res, 'admin/volumes/index.html')
volumes = res.context['volumes_table'].data volumes = res.context['volumes_table'].data
self.assertItemsEqual(volumes, self.cinder_volumes.list()) self.assertItemsEqual(volumes, self.cinder_volumes.list())
@test.create_stubs({api.nova: ('server_list',),
cinder: ('volume_list_paged',),
keystone: ('tenant_list',)})
def _test_index_paginated(self, marker, sort_dir, volumes, url,
has_more, has_prev):
cinder.volume_list_paged(IsA(http.HttpRequest), sort_dir=sort_dir,
marker=marker, paginate=True,
search_opts={'all_tenants': True}) \
.AndReturn([volumes, has_more, has_prev])
api.nova.server_list(IsA(http.HttpRequest), search_opts={
'all_tenants': True}) \
.AndReturn([self.servers.list(), False])
keystone.tenant_list(IsA(http.HttpRequest)) \
.AndReturn([self.tenants.list(), False])
self.mox.ReplayAll()
res = self.client.get(url)
self.assertTemplateUsed(res, 'admin/volumes/index.html')
self.assertEqual(res.status_code, 200)
self.mox.UnsetStubs()
return res
@override_settings(API_RESULT_PAGE_SIZE=2)
def test_index_paginated(self):
size = settings.API_RESULT_PAGE_SIZE
mox_volumes = self.cinder_volumes.list()
# get first page
expected_volumes = mox_volumes[:size]
url = INDEX_URL
res = self._test_index_paginated(marker=None, sort_dir="desc",
volumes=expected_volumes, url=url,
has_more=True, has_prev=False)
volumes = res.context['volumes_table'].data
self.assertItemsEqual(volumes, expected_volumes)
# get second page
expected_volumes = mox_volumes[size:2 * size]
marker = expected_volumes[0].id
next = volume_tables.VolumesTable._meta.pagination_param
url = "?".join([INDEX_URL, "=".join([next, marker])])
res = self._test_index_paginated(marker=marker, sort_dir="desc",
volumes=expected_volumes, url=url,
has_more=True, has_prev=True)
volumes = res.context['volumes_table'].data
self.assertItemsEqual(volumes, expected_volumes)
# get last page
expected_volumes = mox_volumes[-size:]
marker = expected_volumes[0].id
next = volume_tables.VolumesTable._meta.pagination_param
url = "?".join([INDEX_URL, "=".join([next, marker])])
res = self._test_index_paginated(marker=marker, sort_dir="desc",
volumes=expected_volumes, url=url,
has_more=False, has_prev=True)
volumes = res.context['volumes_table'].data
self.assertItemsEqual(volumes, expected_volumes)
@override_settings(API_RESULT_PAGE_SIZE=2)
def test_index_paginated_prev(self):
size = settings.API_RESULT_PAGE_SIZE
mox_volumes = self.cinder_volumes.list()
# prev from some page
expected_volumes = mox_volumes[size:2 * size]
marker = mox_volumes[0].id
prev = volume_tables.VolumesTable._meta.prev_pagination_param
url = "?".join([INDEX_URL, "=".join([prev, marker])])
res = self._test_index_paginated(marker=marker, sort_dir="asc",
volumes=expected_volumes, url=url,
has_more=False, has_prev=True)
volumes = res.context['volumes_table'].data
self.assertItemsEqual(volumes, expected_volumes)
# back to first page
expected_volumes = mox_volumes[:size]
marker = mox_volumes[0].id
prev = volume_tables.VolumesTable._meta.prev_pagination_param
url = "?".join([INDEX_URL, "=".join([prev, marker])])
res = self._test_index_paginated(marker=marker, sort_dir="asc",
volumes=expected_volumes, url=url,
has_more=True, has_prev=False)
volumes = res.context['volumes_table'].data
self.assertItemsEqual(volumes, expected_volumes)
@test.create_stubs({cinder: ('volume_type_list_with_qos_associations', @test.create_stubs({cinder: ('volume_type_list_with_qos_associations',
'qos_spec_list', 'qos_spec_list',
'extension_supported', 'extension_supported',

View File

@ -30,10 +30,21 @@ from openstack_dashboard.dashboards.project.volumes.volumes \
class VolumeTableMixIn(object): class VolumeTableMixIn(object):
_has_more_data = False
_has_prev_data = False
def _get_volumes(self, search_opts=None): def _get_volumes(self, search_opts=None):
try: try:
return api.cinder.volume_list(self.request, marker, sort_dir = self._get_marker()
search_opts=search_opts) volumes, self._has_more_data, self._has_prev_data = \
api.cinder.volume_list_paged(self.request, marker=marker,
search_opts=search_opts,
sort_dir=sort_dir, paginate=True)
if sort_dir == "asc":
volumes.reverse()
return volumes
except Exception: except Exception:
exceptions.handle(self.request, exceptions.handle(self.request,
_('Unable to retrieve volume list.')) _('Unable to retrieve volume list.'))
@ -78,6 +89,18 @@ class VolumeTableMixIn(object):
server_id = att.get('server_id', None) server_id = att.get('server_id', None)
att['instance'] = instances.get(server_id, None) att['instance'] = instances.get(server_id, None)
def _get_marker(self):
prev_marker = self.request.GET.get(
volume_tables.VolumesTable._meta.prev_pagination_param, None)
if prev_marker:
return prev_marker, "asc"
else:
marker = self.request.GET.get(
volume_tables.VolumesTable._meta.pagination_param, None)
if marker:
return marker, "desc"
return None, "desc"
class VolumeTab(tabs.TableTab, VolumeTableMixIn): class VolumeTab(tabs.TableTab, VolumeTableMixIn):
table_classes = (volume_tables.VolumesTable,) table_classes = (volume_tables.VolumesTable,)
@ -94,6 +117,12 @@ class VolumeTab(tabs.TableTab, VolumeTableMixIn):
volumes, instances, volume_ids_with_snapshots) volumes, instances, volume_ids_with_snapshots)
return volumes return volumes
def has_prev_data(self, table):
return self._has_prev_data
def has_more_data(self, table):
return self._has_more_data
class SnapshotTab(tabs.TableTab): class SnapshotTab(tabs.TableTab):
table_classes = (vol_snapshot_tables.VolumeSnapshotsTable,) table_classes = (vol_snapshot_tables.VolumeSnapshotsTable,)

View File

@ -12,12 +12,16 @@
# License for the specific language governing permissions and limitations # License for the specific language governing permissions and limitations
# under the License. # under the License.
from django.conf import settings
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from django import http from django import http
from django.test.utils import override_settings
from mox3.mox import IsA # noqa from mox3.mox import IsA # noqa
from openstack_dashboard import api from openstack_dashboard import api
from openstack_dashboard.dashboards.project.volumes.volumes \
import tables as volume_tables
from openstack_dashboard.test import helpers as test from openstack_dashboard.test import helpers as test
@ -29,6 +33,7 @@ VOLUME_BACKUPS_TAB_URL = reverse('horizon:project:volumes:backups_tab')
class VolumeAndSnapshotsTests(test.TestCase): class VolumeAndSnapshotsTests(test.TestCase):
@test.create_stubs({api.cinder: ('tenant_absolute_limits', @test.create_stubs({api.cinder: ('tenant_absolute_limits',
'volume_list', 'volume_list',
'volume_list_paged',
'volume_snapshot_list', 'volume_snapshot_list',
'volume_backup_supported', 'volume_backup_supported',
'volume_backup_list', 'volume_backup_list',
@ -41,8 +46,10 @@ class VolumeAndSnapshotsTests(test.TestCase):
api.cinder.volume_backup_supported(IsA(http.HttpRequest)).\ api.cinder.volume_backup_supported(IsA(http.HttpRequest)).\
MultipleTimes().AndReturn(backup_supported) MultipleTimes().AndReturn(backup_supported)
api.cinder.volume_list(IsA(http.HttpRequest), search_opts=None).\ api.cinder.volume_list_paged(IsA(http.HttpRequest), marker=None,
AndReturn(volumes) sort_dir='desc', search_opts=None,
paginate=True).\
AndReturn([volumes, False, False])
api.nova.server_list(IsA(http.HttpRequest), search_opts=None).\ api.nova.server_list(IsA(http.HttpRequest), search_opts=None).\
AndReturn([self.servers.list(), False]) AndReturn([self.servers.list(), False])
api.cinder.volume_snapshot_list( api.cinder.volume_snapshot_list(
@ -77,3 +84,94 @@ class VolumeAndSnapshotsTests(test.TestCase):
def test_index_backup_not_supported(self): def test_index_backup_not_supported(self):
self._test_index(backup_supported=False) self._test_index(backup_supported=False)
@test.create_stubs({api.cinder: ('tenant_absolute_limits',
'volume_list_paged',
'volume_backup_supported',
),
api.nova: ('server_list',)})
def _test_index_paginated(self, marker, sort_dir, volumes, url,
has_more, has_prev):
backup_supported = True
api.cinder.volume_backup_supported(IsA(http.HttpRequest)).\
MultipleTimes().AndReturn(backup_supported)
api.cinder.volume_list_paged(IsA(http.HttpRequest), marker=marker,
sort_dir=sort_dir, search_opts=None,
paginate=True).\
AndReturn([volumes, has_more, has_prev])
api.nova.server_list(IsA(http.HttpRequest), search_opts=None).\
AndReturn([self.servers.list(), False])
api.cinder.tenant_absolute_limits(IsA(http.HttpRequest)).MultipleTimes().\
AndReturn(self.cinder_limits['absolute'])
self.mox.ReplayAll()
res = self.client.get(url)
self.assertEqual(res.status_code, 200)
self.assertTemplateUsed(res, 'project/volumes/index.html')
self.mox.UnsetStubs()
return res
@override_settings(API_RESULT_PAGE_SIZE=2)
def test_index_paginated(self):
mox_volumes = self.cinder_volumes.list()
size = settings.API_RESULT_PAGE_SIZE
# get first page
expected_volumes = mox_volumes[:size]
url = INDEX_URL
res = self._test_index_paginated(marker=None, sort_dir="desc",
volumes=expected_volumes, url=url,
has_more=True, has_prev=False)
volumes = res.context['volumes_table'].data
self.assertItemsEqual(volumes, expected_volumes)
# get second page
expected_volumes = mox_volumes[size:2 * size]
marker = expected_volumes[0].id
next = volume_tables.VolumesTable._meta.pagination_param
url = "?".join([INDEX_URL, "=".join([next, marker])])
res = self._test_index_paginated(marker=marker, sort_dir="desc",
volumes=expected_volumes, url=url,
has_more=True, has_prev=True)
volumes = res.context['volumes_table'].data
self.assertItemsEqual(volumes, expected_volumes)
# get last page
expected_volumes = mox_volumes[-size:]
marker = expected_volumes[0].id
next = volume_tables.VolumesTable._meta.pagination_param
url = "?".join([INDEX_URL, "=".join([next, marker])])
res = self._test_index_paginated(marker=marker, sort_dir="desc",
volumes=expected_volumes, url=url,
has_more=False, has_prev=True)
volumes = res.context['volumes_table'].data
self.assertItemsEqual(volumes, expected_volumes)
@override_settings(API_RESULT_PAGE_SIZE=2)
def test_index_paginated_prev_page(self):
mox_volumes = self.cinder_volumes.list()
size = settings.API_RESULT_PAGE_SIZE
# prev from some page
expected_volumes = mox_volumes[size:2 * size]
marker = expected_volumes[0].id
prev = volume_tables.VolumesTable._meta.prev_pagination_param
url = "?".join([INDEX_URL, "=".join([prev, marker])])
res = self._test_index_paginated(marker=marker, sort_dir="asc",
volumes=expected_volumes, url=url,
has_more=True, has_prev=True)
volumes = res.context['volumes_table'].data
self.assertItemsEqual(volumes, expected_volumes)
# back to first page
expected_volumes = mox_volumes[:size]
marker = expected_volumes[0].id
prev = volume_tables.VolumesTable._meta.prev_pagination_param
url = "?".join([INDEX_URL, "=".join([prev, marker])])
res = self._test_index_paginated(marker=marker, sort_dir="asc",
volumes=expected_volumes, url=url,
has_more=True, has_prev=False)
volumes = res.context['volumes_table'].data
self.assertItemsEqual(volumes, expected_volumes)

View File

@ -42,6 +42,7 @@ class VolumeViewTests(test.TestCase):
'volume_type_list', 'volume_type_list',
'volume_type_default', 'volume_type_default',
'volume_list', 'volume_list',
'volume_list_paged',
'availability_zone_list', 'availability_zone_list',
'extension_supported'), 'extension_supported'),
api.glance: ('image_list_detailed',), api.glance: ('image_list_detailed',),
@ -853,7 +854,7 @@ class VolumeViewTests(test.TestCase):
self.assertEqual(res.context['form'].errors['__all__'], expected_error) self.assertEqual(res.context['form'].errors['__all__'], expected_error)
@test.create_stubs({cinder: ('tenant_absolute_limits', @test.create_stubs({cinder: ('tenant_absolute_limits',
'volume_list', 'volume_list_paged',
'volume_snapshot_list', 'volume_snapshot_list',
'volume_backup_supported', 'volume_backup_supported',
'volume_delete',), 'volume_delete',),
@ -866,16 +867,18 @@ class VolumeViewTests(test.TestCase):
cinder.volume_backup_supported(IsA(http.HttpRequest)). \ cinder.volume_backup_supported(IsA(http.HttpRequest)). \
MultipleTimes().AndReturn(True) MultipleTimes().AndReturn(True)
cinder.volume_list(IsA(http.HttpRequest), search_opts=None).\ cinder.volume_list_paged(
AndReturn(volumes) IsA(http.HttpRequest), marker=None, paginate=True, sort_dir='desc',
search_opts=None).AndReturn([volumes, False, False])
cinder.volume_snapshot_list(IsA(http.HttpRequest), cinder.volume_snapshot_list(IsA(http.HttpRequest),
search_opts=None).\ search_opts=None).\
AndReturn([]) AndReturn([])
cinder.volume_delete(IsA(http.HttpRequest), volume.id) cinder.volume_delete(IsA(http.HttpRequest), volume.id)
api.nova.server_list(IsA(http.HttpRequest), search_opts=None).\ api.nova.server_list(IsA(http.HttpRequest), search_opts=None).\
AndReturn([self.servers.list(), False]) AndReturn([self.servers.list(), False])
cinder.volume_list(IsA(http.HttpRequest), search_opts=None).\ cinder.volume_list_paged(
AndReturn(volumes) IsA(http.HttpRequest), marker=None, paginate=True, sort_dir='desc',
search_opts=None).AndReturn([volumes, False, False])
cinder.volume_snapshot_list(IsA(http.HttpRequest), cinder.volume_snapshot_list(IsA(http.HttpRequest),
search_opts=None).\ search_opts=None).\
AndReturn([]) AndReturn([])
@ -1086,7 +1089,7 @@ class VolumeViewTests(test.TestCase):
'The create snapshot button should be disabled') 'The create snapshot button should be disabled')
@test.create_stubs({cinder: ('tenant_absolute_limits', @test.create_stubs({cinder: ('tenant_absolute_limits',
'volume_list', 'volume_list_paged',
'volume_snapshot_list', 'volume_snapshot_list',
'volume_backup_supported',), 'volume_backup_supported',),
api.nova: ('server_list',)}) api.nova: ('server_list',)})
@ -1098,8 +1101,9 @@ class VolumeViewTests(test.TestCase):
api.cinder.volume_backup_supported(IsA(http.HttpRequest)). \ api.cinder.volume_backup_supported(IsA(http.HttpRequest)). \
MultipleTimes().AndReturn(True) MultipleTimes().AndReturn(True)
cinder.volume_list(IsA(http.HttpRequest), search_opts=None)\ cinder.volume_list_paged(IsA(http.HttpRequest), sort_dir='desc',
.AndReturn(volumes) marker=None, paginate=True, search_opts=None)\
.AndReturn([volumes, False, False])
cinder.volume_snapshot_list(IsA(http.HttpRequest), cinder.volume_snapshot_list(IsA(http.HttpRequest),
search_opts=None).\ search_opts=None).\
AndReturn([]) AndReturn([])
@ -1127,7 +1131,7 @@ class VolumeViewTests(test.TestCase):
create_action.policy_rules) create_action.policy_rules)
@test.create_stubs({cinder: ('tenant_absolute_limits', @test.create_stubs({cinder: ('tenant_absolute_limits',
'volume_list', 'volume_list_paged',
'volume_snapshot_list', 'volume_snapshot_list',
'volume_backup_supported',), 'volume_backup_supported',),
api.nova: ('server_list',)}) api.nova: ('server_list',)})
@ -1138,8 +1142,9 @@ class VolumeViewTests(test.TestCase):
api.cinder.volume_backup_supported(IsA(http.HttpRequest)). \ api.cinder.volume_backup_supported(IsA(http.HttpRequest)). \
MultipleTimes().AndReturn(True) MultipleTimes().AndReturn(True)
cinder.volume_list(IsA(http.HttpRequest), search_opts=None)\ cinder.volume_list_paged(
.AndReturn(volumes) IsA(http.HttpRequest), marker=None, paginate=True, sort_dir='desc',
search_opts=None).AndReturn([volumes, False, False])
cinder.volume_snapshot_list(IsA(http.HttpRequest), cinder.volume_snapshot_list(IsA(http.HttpRequest),
search_opts=None).\ search_opts=None).\
AndReturn([]) AndReturn([])
@ -1515,7 +1520,7 @@ class VolumeViewTests(test.TestCase):
def test_encryption_true(self): def test_encryption_true(self):
self._test_encryption(True) self._test_encryption(True)
@test.create_stubs({cinder: ('volume_list', @test.create_stubs({cinder: ('volume_list_paged',
'volume_snapshot_list', 'volume_snapshot_list',
'volume_backup_supported', 'volume_backup_supported',
'tenant_absolute_limits'), 'tenant_absolute_limits'),
@ -1528,8 +1533,11 @@ class VolumeViewTests(test.TestCase):
cinder.volume_backup_supported(IsA(http.HttpRequest))\ cinder.volume_backup_supported(IsA(http.HttpRequest))\
.MultipleTimes('backup_supported').AndReturn(False) .MultipleTimes('backup_supported').AndReturn(False)
cinder.volume_list(IsA(http.HttpRequest), search_opts=None)\
.AndReturn(self.volumes.list()) cinder.volume_list_paged(
IsA(http.HttpRequest), marker=None, sort_dir='desc',
search_opts=None, paginate=True)\
.AndReturn([self.volumes.list(), False, False])
cinder.volume_snapshot_list(IsA(http.HttpRequest), cinder.volume_snapshot_list(IsA(http.HttpRequest),
search_opts=None).\ search_opts=None).\
AndReturn(self.cinder_volume_snapshots.list()) AndReturn(self.cinder_volume_snapshots.list())
@ -1580,7 +1588,7 @@ class VolumeViewTests(test.TestCase):
"only have 80GiB of your quota available.") "only have 80GiB of your quota available.")
@test.create_stubs({cinder: ('volume_backup_supported', @test.create_stubs({cinder: ('volume_backup_supported',
'volume_list', 'volume_list_paged',
'volume_snapshot_list', 'volume_snapshot_list',
'tenant_absolute_limits'), 'tenant_absolute_limits'),
api.nova: ('server_list',)}) api.nova: ('server_list',)})
@ -1589,8 +1597,10 @@ class VolumeViewTests(test.TestCase):
cinder.volume_backup_supported(IsA(http.HttpRequest))\ cinder.volume_backup_supported(IsA(http.HttpRequest))\
.MultipleTimes().AndReturn(False) .MultipleTimes().AndReturn(False)
cinder.volume_list(IsA(http.HttpRequest), search_opts=None)\ cinder.volume_list_paged(
.AndReturn(self.volumes.list()) IsA(http.HttpRequest), marker=None, sort_dir='desc',
search_opts=None, paginate=True)\
.AndReturn([self.volumes.list(), False, False])
cinder.volume_snapshot_list(IsA(http.HttpRequest), cinder.volume_snapshot_list(IsA(http.HttpRequest),
search_opts=None).\ search_opts=None).\
AndReturn([]) AndReturn([])
@ -1632,7 +1642,7 @@ class VolumeViewTests(test.TestCase):
self.assertNoFormErrors(res) self.assertNoFormErrors(res)
@test.create_stubs({cinder: ('volume_backup_supported', @test.create_stubs({cinder: ('volume_backup_supported',
'volume_list', 'volume_list_paged',
'volume_snapshot_list', 'volume_snapshot_list',
'transfer_delete', 'transfer_delete',
'tenant_absolute_limits'), 'tenant_absolute_limits'),
@ -1652,8 +1662,10 @@ class VolumeViewTests(test.TestCase):
cinder.volume_backup_supported(IsA(http.HttpRequest))\ cinder.volume_backup_supported(IsA(http.HttpRequest))\
.MultipleTimes().AndReturn(False) .MultipleTimes().AndReturn(False)
cinder.volume_list(IsA(http.HttpRequest), search_opts=None)\ cinder.volume_list_paged(
.AndReturn(volumes) IsA(http.HttpRequest), marker=None, search_opts=None,
sort_dir='desc', paginate=True)\
.AndReturn([volumes, False, False])
cinder.volume_snapshot_list(IsA(http.HttpRequest), cinder.volume_snapshot_list(IsA(http.HttpRequest),
search_opts=None).\ search_opts=None).\
AndReturn([]) AndReturn([])

View File

@ -62,6 +62,21 @@
* @param {Object} params * @param {Object} params
* Query parameters. Optional. * Query parameters. Optional.
* *
* @param {boolean} params.paginate
* True to paginate automatically.
*
* @param {string} params.marker
* Specifies the image of the last-seen image.
*
* The typical pattern of limit and marker is to make an
* initial limited request and then to use the last
* image from the response as the marker parameter
* in a subsequent limited request. With paginate, limit
* is automatically set.
*
* @param {string} params.sort_dir
* The sort direction ('asc' or 'desc').
*
* @param {string} param.search_opts * @param {string} param.search_opts
* Filters to pass through the API. * Filters to pass through the API.
* For example, "status": "available" will show all available volumes. * For example, "status": "available" will show all available volumes.

View File

@ -42,20 +42,22 @@ class CinderRestTestCase(test.TestCase):
request = self.mock_rest_request(GET={'all_projects': 'true'}) request = self.mock_rest_request(GET={'all_projects': 'true'})
else: else:
request = self.mock_rest_request(**{'GET': filters}) request = self.mock_rest_request(**{'GET': filters})
cc.volume_list.return_value = [ cc.volume_list_paged.return_value = [
mock.Mock(**{'to_dict.return_value': {'id': 'one'}}), mock.Mock(**{'to_dict.return_value': {'id': 'one'}}),
mock.Mock(**{'to_dict.return_value': {'id': 'two'}}), mock.Mock(**{'to_dict.return_value': {'id': 'two'}}),
] ], False, False
response = cinder.Volumes().get(request) response = cinder.Volumes().get(request)
self.assertStatusCode(response, 200) self.assertStatusCode(response, 200)
self.assertEqual(response.json, self.assertEqual(response.json,
{"items": [{"id": "one"}, {"id": "two"}]}) {"items": [{"id": "one"}, {"id": "two"}],
"has_more_data": False,
"has_prev_data": False})
if all: if all:
cc.volume_list.assert_called_once_with(request, cc.volume_list_paged.assert_called_once_with(request,
{'all_tenants': 1}) {'all_tenants': 1})
else: else:
cc.volume_list.assert_called_once_with(request, cc.volume_list_paged.assert_called_once_with(request,
search_opts=filters) search_opts=filters)
@mock.patch.object(cinder.api, 'cinder') @mock.patch.object(cinder.api, 'cinder')
def test_volume_get(self, cc): def test_volume_get(self, cc):

View File

@ -12,6 +12,7 @@
# License for the specific language governing permissions and limitations # License for the specific language governing permissions and limitations
# under the License. # under the License.
from django.conf import settings
from django.test.utils import override_settings from django.test.utils import override_settings
import six import six
@ -37,8 +38,182 @@ class CinderApiTests(test.APITestCase):
search_opts=search_opts,).AndReturn(volume_transfers) search_opts=search_opts,).AndReturn(volume_transfers)
self.mox.ReplayAll() self.mox.ReplayAll()
# No assertions are necessary. Verification is handled by mox. api_volumes = api.cinder.volume_list(self.request,
api.cinder.volume_list(self.request, search_opts=search_opts) search_opts=search_opts)
self.assertEqual(len(volumes), len(api_volumes))
def test_volume_list_paged(self):
search_opts = {'all_tenants': 1}
detailed = True
volumes = self.cinder_volumes.list()
volume_transfers = self.cinder_volume_transfers.list()
cinderclient = self.stub_cinderclient()
cinderclient.volumes = self.mox.CreateMockAnything()
cinderclient.volumes.list(search_opts=search_opts,).AndReturn(volumes)
cinderclient.transfers = self.mox.CreateMockAnything()
cinderclient.transfers.list(
detailed=detailed,
search_opts=search_opts,).AndReturn(volume_transfers)
self.mox.ReplayAll()
api_volumes, has_more, has_prev = api.cinder.volume_list_paged(
self.request, search_opts=search_opts)
self.assertEqual(len(volumes), len(api_volumes))
self.assertFalse(has_more)
self.assertFalse(has_prev)
@override_settings(API_RESULT_PAGE_SIZE=2)
@override_settings(OPENSTACK_API_VERSIONS={'volume': 2})
def test_volume_list_paginate_first_page(self):
api.cinder.VERSIONS._active = None
page_size = settings.API_RESULT_PAGE_SIZE
volumes = self.cinder_volumes.list()
volume_transfers = self.cinder_volume_transfers.list()
search_opts = {'all_tenants': 1}
mox_volumes = volumes[:page_size + 1]
expected_volumes = mox_volumes[:-1]
cinderclient = self.stub_cinderclient()
cinderclient.volumes = self.mox.CreateMockAnything()
cinderclient.volumes.list(search_opts=search_opts, limit=page_size + 1,
sort='created_at:desc', marker=None).\
AndReturn(mox_volumes)
cinderclient.transfers = self.mox.CreateMockAnything()
cinderclient.transfers.list(
detailed=True,
search_opts=search_opts,).AndReturn(volume_transfers)
self.mox.ReplayAll()
api_volumes, more_data, prev_data = api.cinder.volume_list_paged(
self.request, search_opts=search_opts, paginate=True)
self.assertEqual(len(expected_volumes), len(api_volumes))
self.assertTrue(more_data)
self.assertFalse(prev_data)
@override_settings(API_RESULT_PAGE_SIZE=2)
@override_settings(OPENSTACK_API_VERSIONS={'volume': 2})
def test_volume_list_paginate_second_page(self):
api.cinder.VERSIONS._active = None
page_size = settings.API_RESULT_PAGE_SIZE
volumes = self.cinder_volumes.list()
volume_transfers = self.cinder_volume_transfers.list()
search_opts = {'all_tenants': 1}
mox_volumes = volumes[page_size:page_size * 2 + 1]
expected_volumes = mox_volumes[:-1]
marker = expected_volumes[0].id
cinderclient = self.stub_cinderclient()
cinderclient.volumes = self.mox.CreateMockAnything()
cinderclient.volumes.list(search_opts=search_opts, limit=page_size + 1,
sort='created_at:desc', marker=marker).\
AndReturn(mox_volumes)
cinderclient.transfers = self.mox.CreateMockAnything()
cinderclient.transfers.list(
detailed=True,
search_opts=search_opts,).AndReturn(volume_transfers)
self.mox.ReplayAll()
api_volumes, more_data, prev_data = api.cinder.volume_list_paged(
self.request, search_opts=search_opts, marker=marker,
paginate=True)
self.assertEqual(len(expected_volumes), len(api_volumes))
self.assertTrue(more_data)
self.assertTrue(prev_data)
@override_settings(API_RESULT_PAGE_SIZE=2)
@override_settings(OPENSTACK_API_VERSIONS={'volume': 2})
def test_volume_list_paginate_last_page(self):
api.cinder.VERSIONS._active = None
page_size = settings.API_RESULT_PAGE_SIZE
volumes = self.cinder_volumes.list()
volume_transfers = self.cinder_volume_transfers.list()
search_opts = {'all_tenants': 1}
mox_volumes = volumes[-1 * page_size:]
expected_volumes = mox_volumes
marker = expected_volumes[0].id
cinderclient = self.stub_cinderclient()
cinderclient.volumes = self.mox.CreateMockAnything()
cinderclient.volumes.list(search_opts=search_opts, limit=page_size + 1,
sort='created_at:desc', marker=marker).\
AndReturn(mox_volumes)
cinderclient.transfers = self.mox.CreateMockAnything()
cinderclient.transfers.list(
detailed=True,
search_opts=search_opts,).AndReturn(volume_transfers)
self.mox.ReplayAll()
api_volumes, more_data, prev_data = api.cinder.volume_list_paged(
self.request, search_opts=search_opts, marker=marker,
paginate=True)
self.assertEqual(len(expected_volumes), len(api_volumes))
self.assertFalse(more_data)
self.assertTrue(prev_data)
@override_settings(API_RESULT_PAGE_SIZE=2)
@override_settings(OPENSTACK_API_VERSIONS={'volume': 2})
def test_volume_list_paginate_back_from_some_page(self):
api.cinder.VERSIONS._active = None
page_size = settings.API_RESULT_PAGE_SIZE
volumes = self.cinder_volumes.list()
volume_transfers = self.cinder_volume_transfers.list()
search_opts = {'all_tenants': 1}
mox_volumes = volumes[page_size:page_size * 2 + 1]
expected_volumes = mox_volumes[:-1]
marker = expected_volumes[0].id
cinderclient = self.stub_cinderclient()
cinderclient.volumes = self.mox.CreateMockAnything()
cinderclient.volumes.list(search_opts=search_opts, limit=page_size + 1,
sort='created_at:asc', marker=marker).\
AndReturn(mox_volumes)
cinderclient.transfers = self.mox.CreateMockAnything()
cinderclient.transfers.list(
detailed=True,
search_opts=search_opts,).AndReturn(volume_transfers)
self.mox.ReplayAll()
api_volumes, more_data, prev_data = api.cinder.volume_list_paged(
self.request, search_opts=search_opts, sort_dir="asc",
marker=marker, paginate=True)
self.assertEqual(len(expected_volumes), len(api_volumes))
self.assertTrue(more_data)
self.assertTrue(prev_data)
@override_settings(API_RESULT_PAGE_SIZE=2)
@override_settings(OPENSTACK_API_VERSIONS={'volume': 2})
def test_volume_list_paginate_back_to_first_page(self):
api.cinder.VERSIONS._active = None
page_size = settings.API_RESULT_PAGE_SIZE
volumes = self.cinder_volumes.list()
volume_transfers = self.cinder_volume_transfers.list()
search_opts = {'all_tenants': 1}
mox_volumes = volumes[:page_size]
expected_volumes = mox_volumes
marker = expected_volumes[0].id
cinderclient = self.stub_cinderclient()
cinderclient.volumes = self.mox.CreateMockAnything()
cinderclient.volumes.list(search_opts=search_opts, limit=page_size + 1,
sort='created_at:asc', marker=marker).\
AndReturn(mox_volumes)
cinderclient.transfers = self.mox.CreateMockAnything()
cinderclient.transfers.list(
detailed=True,
search_opts=search_opts,).AndReturn(volume_transfers)
self.mox.ReplayAll()
api_volumes, more_data, prev_data = api.cinder.volume_list_paged(
self.request, search_opts=search_opts, sort_dir="asc",
marker=marker, paginate=True)
self.assertEqual(len(expected_volumes), len(api_volumes))
self.assertTrue(more_data)
self.assertFalse(prev_data)
def test_volume_snapshot_list(self): def test_volume_snapshot_list(self):
search_opts = {'all_tenants': 1} search_opts = {'all_tenants': 1}