Add Quota SQL database models & api

Change-Id: Iebd2e536a167eac42b24822ad9fabfd8dde8ced2
Partial-Implements: blueprint quota-support
This commit is contained in:
Kien Nguyen 2018-04-06 17:16:12 +07:00
parent 3c4b39347c
commit 697412bdb8
8 changed files with 509 additions and 1 deletions

View File

@ -681,3 +681,20 @@ class ContainerActionEventNotFound(ZunException):
class ServerNotUsable(ZunException):
message = _("Zun server not usable")
code = 404
class QuotaNotFound(NotFound):
message = _("Quota could not be found.")
class ProjectQuotaNotFound(QuotaNotFound):
message = _("Quota for project %(project_id)s could not be found.")
class QuotaExists(ZunException):
message = _("Quota exists for project %(project_id)s, "
"resource %(resource)s.")
class QuotaClassNotFound(QuotaNotFound):
message = _("Quota class %(class_name)s could not be found.")

View File

@ -890,3 +890,72 @@ def action_event_finish(context, values):
def action_events_get(context, action_id):
"""Get the events by action id."""
return _get_dbdriver_instance().action_events_get(context, action_id)
@profiler.trace("db")
def quota_create(context, project_id, resource, limit):
"""Create a quota for the given project and resource"""
return _get_dbdriver_instance().quota_create(context, project_id,
resource, limit)
@profiler.trace("db")
def quota_get(context, project_id, resource):
"""Retrieve a quota or raise if it does not exist"""
return _get_dbdriver_instance().quota_get(context, project_id,
resource)
@profiler.trace("db")
def quota_get_all_by_project(context, project_id):
"""Retrieve all quotas associated with a given project"""
return _get_dbdriver_instance().quota_get_all_by_project(context,
project_id)
@profiler.trace("db")
def quota_update(context, project_id, resource, limit):
"""Update a quota or raise if it does not exist"""
return _get_dbdriver_instance().quota_update(context, project_id,
resource, limit)
@profiler.trace("db")
def quota_destroy(context, project_id, resource):
"""Destroy resource quota associated with a given project"""
return _get_dbdriver_instance().quota_destroy(context, project_id,
resource)
@profiler.trace("db")
def quota_class_create(context, class_name, resource, limit):
"""Create a quota class for the given name and resource"""
return _get_dbdriver_instance().quota_class_create(context, class_name,
resource, limit)
@profiler.trace("db")
def quota_class_get(context, class_name, resource):
"""Retrieve a quota class or raise if it does not exist"""
return _get_dbdriver_instance().quota_class_get(context, class_name,
resource)
@profiler.trace("db")
def quota_class_get_default(context):
"""Retrieve all default quota"""
return _get_dbdriver_instance().quota_class_get_default(context)
@profiler.trace("db")
def quota_class_get_all_by_name(context, class_name):
"""Retrieve all quotas associated with a given quota class"""
return _get_dbdriver_instance().quota_class_get_all_by_name(
context, class_name)
@profiler.trace("db")
def quota_class_update(context, class_name, resource, limit):
"""Update a quota class or raise if it does not exist"""
return _get_dbdriver_instance().quota_class_update(context, class_name,
resource, limit)

View File

@ -0,0 +1,59 @@
# 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.
"""Create quota & quota class tables
Revision ID: 2b045cb595db
Revises: 238f94009eab
Create Date: 2018-04-09 13:33:52.522262
"""
# revision identifiers, used by Alembic.
revision = '2b045cb595db'
down_revision = '238f94009eab'
branch_labels = None
depends_on = None
from alembic import op
import sqlalchemy as sa
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_table(
'quotas',
sa.Column('created_at', sa.DateTime()),
sa.Column('updated_at', sa.DateTime()),
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('project_id', sa.String(length=255), index=True),
sa.Column('resource', sa.String(length=255), nullable=False),
sa.Column('hard_limit', sa.Integer()),
sa.PrimaryKeyConstraint('id'),
mysql_charset='utf8',
mysql_engine='InnoDB'
)
op.create_table(
'quota_classes',
sa.Column('created_at', sa.DateTime()),
sa.Column('updated_at', sa.DateTime()),
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('class_name', sa.String(length=255), index=True),
sa.Column('resource', sa.String(length=255)),
sa.Column('hard_limit', sa.Integer()),
sa.Index('quota_classes_class_name_idx', 'class_name'),
sa.PrimaryKeyConstraint('id'),
mysql_charset='utf8',
mysql_engine='InnoDB'
)
# ### end Alembic commands ###

