Quota management for Nova-APIGW (part3 os-quota-sets and limits )

Implement os-quota-sets and limits in  Nova-APIGW, support command
quota-show, quota-defaults, quota-update and limits, also can issue
curl to show detail information of quotas.

Quota usage update according to VM provision will be implemented
later

The quota management and control in Tricircle is described in the
design doc:
https://docs.google.com/document/d/18kZZ1snMOCD9IQvUKI5NVDzSASpw-QKj7l2zNqMEd3g/

https://blueprints.launchpad.net/tricircle/+spec/implement-stateless

Change-Id: I54ea514630684f78d757cff732b54dafdfa768b6
Signed-off-by: Chaoyi Huang <joehuang@huawei.com>
This commit is contained in:
Chaoyi Huang 2016-02-29 15:37:18 +08:00
parent 98eb4b3389
commit 2c996e7c61
8 changed files with 928 additions and 70 deletions

View File

@ -280,6 +280,11 @@ class Client(object):
"""
self._update_endpoint_from_keystone(cxt, False)
def get_keystone_client_by_context(self, ctx):
client_session = self._get_keystone_session()
return keystone_client.Client(auth_url=cfg.CONF.client.identity_url,
session=client_session)
@_safe_operation('client')
def get_native_client(self, resource, cxt):
"""Get native python client instance

View File

@ -1024,7 +1024,7 @@ class AllQuotaEngine(QuotaEngine):
('injected_files', 'quota_injected_files'),
('injected_file_content_bytes',
'quota_injected_file_content_bytes'),
('injected_file_path_length',
('injected_file_path_bytes',
'quota_injected_file_path_length'),
]
@ -1344,12 +1344,12 @@ class QuotaSetOperation(object):
target_project = self._get_project(context, target_project_id)
parent_id = target_project.parent_id
context_project = self._get_project(context,
context.project_id,
subtree_as_ids=True)
if parent_id:
# Get the children of the project which the token is scoped to
# in order to know if the target_project is in its hierarchy.
context_project = self._get_project(context,
context.project_id,
subtree_as_ids=True)
self._authorize_update_or_delete(context_project,
target_project.id,
parent_id)
@ -1357,13 +1357,23 @@ class QuotaSetOperation(object):
context, parent_id)
else:
if context.project_id != target_project_id:
# if the target project has no parent and descendant, then
# the operation is allowed only if the context project also
# has no parent and descendant, that means only flat mode
# (current mode without hierarchy) is allowed
if not target_project.subtree and \
not context_project.parent_id and \
not context_project.subtree:
pass
elif context.project_id != target_project_id:
param_msg = _("context.project_id = %(ctx_project_id)s, "
"target_project_id = %(target_project_id)s ") % {
"ctx_project_id": context.project_id,
"target_project_id": target_project.id}
msg = _("context project is not a root project %s") % param_msg
msg = _("Can not update quota for %s") % param_msg
LOG.error(msg=msg)
raise t_exceptions.HTTPForbiddenError
@ -1450,19 +1460,53 @@ class QuotaSetOperation(object):
if not context.is_admin:
raise t_exceptions.AdminRequired
target_tenant_id = self.target_tenant_id
target_project_id = self.target_tenant_id
# Get the parent_id of the target project to verify whether we are
# dealing with hierarchical namespace or non-hierarchical namespace.
target_project = self._get_project(context, target_tenant_id)
target_project = self._get_project(context, target_project_id)
parent_id = target_project.parent_id
# Get the children of the project which the token is scoped to in
# order to know if the target_project is in its hierarchy.
context_project = self._get_project(context,
context.project_id,
subtree_as_ids=True)
if parent_id:
self._authorize_update_or_delete(context_project,
target_project.id,
parent_id)
parent_project_quotas = QUOTAS.get_project_quotas(
context, parent_id, parent_project_id=parent_id)
else:
# if the target project has no parent and descendant, then
# the operation is allowed only if the context project also
# has no parent and descendant, that means only flat mode
# (current mode without hierarchy) is allowed
if not target_project.subtree and \
not context_project.parent_id and \
not context_project.subtree:
pass
elif context.project_id != target_project_id:
param_msg = _("context.project_id = %(ctx_project_id)s, "
"target_project_id = %(target_project_id)s ") % {
"ctx_project_id": context.project_id,
"target_project_id": target_project.id}
msg = _("Can not delete quota for %s") % param_msg
LOG.error(msg=msg)
raise t_exceptions.HTTPForbiddenError
try:
project_quotas = QUOTAS.get_project_quotas(
context, target_project.id, usages=True,
parent_project_id=parent_id, defaults=False)
except t_exceptions.NotAuthorized:
msg = _("Not authorized to delete %s") % target_tenant_id
msg = _("Not authorized to delete %s") % target_project_id
LOG.exception(msg)
raise
@ -1474,33 +1518,22 @@ class QuotaSetOperation(object):
if project_quotas[key]['allocated'] != 0:
msg = _("About to delete child projects having "
"non-zero quota. This should not be performed"
" %s") % target_tenant_id
" %s") % target_project_id
LOG.exception(msg)
raise t_exceptions.ChildQuotaNotZero
# Delete child quota first and later update parent's quota.
try:
# TODO(joehuang) support destroy quota by user
db_api.quota_destroy_by_project(context, target_project.id)
except t_exceptions.AdminRequired:
msg = _('Admin or tenant itself or parent tenant'
' required to delete quota'
' %s') % target_project.id
LOG.exception(msg)
raise
if parent_id:
# Get the children of the project which the token is scoped to in
# order to know if the target_project is in its hierarchy.
context_project = self._get_project(context,
context.project_id,
subtree_as_ids=True)
self._authorize_update_or_delete(context_project,
target_project.id,
parent_id)
parent_project_quotas = QUOTAS.get_project_quotas(
context, parent_id, parent_project_id=parent_id)
# Delete child quota first and later update parent's quota.
try:
# TODO(joehuang) support destroy quota by user
db_api.quota_destroy_by_project(context, target_project.id)
except t_exceptions.AdminRequired:
msg = _('Admin or tenant itself or parent tenant'
' required to delete quota'
' %s') % target_project.id
LOG.exception(msg)
raise
# Update the allocated of the parent
for key, value in project_quotas.items():
project_hard_limit = project_quotas[key]['limit']
@ -1508,16 +1541,6 @@ class QuotaSetOperation(object):
parent_allocated -= project_hard_limit
db_api.quota_allocated_update(context, parent_id, key,
parent_allocated)
else:
try:
# TODO(joehuang) support destroy quota by user
db_api.quota_destroy_by_project(context, target_project.id)
except t_exceptions.AdminRequired:
msg = _('Admin or tenant itself or parent tenant'
' required to delete quota'
' %s') % target_project.id
LOG.exception(msg)
raise
def show_default_quota(self, context):
try:

