Merge "Add Quota SQL database models & api"
This commit is contained in:
commit
95611e8bf2
@ -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.")
|
||||
|
@ -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)
|
||||
|
@ -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 ###
|
@ -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)
|
||||
|
@ -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)
|
||||
|
107
zun/tests/unit/db/test_quota_classes.py
Normal file
107
zun/tests/unit/db/test_quota_classes.py
Normal 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)
|
86
zun/tests/unit/db/test_quotas.py
Normal file
86
zun/tests/unit/db/test_quotas.py
Normal 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)
|
@ -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)
|
||||
|
Loading…
Reference in New Issue
Block a user