View File

@ -40,6 +40,8 @@ CONF = zun.conf.CONF
_FACADE = None
_DEFAULT_QUOTA_NAME = 'default'
def _create_facade_lazily():
global _FACADE
@ -1043,3 +1045,117 @@ class Connection(object):
events = _paginate_query(models.ContainerActionEvent, sort_dir='desc',
sort_key='created_at', query=query)
return events
def quota_create(self, context, project_id, resource, limit):
quota_ref = models.Quota()
quota_ref.project_id = project_id
quota_ref.resource = resource
quota_ref.hard_limit = limit
session = get_session()
try:
quota_ref.save(session=session)
except db_exc.DBDuplicateEntry:
raise exception.QuotaExists(project_id=project_id,
resource=resource)
return quota_ref
def quota_get(self, context, project_id, resource):
session = get_session()
with session.begin():
query = model_query(models.Quota, session=session).\
filter_by(project_id=project_id).\
filter_by(resource=resource)
result = query.first()
if not result:
raise exception.ProjectQuotaNotFound(project_id=project_id)
return result
def quota_get_all_by_project(self, context, project_id):
session = get_session()
with session.begin():
rows = model_query(models.Quota, session=session).\
filter_by(project_id=project_id).\
all()
result = {'project_id': project_id}
for row in rows:
result[row.resource] = row.hard_limit
return result
def quota_update(self, context, project_id, resource, limit):
session = get_session()
with session.begin():
query = model_query(models.Quota, session=session).\
filter_by(project_id=project_id).\
filter_by(resource=resource)
result = query.update({'hard_limit': limit})
if not result:
raise exception.ProjectQuotaNotFound(project_id=project_id)
def quota_destroy(self, context, project_id, resource):
session = get_session()
with session.begin():
query = model_query(models.Quota, session=session).\
filter_by(project_id=project_id).\
filter_by(resource=resource)
query.delete()
def quota_class_create(self, 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 = get_session()
with session.begin():
quota_class_ref.save(session=session)
return quota_class_ref
def quota_class_get(self, context, class_name, resource):
session = get_session()
with session.begin():
result = model_query(models.QuotaClass, session=session).\
filter_by(class_name=class_name).\
filter_by(resource=resource).\
first()
if not result:
raise exception.QuotaClassNotFound(class_name=class_name)
return result
def quota_class_get_default(self, context):
session = get_session()
with session.begin():
rows = model_query(models.QuotaClass, session=session).\
filter_by(class_name=_DEFAULT_QUOTA_NAME).\
all()
result = {'class_name': _DEFAULT_QUOTA_NAME}
for row in rows:
result[row.resource] = row.hard_limit
return result
def quota_class_get_all_by_name(self, context, class_name):
session = get_session()
with session.begin():
rows = model_query(models.QuotaClass, session=session).\
filter_by(class_name=class_name).\
all()
result = {'class_name': class_name}
for row in rows:
result[row.resource] = row.hard_limit
return result
def quota_class_update(self, context, class_name, resource, limit):
session = get_session()
with session.begin():
result = model_query(models.QuotaClass, session=session).\
filter_by(class_name=class_name).\
filter_by(resource=resource).\
update({'hard_limit': limit})
if not result:
raise exception.QuotaClassNotFound(class_name=class_name)

View File

@ -201,7 +201,7 @@ class Image(Base):
__table_args__ = (
schema.UniqueConstraint('repo', 'tag', name='uniq_image0repotag'),
table_args()
)
)
id = Column(Integer, primary_key=True)
project_id = Column(String(255))
user_id = Column(String(255))
@ -460,3 +460,39 @@ class ContainerActionEvent(Base):
result = Column(String(255))
traceback = Column(Text)
details = Column(Text)
class Quota(Base):
"""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 None then the
resource is unlimited.
"""
__tablename__ = 'quotas'
__table_args__ = ()
id = Column(Integer, primary_key=True, nullable=False)
project_id = Column(String(255), index=True)
resource = Column(String(255), nullable=False)
hard_limit = Column(Integer)
class QuotaClass(Base):
"""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 None, then the resource is unlimited.
"""
__tablename__ = 'quota_classes'
__table_args__ = (
Index('quota_classes_class_name_idx', 'class_name'),
)
id = Column(Integer, primary_key=True, nullable=False)
class_name = Column(String(255), index=True)
resource = Column(String(255))
hard_limit = Column(Integer)

