1# Copyright 2010 United States Government as represented by the 2# Administrator of the National Aeronautics and Space Administration. 3# All Rights Reserved. 4# 5# Licensed under the Apache License, Version 2.0 (the "License"); you may 6# not use this file except in compliance with the License. You may obtain 7# a copy of the License at 8# 9# http://www.apache.org/licenses/LICENSE-2.0 10# 11# Unless required by applicable law or agreed to in writing, software 12# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 13# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 14# License for the specific language governing permissions and limitations 15# under the License. 16 17"""Quotas for volumes.""" 18 19from collections import deque 20import datetime 21 22from oslo_config import cfg 23from oslo_log import log as logging 24from oslo_log import versionutils 25from oslo_utils import importutils 26from oslo_utils import timeutils 27import six 28 29from cinder import context 30from cinder import db 31from cinder import exception 32from cinder.i18n import _ 33from cinder import quota_utils 34 35 36LOG = logging.getLogger(__name__) 37 38quota_opts = [ 39 cfg.IntOpt('quota_volumes', 40 default=10, 41 help='Number of volumes allowed per project'), 42 cfg.IntOpt('quota_snapshots', 43 default=10, 44 help='Number of volume snapshots allowed per project'), 45 cfg.IntOpt('quota_consistencygroups', 46 default=10, 47 help='Number of consistencygroups allowed per project'), 48 cfg.IntOpt('quota_groups', 49 default=10, 50 help='Number of groups allowed per project'), 51 cfg.IntOpt('quota_gigabytes', 52 default=1000, 53 help='Total amount of storage, in gigabytes, allowed ' 54 'for volumes and snapshots per project'), 55 cfg.IntOpt('quota_backups', 56 default=10, 57 help='Number of volume backups allowed per project'), 58 cfg.IntOpt('quota_backup_gigabytes', 59 default=1000, 60 help='Total amount of storage, in gigabytes, allowed ' 61 'for backups per project'), 62 cfg.IntOpt('reservation_expire', 63 default=86400, 64 help='Number of seconds until a reservation expires'), 65 cfg.IntOpt('reservation_clean_interval', 66 default='$reservation_expire', 67 help='Interval between periodic task runs to clean expired ' 68 'reservations in seconds.'), 69 cfg.IntOpt('until_refresh', 70 default=0, 71 help='Count of reservations until usage is refreshed'), 72 cfg.IntOpt('max_age', 73 default=0, 74 help='Number of seconds between subsequent usage refreshes'), 75 cfg.StrOpt('quota_driver', 76 default="cinder.quota.DbQuotaDriver", 77 help='Default driver to use for quota checks'), 78 cfg.BoolOpt('use_default_quota_class', 79 default=True, 80 help='Enables or disables use of default quota class ' 81 'with default quota.'), 82 cfg.IntOpt('per_volume_size_limit', 83 default=-1, 84 help='Max size allowed per volume, in gigabytes'), ] 85 86CONF = cfg.CONF 87CONF.register_opts(quota_opts) 88 89 90class DbQuotaDriver(object): 91 92 """Driver to perform check to enforcement of quotas. 93 94 Also allows to obtain quota information. 95 The default driver utilizes the local database. 96 """ 97 98 def get_by_project(self, context, project_id, resource_name): 99 """Get a specific quota by project.""" 100 101 return db.quota_get(context, project_id, resource_name) 102 103 def get_by_class(self, context, quota_class, resource_name): 104 """Get a specific quota by quota class.""" 105 106 return db.quota_class_get(context, quota_class, resource_name) 107 108 def get_default(self, context, resource, project_id): 109 """Get a specific default quota for a resource.""" 110 default_quotas = db.quota_class_get_defaults(context) 111 return default_quotas.get(resource.name, resource.default) 112 113 def get_defaults(self, context, resources, project_id=None): 114 """Given a list of resources, retrieve the default quotas. 115 116 Use the class quotas named `_DEFAULT_QUOTA_NAME` as default quotas, 117 if it exists. 118 119 :param context: The request context, for access checks. 120 :param resources: A dictionary of the registered resources. 121 :param project_id: The id of the current project 122 """ 123 124 quotas = {} 125 default_quotas = {} 126 if CONF.use_default_quota_class: 127 default_quotas = db.quota_class_get_defaults(context) 128 129 for resource in resources.values(): 130 if default_quotas: 131 if resource.name not in default_quotas: 132 versionutils.report_deprecated_feature(LOG, _( 133 "Default quota for resource: %(res)s is set " 134 "by the default quota flag: quota_%(res)s, " 135 "it is now deprecated. Please use the " 136 "default quota class for default " 137 "quota.") % {'res': resource.name}) 138 quotas[resource.name] = default_quotas.get(resource.name, 139 resource.default) 140 return quotas 141 142 def get_class_quotas(self, context, resources, quota_class, 143 defaults=True): 144 """Given list of resources, retrieve the quotas for given quota class. 145 146 :param context: The request context, for access checks. 147 :param resources: A dictionary of the registered resources. 148 :param quota_class: The name of the quota class to return 149 quotas for. 150 :param defaults: If True, the default value will be reported 151 if there is no specific value for the 152 resource. 153 """ 154 155 quotas = {} 156 default_quotas = {} 157 class_quotas = db.quota_class_get_all_by_name(context, quota_class) 158 if defaults: 159 default_quotas = db.quota_class_get_defaults(context) 160 for resource in resources.values(): 161 if resource.name in class_quotas: 162 quotas[resource.name] = class_quotas[resource.name] 163 continue 164 165 if defaults: 166 quotas[resource.name] = default_quotas.get(resource.name, 167 resource.default) 168 169 return quotas 170 171 def get_project_quotas(self, context, resources, project_id, 172 quota_class=None, defaults=True, 173 usages=True): 174 """Retrieve quotas for a project. 175 176 Given a list of resources, retrieve the quotas for the given 177 project. 178 179 :param context: The request context, for access checks. 180 :param resources: A dictionary of the registered resources. 181 :param project_id: The ID of the project to return quotas for. 182 :param quota_class: If project_id != context.project_id, the 183 quota class cannot be determined. This 184 parameter allows it to be specified. It 185 will be ignored if project_id == 186 context.project_id. 187 :param defaults: If True, the quota class value (or the 188 default value, if there is no value from the 189 quota class) will be reported if there is no 190 specific value for the resource. 191 :param usages: If True, the current in_use, reserved and allocated 192 counts will also be returned. 193 """ 194 195 quotas = {} 196 project_quotas = db.quota_get_all_by_project(context, project_id) 197 allocated_quotas = None 198 default_quotas = None 199 if usages: 200 project_usages = db.quota_usage_get_all_by_project(context, 201 project_id) 202 allocated_quotas = db.quota_allocated_get_all_by_project( 203 context, project_id) 204 allocated_quotas.pop('project_id') 205 206 # Get the quotas for the appropriate class. If the project ID 207 # matches the one in the context, we use the quota_class from 208 # the context, otherwise, we use the provided quota_class (if 209 # any) 210 if project_id == context.project_id: 211 quota_class = context.quota_class 212 if quota_class: 213 class_quotas = db.quota_class_get_all_by_name(context, quota_class) 214 else: 215 class_quotas = {} 216 217 for resource in resources.values(): 218 # Omit default/quota class values 219 if not defaults and resource.name not in project_quotas: 220 continue 221 222 quota_val = project_quotas.get(resource.name) 223 if quota_val is None: 224 quota_val = class_quotas.get(resource.name) 225 if quota_val is None: 226 # Lazy load the default quotas 227 if default_quotas is None: 228 default_quotas = self.get_defaults( 229 context, resources, project_id) 230 quota_val = default_quotas[resource.name] 231 232 quotas[resource.name] = {'limit': quota_val} 233 234 # Include usages if desired. This is optional because one 235 # internal consumer of this interface wants to access the 236 # usages directly from inside a transaction. 237 if usages: 238 usage = project_usages.get(resource.name, {}) 239 quotas[resource.name].update( 240 in_use=usage.get('in_use', 0), 241 reserved=usage.get('reserved', 0), ) 242 if allocated_quotas: 243 quotas[resource.name].update( 244 allocated=allocated_quotas.get(resource.name, 0), ) 245 return quotas 246 247 def _get_quotas(self, context, resources, keys, has_sync, project_id=None): 248 """A helper method which retrieves the quotas for specific resources. 249 250 This specific resource is identified by keys, and which apply to the 251 current context. 252 253 :param context: The request context, for access checks. 254 :param resources: A dictionary of the registered resources. 255 :param keys: A list of the desired quotas to retrieve. 256 :param has_sync: If True, indicates that the resource must 257 have a sync attribute; if False, indicates 258 that the resource must NOT have a sync 259 attribute. 260 :param project_id: Specify the project_id if current context 261 is admin and admin wants to impact on 262 common user's tenant. 263 """ 264 265 # Filter resources 266 if has_sync: 267 sync_filt = lambda x: hasattr(x, 'sync') 268 else: 269 sync_filt = lambda x: not hasattr(x, 'sync') 270 desired = set(keys) 271 sub_resources = {k: v for k, v in resources.items() 272 if k in desired and sync_filt(v)} 273 274 # Make sure we accounted for all of them... 275 if len(keys) != len(sub_resources): 276 unknown = desired - set(sub_resources.keys()) 277 raise exception.QuotaResourceUnknown(unknown=sorted(unknown)) 278 279 # Grab and return the quotas (without usages) 280 quotas = self.get_project_quotas(context, sub_resources, 281 project_id, 282 context.quota_class, usages=False) 283 284 return {k: v['limit'] for k, v in quotas.items()} 285 286 def limit_check(self, context, resources, values, project_id=None): 287 """Check simple quota limits. 288 289 For limits--those quotas for which there is no usage 290 synchronization function--this method checks that a set of 291 proposed values are permitted by the limit restriction. 292 293 This method will raise a QuotaResourceUnknown exception if a 294 given resource is unknown or if it is not a simple limit 295 resource. 296 297 If any of the proposed values is over the defined quota, an 298 OverQuota exception will be raised with the sorted list of the 299 resources which are too high. Otherwise, the method returns 300 nothing. 301 302 :param context: The request context, for access checks. 303 :param resources: A dictionary of the registered resources. 304 :param values: A dictionary of the values to check against the 305 quota. 306 :param project_id: Specify the project_id if current context 307 is admin and admin wants to impact on 308 common user's tenant. 309 """ 310 311 # Ensure no value is less than zero 312 unders = [key for key, val in values.items() if val < 0] 313 if unders: 314 raise exception.InvalidQuotaValue(unders=sorted(unders)) 315 316 # If project_id is None, then we use the project_id in context 317 if project_id is None: 318 project_id = context.project_id 319 320 # Get the applicable quotas 321 quotas = self._get_quotas(context, resources, values.keys(), 322 has_sync=False, project_id=project_id) 323 # Check the quotas and construct a list of the resources that 324 # would be put over limit by the desired values 325 overs = [key for key, val in values.items() 326 if quotas[key] >= 0 and quotas[key] < val] 327 if overs: 328 raise exception.OverQuota(overs=sorted(overs), quotas=quotas, 329 usages={}) 330 331 def reserve(self, context, resources, deltas, expire=None, 332 project_id=None): 333 """Check quotas and reserve resources. 334 335 For counting quotas--those quotas for which there is a usage 336 synchronization function--this method checks quotas against 337 current usage and the desired deltas. 338 339 This method will raise a QuotaResourceUnknown exception if a 340 given resource is unknown or if it does not have a usage 341 synchronization function. 342 343 If any of the proposed values is over the defined quota, an 344 OverQuota exception will be raised with the sorted list of the 345 resources which are too high. Otherwise, the method returns a 346 list of reservation UUIDs which were created. 347 348 :param context: The request context, for access checks. 349 :param resources: A dictionary of the registered resources. 350 :param deltas: A dictionary of the proposed delta changes. 351 :param expire: An optional parameter specifying an expiration 352 time for the reservations. If it is a simple 353 number, it is interpreted as a number of 354 seconds and added to the current time; if it is 355 a datetime.timedelta object, it will also be 356 added to the current time. A datetime.datetime 357 object will be interpreted as the absolute 358 expiration time. If None is specified, the 359 default expiration time set by 360 --default-reservation-expire will be used (this 361 value will be treated as a number of seconds). 362 :param project_id: Specify the project_id if current context 363 is admin and admin wants to impact on 364 common user's tenant. 365 """ 366 367 # Set up the reservation expiration 368 if expire is None: 369 expire = CONF.reservation_expire 370 if isinstance(expire, six.integer_types): 371 expire = datetime.timedelta(seconds=expire) 372 if isinstance(expire, datetime.timedelta): 373 expire = timeutils.utcnow() + expire 374 if not isinstance(expire, datetime.datetime): 375 raise exception.InvalidReservationExpiration(expire=expire) 376 377 # If project_id is None, then we use the project_id in context 378 if project_id is None: 379 project_id = context.project_id 380 381 # Get the applicable quotas. 382 # NOTE(Vek): We're not worried about races at this point. 383 # Yes, the admin may be in the process of reducing 384 # quotas, but that's a pretty rare thing. 385 quotas = self._get_quotas(context, resources, deltas.keys(), 386 has_sync=True, project_id=project_id) 387 return self._reserve(context, resources, quotas, deltas, expire, 388 project_id) 389 390 def _reserve(self, context, resources, quotas, deltas, expire, project_id): 391 # NOTE(Vek): Most of the work here has to be done in the DB 392 # API, because we have to do it in a transaction, 393 # which means access to the session. Since the 394 # session isn't available outside the DBAPI, we 395 # have to do the work there. 396 return db.quota_reserve(context, resources, quotas, deltas, expire, 397 CONF.until_refresh, CONF.max_age, 398 project_id=project_id) 399 400 def commit(self, context, reservations, project_id=None): 401 """Commit reservations. 402 403 :param context: The request context, for access checks. 404 :param reservations: A list of the reservation UUIDs, as 405 returned by the reserve() method. 406 :param project_id: Specify the project_id if current context 407 is admin and admin wants to impact on 408 common user's tenant. 409 """ 410 # If project_id is None, then we use the project_id in context 411 if project_id is None: 412 project_id = context.project_id 413 414 db.reservation_commit(context, reservations, project_id=project_id) 415 416 def rollback(self, context, reservations, project_id=None): 417 """Roll back reservations. 418 419 :param context: The request context, for access checks. 420 :param reservations: A list of the reservation UUIDs, as 421 returned by the reserve() method. 422 :param project_id: Specify the project_id if current context 423 is admin and admin wants to impact on 424 common user's tenant. 425 """ 426 # If project_id is None, then we use the project_id in context 427 if project_id is None: 428 project_id = context.project_id 429 430 db.reservation_rollback(context, reservations, project_id=project_id) 431 432 def destroy_by_project(self, context, project_id): 433 """Destroy all limit quotas associated with a project. 434 435 Leave usage and reservation quotas intact. 436 437 :param context: The request context, for access checks. 438 :param project_id: The ID of the project being deleted. 439 """ 440 db.quota_destroy_by_project(context, project_id) 441 442 def expire(self, context): 443 """Expire reservations. 444 445 Explores all currently existing reservations and rolls back 446 any that have expired. 447 448 :param context: The request context, for access checks. 449 """ 450 451 db.reservation_expire(context) 452 453 454class NestedDbQuotaDriver(DbQuotaDriver): 455 def validate_nested_setup(self, ctxt, resources, project_tree, 456 fix_allocated_quotas=False): 457 """Ensures project_tree has quotas that make sense as nested quotas. 458 459 Validates the following: 460 * No parent project has child_projects who have more combined quota 461 than the parent's quota limit 462 * No child quota has a larger in-use value than it's current limit 463 (could happen before because child default values weren't enforced) 464 * All parent projects' "allocated" quotas match the sum of the limits 465 of its children projects 466 467 TODO(mc_nair): need a better way to "flip the switch" to use nested 468 quotas to make this less race-ee 469 """ 470 self._allocated = {} 471 project_queue = deque(project_tree.items()) 472 borked_allocated_quotas = {} 473 474 while project_queue: 475 # Tuple of (current root node, subtree) 476 cur_proj_id, project_subtree = project_queue.popleft() 477 478 # If we're on a leaf node, no need to do validation on it, and in 479 # order to avoid complication trying to get its children, skip it. 480 if not project_subtree: 481 continue 482 483 cur_project_quotas = self.get_project_quotas( 484 ctxt, resources, cur_proj_id) 485 486 # Validate each resource when compared to it's child quotas 487 for resource in cur_project_quotas.keys(): 488 parent_quota = cur_project_quotas[resource] 489 parent_limit = parent_quota['limit'] 490 parent_usage = (parent_quota['in_use'] + 491 parent_quota['reserved']) 492 493 cur_parent_allocated = parent_quota.get('allocated', 0) 494 calc_parent_allocated = self._get_cur_project_allocated( 495 ctxt, resources[resource], {cur_proj_id: project_subtree}) 496 497 if parent_limit > 0: 498 parent_free_quota = parent_limit - parent_usage 499 if parent_free_quota < calc_parent_allocated: 500 msg = _("Sum of child usage '%(sum)s' is greater " 501 "than free quota of '%(free)s' for project " 502 "'%(proj)s' for resource '%(res)s'. Please " 503 "lower the limit or usage for one or more of " 504 "the following projects: '%(child_ids)s'") % { 505 'sum': calc_parent_allocated, 506 'free': parent_free_quota, 507 'proj': cur_proj_id, 'res': resource, 508 'child_ids': ', '.join(project_subtree.keys()) 509 } 510 raise exception.InvalidNestedQuotaSetup(reason=msg) 511 512 # If "allocated" value wasn't right either err or fix DB 513 if calc_parent_allocated != cur_parent_allocated: 514 if fix_allocated_quotas: 515 try: 516 db.quota_allocated_update(ctxt, cur_proj_id, 517 resource, 518 calc_parent_allocated) 519 except exception.ProjectQuotaNotFound: 520 # If it was default quota create DB entry for it 521 db.quota_create( 522 ctxt, cur_proj_id, resource, 523 parent_limit, allocated=calc_parent_allocated) 524 else: 525 if cur_proj_id not in borked_allocated_quotas: 526 borked_allocated_quotas[cur_proj_id] = {} 527 528 borked_allocated_quotas[cur_proj_id][resource] = { 529 'db_allocated_quota': cur_parent_allocated, 530 'expected_allocated_quota': calc_parent_allocated} 531 532 project_queue.extend(project_subtree.items()) 533 534 if borked_allocated_quotas: 535 msg = _("Invalid allocated quotas defined for the following " 536 "project quotas: %s") % borked_allocated_quotas 537 raise exception.InvalidNestedQuotaSetup(message=msg) 538 539 def _get_cur_project_allocated(self, ctxt, resource, project_tree): 540 """Recursively calculates the allocated value of a project 541 542 :param ctxt: context used to retrieve DB values 543 :param resource: the resource to calculate allocated value for 544 :param project_tree: the project tree used to calculate allocated 545 e.g. {'A': {'B': {'D': None}, 'C': None}} 546 547 A project's "allocated" value depends on: 548 1) the quota limits which have been "given" to it's children, in 549 the case those limits are not unlimited (-1) 550 2) the current quota being used by a child plus whatever the child 551 has given to it's children, in the case of unlimited (-1) limits 552 553 Scenario #2 requires recursively calculating allocated, and in order 554 to efficiently calculate things we will save off any previously 555 calculated allocated values. 556 557 NOTE: this currently leaves a race condition when a project's allocated 558 value has been calculated (with a -1 limit), but then a child project 559 gets a volume created, thus changing the in-use value and messing up 560 the child's allocated value. We should look into updating the allocated 561 values as we're going along and switching to NestedQuotaDriver with 562 flip of a switch. 563 """ 564 # Grab the current node 565 cur_project_id = list(project_tree)[0] 566 project_subtree = project_tree[cur_project_id] 567 res_name = resource.name 568 569 if cur_project_id not in self._allocated: 570 self._allocated[cur_project_id] = {} 571 572 if res_name not in self._allocated[cur_project_id]: 573 # Calculate the allocated value for this resource since haven't yet 574 cur_project_allocated = 0 575 child_proj_ids = project_subtree.keys() if project_subtree else {} 576 res_dict = {res_name: resource} 577 child_project_quotas = {child_id: self.get_project_quotas( 578 ctxt, res_dict, child_id) for child_id in child_proj_ids} 579 580 for child_id, child_quota in child_project_quotas.items(): 581 child_limit = child_quota[res_name]['limit'] 582 # Non-unlimited quota is easy, anything explicitly given to a 583 # child project gets added into allocated value 584 if child_limit != -1: 585 if child_quota[res_name].get('in_use', 0) > child_limit: 586 msg = _("Quota limit invalid for project '%(proj)s' " 587 "for resource '%(res)s': limit of %(limit)d " 588 "is less than in-use value of %(used)d") % { 589 'proj': child_id, 'res': res_name, 590 'limit': child_limit, 591 'used': child_quota[res_name]['in_use'] 592 } 593 raise exception.InvalidNestedQuotaSetup(reason=msg) 594 595 cur_project_allocated += child_limit 596 # For -1, take any quota being eaten up by child, as well as 597 # what the child itself has given up to its children 598 else: 599 child_in_use = child_quota[res_name].get('in_use', 0) 600 # Recursively calculate child's allocated 601 child_alloc = self._get_cur_project_allocated( 602 ctxt, resource, {child_id: project_subtree[child_id]}) 603 cur_project_allocated += child_in_use + child_alloc 604 605 self._allocated[cur_project_id][res_name] = cur_project_allocated 606 607 return self._allocated[cur_project_id][res_name] 608 609 def get_default(self, context, resource, project_id): 610 """Get a specific default quota for a resource.""" 611 resource = super(NestedDbQuotaDriver, self).get_default( 612 context, resource, project_id) 613 614 return 0 if quota_utils.get_parent_project_id( 615 context, project_id) else resource.default 616 617 def get_defaults(self, context, resources, project_id=None): 618 defaults = super(NestedDbQuotaDriver, self).get_defaults( 619 context, resources, project_id) 620 # All defaults are 0 for child project 621 if quota_utils.get_parent_project_id(context, project_id): 622 for key in defaults.keys(): 623 defaults[key] = 0 624 return defaults 625 626 def _reserve(self, context, resources, quotas, deltas, expire, project_id): 627 reserved = [] 628 # As to not change the exception behavior, flag every res that would 629 # be over instead of failing on first OverQuota 630 resources_failed_to_update = [] 631 failed_usages = {} 632 for res in deltas.keys(): 633 try: 634 reserved += db.quota_reserve( 635 context, resources, quotas, {res: deltas[res]}, 636 expire, CONF.until_refresh, CONF.max_age, project_id) 637 if quotas[res] == -1: 638 reserved += quota_utils.update_alloc_to_next_hard_limit( 639 context, resources, deltas, res, expire, project_id) 640 except exception.OverQuota as e: 641 resources_failed_to_update.append(res) 642 failed_usages.update(e.kwargs['usages']) 643 644 if resources_failed_to_update: 645 db.reservation_rollback(context, reserved, project_id) 646 # We change OverQuota to OverVolumeLimit in other places and expect 647 # to find all of the OverQuota kwargs 648 raise exception.OverQuota(overs=sorted(resources_failed_to_update), 649 quotas=quotas, usages=failed_usages) 650 651 return reserved 652 653 654class BaseResource(object): 655 """Describe a single resource for quota checking.""" 656 657 def __init__(self, name, flag=None, parent_project_id=None): 658 """Initializes a Resource. 659 660 :param name: The name of the resource, i.e., "volumes". 661 :param flag: The name of the flag or configuration option 662 which specifies the default value of the quota 663 for this resource. 664 :param parent_project_id: The id of the current project's parent, 665 if any. 666 """ 667 668 self.name = name 669 self.flag = flag 670 self.parent_project_id = parent_project_id 671 672 def quota(self, driver, context, **kwargs): 673 """Given a driver and context, obtain the quota for this resource. 674 675 :param driver: A quota driver. 676 :param context: The request context. 677 :param project_id: The project to obtain the quota value for. 678 If not provided, it is taken from the 679 context. If it is given as None, no 680 project-specific quota will be searched 681 for. 682 :param quota_class: The quota class corresponding to the 683 project, or for which the quota is to be 684 looked up. If not provided, it is taken 685 from the context. If it is given as None, 686 no quota class-specific quota will be 687 searched for. Note that the quota class 688 defaults to the value in the context, 689 which may not correspond to the project if 690 project_id is not the same as the one in 691 the context. 692 """ 693 694 # Get the project ID 695 project_id = kwargs.get('project_id', context.project_id) 696 697 # Ditto for the quota class 698 quota_class = kwargs.get('quota_class', context.quota_class) 699 700 # Look up the quota for the project 701 if project_id: 702 try: 703 return driver.get_by_project(context, project_id, self.name) 704 except exception.ProjectQuotaNotFound: 705 pass 706 707 # Try for the quota class 708 if quota_class: 709 try: 710 return driver.get_by_class(context, quota_class, self.name) 711 except exception.QuotaClassNotFound: 712 pass 713 714 # OK, return the default 715 return driver.get_default(context, self, 716 parent_project_id=self.parent_project_id) 717 718 @property 719 def default(self): 720 """Return the default value of the quota.""" 721 722 if self.parent_project_id: 723 return 0 724 725 return CONF[self.flag] if self.flag else -1 726 727 728class ReservableResource(BaseResource): 729 """Describe a reservable resource.""" 730 731 def __init__(self, name, sync, flag=None): 732 """Initializes a ReservableResource. 733 734 Reservable resources are those resources which directly 735 correspond to objects in the database, i.e., volumes, gigabytes, 736 etc. A ReservableResource must be constructed with a usage 737 synchronization function, which will be called to determine the 738 current counts of one or more resources. 739 740 The usage synchronization function will be passed three 741 arguments: an admin context, the project ID, and an opaque 742 session object, which should in turn be passed to the 743 underlying database function. Synchronization functions 744 should return a dictionary mapping resource names to the 745 current in_use count for those resources; more than one 746 resource and resource count may be returned. Note that 747 synchronization functions may be associated with more than one 748 ReservableResource. 749 750 :param name: The name of the resource, i.e., "volumes". 751 :param sync: A dbapi methods name which returns a dictionary 752 to resynchronize the in_use count for one or more 753 resources, as described above. 754 :param flag: The name of the flag or configuration option 755 which specifies the default value of the quota 756 for this resource. 757 """ 758 759 super(ReservableResource, self).__init__(name, flag=flag) 760 if sync: 761 self.sync = sync 762 763 764class AbsoluteResource(BaseResource): 765 """Describe a non-reservable resource.""" 766 767 pass 768 769 770class CountableResource(AbsoluteResource): 771 """Describe a resource where counts aren't based only on the project ID.""" 772 773 def __init__(self, name, count, flag=None): 774 """Initializes a CountableResource. 775 776 Countable resources are those resources which directly 777 correspond to objects in the database, i.e., volumes, gigabytes, 778 etc., but for which a count by project ID is inappropriate. A 779 CountableResource must be constructed with a counting 780 function, which will be called to determine the current counts 781 of the resource. 782 783 The counting function will be passed the context, along with 784 the extra positional and keyword arguments that are passed to 785 Quota.count(). It should return an integer specifying the 786 count. 787 788 Note that this counting is not performed in a transaction-safe 789 manner. This resource class is a temporary measure to provide 790 required functionality, until a better approach to solving 791 this problem can be evolved. 792 793 :param name: The name of the resource, i.e., "volumes". 794 :param count: A callable which returns the count of the 795 resource. The arguments passed are as described 796 above. 797 :param flag: The name of the flag or configuration option 798 which specifies the default value of the quota 799 for this resource. 800 """ 801 802 super(CountableResource, self).__init__(name, flag=flag) 803 self.count = count 804 805 806class VolumeTypeResource(ReservableResource): 807 """ReservableResource for a specific volume type.""" 808 809 def __init__(self, part_name, volume_type): 810 """Initializes a VolumeTypeResource. 811 812 :param part_name: The kind of resource, i.e., "volumes". 813 :param volume_type: The volume type for this resource. 814 """ 815 816 self.volume_type_name = volume_type['name'] 817 self.volume_type_id = volume_type['id'] 818 name = "%s_%s" % (part_name, self.volume_type_name) 819 super(VolumeTypeResource, self).__init__(name, "_sync_%s" % part_name) 820 821 822class QuotaEngine(object): 823 """Represent the set of recognized quotas.""" 824 825 def __init__(self, quota_driver_class=None): 826 """Initialize a Quota object.""" 827 828 self._resources = {} 829 self._quota_driver_class = quota_driver_class 830 self._driver_class = None 831 832 @property 833 def _driver(self): 834 # Lazy load the driver so we give a chance for the config file to 835 # be read before grabbing the config for which QuotaDriver to use 836 if self._driver_class: 837 return self._driver_class 838 839 if not self._quota_driver_class: 840 # Grab the current driver class from CONF 841 self._quota_driver_class = CONF.quota_driver 842 843 if isinstance(self._quota_driver_class, six.string_types): 844 self._quota_driver_class = importutils.import_object( 845 self._quota_driver_class) 846 847 self._driver_class = self._quota_driver_class 848 return self._driver_class 849 850 def using_nested_quotas(self): 851 """Returns true if nested quotas are being used""" 852 return isinstance(self._driver, NestedDbQuotaDriver) 853 854 def __contains__(self, resource): 855 return resource in self.resources 856 857 def register_resource(self, resource): 858 """Register a resource.""" 859 860 self._resources[resource.name] = resource 861 862 def register_resources(self, resources): 863 """Register a list of resources.""" 864 865 for resource in resources: 866 self.register_resource(resource) 867 868 def get_by_project(self, context, project_id, resource_name): 869 """Get a specific quota by project.""" 870 return self._driver.get_by_project(context, project_id, resource_name) 871 872 def get_by_project_or_default(self, context, project_id, resource_name): 873 """Get specific quota by project or default quota if doesn't exists.""" 874 try: 875 val = self.get_by_project( 876 context, project_id, resource_name).hard_limit 877 except exception.ProjectQuotaNotFound: 878 val = self.get_defaults(context, project_id)[resource_name] 879 880 return val 881 882 def get_by_class(self, context, quota_class, resource_name): 883 """Get a specific quota by quota class.""" 884 885 return self._driver.get_by_class(context, quota_class, resource_name) 886 887 def get_default(self, context, resource, parent_project_id=None): 888 """Get a specific default quota for a resource. 889 890 :param parent_project_id: The id of the current project's parent, 891 if any. 892 """ 893 894 return self._driver.get_default(context, resource, 895 parent_project_id=parent_project_id) 896 897 def get_defaults(self, context, project_id=None): 898 """Retrieve the default quotas. 899 900 :param context: The request context, for access checks. 901 :param project_id: The id of the current project 902 """ 903 904 return self._driver.get_defaults(context, self.resources, 905 project_id) 906 907 def get_class_quotas(self, context, quota_class, defaults=True): 908 """Retrieve the quotas for the given quota class. 909 910 :param context: The request context, for access checks. 911 :param quota_class: The name of the quota class to return 912 quotas for. 913 :param defaults: If True, the default value will be reported 914 if there is no specific value for the 915 resource. 916 """ 917 918 return self._driver.get_class_quotas(context, self.resources, 919 quota_class, defaults=defaults) 920 921 def get_project_quotas(self, context, project_id, quota_class=None, 922 defaults=True, usages=True): 923 """Retrieve the quotas for the given project. 924 925 :param context: The request context, for access checks. 926 :param project_id: The ID of the project to return quotas for. 927 :param quota_class: If project_id != context.project_id, the 928 quota class cannot be determined. This 929 parameter allows it to be specified. 930 :param defaults: If True, the quota class value (or the 931 default value, if there is no value from the 932 quota class) will be reported if there is no 933 specific value for the resource. 934 :param usages: If True, the current in_use, reserved and 935 allocated counts will also be returned. 936 """ 937 return self._driver.get_project_quotas(context, self.resources, 938 project_id, 939 quota_class=quota_class, 940 defaults=defaults, 941 usages=usages) 942 943 def count(self, context, resource, *args, **kwargs): 944 """Count a resource. 945 946 For countable resources, invokes the count() function and 947 returns its result. Arguments following the context and 948 resource are passed directly to the count function declared by 949 the resource. 950 951 :param context: The request context, for access checks. 952 :param resource: The name of the resource, as a string. 953 """ 954 955 # Get the resource 956 res = self.resources.get(resource) 957 if not res or not hasattr(res, 'count'): 958 raise exception.QuotaResourceUnknown(unknown=[resource]) 959 960 return res.count(context, *args, **kwargs) 961 962 def limit_check(self, context, project_id=None, **values): 963 """Check simple quota limits. 964 965 For limits--those quotas for which there is no usage 966 synchronization function--this method checks that a set of 967 proposed values are permitted by the limit restriction. The 968 values to check are given as keyword arguments, where the key 969 identifies the specific quota limit to check, and the value is 970 the proposed value. 971 972 This method will raise a QuotaResourceUnknown exception if a 973 given resource is unknown or if it is not a simple limit 974 resource. 975 976 If any of the proposed values is over the defined quota, an 977 OverQuota exception will be raised with the sorted list of the 978 resources which are too high. Otherwise, the method returns 979 nothing. 980 981 :param context: The request context, for access checks. 982 :param project_id: Specify the project_id if current context 983 is admin and admin wants to impact on 984 common user's tenant. 985 """ 986 987 return self._driver.limit_check(context, self.resources, values, 988 project_id=project_id) 989 990 def reserve(self, context, expire=None, project_id=None, **deltas): 991 """Check quotas and reserve resources. 992 993 For counting quotas--those quotas for which there is a usage 994 synchronization function--this method checks quotas against 995 current usage and the desired deltas. The deltas are given as 996 keyword arguments, and current usage and other reservations 997 are factored into the quota check. 998 999 This method will raise a QuotaResourceUnknown exception if a 1000 given resource is unknown or if it does not have a usage 1001 synchronization function. 1002 1003 If any of the proposed values is over the defined quota, an 1004 OverQuota exception will be raised with the sorted list of the 1005 resources which are too high. Otherwise, the method returns a 1006 list of reservation UUIDs which were created. 1007 1008 :param context: The request context, for access checks. 1009 :param expire: An optional parameter specifying an expiration 1010 time for the reservations. If it is a simple 1011 number, it is interpreted as a number of 1012 seconds and added to the current time; if it is 1013 a datetime.timedelta object, it will also be 1014 added to the current time. A datetime.datetime 1015 object will be interpreted as the absolute 1016 expiration time. If None is specified, the 1017 default expiration time set by 1018 --default-reservation-expire will be used (this 1019 value will be treated as a number of seconds). 1020 :param project_id: Specify the project_id if current context 1021 is admin and admin wants to impact on 1022 common user's tenant. 1023 """ 1024 1025 reservations = self._driver.reserve(context, self.resources, deltas, 1026 expire=expire, 1027 project_id=project_id) 1028 1029 LOG.debug("Created reservations %s", reservations) 1030 1031 return reservations 1032 1033 def commit(self, context, reservations, project_id=None): 1034 """Commit reservations. 1035 1036 :param context: The request context, for access checks. 1037 :param reservations: A list of the reservation UUIDs, as 1038 returned by the reserve() method. 1039 :param project_id: Specify the project_id if current context 1040 is admin and admin wants to impact on 1041 common user's tenant. 1042 """ 1043 1044 try: 1045 self._driver.commit(context, reservations, project_id=project_id) 1046 except Exception: 1047 # NOTE(Vek): Ignoring exceptions here is safe, because the 1048 # usage resynchronization and the reservation expiration 1049 # mechanisms will resolve the issue. The exception is 1050 # logged, however, because this is less than optimal. 1051 LOG.exception("Failed to commit reservations %s", reservations) 1052 1053 def rollback(self, context, reservations, project_id=None): 1054 """Roll back reservations. 1055 1056 :param context: The request context, for access checks. 1057 :param reservations: A list of the reservation UUIDs, as 1058 returned by the reserve() method. 1059 :param project_id: Specify the project_id if current context 1060 is admin and admin wants to impact on 1061 common user's tenant. 1062 """ 1063 1064 try: 1065 self._driver.rollback(context, reservations, project_id=project_id) 1066 except Exception: 1067 # NOTE(Vek): Ignoring exceptions here is safe, because the 1068 # usage resynchronization and the reservation expiration 1069 # mechanisms will resolve the issue. The exception is 1070 # logged, however, because this is less than optimal. 1071 LOG.exception("Failed to roll back reservations %s", reservations) 1072 1073 def destroy_by_project(self, context, project_id): 1074 """Destroy all quota limits associated with a project. 1075 1076 :param context: The request context, for access checks. 1077 :param project_id: The ID of the project being deleted. 1078 """ 1079 1080 self._driver.destroy_by_project(context, project_id) 1081 1082 def expire(self, context): 1083 """Expire reservations. 1084 1085 Explores all currently existing reservations and rolls back 1086 any that have expired. 1087 1088 :param context: The request context, for access checks. 1089 """ 1090 1091 self._driver.expire(context) 1092 1093 def add_volume_type_opts(self, context, opts, volume_type_id): 1094 """Add volume type resource options. 1095 1096 Adds elements to the opts hash for volume type quotas. 1097 If a resource is being reserved ('gigabytes', etc) and the volume 1098 type is set up for its own quotas, these reservations are copied 1099 into keys for 'gigabytes_<volume type name>', etc. 1100 1101 :param context: The request context, for access checks. 1102 :param opts: The reservations options hash. 1103 :param volume_type_id: The volume type id for this reservation. 1104 """ 1105 if not volume_type_id: 1106 return 1107 1108 # NOTE(jdg): set inactive to True in volume_type_get, as we 1109 # may be operating on a volume that was created with a type 1110 # that has since been deleted. 1111 volume_type = db.volume_type_get(context, volume_type_id, True) 1112 1113 for quota in ('volumes', 'gigabytes', 'snapshots'): 1114 if quota in opts: 1115 vtype_quota = "%s_%s" % (quota, volume_type['name']) 1116 opts[vtype_quota] = opts[quota] 1117 1118 @property 1119 def resource_names(self): 1120 return sorted(self.resources.keys()) 1121 1122 @property 1123 def resources(self): 1124 return self._resources 1125 1126 1127class VolumeTypeQuotaEngine(QuotaEngine): 1128 """Represent the set of all quotas.""" 1129 1130 @property 1131 def resources(self): 1132 """Fetches all possible quota resources.""" 1133 1134 result = {} 1135 # Global quotas. 1136 argses = [('volumes', '_sync_volumes', 'quota_volumes'), 1137 ('per_volume_gigabytes', None, 'per_volume_size_limit'), 1138 ('snapshots', '_sync_snapshots', 'quota_snapshots'), 1139 ('gigabytes', '_sync_gigabytes', 'quota_gigabytes'), 1140 ('backups', '_sync_backups', 'quota_backups'), 1141 ('backup_gigabytes', '_sync_backup_gigabytes', 1142 'quota_backup_gigabytes')] 1143 for args in argses: 1144 resource = ReservableResource(*args) 1145 result[resource.name] = resource 1146 1147 # Volume type quotas. 1148 volume_types = db.volume_type_get_all(context.get_admin_context(), 1149 False) 1150 for volume_type in volume_types.values(): 1151 for part_name in ('volumes', 'gigabytes', 'snapshots'): 1152 resource = VolumeTypeResource(part_name, volume_type) 1153 result[resource.name] = resource 1154 return result 1155 1156 def register_resource(self, resource): 1157 raise NotImplementedError(_("Cannot register resource")) 1158 1159 def register_resources(self, resources): 1160 raise NotImplementedError(_("Cannot register resources")) 1161 1162 def update_quota_resource(self, context, old_type_name, new_type_name): 1163 """Update resource in quota. 1164 1165 This is to update resource in quotas, quota_classes, and 1166 quota_usages once the name of a volume type is changed. 1167 1168 :param context: The request context, for access checks. 1169 :param old_type_name: old name of volume type. 1170 :param new_type_name: new name of volume type. 1171 """ 1172 1173 for quota in ('volumes', 'gigabytes', 'snapshots'): 1174 old_res = "%s_%s" % (quota, old_type_name) 1175 new_res = "%s_%s" % (quota, new_type_name) 1176 db.quota_usage_update_resource(context, 1177 old_res, 1178 new_res) 1179 db.quota_class_update_resource(context, 1180 old_res, 1181 new_res) 1182 db.quota_update_resource(context, 1183 old_res, 1184 new_res) 1185 1186 1187class CGQuotaEngine(QuotaEngine): 1188 """Represent the consistencygroup quotas.""" 1189 1190 @property 1191 def resources(self): 1192 """Fetches all possible quota resources.""" 1193 1194 result = {} 1195 # Global quotas. 1196 argses = [('consistencygroups', '_sync_consistencygroups', 1197 'quota_consistencygroups'), ] 1198 for args in argses: 1199 resource = ReservableResource(*args) 1200 result[resource.name] = resource 1201 1202 return result 1203 1204 def register_resource(self, resource): 1205 raise NotImplementedError(_("Cannot register resource")) 1206 1207 def register_resources(self, resources): 1208 raise NotImplementedError(_("Cannot register resources")) 1209 1210 1211class GroupQuotaEngine(QuotaEngine): 1212 """Represent the group quotas.""" 1213 1214 @property 1215 def resources(self): 1216 """Fetches all possible quota resources.""" 1217 1218 result = {} 1219 # Global quotas. 1220 argses = [('groups', '_sync_groups', 1221 'quota_groups'), ] 1222 for args in argses: 1223 resource = ReservableResource(*args) 1224 result[resource.name] = resource 1225 1226 return result 1227 1228 def register_resource(self, resource): 1229 raise NotImplementedError(_("Cannot register resource")) 1230 1231 def register_resources(self, resources): 1232 raise NotImplementedError(_("Cannot register resources")) 1233 1234QUOTAS = VolumeTypeQuotaEngine() 1235CGQUOTAS = CGQuotaEngine() 1236GROUP_QUOTAS = GroupQuotaEngine() 1237