"Fossies" - the Fresh Open Source Software Archive

Member "cinder-14.0.2/cinder/api/contrib/quotas.py" (4 Oct 2019, 19374 Bytes) of package /linux/misc/openstack/cinder-14.0.2.tar.gz:


As a special service "Fossies" has tried to format the requested source page into HTML format using (guessed) Python source code syntax highlighting (style: standard) with prefixed line numbers. Alternatively you can here view or download the uninterpreted source code file. For more information about "quotas.py" see the Fossies "Dox" file reference documentation.

    1 # Copyright 2011 OpenStack Foundation
    2 # All Rights Reserved.
    3 #
    4 #    Licensed under the Apache License, Version 2.0 (the "License"); you may
    5 #    not use this file except in compliance with the License. You may obtain
    6 #    a copy of the License at
    7 #
    8 #         http://www.apache.org/licenses/LICENSE-2.0
    9 #
   10 #    Unless required by applicable law or agreed to in writing, software
   11 #    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
   12 #    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
   13 #    License for the specific language governing permissions and limitations
   14 #    under the License.
   15 
   16 import webob
   17 
   18 from oslo_log import log as logging
   19 from oslo_utils import strutils
   20 
   21 from cinder.api import extensions
   22 from cinder.api.openstack import wsgi
   23 from cinder.api.schemas import quotas
   24 from cinder.api import validation
   25 from cinder import db
   26 from cinder import exception
   27 from cinder.i18n import _
   28 from cinder.policies import quotas as policy
   29 from cinder import quota
   30 from cinder import quota_utils
   31 from cinder import utils
   32 
   33 LOG = logging.getLogger(__name__)
   34 
   35 QUOTAS = quota.QUOTAS
   36 GROUP_QUOTAS = quota.GROUP_QUOTAS
   37 NON_QUOTA_KEYS = quota.NON_QUOTA_KEYS
   38 
   39 
   40 class QuotaSetsController(wsgi.Controller):
   41 
   42     def _format_quota_set(self, project_id, quota_set):
   43         """Convert the quota object to a result dict."""
   44 
   45         quota_set['id'] = str(project_id)
   46 
   47         return dict(quota_set=quota_set)
   48 
   49     def _validate_existing_resource(self, key, value, quota_values):
   50         # -1 limit will always be greater than the existing value
   51         if key == 'per_volume_gigabytes' or value == -1:
   52             return
   53         v = quota_values.get(key, {})
   54         used = (v.get('in_use', 0) + v.get('reserved', 0))
   55         if QUOTAS.using_nested_quotas():
   56             used += v.get('allocated', 0)
   57         if value < used:
   58             msg = (_("Quota %(key)s limit must be equal or greater than "
   59                      "existing resources. Current usage is %(usage)s "
   60                      "and the requested limit is %(limit)s.")
   61                    % {'key': key,
   62                       'usage': used,
   63                       'limit': value})
   64             raise webob.exc.HTTPBadRequest(explanation=msg)
   65 
   66     def _get_quotas(self, context, id, usages=False):
   67         values = QUOTAS.get_project_quotas(context, id, usages=usages)
   68         group_values = GROUP_QUOTAS.get_project_quotas(context, id,
   69                                                        usages=usages)
   70         values.update(group_values)
   71 
   72         if usages:
   73             return values
   74         else:
   75             return {k: v['limit'] for k, v in values.items()}
   76 
   77     def _authorize_update_or_delete(self, context_project,
   78                                     target_project_id,
   79                                     parent_id):
   80         """Checks if update or delete are allowed in the current hierarchy.
   81 
   82         With hierarchical projects, only the admin of the parent or the root
   83         project has privilege to perform quota update and delete operations.
   84 
   85         :param context_project: The project in which the user is scoped to.
   86         :param target_project_id: The id of the project in which the
   87                                   user want to perform an update or
   88                                   delete operation.
   89         :param parent_id: The parent id of the project in which the user
   90                           want to perform an update or delete operation.
   91         """
   92         if context_project.is_admin_project:
   93             # The calling project has admin privileges and should be able
   94             # to operate on all quotas.
   95             return
   96         if context_project.parent_id and parent_id != context_project.id:
   97             msg = _("Update and delete quota operations can only be made "
   98                     "by an admin of immediate parent or by the CLOUD admin.")
   99             raise webob.exc.HTTPForbidden(explanation=msg)
  100 
  101         if context_project.id != target_project_id:
  102             if not self._is_descendant(target_project_id,
  103                                        context_project.subtree):
  104                 msg = _("Update and delete quota operations can only be made "
  105                         "to projects in the same hierarchy of the project in "
  106                         "which users are scoped to.")
  107                 raise webob.exc.HTTPForbidden(explanation=msg)
  108         else:
  109             msg = _("Update and delete quota operations can only be made "
  110                     "by an admin of immediate parent or by the CLOUD admin.")
  111             raise webob.exc.HTTPForbidden(explanation=msg)
  112 
  113     def _authorize_show(self, context_project, target_project):
  114         """Checks if show is allowed in the current hierarchy.
  115 
  116         With hierarchical projects, users are allowed to perform a quota show
  117         operation if they have the cloud admin role or if they belong to at
  118         least one of the following projects: the target project, its immediate
  119         parent project, or the root project of its hierarchy.
  120 
  121         :param context_project: The project in which the user
  122                                 is scoped to.
  123         :param target_project: The project in which the user wants
  124                                to perform a show operation.
  125         """
  126         if context_project.is_admin_project:
  127             # The calling project has admin privileges and should be able
  128             # to view all quotas.
  129             return
  130         if target_project.parent_id:
  131             if target_project.id != context_project.id:
  132                 if not self._is_descendant(target_project.id,
  133                                            context_project.subtree):
  134                     msg = _("Show operations can only be made to projects in "
  135                             "the same hierarchy of the project in which users "
  136                             "are scoped to.")
  137                     raise webob.exc.HTTPForbidden(explanation=msg)
  138                 if context_project.id != target_project.parent_id:
  139                     if context_project.parent_id:
  140                         msg = _("Only users with token scoped to immediate "
  141                                 "parents or root projects are allowed to see "
  142                                 "its children quotas.")
  143                         raise webob.exc.HTTPForbidden(explanation=msg)
  144         elif context_project.parent_id:
  145             msg = _("An user with a token scoped to a subproject is not "
  146                     "allowed to see the quota of its parents.")
  147             raise webob.exc.HTTPForbidden(explanation=msg)
  148 
  149     def _is_descendant(self, target_project_id, subtree):
  150         if subtree is not None:
  151             for key, value in subtree.items():
  152                 if key == target_project_id:
  153                     return True
  154                 if self._is_descendant(target_project_id, value):
  155                     return True
  156         return False
  157 
  158     def show(self, req, id):
  159         """Show quota for a particular tenant
  160 
  161         This works for hierarchical and non-hierarchical projects. For
  162         hierarchical projects admin of current project, immediate
  163         parent of the project or the CLOUD admin are able to perform
  164         a show.
  165 
  166         :param req: request
  167         :param id: target project id that needs to be shown
  168         """
  169         context = req.environ['cinder.context']
  170         params = req.params
  171         target_project_id = id
  172         context.authorize(policy.SHOW_POLICY,
  173                           target={'project_id': target_project_id})
  174 
  175         if not hasattr(params, '__call__') and 'usage' in params:
  176             usage = utils.get_bool_param('usage', params)
  177         else:
  178             usage = False
  179 
  180         if QUOTAS.using_nested_quotas():
  181             # With hierarchical projects, only the admin of the current project
  182             # or the root project has privilege to perform quota show
  183             # operations.
  184             target_project = quota_utils.get_project_hierarchy(
  185                 context, target_project_id)
  186             context_project = quota_utils.get_project_hierarchy(
  187                 context, context.project_id, subtree_as_ids=True,
  188                 is_admin_project=context.is_admin)
  189 
  190             self._authorize_show(context_project, target_project)
  191 
  192         quotas = self._get_quotas(context, target_project_id, usage)
  193         return self._format_quota_set(target_project_id, quotas)
  194 
  195     @validation.schema(quotas.update_quota)
  196     def update(self, req, id, body):
  197         """Update Quota for a particular tenant
  198 
  199         This works for hierarchical and non-hierarchical projects. For
  200         hierarchical projects only immediate parent admin or the
  201         CLOUD admin are able to perform an update.
  202 
  203         :param req: request
  204         :param id: target project id that needs to be updated
  205         :param body: key, value pair that will be applied to
  206                      the resources if the update succeeds
  207         """
  208         context = req.environ['cinder.context']
  209         target_project_id = id
  210         context.authorize(policy.UPDATE_POLICY,
  211                           target={'project_id': target_project_id})
  212         self.validate_string_length(id, 'quota_set_name',
  213                                     min_length=1, max_length=255)
  214 
  215         # Saving off this value since we need to use it multiple times
  216         use_nested_quotas = QUOTAS.using_nested_quotas()
  217         if use_nested_quotas:
  218             # Get the parent_id of the target project to verify whether we are
  219             # dealing with hierarchical namespace or non-hierarchical namespace
  220             target_project = quota_utils.get_project_hierarchy(
  221                 context, target_project_id, parents_as_ids=True)
  222             parent_id = target_project.parent_id
  223 
  224             if parent_id:
  225                 # Get the children of the project which the token is scoped to
  226                 # in order to know if the target_project is in its hierarchy.
  227                 context_project = quota_utils.get_project_hierarchy(
  228                     context, context.project_id, subtree_as_ids=True,
  229                     is_admin_project=context.is_admin)
  230                 self._authorize_update_or_delete(context_project,
  231                                                  target_project.id,
  232                                                  parent_id)
  233 
  234         # NOTE(ankit): Pass #1 - In this loop for body['quota_set'].keys(),
  235         # we validate the quota limits to ensure that we can bail out if
  236         # any of the items in the set is bad. Meanwhile we validate value
  237         # to ensure that the value can't be lower than number of existing
  238         # resources.
  239         quota_values = QUOTAS.get_project_quotas(context, target_project_id,
  240                                                  defaults=False)
  241         group_quota_values = GROUP_QUOTAS.get_project_quotas(context,
  242                                                              target_project_id,
  243                                                              defaults=False)
  244         quota_values.update(group_quota_values)
  245         valid_quotas = {}
  246         reservations = []
  247         for key in body['quota_set'].keys():
  248             if key in NON_QUOTA_KEYS:
  249                 continue
  250             self._validate_existing_resource(key, body['quota_set'][key],
  251                                              quota_values)
  252 
  253             if use_nested_quotas:
  254                 try:
  255                     reservations += self._update_nested_quota_allocated(
  256                         context, target_project, quota_values, key,
  257                         body['quota_set'][key])
  258                 except exception.OverQuota as e:
  259                     if reservations:
  260                         db.reservation_rollback(context, reservations)
  261                     raise webob.exc.HTTPBadRequest(explanation=e.msg)
  262 
  263             valid_quotas[key] = body['quota_set'][key]
  264 
  265         # NOTE(ankit): Pass #2 - At this point we know that all the keys and
  266         # values are valid and we can iterate and update them all in one shot
  267         # without having to worry about rolling back etc as we have done
  268         # the validation up front in the 2 loops above.
  269         for key, value in valid_quotas.items():
  270             try:
  271                 db.quota_update(context, target_project_id, key, value)
  272             except exception.ProjectQuotaNotFound:
  273                 db.quota_create(context, target_project_id, key, value)
  274             except exception.AdminRequired:
  275                 raise webob.exc.HTTPForbidden()
  276 
  277         if reservations:
  278             db.reservation_commit(context, reservations)
  279         return {'quota_set': self._get_quotas(context, target_project_id)}
  280 
  281     def _get_quota_usage(self, quota_obj):
  282         return (quota_obj.get('in_use', 0) + quota_obj.get('allocated', 0) +
  283                 quota_obj.get('reserved', 0))
  284 
  285     def _update_nested_quota_allocated(self, ctxt, target_project,
  286                                        target_project_quotas, res, new_limit):
  287         reservations = []
  288         # per_volume_gigabytes doesn't make sense to nest
  289         if res == "per_volume_gigabytes":
  290             return reservations
  291 
  292         quota_for_res = target_project_quotas.get(res, {})
  293         orig_quota_from_target_proj = quota_for_res.get('limit', 0)
  294         # If limit was -1, we were "taking" current child's usage from parent
  295         if orig_quota_from_target_proj == -1:
  296             orig_quota_from_target_proj = self._get_quota_usage(quota_for_res)
  297 
  298         new_quota_from_target_proj = new_limit
  299         # If we set limit to -1, we will "take" the current usage from parent
  300         if new_limit == -1:
  301             new_quota_from_target_proj = self._get_quota_usage(quota_for_res)
  302 
  303         res_change = new_quota_from_target_proj - orig_quota_from_target_proj
  304         if res_change != 0:
  305             deltas = {res: res_change}
  306             resources = QUOTAS.resources
  307             resources.update(GROUP_QUOTAS.resources)
  308             reservations += quota_utils.update_alloc_to_next_hard_limit(
  309                 ctxt, resources, deltas, res, None, target_project.id)
  310 
  311         return reservations
  312 
  313     def defaults(self, req, id):
  314         context = req.environ['cinder.context']
  315         context.authorize(policy.SHOW_POLICY, target={'project_id': id})
  316         defaults = QUOTAS.get_defaults(context, project_id=id)
  317         group_defaults = GROUP_QUOTAS.get_defaults(context, project_id=id)
  318         defaults.update(group_defaults)
  319         return self._format_quota_set(id, defaults)
  320 
  321     def delete(self, req, id):
  322         """Delete Quota for a particular tenant.
  323 
  324         This works for hierarchical and non-hierarchical projects. For
  325         hierarchical projects only immediate parent admin or the
  326         CLOUD admin are able to perform a delete.
  327 
  328         :param req: request
  329         :param id: target project id that needs to be deleted
  330         """
  331         context = req.environ['cinder.context']
  332         context.authorize(policy.DELETE_POLICY, target={'project_id': id})
  333 
  334         if QUOTAS.using_nested_quotas():
  335             self._delete_nested_quota(context, id)
  336         else:
  337             db.quota_destroy_by_project(context, id)
  338 
  339     def _delete_nested_quota(self, ctxt, proj_id):
  340         # Get the parent_id of the target project to verify whether we are
  341         # dealing with hierarchical namespace or non-hierarchical
  342         # namespace.
  343         try:
  344             project_quotas = QUOTAS.get_project_quotas(
  345                 ctxt, proj_id, usages=True, defaults=False)
  346             project_group_quotas = GROUP_QUOTAS.get_project_quotas(
  347                 ctxt, proj_id, usages=True, defaults=False)
  348             project_quotas.update(project_group_quotas)
  349         except exception.NotAuthorized:
  350             raise webob.exc.HTTPForbidden()
  351 
  352         target_project = quota_utils.get_project_hierarchy(
  353             ctxt, proj_id)
  354         parent_id = target_project.parent_id
  355         if parent_id:
  356             # Get the children of the project which the token is scoped to
  357             # in order to know if the target_project is in its hierarchy.
  358             context_project = quota_utils.get_project_hierarchy(
  359                 ctxt, ctxt.project_id, subtree_as_ids=True)
  360             self._authorize_update_or_delete(context_project,
  361                                              target_project.id,
  362                                              parent_id)
  363 
  364         defaults = QUOTAS.get_defaults(ctxt, proj_id)
  365         defaults.update(GROUP_QUOTAS.get_defaults(ctxt, proj_id))
  366         # If the project which is being deleted has allocated part of its
  367         # quota to its subprojects, then subprojects' quotas should be
  368         # deleted first.
  369         for res, value in project_quotas.items():
  370             if 'allocated' in project_quotas[res].keys():
  371                 if project_quotas[res]['allocated'] > 0:
  372                     msg = _("About to delete child projects having "
  373                             "non-zero quota. This should not be performed")
  374                     raise webob.exc.HTTPBadRequest(explanation=msg)
  375             # Ensure quota usage wouldn't exceed limit on a delete
  376             self._validate_existing_resource(
  377                 res, defaults[res], project_quotas)
  378 
  379         db.quota_destroy_by_project(ctxt, target_project.id)
  380 
  381         for res, limit in project_quotas.items():
  382             # Update child limit to 0 so the parent hierarchy gets it's
  383             # allocated values updated properly
  384             self._update_nested_quota_allocated(
  385                 ctxt, target_project, project_quotas, res, 0)
  386 
  387     def validate_setup_for_nested_quota_use(self, req):
  388         """Validates that the setup supports using nested quotas.
  389 
  390         Ensures that Keystone v3 or greater is being used, and that the
  391         existing quotas make sense to nest in the current hierarchy (e.g. that
  392         no child quota would be larger than it's parent).
  393         """
  394         ctxt = req.environ['cinder.context']
  395         ctxt.authorize(policy.VALIDATE_NESTED_QUOTA_POLICY)
  396         params = req.params
  397         try:
  398             resources = QUOTAS.resources
  399             resources.update(GROUP_QUOTAS.resources)
  400             allocated = params.get('fix_allocated_quotas', 'False')
  401             try:
  402                 fix_allocated = strutils.bool_from_string(allocated,
  403                                                           strict=True)
  404             except ValueError:
  405                 msg = _("Invalid param 'fix_allocated_quotas':%s") % allocated
  406                 raise webob.exc.HTTPBadRequest(explanation=msg)
  407 
  408             quota_utils.validate_setup_for_nested_quota_use(
  409                 ctxt, resources, quota.NestedDbQuotaDriver(),
  410                 fix_allocated_quotas=fix_allocated)
  411         except exception.InvalidNestedQuotaSetup as e:
  412             raise webob.exc.HTTPBadRequest(explanation=e.msg)
  413 
  414 
  415 class Quotas(extensions.ExtensionDescriptor):
  416     """Quota management support."""
  417 
  418     name = "Quotas"
  419     alias = "os-quota-sets"
  420     updated = "2011-08-08T00:00:00+00:00"
  421 
  422     def get_resources(self):
  423         resources = []
  424 
  425         res = extensions.ResourceExtension(
  426             'os-quota-sets', QuotaSetsController(),
  427             member_actions={'defaults': 'GET'},
  428             collection_actions={'validate_setup_for_nested_quota_use': 'GET'})
  429         resources.append(res)
  430 
  431         return resources