View File

@ -126,9 +126,6 @@ def upgrade(migrate_engine):
sql.Column('updated_at', sql.DateTime),
sql.Column('deleted_at', sql.DateTime),
sql.Column('deleted', sql.Integer),
migrate.UniqueConstraint(
'project_id', 'resource', 'deleted',
name='uniq_quotas0project_id0resource0deleted'),
mysql_engine='InnoDB',
mysql_charset='utf8')
@ -142,9 +139,6 @@ def upgrade(migrate_engine):
sql.Column('updated_at', sql.DateTime),
sql.Column('deleted_at', sql.DateTime),
sql.Column('deleted', sql.Integer),
migrate.UniqueConstraint(
'class_name', 'resource', 'deleted',
name='uniq_quota_classes0class_name0resource0deleted'),
mysql_engine='InnoDB',
mysql_charset='utf8')

View File

@ -174,10 +174,7 @@ class Quotas(core.ModelBase, QuotasBase):
Null, then the resource is unlimited.
"""
__tablename__ = 'quotas'
__table_args__ = (
schema.UniqueConstraint(
'project_id', 'resource', 'deleted',
name='uniq_quotas0project_id0resource0deleted'),)
__table_args__ = ()
attributes = ['id', 'project_id', 'resource',
'hard_limit', 'allocated',
'created_at', 'updated_at', 'deleted_at', 'deleted']
@ -196,11 +193,7 @@ class QuotaClasses(core.ModelBase, QuotasBase):
quota-class-show and quota-class-update
"""
__tablename__ = 'quota_classes'
__table_args__ = (
schema.UniqueConstraint(
'class_name', 'resource', 'deleted',
name='uniq_quota_classes0class_name0resource0deleted'),
)
__table_args__ = ()
attributes = ['id', 'class_name', 'resource', 'hard_limit',
'created_at', 'updated_at', 'deleted_at', 'deleted']

View File