View File

@ -0,0 +1,107 @@
# 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.
"""Tests for manipulating Quota via the DB API"""
from zun.common import context
import zun.conf
from zun.db import api as dbapi
from zun.db.sqlalchemy import api as sqlalchemy_dbapi
from zun.tests.unit.db import base
from zun.tests.unit.db import utils
CONF = zun.conf.CONF
class DBQuotaClassesTestCase(base.DbTestCase):
def setUp(self):
super(DBQuotaClassesTestCase, self).setUp()
self.ctx = context.get_admin_context()
self.class_name = 'default'
self.resource = 'containers'
self.limit = 100
def test_create_quota_class(self):
quota_class = utils.create_test_quota_class(context=self.ctx,
class_name=self.class_name,
resource=self.resource,
limit=self.limit)
self.assertEqual(quota_class.class_name, self.class_name)
self.assertEqual(quota_class.resource, self.resource)
self.assertEqual(quota_class.hard_limit, self.limit)
def test_get_quota_class(self):
quota_class = utils.create_test_quota_class(context=self.ctx,
class_name=self.class_name,
resource=self.resource,
limit=self.limit)
res = dbapi.quota_class_get(context=self.ctx,
class_name=quota_class.class_name,
resource=quota_class.resource)
self.assertEqual(quota_class.class_name, res.class_name)
self.assertEqual(quota_class.resource, res.resource)
self.assertEqual(quota_class.hard_limit, res.hard_limit)
def test_get_default_quota_class(self):
default_quota_class_resource_1 = utils.create_test_quota_class(
context=self.ctx,
class_name=sqlalchemy_dbapi._DEFAULT_QUOTA_NAME,
resource='resource_1',
limit=10)
default_quota_class_resource_2 = utils.create_test_quota_class(
context=self.ctx,
class_name=sqlalchemy_dbapi._DEFAULT_QUOTA_NAME,
resource='resource_2',
limit=20)
res = dbapi.quota_class_get_default(self.ctx)
self.assertEqual(res['class_name'],
sqlalchemy_dbapi._DEFAULT_QUOTA_NAME)
self.assertEqual(res[default_quota_class_resource_1.resource],
default_quota_class_resource_1.hard_limit)
self.assertEqual(res[default_quota_class_resource_2.resource],
default_quota_class_resource_2.hard_limit)
def test_get_all_by_name_quota_class(self):
quota_class_resource_1 = utils.create_test_quota_class(
context=self.ctx,
class_name='class_1',
resource='resource_1',
limit=10)
quota_class_resource_2 = utils.create_test_quota_class(
context=self.ctx,
class_name='class_1',
resource='resource_2',
limit=20)
res = dbapi.quota_class_get_all_by_name(self.ctx, 'class_1')
self.assertEqual(res['class_name'],
'class_1')
self.assertEqual(res[quota_class_resource_1.resource],
quota_class_resource_1.hard_limit)
self.assertEqual(res[quota_class_resource_2.resource],
quota_class_resource_2.hard_limit)
def test_update_quota_class(self):
quota_class = utils.create_test_quota_class(context=self.ctx,
class_name=self.class_name,
resource=self.resource,
limit=self.limit)
dbapi.quota_class_update(
self.ctx, quota_class.class_name,
quota_class.resource, 200)
updated_quota_class = dbapi.quota_class_get(
self.ctx, quota_class.class_name,
quota_class.resource)
self.assertEqual(updated_quota_class.hard_limit, 200)

