cinder/cinder/api/contrib/quotas.py

431 lines
19 KiB
Python

# Copyright 2011 OpenStack Foundation
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from oslo_log import log as logging
from oslo_utils import strutils
import webob
from cinder.api import extensions
from cinder.api.openstack import wsgi
from cinder.api.schemas import quotas
from cinder.api import validation
from cinder import db
from cinder import exception
from cinder.i18n import _
from cinder.policies import quotas as policy
from cinder import quota
from cinder import quota_utils
from cinder import utils
LOG = logging.getLogger(__name__)
QUOTAS = quota.QUOTAS
GROUP_QUOTAS = quota.GROUP_QUOTAS
NON_QUOTA_KEYS = quota.NON_QUOTA_KEYS
class QuotaSetsController(wsgi.Controller):
def _format_quota_set(self, project_id, quota_set):
"""Convert the quota object to a result dict."""
quota_set['id'] = str(project_id)
return dict(quota_set=quota_set)
def _validate_existing_resource(self, key, value, quota_values):
# -1 limit will always be greater than the existing value
if key == 'per_volume_gigabytes' or value == -1:
return
v = quota_values.get(key, {})
used = (v.get('in_use', 0) + v.get('reserved', 0))
if QUOTAS.using_nested_quotas():
used += v.get('allocated', 0)
if value < used:
msg = (_("Quota %(key)s limit must be equal or greater than "
"existing resources. Current usage is %(usage)s "
"and the requested limit is %(limit)s.")
% {'key': key,
'usage': used,
'limit': value})
raise webob.exc.HTTPBadRequest(explanation=msg)
def _get_quotas(self, context, id, usages=False):
values = QUOTAS.get_project_quotas(context, id, usages=usages)
group_values = GROUP_QUOTAS.get_project_quotas(context, id,
usages=usages)
values.update(group_values)
if usages:
return values
else:
return {k: v['limit'] for k, v in values.items()}
def _authorize_update_or_delete(self, context_project,
target_project_id,
parent_id):
"""Checks if update or delete are allowed in the current hierarchy.
With hierarchical projects, only the admin of the parent or the root
project has privilege to perform quota update and delete operations.
:param context_project: The project in which the user is scoped to.
:param target_project_id: The id of the project in which the
user want to perform an update or
delete operation.
:param parent_id: The parent id of the project in which the user
want to perform an update or delete operation.
"""
if context_project.is_admin_project:
# The calling project has admin privileges and should be able
# to operate on all quotas.
return
if context_project.parent_id and parent_id != context_project.id:
msg = _("Update and delete quota operations can only be made "
"by an admin of immediate parent or by the CLOUD admin.")
raise webob.exc.HTTPForbidden(explanation=msg)
if context_project.id != target_project_id:
if not self._is_descendant(target_project_id,
context_project.subtree):
msg = _("Update and delete quota operations can only be made "
"to projects in the same hierarchy of the project in "
"which users are scoped to.")
raise webob.exc.HTTPForbidden(explanation=msg)
else:
msg = _("Update and delete quota operations can only be made "
"by an admin of immediate parent or by the CLOUD admin.")
raise webob.exc.HTTPForbidden(explanation=msg)
def _authorize_show(self, context_project, target_project):
"""Checks if show is allowed in the current hierarchy.
With hierarchical projects, users are allowed to perform a quota show
operation if they have the cloud admin role or if they belong to at
least one of the following projects: the target project, its immediate
parent project, or the root project of its hierarchy.
:param context_project: The project in which the user
is scoped to.
:param target_project: The project in which the user wants
to perform a show operation.
"""
if context_project.is_admin_project:
# The calling project has admin privileges and should be able
# to view all quotas.
return
if target_project.parent_id:
if target_project.id != context_project.id:
if not self._is_descendant(target_project.id,
context_project.subtree):
msg = _("Show operations can only be made to projects in "
"the same hierarchy of the project in which users "
"are scoped to.")
raise webob.exc.HTTPForbidden(explanation=msg)
if context_project.id != target_project.parent_id:
if context_project.parent_id:
msg = _("Only users with token scoped to immediate "
"parents or root projects are allowed to see "
"its children quotas.")
raise webob.exc.HTTPForbidden(explanation=msg)
elif context_project.parent_id:
msg = _("An user with a token scoped to a subproject is not "
"allowed to see the quota of its parents.")
raise webob.exc.HTTPForbidden(explanation=msg)
def _is_descendant(self, target_project_id, subtree):
if subtree is not None:
for key, value in subtree.items():
if key == target_project_id:
return True
if self._is_descendant(target_project_id, value):
return True
return False
def show(self, req, id):
"""Show quota for a particular tenant
This works for hierarchical and non-hierarchical projects. For
hierarchical projects admin of current project, immediate
parent of the project or the CLOUD admin are able to perform
a show.
:param req: request
:param id: target project id that needs to be shown
"""
context = req.environ['cinder.context']
params = req.params
target_project_id = id
context.authorize(policy.SHOW_POLICY,
target={'project_id': target_project_id})
if not hasattr(params, '__call__') and 'usage' in params:
usage = utils.get_bool_param('usage', params)
else:
usage = False
if QUOTAS.using_nested_quotas():
# With hierarchical projects, only the admin of the current project
# or the root project has privilege to perform quota show
# operations.
target_project = quota_utils.get_project_hierarchy(
context, target_project_id)
context_project = quota_utils.get_project_hierarchy(
context, context.project_id, subtree_as_ids=True,
is_admin_project=context.is_admin)
self._authorize_show(context_project, target_project)
quotas = self._get_quotas(context, target_project_id, usage)
return self._format_quota_set(target_project_id, quotas)
@validation.schema(quotas.update_quota)
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 will be applied to
the resources if the update succeeds
"""
context = req.environ['cinder.context']
target_project_id = id
context.authorize(policy.UPDATE_POLICY,
target={'project_id': target_project_id})
self.validate_string_length(id, 'quota_set_name',
min_length=1, max_length=255)
# 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, parents_as_ids=True)
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,
is_admin_project=context.is_admin)
self._authorize_update_or_delete(context_project,
target_project.id,
parent_id)
# NOTE(ankit): Pass #1 - 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)
group_quota_values = GROUP_QUOTAS.get_project_quotas(context,
target_project_id,
defaults=False)
quota_values.update(group_quota_values)
valid_quotas = {}
reservations = []
for key in body['quota_set'].keys():
if key in NON_QUOTA_KEYS:
continue
self._validate_existing_resource(key, body['quota_set'][key],
quota_values)
if use_nested_quotas:
try:
reservations += self._update_nested_quota_allocated(
context, target_project, quota_values, key,
body['quota_set'][key])
except exception.OverQuota as e:
if reservations:
db.reservation_rollback(context, reservations)
raise webob.exc.HTTPBadRequest(explanation=e.msg)
valid_quotas[key] = body['quota_set'][key]
# NOTE(ankit): Pass #2 - 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 reservations:
db.reservation_commit(context, reservations)
return {'quota_set': self._get_quotas(context, target_project_id)}
def _get_quota_usage(self, quota_obj):
return (quota_obj.get('in_use', 0) + quota_obj.get('allocated', 0) +
quota_obj.get('reserved', 0))
def _update_nested_quota_allocated(self, ctxt, target_project,
target_project_quotas, res, new_limit):
reservations = []
# per_volume_gigabytes doesn't make sense to nest
if res == "per_volume_gigabytes":
return reservations
quota_for_res = target_project_quotas.get(res, {})
orig_quota_from_target_proj = quota_for_res.get('limit', 0)
# If limit was -1, we were "taking" current child's usage from parent
if orig_quota_from_target_proj == -1:
orig_quota_from_target_proj = self._get_quota_usage(quota_for_res)
new_quota_from_target_proj = new_limit
# If we set limit to -1, we will "take" the current usage from parent
if new_limit == -1:
new_quota_from_target_proj = self._get_quota_usage(quota_for_res)
res_change = new_quota_from_target_proj - orig_quota_from_target_proj
if res_change != 0:
deltas = {res: res_change}
resources = QUOTAS.resources
resources.update(GROUP_QUOTAS.resources)
reservations += quota_utils.update_alloc_to_next_hard_limit(
ctxt, resources, deltas, res, None, target_project.id)
return reservations
def defaults(self, req, id):
context = req.environ['cinder.context']
context.authorize(policy.SHOW_POLICY, target={'project_id': id})
defaults = QUOTAS.get_defaults(context, project_id=id)
group_defaults = GROUP_QUOTAS.get_defaults(context, project_id=id)
defaults.update(group_defaults)
return self._format_quota_set(id, defaults)
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 deleted
"""
context = req.environ['cinder.context']
context.authorize(policy.DELETE_POLICY, target={'project_id': id})
if QUOTAS.using_nested_quotas():
self._delete_nested_quota(context, id)
else:
db.quota_destroy_by_project(context, id)
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)
project_group_quotas = GROUP_QUOTAS.get_project_quotas(
ctxt, proj_id, usages=True, defaults=False)
project_quotas.update(project_group_quotas)
except exception.NotAuthorized:
raise webob.exc.HTTPForbidden()
target_project = quota_utils.get_project_hierarchy(
ctxt, proj_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(
ctxt, ctxt.project_id, subtree_as_ids=True)
self._authorize_update_or_delete(context_project,
target_project.id,
parent_id)
defaults = QUOTAS.get_defaults(ctxt, proj_id)
defaults.update(GROUP_QUOTAS.get_defaults(ctxt, proj_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 res, value in project_quotas.items():
if 'allocated' in project_quotas[res].keys():
if project_quotas[res]['allocated'] > 0:
msg = _("About to delete child projects having "
"non-zero quota. This should not be performed")
raise webob.exc.HTTPBadRequest(explanation=msg)
# Ensure quota usage wouldn't exceed limit on a delete
self._validate_existing_resource(
res, defaults[res], project_quotas)
db.quota_destroy_by_project(ctxt, target_project.id)
for res, limit in project_quotas.items():
# Update child limit to 0 so the parent hierarchy gets it's
# allocated values updated properly
self._update_nested_quota_allocated(
ctxt, target_project, project_quotas, res, 0)
def validate_setup_for_nested_quota_use(self, req):
"""Validates that the setup supports using nested quotas.
Ensures that Keystone v3 or greater is being used, and that the
existing quotas make sense to nest in the current hierarchy (e.g. that
no child quota would be larger than it's parent).
"""
ctxt = req.environ['cinder.context']
ctxt.authorize(policy.VALIDATE_NESTED_QUOTA_POLICY)
params = req.params
try:
resources = QUOTAS.resources
resources.update(GROUP_QUOTAS.resources)
allocated = params.get('fix_allocated_quotas', 'False')
try:
fix_allocated = strutils.bool_from_string(allocated,
strict=True)
except ValueError:
msg = _("Invalid param 'fix_allocated_quotas':%s") % allocated
raise webob.exc.HTTPBadRequest(explanation=msg)
quota_utils.validate_setup_for_nested_quota_use(
ctxt, resources, quota.NestedDbQuotaDriver(),
fix_allocated_quotas=fix_allocated)
except exception.InvalidNestedQuotaSetup as e:
raise webob.exc.HTTPBadRequest(explanation=e.msg)
class Quotas(extensions.ExtensionDescriptor):
"""Quota management support."""
name = "Quotas"
alias = "os-quota-sets"
updated = "2011-08-08T00:00:00+00:00"
def get_resources(self):
resources = []
res = extensions.ResourceExtension(
'os-quota-sets', QuotaSetsController(),
member_actions={'defaults': 'GET'},
collection_actions={'validate_setup_for_nested_quota_use': 'GET'})
resources.append(res)
return resources