diff --git a/cinder/api/openstack/api_version_request.py b/cinder/api/openstack/api_version_request.py index 055d5b5d03c..e56994752c8 100644 --- a/cinder/api/openstack/api_version_request.py +++ b/cinder/api/openstack/api_version_request.py @@ -59,6 +59,7 @@ REST_API_VERSION_HISTORY = """ * 3.9 - Add backup update interface. * 3.10 - Add group_id filter to list/detail volumes in _get_volumes. * 3.11 - Add group types and group specs API. + * 3.12 - Add volumes summary API. """ @@ -67,7 +68,7 @@ REST_API_VERSION_HISTORY = """ # minimum version of the API supported. # Explicitly using /v1 or /v2 enpoints will still work _MIN_API_VERSION = "3.0" -_MAX_API_VERSION = "3.11" +_MAX_API_VERSION = "3.12" _LEGACY_API_VERSION1 = "1.0" _LEGACY_API_VERSION2 = "2.0" diff --git a/cinder/api/openstack/rest_api_version_history.rst b/cinder/api/openstack/rest_api_version_history.rst index 0314b89f5aa..0068e36ac43 100644 --- a/cinder/api/openstack/rest_api_version_history.rst +++ b/cinder/api/openstack/rest_api_version_history.rst @@ -174,3 +174,7 @@ user documentation. 3.11 ---- Added group types and group specs API. + +3.12 +---- + Added volumes/summary API. diff --git a/cinder/api/v3/router.py b/cinder/api/v3/router.py index 72a4cbc8662..9bacc44b9cc 100644 --- a/cinder/api/v3/router.py +++ b/cinder/api/v3/router.py @@ -53,7 +53,7 @@ class APIRouter(cinder.api.openstack.APIRouter): self.resources['volumes'] = volumes.create_resource(ext_mgr) mapper.resource("volume", "volumes", controller=self.resources['volumes'], - collection={'detail': 'GET'}, + collection={'detail': 'GET', 'summary': 'GET'}, member={'action': 'POST'}) self.resources['messages'] = messages.create_resource(ext_mgr) diff --git a/cinder/api/v3/views/volumes.py b/cinder/api/v3/views/volumes.py new file mode 100644 index 00000000000..ecfcf410791 --- /dev/null +++ b/cinder/api/v3/views/volumes.py @@ -0,0 +1,24 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + + +class ViewBuilder(object): + """Model a server API response as a python dictionary.""" + + def quick_summary(self, volume_count, volume_size): + """Number of volumes and size of volumes.""" + return { + 'volume-summary': { + 'total_count': volume_count, + 'total_size': volume_size + }, + } diff --git a/cinder/api/v3/volumes.py b/cinder/api/v3/volumes.py index f8ef58c7d1e..2d3df74dc09 100644 --- a/cinder/api/v3/volumes.py +++ b/cinder/api/v3/volumes.py @@ -16,8 +16,11 @@ from cinder.api import common from cinder.api.openstack import wsgi from cinder.api.v2 import volumes as volumes_v2 +from cinder.api.v3.views import volumes as volume_views_v3 from cinder import utils +SUMMARY_BASE_MICRO_VERSION = '3.12' + class VolumeController(volumes_v2.VolumeController): """The Volumes API controller for the OpenStack API V3.""" @@ -72,6 +75,19 @@ class VolumeController(volumes_v2.VolumeController): volumes = self._view_builder.summary_list(req, volumes) return volumes + @wsgi.Controller.api_version(SUMMARY_BASE_MICRO_VERSION) + def summary(self, req): + """Return summary of volumes.""" + view_builder_v3 = volume_views_v3.ViewBuilder() + context = req.environ['cinder.context'] + filters = req.params.copy() + + utils.remove_invalid_filter_options(context, filters, + self._get_volume_filter_options()) + + volumes = self.volume_api.get_volume_summary(context, filters=filters) + return view_builder_v3.quick_summary(volumes[0], int(volumes[1])) + def create_resource(ext_mgr): return wsgi.Resource(VolumeController(ext_mgr)) diff --git a/cinder/db/api.py b/cinder/db/api.py index 712717ee162..187352a0515 100644 --- a/cinder/db/api.py +++ b/cinder/db/api.py @@ -280,6 +280,16 @@ def volume_get_all_by_project(context, project_id, marker, limit, offset=offset) +def get_volume_summary_all(context): + """Get all volume summary.""" + return IMPL.get_volume_summary_all(context) + + +def get_volume_summary_by_project(context, project_id): + """Get all volume summary belonging to a project.""" + return IMPL.get_volume_summary_by_project(context, project_id) + + def volume_update(context, volume_id, values): """Set the given properties on a volume and update it. diff --git a/cinder/db/sqlalchemy/api.py b/cinder/db/sqlalchemy/api.py index 5a5af77882a..a3d821c6cf5 100644 --- a/cinder/db/sqlalchemy/api.py +++ b/cinder/db/sqlalchemy/api.py @@ -1775,6 +1775,23 @@ def volume_get_all(context, marker, limit, sort_keys=None, sort_dirs=None, return query.all() +@require_admin_context +def get_volume_summary_all(context): + """Retrieves all volumes summary. + + :param context: context to query under + :returns: volume summary of all projects + """ + query = model_query(context, func.count(models.Volume.id), + func.sum(models.Volume.size), read_deleted="no") + + if query is None: + return [] + + result = query.first() + return (result[0] or 0, result[1] or 0) + + @require_admin_context def volume_get_all_by_host(context, host, filters=None): """Retrieves all volumes hosted on a host. @@ -2077,6 +2094,25 @@ def process_sort_params(sort_keys, sort_dirs, default_keys=None, return result_keys, result_dirs +@require_context +def get_volume_summary_by_project(context, project_id): + """Retrieves all volumes summary in a project. + + :param context: context to query under + :param project_id: project for all volumes being retrieved + :returns: volume summary of a project + """ + query = model_query(context, func.count(models.Volume.id), + func.sum(models.Volume.size), read_deleted="no").\ + filter_by(project_id=project_id) + + if query is None: + return [] + + result = query.first() + return (result[0] or 0, result[1] or 0) + + @handle_db_data_error @require_context def volume_update(context, volume_id, values): diff --git a/cinder/objects/volume.py b/cinder/objects/volume.py index 6b3f54fffcd..16f02de3763 100644 --- a/cinder/objects/volume.py +++ b/cinder/objects/volume.py @@ -538,3 +538,13 @@ class VolumeList(base.ObjectListBase, base.CinderObject): expected_attrs = cls._get_expected_attrs(context) return base.obj_make_list(context, cls(context), objects.Volume, volumes, expected_attrs=expected_attrs) + + @classmethod + def get_volume_summary_all(cls, context): + volumes = db.get_volume_summary_all(context) + return volumes + + @classmethod + def get_volume_summary_by_project(cls, context, project_id): + volumes = db.get_volume_summary_by_project(context, project_id) + return volumes diff --git a/cinder/tests/unit/api/v2/test_volumes.py b/cinder/tests/unit/api/v2/test_volumes.py index 50edc4cc231..e33de4f6cdb 100644 --- a/cinder/tests/unit/api/v2/test_volumes.py +++ b/cinder/tests/unit/api/v2/test_volumes.py @@ -128,7 +128,8 @@ class VolumeApiTest(test.TestCase): res_dict = self.controller.detail(req) self.assertTrue(mock_validate.called) - def _vol_in_request_body(self, + @classmethod + def _vol_in_request_body(cls, size=stubs.DEFAULT_VOL_SIZE, name=stubs.DEFAULT_VOL_NAME, description=stubs.DEFAULT_VOL_DESCRIPTION, diff --git a/cinder/tests/unit/api/v3/test_volumes.py b/cinder/tests/unit/api/v3/test_volumes.py index 80d81d1e2a9..2682b22eb7c 100644 --- a/cinder/tests/unit/api/v3/test_volumes.py +++ b/cinder/tests/unit/api/v3/test_volumes.py @@ -20,8 +20,10 @@ from cinder.api.openstack import api_version_request as api_version from cinder.api.v3 import volumes from cinder import context from cinder import db +from cinder import exception from cinder import test from cinder.tests.unit.api import fakes +from cinder.tests.unit.api.v2 import test_volumes as v2_test_volumes from cinder.tests.unit import fake_constants as fake from cinder.volume.api import API as vol_get @@ -145,3 +147,33 @@ class VolumeApiTest(test.TestCase): res_dict = self.controller.index(req) volumes = res_dict['volumes'] self.assertEqual(2, len(volumes)) + + def _fake_volumes_summary_request(self, version='3.12'): + req = fakes.HTTPRequest.blank('/v3/volumes/summary') + req.headers = {'OpenStack-API-Version': 'volume ' + version} + req.api_version_request = api_version.APIVersionRequest(version) + return req + + def test_volumes_summary_in_unsupport_version(self): + """Function call to test summary volumes API in unsupported version""" + req = self._fake_volumes_summary_request(version='3.7') + self.assertRaises(exception.VersionNotFoundForAPIMethod, + self.controller.summary, req) + + def test_volumes_summary_in_supported_version(self): + """Function call to test the summary volumes API for version v3.""" + req = self._fake_volumes_summary_request() + res_dict = self.controller.summary(req) + expected = {'volume-summary': {'total_size': 0.0, 'total_count': 0}} + self.assertEqual(expected, res_dict) + + vol = v2_test_volumes.VolumeApiTest._vol_in_request_body( + availability_zone="nova") + body = {"volume": vol} + req = fakes.HTTPRequest.blank('/v3/volumes') + res_dict = self.controller.create(req, body) + + req = self._fake_volumes_summary_request() + res_dict = self.controller.summary(req) + expected = {'volume-summary': {'total_size': 1.0, 'total_count': 1}} + self.assertEqual(expected, res_dict) diff --git a/cinder/volume/api.py b/cinder/volume/api.py index 4209bf194f8..0a6cb41bad7 100644 --- a/cinder/volume/api.py +++ b/cinder/volume/api.py @@ -523,6 +523,24 @@ class API(base.Base): LOG.info(_LI("Get all volumes completed successfully.")) return volumes + def get_volume_summary(self, context, filters=None): + check_policy(context, 'get_all') + + if filters is None: + filters = {} + + allTenants = utils.get_bool_param('all_tenants', filters) + + if context.is_admin and allTenants: + del filters['all_tenants'] + volumes = objects.VolumeList.get_volume_summary_all(context) + else: + volumes = objects.VolumeList.get_volume_summary_by_project( + context, context.project_id) + + LOG.info(_LI("Get summary completed successfully.")) + return volumes + def get_snapshot(self, context, snapshot_id): check_policy(context, 'get_snapshot') snapshot = objects.Snapshot.get_by_id(context, snapshot_id) diff --git a/releasenotes/notes/volumes-summary-6b2485f339c88a91.yaml b/releasenotes/notes/volumes-summary-6b2485f339c88a91.yaml new file mode 100644 index 00000000000..d09afc6503b --- /dev/null +++ b/releasenotes/notes/volumes-summary-6b2485f339c88a91.yaml @@ -0,0 +1,4 @@ +--- +features: + - A new API to display the volumes summary. This summary API displays the + total number of volumes and total volume's size in GB.