Merge "Add API layer for quota class management"

This commit is contained in:
Jenkins 2016-05-17 05:55:56 +00:00 committed by Gerrit Code Review
commit 53d8732d8f
7 changed files with 233 additions and 20 deletions

View File

@ -21,15 +21,15 @@ import pecan
from pecan import expose
from pecan import request
import itertools
import restcomm
from kingbird.common import consts
from kingbird.common import exceptions
from kingbird.common.i18n import _
from kingbird.common import rpc
from kingbird.common.serializer import KingbirdSerializer as Serializer
from kingbird.common import topics
from kingbird.common import utils
from kingbird.db.sqlalchemy import api as db_api
CONF = cfg.CONF
@ -129,7 +129,7 @@ class QuotaManagerController(object):
if not payload:
pecan.abort(400, _('quota_set in body is required'))
try:
self._validate_quota_limits(payload)
utils.validate_quota_limits(payload)
for resource, limit in payload.iteritems():
try:
# Update quota limit in DB
@ -164,7 +164,7 @@ class QuotaManagerController(object):
payload = payload.get('quota_set')
if not payload:
pecan.abort(400, _('quota_set in body required'))
self._validate_quota_limits(payload)
utils.validate_quota_limits(payload)
for resource in payload:
db_api.quota_destroy(context, project_id, resource)
return {'Deleted quota limits': payload}
@ -187,16 +187,3 @@ class QuotaManagerController(object):
self.client.cast(context, 'quota_sync_for_project',
project_id=project_id)
return 'triggered quota sync for ' + project_id
# to do validate the quota limits
def _validate_quota_limits(self, payload):
for resource in payload:
# Check valid resource name
if resource not in itertools.chain(consts.CINDER_QUOTA_FIELDS,
consts.NOVA_QUOTA_FIELDS,
consts.NEUTRON_QUOTA_FIELDS):
raise exceptions.InvalidInputError
# Check valid quota limit value in case for put/post
if isinstance(payload, dict) and (not isinstance(
payload[resource], int) or payload[resource] <= 0):
raise exceptions.InvalidInputError

View File

@ -17,6 +17,7 @@
import pecan
from kingbird.api.controllers import quota_manager
from kingbird.api.controllers.v1 import quota_class
class RootController(object):
@ -59,6 +60,7 @@ class V1Controller(object):
self.sub_controllers = {
"os-quota-sets": quota_manager.QuotaManagerController,
"os-quota-class-sets": quota_class.QuotaClassSetController,
}
for name, ctrl in self.sub_controllers.items():

View File

View File

@ -0,0 +1,109 @@
# Copyright (c) 2016 Ericsson AB
#
# 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.
from oslo_log import log as logging
import pecan
from pecan import expose
from pecan import request
import six
from kingbird.api.controllers import restcomm
from kingbird.common import consts
from kingbird.common import exceptions
from kingbird.common.i18n import _
from kingbird.common import utils
from kingbird.db import api as db_api
LOG = logging.getLogger(__name__)
class QuotaClassSetController(object):
supported_quotas = []
def __init__(self, *args, **kwargs):
self.supported_quotas = list(
consts.CINDER_QUOTA_FIELDS +
consts.NEUTRON_QUOTA_FIELDS +
consts.NOVA_QUOTA_FIELDS)
def _format_quota_set(self, quota_class, quota_set):
"""Convert the quota object to a result dict."""
if quota_class:
result = dict(id=str(quota_class))
else:
result = {}
for quota in self.supported_quotas:
if quota in quota_set:
result[quota] = quota_set[quota]
return dict(quota_class_set=result)
@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, class_name):
context = restcomm.extract_context_from_environ()
LOG.info("Fetch quotas for [class_name=%s]" % class_name)
values = db_api.quota_class_get_all_by_name(context, class_name)
return self._format_quota_set(class_name, values)
@index.when(method='PUT', template='json')
def put(self, class_name):
"""Update a class."""
context = restcomm.extract_context_from_environ()
LOG.info("Update quota class [class_name=%s]" % class_name)
if not context.is_admin:
pecan.abort(403, _('Admin required'))
if not request.body:
pecan.abort(400, _('Body required'))
quota_class_set = eval(request.body).get('quota_class_set')
if not quota_class_set:
pecan.abort(400, _('Missing quota_class_set in the body'))
utils.validate_quota_limits(quota_class_set)
for key, value in six.iteritems(quota_class_set):
try:
db_api.quota_class_update(context, class_name, key, value)
except exceptions.QuotaClassNotFound:
db_api.quota_class_create(context, class_name, key, value)
values = db_api.quota_class_get_all_by_name(context, class_name)
return self._format_quota_set(class_name, values)
@index.when(method='delete', template='json')
def delete(self, class_name):
context = restcomm.extract_context_from_environ()
if not context.is_admin:
pecan.abort(403, _('Admin required'))
LOG.info("Delete quota class [class_name=%s]" % class_name)
try:
db_api.quota_class_destroy_all(context, class_name)
except exceptions.QuotaClassNotFound:
pecan.abort(404, _('Quota class not found'))

