From bf40945dccacdc4c75c1afb2f963f2668525f9f8 Mon Sep 17 00:00:00 2001 From: wangxiyuan Date: Mon, 24 Apr 2017 10:06:07 +0800 Subject: [PATCH] Return metadata in volume summary Cinder supports filter volumes with metadata, but in some case, users don't know what metadata the volumes contain or what metadata is valid to filter volumes. This patch updated volumes/summary API to return all valid distinct metadata to users. Then users could use these metadatas to filter volumes easily. This function is useful for dashboard, such as Horizon, as well. APIImpact Implements: blueprint metadata-for-volume-summary Change-Id: I33c77d9db88f70d8d3b8ea86c86c01220dcc537c --- cinder/api/openstack/api_version_request.py | 4 +- .../openstack/rest_api_version_history.rst | 4 ++ cinder/api/v3/views/volumes.py | 16 +++-- cinder/api/v3/volumes.py | 13 +++- cinder/db/sqlalchemy/api.py | 17 ++++- cinder/tests/unit/api/v3/test_volumes.py | 66 ++++++++++++++++++- cinder/tests/unit/utils.py | 3 + ...a-for-volume-summary-729ba648db4e4e54.yaml | 4 ++ 8 files changed, 116 insertions(+), 11 deletions(-) create mode 100644 releasenotes/notes/metadata-for-volume-summary-729ba648db4e4e54.yaml diff --git a/cinder/api/openstack/api_version_request.py b/cinder/api/openstack/api_version_request.py index f70c0bc081b..7bc492d971c 100644 --- a/cinder/api/openstack/api_version_request.py +++ b/cinder/api/openstack/api_version_request.py @@ -89,7 +89,7 @@ REST_API_VERSION_HISTORY = """ ``message``, ``attachment``, ``group`` and ``group-snapshot`` list APIs. * 3.35 - Add ``volume-type`` filter to Get-Pools API. - + * 3.36 - Add metadata to volumes/summary response body. """ # The minimum and maximum versions of the API supported @@ -97,7 +97,7 @@ REST_API_VERSION_HISTORY = """ # minimum version of the API supported. # Explicitly using /v1 or /v2 endpoints will still work _MIN_API_VERSION = "3.0" -_MAX_API_VERSION = "3.35" +_MAX_API_VERSION = "3.36" _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 a541c876b87..3de8d56b4de 100644 --- a/cinder/api/openstack/rest_api_version_history.rst +++ b/cinder/api/openstack/rest_api_version_history.rst @@ -317,3 +317,7 @@ user documentation. 3.35 ---- Add ``volume-type`` filter to Get-Pools API. + +3.36 +---- + Add metadata to volumes/summary response body. diff --git a/cinder/api/v3/views/volumes.py b/cinder/api/v3/views/volumes.py index ba126a76d8f..eb9b4058702 100644 --- a/cinder/api/v3/views/volumes.py +++ b/cinder/api/v3/views/volumes.py @@ -19,14 +19,22 @@ 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.""" - def quick_summary(self, volume_count, volume_size): - """Number of volumes and size of volumes.""" - return { + def quick_summary(self, volume_count, volume_size, + all_distinct_metadata=None): + """View of volumes summary. + + It includes number of volumes, size of volumes and all distinct + metadata of volumes. + """ + summary = { 'volume-summary': { 'total_count': volume_count, 'total_size': volume_size - }, + } } + if all_distinct_metadata is not None: + summary['volume-summary']['metadata'] = all_distinct_metadata + return summary def detail(self, request, volume): """Detailed view of a single volume.""" diff --git a/cinder/api/v3/volumes.py b/cinder/api/v3/volumes.py index c69328bf700..aa483b03cb2 100644 --- a/cinder/api/v3/volumes.py +++ b/cinder/api/v3/volumes.py @@ -148,8 +148,17 @@ class VolumeController(volumes_v2.VolumeController): 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])) + num_vols, sum_size, metadata = self.volume_api.get_volume_summary( + context, filters=filters) + + req_version = req.api_version_request + if req_version.matches("3.36"): + all_distinct_metadata = metadata + else: + all_distinct_metadata = None + + return view_builder_v3.quick_summary(num_vols, int(sum_size), + all_distinct_metadata) @wsgi.response(http_client.ACCEPTED) def create(self, req, body): diff --git a/cinder/db/sqlalchemy/api.py b/cinder/db/sqlalchemy/api.py index dda3084536c..c0d3ee1eaf8 100644 --- a/cinder/db/sqlalchemy/api.py +++ b/cinder/db/sqlalchemy/api.py @@ -2100,7 +2100,22 @@ def get_volume_summary(context, project_only): return [] result = query.first() - return (result[0] or 0, result[1] or 0) + + query_metadata = model_query( + context, models.VolumeMetadata.key, models.VolumeMetadata.value, + read_deleted="no") + if project_only: + query_metadata = query_metadata.join( + models.Volume, + models.Volume.id == models.VolumeMetadata.volume_id).filter_by( + project_id=context.project_id) + result_metadata = query_metadata.distinct().all() + + result_metadata_list = collections.defaultdict(list) + for key, value in result_metadata: + result_metadata_list[key].append(value) + + return (result[0] or 0, result[1] or 0, result_metadata_list) @require_admin_context diff --git a/cinder/tests/unit/api/v3/test_volumes.py b/cinder/tests/unit/api/v3/test_volumes.py index 673d32af5b7..0d0062c7a68 100644 --- a/cinder/tests/unit/api/v3/test_volumes.py +++ b/cinder/tests/unit/api/v3/test_volumes.py @@ -30,6 +30,7 @@ from cinder.tests.unit.api import fakes from cinder.tests.unit.api.v2 import fakes as v2_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.tests.unit import utils as test_utils from cinder import utils from cinder.volume import api as volume_api from cinder.volume import api as vol_get @@ -156,8 +157,12 @@ class VolumeApiTest(test.TestCase): 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') + def _fake_volumes_summary_request(self, version='3.12', all_tenant=False, + is_admin=False): + req_url = '/v3/volumes/summary' + if all_tenant: + req_url += '?all_tenants=True' + req = fakes.HTTPRequest.blank(req_url, use_admin_context=is_admin) req.headers = {'OpenStack-API-Version': 'volume ' + version} req.api_version_request = api_version.APIVersionRequest(version) return req @@ -186,6 +191,63 @@ class VolumeApiTest(test.TestCase): expected = {'volume-summary': {'total_size': 1.0, 'total_count': 1}} self.assertEqual(expected, res_dict) + @ddt.data( + ('3.35', {'volume-summary': {'total_size': 0.0, + 'total_count': 0}}), + ('3.36', {'volume-summary': {'total_size': 0.0, + 'total_count': 0, + 'metadata': {}}})) + @ddt.unpack + def test_volume_summary_empty(self, summary_api_version, expect_result): + req = self._fake_volumes_summary_request(version=summary_api_version) + res_dict = self.controller.summary(req) + self.assertEqual(expect_result, res_dict) + + @ddt.data( + ('3.35', {'volume-summary': {'total_size': 2, + 'total_count': 2}}), + ('3.36', {'volume-summary': {'total_size': 2, + 'total_count': 2, + 'metadata': { + 'name': ['test_name1', 'test_name2'], + 'age': ['test_age']}}})) + @ddt.unpack + def test_volume_summary_return_metadata(self, summary_api_version, + expect_result): + test_utils.create_volume(self.ctxt, metadata={'name': 'test_name1', + 'age': 'test_age'}) + test_utils.create_volume(self.ctxt, metadata={'name': 'test_name2', + 'age': 'test_age'}) + ctxt2 = context.RequestContext(fake.USER_ID, fake.PROJECT2_ID, True) + test_utils.create_volume(ctxt2, metadata={'name': 'test_name3'}) + + req = self._fake_volumes_summary_request(version=summary_api_version) + res_dict = self.controller.summary(req) + self.assertEqual(expect_result, res_dict) + + @ddt.data( + ('3.35', {'volume-summary': {'total_size': 2, + 'total_count': 2}}), + ('3.36', {'volume-summary': {'total_size': 2, + 'total_count': 2, + 'metadata': { + 'name': ['test_name1', 'test_name2'], + 'age': ['test_age']}}})) + @ddt.unpack + def test_volume_summary_return_metadata_all_tenant( + self, summary_api_version, expect_result): + test_utils.create_volume(self.ctxt, metadata={'name': 'test_name1', + 'age': 'test_age'}) + ctxt2 = context.RequestContext(fake.USER_ID, fake.PROJECT2_ID, True) + test_utils.create_volume(ctxt2, metadata={'name': 'test_name2', + 'age': 'test_age'}) + + req = self._fake_volumes_summary_request(version=summary_api_version, + all_tenant=True, + is_admin=True) + res_dict = self.controller.summary(req) + self.assertEqual(expect_result, res_dict) + def _vol_in_request_body(self, size=v2_fakes.DEFAULT_VOL_SIZE, name=v2_fakes.DEFAULT_VOL_NAME, diff --git a/cinder/tests/unit/utils.py b/cinder/tests/unit/utils.py index d060e202341..d2ae3f88912 100644 --- a/cinder/tests/unit/utils.py +++ b/cinder/tests/unit/utils.py @@ -69,6 +69,7 @@ def create_volume(ctxt, previous_status=None, testcase_instance=None, id=None, + metadata=None, **kwargs): """Create a volume object in the DB.""" vol = {} @@ -89,6 +90,8 @@ def create_volume(ctxt, vol['group_id'] = group_id if volume_type_id: vol['volume_type_id'] = volume_type_id + if metadata: + vol['metadata'] = metadata for key in kwargs: vol[key] = kwargs[key] vol['replication_status'] = replication_status diff --git a/releasenotes/notes/metadata-for-volume-summary-729ba648db4e4e54.yaml b/releasenotes/notes/metadata-for-volume-summary-729ba648db4e4e54.yaml new file mode 100644 index 00000000000..2c4f7b2f352 --- /dev/null +++ b/releasenotes/notes/metadata-for-volume-summary-729ba648db4e4e54.yaml @@ -0,0 +1,4 @@ +--- +features: + - Added support for get all distinct volumes' metadata from + volume-summary API. \ No newline at end of file