Add Quota class data model
In order to fully support quota we need quota classes. Added implementation of QuotaClass model as well as db api methods for creating/updating/deleting quota classes. Change-Id: I24278b12a7242ece8500a3800bb42904fd9aafc2
This commit is contained in:
parent
fe721d02f4
commit
61605082f2
@ -50,7 +50,6 @@ LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class QuotaManagerController(object):
|
||||
|
||||
VERSION_ALIASES = {
|
||||
'mitaka': '1.0',
|
||||
}
|
||||
@ -89,8 +88,8 @@ class QuotaManagerController(object):
|
||||
if project_id == 'defaults':
|
||||
# Get default quota limits from conf file
|
||||
for resource, limit in \
|
||||
CONF.kingbird_global_limit.iteritems():
|
||||
result[resource.replace('quota_', '')] = limit
|
||||
CONF.kingbird_global_limit.iteritems():
|
||||
result[resource.replace('quota_', '')] = limit
|
||||
else:
|
||||
if action and action != 'detail':
|
||||
pecan.abort(404, _('Invalid request URL'))
|
||||
@ -174,7 +173,7 @@ class QuotaManagerController(object):
|
||||
db_api.quota_destroy_all(context, project_id)
|
||||
return "Deleted all quota limits for the given project"
|
||||
except exceptions.ProjectQuotaNotFound:
|
||||
pecan.abort(404, _('Project quota not found'))
|
||||
pecan.abort(404, _('Project quota not found'))
|
||||
except exceptions.InvalidInputError:
|
||||
pecan.abort(400, _('Invalid input for quota'))
|
||||
|
||||
@ -199,5 +198,5 @@ class QuotaManagerController(object):
|
||||
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
|
||||
payload[resource], int) or payload[resource] <= 0):
|
||||
raise exceptions.InvalidInputError
|
||||
|
@ -89,6 +89,10 @@ class ProjectQuotaNotFound(NotFound):
|
||||
message = _("Quota for project %(project_id) doesn't exist.")
|
||||
|
||||
|
||||
class QuotaClassNotFound(NotFound):
|
||||
message = _("Quota class %(class_name) doesn't exist.")
|
||||
|
||||
|
||||
class ConnectionRefused(KingbirdException):
|
||||
message = _("Connection to the service endpoint is refused")
|
||||
|
||||
|
@ -71,6 +71,41 @@ def quota_destroy_all(context, project_id):
|
||||
return IMPL.quota_destroy(context, project_id)
|
||||
|
||||
|
||||
def quota_class_get(context, class_name, resource):
|
||||
"""Retrieve quota from the given quota class"""
|
||||
return IMPL.quota_class_get(context, class_name, resource)
|
||||
|
||||
|
||||
def quota_class_get_default(context):
|
||||
"""Get default class quotas"""
|
||||
return IMPL.quota_class_get_default(context)
|
||||
|
||||
|
||||
def quota_class_get_all_by_name(context, class_name):
|
||||
"""Get all quota limits for a specified class"""
|
||||
return IMPL.quota_class_get_all_by_name(context, class_name)
|
||||
|
||||
|
||||
def quota_class_create(context, class_name, resource, limit):
|
||||
"""Create a new quota limit in a specified class"""
|
||||
return IMPL.quota_class_create(context, class_name, resource, limit)
|
||||
|
||||
|
||||
def quota_class_destroy(context, class_name, resource):
|
||||
"""Destroy a class quota """
|
||||
return IMPL.quota_class_destroy(context, class_name, resource)
|
||||
|
||||
|
||||
def quota_class_destroy_all(context, class_name):
|
||||
"""Destroy all quotas for class"""
|
||||
return IMPL.quota_class_destroy_all(context, class_name)
|
||||
|
||||
|
||||
def quota_class_update(context, class_name, resource, limit):
|
||||
"""Update a quota or raise if it doesn't exist """
|
||||
return IMPL.quota_class_update(context, class_name, resource, limit)
|
||||
|
||||
|
||||
def db_sync(engine, version=None):
|
||||
"""Migrate the database to `version` or the most recent version."""
|
||||
return IMPL.db_sync(engine, version=version)
|
||||
|
@ -47,6 +47,8 @@ def get_facade():
|
||||
get_engine = lambda: get_facade().get_engine()
|
||||
get_session = lambda: get_facade().get_session()
|
||||
|
||||
_DEFAULT_QUOTA_NAME = 'default'
|
||||
|
||||
|
||||
def get_backend():
|
||||
"""The backend is this module itself."""
|
||||
@ -191,6 +193,89 @@ def quota_destroy_all(context, project_id):
|
||||
quota_ref.delete(session=session)
|
||||
|
||||
|
||||
##########################
|
||||
|
||||
@require_context
|
||||
def _quota_class_get(context, class_name, resource):
|
||||
result = model_query(context, models.QuotaClass). \
|
||||
filter_by(deleted=False). \
|
||||
filter_by(class_name=class_name). \
|
||||
filter_by(resource=resource). \
|
||||
first()
|
||||
|
||||
if not result:
|
||||
raise exception.QuotaClassNotFound(class_name=class_name)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
@require_context
|
||||
def quota_class_get(context, class_name, resource):
|
||||
return _quota_class_get(context, class_name, resource)
|
||||
|
||||
|
||||
@require_context
|
||||
def quota_class_get_default(context):
|
||||
return quota_class_get_all_by_name(context, _DEFAULT_QUOTA_NAME)
|
||||
|
||||
|
||||
@require_context
|
||||
def quota_class_get_all_by_name(context, class_name):
|
||||
rows = model_query(context, models.QuotaClass). \
|
||||
filter_by(deleted=False). \
|
||||
filter_by(class_name=class_name). \
|
||||
all()
|
||||
|
||||
result = {'class_name': class_name}
|
||||
for row in rows:
|
||||
result[row.resource] = row.hard_limit
|
||||
|
||||
return result
|
||||
|
||||
|
||||
@require_admin_context
|
||||
def quota_class_create(context, class_name, resource, limit):
|
||||
quota_class_ref = models.QuotaClass()
|
||||
quota_class_ref.class_name = class_name
|
||||
quota_class_ref.resource = resource
|
||||
quota_class_ref.hard_limit = limit
|
||||
session = _session(context)
|
||||
with session.begin():
|
||||
quota_class_ref.save(session)
|
||||
return quota_class_ref
|
||||
|
||||
|
||||
@require_admin_context
|
||||
def quota_class_update(context, class_name, resource, limit):
|
||||
result = model_query(context, models.QuotaClass). \
|
||||
filter_by(deleted=False). \
|
||||
filter_by(class_name=class_name) .\
|
||||
filter_by(resource=resource). \
|
||||
update({'hard_limit': limit})
|
||||
|
||||
if not result:
|
||||
raise exception.QuotaClassNotFound(class_name=class_name)
|
||||
|
||||
|
||||
@require_admin_context
|
||||
def quota_class_destroy(context, class_name, resource):
|
||||
session = _session(context)
|
||||
quota_class_ref = _quota_class_get(context, class_name, resource)
|
||||
quota_class_ref.delete(session=session)
|
||||
|
||||
|
||||
@require_admin_context
|
||||
def quota_class_destroy_all(context, class_name):
|
||||
session = _session(context)
|
||||
|
||||
quota_classes = model_query(context, models.QuotaClass) .\
|
||||
filter_by(deleted=False). \
|
||||
filter_by(class_name=class_name). \
|
||||
all()
|
||||
for quota_class_ref in quota_classes:
|
||||
quota_class_ref.delete(session=session)
|
||||
|
||||
|
||||
def db_sync(engine, version=None):
|
||||
"""Migrate the database to `version` or the most recent version."""
|
||||
return migration.db_sync(engine, version=version)
|
||||
|
@ -1,3 +1,4 @@
|
||||
# Copyright (c) 2015 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
|
||||
|
@ -0,0 +1,37 @@
|
||||
# Copyright (c) 2015 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 sqlalchemy
|
||||
|
||||
|
||||
def upgrade(migrate_engine):
|
||||
meta = sqlalchemy.MetaData()
|
||||
meta.bind = migrate_engine
|
||||
|
||||
quota_classes = sqlalchemy.Table(
|
||||
'quota_classes', meta,
|
||||
sqlalchemy.Column('id', sqlalchemy.Integer,
|
||||
primary_key=True, nullable=False),
|
||||
sqlalchemy.Column('class_name', sqlalchemy.String(length=255),
|
||||
index=True),
|
||||
sqlalchemy.Column('created_at', sqlalchemy.DateTime),
|
||||
sqlalchemy.Column('updated_at', sqlalchemy.DateTime),
|
||||
sqlalchemy.Column('deleted_at', sqlalchemy.DateTime),
|
||||
sqlalchemy.Column('deleted', sqlalchemy.Integer),
|
||||
sqlalchemy.Column('resource', sqlalchemy.String(length=255)),
|
||||
sqlalchemy.Column('hard_limit', sqlalchemy.Integer,
|
||||
nullable=True),
|
||||
mysql_engine='InnoDB',
|
||||
mysql_charset='utf8'
|
||||
)
|
||||
quota_classes.create()
|
@ -20,7 +20,7 @@ from oslo_config import cfg
|
||||
from oslo_db.sqlalchemy import models
|
||||
|
||||
from sqlalchemy.orm import session as orm_session
|
||||
from sqlalchemy import (Column, Integer, String)
|
||||
from sqlalchemy import (Column, Integer, String, schema)
|
||||
from sqlalchemy.ext.declarative import declarative_base
|
||||
|
||||
CONF = cfg.CONF
|
||||
@ -66,15 +66,48 @@ class KingbirdBase(models.ModelBase,
|
||||
|
||||
|
||||
class Quota(BASE, KingbirdBase):
|
||||
"""Represents a single quota override for a project.
|
||||
|
||||
If there is no row for a given project id and resource, then the
|
||||
default for the quota class is used. If there is no row for a
|
||||
given quota class and resource, then the default for the
|
||||
deployment is used. If the row is present but the hard limit is
|
||||
Null, then the resource is unlimited.
|
||||
"""
|
||||
|
||||
__tablename__ = 'quotas'
|
||||
|
||||
__table_args__ = (
|
||||
schema.UniqueConstraint("project_id", "resource", "deleted",
|
||||
name="uniq_quotas0project_id0resource0deleted"
|
||||
),)
|
||||
|
||||
id = Column(Integer, primary_key=True)
|
||||
|
||||
project_id = Column(String(36))
|
||||
project_id = Column(String(255), index=True)
|
||||
|
||||
resource = Column(String(255), nullable=False)
|
||||
|
||||
hard_limit = Column(Integer, nullable=False)
|
||||
hard_limit = Column(Integer, nullable=True)
|
||||
|
||||
|
||||
class QuotaClass(BASE, KingbirdBase):
|
||||
"""Represents a single quota override for a quota class.
|
||||
|
||||
If there is no row for a given quota class and resource, then the
|
||||
default for the deployment is used. If the row is present but the
|
||||
hard limit is Null, then the resource is unlimited.
|
||||
"""
|
||||
|
||||
__tablename__ = "quota_classes"
|
||||
|
||||
id = Column(Integer, primary_key=True)
|
||||
|
||||
class_name = Column(String(255), index=True)
|
||||
|
||||
resource = Column(String(255))
|
||||
|
||||
hard_limit = Column(Integer, nullable=True)
|
||||
|
||||
|
||||
class SyncLock(BASE, KingbirdBase):
|
||||
|
@ -24,7 +24,6 @@ from kingbird.db.sqlalchemy import api as db_api
|
||||
from kingbird.tests import base
|
||||
from kingbird.tests import utils
|
||||
|
||||
|
||||
get_engine = api.get_engine
|
||||
UUID1 = utils.UUID1
|
||||
UUID2 = utils.UUID2
|
||||
@ -41,7 +40,8 @@ class DBAPIQuotaTest(base.KingbirdTestCase):
|
||||
db_api.db_sync(engine)
|
||||
engine.connect()
|
||||
|
||||
def reset_dummy_db(self):
|
||||
@staticmethod
|
||||
def reset_dummy_db():
|
||||
engine = get_engine()
|
||||
meta = sqlalchemy.MetaData()
|
||||
meta.reflect(bind=engine)
|
||||
@ -51,7 +51,8 @@ class DBAPIQuotaTest(base.KingbirdTestCase):
|
||||
continue
|
||||
engine.execute(table.delete())
|
||||
|
||||
def create_quota_limit(self, ctxt, **kwargs):
|
||||
@staticmethod
|
||||
def create_quota_limit(ctxt, **kwargs):
|
||||
values = {
|
||||
'project_id': utils.UUID1,
|
||||
'resource': "ram",
|
||||
@ -60,6 +61,16 @@ class DBAPIQuotaTest(base.KingbirdTestCase):
|
||||
values.update(kwargs)
|
||||
return db_api.quota_create(ctxt, **values)
|
||||
|
||||
@staticmethod
|
||||
def create_quota_class(ctxt, **kwargs):
|
||||
values = {
|
||||
'class_name': "test_class",
|
||||
'resource': "ram",
|
||||
'limit': 10,
|
||||
}
|
||||
values.update(kwargs)
|
||||
return db_api.quota_class_create(ctxt, **values)
|
||||
|
||||
def setUp(self):
|
||||
super(DBAPIQuotaTest, self).setUp()
|
||||
|
||||
@ -135,8 +146,64 @@ class DBAPIQuotaTest(base.KingbirdTestCase):
|
||||
self.assertIsNotNone(by_project)
|
||||
self.assertEqual(project_id, by_project['project_id'])
|
||||
|
||||
def test_quota_get_by_nonexisting_project(self):
|
||||
def test_quota_get_by_non_existing_project(self):
|
||||
project_id = UUID2
|
||||
expected_quota_set = {'project_id': project_id}
|
||||
project_limit = db_api.quota_get_all_by_project(self.ctx, project_id)
|
||||
self.assertEqual(project_limit, expected_quota_set)
|
||||
|
||||
def test_quota_class_create(self):
|
||||
class_name = "test_class"
|
||||
resource = "cores"
|
||||
quota_class = self.create_quota_class(self.ctx, class_name=class_name,
|
||||
resource=resource, limit=20)
|
||||
|
||||
self.assertIsNotNone(quota_class)
|
||||
|
||||
q_class = db_api.quota_class_get(self.ctx, class_name, resource)
|
||||
self.assertIsNotNone(q_class)
|
||||
self.assertEqual(20, q_class.hard_limit)
|
||||
|
||||
def test_quota_class_update(self):
|
||||
class_name = "test_class"
|
||||
resource = "cores"
|
||||
quota_class = self.create_quota_class(self.ctx, class_name=class_name,
|
||||
resource=resource, limit=20)
|
||||
self.assertIsNotNone(quota_class)
|
||||
|
||||
db_api.quota_class_update(self.ctx, class_name=class_name,
|
||||
resource=resource, limit=30)
|
||||
|
||||
updated_class = db_api.quota_class_get(self.ctx, class_name=class_name,
|
||||
resource=resource)
|
||||
self.assertEqual(30, updated_class.hard_limit)
|
||||
|
||||
def test_quota_class_destroy(self):
|
||||
class_name = "test_class"
|
||||
resource = "cores"
|
||||
quota_class = self.create_quota_class(self.ctx, class_name=class_name,
|
||||
resource=resource, limit=20)
|
||||
self.assertIsNotNone(quota_class)
|
||||
|
||||
db_api.quota_class_destroy(self.ctx, class_name=class_name,
|
||||
resource=resource)
|
||||
|
||||
self.assertRaises(exceptions.QuotaClassNotFound,
|
||||
db_api.quota_class_get,
|
||||
self.ctx, class_name, resource)
|
||||
|
||||
def test_quota_class_destroy_all(self):
|
||||
class_name = "test_class"
|
||||
self.create_quota_class(self.ctx, class_name=class_name,
|
||||
resource='cores', limit=1)
|
||||
self.create_quota_class(self.ctx, class_name=class_name,
|
||||
resource='ram', limit=4)
|
||||
|
||||
db_api.quota_class_destroy_all(self.ctx, class_name=class_name)
|
||||
|
||||
self.assertRaises(exceptions.QuotaClassNotFound,
|
||||
db_api.quota_class_get,
|
||||
self.ctx, class_name, 'cores')
|
||||
self.assertRaises(exceptions.QuotaClassNotFound,
|
||||
db_api.quota_class_get,
|
||||
self.ctx, class_name, 'ram')
|
||||
|
Loading…
Reference in New Issue
Block a user