Add API layer for quota class management
Similarly to quota api, this set of calls will help managing quota classes. Started refactoring the api controllers directory to have versioned controllers. Change-Id: I79429cc9876499a4fad84cc23148584407777531
This commit is contained in:
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user