@ -0,0 +1,269 @@
# Copyright (c) 2015 Huawei Tech. Co., Ltd.
# 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 six
from pecan import expose
from pecan import request
from pecan import response
from pecan import Response
from pecan import rest
from oslo_config import cfg
from oslo_log import log as logging
import tricircle.common.context as t_context
from tricircle.common import exceptions as t_exceptions
from tricircle.common.i18n import _
from tricircle.common import quota
CONF = cfg.CONF
LOG = logging.getLogger(__name__)
class QuotaSetsController(rest.RestController):
def __init__(self, tenant_id):
self.tenant_id = tenant_id
@expose()
def _lookup(self, target_tenant_id, *remainder):
return QuotaController(self.tenant_id, target_tenant_id), remainder
def build_absolute_limits(quotas):
quota_map = {
'maxTotalRAMSize': 'ram',
'maxTotalInstances': 'instances',
'maxTotalCores': 'cores',
'maxTotalKeypairs': 'key_pairs',
'maxTotalFloatingIps': 'floating_ips',
'maxPersonality': 'injected_files',
'maxPersonalitySize': 'injected_file_content_bytes',
'maxSecurityGroups': 'security_groups',
'maxSecurityGroupRules': 'security_group_rules',
'maxServerMeta': 'metadata_items',
'maxServerGroups': 'server_groups',
'maxServerGroupMembers': 'server_group_members',
}
limits = {}
for display_name, key in six.iteritems(quota_map):
if key in quotas:
limits[display_name] = quotas[key]['limit']
return limits
def build_used_limits(quotas):
quota_map = {
'totalRAMUsed': 'ram',
'totalCoresUsed': 'cores',
'totalInstancesUsed': 'instances',
'totalFloatingIpsUsed': 'floating_ips',
'totalSecurityGroupsUsed': 'security_groups',
'totalServerGroupsUsed': 'server_groups',
}
# need to refresh usage from the bottom pods? Now from the data in top
used_limits = {}
for display_name, key in six.iteritems(quota_map):
if key in quotas:
reserved = quotas[key]['reserved']
used_limits[display_name] = quotas[key]['in_use'] + reserved
return used_limits
class LimitsController(rest.RestController):
def __init__(self, tenant_id):
self.tenant_id = tenant_id
@staticmethod
def _reserved(req):
try:
return int(req.GET['reserved'])
except (ValueError, KeyError):
return False
@expose(generic=True, template='json')
def get_all(self):
# TODO(joehuang): add policy controll here
context = t_context.extract_context_from_environ()
context.project_id = self.tenant_id
target_tenant_id = request.params.get('tenant_id', None)
if target_tenant_id:
target_tenant_id.strip()
else:
return Response('tenant_id not given', 400)
qs = quota.QuotaSetOperation(target_tenant_id,
None)
try:
quotas = qs.show_detail_quota(context, show_usage=True)
except t_exceptions.NotFound as e:
msg = str(e)
LOG.exception(msg=msg)
return Response(msg, 404)
except (t_exceptions.AdminRequired,
t_exceptions.NotAuthorized,
t_exceptions.HTTPForbiddenError) as e:
msg = str(e)
LOG.exception(msg=msg)
return Response(msg, 403)
except Exception as e:
msg = str(e)
LOG.exception(msg=msg)
return Response(msg, 400)
# TODO(joehuang): add API rate limits later
ret = {
'limits': {
'rate': {},
'absolute': {},
},
}
ret['limits']['absolute'].update(
build_absolute_limits(quotas['quota_set']))
ret['limits']['absolute'].update(
build_used_limits(quotas['quota_set']))
return ret
class QuotaController(rest.RestController):
def __init__(self, owner_tenant_id, target_tenant_id):
self.owner_tenant_id = owner_tenant_id
self.target_tenant_id = target_tenant_id
@expose(generic=True, template='json')
def put(self, **kw):
context = t_context.extract_context_from_environ()
if not context.is_admin:
# TODO(joahuang): changed to policy control later
# to support reseller admin mode
return Response(_('Admin role required to update quota'), 409)
return self._quota_action('put', **kw)
@expose(generic=True, template='json')
def delete(self):
"""Delete Quota for a particular tenant.
This works for hierarchical and non-hierarchical projects. For
hierarchical projects only immediate parent admin or the
CLOUD admin are able to perform a delete.
:param id: target project id that needs to be deleted
"""
context = t_context.extract_context_from_environ()
if not context.is_admin:
# TODO(joahuang): changed to policy control later
# to support reseller admin mode
return Response(_('Admin role required to delete quota'), 409)
kw = {}
return self._quota_action('delete', **kw)
@expose(generic=True, template='json')
def get_one(self, show_what):
kw = {}
if show_what == 'defaults' or show_what == 'detail':
return self._quota_action(show_what, **kw)
else:
return Response(_('Only show defaults or detail allowed'), 400)
@expose(generic=True, template='json')
def get_all(self):
kw = {}
return self._quota_action('quota-show', **kw)
def _quota_action(self, action, **kw):
context = t_context.extract_context_from_environ()
context.project_id = self.owner_tenant_id
target_tenant_id = self.target_tenant_id
target_user_id = request.params.get('user_id', None)
if target_user_id:
target_user_id.strip()
qs = quota.QuotaSetOperation(target_tenant_id,
target_user_id)
quotas = {}
try:
if action == 'put':
quotas = qs.update(context, **kw)
elif action == 'delete':
qs.delete(context)
response.status = 202
return
elif action == 'defaults':
quotas = qs.show_default_quota(context)
elif action == 'detail':
quotas = qs.show_detail_quota(context, show_usage=True)
# remove the allocated field which is not visible in Nova
for k, v in quotas['quota_set'].iteritems():
if k != 'id':
v.pop('allocated', None)
elif action == 'quota-show':
quotas = qs.show_detail_quota(context, show_usage=False)
else:
return Response('Resource not found', 404)
except t_exceptions.NotFound as e:
msg = str(e)
LOG.exception(msg=msg)
return Response(msg, 404)
except (t_exceptions.AdminRequired,
t_exceptions.NotAuthorized,
t_exceptions.HTTPForbiddenError) as e:
msg = str(e)
LOG.exception(msg=msg)
return Response(msg, 403)
except Exception as e:
msg = str(e)
LOG.exception(msg=msg)
return Response(msg, 400)
return {'quota_set': self._build_visible_quota(quotas['quota_set'])}
def _build_visible_quota(self, quota_set):
quota_map = [
'id', 'instances', 'ram', 'cores', 'key_pairs',
'floating_ips', 'fixed_ips',
'injected_files', 'injected_file_path_bytes',
'injected_file_content_bytes',
'security_groups', 'security_group_rules',
'metadata_items', 'server_groups', 'server_group_members',
]
ret = {}
# only return Nova visible quota items
for k, v in quota_set.iteritems():
if k in quota_map:
ret[k] = v
return ret

