kingbird/kingbird/api/controllers/quota_manager.py

231 lines
9.8 KiB
Python

# Copyright (c) 2016 Ericsson AB
# 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 collections
from oslo_config import cfg
from oslo_log import log as logging
from oslo_log import versionutils
from oslo_utils import uuidutils
import pecan
from pecan import expose
from pecan import request
import restcomm
from kingbird.common import exceptions
from kingbird.common.i18n import _
from kingbird.common import utils
from kingbird.db.sqlalchemy import api as db_api
from kingbird.rpc import client as rpc_client
CONF = cfg.CONF
rpc_api_cap_opt = cfg.StrOpt('kb-engine',
help='Set a version cap for messages sent to'
'kb-engine services. If you plan to do a'
'live upgrade from an old version to a'
'newer version, you should set this option'
'to the old version before beginning the'
'live upgrade procedure. Only upgrading'
'to the next version is supported, so you'
'cannot skip a release for the live upgrade'
'procedure.')
CONF.register_opt(rpc_api_cap_opt, 'upgrade_levels')
LOG = logging.getLogger(__name__)
class QuotaManagerController(object):
VERSION_ALIASES = {
'mitaka': '1.0',
}
def __init__(self, *args, **kwargs):
super(QuotaManagerController, self).__init__(*args, **kwargs)
self.rpc_client = rpc_client.EngineClient()
# to do the version compatibility for future purpose
def _determine_version_cap(self, target):
version_cap = 1.0
return version_cap
@expose(generic=True, template='json')
def index(self):
# Route the request to specific methods with parameters
pass
@index.when(method='GET', template='json')
def get(self, project_id, target_project_id=None, action=None):
context = restcomm.extract_context_from_environ()
valid_project_id = uuidutils.is_uuid_like(project_id)
if not valid_project_id:
pecan.abort(400, _('Invalid request URL'))
if project_id != context.project and not context.is_admin:
pecan.abort(400, _('Invalid request URL'))
if not uuidutils.is_uuid_like(target_project_id)\
and target_project_id != 'defaults':
pecan.abort(400, _('Invalid request URL'))
quota = collections.defaultdict(dict)
result = collections.defaultdict(dict)
try:
if context.is_admin or (project_id == target_project_id)\
or (target_project_id == 'defaults'):
if target_project_id == 'defaults':
# Get default quota limits from conf file
result = self._get_defaults(context,
CONF.kingbird_global_limit)
else:
if action and action != 'detail':
pecan.abort(404, _('Invalid request URL'))
elif action == 'detail':
# Get the current quota usages for a project
result = self.rpc_client.get_total_usage_for_tenant(
context, target_project_id)
else:
# Get quota limits for all the resources for a project
result = db_api.quota_get_all_by_project(
context, target_project_id)
quota['quota_set'] = result
return quota
else:
pecan.abort(403, _('Admin required '))
# Could be raised by get total usage call
except exceptions.InternalError:
pecan.abort(400, _('Error while requesting usage'))
# Tries to update quota limits for a project, if it fails then
# it creates a new entry in DB for that project
@index.when(method='PUT', template='json')
def put(self, project_id, target_project_id, action=None):
context = restcomm.extract_context_from_environ()
valid_project_id = uuidutils.is_uuid_like(project_id)
if not valid_project_id:
pecan.abort(400, _('Invalid request URL'))
if project_id != context.project and not context.is_admin:
pecan.abort(400, _('Invalid request URL'))
if not uuidutils.is_uuid_like(target_project_id):
pecan.abort(400, _('Invalid request URL'))
quota = collections.defaultdict(dict)
quota[target_project_id] = collections.defaultdict(dict)
if action and action != 'sync':
pecan.abort(404, 'Invalid action, only sync is allowed')
elif action == 'sync':
return self.sync(target_project_id, context)
if not context.is_admin:
pecan.abort(403, _('Admin required'))
if not request.body:
pecan.abort(400, _('Body required'))
payload = eval(request.body)
payload = payload.get('quota_set')
if not payload:
pecan.abort(400, _('quota_set in body is required'))
try:
utils.validate_quota_limits(payload)
for resource, limit in payload.iteritems():
try:
# Update quota limit in DB
result = db_api.quota_update(
context,
project_id=target_project_id,
resource=resource,
limit=limit)
except exceptions.ProjectQuotaNotFound:
# If update fails due to project/quota not found
# then create the quota limit
result = db_api.quota_create(
context,
project_id=target_project_id,
resource=resource,
limit=limit)
quota[target_project_id][result.resource] = result.hard_limit
return quota
except exceptions.InvalidInputError:
pecan.abort(400, _('Invalid input for quota limits'))
@index.when(method='delete', template='json')
def delete(self, project_id, target_project_id):
context = restcomm.extract_context_from_environ()
valid_project_id = uuidutils.is_uuid_like(project_id)
if not valid_project_id:
pecan.abort(400, _('Invalid request URL'))
if project_id != context.project and not context.is_admin:
pecan.abort(400, _('Invalid request URL'))
if not uuidutils.is_uuid_like(target_project_id):
pecan.abort(400, _('Invalid request URL'))
if not context.is_admin:
pecan.abort(403, _('Admin required'))
try:
if request.body:
# Delete the mentioned quota limit for the project
payload = eval(request.body)
payload = payload.get('quota_set')
if not payload:
pecan.abort(400, _('quota_set in body required'))
utils.validate_quota_limits(payload)
for resource in payload:
db_api.quota_destroy(context, target_project_id, resource)
return {'Deleted quota limits': payload}
else:
# Delete all quota limits for the project
db_api.quota_destroy_all(context, target_project_id)
return "Deleted all quota limits for the given project"
except exceptions.ProjectQuotaNotFound:
pecan.abort(404, _('Project quota not found'))
except exceptions.InvalidInputError:
pecan.abort(400, _('Invalid input for quota'))
# Private method called by put method for on demand quota sync
def sync(self, project_id, context):
if pecan.request.method != 'PUT':
pecan.abort(405, _('Bad method. Use PUT instead'))
if not context.is_admin:
pecan.abort(403, _('Admin required'))
self.rpc_client.quota_sync_for_project(
context, project_id)
return 'triggered quota sync for ' + project_id
@staticmethod
def _get_defaults(context, config_defaults):
"""Get default quota values.
If the default class is defined, use the values defined
in the class, otherwise use the values from the config.
:param context:
:param config_defaults:
:return:
"""
quotas = {}
default_quotas = {}
if CONF.use_default_quota_class:
default_quotas = db_api.quota_class_get_default(context)
for resource, default in config_defaults.items():
# get rid of the 'quota_' prefix
resource_name = resource[6:]
if default_quotas:
if resource_name not in default_quotas:
versionutils.report_deprecated_feature(LOG, _(
"Default quota for resource: %(res)s is set "
"by the default quota flag: quota_%(res)s, "
"it is now deprecated. Please use the "
"default quota class for default "
"quota.") % {'res': resource_name})
quotas[resource_name] = default_quotas.get(resource_name, default)
return quotas