diff --git a/nova/api/openstack/compute/contrib/quotas.py b/nova/api/openstack/compute/contrib/quotas.py index a0740ebe5215..dba5b49390bb 100644 --- a/nova/api/openstack/compute/contrib/quotas.py +++ b/nova/api/openstack/compute/contrib/quotas.py @@ -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)} diff --git a/nova/cmd/manage.py b/nova/cmd/manage.py index df54e86773cd..79575fe4e804 100644 --- a/nova/cmd/manage.py +++ b/nova/cmd/manage.py @@ -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, diff --git a/nova/db/sqlalchemy/api.py b/nova/db/sqlalchemy/api.py index 5bbb21fe7904..b0b794d0d656 100644 --- a/nova/db/sqlalchemy/api.py +++ b/nova/db/sqlalchemy/api.py @@ -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 diff --git a/nova/db/sqlalchemy/migrate_repo/versions/191_add_quota_uc.py b/nova/db/sqlalchemy/migrate_repo/versions/191_add_quota_uc.py new file mode 100644 index 000000000000..4bc8f7e562fc --- /dev/null +++ b/nova/db/sqlalchemy/migrate_repo/versions/191_add_quota_uc.py @@ -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) diff --git a/nova/db/sqlalchemy/models.py b/nova/db/sqlalchemy/models.py index 5d9c4cbf31ac..6da9d8e19db9 100644 --- a/nova/db/sqlalchemy/models.py +++ b/nova/db/sqlalchemy/models.py @@ -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) diff --git a/nova/exception.py b/nova/exception.py index b435d85b7bb2..39028d1a87d8 100644 --- a/nova/exception.py +++ b/nova/exception.py @@ -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.") diff --git a/nova/tests/db/test_db_api.py b/nova/tests/db/test_db_api.py index 85a31bc2ee91..bca5f6edb6fa 100644 --- a/nova/tests/db/test_db_api.py +++ b/nova/tests/db/test_db_api.py @@ -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): diff --git a/nova/tests/db/test_migrations.py b/nova/tests/db/test_migrations.py index f0c13f02995e..9e0a79161145 100644 --- a/nova/tests/db/test_migrations.py +++ b/nova/tests/db/test_migrations.py @@ -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."""