Merge "Add API layer for quota class management"
This commit is contained in:
commit
53d8732d8f
@ -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
|
||||
|
@ -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():
|
||||
|
0
kingbird/api/controllers/v1/__init__.py
Normal file
0
kingbird/api/controllers/v1/__init__.py
Normal file
109
kingbird/api/controllers/v1/quota_class.py
Normal file
109
kingbird/api/controllers/v1/quota_class.py
Normal 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'))
|
@ -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
|
||||
|
96
kingbird/tests/functional/api/test_quota_class.py
Normal file
96
kingbird/tests/functional/api/test_quota_class.py
Normal 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)
|
@ -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)
|
||||
|
Loading…
x
Reference in New Issue
Block a user