Add unique constraints to Quota.

Added unique constraint 'uniq_quotas0project_id0resource0deleted'
('project_id', 'resource', 'deleted') to Quota model and migrate sripts.
Added new exception `QuotaExists`. Updated quota_create(). Change
quotas duplicates control in create/update code to new DB UC exception.
Tests updated respectively.

blueprint db-enforce-unique-keys

Change-Id: Ia615a1c1a7dc75bd19831fcd0acfc9b78c1b3f6f
This commit is contained in:
Yuriy Zveryanskyy
2013-06-17 15:56:50 +03:00
parent aaa871cc59
commit 5daa62bce9
8 changed files with 93 additions and 6 deletions

View File

@@ -162,9 +162,9 @@ class QuotaSetsController(object):
raise webob.exc.HTTPBadRequest(explanation=msg)
try:
db.quota_update(context, project_id, key, value)
except exception.ProjectQuotaNotFound:
db.quota_create(context, project_id, key, value)
except exception.QuotaExists:
db.quota_update(context, project_id, key, value)
except exception.AdminRequired:
raise webob.exc.HTTPForbidden()
return {'quota_set': self._get_quotas(context, id)}

View File

@@ -234,9 +234,9 @@ class ProjectCommands(object):
if value.lower() == 'unlimited':
value = -1
try:
db.quota_update(ctxt, project_id, key, value)
except exception.ProjectQuotaNotFound:
db.quota_create(ctxt, project_id, key, value)
except exception.QuotaExists:
db.quota_update(ctxt, project_id, key, value)
else:
print _('%(key)s is not a valid quota key. Valid options are: '
'%(options)s.') % {'key': key,

View File

@@ -2586,7 +2586,10 @@ def quota_create(context, project_id, resource, limit):
quota_ref.project_id = project_id
quota_ref.resource = resource
quota_ref.hard_limit = limit
quota_ref.save()
try:
quota_ref.save()
except db_exc.DBDuplicateEntry:
raise exception.QuotaExists(project_id=project_id, resource=resource)
return quota_ref

View File

@@ -0,0 +1,40 @@
# Copyright 2013 Mirantis 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.
#
# vim: tabstop=4 shiftwidth=4 softtabstop=4
from migrate.changeset import UniqueConstraint
from sqlalchemy import MetaData, Table
from nova.db.sqlalchemy import utils
UC_NAME = 'uniq_quotas0project_id0resource0deleted'
COLUMNS = ('project_id', 'resource', 'deleted')
TABLE_NAME = 'quotas'
def upgrade(migrate_engine):
meta = MetaData(bind=migrate_engine)
t = Table(TABLE_NAME, meta, autoload=True)
utils.drop_old_duplicate_entries_from_table(migrate_engine, TABLE_NAME,
True, *COLUMNS)
uc = UniqueConstraint(*COLUMNS, table=t, name=UC_NAME)
uc.create()
def downgrade(migrate_engine):
utils.drop_unique_constraint(migrate_engine, TABLE_NAME, UC_NAME, *COLUMNS)

View File

@@ -367,7 +367,11 @@ class Quota(BASE, NovaBase):
"""
__tablename__ = 'quotas'
__table_args__ = ()
__table_args__ = (
schema.UniqueConstraint("project_id", "resource", "deleted",
name="uniq_quotas0project_id0resource0deleted"
),
)
id = Column(Integer, primary_key=True)
project_id = Column(String(255), nullable=True)

View File

@@ -704,6 +704,11 @@ class QuotaNotFound(NotFound):
message = _("Quota could not be found")
class QuotaExists(Duplicate):
message = _("Quota exists for project %(project_id)s, "
"resource %(resource)s")
class QuotaResourceUnknown(QuotaNotFound):
message = _("Unknown quota resources %(unknown)s.")

View File

@@ -4753,6 +4753,11 @@ class QuotaTestCase(test.TestCase, ModelsObjectComparatorMixin):
for key, value in expected.iteritems():
self.assertEqual(value, quota_usage[key])
def test_quota_create_exists(self):
db.quota_create(self.ctxt, 'project1', 'resource1', 41)
self.assertRaises(exception.QuotaExists, db.quota_create, self.ctxt,
'project1', 'resource1', 42)
class QuotaClassTestCase(test.TestCase, ModelsObjectComparatorMixin):

View File

@@ -1699,6 +1699,36 @@ class TestNovaMigrations(BaseMigrationTestCase, CommonTestsMixIn):
security_groups.insert().execute,
dict(name='group2', project_id='fake', deleted=0))
def _pre_upgrade_191(self, engine):
quotas = db_utils.get_table(engine, 'quotas')
data = [
{'project_id': 'project1', 'resource': 'resource1', 'deleted': 0},
{'project_id': 'project1', 'resource': 'resource1', 'deleted': 0},
{'project_id': 'project2', 'resource': 'resource1', 'deleted': 0},
]
for item in data:
quotas.insert().values(item).execute()
return data
def _check_191(self, engine, data):
quotas = db_utils.get_table(engine, 'quotas')
def get_(project_id, deleted):
deleted_value = 0 if not deleted else quotas.c.id
return quotas.select().\
where(quotas.c.project_id == project_id).\
where(quotas.c.deleted == deleted_value).\
execute().\
fetchall()
self.assertEqual(1, len(get_('project1', False)))
self.assertEqual(1, len(get_('project1', True)))
self.assertEqual(1, len(get_('project2', False)))
self.assertRaises(sqlalchemy.exc.IntegrityError,
quotas.insert().execute,
{'project_id': 'project1', 'resource': 'resource1',
'deleted': 0})
class TestBaremetalMigrations(BaseMigrationTestCase, CommonTestsMixIn):
"""Test sqlalchemy-migrate migrations."""