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:
		@@ -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)}
 | 
			
		||||
 
 | 
			
		||||
@@ -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,
 | 
			
		||||
 
 | 
			
		||||
@@ -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
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										40
									
								
								nova/db/sqlalchemy/migrate_repo/versions/191_add_quota_uc.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										40
									
								
								nova/db/sqlalchemy/migrate_repo/versions/191_add_quota_uc.py
									
									
									
									
									
										Normal 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)
 | 
			
		||||
@@ -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)
 | 
			
		||||
 
 | 
			
		||||
@@ -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.")
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -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):
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -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."""
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user