diff --git a/nova/api/openstack/compute/quota_classes.py b/nova/api/openstack/compute/quota_classes.py index 14cd6e2c6b17..275238507baf 100644 --- a/nova/api/openstack/compute/quota_classes.py +++ b/nova/api/openstack/compute/quota_classes.py @@ -21,8 +21,8 @@ from nova.api.openstack.compute.schemas import quota_classes from nova.api.openstack import extensions from nova.api.openstack import wsgi from nova.api import validation -from nova import db from nova import exception +from nova import objects from nova.policies import quota_class_sets as qcs_policies from nova import quota from nova import utils @@ -94,9 +94,9 @@ class QuotaClassSetsController(wsgi.Controller): for key, value in body['quota_class_set'].items(): try: - db.quota_class_update(context, quota_class, key, value) + objects.Quotas.update_class(context, quota_class, key, value) except exception.QuotaClassNotFound: - db.quota_class_create(context, quota_class, key, value) + objects.Quotas.create_class(context, quota_class, key, value) values = QUOTAS.get_class_quotas(context, quota_class) return self._format_quota_set(None, values, req) diff --git a/nova/cmd/manage.py b/nova/cmd/manage.py index e87c71fe40c9..5665b18688a1 100644 --- a/nova/cmd/manage.py +++ b/nova/cmd/manage.py @@ -276,11 +276,11 @@ class ProjectCommands(object): print(_('Quota limit must be less than %s.') % maximum) return 2 try: - db.quota_create(ctxt, project_id, key, value, - user_id=user_id) + objects.Quotas.create_limit(ctxt, project_id, key, value, + user_id=user_id) except exception.QuotaExists: - db.quota_update(ctxt, project_id, key, value, - user_id=user_id) + objects.Quotas.update_limit(ctxt, project_id, key, value, + user_id=user_id) else: print(_('%(key)s is not a valid quota key. Valid options are: ' '%(options)s.') % {'key': key, diff --git a/nova/exception.py b/nova/exception.py index 8c28c1ac7f80..3bf39455f78e 100644 --- a/nova/exception.py +++ b/nova/exception.py @@ -1035,6 +1035,10 @@ class QuotaClassNotFound(QuotaNotFound): msg_fmt = _("Quota class %(class_name)s could not be found.") +class QuotaClassExists(NovaException): + msg_fmt = _("Quota class %(class_name)s exists for resource %(resource)s") + + class QuotaUsageNotFound(QuotaNotFound): msg_fmt = _("Quota usage for project %(project_id)s could not be found.") diff --git a/nova/objects/quotas.py b/nova/objects/quotas.py index 7cfe31359585..52a97bdab2ed 100644 --- a/nova/objects/quotas.py +++ b/nova/objects/quotas.py @@ -14,7 +14,11 @@ import collections +from oslo_db import exception as db_exc + from nova import db +from nova.db.sqlalchemy import api as db_api +from nova.db.sqlalchemy import api_models from nova import exception from nova.objects import base from nova.objects import fields @@ -71,6 +75,179 @@ class Quotas(base.NovaObject): self.user_id = None self.obj_reset_changes() + @staticmethod + @db_api.api_context_manager.reader + def _get_from_db(context, project_id, resource, user_id=None): + model = api_models.ProjectUserQuota if user_id else api_models.Quota + query = context.session.query(model).\ + filter_by(project_id=project_id).\ + filter_by(resource=resource) + if user_id: + query = query.filter_by(user_id=user_id) + result = query.first() + if not result: + if user_id: + raise exception.ProjectUserQuotaNotFound(project_id=project_id, + user_id=user_id) + else: + raise exception.ProjectQuotaNotFound(project_id=project_id) + return result + + @staticmethod + @db_api.api_context_manager.reader + def _get_all_from_db(context, project_id): + return context.session.query(api_models.ProjectUserQuota).\ + filter_by(project_id=project_id).\ + all() + + @staticmethod + @db_api.api_context_manager.reader + def _get_all_from_db_by_project(context, project_id): + # by_project refers to the returned dict that has a 'project_id' key + rows = context.session.query(api_models.Quota).\ + filter_by(project_id=project_id).\ + all() + result = {'project_id': project_id} + for row in rows: + result[row.resource] = row.hard_limit + return result + + @staticmethod + @db_api.api_context_manager.reader + def _get_all_from_db_by_project_and_user(context, project_id, user_id): + # by_project_and_user refers to the returned dict that has + # 'project_id' and 'user_id' keys + columns = (api_models.ProjectUserQuota.resource, + api_models.ProjectUserQuota.hard_limit) + user_quotas = context.session.query(*columns).\ + filter_by(project_id=project_id).\ + filter_by(user_id=user_id).\ + all() + result = {'project_id': project_id, 'user_id': user_id} + for user_quota in user_quotas: + result[user_quota.resource] = user_quota.hard_limit + return result + + @staticmethod + @db_api.api_context_manager.writer + def _destroy_all_in_db_by_project(context, project_id): + per_project = context.session.query(api_models.Quota).\ + filter_by(project_id=project_id).\ + delete(synchronize_session=False) + per_user = context.session.query(api_models.ProjectUserQuota).\ + filter_by(project_id=project_id).\ + delete(synchronize_session=False) + if not per_project and not per_user: + raise exception.ProjectQuotaNotFound(project_id=project_id) + + @staticmethod + @db_api.api_context_manager.writer + def _destroy_all_in_db_by_project_and_user(context, project_id, user_id): + result = context.session.query(api_models.ProjectUserQuota).\ + filter_by(project_id=project_id).\ + filter_by(user_id=user_id).\ + delete(synchronize_session=False) + if not result: + raise exception.ProjectUserQuotaNotFound(project_id=project_id, + user_id=user_id) + + @staticmethod + @db_api.api_context_manager.reader + def _get_class_from_db(context, class_name, resource): + result = context.session.query(api_models.QuotaClass).\ + filter_by(class_name=class_name).\ + filter_by(resource=resource).\ + first() + if not result: + raise exception.QuotaClassNotFound(class_name=class_name) + return result + + @staticmethod + @db_api.api_context_manager.reader + def _get_all_class_from_db_by_name(context, class_name): + # by_name refers to the returned dict that has a 'class_name' key + rows = context.session.query(api_models.QuotaClass).\ + filter_by(class_name=class_name).\ + all() + result = {'class_name': class_name} + for row in rows: + result[row.resource] = row.hard_limit + return result + + @staticmethod + @db_api.api_context_manager.writer + def _create_limit_in_db(context, project_id, resource, limit, + user_id=None): + # TODO(melwitt): We won't have per project resources after nova-network + # is removed. + per_user = (user_id and + resource not in db_api.quota_get_per_project_resources()) + quota_ref = (api_models.ProjectUserQuota() if per_user + else api_models.Quota()) + if per_user: + quota_ref.user_id = user_id + quota_ref.project_id = project_id + quota_ref.resource = resource + quota_ref.hard_limit = limit + try: + quota_ref.save(context.session) + except db_exc.DBDuplicateEntry: + raise exception.QuotaExists(project_id=project_id, + resource=resource) + return quota_ref + + @staticmethod + @db_api.api_context_manager.writer + def _update_limit_in_db(context, project_id, resource, limit, + user_id=None): + # TODO(melwitt): We won't have per project resources after nova-network + # is removed. + per_user = (user_id and + resource not in db_api.quota_get_per_project_resources()) + model = api_models.ProjectUserQuota if per_user else api_models.Quota + query = context.session.query(model).\ + filter_by(project_id=project_id).\ + filter_by(resource=resource) + if per_user: + query = query.filter_by(user_id=user_id) + + result = query.update({'hard_limit': limit}) + if not result: + if per_user: + raise exception.ProjectUserQuotaNotFound(project_id=project_id, + user_id=user_id) + else: + raise exception.ProjectQuotaNotFound(project_id=project_id) + + @staticmethod + @db_api.api_context_manager.writer + def _create_class_in_db(context, class_name, resource, limit): + # NOTE(melwitt): There's no unique constraint on the QuotaClass model, + # so check for duplicate manually. + try: + Quotas._get_class_from_db(context, class_name, resource) + except exception.QuotaClassNotFound: + pass + else: + raise exception.QuotaClassExists(class_name=class_name, + resource=resource) + quota_class_ref = api_models.QuotaClass() + quota_class_ref.class_name = class_name + quota_class_ref.resource = resource + quota_class_ref.hard_limit = limit + quota_class_ref.save(context.session) + return quota_class_ref + + @staticmethod + @db_api.api_context_manager.writer + def _update_class_in_db(context, class_name, resource, limit): + result = context.session.query(api_models.QuotaClass).\ + filter_by(class_name=class_name).\ + filter_by(resource=resource).\ + update({'hard_limit': limit}) + if not result: + raise exception.QuotaClassNotFound(class_name=class_name) + @classmethod def from_reservations(cls, context, reservations, instance=None): """Transitional for compatibility.""" @@ -207,17 +384,121 @@ class Quotas(base.NovaObject): @base.remotable_classmethod def create_limit(cls, context, project_id, resource, limit, user_id=None): - # NOTE(danms,comstud): Quotas likely needs an overhaul and currently - # doesn't map very well to objects. Since there is quite a bit of - # logic in the db api layer for this, just pass this through for now. - db.quota_create(context, project_id, resource, limit, user_id=user_id) + try: + db.quota_get(context, project_id, resource, user_id=user_id) + except exception.QuotaNotFound: + cls._create_limit_in_db(context, project_id, resource, limit, + user_id=user_id) + else: + raise exception.QuotaExists(project_id=project_id, + resource=resource) @base.remotable_classmethod def update_limit(cls, context, project_id, resource, limit, user_id=None): - # NOTE(danms,comstud): Quotas likely needs an overhaul and currently - # doesn't map very well to objects. Since there is quite a bit of - # logic in the db api layer for this, just pass this through for now. - db.quota_update(context, project_id, resource, limit, user_id=user_id) + try: + cls._update_limit_in_db(context, project_id, resource, limit, + user_id=user_id) + except exception.QuotaNotFound: + db.quota_update(context, project_id, resource, limit, + user_id=user_id) + + @classmethod + def create_class(cls, context, class_name, resource, limit): + try: + db.quota_class_get(context, class_name, resource) + except exception.QuotaClassNotFound: + cls._create_class_in_db(context, class_name, resource, limit) + else: + raise exception.QuotaClassExists(class_name=class_name, + resource=resource) + + @classmethod + def update_class(cls, context, class_name, resource, limit): + try: + cls._update_class_in_db(context, class_name, resource, limit) + except exception.QuotaClassNotFound: + db.quota_class_update(context, class_name, resource, limit) + + # NOTE(melwitt): The following methods are not remotable and return + # dict-like database model objects. We are using classmethods to provide + # a common interface for accessing the api/main databases. + @classmethod + def get(cls, context, project_id, resource, user_id=None): + try: + quota = cls._get_from_db(context, project_id, resource, + user_id=user_id) + except exception.QuotaNotFound: + quota = db.quota_get(context, project_id, resource, + user_id=user_id) + return quota + + @classmethod + def get_all(cls, context, project_id): + api_db_quotas = cls._get_all_from_db(context, project_id) + main_db_quotas = db.quota_get_all(context, project_id) + return api_db_quotas + main_db_quotas + + @classmethod + def get_all_by_project(cls, context, project_id): + api_db_quotas_dict = cls._get_all_from_db_by_project(context, + project_id) + main_db_quotas_dict = db.quota_get_all_by_project(context, project_id) + for k, v in api_db_quotas_dict.items(): + main_db_quotas_dict[k] = v + return main_db_quotas_dict + + @classmethod + def get_all_by_project_and_user(cls, context, project_id, user_id): + api_db_quotas_dict = cls._get_all_from_db_by_project_and_user( + context, project_id, user_id) + main_db_quotas_dict = db.quota_get_all_by_project_and_user( + context, project_id, user_id) + for k, v in api_db_quotas_dict.items(): + main_db_quotas_dict[k] = v + return main_db_quotas_dict + + @classmethod + def destroy_all_by_project(cls, context, project_id): + try: + cls._destroy_all_in_db_by_project(context, project_id) + except exception.ProjectQuotaNotFound: + db.quota_destroy_all_by_project(context, project_id) + + @classmethod + def destroy_all_by_project_and_user(cls, context, project_id, user_id): + try: + cls._destroy_all_in_db_by_project_and_user(context, project_id, + user_id) + except exception.ProjectUserQuotaNotFound: + db.quota_destroy_all_by_project_and_user(context, project_id, + user_id) + + @classmethod + def get_class(cls, context, class_name, resource): + try: + qclass = cls._get_class_from_db(context, class_name, resource) + except exception.QuotaClassNotFound: + qclass = db.quota_class_get(context, class_name, resource) + return qclass + + @classmethod + def get_default_class(cls, context): + try: + qclass = cls._get_all_class_from_db_by_name( + context, db_api._DEFAULT_QUOTA_NAME) + except exception.QuotaClassNotFound: + qclass = db.quota_class_get_default(context) + return qclass + + @classmethod + def get_all_class_by_name(cls, context, class_name): + api_db_quotas_dict = cls._get_all_class_from_db_by_name(context, + class_name) + main_db_quotas_dict = db.quota_class_get_all_by_name(context, + class_name) + for k, v in api_db_quotas_dict.items(): + main_db_quotas_dict[k] = v + return main_db_quotas_dict @base.NovaObjectRegistry.register diff --git a/nova/quota.py b/nova/quota.py index 70b89f5378aa..986c78ff2f37 100644 --- a/nova/quota.py +++ b/nova/quota.py @@ -48,17 +48,18 @@ class DbQuotaDriver(object): def get_by_project_and_user(self, context, project_id, user_id, resource): """Get a specific quota by project and user.""" - return db.quota_get(context, project_id, resource, user_id=user_id) + return objects.Quotas.get(context, project_id, resource, + user_id=user_id) def get_by_project(self, context, project_id, resource): """Get a specific quota by project.""" - return db.quota_get(context, project_id, resource) + return objects.Quotas.get(context, project_id, resource) def get_by_class(self, context, quota_class, resource): """Get a specific quota by quota class.""" - return db.quota_class_get(context, quota_class, resource) + return objects.Quotas.get_class(context, quota_class, resource) def get_defaults(self, context, resources): """Given a list of resources, retrieve the default quotas. @@ -70,7 +71,7 @@ class DbQuotaDriver(object): """ quotas = {} - default_quotas = db.quota_class_get_default(context) + default_quotas = objects.Quotas.get_default_class(context) for resource in resources.values(): # resource.default returns the config options. So if there's not # an entry for the resource in the default class, it uses the @@ -95,7 +96,8 @@ class DbQuotaDriver(object): """ quotas = {} - class_quotas = db.quota_class_get_all_by_name(context, quota_class) + class_quotas = objects.Quotas.get_all_class_by_name(context, + quota_class) for resource in resources.values(): if defaults or resource.name in class_quotas: quotas[resource.name] = class_quotas.get(resource.name, @@ -114,7 +116,8 @@ class DbQuotaDriver(object): if project_id == context.project_id: quota_class = context.quota_class if quota_class: - class_quotas = db.quota_class_get_all_by_name(context, quota_class) + class_quotas = objects.Quotas.get_all_class_by_name(context, + quota_class) else: class_quotas = {} @@ -148,7 +151,7 @@ class DbQuotaDriver(object): # from the class limits to get the remains. For example, if the # class/default is 20 and there are two users each with quota of 5, # then there is quota of 10 left to give out. - all_quotas = db.quota_get_all(context, project_id) + all_quotas = objects.Quotas.get_all(context, project_id) for quota in all_quotas: if quota.resource in modified_quotas: modified_quotas[quota.resource]['remains'] -= \ @@ -246,11 +249,10 @@ class DbQuotaDriver(object): if user_quotas: user_quotas = user_quotas.copy() else: - user_quotas = db.quota_get_all_by_project_and_user(context, - project_id, - user_id) + user_quotas = objects.Quotas.get_all_by_project_and_user( + context, project_id, user_id) # Use the project quota for default user quota. - proj_quotas = project_quotas or db.quota_get_all_by_project( + proj_quotas = project_quotas or objects.Quotas.get_all_by_project( context, project_id) for key, value in proj_quotas.items(): if key not in user_quotas.keys(): @@ -286,7 +288,7 @@ class DbQuotaDriver(object): will be returned. :param project_quotas: Quotas dictionary for the specified project. """ - project_quotas = project_quotas or db.quota_get_all_by_project( + project_quotas = project_quotas or objects.Quotas.get_all_by_project( context, project_id) project_usages = {} if usages: @@ -332,14 +334,13 @@ class DbQuotaDriver(object): """ settable_quotas = {} - db_proj_quotas = db.quota_get_all_by_project(context, project_id) + db_proj_quotas = objects.Quotas.get_all_by_project(context, project_id) project_quotas = self.get_project_quotas(context, resources, project_id, remains=True, project_quotas=db_proj_quotas) if user_id: - setted_quotas = db.quota_get_all_by_project_and_user(context, - project_id, - user_id) + setted_quotas = objects.Quotas.get_all_by_project_and_user( + context, project_id, user_id) user_quotas = self.get_user_quotas(context, resources, project_id, user_id, project_quotas=db_proj_quotas, @@ -491,7 +492,7 @@ class DbQuotaDriver(object): user_id = context.user_id # Get the applicable quotas - project_quotas = db.quota_get_all_by_project(context, project_id) + project_quotas = objects.Quotas.get_all_by_project(context, project_id) quotas = self._get_quotas(context, resources, values.keys(), project_id=project_id, project_quotas=project_quotas) @@ -598,7 +599,7 @@ class DbQuotaDriver(object): # per project quota limits (quotas that have no concept of # user-scoping: fixed_ips, networks, floating_ips) - project_quotas = db.quota_get_all_by_project(context, project_id) + project_quotas = objects.Quotas.get_all_by_project(context, project_id) # per user quotas, project quota limits (for quotas that have # user-scoping, limits for the project) quotas = self._get_quotas(context, resources, all_keys, @@ -731,7 +732,7 @@ class DbQuotaDriver(object): # NOTE(Vek): We're not worried about races at this point. # Yes, the admin may be in the process of reducing # quotas, but that's a pretty rare thing. - project_quotas = db.quota_get_all_by_project(context, project_id) + project_quotas = objects.Quotas.get_all_by_project(context, project_id) LOG.debug('Quota limits for project %(project_id)s: ' '%(project_quotas)s', {'project_id': project_id, 'project_quotas': project_quotas}) @@ -883,25 +884,24 @@ class DbQuotaDriver(object): project_id=project_id, user_id=user_id) def destroy_all_by_project_and_user(self, context, project_id, user_id): - """Destroy all quotas, usages, and reservations associated with a - project and user. + """Destroy all quotas associated with a project and user. :param context: The request context, for access checks. :param project_id: The ID of the project being deleted. :param user_id: The ID of the user being deleted. """ - db.quota_destroy_all_by_project_and_user(context, project_id, user_id) + objects.Quotas.destroy_all_by_project_and_user(context, project_id, + user_id) def destroy_all_by_project(self, context, project_id): - """Destroy all quotas, usages, and reservations associated with a - project. + """Destroy all quotas associated with a project. :param context: The request context, for access checks. :param project_id: The ID of the project being deleted. """ - db.quota_destroy_all_by_project(context, project_id) + objects.Quotas.destroy_all_by_project(context, project_id) def expire(self, context): """Expire reservations. @@ -1215,8 +1215,7 @@ class NoopQuotaDriver(object): pass def destroy_all_by_project_and_user(self, context, project_id, user_id): - """Destroy all quotas, usages, and reservations associated with a - project and user. + """Destroy all quotas associated with a project and user. :param context: The request context, for access checks. :param project_id: The ID of the project being deleted. @@ -1225,8 +1224,7 @@ class NoopQuotaDriver(object): pass def destroy_all_by_project(self, context, project_id): - """Destroy all quotas, usages, and reservations associated with a - project. + """Destroy all quotas associated with a project. :param context: The request context, for access checks. :param project_id: The ID of the project being deleted. @@ -1356,7 +1354,7 @@ class ReservableResource(BaseResource): class AbsoluteResource(BaseResource): - """Describe a non-reservable resource.""" + """Describe a resource that does not correspond to database objects.""" valid_method = 'check' diff --git a/nova/tests/functional/db/test_quotas.py b/nova/tests/functional/db/test_quotas.py new file mode 100644 index 000000000000..9e10f925e1fa --- /dev/null +++ b/nova/tests/functional/db/test_quotas.py @@ -0,0 +1,208 @@ +# 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. + +from nova import context +from nova import exception +from nova.objects import quotas +from nova import test +from nova.tests.unit.db import test_db_api + + +class QuotasObjectTestCase(test.TestCase, + test_db_api.ModelsObjectComparatorMixin): + def setUp(self): + super(QuotasObjectTestCase, self).setUp() + self.context = context.RequestContext('fake-user', 'fake-project') + + def test_create_class(self): + created = quotas.Quotas._create_class_in_db(self.context, 'foo', + 'cores', 10) + db_class = quotas.Quotas._get_class_from_db(self.context, 'foo', + 'cores') + self._assertEqualObjects(created, db_class) + + def test_create_class_exists(self): + quotas.Quotas._create_class_in_db(self.context, 'foo', 'cores', 10) + self.assertRaises(exception.QuotaClassExists, + quotas.Quotas._create_class_in_db, self.context, + 'foo', 'cores', 10) + + def test_update_class(self): + created = quotas.Quotas._create_class_in_db(self.context, 'foo', + 'cores', 10) + quotas.Quotas._update_class_in_db(self.context, 'foo', 'cores', 20) + db_class = quotas.Quotas._get_class_from_db(self.context, 'foo', + 'cores') + # Should have a limit of 20 now + created['hard_limit'] = 20 + self._assertEqualObjects(created, db_class, ignored_keys='updated_at') + + def test_update_class_not_found(self): + self.assertRaises(exception.QuotaClassNotFound, + quotas.Quotas._update_class_in_db, self.context, + 'foo', 'cores', 20) + + def test_create_per_project_limit(self): + created = quotas.Quotas._create_limit_in_db(self.context, + 'fake-project', + 'fixed_ips', 10) + db_limit = quotas.Quotas._get_from_db(self.context, 'fake-project', + 'fixed_ips') + self._assertEqualObjects(created, db_limit) + + def test_create_per_user_limit(self): + created = quotas.Quotas._create_limit_in_db(self.context, + 'fake-project', 'cores', + 10, user_id='fake-user') + db_limit = quotas.Quotas._get_from_db(self.context, 'fake-project', + 'cores', user_id='fake-user') + self._assertEqualObjects(created, db_limit) + + def test_create_limit_duplicate(self): + quotas.Quotas._create_limit_in_db(self.context, 'fake-project', + 'cores', 10) + self.assertRaises(exception.QuotaExists, + quotas.Quotas._create_limit_in_db, self.context, + 'fake-project', 'cores', 20) + + def test_update_per_project_limit(self): + created = quotas.Quotas._create_limit_in_db(self.context, + 'fake-project', + 'fixed_ips', 10) + quotas.Quotas._update_limit_in_db(self.context, 'fake-project', + 'fixed_ips', 20) + db_limit = quotas.Quotas._get_from_db(self.context, 'fake-project', + 'fixed_ips') + # Should have a limit of 20 now + created['hard_limit'] = 20 + self._assertEqualObjects(created, db_limit, ignored_keys='updated_at') + + def test_update_per_project_limit_not_found(self): + self.assertRaises(exception.ProjectQuotaNotFound, + quotas.Quotas._update_limit_in_db, self.context, + 'fake-project', 'fixed_ips', 20) + + def test_update_per_user_limit(self): + created = quotas.Quotas._create_limit_in_db(self.context, + 'fake-project', 'cores', + 10, user_id='fake-user') + quotas.Quotas._update_limit_in_db(self.context, 'fake-project', + 'cores', 20, user_id='fake-user') + db_limit = quotas.Quotas._get_from_db(self.context, 'fake-project', + 'cores', user_id='fake-user') + # Should have a limit of 20 now + created['hard_limit'] = 20 + self._assertEqualObjects(created, db_limit, ignored_keys='updated_at') + + def test_update_per_user_limit_not_found(self): + self.assertRaises(exception.ProjectUserQuotaNotFound, + quotas.Quotas._update_limit_in_db, self.context, + 'fake-project', 'cores', 20, user_id='fake-user') + + def test_get_per_project_limit_not_found(self): + self.assertRaises(exception.ProjectQuotaNotFound, + quotas.Quotas._get_from_db, self.context, + 'fake-project', 'fixed_ips') + + def test_get_per_user_limit_not_found(self): + self.assertRaises(exception.ProjectUserQuotaNotFound, + quotas.Quotas._get_from_db, self.context, + 'fake-project', 'cores', user_id='fake-user') + + def test_get_all_per_user_limits(self): + created = [] + created.append(quotas.Quotas._create_limit_in_db(self.context, + 'fake-project', + 'cores', 10, + user_id='fake-user')) + created.append(quotas.Quotas._create_limit_in_db(self.context, + 'fake-project', 'ram', + 8192, + user_id='fake-user')) + db_limits = quotas.Quotas._get_all_from_db(self.context, + 'fake-project') + for i, db_limit in enumerate(db_limits): + self._assertEqualObjects(created[i], db_limit) + + def test_get_all_per_project_limits_by_project(self): + quotas.Quotas._create_limit_in_db(self.context, 'fake-project', + 'fixed_ips', 20) + quotas.Quotas._create_limit_in_db(self.context, 'fake-project', + 'floating_ips', 10) + limits_dict = quotas.Quotas._get_all_from_db_by_project(self.context, + 'fake-project') + self.assertEqual('fake-project', limits_dict['project_id']) + self.assertEqual(20, limits_dict['fixed_ips']) + self.assertEqual(10, limits_dict['floating_ips']) + + def test_get_all_per_user_limits_by_project_and_user(self): + quotas.Quotas._create_limit_in_db(self.context, 'fake-project', + 'instances', 5, user_id='fake-user') + quotas.Quotas._create_limit_in_db(self.context, 'fake-project', + 'cores', 10, user_id='fake-user') + limits_dict = quotas.Quotas._get_all_from_db_by_project_and_user( + self.context, 'fake-project', 'fake-user') + self.assertEqual('fake-project', limits_dict['project_id']) + self.assertEqual('fake-user', limits_dict['user_id']) + self.assertEqual(5, limits_dict['instances']) + self.assertEqual(10, limits_dict['cores']) + + def test_destroy_per_project_and_per_user_limits(self): + # per user limit + quotas.Quotas._create_limit_in_db(self.context, 'fake-project', + 'instances', 5, user_id='fake-user') + # per project limit + quotas.Quotas._create_limit_in_db(self.context, 'fake-project', + 'fixed_ips', 10) + quotas.Quotas._destroy_all_in_db_by_project(self.context, + 'fake-project') + self.assertRaises(exception.ProjectUserQuotaNotFound, + quotas.Quotas._get_from_db, self.context, + 'fake-project', 'instances', user_id='fake-user') + self.assertRaises(exception.ProjectQuotaNotFound, + quotas.Quotas._get_from_db, self.context, + 'fake-project', 'fixed_ips') + + def test_destroy_per_project_and_per_user_limits_not_found(self): + self.assertRaises(exception.ProjectQuotaNotFound, + quotas.Quotas._destroy_all_in_db_by_project, + self.context, 'fake-project') + + def test_destroy_per_user_limits(self): + quotas.Quotas._create_limit_in_db(self.context, 'fake-project', + 'instances', 5, user_id='fake-user') + quotas.Quotas._destroy_all_in_db_by_project_and_user(self.context, + 'fake-project', + 'fake-user') + self.assertRaises(exception.ProjectUserQuotaNotFound, + quotas.Quotas._get_from_db, self.context, + 'fake-project', 'instances', user_id='fake-user') + + def test_destroy_per_user_limits_not_found(self): + self.assertRaises( + exception.ProjectUserQuotaNotFound, + quotas.Quotas._destroy_all_in_db_by_project_and_user, + self.context, 'fake-project', 'fake-user') + + def test_get_class_not_found(self): + self.assertRaises(exception.QuotaClassNotFound, + quotas.Quotas._get_class_from_db, self.context, + 'foo', 'cores') + + def test_get_all_class_by_name(self): + quotas.Quotas._create_class_in_db(self.context, 'foo', 'instances', 5) + quotas.Quotas._create_class_in_db(self.context, 'foo', 'cores', 10) + limits_dict = quotas.Quotas._get_all_class_from_db_by_name( + self.context, 'foo') + self.assertEqual('foo', limits_dict['class_name']) + self.assertEqual(5, limits_dict['instances']) + self.assertEqual(10, limits_dict['cores']) diff --git a/nova/tests/unit/api/openstack/compute/test_serversV21.py b/nova/tests/unit/api/openstack/compute/test_serversV21.py index 1a900a0de138..267e46665e62 100644 --- a/nova/tests/unit/api/openstack/compute/test_serversV21.py +++ b/nova/tests/unit/api/openstack/compute/test_serversV21.py @@ -3232,8 +3232,8 @@ class ServersControllerCreateTest(test.TestCase): self.assertEqual(encodeutils.safe_decode(robj['Location']), selfhref) - @mock.patch('nova.db.quota_get_all_by_project') - @mock.patch('nova.db.quota_get_all_by_project_and_user') + @mock.patch('nova.objects.Quotas.get_all_by_project') + @mock.patch('nova.objects.Quotas.get_all_by_project_and_user') @mock.patch('nova.objects.Quotas.count_as_dict') def _do_test_create_instance_above_quota(self, resource, allowed, quota, expected_msg, mock_count, mock_get_all_pu, diff --git a/nova/tests/unit/objects/test_quotas.py b/nova/tests/unit/objects/test_quotas.py index e7afb7618250..0f1f470e9e8a 100644 --- a/nova/tests/unit/objects/test_quotas.py +++ b/nova/tests/unit/objects/test_quotas.py @@ -15,6 +15,7 @@ import mock from nova import context +from nova.db.sqlalchemy import api as db_api from nova import exception from nova.objects import quotas as quotas_obj from nova import quota @@ -140,14 +141,22 @@ class _TestQuotasObject(object): quotas.rollback() self.assertFalse(rollback_mock.called) - @mock.patch('nova.db.quota_create') - def test_create_limit(self, mock_create): + @mock.patch('nova.db.quota_get', side_effect=exception.QuotaNotFound) + @mock.patch('nova.objects.Quotas._create_limit_in_db') + def test_create_limit(self, mock_create, mock_get): quotas_obj.Quotas.create_limit(self.context, 'fake-project', 'foo', 10, user_id='user') mock_create.assert_called_once_with(self.context, 'fake-project', 'foo', 10, user_id='user') - @mock.patch('nova.db.quota_update') + @mock.patch('nova.db.quota_get') + @mock.patch('nova.objects.Quotas._create_limit_in_db') + def test_create_limit_exists_in_main(self, mock_create, mock_get): + self.assertRaises(exception.QuotaExists, + quotas_obj.Quotas.create_limit, self.context, + 'fake-project', 'foo', 10, user_id='user') + + @mock.patch('nova.objects.Quotas._update_limit_in_db') def test_update_limit(self, mock_update): quotas_obj.Quotas.update_limit(self.context, 'fake-project', 'foo', 10, user_id='user') @@ -279,6 +288,202 @@ class _TestQuotasObject(object): user_values={'foo': 2}, user_id='a-user') + @mock.patch('nova.objects.Quotas._update_limit_in_db', + side_effect=exception.QuotaNotFound) + @mock.patch('nova.db.quota_update') + def test_update_limit_main(self, mock_update_main, mock_update): + quotas_obj.Quotas.update_limit(self.context, 'fake-project', + 'foo', 10, user_id='user') + mock_update.assert_called_once_with(self.context, 'fake-project', + 'foo', 10, user_id='user') + mock_update_main.assert_called_once_with(self.context, 'fake-project', + 'foo', 10, + user_id='user') + + @mock.patch('nova.objects.Quotas._get_from_db') + def test_get(self, mock_get): + qclass = quotas_obj.Quotas.get(self.context, 'fake-project', 'foo', + user_id='user') + mock_get.assert_called_once_with(self.context, 'fake-project', 'foo', + user_id='user') + self.assertEqual(mock_get.return_value, qclass) + + @mock.patch('nova.objects.Quotas._get_from_db', + side_effect=exception.QuotaNotFound) + @mock.patch('nova.db.quota_get') + def test_get_main(self, mock_get_main, mock_get): + quotas_obj.Quotas.get(self.context, 'fake-project', 'foo', + user_id='user') + mock_get.assert_called_once_with(self.context, 'fake-project', 'foo', + user_id='user') + mock_get_main.assert_called_once_with(self.context, 'fake-project', + 'foo', user_id='user') + + @mock.patch('nova.objects.Quotas._get_all_from_db') + @mock.patch('nova.db.quota_get_all') + def test_get_all(self, mock_get_all_main, mock_get_all): + mock_get_all.return_value = ['api1'] + mock_get_all_main.return_value = ['main1', 'main2'] + quotas = quotas_obj.Quotas.get_all(self.context, 'fake-project') + mock_get_all.assert_called_once_with(self.context, 'fake-project') + mock_get_all_main.assert_called_once_with(self.context, 'fake-project') + self.assertEqual(['api1', 'main1', 'main2'], quotas) + + @mock.patch('nova.objects.Quotas._get_all_from_db_by_project') + @mock.patch('nova.db.quota_get_all_by_project') + def test_get_all_by_project(self, mock_get_all_main, mock_get_all): + mock_get_all.return_value = {'project_id': 'fake-project', + 'fixed_ips': 20, 'floating_ips': 5} + mock_get_all_main.return_value = {'project_id': 'fake-project', + 'fixed_ips': 10, 'networks': 5} + quotas_dict = quotas_obj.Quotas.get_all_by_project(self.context, + 'fake-project') + mock_get_all.assert_called_once_with(self.context, 'fake-project') + mock_get_all_main.assert_called_once_with(self.context, 'fake-project') + expected = {'project_id': 'fake-project', 'fixed_ips': 20, + 'floating_ips': 5, 'networks': 5} + self.assertEqual(expected, quotas_dict) + + @mock.patch('nova.objects.Quotas._get_all_from_db_by_project_and_user') + @mock.patch('nova.db.quota_get_all_by_project_and_user') + def test_get_all_by_project_and_user(self, mock_get_all_main, + mock_get_all): + mock_get_all.return_value = {'project_id': 'fake-project', + 'user_id': 'user', 'instances': 5, + 'cores': 10} + mock_get_all_main.return_value = {'project_id': 'fake-project', + 'user_id': 'user', 'instances': 10, + 'ram': 8192} + quotas_dict = quotas_obj.Quotas.get_all_by_project_and_user( + self.context, 'fake-project', 'user') + mock_get_all.assert_called_once_with(self.context, 'fake-project', + 'user') + mock_get_all_main.assert_called_once_with(self.context, 'fake-project', + 'user') + expected = {'project_id': 'fake-project', 'user_id': 'user', + 'instances': 5, 'cores': 10, 'ram': 8192} + self.assertEqual(expected, quotas_dict) + + @mock.patch('nova.objects.Quotas._destroy_all_in_db_by_project') + def test_destroy_all_by_project(self, mock_destroy_all): + quotas_obj.Quotas.destroy_all_by_project(self.context, 'fake-project') + mock_destroy_all.assert_called_once_with(self.context, 'fake-project') + + @mock.patch('nova.objects.Quotas._destroy_all_in_db_by_project', + side_effect=exception.ProjectQuotaNotFound( + project_id='fake-project')) + @mock.patch('nova.db.quota_destroy_all_by_project') + def test_destroy_all_by_project_main(self, mock_destroy_all_main, + mock_destroy_all): + quotas_obj.Quotas.destroy_all_by_project(self.context, 'fake-project') + mock_destroy_all.assert_called_once_with(self.context, 'fake-project') + mock_destroy_all_main.assert_called_once_with(self.context, + 'fake-project') + + @mock.patch('nova.objects.Quotas._destroy_all_in_db_by_project_and_user') + def test_destroy_all_by_project_and_user(self, mock_destroy_all): + quotas_obj.Quotas.destroy_all_by_project_and_user(self.context, + 'fake-project', + 'user') + mock_destroy_all.assert_called_once_with(self.context, 'fake-project', + 'user') + + @mock.patch('nova.objects.Quotas._destroy_all_in_db_by_project_and_user', + side_effect=exception.ProjectUserQuotaNotFound( + user_id='user', project_id='fake-project')) + @mock.patch('nova.db.quota_destroy_all_by_project_and_user') + def test_destroy_all_by_project_and_user_main(self, mock_destroy_all_main, + mock_destroy_all): + quotas_obj.Quotas.destroy_all_by_project_and_user(self.context, + 'fake-project', + 'user') + mock_destroy_all.assert_called_once_with(self.context, 'fake-project', + 'user') + mock_destroy_all_main.assert_called_once_with(self.context, + 'fake-project', 'user') + + @mock.patch('nova.objects.Quotas._get_class_from_db') + def test_get_class(self, mock_get): + qclass = quotas_obj.Quotas.get_class(self.context, 'class', 'resource') + mock_get.assert_called_once_with(self.context, 'class', 'resource') + self.assertEqual(mock_get.return_value, qclass) + + @mock.patch('nova.objects.Quotas._get_class_from_db', + side_effect=exception.QuotaClassNotFound(class_name='class')) + @mock.patch('nova.db.quota_class_get') + def test_get_class_main(self, mock_get_main, mock_get): + qclass = quotas_obj.Quotas.get_class(self.context, 'class', 'resource') + mock_get.assert_called_once_with(self.context, 'class', 'resource') + mock_get_main.assert_called_once_with(self.context, 'class', + 'resource') + self.assertEqual(mock_get_main.return_value, qclass) + + @mock.patch('nova.objects.Quotas._get_all_class_from_db_by_name') + def test_get_default_class(self, mock_get_all): + qclass = quotas_obj.Quotas.get_default_class(self.context) + mock_get_all.assert_called_once_with(self.context, + db_api._DEFAULT_QUOTA_NAME) + self.assertEqual(mock_get_all.return_value, qclass) + + @mock.patch('nova.objects.Quotas._get_all_class_from_db_by_name', + side_effect=exception.QuotaClassNotFound(class_name='class')) + @mock.patch('nova.db.quota_class_get_default') + def test_get_default_class_main(self, mock_get_default_main, mock_get_all): + qclass = quotas_obj.Quotas.get_default_class(self.context) + mock_get_all.assert_called_once_with(self.context, + db_api._DEFAULT_QUOTA_NAME) + mock_get_default_main.assert_called_once_with(self.context) + self.assertEqual(mock_get_default_main.return_value, qclass) + + @mock.patch('nova.objects.Quotas._get_all_class_from_db_by_name') + @mock.patch('nova.db.quota_class_get_all_by_name') + def test_get_class_by_name(self, mock_get_all_main, mock_get_all): + mock_get_all.return_value = {'class_name': 'foo', 'cores': 10, + 'instances': 5} + mock_get_all_main.return_value = {'class_name': 'foo', 'cores': 20, + 'fixed_ips': 10} + quotas_dict = quotas_obj.Quotas.get_all_class_by_name(self.context, + 'foo') + mock_get_all.assert_called_once_with(self.context, 'foo') + mock_get_all_main.assert_called_once_with(self.context, 'foo') + expected = {'class_name': 'foo', 'cores': 10, 'instances': 5, + 'fixed_ips': 10} + self.assertEqual(expected, quotas_dict) + + @mock.patch('nova.db.quota_class_get', + side_effect=exception.QuotaClassNotFound(class_name='class')) + @mock.patch('nova.objects.Quotas._create_class_in_db') + def test_create_class(self, mock_create, mock_get): + quotas_obj.Quotas.create_class(self.context, 'class', 'resource', + 'limit') + mock_create.assert_called_once_with(self.context, 'class', 'resource', + 'limit') + + @mock.patch('nova.db.quota_class_get') + @mock.patch('nova.objects.Quotas._create_class_in_db') + def test_create_class_exists_in_main(self, mock_create, mock_get): + self.assertRaises(exception.QuotaClassExists, + quotas_obj.Quotas.create_class, self.context, + 'class', 'resource', 'limit') + + @mock.patch('nova.objects.Quotas._update_class_in_db') + def test_update_class(self, mock_update): + quotas_obj.Quotas.update_class(self.context, 'class', 'resource', + 'limit') + mock_update.assert_called_once_with(self.context, 'class', 'resource', + 'limit') + + @mock.patch('nova.objects.Quotas._update_class_in_db', + side_effect=exception.QuotaClassNotFound(class_name='class')) + @mock.patch('nova.db.quota_class_update') + def test_update_class_main(self, mock_update_main, mock_update): + quotas_obj.Quotas.update_class(self.context, 'class', 'resource', + 'limit') + mock_update.assert_called_once_with(self.context, 'class', 'resource', + 'limit') + mock_update_main.assert_called_once_with(self.context, 'class', + 'resource', 'limit') + class TestQuotasObject(_TestQuotasObject, test_objects._LocalTest): pass diff --git a/nova/tests/unit/test_quota.py b/nova/tests/unit/test_quota.py index 680c0f4cbc05..9a681cad1756 100644 --- a/nova/tests/unit/test_quota.py +++ b/nova/tests/unit/test_quota.py @@ -908,7 +908,7 @@ class DbQuotaDriverTestCase(test.TestCase): def _stub_quota_class_get_default(self): # Stub out quota_class_get_default - def fake_qcgd(context): + def fake_qcgd(cls, context): self.calls.append('quota_class_get_default') return dict( instances=5, @@ -916,11 +916,11 @@ class DbQuotaDriverTestCase(test.TestCase): metadata_items=64, injected_file_content_bytes=5 * 1024, ) - self.stub_out('nova.db.quota_class_get_default', fake_qcgd) + self.stub_out('nova.objects.Quotas.get_default_class', fake_qcgd) def _stub_quota_class_get_all_by_name(self): # Stub out quota_class_get_all_by_name - def fake_qcgabn(context, quota_class): + def fake_qcgabn(cls, context, quota_class): self.calls.append('quota_class_get_all_by_name') self.assertEqual(quota_class, 'test_class') return dict( @@ -929,7 +929,7 @@ class DbQuotaDriverTestCase(test.TestCase): metadata_items=64, injected_file_content_bytes=5 * 1024, ) - self.stub_out('nova.db.quota_class_get_all_by_name', fake_qcgabn) + self.stub_out('nova.objects.Quotas.get_all_class_by_name', fake_qcgabn) def test_get_class_quotas(self): self._stub_quota_class_get_all_by_name()