Merge "Add project_id admin filter to limits API"
This commit is contained in:
commit
54affb4437
@ -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 = {
|
||||
|
@ -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"
|
||||
|
||||
|
@ -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.
|
||||
|
54
cinder/api/v3/limits.py
Normal file
54
cinder/api/v3/limits.py
Normal file
@ -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())
|
@ -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
|
||||
|
@ -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": {
|
||||
|
70
cinder/tests/unit/api/v3/test_limits.py
Normal file
70
cinder/tests/unit/api/v3/test_limits.py
Normal file
@ -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'])
|
@ -0,0 +1,3 @@
|
||||
---
|
||||
features:
|
||||
- Supported ``project_id`` admin filters to limits API.
|
Loading…
Reference in New Issue
Block a user