Resource Quota - Adding quota API
Change-Id: I7d2da1f86edae002531a529c4ffb469ce9f1777b Partially-Implements: blueprint resource-quota
This commit is contained in:
parent
ccc04d67fc
commit
aa56874bfb
@ -35,6 +35,12 @@
|
||||
"clustertemplate:update": "rule:default",
|
||||
"clustertemplate:publish": "rule:admin_or_owner",
|
||||
|
||||
"quotas:get": "rule:default",
|
||||
"quotas:get_all": "rule:admin_api",
|
||||
"quotas:create": "rule:admin_api",
|
||||
"quotas:update": "rule:admin_api",
|
||||
"quotas:delete": "rule:admin_api",
|
||||
|
||||
"certificate:create": "rule:admin_or_user",
|
||||
"certificate:get": "rule:admin_or_user",
|
||||
|
||||
|
@ -30,6 +30,7 @@ from magnum.api.controllers.v1 import certificate
|
||||
from magnum.api.controllers.v1 import cluster
|
||||
from magnum.api.controllers.v1 import cluster_template
|
||||
from magnum.api.controllers.v1 import magnum_services
|
||||
from magnum.api.controllers.v1 import quota
|
||||
from magnum.api.controllers.v1 import stats
|
||||
from magnum.api.controllers import versions as ver
|
||||
from magnum.api import expose
|
||||
@ -86,6 +87,9 @@ class V1(controllers_base.APIBase):
|
||||
clusters = [link.Link]
|
||||
"""Links to the clusters resource"""
|
||||
|
||||
quotas = [link.Link]
|
||||
"""Links to the quotas resource"""
|
||||
|
||||
certificates = [link.Link]
|
||||
"""Links to the certificates resource"""
|
||||
|
||||
@ -133,6 +137,12 @@ class V1(controllers_base.APIBase):
|
||||
pecan.request.host_url,
|
||||
'clusters', '',
|
||||
bookmark=True)]
|
||||
v1.quotas = [link.Link.make_link('self', pecan.request.host_url,
|
||||
'quotas', ''),
|
||||
link.Link.make_link('bookmark',
|
||||
pecan.request.host_url,
|
||||
'quotas', '',
|
||||
bookmark=True)]
|
||||
v1.certificates = [link.Link.make_link('self', pecan.request.host_url,
|
||||
'certificates', ''),
|
||||
link.Link.make_link('bookmark',
|
||||
@ -161,6 +171,7 @@ class Controller(controllers_base.Controller):
|
||||
baymodels = baymodel.BayModelsController()
|
||||
clusters = cluster.ClustersController()
|
||||
clustertemplates = cluster_template.ClusterTemplatesController()
|
||||
quotas = quota.QuotaController()
|
||||
certificates = certificate.CertificateController()
|
||||
mservices = magnum_services.MagnumServiceController()
|
||||
stats = stats.StatsController()
|
||||
|
211
magnum/api/controllers/v1/quota.py
Normal file
211
magnum/api/controllers/v1/quota.py
Normal file
@ -0,0 +1,211 @@
|
||||
# Copyright 2013 UnitedStack Inc.
|
||||
# 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.
|
||||
|
||||
from oslo_log import log as logging
|
||||
import pecan
|
||||
import wsme
|
||||
from wsme import types as wtypes
|
||||
|
||||
from magnum.api.controllers import base
|
||||
from magnum.api.controllers.v1 import collection
|
||||
from magnum.api import expose
|
||||
from magnum.api import utils as api_utils
|
||||
from magnum.common import exception
|
||||
from magnum.common import policy
|
||||
from magnum.i18n import _
|
||||
from magnum import objects
|
||||
from magnum.objects import fields
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Quota(base.APIBase):
|
||||
"""API representation of a project Quota.
|
||||
|
||||
This class enforces type checking and value constraints, and converts
|
||||
between the internal object model and the API representation of Quota.
|
||||
"""
|
||||
id = wsme.wsattr(wtypes.IntegerType(minimum=1))
|
||||
"""unique id"""
|
||||
|
||||
hard_limit = wsme.wsattr(wtypes.IntegerType(minimum=1), default=1)
|
||||
"""The hard limit for total number of clusters. Default to 1 if not set"""
|
||||
|
||||
project_id = wsme.wsattr(wtypes.StringType(min_length=1, max_length=255),
|
||||
default=None)
|
||||
"""The project id"""
|
||||
|
||||
resource = wsme.wsattr(wtypes.Enum(str, *fields.QuotaResourceName.ALL),
|
||||
default='Cluster')
|
||||
"""The resource name"""
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super(Quota, self).__init__()
|
||||
self.fields = []
|
||||
for field in objects.Quota.fields:
|
||||
# Skip fields we do not expose.
|
||||
if not hasattr(self, field):
|
||||
continue
|
||||
self.fields.append(field)
|
||||
setattr(self, field, kwargs.get(field, wtypes.Unset))
|
||||
|
||||
@classmethod
|
||||
def convert(cls, quota):
|
||||
return Quota(**quota.as_dict())
|
||||
|
||||
|
||||
class QuotaCollection(collection.Collection):
|
||||
"""API representation of a collection of quotas."""
|
||||
|
||||
quotas = [Quota]
|
||||
"""A list containing quota objects"""
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
self._type = 'quotas'
|
||||
|
||||
@staticmethod
|
||||
def convert(quotas, limit, **kwargs):
|
||||
collection = QuotaCollection()
|
||||
collection.quotas = [Quota.convert(p) for p in quotas]
|
||||
collection.next = collection.get_next(limit, **kwargs)
|
||||
return collection
|
||||
|
||||
|
||||
class QuotaController(base.Controller):
|
||||
"""REST controller for Quotas."""
|
||||
|
||||
def __init__(self):
|
||||
super(QuotaController, self).__init__()
|
||||
|
||||
_custom_actions = {
|
||||
'detail': ['GET'],
|
||||
}
|
||||
|
||||
def _get_quota_collection(self, marker, limit, sort_key, sort_dir,
|
||||
filters):
|
||||
|
||||
limit = api_utils.validate_limit(limit)
|
||||
sort_dir = api_utils.validate_sort_dir(sort_dir)
|
||||
|
||||
marker_obj = None
|
||||
if marker:
|
||||
marker_obj = objects.Quota.get_by_id(pecan.request.context,
|
||||
marker)
|
||||
|
||||
quotas = objects.Quota.list(pecan.request.context,
|
||||
limit,
|
||||
marker_obj,
|
||||
sort_key=sort_key,
|
||||
sort_dir=sort_dir,
|
||||
filters=filters)
|
||||
|
||||
return QuotaCollection.convert(quotas,
|
||||
limit,
|
||||
sort_key=sort_key,
|
||||
sort_dir=sort_dir)
|
||||
|
||||
@expose.expose(QuotaCollection, int, int, wtypes.text, wtypes.text, bool)
|
||||
def get_all(self, marker=None, limit=None, sort_key='id',
|
||||
sort_dir='asc', all_tenants=False):
|
||||
"""Retrieve a list of quotas.
|
||||
|
||||
:param marker: pagination marker for large data sets.
|
||||
:param limit: maximum number of resources to return in a single result.
|
||||
:param sort_key: column to sort results by. Default: id.
|
||||
:param sort_dir: direction to sort. "asc" or "desc". Default: asc.
|
||||
:param all_tenants: a flag to indicate all or current tenant.
|
||||
"""
|
||||
context = pecan.request.context
|
||||
policy.enforce(context, 'quota:get_all',
|
||||
action='quota:get_all')
|
||||
|
||||
filters = {}
|
||||
if not context.is_admin or not all_tenants:
|
||||
filters = {"project_id": context.project_id}
|
||||
|
||||
return self._get_quota_collection(marker,
|
||||
limit,
|
||||
sort_key,
|
||||
sort_dir,
|
||||
filters)
|
||||
|
||||
@expose.expose(Quota, wtypes.text, wtypes.text)
|
||||
def get_one(self, project_id, resource):
|
||||
"""Retrieve Quota information for the given project_id.
|
||||
|
||||
:param id: project id.
|
||||
:param resource: resource name.
|
||||
"""
|
||||
context = pecan.request.context
|
||||
policy.enforce(context, 'quota:get', action='quota:get')
|
||||
|
||||
if not context.is_admin and project_id != context.project_id:
|
||||
raise exception.NotAuthorized()
|
||||
|
||||
quota = objects.Quota.get_quota_by_project_id_resource(context,
|
||||
project_id,
|
||||
resource)
|
||||
return Quota.convert(quota)
|
||||
|
||||
@expose.expose(Quota, body=Quota, status_code=201)
|
||||
def post(self, quota):
|
||||
"""Create Quota.
|
||||
|
||||
:param quota: a json document to create this Quota.
|
||||
"""
|
||||
|
||||
context = pecan.request.context
|
||||
policy.enforce(context, 'quota:create', action='quota:create')
|
||||
|
||||
quota_dict = quota.as_dict()
|
||||
if 'project_id'not in quota_dict or not quota_dict['project_id']:
|
||||
msg = _('Must provide a valid project ID.')
|
||||
raise exception.InvalidParameterValue(message=msg)
|
||||
|
||||
new_quota = objects.Quota(context, **quota_dict)
|
||||
new_quota.create()
|
||||
return Quota.convert(new_quota)
|
||||
|
||||
@expose.expose(Quota, wtypes.text, wtypes.text, body=Quota,
|
||||
status_code=202)
|
||||
def patch(self, project_id, resource, quotapatch):
|
||||
"""Update Quota for a given project_id.
|
||||
|
||||
:param project_id: project id.
|
||||
:param resource: resource name.
|
||||
:param quotapatch: a json document to update Quota.
|
||||
"""
|
||||
|
||||
context = pecan.request.context
|
||||
policy.enforce(context, 'quota:update', action='quota:update')
|
||||
quota_dict = quotapatch.as_dict()
|
||||
quota_dict['project_id'] = project_id
|
||||
quota_dict['resource'] = resource
|
||||
db_quota = objects.Quota.update_quota(context, project_id, quota_dict)
|
||||
return Quota.convert(db_quota)
|
||||
|
||||
@expose.expose(None, wtypes.text, wtypes.text, status_code=204)
|
||||
def delete(self, project_id, resource):
|
||||
"""Delete Quota for a given project_id and resource.
|
||||
|
||||
:param project_id: project id.
|
||||
:param resource: resource name.
|
||||
"""
|
||||
|
||||
context = pecan.request.context
|
||||
policy.enforce(context, 'quota:delete', action='quota:delete')
|
||||
quota_dict = {"project_id": project_id, "resource": resource}
|
||||
quota = objects.Quota(context, **quota_dict)
|
||||
quota.delete()
|
@ -16,6 +16,7 @@ from magnum.objects import certificate
|
||||
from magnum.objects import cluster
|
||||
from magnum.objects import cluster_template
|
||||
from magnum.objects import magnum_service
|
||||
from magnum.objects import quota
|
||||
from magnum.objects import stats
|
||||
from magnum.objects import x509keypair
|
||||
|
||||
@ -23,6 +24,7 @@ from magnum.objects import x509keypair
|
||||
Cluster = cluster.Cluster
|
||||
ClusterTemplate = cluster_template.ClusterTemplate
|
||||
MagnumService = magnum_service.MagnumService
|
||||
Quota = quota.Quota
|
||||
X509KeyPair = x509keypair.X509KeyPair
|
||||
Certificate = certificate.Certificate
|
||||
Stats = stats.Stats
|
||||
@ -31,4 +33,5 @@ __all__ = (Cluster,
|
||||
MagnumService,
|
||||
X509KeyPair,
|
||||
Certificate,
|
||||
Stats)
|
||||
Stats,
|
||||
Quota)
|
||||
|
@ -84,6 +84,18 @@ class DockerStorageDriver(fields.Enum):
|
||||
valid_values=DockerStorageDriver.ALL)
|
||||
|
||||
|
||||
class QuotaResourceName(fields.Enum):
|
||||
ALL = (
|
||||
CLUSTER,
|
||||
) = (
|
||||
'Cluster',
|
||||
)
|
||||
|
||||
def __init__(self):
|
||||
super(QuotaResourceName, self).__init__(
|
||||
valid_values=QuotaResourceName.ALL)
|
||||
|
||||
|
||||
class ServerType(fields.Enum):
|
||||
ALL = (
|
||||
VM, BM,
|
||||
|
142
magnum/objects/quota.py
Normal file
142
magnum/objects/quota.py
Normal file
@ -0,0 +1,142 @@
|
||||
# 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_versionedobjects import fields
|
||||
|
||||
from magnum.db import api as dbapi
|
||||
from magnum.objects import base
|
||||
|
||||
|
||||
@base.MagnumObjectRegistry.register
|
||||
class Quota(base.MagnumPersistentObject, base.MagnumObject,
|
||||
base.MagnumObjectDictCompat):
|
||||
# Version 1.0: Initial version
|
||||
VERSION = '1.0'
|
||||
|
||||
dbapi = dbapi.get_instance()
|
||||
|
||||
fields = {
|
||||
'id': fields.IntegerField(),
|
||||
'project_id': fields.StringField(nullable=False),
|
||||
'resource': fields.StringField(nullable=False),
|
||||
'hard_limit': fields.IntegerField(nullable=False),
|
||||
}
|
||||
|
||||
@base.remotable_classmethod
|
||||
def get_quota_by_project_id_resource(cls, context, project_id, resource):
|
||||
"""Find a quota based on its integer id and return a Quota object.
|
||||
|
||||
:param project_id: the id of a project.
|
||||
:param resource: resource name.
|
||||
:param context: Security context
|
||||
:returns: a :class:`Quota` object.
|
||||
"""
|
||||
db_quota = cls.dbapi.get_quota_by_project_id_resource(project_id,
|
||||
resource)
|
||||
quota = Quota._from_db_object(cls(context), db_quota)
|
||||
return quota
|
||||
|
||||
@staticmethod
|
||||
def _from_db_object(quota, db_quota):
|
||||
"""Converts a database entity to a formal object."""
|
||||
for field in quota.fields:
|
||||
setattr(quota, field, db_quota[field])
|
||||
|
||||
quota.obj_reset_changes()
|
||||
return quota
|
||||
|
||||
@staticmethod
|
||||
def _from_db_object_list(db_objects, cls, context):
|
||||
"""Converts a list of database entities to a list of formal objects."""
|
||||
return [Quota._from_db_object(cls(context), obj)
|
||||
for obj in db_objects]
|
||||
|
||||
@base.remotable_classmethod
|
||||
def get_by_id(cls, context, quota_id):
|
||||
"""Find a quota based on its integer id and return a Quota object.
|
||||
|
||||
:param quota_id: the id of a quota.
|
||||
:param context: Security context
|
||||
:returns: a :class:`Quota` object.
|
||||
"""
|
||||
db_quota = cls.dbapi.get_quota_by_id(context, quota_id)
|
||||
quota = Quota._from_db_object(cls(context), db_quota)
|
||||
return quota
|
||||
|
||||
@base.remotable_classmethod
|
||||
def list(cls, context, limit=None, marker=None,
|
||||
sort_key=None, sort_dir=None, filters=None):
|
||||
"""Return a list of Quota objects.
|
||||
|
||||
:param context: Security context.
|
||||
:param limit: maximum number of resources to return in a single result.
|
||||
:param marker: pagination marker for large data sets.
|
||||
:param sort_key: column to sort results by.
|
||||
:param sort_dir: direction to sort. "asc" or "desc".
|
||||
:param filters: filter dict, can includes 'project_id',
|
||||
'resource'.
|
||||
:returns: a list of :class:`Quota` object.
|
||||
|
||||
"""
|
||||
db_quotas = cls.dbapi.get_quota_list(context,
|
||||
limit=limit,
|
||||
marker=marker,
|
||||
sort_key=sort_key,
|
||||
sort_dir=sort_dir,
|
||||
filters=filters)
|
||||
return Quota._from_db_object_list(db_quotas, cls, context)
|
||||
|
||||
@base.remotable_classmethod
|
||||
def quota_get_all_by_project_id(cls, context, project_id):
|
||||
"""Find a quota based on project id.
|
||||
|
||||
:param project_id: the project id.
|
||||
:param context: Security context
|
||||
:returns: a :class:`Quota` object.
|
||||
"""
|
||||
quotas = cls.dbapi.get_quota_by_project_id(context, project_id)
|
||||
return Quota._from_db_object_list(quotas, cls, context)
|
||||
|
||||
@base.remotable
|
||||
def create(self, context=None):
|
||||
"""Save a quota based on project id.
|
||||
|
||||
:param context: security context.
|
||||
:returns: a :class:`Quota` object.
|
||||
"""
|
||||
values = self.obj_get_changes()
|
||||
db_quota = self.dbapi.create_quota(values)
|
||||
self._from_db_object(self, db_quota)
|
||||
|
||||
@base.remotable
|
||||
def delete(self, context=None):
|
||||
"""Delete the quota from the DB.
|
||||
|
||||
:param context: Security context. NOTE: This should only
|
||||
be used internally by the indirection_api.
|
||||
Unfortunately, RPC requires context as the first
|
||||
argument, even though we don't use it.
|
||||
A context should be set when instantiating the
|
||||
object, e.g.: Quota(context)
|
||||
"""
|
||||
self.dbapi.delete_quota(self.project_id, self.resource)
|
||||
self.obj_reset_changes()
|
||||
|
||||
@base.remotable_classmethod
|
||||
def update_quota(cls, context, project_id, quota):
|
||||
"""Save a quota based on project id.
|
||||
|
||||
:param quota: quota.
|
||||
:returns: a :class:`Quota` object.
|
||||
"""
|
||||
db_quota = cls.dbapi.update_quota(project_id, quota)
|
||||
return Quota._from_db_object(cls(context), db_quota)
|
@ -69,6 +69,10 @@ class TestRootController(api_base.FunctionalTest):
|
||||
u'rel': u'self'},
|
||||
{u'href': u'http://localhost/clusters/',
|
||||
u'rel': u'bookmark'}],
|
||||
u'quotas': [{u'href': u'http://localhost/v1/quotas/',
|
||||
u'rel': u'self'},
|
||||
{u'href': u'http://localhost/quotas/',
|
||||
u'rel': u'bookmark'}],
|
||||
u'clustertemplates':
|
||||
[{u'href': u'http://localhost/v1/clustertemplates/',
|
||||
u'rel': u'self'},
|
||||
|
210
magnum/tests/unit/api/controllers/v1/test_quota.py
Normal file
210
magnum/tests/unit/api/controllers/v1/test_quota.py
Normal file
@ -0,0 +1,210 @@
|
||||
# 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
|
||||
|
||||
from magnum.api.controllers.v1 import quota as api_quota
|
||||
from magnum.tests import base
|
||||
from magnum.tests.unit.api import base as api_base
|
||||
from magnum.tests.unit.api import utils as apiutils
|
||||
from magnum.tests.unit.objects import utils as obj_utils
|
||||
|
||||
|
||||
class TestQuotaObject(base.TestCase):
|
||||
def test_quota_init(self):
|
||||
quota_dict = apiutils.quota_post_data()
|
||||
del quota_dict['hard_limit']
|
||||
quota = api_quota.Quota(**quota_dict)
|
||||
self.assertEqual(1, quota.hard_limit)
|
||||
|
||||
|
||||
class TestQuota(api_base.FunctionalTest):
|
||||
_quota_attrs = ("project_id", "resource", "hard_limit")
|
||||
|
||||
def setUp(self):
|
||||
super(TestQuota, self).setUp()
|
||||
|
||||
def test_empty(self):
|
||||
response = self.get_json('/quotas')
|
||||
self.assertEqual([], response['quotas'])
|
||||
|
||||
def test_one(self):
|
||||
quota = obj_utils.create_test_quota(self.context)
|
||||
response = self.get_json('/quotas')
|
||||
self.assertEqual(quota.project_id, response['quotas'][0]["project_id"])
|
||||
self._verify_attrs(self._quota_attrs, response['quotas'][0])
|
||||
|
||||
def test_get_one(self):
|
||||
quota = obj_utils.create_test_quota(self.context)
|
||||
response = self.get_json('/quotas/%s/%s' % (quota['project_id'],
|
||||
quota['resource']))
|
||||
self.assertEqual(quota.project_id, response['project_id'])
|
||||
self.assertEqual(quota.resource, response['resource'])
|
||||
|
||||
def test_get_one_not_found(self):
|
||||
response = self.get_json(
|
||||
'/quotas/fake_project/invalid_res',
|
||||
expect_errors=True)
|
||||
self.assertEqual(404, response.status_int)
|
||||
self.assertEqual('application/json', response.content_type)
|
||||
self.assertTrue(response.json['errors'])
|
||||
|
||||
def test_get_one_not_authorized(self):
|
||||
obj_utils.create_test_quota(self.context)
|
||||
response = self.get_json(
|
||||
'/quotas/invalid_proj/invalid_res',
|
||||
expect_errors=True)
|
||||
self.assertEqual(403, response.status_int)
|
||||
self.assertEqual('application/json', response.content_type)
|
||||
self.assertTrue(response.json['errors'])
|
||||
|
||||
@mock.patch("magnum.common.policy.enforce")
|
||||
@mock.patch("magnum.common.context.make_context")
|
||||
def test_get_all_admin_all_tenants(self, mock_context, mock_policy):
|
||||
mock_context.return_value = self.context
|
||||
quota_list = []
|
||||
for i in range(4):
|
||||
quota = obj_utils.create_test_quota(self.context,
|
||||
project_id="proj-id-"+str(i))
|
||||
quota_list.append(quota)
|
||||
|
||||
self.context.is_admin = True
|
||||
response = self.get_json('/quotas?all_tenants=True')
|
||||
self.assertEqual(4, len(response['quotas']))
|
||||
expected = [r.project_id for r in quota_list]
|
||||
res_proj_ids = [r['project_id'] for r in response['quotas']]
|
||||
self.assertEqual(sorted(expected), sorted(res_proj_ids))
|
||||
|
||||
@mock.patch("magnum.common.policy.enforce")
|
||||
@mock.patch("magnum.common.context.make_context")
|
||||
def test_get_all_admin_not_all_tenants(self, mock_context, mock_policy):
|
||||
mock_context.return_value = self.context
|
||||
quota_list = []
|
||||
for i in range(4):
|
||||
quota = obj_utils.create_test_quota(self.context,
|
||||
project_id="proj-id-"+str(i))
|
||||
quota_list.append(quota)
|
||||
|
||||
self.context.is_admin = True
|
||||
self.context.project_id = 'proj-id-1'
|
||||
response = self.get_json('/quotas')
|
||||
self.assertEqual(1, len(response['quotas']))
|
||||
self.assertEqual('proj-id-1', response['quotas'][0]['project_id'])
|
||||
|
||||
@mock.patch("magnum.common.policy.enforce")
|
||||
@mock.patch("magnum.common.context.make_context")
|
||||
def test_get_all_admin_all_with_pagination_marker(self, mock_context,
|
||||
mock_policy):
|
||||
mock_context.return_value = self.context
|
||||
quota_list = []
|
||||
for i in range(4):
|
||||
quota = obj_utils.create_test_quota(self.context,
|
||||
project_id="proj-id-"+str(i))
|
||||
quota_list.append(quota)
|
||||
|
||||
self.context.is_admin = True
|
||||
response = self.get_json('/quotas?limit=3&marker=%s&all_tenants=True'
|
||||
% quota_list[2].id)
|
||||
self.assertEqual(1, len(response['quotas']))
|
||||
self.assertEqual(quota_list[-1].project_id,
|
||||
response['quotas'][0]['project_id'])
|
||||
|
||||
def test_get_all_non_admin(self):
|
||||
quota_list = []
|
||||
for i in range(4):
|
||||
quota = obj_utils.create_test_quota(self.context,
|
||||
project_id="proj-id-"+str(i))
|
||||
quota_list.append(quota)
|
||||
|
||||
headers = {'X-Project-Id': 'proj-id-2'}
|
||||
response = self.get_json('/quotas', headers=headers)
|
||||
self.assertEqual(1, len(response['quotas']))
|
||||
self.assertEqual('proj-id-2', response['quotas'][0]['project_id'])
|
||||
|
||||
def test_create_quota(self):
|
||||
quota_dict = apiutils.quota_post_data()
|
||||
response = self.post_json('/quotas', quota_dict)
|
||||
self.assertEqual('application/json', response.content_type)
|
||||
self.assertEqual(201, response.status_int)
|
||||
self.assertEqual(quota_dict['project_id'], response.json['project_id'])
|
||||
|
||||
def test_create_quota_invalid_resource(self):
|
||||
quota_dict = apiutils.quota_post_data()
|
||||
quota_dict['resource'] = 'invalid-res'
|
||||
response = self.post_json('/quotas', quota_dict, expect_errors=True)
|
||||
self.assertEqual('application/json', response.content_type)
|
||||
self.assertEqual(400, response.status_int)
|
||||
self.assertTrue(response.json['errors'])
|
||||
|
||||
def test_create_quota_invalid_hard_limit(self):
|
||||
quota_dict = apiutils.quota_post_data()
|
||||
quota_dict['hard_limit'] = -10
|
||||
response = self.post_json('/quotas', quota_dict, expect_errors=True)
|
||||
self.assertEqual('application/json', response.content_type)
|
||||
self.assertEqual(400, response.status_int)
|
||||
self.assertTrue(response.json['errors'])
|
||||
|
||||
def test_create_quota_no_project_id(self):
|
||||
quota_dict = apiutils.quota_post_data()
|
||||
del quota_dict['project_id']
|
||||
response = self.post_json('/quotas', quota_dict, expect_errors=True)
|
||||
self.assertEqual('application/json', response.content_type)
|
||||
self.assertEqual(400, response.status_int)
|
||||
self.assertTrue(response.json['errors'])
|
||||
|
||||
def test_patch_quota(self):
|
||||
quota_dict = apiutils.quota_post_data(hard_limit=5)
|
||||
response = self.post_json('/quotas', quota_dict)
|
||||
self.assertEqual('application/json', response.content_type)
|
||||
self.assertEqual(201, response.status_int)
|
||||
self.assertEqual(quota_dict['project_id'], response.json['project_id'])
|
||||
self.assertEqual(5, response.json['hard_limit'])
|
||||
|
||||
quota_dict['hard_limit'] = 20
|
||||
response = self.patch_json('/quotas', quota_dict)
|
||||
self.assertEqual('application/json', response.content_type)
|
||||
self.assertEqual(202, response.status_int)
|
||||
self.assertEqual(20, response.json['hard_limit'])
|
||||
|
||||
def test_patch_quota_not_found(self):
|
||||
quota_dict = apiutils.quota_post_data()
|
||||
response = self.post_json('/quotas', quota_dict)
|
||||
self.assertEqual('application/json', response.content_type)
|
||||
self.assertEqual(201, response.status_int)
|
||||
|
||||
# update quota with non-existing project id
|
||||
update_dict = {'project_id': 'not-found',
|
||||
'hard_limit': 20,
|
||||
'resource': 'Cluster'}
|
||||
response = self.patch_json('/quotas', update_dict, expect_errors=True)
|
||||
self.assertEqual('application/json', response.content_type)
|
||||
self.assertEqual(404, response.status_int)
|
||||
self.assertTrue(response.json['errors'])
|
||||
|
||||
def test_delete_quota(self):
|
||||
quota_dict = apiutils.quota_post_data()
|
||||
response = self.post_json('/quotas', quota_dict)
|
||||
self.assertEqual('application/json', response.content_type)
|
||||
self.assertEqual(201, response.status_int)
|
||||
|
||||
project_id = quota_dict['project_id']
|
||||
resource = quota_dict['resource']
|
||||
# delete quota
|
||||
self.delete('/quotas/%s/%s' % (project_id, resource))
|
||||
|
||||
# now check that quota does not exist
|
||||
response = self.get_json(
|
||||
'/quotas/%s/%s' % (project_id, resource),
|
||||
expect_errors=True)
|
||||
self.assertEqual(404, response.status_int)
|
||||
self.assertEqual('application/json', response.content_type)
|
||||
self.assertTrue(response.json['errors'])
|
@ -67,6 +67,10 @@ def cert_post_data(**kw):
|
||||
}
|
||||
|
||||
|
||||
def quota_post_data(**kw):
|
||||
return utils.get_test_quota(**kw)
|
||||
|
||||
|
||||
def mservice_get_data(**kw):
|
||||
"""Simulate what the RPC layer will get from DB """
|
||||
faketime = datetime.datetime(2001, 1, 1, tzinfo=pytz.UTC)
|
||||
|
@ -362,6 +362,7 @@ object_data = {
|
||||
'X509KeyPair': '1.2-d81950af36c59a71365e33ce539d24f9',
|
||||
'MagnumService': '1.0-2d397ec59b0046bd5ec35cd3e06efeca',
|
||||
'Stats': '1.0-73a1cd6e3c0294c932a66547faba216c',
|
||||
'Quota': '1.0-94e100aebfa88f7d8428e007f2049c18',
|
||||
}
|
||||
|
||||
|
||||
|
@ -89,6 +89,33 @@ def create_test_cluster(context, **kw):
|
||||
return cluster
|
||||
|
||||
|
||||
def get_test_quota(context, **kw):
|
||||
"""Return a Quota object with appropriate attributes.
|
||||
|
||||
NOTE: The object leaves the attributes marked as changed, such
|
||||
that a create() could be used to commit it to the DB.
|
||||
"""
|
||||
db_quota = db_utils.get_test_quota(**kw)
|
||||
# Let DB generate ID if it isn't specified explicitly
|
||||
if 'id' not in kw:
|
||||
del db_quota['id']
|
||||
quota = objects.Quota(context)
|
||||
for key in db_quota:
|
||||
setattr(quota, key, db_quota[key])
|
||||
return quota
|
||||
|
||||
|
||||
def create_test_quota(context, **kw):
|
||||
"""Create and return a test Quota object.
|
||||
|
||||
Create a quota in the DB and return a Quota object with appropriate
|
||||
attributes.
|
||||
"""
|
||||
quota = get_test_quota(context, **kw)
|
||||
quota.create()
|
||||
return quota
|
||||
|
||||
|
||||
def get_test_x509keypair(context, **kw):
|
||||
"""Return a X509KeyPair object with appropriate attributes.
|
||||
|
||||
|
5
releasenotes/notes/quota-api-182cd1bc9e706b17.yaml
Normal file
5
releasenotes/notes/quota-api-182cd1bc9e706b17.yaml
Normal file
@ -0,0 +1,5 @@
|
||||
---
|
||||
features:
|
||||
- This release introduces 'quota' endpoint that enable admin
|
||||
users to set, update and show quota for a given tenant.
|
||||
A non-admin user can get self quota limits.
|
Loading…
x
Reference in New Issue
Block a user