View File

@ -28,6 +28,7 @@ from tricircle.common import xrpcapi
from tricircle.nova_apigw.controllers import aggregate
from tricircle.nova_apigw.controllers import flavor
from tricircle.nova_apigw.controllers import image
from tricircle.nova_apigw.controllers import quota_sets
from tricircle.nova_apigw.controllers import server
@ -89,6 +90,8 @@ class V21Controller(object):
'os-aggregates': aggregate.AggregateController,
'servers': server.ServerController,
'images': image.ImageController,
'os-quota-sets': quota_sets.QuotaSetsController,
'limits': quota_sets.LimitsController,
}
def _get_resource_controller(self, project_id, remainder):

View File

@ -0,0 +1,479 @@
# Copyright (c) 2015 Huawei Technologies Co., Ltd.
# 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 copy
import mock
import pecan
from pecan.configuration import set_config
from pecan.testing import load_test_app
from oslo_config import cfg
from oslo_config import fixture as fixture_config
from oslo_serialization import jsonutils
from tricircle.nova_apigw import app
from tricircle.nova_apigw.controllers import quota_sets
from tricircle.common import context
from tricircle.common import exceptions as t_exceptions
from tricircle.common import quota
from tricircle.db import core
from tricircle.tests.unit.common import test_quota
QUOTAS = quota.QUOTAS
def _make_body(tenant_id='foo', root=True, **kw):
resources = copy.copy(kw)
if tenant_id:
resources['id'] = tenant_id
if root:
result = {'quota_set': resources}
else:
result = resources
return result
def _update_body(src_body, root=True, **kw):
for k, v in kw.iteritems():
if root:
src_body['quota_set'][k] = v
else:
src_body[k] = v
return src_body
def _update_subproject_body(src_body, root=True, **kw):
for k, v in kw.iteritems():
if root:
src_body['quota_set'][k] = v
else:
src_body[k] = v
if root:
for k, v in src_body['quota_set'].iteritems():
if not kw.get(k):
src_body['quota_set'][k] = 0
else:
for k, v in src_body.iteritems():
if not kw.get(k) and k != 'id':
src_body[k] = 0
return src_body
def _make_subproject_body(tenant_id='foo', root=True, **kw):
return _make_body(tenant_id=tenant_id, root=root, **kw)
class QuotaControllerTest(test_quota.QuotaSetsOperationTest):
def setUp(self):
super(QuotaControllerTest, self).setUp()
self.addCleanup(set_config, {}, overwrite=True)
cfg.CONF.register_opts(app.common_opts)
self.CONF = self.useFixture(fixture_config.Config()).conf
self.CONF.set_override('auth_strategy', 'noauth')
self.exception_string = 'NotFound'
self.test_exception = [
{'exception_raise': 'NotFound',
'expected_error': 404},
{'exception_raise': 'AdminRequired',
'expected_error': 403},
{'exception_raise': 'NotAuthorized',
'expected_error': 403},
{'exception_raise': 'HTTPForbiddenError',
'expected_error': 403},
{'exception_raise': 'Conflict',
'expected_error': 400}, ]
self._flags_rest(use_default_quota_class=True)
self.app = self._make_app()
def _make_app(self, enable_acl=False):
self.config = {
'app': {
'root':
'tricircle.nova_apigw.controllers.root.RootController',
'modules': ['tricircle.nova_apigw'],
'enable_acl': enable_acl,
'errors': {
400: '/error',
'__force_dict__': True
}
},
}
return load_test_app(self.config)
def _override_config_rest(self, name, override, group=None):
"""Cleanly override CONF variables."""
self.CONF.set_override(name, override, group)
self.addCleanup(self.CONF.clear_override, name, group)
def _flags_rest(self, **kw):
"""Override CONF variables for a test."""
for k, v in kw.items():
self._override_config_rest(k, v, group='quota')
def tearDown(self):
super(QuotaControllerTest, self).tearDown()
pecan.set_config({}, overwrite=True)
cfg.CONF.unregister_opts(app.common_opts)
core.ModelBase.metadata.drop_all(core.get_engine())
def _get_mock_ctx(self):
return self.ctx
def _mock_func_and_obj(self):
quota.QuotaSetOperation._get_project = mock.Mock()
quota.QuotaSetOperation._get_project.side_effect = self._get_project
context.extract_context_from_environ = mock.Mock()
context.extract_context_from_environ.side_effect = self._get_mock_ctx
def test_quota_set_update_show_defaults(self):
self._mock_func_and_obj()
# show quota before update, should be equal to defaults
url = self._url_for_quota_set(self.A.id, self.A.id)
res = self.app.get(url, expect_errors=True)
self.assertEqual(res.status_int, 200)
json_body = jsonutils.loads(res.body)
default_body = _make_body(tenant_id=self.A.id, root=True,
**self.default_quota)
result = self._DictIn(json_body['quota_set'],
default_body['quota_set'])
self.assertEqual(result, True)
# quota update with wrong parameter
quota_a = dict(instances=5, cores=10, ram=25600)
url = self._url_for_quota_set(self.A.id, self.A.id)
res = self.app.put_json(url,
{'quota_s': quota_a},
expect_errors=True)
self.assertIn(res.status_int, [400, 403, 404])
# quota update with non-admin
self.ctx.is_admin = False
quota_a = dict(instances=5, cores=10, ram=25600)
url = self._url_for_quota_set(self.A.id, self.A.id)
res = self.app.put_json(url,
{'quota_set': quota_a},
expect_errors=True)
self.assertIn(res.status_int, [409])
self.ctx.is_admin = True
# show quota after update
quota_a = dict(instances=5, cores=10, ram=25600)
url = self._url_for_quota_set(self.A.id, self.A.id)
res = self.app.put_json(url,
{'quota_set': quota_a},
expect_errors=True)
json_body = jsonutils.loads(res.body)
updated_a = _make_body(tenant_id=self.A.id, root=True,
**self.default_quota)
updated_a = _update_body(updated_a, root=True, **quota_a)
result = self._DictIn(json_body['quota_set'], updated_a['quota_set'])
self.assertEqual(result, True)
self.ctx.is_admin = False
url = self._url_for_quota_set(self.A.id, self.A.id)
res = self.app.get(url, expect_errors=True)
self.assertIn(res.status_int, [400, 403, 404])
self.ctx.is_admin = True
# show quota after update for child
quota_b = dict(instances=3, cores=5, ram=12800)
url = self._url_for_quota_set(self.A.id, self.B.id)
res = self.app.put_json(url,
{'quota_set': quota_b},
expect_errors=True)
json_body = jsonutils.loads(res.body)
updated_b = _make_body(tenant_id=self.B.id, root=False,
**self.default_quota)
updated_b = _update_subproject_body(updated_b, root=False, **quota_b)
result = self._DictIn(json_body['quota_set'], updated_b)
self.assertEqual(result, True)
# show default quota after update
url = self._url_for_quota_set(self.A.id, self.A.id)
res = self.app.get(url + '/defaults', expect_errors=True)
json_body = jsonutils.loads(res.body)
result = self._DictIn(json_body['quota_set'],
default_body['quota_set'])
self.assertEqual(result, True)
# show default quota for child, should be all 0
quota_c = {}
url = self._url_for_quota_set(self.A.id, self.B.id)
res = self.app.get(url + '/defaults', expect_errors=True)
json_body = jsonutils.loads(res.body)
updated_c = _make_body(tenant_id=self.B.id, root=False,
**self.default_quota)
updated_c = _update_subproject_body(updated_c, root=False, **quota_c)
result = self._DictIn(json_body['quota_set'], updated_c)
self.assertEqual(result, True)
# show quota after update
url = self._url_for_quota_set(self.A.id, self.A.id)
res = self.app.get(url, expect_errors=True)
json_body = jsonutils.loads(res.body)
result = self._DictIn(json_body['quota_set'], updated_a['quota_set'])
self.assertEqual(result, True)
# show quota for child, should be equal to update_b
url = self._url_for_quota_set(self.A.id, self.B.id)
res = self.app.get(url, expect_errors=True)
json_body = jsonutils.loads(res.body)
result = self._DictIn(json_body['quota_set'], updated_b)
self.assertEqual(result, True)
# delete with non-admin
self.ctx.is_admin = False
url = self._url_for_quota_set(self.A.id, self.A.id)
res = self.app.delete(url, expect_errors=True)
self.assertIn(res.status_int, [409])
self.ctx.is_admin = True
# delete parent quota when child quota is not zero
url = self._url_for_quota_set(self.A.id, self.A.id)
res = self.app.delete(url, expect_errors=True)
self.assertIn(res.status_int, [400, 403, 404])
# delete child quota
url = self._url_for_quota_set(self.A.id, self.B.id)
res = self.app.delete(url, expect_errors=True)
self.assertEqual(res.status_int, 202)
# delete parent quota when child quota is deleted
url = self._url_for_quota_set(self.A.id, self.A.id)
res = self.app.delete(url, expect_errors=True)
self.assertEqual(res.status_int, 202)
# show quota for parent after delete, equal to defaults
url = self._url_for_quota_set(self.A.id, self.A.id)
res = self.app.get(url, expect_errors=True)
json_body = jsonutils.loads(res.body)
result = self._DictIn(json_body['quota_set'],
default_body['quota_set'])
self.assertEqual(result, True)
# show quota for child after delete, should be all 0
url = self._url_for_quota_set(self.A.id, self.B.id)
res = self.app.get(url, expect_errors=True)
json_body = jsonutils.loads(res.body)
result = self._DictIn(json_body['quota_set'], updated_c)
self.assertEqual(result, True)
def test_quota_detail_limits(self):
self._mock_func_and_obj()
def _make_default_detail_body(tenant_id='foo'):
resources = copy.copy(self.default_quota)
for k, v in self.default_quota.iteritems():
resources[k] = {}
resources[k]['limit'] = v
resources[k]['reserved'] = 0
resources[k]['in_use'] = 0
if tenant_id:
resources['id'] = tenant_id
return resources
def _update_usage_in_default_detail(quota_item,
reserved, in_use, **kw):
kw[quota_item]['reserved'] = reserved
kw[quota_item]['in_use'] = in_use
return kw
# show quota usage before update, should be equal to defaults
url = self._url_for_quota_set(self.A.id, self.A.id)
res = self.app.get(url + '/detail', expect_errors=True)
self.assertEqual(res.status_int, 200)
json_body = jsonutils.loads(res.body)
default_detail = _make_default_detail_body(self.A.id)
result = self._DictIn(json_body['quota_set'], default_detail)
self.assertEqual(result, True)
# show quota usage after reserve and in_use update
inuse_opts = {'instances': 2, 'cores': 5}
reserve_opts = {'instances': 3, 'cores': 3}
self.ctx.project_id = self.A.id
reservations = QUOTAS.reserve(self.ctx,
project_id=self.A.id,
**inuse_opts)
QUOTAS.commit(self.ctx, reservations, self.A.id)
QUOTAS.reserve(self.ctx, project_id=self.A.id, **reserve_opts)
url = self._url_for_quota_set(self.A.id, self.A.id)
res = self.app.get(url + '/detail', expect_errors=True)
self.assertEqual(res.status_int, 200)
json_body = jsonutils.loads(res.body)
default_detail = _make_default_detail_body(self.A.id)
update_detail = _update_usage_in_default_detail(
'instances',
reserve_opts['instances'],
inuse_opts['instances'],
**default_detail)
update_detail = _update_usage_in_default_detail(
'cores',
reserve_opts['cores'],
inuse_opts['cores'],
**update_detail)
result = self._DictIn(json_body['quota_set'], update_detail)
self.assertEqual(result, True)
# Wrong parameter
url = '/v2.1/' + self.A.id + '/limits?_id=' + self.A.id
res = self.app.get(url, expect_errors=True)
self.assertIn(res.status_int, [400, 403, 404])
url = '/v2.1/' + self.A.id + '/limits'
res = self.app.get(url, expect_errors=True)
self.assertIn(res.status_int, [400, 403, 404])
self.ctx.is_admin = False
url = '/v2.1/' + self.B.id + '/limits?tenant_id=' + self.C.id
res = self.app.get(url, expect_errors=True)
self.assertIn(res.status_int, [400, 403, 404])
self.ctx.is_admin = True
# test absolute limits and usage
url = '/v2.1/' + self.A.id + '/limits?tenant_id=' + self.A.id
res = self.app.get(url, expect_errors=True)
self.assertEqual(res.status_int, 200)
json_body = jsonutils.loads(res.body)
ret_limits = json_body['limits']['absolute']
absolute = {}
absolute.update(quota_sets.build_absolute_limits(update_detail))
absolute.update(quota_sets.build_used_limits(update_detail))
result = self._DictIn(absolute, ret_limits)
self.assertEqual(result, True)
# test child limits, set child quota
quota_b = dict(instances=3, cores=5)
url = self._url_for_quota_set(self.A.id, self.B.id)
res = self.app.put_json(url,
{'quota_set': quota_b},
expect_errors=True)
json_body = jsonutils.loads(res.body)
updated_b = _make_body(tenant_id=self.B.id, root=False,
**self.default_quota)
updated_b = _update_subproject_body(updated_b, root=False, **quota_b)
result = self._DictIn(json_body['quota_set'], updated_b)
self.assertEqual(result, True)
# test child limits, use and reserve child quota
inuse_opts = {'instances': 1, 'cores': 1}
reserve_opts = {'instances': 1, 'cores': 2}
self.ctx.project_id = self.A.id
reservations = QUOTAS.reserve(self.ctx,
project_id=self.B.id,
**inuse_opts)
QUOTAS.commit(self.ctx, reservations, self.B.id)
QUOTAS.reserve(self.ctx, project_id=self.B.id, **reserve_opts)
url = self._url_for_quota_set(self.A.id, self.B.id)
res = self.app.get(url + '/detail', expect_errors=True)
self.assertEqual(res.status_int, 200)
child_json_body = jsonutils.loads(res.body)
self.assertEqual(
child_json_body['quota_set']['instances']['limit'],
quota_b['instances'])
self.assertEqual(
child_json_body['quota_set']['instances']['in_use'],
inuse_opts['instances'])
self.assertEqual(
child_json_body['quota_set']['instances']['reserved'],
reserve_opts['instances'])
self.assertEqual(
child_json_body['quota_set']['cores']['limit'],
quota_b['cores'])
self.assertEqual(
child_json_body['quota_set']['cores']['in_use'],
inuse_opts['cores'])
self.assertEqual(
child_json_body['quota_set']['cores']['reserved'],
reserve_opts['cores'])
# test child limits, get child quota limits and compare
url = '/v2.1/' + self.A.id + '/limits?tenant_id=' + self.B.id
res = self.app.get(url, expect_errors=True)
self.assertEqual(res.status_int, 200)
json_body = jsonutils.loads(res.body)
ret_limits = json_body['limits']['absolute']
self.assertEqual(
ret_limits['maxTotalInstances'],
quota_b['instances'])
self.assertEqual(
ret_limits['maxTotalCores'],
quota_b['cores'])
self.assertEqual(
ret_limits['totalInstancesUsed'],
inuse_opts['instances'] + reserve_opts['instances'])
self.assertEqual(
ret_limits['totalCoresUsed'],
inuse_opts['cores'] + reserve_opts['cores'])
def _show_detail_exception(self, context, show_usage=False):
for todo_exception in self.test_exception:
if todo_exception['exception_raise'] == self.exception_string:
e = getattr(t_exceptions, self.exception_string)
raise e()
def test_quota_sets_exception_catch(self):
orig_show = quota.QuotaSetOperation.show_detail_quota
quota.QuotaSetOperation.show_detail_quota = mock.Mock()
quota.QuotaSetOperation.show_detail_quota.side_effect = \
self._show_detail_exception
# show quota usage before update, should be equal to defaults
for todo_exception in self.test_exception:
self.exception_string = todo_exception['exception_raise']
url = self._url_for_quota_set(self.A.id, self.A.id)
# exception raised in LimitsController
res = self.app.get(url + '/detail', expect_errors=True)
self.assertEqual(res.status_int, todo_exception['expected_error'])
# exception raised in QuotaSetController
res = self.app.get(url, expect_errors=True)
self.assertEqual(res.status_int, todo_exception['expected_error'])
quota.QuotaSetOperation.show_detail_quota = orig_show
def _url_for_quota_set(self, owner_tenant_id, target_tenant_id):
return '/v2.1/' + owner_tenant_id + \
'/os-quota-sets/' + target_tenant_id
def _DictIn(self, dict_small, dict_full):
for k, v in dict_small.iteritems():
if dict_full[k] != v:
return False
return True