View File

@ -0,0 +1,86 @@
# 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.
"""Tests for manipulating Quota via the DB API"""
from zun.common import context
from zun.common import exception
import zun.conf
from zun.db import api as dbapi
from zun.tests.unit.db import base
from zun.tests.unit.db import utils
CONF = zun.conf.CONF
class DBQuotaTestCase(base.DbTestCase):
def setUp(self):
super(DBQuotaTestCase, self).setUp()
self.ctx = context.get_admin_context()
self.project_id = 'fake_project_id'
self.resource = 'containers'
self.limit = 100
def test_create_quota(self):
quota = utils.create_test_quota(context=self.ctx,
project_id=self.project_id,
resource=self.resource,
limit=self.limit)
self.assertEqual(quota.project_id, self.project_id)
self.assertEqual(quota.resource, self.resource)
self.assertEqual(quota.hard_limit, self.limit)
def test_get_quota(self):
quota = utils.create_test_quota(context=self.ctx,
project_id=self.project_id,
resource=self.resource,
limit=self.limit)
res = dbapi.quota_get(context=self.ctx,
project_id=quota.project_id,
resource=quota.resource)
self.assertEqual(quota.project_id, res.project_id)
self.assertEqual(quota.resource, res.resource)
self.assertEqual(quota.hard_limit, res.hard_limit)
def test_get_all_project_quota(self):
quota_1 = utils.create_test_quota(context=self.ctx,
project_d=self.project_id,
resource='resource_1',
limit=10)
quota_2 = utils.create_test_quota(context=self.ctx,
project_id=self.project_id,
resource='resource_2',
limit=20)
quotas = dbapi.quota_get_all_by_project(self.ctx, self.project_id)
self.assertEqual(quotas['project_id'], self.project_id)
self.assertEqual(quotas[quota_1.resource], quota_1.hard_limit)
self.assertEqual(quotas[quota_2.resource], quota_2.hard_limit)
def test_destroy_quota(self):
quota = utils.create_test_quota(context=self.ctx,
project_id=self.project_id,
resource=self.resource,
limit=self.limit)
dbapi.quota_destroy(self.ctx, quota.project_id, quota.resource)
self.assertRaises(exception.ProjectQuotaNotFound, dbapi.quota_get,
self.ctx, quota.project_id, quota.resource)
def test_update_quota(self):
quota = utils.create_test_quota(context=self.ctx,
project_id=self.project_id,
resource=self.resource,
limit=self.limit)
dbapi.quota_update(self.ctx, quota.project_id,
quota.resource, 200)
updated_quota = dbapi.quota_get(self.ctx, quota.project_id,
quota.resource)
self.assertEqual(updated_quota.hard_limit, 200)

View File

@ -495,3 +495,21 @@ def get_test_action_event(**kwargs):
for k, v in event_values.items():
setattr(fake_event, k, v)
return fake_event
def create_test_quota(**kwargs):
context = kwargs.get('context')
project_id = kwargs.get('project_id', 'fake_project_id')
resource = kwargs.get('resource', 'containers')
limit = kwargs.get('limit', 100)
dbapi = _get_dbapi()
return dbapi.quota_create(context, project_id, resource, limit)
def create_test_quota_class(**kwargs):
context = kwargs.get('context')
class_name = kwargs.get('class_name', 'default')
resource = kwargs.get('resource', 'containers')
limit = kwargs.get('limit', 100)
dbapi = _get_dbapi()
return dbapi.quota_class_create(context, class_name, resource, limit)