diff --git a/cinder/api/contrib/used_limits.py b/cinder/api/contrib/used_limits.py index 2665ed21da4..1c638a2826b 100644 --- a/cinder/api/contrib/used_limits.py +++ b/cinder/api/contrib/used_limits.py @@ -27,7 +27,17 @@ class UsedLimitsController(wsgi.Controller): def index(self, req, resp_obj): context = req.environ['cinder.context'] if authorize(context): - quotas = QUOTAS.get_project_quotas(context, context.project_id, + params = req.params.copy() + req_version = req.api_version_request + + # TODO(wangxiyuan): Support "tenant_id" here to keep the backwards + # compatibility. Remove it once we drop all support for "tenant". + if req_version.matches(None, "3.38") or not context.is_admin: + params.pop('project_id', None) + params.pop('tenant_id', None) + project_id = params.get( + 'project_id', params.get('tenant_id', context.project_id)) + quotas = QUOTAS.get_project_quotas(context, project_id, usages=True) quota_map = { diff --git a/cinder/api/openstack/api_version_request.py b/cinder/api/openstack/api_version_request.py index 3d71906154f..1289b1f168a 100644 --- a/cinder/api/openstack/api_version_request.py +++ b/cinder/api/openstack/api_version_request.py @@ -92,6 +92,7 @@ REST_API_VERSION_HISTORY = """ * 3.36 - Add metadata to volumes/summary response body. * 3.37 - Support sort backup by "name". * 3.38 - Add replication group API (Tiramisu). + * 3.39 - Add ``project_id`` admin filters support to limits. """ # The minimum and maximum versions of the API supported @@ -99,7 +100,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.38" +_MAX_API_VERSION = "3.39" _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 987989b7330..22aeb896f89 100644 --- a/cinder/api/openstack/rest_api_version_history.rst +++ b/cinder/api/openstack/rest_api_version_history.rst @@ -330,3 +330,7 @@ user documentation. ---- Added enable_replication/disable_replication/failover_replication/ list_replication_targets for replication groups (Tiramisu). + +3.39 +---- + Add ``project_id`` admin filters support to limits. diff --git a/cinder/api/v3/limits.py b/cinder/api/v3/limits.py new file mode 100644 index 00000000000..cc739a3df32 --- /dev/null +++ b/cinder/api/v3/limits.py @@ -0,0 +1,54 @@ +# +# 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. + +"""The limits V3 api.""" + +from cinder.api.openstack import wsgi +from cinder.api.v2 import limits as limits_v2 +from cinder.api.views import limits as limits_views +from cinder import quota + +QUOTAS = quota.QUOTAS + + +class LimitsController(limits_v2.LimitsController): + """Controller for accessing limits in the OpenStack API.""" + + def index(self, req): + """Return all global and rate limit information.""" + context = req.environ['cinder.context'] + params = req.params.copy() + req_version = req.api_version_request + + # TODO(wangxiyuan): Support "tenant_id" here to keep the backwards + # compatibility. Remove it once we drop all support for "tenant". + if req_version.matches(None, "3.38") or not context.is_admin: + params.pop('project_id', None) + params.pop('tenant_id', None) + project_id = params.get( + 'project_id', params.get('tenant_id', context.project_id)) + + quotas = QUOTAS.get_project_quotas(context, project_id, + usages=False) + abs_limits = {k: v['limit'] for k, v in quotas.items()} + rate_limits = req.environ.get("cinder.limits", []) + + builder = self._get_view_builder(req) + return builder.build(rate_limits, abs_limits) + + def _get_view_builder(self, req): + return limits_views.ViewBuilder() + + +def create_resource(): + return wsgi.Resource(LimitsController()) diff --git a/cinder/api/v3/router.py b/cinder/api/v3/router.py index 53cadbebfa6..f45e337688e 100644 --- a/cinder/api/v3/router.py +++ b/cinder/api/v3/router.py @@ -21,7 +21,6 @@ WSGI middleware for OpenStack Volume API. from cinder.api import extensions import cinder.api.openstack -from cinder.api.v2 import limits from cinder.api.v2 import snapshot_metadata from cinder.api.v2 import types from cinder.api.v3 import attachments @@ -32,6 +31,7 @@ from cinder.api.v3 import group_snapshots from cinder.api.v3 import group_specs from cinder.api.v3 import group_types from cinder.api.v3 import groups +from cinder.api.v3 import limits from cinder.api.v3 import messages from cinder.api.v3 import resource_filters from cinder.api.v3 import snapshot_manage diff --git a/cinder/tests/unit/api/contrib/test_used_limits.py b/cinder/tests/unit/api/contrib/test_used_limits.py index 6396798bcc9..cca43233d5a 100644 --- a/cinder/tests/unit/api/contrib/test_used_limits.py +++ b/cinder/tests/unit/api/contrib/test_used_limits.py @@ -13,9 +13,11 @@ # License for the specific language governing permissions and limitations # under the License. +import ddt import mock from cinder.api.contrib import used_limits +from cinder.api.openstack import api_version_request from cinder.api.openstack import wsgi from cinder import exception from cinder import test @@ -24,21 +26,37 @@ from cinder.tests.unit import fake_constants as fake class FakeRequest(object): - def __init__(self, context): + def __init__(self, context, filter=None, api_version='2.0'): self.environ = {'cinder.context': context} + self.params = filter or {} + self.api_version_request = api_version_request.APIVersionRequest( + api_version) +@ddt.ddt class UsedLimitsTestCase(test.TestCase): def setUp(self): """Run before each test.""" super(UsedLimitsTestCase, self).setUp() self.controller = used_limits.UsedLimitsController() + @ddt.data(('2.0', False), ('3.38', True), ('3.38', False), ('3.39', True), + ('3.39', False)) @mock.patch('cinder.quota.QUOTAS.get_project_quotas') @mock.patch('cinder.policy.enforce') - def test_used_limits(self, _mock_policy_enforce, _mock_get_project_quotas): + def test_used_limits(self, ver_project, _mock_policy_enforce, + _mock_get_project_quotas): + version, has_project = ver_project fake_req = FakeRequest(fakes.FakeRequestContext(fake.USER_ID, - fake.PROJECT_ID)) + fake.PROJECT_ID, + is_admin=True), + api_version=version) + if has_project: + fake_req = FakeRequest(fakes.FakeRequestContext(fake.USER_ID, + fake.PROJECT_ID, + is_admin=True), + filter={'project_id': fake.UUID1}, + api_version=version) obj = { "limits": { "rate": [], @@ -46,26 +64,39 @@ class UsedLimitsTestCase(test.TestCase): }, } res = wsgi.ResponseObject(obj) - quota_map = { - 'totalVolumesUsed': 'volumes', - 'totalGigabytesUsed': 'gigabytes', - 'totalSnapshotsUsed': 'snapshots', - } - limits = {} - for display_name, q in quota_map.items(): - limits[q] = {'limit': 2, - 'in_use': 1} - _mock_get_project_quotas.return_value = limits + def get_project_quotas(context, project_id, quota_class=None, + defaults=True, usages=True): + if project_id == fake.UUID1: + return {"gigabytes": {'limit': 5, 'in_use': 1}} + return {"gigabytes": {'limit': 10, 'in_use': 2}} + _mock_get_project_quotas.side_effect = get_project_quotas # allow user to access used limits _mock_policy_enforce.return_value = None self.controller.index(fake_req, res) abs_limits = res.obj['limits']['absolute'] - for used_limit, value in abs_limits.items(): - self.assertEqual(value, - limits[quota_map[used_limit]]['in_use']) + + # if admin, only 3.39 and req contains project_id filter, cinder + # returns the specified project's quota. + if version == '3.39' and has_project: + self.assertEqual(1, abs_limits['totalGigabytesUsed']) + else: + self.assertEqual(2, abs_limits['totalGigabytesUsed']) + + fake_req = FakeRequest(fakes.FakeRequestContext(fake.USER_ID, + fake.PROJECT_ID), + api_version=version) + if has_project: + fake_req = FakeRequest(fakes.FakeRequestContext(fake.USER_ID, + fake.PROJECT_ID), + filter={'project_id': fake.UUID1}, + api_version=version) + # if non-admin, cinder always returns self quota. + self.controller.index(fake_req, res) + abs_limits = res.obj['limits']['absolute'] + self.assertEqual(2, abs_limits['totalGigabytesUsed']) obj = { "limits": { diff --git a/cinder/tests/unit/api/v3/test_limits.py b/cinder/tests/unit/api/v3/test_limits.py new file mode 100644 index 00000000000..14aa726b57c --- /dev/null +++ b/cinder/tests/unit/api/v3/test_limits.py @@ -0,0 +1,70 @@ +# Copyright 2017 Huawei Corporation +# All Rights Reserved. +# +# 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. + +import ddt +import mock + +from cinder.api.openstack import api_version_request as api_version +from cinder.api.v3 import limits +from cinder import test +from cinder.tests.unit.api import fakes +from cinder.tests.unit import fake_constants as fake + + +@ddt.ddt +class LimitsControllerTest(test.TestCase): + def setUp(self): + super(LimitsControllerTest, self).setUp() + self.controller = limits.LimitsController() + + @ddt.data(('3.38', True), ('3.38', False), ('3.39', True), ('3.39', False)) + @mock.patch('cinder.quota.VolumeTypeQuotaEngine.get_project_quotas') + def test_get_limit_with_project_id(self, ver_project, mock_get_quotas): + max_ver, has_project = ver_project + req = fakes.HTTPRequest.blank('/v3/limits', use_admin_context=True) + if has_project: + req = fakes.HTTPRequest.blank( + '/v3/limits?project_id=%s' % fake.UUID1, + use_admin_context=True) + req.api_version_request = api_version.APIVersionRequest(max_ver) + + def get_project_quotas(context, project_id, quota_class=None, + defaults=True, usages=True): + if project_id == fake.UUID1: + return {"gigabytes": {'limit': 5}} + return {"gigabytes": {'limit': 10}} + mock_get_quotas.side_effect = get_project_quotas + + resp_dict = self.controller.index(req) + # if admin, only 3.39 and req contains project_id filter, cinder + # returns the specified project's quota. + if max_ver == '3.39' and has_project: + self.assertEqual( + 5, resp_dict['limits']['absolute']['maxTotalVolumeGigabytes']) + else: + self.assertEqual( + 10, resp_dict['limits']['absolute']['maxTotalVolumeGigabytes']) + + # if non-admin, cinder always returns self quota. + req = fakes.HTTPRequest.blank('/v3/limits', use_admin_context=False) + if has_project: + req = fakes.HTTPRequest.blank( + '/v3/limits?project_id=%s' % fake.UUID1, + use_admin_context=False) + req.api_version_request = api_version.APIVersionRequest(max_ver) + resp_dict = self.controller.index(req) + + self.assertEqual( + 10, resp_dict['limits']['absolute']['maxTotalVolumeGigabytes']) diff --git a/releasenotes/notes/support-project-id-filter-for-limit-bc5d49e239baee2a.yaml b/releasenotes/notes/support-project-id-filter-for-limit-bc5d49e239baee2a.yaml new file mode 100644 index 00000000000..88a13e6f239 --- /dev/null +++ b/releasenotes/notes/support-project-id-filter-for-limit-bc5d49e239baee2a.yaml @@ -0,0 +1,3 @@ +--- +features: + - Supported ``project_id`` admin filters to limits API.