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:
Dimitri Mazmanov 2016-05-02 15:16:06 +02:00
parent fe721d02f4
commit 61605082f2
8 changed files with 274 additions and 13 deletions

View File

@ -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

View File

@ -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")

View File

@ -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)

View File

@ -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)

View File

@ -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

View File

@ -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()

View File

@ -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):

View File

@ -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')