View File

@ -13,7 +13,10 @@
# See the License for the specific language governing permissions and
# limitations under the License.
from itertools import izip_longest
import itertools
from kingbird.common import consts
from kingbird.common import exceptions
def get_import_path(cls):
@ -23,4 +26,18 @@ def get_import_path(cls):
# Returns a iterator of tuples containing batch_size number of objects in each
def get_batch_projects(batch_size, project_list, fillvalue=None):
args = [iter(project_list)] * batch_size
return izip_longest(fillvalue=fillvalue, *args)
return itertools.izip_longest(fillvalue=fillvalue, *args)
# to do validate the quota limits
def validate_quota_limits(payload):
for resource in payload:
# Check valid resource name
if resource not in itertools.chain(consts.CINDER_QUOTA_FIELDS,
consts.NOVA_QUOTA_FIELDS,
consts.NEUTRON_QUOTA_FIELDS):
raise exceptions.InvalidInputError
# Check valid quota limit value in case for put/post
if isinstance(payload, dict) and (not isinstance(
payload[resource], int) or payload[resource] <= 0):
raise exceptions.InvalidInputError

View File

@ -0,0 +1,96 @@
# Copyright (c) 2016 Ericsson AB
#
# 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 mock
import webtest
from oslo_config import cfg
from kingbird.api.controllers.v1 import quota_class
from kingbird.common import config
from kingbird.tests.functional.api.testroot import KBFunctionalTest
config.register_options()
OPT_GROUP_NAME = 'keystone_authtoken'
cfg.CONF.import_group(OPT_GROUP_NAME, "keystonemiddleware.auth_token")
class Result(object):
def __init__(self, class_name, resource, hard_limit):
self.class_name = class_name
self.resource = resource
self.hard_limit = hard_limit
class TestQuotaClassController(KBFunctionalTest):
def setUp(self):
super(TestQuotaClassController, self).setUp()
cfg.CONF.set_override('admin_tenant', 'fake_tenant_id',
group='cache')
@mock.patch.object(quota_class, 'db_api')
def test_get_all_admin(self, mock_db_api):
result = Result('class1', 'ram', 100)
mock_db_api.quota_class_get_all_by_name.return_value = \
{"class_name": result.class_name,
result.resource: result.hard_limit}
response = self.app.get(
'/v1.0/fake_tenant_id/os-quota-class-sets/class1',
headers={'X_ROLE': 'admin'})
self.assertEqual(response.status_int, 200)
self.assertEqual({'quota_class_set': {'id': 'class1', 'ram': 100}},
eval(response.text))
@mock.patch.object(quota_class, 'db_api')
def test_put_admin(self, mock_db_api):
result = Result('class1', 'cores', 10)
mock_db_api.quota_class_get_all_by_name.return_value = \
{"class_name": result.class_name,
result.resource: result.hard_limit}
data = {"quota_class_set": {result.resource: result.hard_limit}}
response = self.app.put_json(
'/v1.0/fake_tenant_id/os-quota-class-sets/class1',
headers={'X-Tenant-Id': 'fake_tenant', 'X_ROLE': 'admin'},
params=data)
self.assertEqual(response.status_int, 200)
self.assertEqual({'quota_class_set': {'id': 'class1', 'cores': 10}},
eval(response.text))
@mock.patch.object(quota_class, 'db_api')
def test_delete_all_admin(self, mock_db_api):
result = Result('class1', 'cores', 10)
mock_db_api.quota_destroy_all.return_value = result
response = self.app.delete_json(
'/v1.0/fake_tenant_id/os-quota-class-sets/class1',
headers={'X-Tenant-Id': 'fake_tenant', 'X_ROLE': 'admin'})
self.assertEqual(response.status_int, 200)
def test_delete_all_non_admin(self):
try:
self.app.delete_json(
'/v1.0/fake_tenant_id/os-quota-class-sets/class1',
headers={'X-Tenant-Id': 'fake_tenant'})
except webtest.app.AppError as admin_exception:
self.assertIn('Admin required', admin_exception.message)
def test_put_non_admin(self):
result = Result('class1', 'cores', 10)
data = {"quota_class_set": {result.resource: result.hard_limit}}
try:
self.app.put_json(
'/v1.0/fake_tenant_id/os-quota-class-sets/class1',
headers={'X-Tenant-Id': 'fake_tenant'},
params=data)
except webtest.app.AppError as admin_exception:
self.assertIn('Admin required', admin_exception.message)

View File

@ -118,9 +118,11 @@ class TestV1Controller(KBFunctionalTest):
links = json_body.get('links')
v1_link = links[0]
quota_manager_link = links[1]
quota_class_link = links[1]
quota_manager_link = links[2]
self.assertEqual('self', v1_link['rel'])
self.assertEqual('os-quota-sets', quota_manager_link['rel'])
self.assertEqual('os-quota-class-sets', quota_class_link['rel'])
def _test_method_returns_405(self, method):
api_method = getattr(self.app, method)