diff --git a/api-ref/source/v3/ext-backups.inc b/api-ref/source/v3/ext-backups.inc index 74645cd52b9..b1aa071c2a5 100644 --- a/api-ref/source/v3/ext-backups.inc +++ b/api-ref/source/v3/ext-backups.inc @@ -62,6 +62,7 @@ Request - limit: limit - offset: offset - marker: marker + - with_count: with_count Response Parameters @@ -88,6 +89,7 @@ Response Parameters - data_timestamp: data_timestamp - snapshot_id: snapshot_id_2 - os-backup-project-attr:project_id: os-backup-project-attr:project_id + - count: count Response Example ---------------- @@ -329,6 +331,7 @@ Request - sort: sort - limit: limit - marker: marker + - with_count: with_count Response Parameters ------------------- @@ -339,6 +342,7 @@ Response Parameters - id: id_1 - links: links_1 - name: name_1 + - count: count Response Example ---------------- diff --git a/api-ref/source/v3/parameters.yaml b/api-ref/source/v3/parameters.yaml index e76e95b05e2..5314ee25dbc 100644 --- a/api-ref/source/v3/parameters.yaml +++ b/api-ref/source/v3/parameters.yaml @@ -398,6 +398,13 @@ vol_type_id_query: in: query required: true type: string +with_count: + description: | + Whether to show ``count`` in API response or not, default is ``False``. + in: query + required: false + type: boolean + min_version: 3.45 # variables in body absolute: @@ -740,6 +747,13 @@ control_location: in: body required: false type: string +count: + description: | + The total count of requested resource before pagination is applied. + in: body + required: false + type: integer + min_version: 3.45 create-from-src: description: | The create from source action. diff --git a/api-ref/source/v3/samples/backups-list-detailed-response.json b/api-ref/source/v3/samples/backups-list-detailed-response.json index f6e8bef5a79..fa37f029980 100644 --- a/api-ref/source/v3/samples/backups-list-detailed-response.json +++ b/api-ref/source/v3/samples/backups-list-detailed-response.json @@ -54,5 +54,6 @@ "is_incremental": true, "has_dependent_backups": false } - ] + ], + "count": 10 } diff --git a/api-ref/source/v3/samples/backups-list-response.json b/api-ref/source/v3/samples/backups-list-response.json index 5c818b16aeb..9095716fe4a 100644 --- a/api-ref/source/v3/samples/backups-list-response.json +++ b/api-ref/source/v3/samples/backups-list-response.json @@ -56,5 +56,6 @@ "id": "4dbf0ec2-0b57-4669-9823-9f7c76f2b4f8", "size": 1 } - ] + ], + "count": 10 } diff --git a/api-ref/source/v3/samples/snapshots-list-detailed-response.json b/api-ref/source/v3/samples/snapshots-list-detailed-response.json index f077cf629f8..9dece0a8e16 100644 --- a/api-ref/source/v3/samples/snapshots-list-detailed-response.json +++ b/api-ref/source/v3/samples/snapshots-list-detailed-response.json @@ -15,5 +15,6 @@ "id": "b1323cda-8e4b-41c1-afc5-2fc791809c8c", "description": "volume snapshot" } - ] + ], + "count": 10 } diff --git a/api-ref/source/v3/samples/snapshots-list-response.json b/api-ref/source/v3/samples/snapshots-list-response.json index 8ba90085cb8..0e77609c0cc 100644 --- a/api-ref/source/v3/samples/snapshots-list-response.json +++ b/api-ref/source/v3/samples/snapshots-list-response.json @@ -16,5 +16,6 @@ "id": "b1323cda-8e4b-41c1-afc5-2fc791809c8c", "description": "volume snapshot" } - ] + ], + "count": 10 } diff --git a/api-ref/source/v3/samples/volumes-list-detailed-response.json b/api-ref/source/v3/samples/volumes-list-detailed-response.json index 28768fd30e9..2d190beecd3 100644 --- a/api-ref/source/v3/samples/volumes-list-detailed-response.json +++ b/api-ref/source/v3/samples/volumes-list-detailed-response.json @@ -98,5 +98,6 @@ "created_at": "2015-11-29T02:25:18.000000", "volume_type": "lvmdriver-1" } - ] + ], + "count": 10 } diff --git a/api-ref/source/v3/samples/volumes-list-response.json b/api-ref/source/v3/samples/volumes-list-response.json index eb4ebafa103..96ad313c197 100644 --- a/api-ref/source/v3/samples/volumes-list-response.json +++ b/api-ref/source/v3/samples/volumes-list-response.json @@ -28,5 +28,6 @@ ], "name": "vol-003" } - ] + ], + "count": 10 } diff --git a/api-ref/source/v3/volumes-v3-snapshots.inc b/api-ref/source/v3/volumes-v3-snapshots.inc index 34455904d62..e717fb80de4 100644 --- a/api-ref/source/v3/volumes-v3-snapshots.inc +++ b/api-ref/source/v3/volumes-v3-snapshots.inc @@ -59,6 +59,7 @@ Request - limit: limit - offset: offset - marker: marker + - with_count: with_count Response Parameters @@ -77,6 +78,7 @@ Response Parameters - size: size - id: id - metadata: metadata + - count: count Response Example ---------------- @@ -164,6 +166,7 @@ Request - limit: limit - offset: offset - marker: marker + - with_count: with_count Response Parameters @@ -180,6 +183,7 @@ Response Parameters - metadata: metadata - id: id - size: size + - count: count Response Example ---------------- diff --git a/api-ref/source/v3/volumes-v3-volumes.inc b/api-ref/source/v3/volumes-v3-volumes.inc index 8f2424c05a3..e696dcc392d 100644 --- a/api-ref/source/v3/volumes-v3-volumes.inc +++ b/api-ref/source/v3/volumes-v3-volumes.inc @@ -86,6 +86,7 @@ Request - limit: limit - offset: offset - marker: marker + - with_count: with_count Response Parameters @@ -121,6 +122,7 @@ Response Parameters - os-volume-replication:driver_data: os-volume-replication:driver_data - volumes: volumes - volume_type: volume_type + - count: count @@ -260,6 +262,7 @@ Request - limit: limit - offset: offset - marker: marker + - with_count: with_count Response Parameters @@ -271,6 +274,7 @@ Response Parameters - id: id_5 - links: links_3 - name: name_13 + - count: count diff --git a/cinder/api/contrib/backups.py b/cinder/api/contrib/backups.py index 80c03bc635b..84a3e2d3616 100644 --- a/cinder/api/contrib/backups.py +++ b/cinder/api/contrib/backups.py @@ -30,6 +30,7 @@ from cinder import backup as backupAPI from cinder import exception from cinder.i18n import _ from cinder import utils +from cinder import volume as volumeAPI LOG = logging.getLogger(__name__) @@ -41,6 +42,7 @@ class BackupsController(wsgi.Controller): def __init__(self): self.backup_api = backupAPI.API() + self.volume_api = volumeAPI.API() super(BackupsController, self).__init__() def show(self, req, id): @@ -100,6 +102,10 @@ class BackupsController(wsgi.Controller): marker, limit, offset = common.get_pagination_params(filters) sort_keys, sort_dirs = common.get_sort_params(filters) + show_count = False + if req_version.matches(mv.SUPPORT_COUNT_INFO): + show_count = utils.get_bool_param('with_count', filters) + filters.pop('with_count') self._convert_sort_name(req_version, sort_keys) self._process_backup_filtering(context=context, filters=filters, req_version=req_version) @@ -107,7 +113,7 @@ class BackupsController(wsgi.Controller): if 'name' in filters: filters['display_name'] = filters.pop('name') - backups = self.backup_api.get_all(context, search_opts=filters, + backups = self.backup_api.get_all(context, search_opts=filters.copy(), marker=marker, limit=limit, offset=offset, @@ -115,12 +121,18 @@ class BackupsController(wsgi.Controller): sort_dirs=sort_dirs, ) + total_count = None + if show_count: + total_count = self.volume_api.calculate_resource_count( + context, 'backup', filters) req.cache_db_backups(backups.objects) if is_detail: - backups = self._view_builder.detail_list(req, backups.objects) + backups = self._view_builder.detail_list(req, backups.objects, + total_count) else: - backups = self._view_builder.summary_list(req, backups.objects) + backups = self._view_builder.summary_list(req, backups.objects, + total_count) return backups # TODO(frankm): Add some checks here including diff --git a/cinder/api/microversions.py b/cinder/api/microversions.py index 2f691bb0b48..a724f3cdd99 100644 --- a/cinder/api/microversions.py +++ b/cinder/api/microversions.py @@ -127,6 +127,8 @@ BACKUP_METADATA = '3.43' NEW_ATTACH_COMPLETION = '3.44' +SUPPORT_COUNT_INFO = '3.45' + def get_mv_header(version): """Gets a formatted HTTP microversion header. diff --git a/cinder/api/openstack/api_version_request.py b/cinder/api/openstack/api_version_request.py index ec0d4967f1e..381e04c1645 100644 --- a/cinder/api/openstack/api_version_request.py +++ b/cinder/api/openstack/api_version_request.py @@ -107,6 +107,8 @@ REST_API_VERSION_HISTORY = """ state is intentionally NOT allowed. * 3.43 - Support backup CRUD with metadata. * 3.44 - Add attachment-complete. + * 3.45 - Add ``count`` field to volume, backup and snapshot list and + detail APIs. """ # The minimum and maximum versions of the API supported @@ -114,7 +116,7 @@ REST_API_VERSION_HISTORY = """ # minimum version of the API supported. # Explicitly using /v2 endpoints will still work _MIN_API_VERSION = "3.0" -_MAX_API_VERSION = "3.44" +_MAX_API_VERSION = "3.45" _LEGACY_API_VERSION2 = "2.0" UPDATED = "2017-09-19T20:18:14Z" diff --git a/cinder/api/openstack/rest_api_version_history.rst b/cinder/api/openstack/rest_api_version_history.rst index 87f8d6f771f..22f8ca11fe2 100644 --- a/cinder/api/openstack/rest_api_version_history.rst +++ b/cinder/api/openstack/rest_api_version_history.rst @@ -373,3 +373,7 @@ user documentation. Support attachment completion. See the `API reference `__ for details. + +3.45 +---- + Add ``count`` field to volume, backup and snapshot list and detail APIs. diff --git a/cinder/api/v3/snapshots.py b/cinder/api/v3/snapshots.py index 00c55a958df..276967d6c92 100644 --- a/cinder/api/v3/snapshots.py +++ b/cinder/api/v3/snapshots.py @@ -78,6 +78,12 @@ class SnapshotsController(snapshots_v2.SnapshotsController): sort_keys, sort_dirs = common.get_sort_params(search_opts) marker, limit, offset = common.get_pagination_params(search_opts) + req_version = req.api_version_request + show_count = False + if req_version.matches(mv.SUPPORT_COUNT_INFO): + show_count = utils.get_bool_param('with_count', search_opts) + search_opts.pop('with_count') + # process filters self._process_snapshot_filtering(context=context, filters=search_opts, @@ -93,20 +99,27 @@ class SnapshotsController(snapshots_v2.SnapshotsController): if 'name' in search_opts: search_opts['display_name'] = search_opts.pop('name') - snapshots = self.volume_api.get_all_snapshots(context, - search_opts=search_opts, - marker=marker, - limit=limit, - sort_keys=sort_keys, - sort_dirs=sort_dirs, - offset=offset) + snapshots = self.volume_api.get_all_snapshots( + context, + search_opts=search_opts.copy(), + marker=marker, + limit=limit, + sort_keys=sort_keys, + sort_dirs=sort_dirs, + offset=offset) + total_count = None + if show_count: + total_count = self.volume_api.calculate_resource_count( + context, 'snapshot', search_opts) req.cache_db_snapshots(snapshots.objects) if is_detail: - snapshots = self._view_builder.detail_list(req, snapshots.objects) + snapshots = self._view_builder.detail_list(req, snapshots.objects, + total_count) else: - snapshots = self._view_builder.summary_list(req, snapshots.objects) + snapshots = self._view_builder.summary_list(req, snapshots.objects, + total_count) return snapshots diff --git a/cinder/api/v3/views/volumes.py b/cinder/api/v3/views/volumes.py index 038d4e0f750..168a24a4e2e 100644 --- a/cinder/api/v3/views/volumes.py +++ b/cinder/api/v3/views/volumes.py @@ -20,6 +20,8 @@ from cinder.api.v2.views import volumes as views_v2 class ViewBuilder(views_v2.ViewBuilder): """Model a volumes API V3 response as a python dictionary.""" + _collection_name = "volumes" + def quick_summary(self, volume_count, volume_size, all_distinct_metadata=None): """View of volumes summary. @@ -53,3 +55,32 @@ class ViewBuilder(views_v2.ViewBuilder): volume_ref['volume']['provider_id'] = volume.get('provider_id') return volume_ref + + def _list_view(self, func, request, volumes, volume_count, + coll_name=_collection_name): + """Provide a view for a list of volumes. + + :param func: Function used to format the volume data + :param request: API request + :param volumes: List of volumes in dictionary format + :param volume_count: Length of the original list of volumes + :param coll_name: Name of collection, used to generate the next link + for a pagination query + :returns: Volume data in dictionary format + """ + volumes_list = [func(request, volume)['volume'] for volume in volumes] + volumes_links = self._get_collection_links(request, + volumes, + coll_name, + volume_count) + volumes_dict = {"volumes": volumes_list} + + if volumes_links: + volumes_dict['volumes_links'] = volumes_links + + req_version = request.api_version_request + if req_version.matches( + mv.SUPPORT_COUNT_INFO, None) and volume_count is not None: + volumes_dict['count'] = volume_count + + return volumes_dict diff --git a/cinder/api/v3/volumes.py b/cinder/api/v3/volumes.py index 10615265369..4aa84801c9c 100644 --- a/cinder/api/v3/volumes.py +++ b/cinder/api/v3/volumes.py @@ -97,6 +97,11 @@ class VolumeController(volumes_v2.VolumeController): sort_keys, sort_dirs = common.get_sort_params(params) filters = params + show_count = False + if req_version.matches(mv.SUPPORT_COUNT_INFO): + show_count = utils.get_bool_param('with_count', filters) + filters.pop('with_count') + self._process_volume_filtering(context=context, filters=filters, req_version=req_version) @@ -114,9 +119,13 @@ class VolumeController(volumes_v2.VolumeController): volumes = self.volume_api.get_all(context, marker, limit, sort_keys=sort_keys, sort_dirs=sort_dirs, - filters=filters, + filters=filters.copy(), viewable_admin_meta=True, offset=offset) + total_count = None + if show_count: + total_count = self.volume_api.calculate_resource_count( + context, 'volume', filters) for volume in volumes: utils.add_visible_admin_metadata(volume) @@ -124,9 +133,11 @@ class VolumeController(volumes_v2.VolumeController): req.cache_db_volumes(volumes.objects) if is_detail: - volumes = self._view_builder.detail_list(req, volumes) + volumes = self._view_builder.detail_list( + req, volumes, total_count) else: - volumes = self._view_builder.summary_list(req, volumes) + volumes = self._view_builder.summary_list( + req, volumes, total_count) return volumes @wsgi.Controller.api_version(mv.VOLUME_SUMMARY) diff --git a/cinder/api/views/backups.py b/cinder/api/views/backups.py index df39696bb14..b69a1479714 100644 --- a/cinder/api/views/backups.py +++ b/cinder/api/views/backups.py @@ -92,6 +92,9 @@ class ViewBuilder(common.ViewBuilder): if backups_links: backups_dict['backups_links'] = backups_links + if backup_count is not None: + backups_dict['count'] = backup_count + return backups_dict def export_summary(self, request, export): diff --git a/cinder/api/views/snapshots.py b/cinder/api/views/snapshots.py index 56f77337b59..7d562307c4d 100644 --- a/cinder/api/views/snapshots.py +++ b/cinder/api/views/snapshots.py @@ -75,4 +75,7 @@ class ViewBuilder(common.ViewBuilder): if snapshots_links: snapshots_dict[self._collection_name + '_links'] = snapshots_links + if snapshot_count is not None: + snapshots_dict['count'] = snapshot_count + return snapshots_dict diff --git a/cinder/db/api.py b/cinder/db/api.py index c6df6cbf4a0..4297fab57b7 100644 --- a/cinder/db/api.py +++ b/cinder/db/api.py @@ -281,6 +281,10 @@ def volume_get_all(context, marker=None, limit=None, sort_keys=None, offset=offset) +def calculate_resource_count(context, resource_type, filters): + return IMPL.calculate_resource_count(context, resource_type, filters) + + def volume_get_all_by_host(context, host, filters=None): """Get all volumes belonging to a host.""" return IMPL.volume_get_all_by_host(context, host, filters=filters) diff --git a/cinder/db/sqlalchemy/api.py b/cinder/db/sqlalchemy/api.py index e1fdbcfb3a7..786125db511 100644 --- a/cinder/db/sqlalchemy/api.py +++ b/cinder/db/sqlalchemy/api.py @@ -2364,6 +2364,23 @@ def _generate_paginate_query(context, session, marker, limit, sort_keys, offset=offset) +def calculate_resource_count(context, resource_type, filters): + """Calculate total count with filters applied""" + + session = get_session() + if resource_type not in CALCULATE_COUNT_HELPERS.keys(): + raise exception.InvalidInput( + reason=_("Model %s doesn't support " + "counting resource.") % resource_type) + get_query, process_filters = CALCULATE_COUNT_HELPERS[resource_type] + query = get_query(context, session=session) + if filters: + query = process_filters(query, filters) + if query is None: + return 0 + return query.with_entities(func.count()).scalar() + + @apply_like_filters(model=models.Volume) def _process_volume_filters(query, filters): """Common filter processing for Volume queries. @@ -6589,6 +6606,13 @@ PAGINATION_HELPERS = { } +CALCULATE_COUNT_HELPERS = { + 'volume': (_volume_get_query, _process_volume_filters), + 'snapshot': (_snaps_get_query, _process_snaps_filters), + 'backup': (_backups_get_query, _process_backups_filters), +} + + ############################### diff --git a/cinder/tests/unit/api/v3/test_backups.py b/cinder/tests/unit/api/v3/test_backups.py index 549a7140b50..c4e7c693f11 100644 --- a/cinder/tests/unit/api/v3/test_backups.py +++ b/cinder/tests/unit/api/v3/test_backups.py @@ -17,6 +17,7 @@ import ddt import mock +from oslo_utils import strutils import webob from cinder.api import microversions as mv @@ -88,6 +89,90 @@ class BackupsControllerAPITestCase(test.TestCase): self.controller.update, req, fake.BACKUP_ID, body) + def _create_multiple_backups_with_different_project(self): + test_utils.create_backup( + context.RequestContext(fake.USER_ID, fake.PROJECT_ID, True)) + test_utils.create_backup( + context.RequestContext(fake.USER_ID, fake.PROJECT_ID, True)) + test_utils.create_backup( + context.RequestContext(fake.USER_ID, fake.PROJECT2_ID, True)) + + @ddt.data('backups', 'backups/detail') + def test_list_backup_with_count_param_version_not_matched(self, action): + self._create_multiple_backups_with_different_project() + + is_detail = True if 'detail' in action else False + req = fakes.HTTPRequest.blank("/v3/%s?with_count=True" % action) + req.headers = mv.get_mv_header( + mv.get_prior_version(mv.SUPPORT_COUNT_INFO)) + req.api_version_request = mv.get_api_version( + mv.get_prior_version(mv.SUPPORT_COUNT_INFO)) + ctxt = context.RequestContext(fake.USER_ID, fake.PROJECT_ID, True) + req.environ['cinder.context'] = ctxt + res_dict = self.controller._get_backups(req, is_detail=is_detail) + self.assertNotIn('count', res_dict) + + @ddt.data({'method': 'backups', + 'display_param': 'True'}, + {'method': 'backups', + 'display_param': 'False'}, + {'method': 'backups', + 'display_param': '1'}, + {'method': 'backups/detail', + 'display_param': 'True'}, + {'method': 'backups/detail', + 'display_param': 'False'}, + {'method': 'backups/detail', + 'display_param': '1'} + ) + @ddt.unpack + def test_list_backups_with_count_param(self, method, display_param): + self._create_multiple_backups_with_different_project() + + is_detail = True if 'detail' in method else False + show_count = strutils.bool_from_string(display_param, strict=True) + # Request with 'with_count' and 'limit' + req = fakes.HTTPRequest.blank( + "/v3/%s?with_count=%s&limit=1" % (method, display_param)) + req.headers = mv.get_mv_header(mv.SUPPORT_COUNT_INFO) + req.api_version_request = mv.get_api_version(mv.SUPPORT_COUNT_INFO) + ctxt = context.RequestContext(fake.USER_ID, fake.PROJECT_ID, False) + req.environ['cinder.context'] = ctxt + res_dict = self.controller._get_backups(req, is_detail=is_detail) + self.assertEqual(1, len(res_dict['backups'])) + if show_count: + self.assertEqual(2, res_dict['count']) + else: + self.assertNotIn('count', res_dict) + + # Request with 'with_count' + req = fakes.HTTPRequest.blank( + "/v3/%s?with_count=%s" % (method, display_param)) + req.headers = mv.get_mv_header(mv.SUPPORT_COUNT_INFO) + req.api_version_request = mv.get_api_version(mv.SUPPORT_COUNT_INFO) + ctxt = context.RequestContext(fake.USER_ID, fake.PROJECT_ID, False) + req.environ['cinder.context'] = ctxt + res_dict = self.controller._get_backups(req, is_detail=is_detail) + self.assertEqual(2, len(res_dict['backups'])) + if show_count: + self.assertEqual(2, res_dict['count']) + else: + self.assertNotIn('count', res_dict) + + # Request with admin context and 'all_tenants' + req = fakes.HTTPRequest.blank( + "/v3/%s?with_count=%s&all_tenants=1" % (method, display_param)) + req.headers = mv.get_mv_header(mv.SUPPORT_COUNT_INFO) + req.api_version_request = mv.get_api_version(mv.SUPPORT_COUNT_INFO) + ctxt = context.RequestContext(fake.USER_ID, fake.PROJECT_ID, True) + req.environ['cinder.context'] = ctxt + res_dict = self.controller._get_backups(req, is_detail=is_detail) + self.assertEqual(3, len(res_dict['backups'])) + if show_count: + self.assertEqual(3, res_dict['count']) + else: + self.assertNotIn('count', res_dict) + @ddt.data(mv.get_prior_version(mv.RESOURCE_FILTER), mv.RESOURCE_FILTER, mv.LIKE_FILTER) diff --git a/cinder/tests/unit/api/v3/test_snapshots.py b/cinder/tests/unit/api/v3/test_snapshots.py index 3a831897858..c6f3da7e2ca 100644 --- a/cinder/tests/unit/api/v3/test_snapshots.py +++ b/cinder/tests/unit/api/v3/test_snapshots.py @@ -14,8 +14,8 @@ # under the License. import ddt - import mock +from oslo_utils import strutils from cinder.api import microversions as mv from cinder.api.v3 import snapshots @@ -151,6 +151,97 @@ class SnapshotApiTest(test.TestCase): self.assertEqual(1, len(res_dict['snapshots'])) self.assertEqual(snapshot1.id, res_dict['snapshots'][0]['id']) + def _create_multiple_snapshots_with_different_project(self): + volume1 = test_utils.create_volume(self.ctx, + project=fake.PROJECT_ID) + volume2 = test_utils.create_volume(self.ctx, + project=fake.PROJECT2_ID) + test_utils.create_snapshot( + context.RequestContext(fake.USER_ID, fake.PROJECT_ID, True), + volume1.id) + test_utils.create_snapshot( + context.RequestContext(fake.USER_ID, fake.PROJECT_ID, True), + volume1.id) + test_utils.create_snapshot( + context.RequestContext(fake.USER_ID, fake.PROJECT2_ID, True), + volume2.id) + + @ddt.data('snapshots', 'snapshots/detail') + def test_list_snapshot_with_count_param_version_not_matched(self, action): + self._create_multiple_snapshots_with_different_project() + + is_detail = True if 'detail' in action else False + req = fakes.HTTPRequest.blank("/v3/%s?with_count=True" % action) + req.headers = mv.get_mv_header( + mv.get_prior_version(mv.SUPPORT_COUNT_INFO)) + req.api_version_request = mv.get_api_version( + mv.get_prior_version(mv.SUPPORT_COUNT_INFO)) + ctxt = context.RequestContext(fake.USER_ID, fake.PROJECT_ID, True) + req.environ['cinder.context'] = ctxt + res_dict = self.controller._items(req, is_detail=is_detail) + self.assertNotIn('count', res_dict) + + @ddt.data({'method': 'snapshots', + 'display_param': 'True'}, + {'method': 'snapshots', + 'display_param': 'False'}, + {'method': 'snapshots', + 'display_param': '1'}, + {'method': 'snapshots/detail', + 'display_param': 'True'}, + {'method': 'snapshots/detail', + 'display_param': 'False'}, + {'method': 'snapshots/detail', + 'display_param': '1'} + ) + @ddt.unpack + def test_list_snapshot_with_count_param(self, method, display_param): + self._create_multiple_snapshots_with_different_project() + + is_detail = True if 'detail' in method else False + show_count = strutils.bool_from_string(display_param, strict=True) + # Request with 'with_count' and 'limit' + req = fakes.HTTPRequest.blank( + "/v3/%s?with_count=%s&limit=1" % (method, display_param)) + req.headers = mv.get_mv_header(mv.SUPPORT_COUNT_INFO) + req.api_version_request = mv.get_api_version(mv.SUPPORT_COUNT_INFO) + ctxt = context.RequestContext(fake.USER_ID, fake.PROJECT_ID, False) + req.environ['cinder.context'] = ctxt + res_dict = self.controller._items(req, is_detail=is_detail) + self.assertEqual(1, len(res_dict['snapshots'])) + if show_count: + self.assertEqual(2, res_dict['count']) + else: + self.assertNotIn('count', res_dict) + + # Request with 'with_count' + req = fakes.HTTPRequest.blank( + "/v3/%s?with_count=%s" % (method, display_param)) + req.headers = mv.get_mv_header(mv.SUPPORT_COUNT_INFO) + req.api_version_request = mv.get_api_version(mv.SUPPORT_COUNT_INFO) + ctxt = context.RequestContext(fake.USER_ID, fake.PROJECT_ID, False) + req.environ['cinder.context'] = ctxt + res_dict = self.controller._items(req, is_detail=is_detail) + self.assertEqual(2, len(res_dict['snapshots'])) + if show_count: + self.assertEqual(2, res_dict['count']) + else: + self.assertNotIn('count', res_dict) + + # Request with admin context and 'all_tenants' + req = fakes.HTTPRequest.blank( + "/v3/%s?with_count=%s&all_tenants=1" % (method, display_param)) + req.headers = mv.get_mv_header(mv.SUPPORT_COUNT_INFO) + req.api_version_request = mv.get_api_version(mv.SUPPORT_COUNT_INFO) + ctxt = context.RequestContext(fake.USER_ID, fake.PROJECT_ID, True) + req.environ['cinder.context'] = ctxt + res_dict = self.controller._items(req, is_detail=is_detail) + self.assertEqual(3, len(res_dict['snapshots'])) + if show_count: + self.assertEqual(3, res_dict['count']) + else: + self.assertNotIn('count', res_dict) + def test_snapshot_list_with_sort_name(self): self._create_snapshot(name='test1') self._create_snapshot(name='test2') diff --git a/cinder/tests/unit/api/v3/test_volumes.py b/cinder/tests/unit/api/v3/test_volumes.py index d644c6ce9db..9e19182ae5e 100644 --- a/cinder/tests/unit/api/v3/test_volumes.py +++ b/cinder/tests/unit/api/v3/test_volumes.py @@ -16,6 +16,7 @@ import ddt import iso8601 import mock +from oslo_utils import strutils import webob from cinder.api import extensions @@ -113,6 +114,16 @@ class VolumeApiTest(test.TestCase): fake.GROUP2_ID}) return [vol1, vol2] + def _create_multiple_volumes_with_different_project(self): + # Create volumes in project 1 + db.volume_create(self.ctxt, {'display_name': 'test1', + 'project_id': fake.PROJECT_ID}) + db.volume_create(self.ctxt, {'display_name': 'test2', + 'project_id': fake.PROJECT_ID}) + # Create volume in project 2 + db.volume_create(self.ctxt, {'display_name': 'test3', + 'project_id': fake.PROJECT2_ID}) + def test_volume_index_filter_by_glance_metadata(self): vols = self._create_volume_with_glance_metadata() req = fakes.HTTPRequest.blank("/v3/volumes?glance_metadata=" @@ -149,6 +160,82 @@ class VolumeApiTest(test.TestCase): self.assertEqual(1, len(volumes)) self.assertEqual(vols[0].id, volumes[0]['id']) + @ddt.data('volumes', 'volumes/detail') + def test_list_volume_with_count_param_version_not_matched(self, action): + self._create_multiple_volumes_with_different_project() + + is_detail = True if 'detail' in action else False + req = fakes.HTTPRequest.blank("/v3/%s?with_count=True" % action) + req.headers = mv.get_mv_header( + mv.get_prior_version(mv.SUPPORT_COUNT_INFO)) + req.api_version_request = mv.get_api_version( + mv.get_prior_version(mv.SUPPORT_COUNT_INFO)) + ctxt = context.RequestContext(fake.USER_ID, fake.PROJECT_ID, True) + req.environ['cinder.context'] = ctxt + res_dict = self.controller._get_volumes(req, is_detail=is_detail) + self.assertNotIn('count', res_dict) + + @ddt.data({'method': 'volumes', + 'display_param': 'True'}, + {'method': 'volumes', + 'display_param': 'False'}, + {'method': 'volumes', + 'display_param': '1'}, + {'method': 'volumes/detail', + 'display_param': 'True'}, + {'method': 'volumes/detail', + 'display_param': 'False'}, + {'method': 'volumes/detail', + 'display_param': '1'} + ) + @ddt.unpack + def test_list_volume_with_count_param(self, method, display_param): + self._create_multiple_volumes_with_different_project() + + is_detail = True if 'detail' in method else False + show_count = strutils.bool_from_string(display_param, strict=True) + # Request with 'with_count' and 'limit' + req = fakes.HTTPRequest.blank( + "/v3/%s?with_count=%s&limit=1" % (method, display_param)) + req.headers = mv.get_mv_header(mv.SUPPORT_COUNT_INFO) + req.api_version_request = mv.get_api_version(mv.SUPPORT_COUNT_INFO) + ctxt = context.RequestContext(fake.USER_ID, fake.PROJECT_ID, False) + req.environ['cinder.context'] = ctxt + res_dict = self.controller._get_volumes(req, is_detail=is_detail) + self.assertEqual(1, len(res_dict['volumes'])) + if show_count: + self.assertEqual(2, res_dict['count']) + else: + self.assertNotIn('count', res_dict) + + # Request with 'with_count' + req = fakes.HTTPRequest.blank( + "/v3/%s?with_count=%s" % (method, display_param)) + req.headers = mv.get_mv_header(mv.SUPPORT_COUNT_INFO) + req.api_version_request = mv.get_api_version(mv.SUPPORT_COUNT_INFO) + ctxt = context.RequestContext(fake.USER_ID, fake.PROJECT_ID, False) + req.environ['cinder.context'] = ctxt + res_dict = self.controller._get_volumes(req, is_detail=is_detail) + self.assertEqual(2, len(res_dict['volumes'])) + if show_count: + self.assertEqual(2, res_dict['count']) + else: + self.assertNotIn('count', res_dict) + + # Request with admin context and 'all_tenants' + req = fakes.HTTPRequest.blank( + "/v3/%s?with_count=%s&all_tenants=1" % (method, display_param)) + req.headers = mv.get_mv_header(mv.SUPPORT_COUNT_INFO) + req.api_version_request = mv.get_api_version(mv.SUPPORT_COUNT_INFO) + ctxt = context.RequestContext(fake.USER_ID, fake.PROJECT_ID, True) + req.environ['cinder.context'] = ctxt + res_dict = self.controller._get_volumes(req, is_detail=is_detail) + self.assertEqual(3, len(res_dict['volumes'])) + if show_count: + self.assertEqual(3, res_dict['count']) + else: + self.assertNotIn('count', res_dict) + def test_volume_index_filter_by_group_id_in_unsupport_version(self): self._create_volume_with_group() req = fakes.HTTPRequest.blank(("/v3/volumes?group_id=%s") % diff --git a/cinder/volume/api.py b/cinder/volume/api.py index 87365c84fda..00d983578e7 100644 --- a/cinder/volume/api.py +++ b/cinder/volume/api.py @@ -535,6 +535,15 @@ class API(base.Base): LOG.info("Volume info retrieved successfully.", resource=volume) return volume + def calculate_resource_count(self, context, resource_type, filters): + filters = filters if filters else {} + allTenants = utils.get_bool_param('all_tenants', filters) + if context.is_admin and allTenants: + del filters['all_tenants'] + else: + filters['project_id'] = context.project_id + return db.calculate_resource_count(context, resource_type, filters) + def get_all(self, context, marker=None, limit=None, sort_keys=None, sort_dirs=None, filters=None, viewable_admin_meta=False, offset=None): diff --git a/releasenotes/notes/add-count-info-in-list-api-e43wac44yu750c23.yaml b/releasenotes/notes/add-count-info-in-list-api-e43wac44yu750c23.yaml new file mode 100644 index 00000000000..dc019af23d9 --- /dev/null +++ b/releasenotes/notes/add-count-info-in-list-api-e43wac44yu750c23.yaml @@ -0,0 +1,3 @@ +--- +features: + - Added count info in volume, snapshot and backup's list APIs since 3.45.