def _delete_nested_quota(self, ctxt, proj_id): # Get the parent_id of the target project to verify whether we are # dealing with hierarchical namespace or non-hierarchical # namespace. try: project_quotas = QUOTAS.get_project_quotas( ctxt, proj_id, usages=True, defaults=False) except exception.NotAuthorized: raise webob.exc.HTTPForbidden() target_project = quota_utils.get_project_hierarchy( ctxt, proj_id) parent_id = target_project.parent_id # If the project which is being deleted has allocated part of its # quota to its subprojects, then subprojects' quotas should be # deleted first. for key, value in project_quotas.items(): if 'allocated' in project_quotas[key].keys(): if project_quotas[key]['allocated'] != 0: msg = _("About to delete child projects having " "non-zero quota. This should not be performed") raise webob.exc.HTTPBadRequest(explanation=msg) if parent_id: # Get the children of the project which the token is scoped to # in order to know if the target_project is in its hierarchy. context_project = quota_utils.get_project_hierarchy( ctxt, ctxt.project_id, subtree_as_ids=True) self._authorize_update_or_delete(context_project, target_project.id, parent_id) parent_project_quotas = QUOTAS.get_project_quotas( ctxt, parent_id) # Delete child quota first and later update parent's quota. try: db.quota_destroy_by_project(ctxt, target_project.id) except exception.AdminRequired: raise webob.exc.HTTPForbidden() # The parent "gives" quota to its child using the "allocated" value # and since the child project is getting deleted, we should restore # the child projects quota to the parent quota, but lowering it's # allocated value for key, value in project_quotas.items(): project_hard_limit = project_quotas[key]['limit'] parent_allocated = parent_project_quotas[key]['allocated'] parent_allocated -= project_hard_limit db.quota_allocated_update(ctxt, parent_id, key, parent_allocated)
def update(self, req, id, body): """Update Quota for a particular tenant This works for hierarchical and non-hierarchical projects. For hierarchical projects only immediate parent admin or the CLOUD admin are able to perform an update. :param req: request :param id: target project id that needs to be updated :param body: key, value pair that that will be applied to the resources if the update succeeds """ context = req.environ['cinder.context'] authorize_update(context) self.validate_string_length(id, 'quota_set_name', min_length=1, max_length=255) self.assert_valid_body(body, 'quota_set') # Get the optional argument 'skip_validation' from body, # if skip_validation is False, then validate existing resource. skip_flag = body.get('skip_validation', True) if not utils.is_valid_boolstr(skip_flag): msg = _("Invalid value '%s' for skip_validation.") % skip_flag raise exception.InvalidParameterValue(err=msg) skip_flag = strutils.bool_from_string(skip_flag) target_project_id = id bad_keys = [] # NOTE(ankit): Pass #1 - In this loop for body['quota_set'].items(), # we figure out if we have any bad keys. for key, value in body['quota_set'].items(): if (key not in QUOTAS and key not in NON_QUOTA_KEYS): bad_keys.append(key) continue if len(bad_keys) > 0: msg = _("Bad key(s) in quota set: %s") % ",".join(bad_keys) raise webob.exc.HTTPBadRequest(explanation=msg) # Saving off this value since we need to use it multiple times use_nested_quotas = QUOTAS.using_nested_quotas() if use_nested_quotas: # Get the parent_id of the target project to verify whether we are # dealing with hierarchical namespace or non-hierarchical namespace target_project = quota_utils.get_project_hierarchy( context, target_project_id) parent_id = target_project.parent_id if parent_id: # Get the children of the project which the token is scoped to # in order to know if the target_project is in its hierarchy. context_project = quota_utils.get_project_hierarchy( context, context.project_id, subtree_as_ids=True) self._authorize_update_or_delete(context_project, target_project.id, parent_id) parent_project_quotas = QUOTAS.get_project_quotas( context, parent_id) # NOTE(ankit): Pass #2 - In this loop for body['quota_set'].keys(), # we validate the quota limits to ensure that we can bail out if # any of the items in the set is bad. Meanwhile we validate value # to ensure that the value can't be lower than number of existing # resources. quota_values = QUOTAS.get_project_quotas(context, target_project_id, defaults=False) valid_quotas = {} allocated_quotas = {} for key in body['quota_set'].keys(): if key in NON_QUOTA_KEYS: continue if not skip_flag: self._validate_existing_resource(key, value, quota_values) if use_nested_quotas and parent_id: value = self._validate_quota_limit(body['quota_set'], key, quota_values, parent_project_quotas) if value < 0: # TODO(mc_nair): extend to handle -1 limits and recurse up # the hierarchy msg = _("Quota can't be set to -1 for child projects.") raise webob.exc.HTTPBadRequest(explanation=msg) original_quota = 0 if quota_values.get(key): original_quota = quota_values[key]['limit'] allocated_quotas[key] = ( parent_project_quotas[key].get('allocated', 0) + value - original_quota) else: value = self._validate_quota_limit(body['quota_set'], key) valid_quotas[key] = value # NOTE(ankit): Pass #3 - At this point we know that all the keys and # values are valid and we can iterate and update them all in one shot # without having to worry about rolling back etc as we have done # the validation up front in the 2 loops above. for key, value in valid_quotas.items(): try: db.quota_update(context, target_project_id, key, value) except exception.ProjectQuotaNotFound: db.quota_create(context, target_project_id, key, value) except exception.AdminRequired: raise webob.exc.HTTPForbidden() # If hierarchical projects, update child's quota first # and then parents quota. In future this needs to be an # atomic operation. if use_nested_quotas and parent_id: if key in allocated_quotas.keys(): try: db.quota_allocated_update(context, parent_id, key, allocated_quotas[key]) except exception.ProjectQuotaNotFound: parent_limit = parent_project_quotas[key]['limit'] db.quota_create(context, parent_id, key, parent_limit, allocated=allocated_quotas[key]) return {'quota_set': self._get_quotas(context, target_project_id)}
def delete(self, req, id): """Delete Quota for a particular tenant. This works for hierarchical and non-hierarchical projects. For hierarchical projects only immediate parent admin or the CLOUD admin are able to perform a delete. :param req: request :param id: target project id that needs to be updated """ context = req.environ['cinder.context'] authorize_delete(context) # Get the parent_id of the target project to verify whether we are # dealing with hierarchical namespace or non-hierarchical namespace. target_project = self._get_project(context, id) parent_id = target_project.parent_id try: project_quotas = QUOTAS.get_project_quotas( context, target_project.id, usages=True, parent_project_id=parent_id, defaults=False) except exception.NotAuthorized: raise webob.exc.HTTPForbidden() # If the project which is being deleted has allocated part of its quota # to its subprojects, then subprojects' quotas should be deleted first. for key, value in project_quotas.items(): if 'allocated' in project_quotas[key].keys(): if project_quotas[key]['allocated'] != 0: msg = _("About to delete child projects having " "non-zero quota. This should not be performed") raise webob.exc.HTTPBadRequest(explanation=msg) if parent_id: # Get the children of the project which the token is scoped to in # order to know if the target_project is in its hierarchy. context_project = self._get_project(context, context.project_id, subtree_as_ids=True) self._authorize_update_or_delete(context_project, target_project.id, parent_id) parent_project_quotas = QUOTAS.get_project_quotas( context, parent_id, parent_project_id=parent_id) # Delete child quota first and later update parent's quota. try: db.quota_destroy_by_project(context, target_project.id) except exception.AdminRequired: raise webob.exc.HTTPForbidden() # Update the allocated of the parent for key, value in project_quotas.items(): project_hard_limit = project_quotas[key]['limit'] parent_allocated = parent_project_quotas[key]['allocated'] parent_allocated -= project_hard_limit db.quota_allocated_update(context, parent_id, key, parent_allocated) else: try: db.quota_destroy_by_project(context, target_project.id) except exception.AdminRequired: raise webob.exc.HTTPForbidden()
def update(self, req, id, body): """Update Quota for a particular tenant This works for hierarchical and non-hierarchical projects. For hierarchical projects only immediate parent admin or the CLOUD admin are able to perform an update. :param req: request :param id: target project id that needs to be updated :param body: key, value pair that that will be applied to the resources if the update succeeds """ context = req.environ['cinder.context'] authorize_update(context) self.validate_string_length(id, 'quota_set_name', min_length=1, max_length=255) self.assert_valid_body(body, 'quota_set') # Get the optional argument 'skip_validation' from body, # if skip_validation is False, then validate existing resource. skip_flag = body.get('skip_validation', True) if not utils.is_valid_boolstr(skip_flag): msg = _("Invalid value '%s' for skip_validation.") % skip_flag raise exception.InvalidParameterValue(err=msg) skip_flag = strutils.bool_from_string(skip_flag) target_project_id = id bad_keys = [] # NOTE(ankit): Pass #1 - In this loop for body['quota_set'].items(), # we figure out if we have any bad keys. for key, value in body['quota_set'].items(): if (key not in QUOTAS and key not in NON_QUOTA_KEYS): bad_keys.append(key) continue if len(bad_keys) > 0: msg = _("Bad key(s) in quota set: %s") % ",".join(bad_keys) raise webob.exc.HTTPBadRequest(explanation=msg) # Get the parent_id of the target project to verify whether we are # dealing with hierarchical namespace or non-hierarchical namespace. target_project = self._get_project(context, target_project_id) parent_id = target_project.parent_id if parent_id: # Get the children of the project which the token is scoped to # in order to know if the target_project is in its hierarchy. context_project = self._get_project(context, context.project_id, subtree_as_ids=True) self._authorize_update_or_delete(context_project, target_project.id, parent_id) parent_project_quotas = QUOTAS.get_project_quotas( context, parent_id) # NOTE(ankit): Pass #2 - In this loop for body['quota_set'].keys(), # we validate the quota limits to ensure that we can bail out if # any of the items in the set is bad. Meanwhile we validate value # to ensure that the value can't be lower than number of existing # resources. quota_values = QUOTAS.get_project_quotas(context, target_project_id, defaults=False) valid_quotas = {} allocated_quotas = {} for key in body['quota_set'].keys(): if key in NON_QUOTA_KEYS: continue if not skip_flag: self._validate_existing_resource(key, value, quota_values) if parent_id: value = self._validate_quota_limit(body['quota_set'], key, quota_values, parent_project_quotas) original_quota = 0 if quota_values.get(key): original_quota = quota_values[key]['limit'] allocated_quotas[key] = ( parent_project_quotas[key].get('allocated', 0) + value - original_quota) else: value = self._validate_quota_limit(body['quota_set'], key) valid_quotas[key] = value # NOTE(ankit): Pass #3 - At this point we know that all the keys and # values are valid and we can iterate and update them all in one shot # without having to worry about rolling back etc as we have done # the validation up front in the 2 loops above. for key, value in valid_quotas.items(): try: db.quota_update(context, target_project_id, key, value) except exception.ProjectQuotaNotFound: db.quota_create(context, target_project_id, key, value) except exception.AdminRequired: raise webob.exc.HTTPForbidden() # If hierarchical projects, update child's quota first # and then parents quota. In future this needs to be an # atomic operation. if parent_id: if key in allocated_quotas.keys(): try: db.quota_allocated_update(context, parent_id, key, allocated_quotas[key]) except exception.ProjectQuotaNotFound: parent_limit = parent_project_quotas[key]['limit'] db.quota_create(context, parent_id, key, parent_limit, allocated=allocated_quotas[key]) return { 'quota_set': self._get_quotas(context, target_project_id, parent_project_id=parent_id) }
def delete(self, req, id): """Delete Quota for a particular tenant. This works for hierarchical and non-hierarchical projects. For hierarchical projects only immediate parent admin or the CLOUD admin are able to perform a delete. :param req: request :param id: target project id that needs to be updated """ context = req.environ['cinder.context'] authorize_delete(context) # Get the parent_id of the target project to verify whether we are # dealing with hierarchical namespace or non-hierarchical namespace. target_project = self._get_project(context, id) parent_id = target_project.parent_id try: project_quotas = QUOTAS.get_project_quotas( context, target_project.id, usages=True, parent_project_id=parent_id) except exception.NotAuthorized: raise webob.exc.HTTPForbidden() # If the project which is being deleted has allocated part of its quota # to its subprojects, then subprojects' quotas should be deleted first. for key, value in project_quotas.items(): if 'allocated' in project_quotas[key].keys(): if project_quotas[key]['allocated'] != 0: msg = _("About to delete child projects having " "non-zero quota. This should not be performed") raise webob.exc.HTTPBadRequest(explanation=msg) if parent_id: # Get the children of the project which the token is scoped to in # order to know if the target_project is in its hierarchy. context_project = self._get_project(context, context.project_id, subtree_as_ids=True) self._authorize_update_or_delete(context_project, target_project.id, parent_id) parent_project_quotas = QUOTAS.get_project_quotas( context, parent_id, parent_project_id=parent_id) # Delete child quota first and later update parent's quota. try: db.quota_destroy_by_project(context, target_project.id) except exception.AdminRequired: raise webob.exc.HTTPForbidden() # Update the allocated of the parent for key, value in project_quotas.items(): project_hard_limit = project_quotas[key]['limit'] parent_allocated = parent_project_quotas[key]['allocated'] parent_allocated -= project_hard_limit db.quota_allocated_update(context, parent_id, key, parent_allocated) else: try: db.quota_destroy_by_project(context, target_project.id) except exception.AdminRequired: raise webob.exc.HTTPForbidden()
def validate_nested_setup(self, ctxt, resources, project_tree, fix_allocated_quotas=False): """Ensures project_tree has quotas that make sense as nested quotas. Validates the following: * No child projects have a limit of -1 * No parent project has child_projects who have more combined quota than the parent's quota limit * No child quota has a larger in-use value than it's current limit (could happen before because child default values weren't enforced) * All parent projects' "allocated" quotas match the sum of the limits of its children projects """ project_queue = deque(project_tree.items()) borked_allocated_quotas = {} while project_queue: # Tuple of (current root node, subtree) cur_project_id, project_subtree = project_queue.popleft() # If we're on a leaf node, no need to do validation on it, and in # order to avoid complication trying to get its children, skip it. if not project_subtree: continue cur_project_quotas = self.get_project_quotas( ctxt, resources, cur_project_id) child_project_ids = project_subtree.keys() child_project_quotas = {child_id: self.get_project_quotas( ctxt, resources, child_id) for child_id in child_project_ids} # Validate each resource when compared to it's child quotas for resource in cur_project_quotas.keys(): child_limit_sum = 0 for child_id, child_quota in child_project_quotas.items(): child_limit = child_quota[resource]['limit'] # Don't want to continue validation if -1 limit for child # TODO(mc_nair) - remove when allowing -1 for subprojects if child_limit < 0: msg = _("Quota limit is -1 for child project " "'%(proj)s' for resource '%(res)s'") % { 'proj': child_id, 'res': resource } raise exception.InvalidNestedQuotaSetup(reason=msg) # Handle the case that child default quotas weren't being # properly enforced before elif child_quota[resource].get('in_use', 0) > child_limit: msg = _("Quota limit invalid for project '%(proj)s' " "for resource '%(res)s': limit of %(limit)d " "is less than in-use value of %(used)d") % { 'proj': child_id, 'res': resource, 'limit': child_limit, 'used': child_quota[resource]['in_use'] } raise exception.InvalidNestedQuotaSetup(reason=msg) child_limit_sum += child_quota[resource]['limit'] parent_quota = cur_project_quotas[resource] parent_limit = parent_quota['limit'] parent_usage = parent_quota['in_use'] parent_allocated = parent_quota.get('allocated', 0) if parent_limit > 0: parent_free_quota = parent_limit - parent_usage if parent_free_quota < child_limit_sum: msg = _("Sum of child limits '%(sum)s' is greater " "than free quota of '%(free)s' for project " "'%(proj)s' for resource '%(res)s'. Please " "lower the limit for one or more of the " "following projects: '%(child_ids)s'") % { 'sum': child_limit_sum, 'free': parent_free_quota, 'proj': cur_project_id, 'res': resource, 'child_ids': ', '.join(child_project_ids) } raise exception.InvalidNestedQuotaSetup(reason=msg) # Deal with the fact that using -1 limits in the past may # have messed some allocated values in DB if parent_allocated != child_limit_sum: # Decide whether to fix the allocated val or just # keep track of what's messed up if fix_allocated_quotas: try: db.quota_allocated_update(ctxt, cur_project_id, resource, child_limit_sum) except exception.ProjectQuotaNotFound: # Handles the case that the project is using # default quota value so nothing present to update db.quota_create( ctxt, cur_project_id, resource, parent_limit, allocated=child_limit_sum) else: if cur_project_id not in borked_allocated_quotas: borked_allocated_quotas[cur_project_id] = {} borked_allocated_quotas[cur_project_id][resource] = { 'db_allocated_quota': parent_allocated, 'expected_allocated_quota': child_limit_sum} project_queue.extend(project_subtree.items()) if borked_allocated_quotas: msg = _("Invalid allocated quotas defined for the following " "project quotas: %s") % borked_allocated_quotas raise exception.InvalidNestedQuotaSetup(message=msg)