diff --git a/nova/compute/api.py b/nova/compute/api.py index 7a27ae352d8e..380cb83ec035 100644 --- a/nova/compute/api.py +++ b/nova/compute/api.py @@ -1057,13 +1057,14 @@ class API(base.Base): if instance_group: if check_server_group_quota: - count = objects.Quotas.count(context, + count = objects.Quotas.count_as_dict(context, 'server_group_members', instance_group, context.user_id) + count_value = count['user']['server_group_members'] try: - objects.Quotas.limit_check(context, - server_group_members=count + 1) + objects.Quotas.limit_check( + context, server_group_members=count_value + 1) except exception.OverQuota: msg = _("Quota exceeded, too many servers in " "group") @@ -4861,10 +4862,11 @@ class KeypairAPI(base.Base): reason=_('Keypair name must be string and between ' '1 and 255 characters long')) - count = objects.Quotas.count(context, 'key_pairs', user_id) + count = objects.Quotas.count_as_dict(context, 'key_pairs', user_id) + count_value = count['user']['key_pairs'] try: - objects.Quotas.limit_check(context, key_pairs=count + 1) + objects.Quotas.limit_check(context, key_pairs=count_value + 1) except exception.OverQuota: raise exception.KeypairLimitExceeded() @@ -5189,9 +5191,11 @@ class SecurityGroupAPI(base.Base, security_group_base.SecurityGroupBase): this function is written to support both. """ - count = objects.Quotas.count(context, 'security_group_rules', id) + count = objects.Quotas.count_as_dict(context, + 'security_group_rules', id) + count_value = count['user']['security_group_rules'] try: - projected = count + len(vals) + projected = count_value + len(vals) objects.Quotas.limit_check(context, security_group_rules=projected) except exception.OverQuota: msg = _("Quota exceeded, too many security group rules.") diff --git a/nova/objects/quotas.py b/nova/objects/quotas.py index 6cdb07917dae..1fb228b5e332 100644 --- a/nova/objects/quotas.py +++ b/nova/objects/quotas.py @@ -12,8 +12,10 @@ # License for the specific language governing permissions and limitations # under the License. +import collections from nova import db +from nova import exception from nova.objects import base from nova.objects import fields from nova import quota @@ -51,7 +53,9 @@ class Quotas(base.NovaObject): # Version 1.0: initial version # Version 1.1: Added create_limit() and update_limit() # Version 1.2: Added limit_check() and count() - VERSION = '1.2' + # Version 1.3: Added check_deltas(), limit_check_project_and_user(), + # and count_as_dict() + VERSION = '1.3' fields = { 'reservations': fields.ListOfStringsField(nullable=True), @@ -122,12 +126,85 @@ class Quotas(base.NovaObject): return quota.QUOTAS.limit_check( context, project_id=project_id, user_id=user_id, **values) + @base.remotable_classmethod + def limit_check_project_and_user(cls, context, project_values=None, + user_values=None, project_id=None, + user_id=None): + """Check values against quota limits.""" + return quota.QUOTAS.limit_check_project_and_user(context, + project_values=project_values, user_values=user_values, + project_id=project_id, user_id=user_id) + + # NOTE(melwitt): This can be removed once no old code can call count(). @base.remotable_classmethod def count(cls, context, resource, *args, **kwargs): """Count a resource.""" - return quota.QUOTAS.count( + count = quota.QUOTAS.count_as_dict(context, resource, *args, **kwargs) + key = 'user' if 'user' in count else 'project' + return count[key][resource] + + @base.remotable_classmethod + def count_as_dict(cls, context, resource, *args, **kwargs): + """Count a resource and return a dict.""" + return quota.QUOTAS.count_as_dict( context, resource, *args, **kwargs) + @base.remotable_classmethod + def check_deltas(cls, context, deltas, *count_args, **count_kwargs): + """Check usage delta against quota limits. + + This does a Quotas.count_as_dict() followed by a + Quotas.limit_check_project_and_user() using the provided deltas. + + :param context: The request context, for access checks + :param deltas: A dict of {resource_name: delta, ...} to check against + the quota limits + :param count_args: Optional positional arguments to pass to + count_as_dict() + :param count_kwargs: Optional keyword arguments to pass to + count_as_dict() + :param check_project_id: Optional project_id for scoping the limit + check to a different project than in the + context + :param check_user_id: Optional user_id for scoping the limit check to a + different user than in the context + :raises: exception.OverQuota if the limit check exceeds the quota + limits + """ + # We can't do f(*args, kw=None, **kwargs) in python 2.x + check_project_id = count_kwargs.pop('check_project_id', None) + check_user_id = count_kwargs.pop('check_user_id', None) + + check_kwargs = collections.defaultdict(dict) + for resource in deltas: + # If we already counted a resource in a batch count, avoid + # unnecessary re-counting and avoid creating empty dicts in + # the defaultdict. + if (resource in check_kwargs.get('project_values', {}) or + resource in check_kwargs.get('user_values', {})): + continue + count = cls.count_as_dict(context, resource, *count_args, + **count_kwargs) + for res in count.get('project', {}): + if res in deltas: + total = count['project'][res] + deltas[res] + check_kwargs['project_values'][res] = total + for res in count.get('user', {}): + if res in deltas: + total = count['user'][res] + deltas[res] + check_kwargs['user_values'][res] = total + if check_project_id is not None: + check_kwargs['project_id'] = check_project_id + if check_user_id is not None: + check_kwargs['user_id'] = check_user_id + try: + cls.limit_check_project_and_user(context, **check_kwargs) + except exception.OverQuota as exc: + # Report usage in the exception when going over quota + key = 'user' if 'user' in count else 'project' + exc.kwargs['usages'] = count[key] + raise exc + @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 diff --git a/nova/quota.py b/nova/quota.py index 984d912a614c..a94498644280 100644 --- a/nova/quota.py +++ b/nova/quota.py @@ -16,6 +16,7 @@ """Quotas for resources per project.""" +import copy import datetime from oslo_log import log as logging @@ -69,6 +70,9 @@ class DbQuotaDriver(object): quotas = {} default_quotas = db.quota_class_get_default(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 + # config option. quotas[resource.name] = default_quotas.get(resource.name, resource.default) @@ -132,11 +136,16 @@ class DbQuotaDriver(object): in_use=usage.get('in_use', 0), reserved=usage.get('reserved', 0), ) - # Initialize remains quotas. + + # Initialize remains quotas with the default limits. if remains: modified_quotas[resource.name].update(remains=limit) if remains: + # Get all user quotas for a project and subtract their limits + # 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) for quota in all_quotas: if quota.resource in modified_quotas: @@ -145,6 +154,68 @@ class DbQuotaDriver(object): return modified_quotas + def _get_usages(self, context, resources, project_id, user_id=None): + """Get usages of specified resources. + + This function is called to get resource usages for validating quota + limit creates or updates in the os-quota-sets API and for displaying + resource usages in the os-used-limits API. This function is not used + for checking resource usage against quota limits. + + :param context: The request context for access checks + :param resources: The dict of Resources for which to get usages + :param project_id: The project_id for scoping the usage count + :param user_id: Optional user_id for scoping the usage count + :returns: A dict containing resources and their usage information, + for example: + {'project_id': 'project-uuid', + 'user_id': 'user-uuid', + 'instances': {'in_use': 5}, + 'fixed_ips': {'in_use': 5}} + """ + usages = {} + for resource in resources.values(): + # NOTE(melwitt): This is to keep things working while we're in the + # middle of converting ReservableResources to CountableResources. + # We should skip resources that are not countable and eventually + # when there are no more ReservableResources, we won't need this. + if not isinstance(resource, CountableResource): + continue + if resource.name in usages: + # This is needed because for any of the resources: + # ('instances', 'cores', 'ram'), they are counted at the same + # time for efficiency (query the instances table once instead + # of multiple times). So, a count of any one of them contains + # counts for the others and we can avoid re-counting things. + continue + if resource.name in ('key_pairs', 'server_group_members', + 'security_group_rules'): + # These per user resources are special cases whose usages + # are not considered when validating limit create/update or + # displaying used limits. They are always zero. + usages[resource.name] = {'in_use': 0} + else: + if resource.name in db.quota_get_per_project_resources(): + count = resource.count_as_dict(context, project_id) + key = 'project' + else: + # NOTE(melwitt): This assumes a specific signature for + # count_as_dict(). Usages used to be records in the + # database but now we are counting resources. The + # count_as_dict() function signature needs to match this + # call, else it should get a conditional in this function. + count = resource.count_as_dict(context, project_id, + user_id=user_id) + key = 'user' if user_id else 'project' + # Example count_as_dict() return value: + # {'project': {'instances': 5}, + # 'user': {'instances': 2}} + counted_resources = count[key].keys() + for res in counted_resources: + count_value = count[key][res] + usages[res] = {'in_use': count_value} + return usages + def get_user_quotas(self, context, resources, project_id, user_id, quota_class=None, defaults=True, usages=True, project_quotas=None, @@ -183,11 +254,21 @@ class DbQuotaDriver(object): for key, value in proj_quotas.items(): if key not in user_quotas.keys(): user_quotas[key] = value - user_usages = None + user_usages = {} if usages: - user_usages = db.quota_usage_get_all_by_project_and_user(context, - project_id, - user_id) + user_usages = self._get_usages(context, resources, project_id, + user_id=user_id) + # TODO(melwitt): This is for compat with ReservableResources and + # should be removed when all of the ReservableResources have been + # removed. ReservableResource usage comes from the quota_usages + # table in the database, so we need to query usages from there as + # long as we still have ReservableResources. + from_usages_table = db.quota_usage_get_all_by_project_and_user( + context, project_id, user_id) + for k, v in from_usages_table.items(): + if k in user_usages: + continue + user_usages[k] = v return self._process_quotas(context, resources, project_id, user_quotas, quota_class, defaults=defaults, usages=user_usages) @@ -218,11 +299,20 @@ class DbQuotaDriver(object): """ project_quotas = project_quotas or db.quota_get_all_by_project( context, project_id) - project_usages = None + project_usages = {} if usages: - LOG.debug('Getting all quota usages for project: %s', project_id) - project_usages = db.quota_usage_get_all_by_project(context, - project_id) + project_usages = self._get_usages(context, resources, project_id) + # TODO(melwitt): This is for compat with ReservableResources and + # should be removed when all of the ReservableResources have been + # removed. ReservableResource usage comes from the quota_usages + # table in the database, so we need to query usages from there as + # long as we still have ReservableResources. + from_usages_table = db.quota_usage_get_all_by_project(context, + project_id) + for k, v in from_usages_table.items(): + if k in project_usages: + continue + project_usages[k] = v return self._process_quotas(context, resources, project_id, project_quotas, quota_class, defaults=defaults, usages=project_usages, @@ -277,9 +367,22 @@ class DbQuotaDriver(object): project_quotas=db_proj_quotas, user_quotas=setted_quotas) for key, value in user_quotas.items(): + # Maximum is the remaining quota for a project (class/default + # minus the sum of all user quotas in the project), plus the + # given user's quota. So if the class/default is 20 and there + # are two users each with quota of 5, then there is quota of + # 10 remaining. The given user currently has quota of 5, so + # the maximum you could update their quota to would be 15. + # Class/default 20 - currently used in project 10 + current + # user 5 = 15. maximum = \ self._sum_quota_values(project_quotas[key]['remains'], setted_quotas.get(key, 0)) + # This function is called for the quota_sets api and the + # corresponding nova-manage command. The idea is when someone + # attempts to update a quota, the value chosen must be at least + # as much as the current usage and less than or equal to the + # project limit less the sum of existing per user limits. minimum = value['in_use'] + value['reserved'] settable_quotas[key] = {'minimum': minimum, 'maximum': maximum} else: @@ -404,7 +507,7 @@ class DbQuotaDriver(object): is admin and admin wants to impact on common user. """ - _valid_method_call_check_resources(values, 'check') + _valid_method_call_check_resources(values, 'check', resources) # Ensure no value is less than zero unders = [key for key, val in values.items() if val < 0] @@ -443,6 +546,151 @@ class DbQuotaDriver(object): raise exception.OverQuota(overs=sorted(overs), quotas=quotas, usages={}, headroom=headroom) + def limit_check_project_and_user(self, context, resources, + project_values=None, user_values=None, + project_id=None, user_id=None): + """Check values (usage + desired delta) against quota limits. + + For limits--this method checks that a set of + proposed values are permitted by the limit restriction. + + This method will raise a QuotaResourceUnknown exception if a + given resource is unknown or if it is not a simple limit + resource. + + If any of the proposed values is over the defined quota, an + OverQuota exception will be raised with the sorted list of the + resources which are too high. Otherwise, the method returns + nothing. + + :param context: The request context, for access checks + :param resources: A dictionary of the registered resources + :param project_values: Optional dict containing the resource values to + check against project quota, + e.g. {'instances': 1, 'cores': 2, 'memory_mb': 512} + :param user_values: Optional dict containing the resource values to + check against user quota, + e.g. {'instances': 1, 'cores': 2, 'memory_mb': 512} + :param project_id: Optional project_id for scoping the limit check to a + different project than in the context + :param user_id: Optional user_id for scoping the limit check to a + different user than in the context + """ + if project_values is None: + project_values = {} + if user_values is None: + user_values = {} + + _valid_method_call_check_resources(project_values, 'check', resources) + _valid_method_call_check_resources(user_values, 'check', resources) + + if not any([project_values, user_values]): + raise exception.Invalid( + 'Must specify at least one of project_values or user_values ' + 'for the limit check.') + + # Ensure no value is less than zero + for vals in (project_values, user_values): + unders = [key for key, val in vals.items() if val < 0] + if unders: + raise exception.InvalidQuotaValue(unders=sorted(unders)) + + # Get a set of all keys for calling _get_quotas() so we get all of the + # resource limits we need. + all_keys = set(project_values).union(user_values) + + # Keys that are in both project_values and user_values need to be + # checked against project quota and user quota, respectively. + # Keys that are not in both only need to be checked against project + # quota or user quota, if it is defined. Separate the keys that don't + # need to be checked against both quotas, merge them into one dict, + # and remove them from project_values and user_values. + keys_to_merge = set(project_values).symmetric_difference(user_values) + merged_values = {} + for key in keys_to_merge: + # The key will be either in project_values or user_values based on + # the earlier symmetric_difference. + merged_values[key] = (project_values.get(key) or + user_values.get(key)) + project_values.pop(key, None) + user_values.pop(key, None) + + # If project_id is None, then we use the project_id in context + if project_id is None: + project_id = context.project_id + # If user id is None, then we use the user_id in context + if user_id is None: + user_id = context.user_id + + # Get the applicable quotas. They will be merged together (taking the + # min limit) if project_values and user_values were not specified + # together. + + # 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) + # per user quotas, project quota limits (for quotas that have + # user-scoping, limits for the project) + quotas = self._get_quotas(context, resources, all_keys, + has_sync=False, project_id=project_id, + project_quotas=project_quotas) + # per user quotas, user quota limits (for quotas that have + # user-scoping, the limits for the user) + user_quotas = self._get_quotas(context, resources, all_keys, + has_sync=False, project_id=project_id, + user_id=user_id, + project_quotas=project_quotas) + + if merged_values: + # This is for resources that are not counted across a project and + # must pass both the quota for the project and the quota for the + # user. + # Combine per user project quotas and user_quotas for use in the + # checks, taking the minimum limit between the two. + merged_quotas = copy.deepcopy(quotas) + for k, v in user_quotas.items(): + if k in merged_quotas: + merged_quotas[k] = min(merged_quotas[k], v) + else: + merged_quotas[k] = v + + # Check the quotas and construct a list of the resources that + # would be put over limit by the desired values + overs = [key for key, val in merged_values.items() + if merged_quotas[key] >= 0 and merged_quotas[key] < val] + if overs: + headroom = {} + for key in overs: + headroom[key] = merged_quotas[key] + raise exception.OverQuota(overs=sorted(overs), + quotas=merged_quotas, usages={}, + headroom=headroom) + + # This is for resources that are counted across a project and + # across a user (instances, cores, ram, security_groups, + # server_groups). The project_values must pass the quota for the + # project and the user_values must pass the quota for the user. + over_user_quota = False + overs = [] + for key in user_values.keys(): + # project_values and user_values should contain the same keys or + # be empty after the keys in the symmetric_difference were removed + # from both dicts. + if quotas[key] >= 0 and quotas[key] < project_values[key]: + overs.append(key) + elif (user_quotas[key] >= 0 and + user_quotas[key] < user_values[key]): + overs.append(key) + over_user_quota = True + if overs: + quotas_exceeded = user_quotas if over_user_quota else quotas + headroom = {} + for key in overs: + headroom[key] = quotas_exceeded[key] + raise exception.OverQuota(overs=sorted(overs), + quotas=quotas_exceeded, usages={}, + headroom=headroom) + def reserve(self, context, resources, deltas, expire=None, project_id=None, user_id=None): """Check quotas and reserve resources. @@ -481,7 +729,7 @@ class DbQuotaDriver(object): is admin and admin wants to impact on common user. """ - _valid_method_call_check_resources(deltas, 'reserve') + _valid_method_call_check_resources(deltas, 'reserve', resources) # Set up the reservation expiration if expire is None: @@ -855,6 +1103,38 @@ class NoopQuotaDriver(object): """ pass + def limit_check_project_and_user(self, context, resources, + project_values=None, user_values=None, + project_id=None, user_id=None): + """Check values against quota limits. + + For limits--this method checks that a set of + proposed values are permitted by the limit restriction. + + This method will raise a QuotaResourceUnknown exception if a + given resource is unknown or if it is not a simple limit + resource. + + If any of the proposed values is over the defined quota, an + OverQuota exception will be raised with the sorted list of the + resources which are too high. Otherwise, the method returns + nothing. + + :param context: The request context, for access checks + :param resources: A dictionary of the registered resources + :param project_values: Optional dict containing the resource values to + check against project quota, + e.g. {'instances': 1, 'cores': 2, 'memory_mb': 512} + :param user_values: Optional dict containing the resource values to + check against user quota, + e.g. {'instances': 1, 'cores': 2, 'memory_mb': 512} + :param project_id: Optional project_id for scoping the limit check to a + different project than in the context + :param user_id: Optional user_id for scoping the limit check to a + different user than in the context + """ + pass + def reserve(self, context, resources, deltas, expire=None, project_id=None, user_id=None): """Check quotas and reserve resources. @@ -1117,7 +1397,7 @@ class CountableResource(AbsoluteResource): project ID. """ - def __init__(self, name, count, flag=None): + def __init__(self, name, count_as_dict, flag=None): """Initializes a CountableResource. Countable resources are those resources which directly @@ -1129,8 +1409,28 @@ class CountableResource(AbsoluteResource): The counting function will be passed the context, along with the extra positional and keyword arguments that are passed to - Quota.count(). It should return an integer specifying the - count. + Quota.count_as_dict(). It should return a dict specifying the + count scoped to a project and/or a user. + + Example count of instances, cores, or ram returned as a rollup + of all the resources since we only want to query the instances + table once, not multiple times, for each resource. + Instances, cores, and ram are counted across a project and + across a user: + + {'project': {'instances': 5, 'cores': 8, 'ram': 4096}, + 'user': {'instances': 1, 'cores': 2, 'ram': 512}} + + Example count of server groups keeping a consistent format. + Server groups are counted across a project and across a user: + + {'project': {'server_groups': 7}, + 'user': {'server_groups': 2}} + + Example count of key pairs keeping a consistent format. + Key pairs are counted across a user only: + + {'user': {'key_pairs': 5}} Note that this counting is not performed in a transaction-safe manner. This resource class is a temporary measure to provide @@ -1138,16 +1438,16 @@ class CountableResource(AbsoluteResource): this problem can be evolved. :param name: The name of the resource, i.e., "instances". - :param count: A callable which returns the count of the - resource. The arguments passed are as described - above. + :param count_as_dict: A callable which returns the count of the + resource as a dict. The arguments passed are as + described above. :param flag: The name of the flag or configuration option which specifies the default value of the quota for this resource. """ super(CountableResource, self).__init__(name, flag=flag) - self.count = count + self.count_as_dict = count_as_dict class QuotaEngine(object): @@ -1170,13 +1470,6 @@ class QuotaEngine(object): self.__driver = self._driver_cls return self.__driver - def __contains__(self, resource): - return resource in self._resources - - def __getitem__(self, key): - if key in self._resources: - return self._resources[key] - def register_resource(self, resource): """Register a resource.""" @@ -1289,25 +1582,33 @@ class QuotaEngine(object): project_id, user_id=user_id) - def count(self, context, resource, *args, **kwargs): - """Count a resource. + def count_as_dict(self, context, resource, *args, **kwargs): + """Count a resource and return a dict. - For countable resources, invokes the count() function and + For countable resources, invokes the count_as_dict() function and returns its result. Arguments following the context and resource are passed directly to the count function declared by the resource. :param context: The request context, for access checks. :param resource: The name of the resource, as a string. + :returns: A dict containing the count(s) for the resource, for example: + {'project': {'instances': 2, 'cores': 4, 'ram': 1024}, + 'user': {'instances': 1, 'cores': 2, 'ram': 512}} + + another example: + {'user': {'key_pairs': 5}} """ # Get the resource res = self._resources.get(resource) - if not res or not hasattr(res, 'count'): + if not res or not hasattr(res, 'count_as_dict'): raise exception.QuotaResourceUnknown(unknown=[resource]) - return res.count(context, *args, **kwargs) + return res.count_as_dict(context, *args, **kwargs) + # TODO(melwitt): This can be removed once no old code can call + # limit_check(). It will be replaced with limit_check_project_and_user(). def limit_check(self, context, project_id=None, user_id=None, **values): """Check simple quota limits. @@ -1339,6 +1640,39 @@ class QuotaEngine(object): return self._driver.limit_check(context, self._resources, values, project_id=project_id, user_id=user_id) + def limit_check_project_and_user(self, context, project_values=None, + user_values=None, project_id=None, + user_id=None): + """Check values against quota limits. + + For limits--this method checks that a set of + proposed values are permitted by the limit restriction. + + This method will raise a QuotaResourceUnknown exception if a + given resource is unknown or if it is not a simple limit + resource. + + If any of the proposed values is over the defined quota, an + OverQuota exception will be raised with the sorted list of the + resources which are too high. Otherwise, the method returns + nothing. + + :param context: The request context, for access checks + :param project_values: Optional dict containing the resource values to + check against project quota, + e.g. {'instances': 1, 'cores': 2, 'memory_mb': 512} + :param user_values: Optional dict containing the resource values to + check against user quota, + e.g. {'instances': 1, 'cores': 2, 'memory_mb': 512} + :param project_id: Optional project_id for scoping the limit check to a + different project than in the context + :param user_id: Optional user_id for scoping the limit check to a + different user than in the context + """ + return self._driver.limit_check_project_and_user( + context, self._resources, project_values=project_values, + user_values=user_values, project_id=project_id, user_id=user_id) + def reserve(self, context, expire=None, project_id=None, user_id=None, **deltas): """Check quotas and reserve resources. @@ -1512,14 +1846,23 @@ class QuotaEngine(object): return sorted(self._resources.keys()) -def _keypair_get_count_by_user(*args, **kwargs): - """Helper method to avoid referencing objects.KeyPairList on import.""" - return objects.KeyPairList.get_count_by_user(*args, **kwargs) +def _keypair_get_count_by_user(context, user_id): + count = objects.KeyPairList.get_count_by_user(context, user_id) + return {'user': {'key_pairs': count}} def _server_group_count_members_by_user(context, group, user_id): - """Helper method to avoid referencing objects.InstanceGroup on import.""" - return group.count_members_by_user(user_id) + count = group.count_members_by_user(user_id) + return {'user': {'server_group_members': count}} + + +def _security_group_rule_count_by_group(context, security_group_id): + count = db.security_group_rule_count_by_group(context, security_group_id) + # NOTE(melwitt): Neither 'project' nor 'user' fit perfectly here as + # security group rules are counted per security group, not by user or + # project. But, the quota limits for security_group_rules can be scoped to + # a user, so we'll use 'user' here. + return {'user': {'security_group_rules': count}} QUOTAS = QuotaEngine() @@ -1541,10 +1884,9 @@ resources = [ AbsoluteResource('injected_file_path_bytes', 'injected_file_path_length'), CountableResource('security_group_rules', - db.security_group_rule_count_by_group, + _security_group_rule_count_by_group, 'security_group_rules'), - CountableResource('key_pairs', _keypair_get_count_by_user, - 'key_pairs'), + CountableResource('key_pairs', _keypair_get_count_by_user, 'key_pairs'), ReservableResource('server_groups', '_sync_server_groups', 'server_groups'), CountableResource('server_group_members', @@ -1556,17 +1898,22 @@ resources = [ QUOTAS.register_resources(resources) -def _valid_method_call_check_resource(name, method): - if name not in QUOTAS: +def _valid_method_call_check_resource(name, method, resources): + if name not in resources: raise exception.InvalidQuotaMethodUsage(method=method, res=name) - res = QUOTAS[name] + res = resources[name] if res.valid_method != method: raise exception.InvalidQuotaMethodUsage(method=method, res=name) -def _valid_method_call_check_resources(resource, method): - """A method to check whether the resource can use the quota method.""" +def _valid_method_call_check_resources(resource_values, method, resources): + """A method to check whether the resource can use the quota method. - for name in resource.keys(): - _valid_method_call_check_resource(name, method) + :param resource_values: Dict containing the resource names and values + :param method: The quota method to check + :param resources: Dict containing Resource objects to validate against + """ + + for name in resource_values.keys(): + _valid_method_call_check_resource(name, method, resources) diff --git a/nova/tests/unit/api/openstack/compute/test_keypairs.py b/nova/tests/unit/api/openstack/compute/test_keypairs.py index 16fa74a1500d..01dd19967021 100644 --- a/nova/tests/unit/api/openstack/compute/test_keypairs.py +++ b/nova/tests/unit/api/openstack/compute/test_keypairs.py @@ -184,9 +184,9 @@ class KeypairsTestV21(test.TestCase): def test_keypair_import_quota_limit(self): def fake_quotas_count(self, context, resource, *args, **kwargs): - return 100 + return {'user': {'key_pairs': 100}} - self.stubs.Set(QUOTAS, "count", fake_quotas_count) + self.stubs.Set(QUOTAS, "count_as_dict", fake_quotas_count) body = { 'keypair': { @@ -210,9 +210,9 @@ class KeypairsTestV21(test.TestCase): def test_keypair_create_quota_limit(self): def fake_quotas_count(self, context, resource, *args, **kwargs): - return 100 + return {'user': {'key_pairs': 100}} - self.stubs.Set(QUOTAS, "count", fake_quotas_count) + self.stubs.Set(QUOTAS, "count_as_dict", fake_quotas_count) body = { 'keypair': { diff --git a/nova/tests/unit/api/openstack/compute/test_serversV21.py b/nova/tests/unit/api/openstack/compute/test_serversV21.py index f331aded0c6a..4bcacde054de 100644 --- a/nova/tests/unit/api/openstack/compute/test_serversV21.py +++ b/nova/tests/unit/api/openstack/compute/test_serversV21.py @@ -3209,7 +3209,7 @@ class ServersControllerCreateTest(test.TestCase): self.assertEqual(group.uuid, fake_group.uuid) self.assertEqual(user_id, self.req.environ['nova.context'].user_id) - return 10 + return {'user': {'server_group_members': 10}} def fake_limit_check(context, **kwargs): if 'server_group_members' in kwargs: @@ -3218,7 +3218,7 @@ class ServersControllerCreateTest(test.TestCase): def fake_instance_destroy(context, uuid, constraint): return fakes.stub_instance(1) - self.stubs.Set(fakes.QUOTAS, 'count', fake_count) + self.stubs.Set(fakes.QUOTAS, 'count_as_dict', fake_count) self.stubs.Set(fakes.QUOTAS, 'limit_check', fake_limit_check) self.stub_out('nova.db.instance_destroy', fake_instance_destroy) self.body['os:scheduler_hints'] = {'group': fake_group.uuid} diff --git a/nova/tests/unit/compute/test_keypairs.py b/nova/tests/unit/compute/test_keypairs.py index b5d737741a10..9bdf44f8faf8 100644 --- a/nova/tests/unit/compute/test_keypairs.py +++ b/nova/tests/unit/compute/test_keypairs.py @@ -151,9 +151,9 @@ class CreateImportSharedTestMixIn(object): def test_quota_limit(self): def fake_quotas_count(self, context, resource, *args, **kwargs): - return CONF.quota.key_pairs + return {'user': {'key_pairs': CONF.quota.key_pairs}} - self.stubs.Set(QUOTAS, "count", fake_quotas_count) + self.stubs.Set(QUOTAS, "count_as_dict", fake_quotas_count) msg = "Maximum number of key pairs exceeded" self.assertKeypairRaises(exception.KeypairLimitExceeded, msg, 'foo') diff --git a/nova/tests/unit/objects/test_objects.py b/nova/tests/unit/objects/test_objects.py index 31df154694a1..b7b5bd4943ad 100644 --- a/nova/tests/unit/objects/test_objects.py +++ b/nova/tests/unit/objects/test_objects.py @@ -1142,8 +1142,8 @@ object_data = { 'PciDevicePool': '1.1-3f5ddc3ff7bfa14da7f6c7e9904cc000', 'PciDevicePoolList': '1.1-15ecf022a68ddbb8c2a6739cfc9f8f5e', 'PowerVMLiveMigrateData': '1.1-ac0fdd26da685f12d7038782cabd393a', - 'Quotas': '1.2-1fe4cd50593aaf5d36a6dc5ab3f98fb3', - 'QuotasNoOp': '1.2-e041ddeb7dc8188ca71706f78aad41c1', + 'Quotas': '1.3-40fcefe522111dddd3e5e6155702cf4e', + 'QuotasNoOp': '1.3-b19f8a5d187f75ccf372aa23c5f906a4', 'RequestSpec': '1.8-35033ecef47a880f9a5e46e2269e2b97', 'ResourceClass': '1.0-e6b367e2cf1733c5f3526f20a3286fe9', 'ResourceClassList': '1.1-15ecf022a68ddbb8c2a6739cfc9f8f5e', diff --git a/nova/tests/unit/objects/test_quotas.py b/nova/tests/unit/objects/test_quotas.py index 0190911e54a4..e7afb7618250 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 import exception from nova.objects import quotas as quotas_obj from nova import quota from nova import test @@ -153,6 +154,131 @@ class _TestQuotasObject(object): mock_update.assert_called_once_with(self.context, 'fake-project', 'foo', 10, user_id='user') + @mock.patch.object(QUOTAS, 'count_as_dict') + def test_count(self, mock_count): + # key_pairs can't actually be counted across a project, this is just + # for testing. + mock_count.return_value = {'project': {'key_pairs': 5}, + 'user': {'key_pairs': 4}} + count = quotas_obj.Quotas.count(self.context, 'key_pairs', 'a-user') + self.assertEqual(4, count) + + # key_pairs can't actually be counted across a project, this is just + # for testing. + mock_count.return_value = {'project': {'key_pairs': 5}} + count = quotas_obj.Quotas.count(self.context, 'key_pairs', 'a-user') + self.assertEqual(5, count) + + mock_count.return_value = {'user': {'key_pairs': 3}} + count = quotas_obj.Quotas.count(self.context, 'key_pairs', 'a-user') + self.assertEqual(3, count) + + @mock.patch('nova.objects.Quotas.count_as_dict') + def test_check_deltas(self, mock_count): + self.flags(key_pairs=3, group='quota') + self.flags(server_group_members=3, group='quota') + + def fake_count(context, resource): + if resource in ('key_pairs', 'server_group_members'): + return {'project': {'key_pairs': 2, 'server_group_members': 2}, + 'user': {'key_pairs': 1, 'server_group_members': 2}} + else: + return {'user': {resource: 2}} + + mock_count.side_effect = fake_count + deltas = {'key_pairs': 1, + 'server_group_members': 1, + 'security_group_rules': 1} + project_id = 'fake-other-project' + user_id = 'fake-other-user' + quotas_obj.Quotas.check_deltas(self.context, deltas, + check_project_id=project_id, + check_user_id=user_id) + # Should be called twice: once for key_pairs/server_group_members, + # once for security_group_rules. + self.assertEqual(2, mock_count.call_count) + call1 = mock.call(self.context, 'key_pairs') + call2 = mock.call(self.context, 'server_group_members') + call3 = mock.call(self.context, 'security_group_rules') + self.assertTrue(call1 in mock_count.mock_calls or + call2 in mock_count.mock_calls) + self.assertIn(call3, mock_count.mock_calls) + + @mock.patch('nova.objects.Quotas.count_as_dict') + def test_check_deltas_zero(self, mock_count): + # This will test that we will raise OverQuota if given a zero delta if + # an object creation has put us over the allowed quota. + # This is for the scenario where we recheck quota and delete an object + # if we have gone over quota during a race. + self.flags(key_pairs=3, group='quota') + self.flags(server_group_members=3, group='quota') + + def fake_count(context, resource): + return {'user': {resource: 4}} + + mock_count.side_effect = fake_count + deltas = {'key_pairs': 0, 'server_group_members': 0} + project_id = 'fake-other-project' + user_id = 'fake-other-user' + self.assertRaises(exception.OverQuota, quotas_obj.Quotas.check_deltas, + self.context, deltas, + check_project_id=project_id, + check_user_id=user_id) + # Should be called twice, once for key_pairs, once for + # server_group_members + self.assertEqual(2, mock_count.call_count) + call1 = mock.call(self.context, 'key_pairs') + call2 = mock.call(self.context, 'server_group_members') + mock_count.assert_has_calls([call1, call2], any_order=True) + + @mock.patch('nova.objects.Quotas.count_as_dict') + def test_check_deltas_negative(self, mock_count): + """Test check_deltas with a negative delta. + + Negative deltas probably won't be used going forward for countable + resources because there are no usage records to decrement and there + won't be quota operations done when deleting resources. When resources + are deleted, they will no longer be reflected in the count. + """ + self.flags(key_pairs=3, group='quota') + mock_count.return_value = {'user': {'key_pairs': 4}} + deltas = {'key_pairs': -1} + # Should pass because the delta makes 3 key_pairs + quotas_obj.Quotas.check_deltas(self.context, deltas, 'a-user', + something='something') + # args for the count function should get passed along + mock_count.assert_called_once_with(self.context, 'key_pairs', 'a-user', + something='something') + + @mock.patch('nova.objects.Quotas.count_as_dict') + @mock.patch('nova.objects.Quotas.limit_check_project_and_user') + def test_check_deltas_limit_check_scoping(self, mock_check, mock_count): + # check_project_id and check_user_id kwargs should get passed along to + # limit_check_project_and_user() + mock_count.return_value = {'project': {'foo': 5}, 'user': {'foo': 1}} + deltas = {'foo': 1} + + quotas_obj.Quotas.check_deltas(self.context, deltas, 'a-project') + mock_check.assert_called_once_with(self.context, + project_values={'foo': 6}, + user_values={'foo': 2}) + + mock_check.reset_mock() + quotas_obj.Quotas.check_deltas(self.context, deltas, 'a-project', + check_project_id='a-project') + mock_check.assert_called_once_with(self.context, + project_values={'foo': 6}, + user_values={'foo': 2}, + project_id='a-project') + + mock_check.reset_mock() + quotas_obj.Quotas.check_deltas(self.context, deltas, 'a-project', + check_user_id='a-user') + mock_check.assert_called_once_with(self.context, + project_values={'foo': 6}, + user_values={'foo': 2}, + user_id='a-user') + class TestQuotasObject(_TestQuotasObject, test_objects._LocalTest): pass diff --git a/nova/tests/unit/test_quota.py b/nova/tests/unit/test_quota.py index 49010335a527..f0ed60c39f02 100644 --- a/nova/tests/unit/test_quota.py +++ b/nova/tests/unit/test_quota.py @@ -36,6 +36,22 @@ import nova.tests.unit.image.fake CONF = nova.conf.CONF +def _get_fake_get_usages(updates=None): + # These values are not realistic (they should all be 0) and are + # only for testing that countable usages get included in the + # results. + usages = {'security_group_rules': {'in_use': 1}, + 'key_pairs': {'in_use': 2}, + 'server_group_members': {'in_use': 3}} + if updates: + usages.update(updates) + + def fake_get_usages(*a, **k): + return usages + + return fake_get_usages + + class QuotaIntegrationTestCase(test.TestCase): REQUIRES_LOCKING = True @@ -258,8 +274,10 @@ class QuotaIntegrationTestCase(test.TestCase): @enginefacade.transaction_context_provider -class FakeContext(object): +class FakeContext(context.RequestContext): def __init__(self, project_id, quota_class): + super(FakeContext, self).__init__(project_id=project_id, + quota_class=quota_class) self.is_admin = False self.user_id = 'fake_user' self.project_id = project_id @@ -334,6 +352,12 @@ class FakeDriver(object): self.called.append(('limit_check', context, resources, values, project_id, user_id)) + def limit_check_project_and_user(self, context, resources, + project_values=None, user_values=None, + project_id=None, user_id=None): + self.called.append(('limit_check_project_and_user', context, resources, + project_values, user_values, project_id, user_id)) + def reserve(self, context, resources, deltas, expire=None, project_id=None, user_id=None): self.called.append(('reserve', context, resources, deltas, @@ -463,41 +487,41 @@ class BaseResourceTestCase(test.TestCase): self.assertRaises(exception.InvalidQuotaMethodUsage, quota._valid_method_call_check_resources, - resources, 'limit') + resources, 'limit', quota.QUOTAS._resources) def test_valid_method_call_check_invalid_method(self): resources = {'key_pairs': 1} self.assertRaises(exception.InvalidQuotaMethodUsage, quota._valid_method_call_check_resources, - resources, 'dummy') + resources, 'dummy', quota.QUOTAS._resources) def test_valid_method_call_check_multiple(self): resources = {'key_pairs': 1, 'dummy': 2} self.assertRaises(exception.InvalidQuotaMethodUsage, quota._valid_method_call_check_resources, - resources, 'check') + resources, 'check', quota.QUOTAS._resources) resources = {'key_pairs': 1, 'instances': 2, 'dummy': 3} self.assertRaises(exception.InvalidQuotaMethodUsage, quota._valid_method_call_check_resources, - resources, 'check') + resources, 'check', quota.QUOTAS._resources) def test_valid_method_call_check_wrong_method_reserve(self): resources = {'key_pairs': 1} self.assertRaises(exception.InvalidQuotaMethodUsage, quota._valid_method_call_check_resources, - resources, 'reserve') + resources, 'reserve', quota.QUOTAS._resources) def test_valid_method_call_check_wrong_method_check(self): resources = {'fixed_ips': 1} self.assertRaises(exception.InvalidQuotaMethodUsage, quota._valid_method_call_check_resources, - resources, 'check') + resources, 'check', quota.QUOTAS._resources) class QuotaEngineTestCase(test.TestCase): @@ -660,36 +684,37 @@ class QuotaEngineTestCase(test.TestCase): self.assertEqual(result1, quota_obj._resources) self.assertEqual(result2, quota_obj._resources) - def test_count_no_resource(self): + def test_count_as_dict_no_resource(self): context = FakeContext(None, None) driver = FakeDriver() quota_obj = self._make_quota_obj(driver) self.assertRaises(exception.QuotaResourceUnknown, - quota_obj.count, context, 'test_resource5', + quota_obj.count_as_dict, context, 'test_resource5', True, foo='bar') - def test_count_wrong_resource(self): + def test_count_as_dict_wrong_resource(self): context = FakeContext(None, None) driver = FakeDriver() quota_obj = self._make_quota_obj(driver) self.assertRaises(exception.QuotaResourceUnknown, - quota_obj.count, context, 'test_resource1', + quota_obj.count_as_dict, context, 'test_resource1', True, foo='bar') - def test_count(self): - def fake_count(context, *args, **kwargs): + def test_count_as_dict(self): + def fake_count_as_dict(context, *args, **kwargs): self.assertEqual(args, (True,)) self.assertEqual(kwargs, dict(foo='bar')) - return 5 + return {'project': {'test_resource5': 5}} context = FakeContext(None, None) driver = FakeDriver() quota_obj = self._make_quota_obj(driver) - quota_obj.register_resource(quota.CountableResource('test_resource5', - fake_count)) - result = quota_obj.count(context, 'test_resource5', True, foo='bar') + quota_obj.register_resource( + quota.CountableResource('test_resource5', fake_count_as_dict)) + result = quota_obj.count_as_dict(context, 'test_resource5', True, + foo='bar') - self.assertEqual(result, 5) + self.assertEqual({'project': {'test_resource5': 5}}, result) def test_limit_check(self): context = FakeContext(None, None) @@ -707,6 +732,23 @@ class QuotaEngineTestCase(test.TestCase): ), None, None), ]) + def test_limit_check_project_and_user(self): + context = FakeContext(None, None) + driver = FakeDriver() + quota_obj = self._make_quota_obj(driver) + project_values = dict(test_resource1=4, test_resource2=3) + user_values = dict(test_resource3=2, test_resource4=1) + quota_obj.limit_check_project_and_user(context, + project_values=project_values, + user_values=user_values) + + self.assertEqual([('limit_check_project_and_user', context, + quota_obj._resources, + dict(test_resource1=4, test_resource2=3), + dict(test_resource3=2, test_resource4=1), + None, None)], + driver.called) + def test_reserve(self): context = FakeContext(None, None) driver = FakeDriver(reservations=[ @@ -980,12 +1022,100 @@ class DbQuotaDriverTestCase(test.TestCase): self._stub_quota_class_get_all_by_name() - def test_get_user_quotas(self): + def _get_fake_countable_resources(self): + # Create several countable resources with fake count functions + def fake_instances_cores_ram_count(*a, **k): + return {'project': {'instances': 2, 'cores': 4, 'ram': 1024}, + 'user': {'instances': 1, 'cores': 2, 'ram': 512}} + + def fake_security_group_count(*a, **k): + return {'project': {'security_groups': 2}, + 'user': {'security_groups': 1}} + + def fake_server_group_count(*a, **k): + return {'project': {'server_groups': 5}, + 'user': {'server_groups': 3}} + + resources = {} + resources['key_pairs'] = quota.CountableResource( + 'key_pairs', lambda *a, **k: {'user': {'key_pairs': 1}}, + 'key_pairs') + resources['instances'] = quota.CountableResource( + 'instances', fake_instances_cores_ram_count, 'instances') + resources['cores'] = quota.CountableResource( + 'cores', fake_instances_cores_ram_count, 'cores') + resources['ram'] = quota.CountableResource( + 'ram', fake_instances_cores_ram_count, 'ram') + resources['security_groups'] = quota.CountableResource( + 'security_groups', fake_security_group_count, 'security_groups') + resources['floating_ips'] = quota.CountableResource( + 'floating_ips', lambda *a, **k: {'project': {'floating_ips': 4}}, + 'floating_ips') + resources['fixed_ips'] = quota.CountableResource( + 'fixed_ips', lambda *a, **k: {'project': {'fixed_ips': 5}}, + 'fixed_ips') + resources['server_groups'] = quota.CountableResource( + 'server_groups', fake_server_group_count, 'server_groups') + resources['server_group_members'] = quota.CountableResource( + 'server_group_members', + lambda *a, **k: {'user': {'server_group_members': 7}}, + 'server_group_members') + resources['security_group_rules'] = quota.CountableResource( + 'security_group_rules', + lambda *a, **k: {'project': {'security_group_rules': 8}}, + 'security_group_rules') + return resources + + def test_get_usages_for_project(self): + resources = self._get_fake_countable_resources() + actual = self.driver._get_usages( + FakeContext('test_project', 'test_class'), resources, + 'test_project') + # key_pairs, server_group_members, and security_group_rules are never + # counted as a usage. Their counts are only for quota limit checking. + expected = {'key_pairs': {'in_use': 0}, + 'instances': {'in_use': 2}, + 'cores': {'in_use': 4}, + 'ram': {'in_use': 1024}, + 'security_groups': {'in_use': 2}, + 'floating_ips': {'in_use': 4}, + 'fixed_ips': {'in_use': 5}, + 'server_groups': {'in_use': 5}, + 'server_group_members': {'in_use': 0}, + 'security_group_rules': {'in_use': 0}} + self.assertEqual(expected, actual) + + def test_get_usages_for_user(self): + resources = self._get_fake_countable_resources() + actual = self.driver._get_usages( + FakeContext('test_project', 'test_class'), resources, + 'test_project', user_id='fake_user') + # key_pairs, server_group_members, and security_group_rules are never + # counted as a usage. Their counts are only for quota limit checking. + expected = {'key_pairs': {'in_use': 0}, + 'instances': {'in_use': 1}, + 'cores': {'in_use': 2}, + 'ram': {'in_use': 512}, + 'security_groups': {'in_use': 1}, + 'floating_ips': {'in_use': 4}, + 'fixed_ips': {'in_use': 5}, + 'server_groups': {'in_use': 3}, + 'server_group_members': {'in_use': 0}, + 'security_group_rules': {'in_use': 0}} + self.assertEqual(expected, actual) + + @mock.patch('nova.quota.DbQuotaDriver._get_usages') + def test_get_user_quotas(self, mock_get_usages): + # This will test that the counted usage will not be overwritten by + # the quota_usages records (in_use=2, reserved=2) from the database. + usages = {'instances': {'in_use': 5}} + mock_get_usages.side_effect = _get_fake_get_usages(updates=usages) + self.maxDiff = None self._stub_get_by_project_and_user() + ctxt = FakeContext('test_project', 'test_class') result = self.driver.get_user_quotas( - FakeContext('test_project', 'test_class'), - quota.QUOTAS._resources, 'test_project', 'fake_user') + ctxt, quota.QUOTAS._resources, 'test_project', 'fake_user') self.assertEqual(self.calls, [ 'quota_get_all_by_project_and_user', @@ -993,11 +1123,14 @@ class DbQuotaDriverTestCase(test.TestCase): 'quota_usage_get_all_by_project_and_user', 'quota_class_get_all_by_name', ]) + mock_get_usages.assert_called_once_with(ctxt, quota.QUOTAS._resources, + 'test_project', + user_id='fake_user') self.assertEqual(result, dict( instances=dict( limit=5, - in_use=2, - reserved=2, + in_use=5, + reserved=0, ), cores=dict( limit=10, @@ -1046,12 +1179,12 @@ class DbQuotaDriverTestCase(test.TestCase): ), security_group_rules=dict( limit=20, - in_use=0, + in_use=1, reserved=0, ), key_pairs=dict( limit=100, - in_use=0, + in_use=2, reserved=0, ), server_groups=dict( @@ -1061,7 +1194,7 @@ class DbQuotaDriverTestCase(test.TestCase): ), server_group_members=dict( limit=10, - in_use=0, + in_use=3, reserved=0, ), )) @@ -1127,12 +1260,18 @@ class DbQuotaDriverTestCase(test.TestCase): self._stub_quota_class_get_all_by_name() self._stub_quota_class_get_default() - def test_get_project_quotas(self): + @mock.patch('nova.quota.DbQuotaDriver._get_usages') + def test_get_project_quotas(self, mock_get_usages): + # This will test that the counted usage will not be overwritten by + # the quota_usages records (in_use=2, reserved=2) from the database. + usages = {'instances': {'in_use': 5}} + mock_get_usages.side_effect = _get_fake_get_usages(updates=usages) + self.maxDiff = None self._stub_get_by_project() + ctxt = FakeContext('test_project', 'test_class') result = self.driver.get_project_quotas( - FakeContext('test_project', 'test_class'), - quota.QUOTAS._resources, 'test_project') + ctxt, quota.QUOTAS._resources, 'test_project') self.assertEqual(self.calls, [ 'quota_get_all_by_project', @@ -1140,11 +1279,13 @@ class DbQuotaDriverTestCase(test.TestCase): 'quota_class_get_all_by_name', 'quota_class_get_default', ]) + mock_get_usages.assert_called_once_with(ctxt, quota.QUOTAS._resources, + 'test_project') self.assertEqual(result, dict( instances=dict( limit=5, - in_use=2, - reserved=2, + in_use=5, + reserved=0, ), cores=dict( limit=10, @@ -1193,12 +1334,12 @@ class DbQuotaDriverTestCase(test.TestCase): ), security_group_rules=dict( limit=20, - in_use=0, + in_use=1, reserved=0, ), key_pairs=dict( limit=100, - in_use=0, + in_use=2, reserved=0, ), server_groups=dict( @@ -1208,17 +1349,19 @@ class DbQuotaDriverTestCase(test.TestCase): ), server_group_members=dict( limit=10, - in_use=0, + in_use=3, reserved=0, ), )) - def test_get_project_quotas_with_remains(self): + @mock.patch('nova.quota.DbQuotaDriver._get_usages', + side_effect=_get_fake_get_usages()) + def test_get_project_quotas_with_remains(self, mock_get_usages): self.maxDiff = None self._stub_get_by_project() + ctxt = FakeContext('test_project', 'test_class') result = self.driver.get_project_quotas( - FakeContext('test_project', 'test_class'), - quota.QUOTAS._resources, 'test_project', remains=True) + ctxt, quota.QUOTAS._resources, 'test_project', remains=True) self.assertEqual(self.calls, [ 'quota_get_all_by_project', @@ -1227,6 +1370,8 @@ class DbQuotaDriverTestCase(test.TestCase): 'quota_class_get_default', 'quota_get_all', ]) + mock_get_usages.assert_called_once_with(ctxt, quota.QUOTAS._resources, + 'test_project') self.assertEqual(result, dict( instances=dict( limit=5, @@ -1290,13 +1435,13 @@ class DbQuotaDriverTestCase(test.TestCase): ), security_group_rules=dict( limit=20, - in_use=0, + in_use=1, reserved=0, remains=20, ), key_pairs=dict( limit=100, - in_use=0, + in_use=2, reserved=0, remains=100, ), @@ -1308,24 +1453,29 @@ class DbQuotaDriverTestCase(test.TestCase): ), server_group_members=dict( limit=10, - in_use=0, + in_use=3, reserved=0, remains=10, ), )) - def test_get_user_quotas_alt_context_no_class(self): + @mock.patch('nova.quota.DbQuotaDriver._get_usages', + side_effect=_get_fake_get_usages()) + def test_get_user_quotas_alt_context_no_class(self, mock_get_usages): self.maxDiff = None self._stub_get_by_project_and_user() + ctxt = FakeContext('other_project', None) result = self.driver.get_user_quotas( - FakeContext('test_project', None), - quota.QUOTAS._resources, 'test_project', 'fake_user') + ctxt, quota.QUOTAS._resources, 'test_project', 'fake_user') self.assertEqual(self.calls, [ 'quota_get_all_by_project_and_user', 'quota_get_all_by_project', 'quota_usage_get_all_by_project_and_user', ]) + mock_get_usages.assert_called_once_with(ctxt, quota.QUOTAS._resources, + 'test_project', + user_id='fake_user') self.assertEqual(result, dict( instances=dict( limit=10, @@ -1379,12 +1529,12 @@ class DbQuotaDriverTestCase(test.TestCase): ), security_group_rules=dict( limit=20, - in_use=0, + in_use=1, reserved=0, ), key_pairs=dict( limit=100, - in_use=0, + in_use=2, reserved=0, ), server_groups=dict( @@ -1394,23 +1544,27 @@ class DbQuotaDriverTestCase(test.TestCase): ), server_group_members=dict( limit=10, - in_use=0, + in_use=3, reserved=0, ), )) - def test_get_project_quotas_alt_context_no_class(self): + @mock.patch('nova.quota.DbQuotaDriver._get_usages', + side_effect=_get_fake_get_usages()) + def test_get_project_quotas_alt_context_no_class(self, mock_get_usages): self.maxDiff = None self._stub_get_by_project() + ctxt = FakeContext('other_project', None) result = self.driver.get_project_quotas( - FakeContext('other_project', 'other_class'), - quota.QUOTAS._resources, 'test_project') + ctxt, quota.QUOTAS._resources, 'test_project') self.assertEqual(self.calls, [ 'quota_get_all_by_project', 'quota_usage_get_all_by_project', 'quota_class_get_default', ]) + mock_get_usages.assert_called_once_with(ctxt, quota.QUOTAS._resources, + 'test_project') self.assertEqual(result, dict( instances=dict( limit=5, @@ -1464,12 +1618,12 @@ class DbQuotaDriverTestCase(test.TestCase): ), security_group_rules=dict( limit=20, - in_use=0, + in_use=1, reserved=0, ), key_pairs=dict( limit=100, - in_use=0, + in_use=2, reserved=0, ), server_groups=dict( @@ -1479,17 +1633,19 @@ class DbQuotaDriverTestCase(test.TestCase): ), server_group_members=dict( limit=10, - in_use=0, + in_use=3, reserved=0, ), )) - def test_get_user_quotas_alt_context_with_class(self): + @mock.patch('nova.quota.DbQuotaDriver._get_usages', + side_effect=_get_fake_get_usages()) + def test_get_user_quotas_alt_context_with_class(self, mock_get_usages): self.maxDiff = None self._stub_get_by_project_and_user() + ctxt = FakeContext('other_project', 'other_class') result = self.driver.get_user_quotas( - FakeContext('test_project', 'test_class'), - quota.QUOTAS._resources, 'test_project', 'fake_user', + ctxt, quota.QUOTAS._resources, 'test_project', 'fake_user', quota_class='test_class') self.assertEqual(self.calls, [ @@ -1498,6 +1654,9 @@ class DbQuotaDriverTestCase(test.TestCase): 'quota_usage_get_all_by_project_and_user', 'quota_class_get_all_by_name', ]) + mock_get_usages.assert_called_once_with(ctxt, quota.QUOTAS._resources, + 'test_project', + user_id='fake_user') self.assertEqual(result, dict( instances=dict( limit=5, @@ -1551,12 +1710,12 @@ class DbQuotaDriverTestCase(test.TestCase): ), security_group_rules=dict( limit=20, - in_use=0, + in_use=1, reserved=0, ), key_pairs=dict( limit=100, - in_use=0, + in_use=2, reserved=0, ), server_groups=dict( @@ -1566,17 +1725,20 @@ class DbQuotaDriverTestCase(test.TestCase): ), server_group_members=dict( limit=10, - in_use=0, + in_use=3, reserved=0, ), )) - def test_get_project_quotas_alt_context_with_class(self): + @mock.patch('nova.quota.DbQuotaDriver._get_usages', + side_effect=_get_fake_get_usages()) + def test_get_project_quotas_alt_context_with_class(self, mock_get_usages): self.maxDiff = None self._stub_get_by_project() + ctxt = FakeContext('other_project', 'other_class') result = self.driver.get_project_quotas( - FakeContext('other_project', 'other_class'), - quota.QUOTAS._resources, 'test_project', quota_class='test_class') + ctxt, quota.QUOTAS._resources, 'test_project', + quota_class='test_class') self.assertEqual(self.calls, [ 'quota_get_all_by_project', @@ -1584,6 +1746,8 @@ class DbQuotaDriverTestCase(test.TestCase): 'quota_class_get_all_by_name', 'quota_class_get_default', ]) + mock_get_usages.assert_called_once_with(ctxt, quota.QUOTAS._resources, + 'test_project') self.assertEqual(result, dict( instances=dict( limit=5, @@ -1637,12 +1801,12 @@ class DbQuotaDriverTestCase(test.TestCase): ), security_group_rules=dict( limit=20, - in_use=0, + in_use=1, reserved=0, ), key_pairs=dict( limit=100, - in_use=0, + in_use=2, reserved=0, ), server_groups=dict( @@ -1652,16 +1816,18 @@ class DbQuotaDriverTestCase(test.TestCase): ), server_group_members=dict( limit=10, - in_use=0, + in_use=3, reserved=0, ), )) - def test_get_user_quotas_no_defaults(self): + @mock.patch('nova.quota.DbQuotaDriver._get_usages', + side_effect=_get_fake_get_usages()) + def test_get_user_quotas_no_defaults(self, mock_get_usages): self._stub_get_by_project_and_user() + ctxt = FakeContext('test_project', 'test_class') result = self.driver.get_user_quotas( - FakeContext('test_project', 'test_class'), - quota.QUOTAS._resources, 'test_project', 'fake_user', + ctxt, quota.QUOTAS._resources, 'test_project', 'fake_user', defaults=False) self.assertEqual(self.calls, [ @@ -1670,6 +1836,9 @@ class DbQuotaDriverTestCase(test.TestCase): 'quota_usage_get_all_by_project_and_user', 'quota_class_get_all_by_name', ]) + mock_get_usages.assert_called_once_with(ctxt, quota.QUOTAS._resources, + 'test_project', + user_id='fake_user') self.assertEqual(result, dict( cores=dict( limit=10, @@ -1688,11 +1857,13 @@ class DbQuotaDriverTestCase(test.TestCase): ), )) - def test_get_project_quotas_no_defaults(self): + @mock.patch('nova.quota.DbQuotaDriver._get_usages', + side_effect=_get_fake_get_usages()) + def test_get_project_quotas_no_defaults(self, mock_get_usages): self._stub_get_by_project() + ctxt = FakeContext('test_project', 'test_class') result = self.driver.get_project_quotas( - FakeContext('test_project', 'test_class'), - quota.QUOTAS._resources, 'test_project', defaults=False) + ctxt, quota.QUOTAS._resources, 'test_project', defaults=False) self.assertEqual(self.calls, [ 'quota_get_all_by_project', @@ -1700,6 +1871,8 @@ class DbQuotaDriverTestCase(test.TestCase): 'quota_class_get_all_by_name', 'quota_class_get_default', ]) + mock_get_usages.assert_called_once_with(ctxt, quota.QUOTAS._resources, + 'test_project') self.assertEqual(result, dict( cores=dict( limit=10, @@ -2231,6 +2404,94 @@ class DbQuotaDriverTestCase(test.TestCase): quota.QUOTAS._resources, dict(metadata_items=128)) + def test_limit_check_project_and_user_no_values(self): + self.assertRaises(exception.Invalid, + self.driver.limit_check_project_and_user, + FakeContext('test_project', 'test_class'), + quota.QUOTAS._resources) + + def test_limit_check_project_and_user_under(self): + self._stub_get_project_quotas() + ctxt = FakeContext('test_project', 'test_class') + resources = self._get_fake_countable_resources() + # Check: only project_values, only user_values, and then both. + kwargs = [{'project_values': {'fixed_ips': -1}}, + {'user_values': {'key_pairs': -1}}, + {'project_values': {'instances': -1}, + 'user_values': {'instances': -1}}] + for kwarg in kwargs: + self.assertRaises(exception.InvalidQuotaValue, + self.driver.limit_check_project_and_user, + ctxt, resources, **kwarg) + + def test_limit_check_project_and_user_over_project(self): + # Check the case where user_values pass user quota but project_values + # exceed project quota. + self.flags(instances=5, group='quota') + self._stub_get_project_quotas() + resources = self._get_fake_countable_resources() + self.assertRaises(exception.OverQuota, + self.driver.limit_check_project_and_user, + FakeContext('test_project', 'test_class'), + resources, + project_values=dict(instances=6), + user_values=dict(instances=5)) + + def test_limit_check_project_and_user_over_user(self): + self.flags(instances=5, group='quota') + self._stub_get_project_quotas() + resources = self._get_fake_countable_resources() + # It's not realistic for user_values to be higher than project_values, + # but this is just for testing the fictional case where project_values + # pass project quota but user_values exceed user quota. + self.assertRaises(exception.OverQuota, + self.driver.limit_check_project_and_user, + FakeContext('test_project', 'test_class'), + resources, + project_values=dict(instances=5), + user_values=dict(instances=6)) + + def test_limit_check_project_and_user_overs(self): + self._stub_get_project_quotas() + ctxt = FakeContext('test_project', 'test_class') + resources = self._get_fake_countable_resources() + # Check: only project_values, only user_values, and then both. + kwargs = [{'project_values': {'fixed_ips': 10241}}, + {'user_values': {'key_pairs': 256}}, + {'project_values': {'instances': 512}, + 'user_values': {'instances': 256}}] + for kwarg in kwargs: + self.assertRaises(exception.OverQuota, + self.driver.limit_check_project_and_user, + ctxt, resources, **kwarg) + + def test_limit_check_project_and_user_unlimited(self): + self.flags(fixed_ips=-1, group='quota') + self.flags(key_pairs=-1, group='quota') + self.flags(instances=-1, group='quota') + self._stub_get_project_quotas() + ctxt = FakeContext('test_project', 'test_class') + resources = self._get_fake_countable_resources() + # Check: only project_values, only user_values, and then both. + kwargs = [{'project_values': {'fixed_ips': 32767}}, + {'user_values': {'key_pairs': 32767}}, + {'project_values': {'instances': 32767}, + 'user_values': {'instances': 32767}}] + for kwarg in kwargs: + self.driver.limit_check_project_and_user(ctxt, resources, **kwarg) + + def test_limit_check_project_and_user(self): + self._stub_get_project_quotas() + ctxt = FakeContext('test_project', 'test_class') + resources = self._get_fake_countable_resources() + # Check: only project_values, only user_values, and then both. + kwargs = [{'project_values': {'fixed_ips': 5}}, + {'user_values': {'key_pairs': 5}}, + {'project_values': {'instances': 5}, + 'user_values': {'instances': 5}}] + for kwarg in kwargs: + self.driver.limit_check_project_and_user(ctxt, resources, **kwarg) + def _stub_quota_reserve(self): def fake_quota_reserve(context, resources, quotas, user_quotas, deltas, expire, until_refresh, max_age, project_id=None,