View File

@ -664,7 +664,7 @@ class DbQuotaDriverTestCase(QuotaTestBase, base.TestCase):
self.default_quota = dict(
instances=10, cores=20, ram=51200,
floating_ips=10, fixed_ips=-1, metadata_items=128,
injected_files=5, injected_file_path_length=255,
injected_files=5, injected_file_path_bytes=255,
injected_file_content_bytes=10240,
security_groups=10, security_group_rules=20, key_pairs=100,
server_groups=10, server_group_members=10,
@ -686,7 +686,7 @@ class DbQuotaDriverTestCase(QuotaTestBase, base.TestCase):
quota_metadata_items=self.default_quota['metadata_items'],
quota_injected_files=self.default_quota['injected_files'],
quota_injected_file_path_length=self.default_quota[
'injected_file_path_length'],
'injected_file_path_bytes'],
quota_injected_file_content_bytes=self.default_quota[
'injected_file_content_bytes'],
quota_security_groups=self.default_quota['security_groups'],
@ -1669,16 +1669,16 @@ class QuotaSetsOperationTest(DbQuotaDriverTestCase, base.TestCase):
"""Sets an environment used for nested quotas tests.
Create a project hierarchy such as follows:
+-----------+
| |
| A |
| / \ |
| B C |
| / |
| D |
| | |
| E |
+-----------+
+-----------++-----------++-----------+
| || || |
| A || F || G |
| / \ || || |
| B C || || |
| / || || |
| D || || |
| | || || |
| E || || |
+-----------++-----------++-----------+
"""
self.A = self.FakeProject(id=uuidutils.generate_uuid(),
parent_id=None)
@ -1690,6 +1690,10 @@ class QuotaSetsOperationTest(DbQuotaDriverTestCase, base.TestCase):
parent_id=self.B.id)
self.E = self.FakeProject(id=uuidutils.generate_uuid(),
parent_id=self.D.id)
self.F = self.FakeProject(id=uuidutils.generate_uuid(),
parent_id=None)
self.G = self.FakeProject(id=uuidutils.generate_uuid(),
parent_id=None)
# update projects subtrees
self.D.subtree = {self.E.id: self.E.subtree}
@ -1699,7 +1703,8 @@ class QuotaSetsOperationTest(DbQuotaDriverTestCase, base.TestCase):
# project_by_id attribute is used to recover a project based on its id
self.project_by_id = {self.A.id: self.A, self.B.id: self.B,
self.C.id: self.C, self.D.id: self.D,
self.E.id: self.E}
self.E.id: self.E, self.F.id: self.F,
self.G.id: self.G, }
def _create_default_class(self):
for k, v in self.default_quota.items():
@ -1988,6 +1993,46 @@ class QuotaSetsOperationTest(DbQuotaDriverTestCase, base.TestCase):
self.assertRaises(exceptions.HTTPForbiddenError, qso.update,
self.ctx, **updated)
def test_update_subproject_not_in_hierarchy2(self):
qso = quota.QuotaSetOperation(self.F.id)
qso._get_project = mock.Mock()
qso._get_project.side_effect = self._get_project
self.ctx.project_id = self.A.id
updated = _make_body(tenant_id=None, root=True,
**self.test_class_quota)
expected = _make_body(tenant_id=None, root=True,
**self.test_class_expected_result)
# Update the quota of F is not allowed
self.assertRaises(exceptions.HTTPForbiddenError, qso.update,
self.ctx, **updated)
self.ctx.project_id = self.B.id
self.assertRaises(exceptions.HTTPForbiddenError, qso.update,
self.ctx, **updated)
# only admin is allowed yet
self.ctx.is_admin = False
self.ctx.project_id = self.G.id
self.assertRaises(exceptions.AdminRequired, qso.update,
self.ctx, **updated)
self.ctx.is_admin = True
self.ctx.project_id = self.G.id
result = qso.update(self.ctx, **updated)
self.assertDictMatch(expected, result)
self.ctx.is_admin = True
self.ctx.project_id = self.F.id
result = qso.update(self.ctx, **updated)
self.assertDictMatch(expected, result)
self.ctx.is_admin = False
self.ctx.project_id = self.F.id
self.assertRaises(exceptions.AdminRequired, qso.update,
self.ctx, **updated)
def test_update_subproject_with_not_root_context_project(self):
qso = quota.QuotaSetOperation(self.A.id)
qso._get_project = mock.Mock()
@ -2163,6 +2208,53 @@ class QuotaSetsOperationTest(DbQuotaDriverTestCase, base.TestCase):
result_show_after = qso.show_detail_quota(self.ctx)
self.assertDictMatch(result_show, result_show_after)
def test_delete_subproject_not_in_hierarchy2(self):
qso = quota.QuotaSetOperation(self.F.id)
qso._get_project = mock.Mock()
qso._get_project.side_effect = self._get_project
self.ctx.project_id = self.A.id
# delete the quota of F is not allowed
self.assertRaises(exceptions.HTTPForbiddenError, qso.delete,
self.ctx)
self.ctx.project_id = self.B.id
self.assertRaises(exceptions.HTTPForbiddenError, qso.delete,
self.ctx)
# only admin is allowed yet
self.ctx.project_id = self.G.id
self.ctx.is_admin = False
self.assertRaises(exceptions.AdminRequired, qso.delete,
self.ctx)
self.ctx.project_id = self.G.id
self.ctx.is_admin = True
result_show = qso.show_detail_quota(self.ctx)
updated = _make_body(tenant_id=None, root=True,
**self.test_class_quota)
expected = _make_body(tenant_id=None, root=True,
**self.test_class_expected_result)
self.ctx.is_admin = True
self.ctx.project_id = self.G.id
result = qso.update(self.ctx, **updated)
self.assertDictMatch(expected, result)
qso.delete(self.ctx)
result_show_after = qso.show_detail_quota(self.ctx)
self.assertDictMatch(result_show, result_show_after)
self.ctx.project_id = self.F.id
self.ctx.is_admin = True
qso.delete(self.ctx)
result_show_after = qso.show_detail_quota(self.ctx)
self.assertDictMatch(result_show, result_show_after)
self.ctx.project_id = self.F.id
self.ctx.is_admin = False
self.assertRaises(exceptions.AdminRequired, qso.delete,
self.ctx)
def test_subproject_delete(self):
qso = quota.QuotaSetOperation(self.A.id)
qso._get_project = mock.Mock()