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:
parent
98eb4b3389
commit
2c996e7c61
@ -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
|
||||
|
@ -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:
|
||||
|
@ -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')
|
||||
|
||||
|
@ -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']
|
||||
|
||||
|
269
tricircle/nova_apigw/controllers/quota_sets.py
Normal file
269
tricircle/nova_apigw/controllers/quota_sets.py
Normal 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
|
@ -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):
|
||||
|
@ -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
|
@ -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()
|
||||
|
Loading…
Reference in New Issue
Block a user