Split out NestedQuotas into a separate driver

Fixes the following issues with NestedQuotas:
  * Requires conf setting change to use nested quota driver
  * Enforces default child quota value with volume creation
  * Disables the use of -1 to be set for child quotas
  * Adds an admin only API command which can be used to validate
    the current setup for nested quotas, and can update existing
    allocated quotas in the DB which have been incorrectly set
    by previous use of child limits with -1

There will be follow-up patches with the following improvements:
  * make -1 limits functional for child projects
  * cache the Keystone project heirarchies to improve efficiency

Note: ideally validation of nested quotas would occur in the setup
of the nested quota driver, but doing the validation requires a
view of ALL projects present in Keystone, so unless we require Keystone
change to allow "cinder" service user to be able to list/get projects,
we need the admin-only API for validation that should be called by
cloud-admin.

DocImpact

Change-Id: Ibbd6f47c370d8f10c08cba358574b55e3059dcd1
Closes-Bug: #1531502
Partial-Bug: #1537189
Related-Bug: #1535878
This commit is contained in:
Ryan McNair 2016-01-30 16:24:32 +00:00
parent c9457d1452
commit 7ebd4904b9
9 changed files with 1232 additions and 557 deletions

View File

@ -15,11 +15,6 @@
import webob import webob
from keystoneclient.auth.identity.generic import token
from keystoneclient import client
from keystoneclient import exceptions
from keystoneclient import session
from cinder.api import extensions from cinder.api import extensions
from cinder.api.openstack import wsgi from cinder.api.openstack import wsgi
from cinder.api import xmlutil from cinder.api import xmlutil
@ -28,6 +23,7 @@ from cinder.db.sqlalchemy import api as sqlalchemy_api
from cinder import exception from cinder import exception
from cinder.i18n import _ from cinder.i18n import _
from cinder import quota from cinder import quota
from cinder import quota_utils
from cinder import utils from cinder import utils
from oslo_config import cfg from oslo_config import cfg
@ -57,17 +53,6 @@ class QuotaTemplate(xmlutil.TemplateBuilder):
class QuotaSetsController(wsgi.Controller): class QuotaSetsController(wsgi.Controller):
class GenericProjectInfo(object):
"""Abstraction layer for Keystone V2 and V3 project objects"""
def __init__(self, project_id, project_keystone_api_version,
project_parent_id=None, project_subtree=None):
self.id = project_id
self.keystone_api_version = project_keystone_api_version
self.parent_id = project_parent_id
self.subtree = project_subtree
def _format_quota_set(self, project_id, quota_set): def _format_quota_set(self, project_id, quota_set):
"""Convert the quota object to a result dict.""" """Convert the quota object to a result dict."""
@ -75,20 +60,6 @@ class QuotaSetsController(wsgi.Controller):
return dict(quota_set=quota_set) return dict(quota_set=quota_set)
def _keystone_client(self, context):
"""Creates and returns an instance of a generic keystone client.
:param context: The request context
:return: keystoneclient.client.Client object
"""
auth_plugin = token.Token(
auth_url=CONF.keystone_authtoken.auth_uri,
token=context.auth_token,
project_id=context.project_id)
client_session = session.Session(auth=auth_plugin)
return client.Client(auth_url=CONF.keystone_authtoken.auth_uri,
session=client_session)
def _validate_existing_resource(self, key, value, quota_values): def _validate_existing_resource(self, key, value, quota_values):
if key == 'per_volume_gigabytes': if key == 'per_volume_gigabytes':
return return
@ -103,7 +74,11 @@ class QuotaSetsController(wsgi.Controller):
limit = self.validate_integer(quota[key], key, min_value=-1, limit = self.validate_integer(quota[key], key, min_value=-1,
max_value=db.MAX_INT) max_value=db.MAX_INT)
if parent_project_quotas: # If a parent quota is unlimited (-1) no validation needs to happen
# for the amount of existing free quota
# TODO(mc_nair): will need to recurse up for nested quotas once
# -1 child project values are enabled
if parent_project_quotas and parent_project_quotas[key]['limit'] != -1:
free_quota = (parent_project_quotas[key]['limit'] - free_quota = (parent_project_quotas[key]['limit'] -
parent_project_quotas[key]['in_use'] - parent_project_quotas[key]['in_use'] -
parent_project_quotas[key]['reserved'] - parent_project_quotas[key]['reserved'] -
@ -112,15 +87,22 @@ class QuotaSetsController(wsgi.Controller):
current = 0 current = 0
if project_quotas.get(key): if project_quotas.get(key):
current = project_quotas[key]['limit'] current = project_quotas[key]['limit']
# -1 limit doesn't change free quota available in parent
if current == -1:
current = 0
if limit - current > free_quota: # Add back the existing quota limit (if any is set) from the
# current free quota since it will be getting reset and is part
# of the parent's allocated value
free_quota += current
if limit > free_quota:
msg = _("Free quota available is %s.") % free_quota msg = _("Free quota available is %s.") % free_quota
raise webob.exc.HTTPBadRequest(explanation=msg) raise webob.exc.HTTPBadRequest(explanation=msg)
return limit return limit
def _get_quotas(self, context, id, usages=False, parent_project_id=None): def _get_quotas(self, context, id, usages=False):
values = QUOTAS.get_project_quotas(context, id, usages=usages, values = QUOTAS.get_project_quotas(context, id, usages=usages)
parent_project_id=parent_project_id)
if usages: if usages:
return values return values
@ -199,27 +181,6 @@ class QuotaSetsController(wsgi.Controller):
return True return True
return False return False
def _get_project(self, context, id, subtree_as_ids=False):
"""A Helper method to get the project hierarchy.
Along with Hierachical Multitenancy in keystone API v3, projects can be
hierarchically organized. Therefore, we need to know the project
hierarchy, if any, in order to do quota operations properly.
"""
try:
keystone = self._keystone_client(context)
generic_project = self.GenericProjectInfo(id, keystone.version)
if keystone.version == 'v3':
project = keystone.projects.get(id,
subtree_as_ids=subtree_as_ids)
generic_project.parent_id = project.parent_id
generic_project.subtree = (
project.subtree if subtree_as_ids else None)
except exceptions.NotFound:
msg = (_("Tenant ID: %s does not exist.") % id)
raise webob.exc.HTTPNotFound(explanation=msg)
return generic_project
@wsgi.serializers(xml=QuotaTemplate) @wsgi.serializers(xml=QuotaTemplate)
def show(self, req, id): def show(self, req, id):
"""Show quota for a particular tenant """Show quota for a particular tenant
@ -242,21 +203,16 @@ class QuotaSetsController(wsgi.Controller):
else: else:
usage = False usage = False
try: if QUOTAS.using_nested_quotas():
# With hierarchical projects, only the admin of the current project # With hierarchical projects, only the admin of the current project
# or the root project has privilege to perform quota show # or the root project has privilege to perform quota show
# operations. # operations.
target_project = self._get_project(context, target_project_id) target_project = quota_utils.get_project_hierarchy(
context_project = self._get_project(context, context.project_id, context, target_project_id)
subtree_as_ids=True) context_project = quota_utils.get_project_hierarchy(
context, context.project_id, subtree_as_ids=True)
self._authorize_show(context_project, target_project) self._authorize_show(context_project, target_project)
parent_project_id = target_project.parent_id
except exceptions.Forbidden:
# NOTE(e0ne): Keystone API v2 requires admin permissions for
# project_get method. We ignore Forbidden exception for
# non-admin users.
parent_project_id = None
try: try:
sqlalchemy_api.authorize_project_context(context, sqlalchemy_api.authorize_project_context(context,
@ -264,8 +220,7 @@ class QuotaSetsController(wsgi.Controller):
except exception.NotAuthorized: except exception.NotAuthorized:
raise webob.exc.HTTPForbidden() raise webob.exc.HTTPForbidden()
quotas = self._get_quotas(context, target_project_id, usage, quotas = self._get_quotas(context, target_project_id, usage)
parent_project_id=parent_project_id)
return self._format_quota_set(target_project_id, quotas) return self._format_quota_set(target_project_id, quotas)
@wsgi.serializers(xml=QuotaTemplate) @wsgi.serializers(xml=QuotaTemplate)
@ -311,22 +266,25 @@ class QuotaSetsController(wsgi.Controller):
msg = _("Bad key(s) in quota set: %s") % ",".join(bad_keys) msg = _("Bad key(s) in quota set: %s") % ",".join(bad_keys)
raise webob.exc.HTTPBadRequest(explanation=msg) raise webob.exc.HTTPBadRequest(explanation=msg)
# Get the parent_id of the target project to verify whether we are # Saving off this value since we need to use it multiple times
# dealing with hierarchical namespace or non-hierarchical namespace. use_nested_quotas = QUOTAS.using_nested_quotas()
target_project = self._get_project(context, target_project_id) if use_nested_quotas:
parent_id = target_project.parent_id # 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: if parent_id:
# Get the children of the project which the token is scoped to # Get the children of the project which the token is scoped to
# in order to know if the target_project is in its hierarchy. # in order to know if the target_project is in its hierarchy.
context_project = self._get_project(context, context_project = quota_utils.get_project_hierarchy(
context.project_id, context, context.project_id, subtree_as_ids=True)
subtree_as_ids=True) self._authorize_update_or_delete(context_project,
self._authorize_update_or_delete(context_project, target_project.id,
target_project.id, parent_id)
parent_id) parent_project_quotas = QUOTAS.get_project_quotas(
parent_project_quotas = QUOTAS.get_project_quotas( context, parent_id)
context, parent_id)
# NOTE(ankit): Pass #2 - In this loop for body['quota_set'].keys(), # 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 # we validate the quota limits to ensure that we can bail out if
@ -344,10 +302,17 @@ class QuotaSetsController(wsgi.Controller):
if not skip_flag: if not skip_flag:
self._validate_existing_resource(key, value, quota_values) self._validate_existing_resource(key, value, quota_values)
if parent_id: if use_nested_quotas and parent_id:
value = self._validate_quota_limit(body['quota_set'], key, value = self._validate_quota_limit(body['quota_set'], key,
quota_values, quota_values,
parent_project_quotas) 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 original_quota = 0
if quota_values.get(key): if quota_values.get(key):
original_quota = quota_values[key]['limit'] original_quota = quota_values[key]['limit']
@ -373,7 +338,7 @@ class QuotaSetsController(wsgi.Controller):
# If hierarchical projects, update child's quota first # If hierarchical projects, update child's quota first
# and then parents quota. In future this needs to be an # and then parents quota. In future this needs to be an
# atomic operation. # atomic operation.
if parent_id: if use_nested_quotas and parent_id:
if key in allocated_quotas.keys(): if key in allocated_quotas.keys():
try: try:
db.quota_allocated_update(context, parent_id, key, db.quota_allocated_update(context, parent_id, key,
@ -383,24 +348,15 @@ class QuotaSetsController(wsgi.Controller):
db.quota_create(context, parent_id, key, parent_limit, db.quota_create(context, parent_id, key, parent_limit,
allocated=allocated_quotas[key]) allocated=allocated_quotas[key])
return {'quota_set': self._get_quotas(context, target_project_id, return {'quota_set': self._get_quotas(context, target_project_id)}
parent_project_id=parent_id)}
@wsgi.serializers(xml=QuotaTemplate) @wsgi.serializers(xml=QuotaTemplate)
def defaults(self, req, id): def defaults(self, req, id):
context = req.environ['cinder.context'] context = req.environ['cinder.context']
authorize_show(context) authorize_show(context)
try:
project = self._get_project(context, context.project_id)
parent_id = project.parent_id
except exceptions.Forbidden:
# NOTE(e0ne): Keystone API v2 requires admin permissions for
# project_get method. We ignore Forbidden exception for
# non-admin users.
parent_id = context.project_id
return self._format_quota_set(id, QUOTAS.get_defaults( return self._format_quota_set(id, QUOTAS.get_defaults(
context, parent_project_id=parent_id)) context, project_id=id))
@wsgi.serializers(xml=QuotaTemplate) @wsgi.serializers(xml=QuotaTemplate)
def delete(self, req, id): def delete(self, req, id):
@ -416,20 +372,30 @@ class QuotaSetsController(wsgi.Controller):
context = req.environ['cinder.context'] context = req.environ['cinder.context']
authorize_delete(context) authorize_delete(context)
# Get the parent_id of the target project to verify whether we are if QUOTAS.using_nested_quotas():
# dealing with hierarchical namespace or non-hierarchical namespace. self._delete_nested_quota(context, id)
target_project = self._get_project(context, id) else:
parent_id = target_project.parent_id try:
db.quota_destroy_by_project(context, id)
except exception.AdminRequired:
raise webob.exc.HTTPForbidden()
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: try:
project_quotas = QUOTAS.get_project_quotas( project_quotas = QUOTAS.get_project_quotas(
context, target_project.id, usages=True, ctxt, proj_id, usages=True, defaults=False)
parent_project_id=parent_id, defaults=False)
except exception.NotAuthorized: except exception.NotAuthorized:
raise webob.exc.HTTPForbidden() raise webob.exc.HTTPForbidden()
# If the project which is being deleted has allocated part of its quota target_project = quota_utils.get_project_hierarchy(
# to its subprojects, then subprojects' quotas should be deleted first. 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(): for key, value in project_quotas.items():
if 'allocated' in project_quotas[key].keys(): if 'allocated' in project_quotas[key].keys():
if project_quotas[key]['allocated'] != 0: if project_quotas[key]['allocated'] != 0:
@ -438,35 +404,48 @@ class QuotaSetsController(wsgi.Controller):
raise webob.exc.HTTPBadRequest(explanation=msg) raise webob.exc.HTTPBadRequest(explanation=msg)
if parent_id: if parent_id:
# Get the children of the project which the token is scoped to in # Get the children of the project which the token is scoped to
# order to know if the target_project is in its hierarchy. # in order to know if the target_project is in its hierarchy.
context_project = self._get_project(context, context_project = quota_utils.get_project_hierarchy(
context.project_id, ctxt, ctxt.project_id, subtree_as_ids=True)
subtree_as_ids=True)
self._authorize_update_or_delete(context_project, self._authorize_update_or_delete(context_project,
target_project.id, target_project.id,
parent_id) parent_id)
parent_project_quotas = QUOTAS.get_project_quotas( parent_project_quotas = QUOTAS.get_project_quotas(
context, parent_id, parent_project_id=parent_id) ctxt, parent_id)
# Delete child quota first and later update parent's quota. # Delete child quota first and later update parent's quota.
try: try:
db.quota_destroy_by_project(context, target_project.id) db.quota_destroy_by_project(ctxt, target_project.id)
except exception.AdminRequired: except exception.AdminRequired:
raise webob.exc.HTTPForbidden() raise webob.exc.HTTPForbidden()
# Update the allocated of the parent # 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(): for key, value in project_quotas.items():
project_hard_limit = project_quotas[key]['limit'] project_hard_limit = project_quotas[key]['limit']
parent_allocated = parent_project_quotas[key]['allocated'] parent_allocated = parent_project_quotas[key]['allocated']
parent_allocated -= project_hard_limit parent_allocated -= project_hard_limit
db.quota_allocated_update(context, parent_id, key, db.quota_allocated_update(ctxt, parent_id, key,
parent_allocated) parent_allocated)
else:
try: def validate_setup_for_nested_quota_use(self, req):
db.quota_destroy_by_project(context, target_project.id) """Validates that the setup supports using nested quotas.
except exception.AdminRequired:
raise webob.exc.HTTPForbidden() 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']
params = req.params
try:
quota_utils.validate_setup_for_nested_quota_use(
ctxt, QUOTAS.resources, quota.NestedDbQuotaDriver(),
fix_allocated_quotas=params.get('fix_allocated_quotas'))
except exception.InvalidNestedQuotaSetup as e:
raise webob.exc.HTTPBadRequest(explanation=e.msg)
class Quotas(extensions.ExtensionDescriptor): class Quotas(extensions.ExtensionDescriptor):
@ -480,9 +459,10 @@ class Quotas(extensions.ExtensionDescriptor):
def get_resources(self): def get_resources(self):
resources = [] resources = []
res = extensions.ResourceExtension('os-quota-sets', res = extensions.ResourceExtension(
QuotaSetsController(), 'os-quota-sets', QuotaSetsController(),
member_actions={'defaults': 'GET'}) member_actions={'defaults': 'GET'},
collection_actions={'validate_setup_for_nested_quota_use': 'GET'})
resources.append(res) resources.append(res)
return resources return resources

View File

@ -376,6 +376,11 @@ class InvalidQuotaValue(Invalid):
"resources: %(unders)s") "resources: %(unders)s")
class InvalidNestedQuotaSetup(CinderException):
message = _("Project quotas are not properly setup for nested quotas: "
"%(reason)s.")
class QuotaNotFound(NotFound): class QuotaNotFound(NotFound):
message = _("Quota could not be found") message = _("Quota could not be found")

View File

@ -16,7 +16,7 @@
"""Quotas for volumes.""" """Quotas for volumes."""
from collections import deque
import datetime import datetime
from oslo_config import cfg from oslo_config import cfg
@ -30,6 +30,7 @@ from cinder import context
from cinder import db from cinder import db
from cinder import exception from cinder import exception
from cinder.i18n import _, _LE from cinder.i18n import _, _LE
from cinder import quota_utils
LOG = logging.getLogger(__name__) LOG = logging.getLogger(__name__)
@ -65,7 +66,7 @@ quota_opts = [
default=0, default=0,
help='Number of seconds between subsequent usage refreshes'), help='Number of seconds between subsequent usage refreshes'),
cfg.StrOpt('quota_driver', cfg.StrOpt('quota_driver',
default='cinder.quota.DbQuotaDriver', default="cinder.quota.DbQuotaDriver",
help='Default driver to use for quota checks'), help='Default driver to use for quota checks'),
cfg.BoolOpt('use_default_quota_class', cfg.BoolOpt('use_default_quota_class',
default=True, default=True,
@ -97,18 +98,12 @@ class DbQuotaDriver(object):
return db.quota_class_get(context, quota_class, resource_name) return db.quota_class_get(context, quota_class, resource_name)
def get_default(self, context, resource, parent_project_id=None): def get_default(self, context, resource, project_id):
"""Get a specific default quota for a resource. """Get a specific default quota for a resource."""
:param parent_project_id: The id of the current project's parent,
if any.
"""
default_quotas = db.quota_class_get_default(context) default_quotas = db.quota_class_get_default(context)
default_quota_value = 0 if parent_project_id else resource.default return default_quotas.get(resource.name, resource.default)
return default_quotas.get(resource.name, default_quota_value)
def get_defaults(self, context, resources, parent_project_id=None): def get_defaults(self, context, resources, project_id=None):
"""Given a list of resources, retrieve the default quotas. """Given a list of resources, retrieve the default quotas.
Use the class quotas named `_DEFAULT_QUOTA_NAME` as default quotas, Use the class quotas named `_DEFAULT_QUOTA_NAME` as default quotas,
@ -116,13 +111,12 @@ class DbQuotaDriver(object):
:param context: The request context, for access checks. :param context: The request context, for access checks.
:param resources: A dictionary of the registered resources. :param resources: A dictionary of the registered resources.
:param parent_project_id: The id of the current project's parent, :param project_id: The id of the current project
if any.
""" """
quotas = {} quotas = {}
default_quotas = {} default_quotas = {}
if CONF.use_default_quota_class and not parent_project_id: if CONF.use_default_quota_class:
default_quotas = db.quota_class_get_default(context) default_quotas = db.quota_class_get_default(context)
for resource in resources.values(): for resource in resources.values():
@ -135,8 +129,7 @@ class DbQuotaDriver(object):
"default quota class for default " "default quota class for default "
"quota.") % {'res': resource.name}) "quota.") % {'res': resource.name})
quotas[resource.name] = default_quotas.get(resource.name, quotas[resource.name] = default_quotas.get(resource.name,
(0 if parent_project_id resource.default)
else resource.default))
return quotas return quotas
def get_class_quotas(self, context, resources, quota_class, def get_class_quotas(self, context, resources, quota_class,
@ -170,7 +163,7 @@ class DbQuotaDriver(object):
def get_project_quotas(self, context, resources, project_id, def get_project_quotas(self, context, resources, project_id,
quota_class=None, defaults=True, quota_class=None, defaults=True,
usages=True, parent_project_id=None): usages=True):
"""Retrieve quotas for a project. """Retrieve quotas for a project.
Given a list of resources, retrieve the quotas for the given Given a list of resources, retrieve the quotas for the given
@ -190,12 +183,11 @@ class DbQuotaDriver(object):
specific value for the resource. specific value for the resource.
:param usages: If True, the current in_use, reserved and allocated :param usages: If True, the current in_use, reserved and allocated
counts will also be returned. counts will also be returned.
:param parent_project_id: The id of the current project's parent,
if any.
""" """
quotas = {} quotas = {}
project_quotas = db.quota_get_all_by_project(context, project_id) project_quotas = db.quota_get_all_by_project(context, project_id)
allocated_quotas = None
if usages: if usages:
project_usages = db.quota_usage_get_all_by_project(context, project_usages = db.quota_usage_get_all_by_project(context,
project_id) project_id)
@ -214,8 +206,8 @@ class DbQuotaDriver(object):
else: else:
class_quotas = {} class_quotas = {}
default_quotas = self.get_defaults(context, resources, # TODO(mc_nair): change this to be lazy loaded
parent_project_id=parent_project_id) default_quotas = self.get_defaults(context, resources, project_id)
for resource in resources.values(): for resource in resources.values():
# Omit default/quota class values # Omit default/quota class values
@ -237,15 +229,12 @@ class DbQuotaDriver(object):
quotas[resource.name].update( quotas[resource.name].update(
in_use=usage.get('in_use', 0), in_use=usage.get('in_use', 0),
reserved=usage.get('reserved', 0), ) reserved=usage.get('reserved', 0), )
if allocated_quotas:
if parent_project_id or allocated_quotas: quotas[resource.name].update(
quotas[resource.name].update( allocated=allocated_quotas.get(resource.name, 0), )
allocated=allocated_quotas.get(resource.name, 0), )
return quotas return quotas
def _get_quotas(self, context, resources, keys, has_sync, project_id=None, def _get_quotas(self, context, resources, keys, has_sync, project_id=None):
parent_project_id=None):
"""A helper method which retrieves the quotas for specific resources. """A helper method which retrieves the quotas for specific resources.
This specific resource is identified by keys, and which apply to the This specific resource is identified by keys, and which apply to the
@ -261,8 +250,6 @@ class DbQuotaDriver(object):
:param project_id: Specify the project_id if current context :param project_id: Specify the project_id if current context
is admin and admin wants to impact on is admin and admin wants to impact on
common user's tenant. common user's tenant.
:param parent_project_id: The id of the current project's parent,
if any.
""" """
# Filter resources # Filter resources
@ -282,8 +269,7 @@ class DbQuotaDriver(object):
# Grab and return the quotas (without usages) # Grab and return the quotas (without usages)
quotas = self.get_project_quotas(context, sub_resources, quotas = self.get_project_quotas(context, sub_resources,
project_id, project_id,
context.quota_class, usages=False, context.quota_class, usages=False)
parent_project_id=parent_project_id)
return {k: v['limit'] for k, v in quotas.items()} return {k: v['limit'] for k, v in quotas.items()}
@ -452,6 +438,134 @@ class DbQuotaDriver(object):
db.reservation_expire(context) db.reservation_expire(context)
class NestedDbQuotaDriver(DbQuotaDriver):
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)
def get_default(self, context, resource, project_id):
"""Get a specific default quota for a resource."""
resource = super(NestedDbQuotaDriver, self).get_default(
context, resource, project_id)
return 0 if quota_utils.get_parent_project_id(
context, project_id) else resource.default
def get_defaults(self, context, resources, project_id=None):
defaults = super(NestedDbQuotaDriver, self).get_defaults(
context, resources, project_id)
# All defaults are 0 for child project
if quota_utils.get_parent_project_id(context, project_id):
for key in defaults.keys():
defaults[key] = 0
return defaults
class BaseResource(object): class BaseResource(object):
"""Describe a single resource for quota checking.""" """Describe a single resource for quota checking."""
@ -626,14 +740,31 @@ class QuotaEngine(object):
def __init__(self, quota_driver_class=None): def __init__(self, quota_driver_class=None):
"""Initialize a Quota object.""" """Initialize a Quota object."""
if not quota_driver_class:
quota_driver_class = CONF.quota_driver
if isinstance(quota_driver_class, six.string_types):
quota_driver_class = importutils.import_object(quota_driver_class)
self._resources = {} self._resources = {}
self._driver = quota_driver_class self._quota_driver_class = quota_driver_class
self._driver_class = None
@property
def _driver(self):
# Lazy load the driver so we give a chance for the config file to
# be read before grabbing the config for which QuotaDriver to use
if self._driver_class:
return self._driver_class
if not self._quota_driver_class:
# Grab the current driver class from CONF
self._quota_driver_class = CONF.quota_driver
if isinstance(self._quota_driver_class, six.string_types):
self._quota_driver_class = importutils.import_object(
self._quota_driver_class)
self._driver_class = self._quota_driver_class
return self._driver_class
def using_nested_quotas(self):
"""Returns true if nested quotas are being used"""
return isinstance(self._driver, NestedDbQuotaDriver)
def __contains__(self, resource): def __contains__(self, resource):
return resource in self.resources return resource in self.resources
@ -669,16 +800,15 @@ class QuotaEngine(object):
return self._driver.get_default(context, resource, return self._driver.get_default(context, resource,
parent_project_id=parent_project_id) parent_project_id=parent_project_id)
def get_defaults(self, context, parent_project_id=None): def get_defaults(self, context, project_id=None):
"""Retrieve the default quotas. """Retrieve the default quotas.
:param context: The request context, for access checks. :param context: The request context, for access checks.
:param parent_project_id: The id of the current project's parent, :param project_id: The id of the current project
if any.
""" """
return self._driver.get_defaults(context, self.resources, return self._driver.get_defaults(context, self.resources,
parent_project_id) project_id)
def get_class_quotas(self, context, quota_class, defaults=True): def get_class_quotas(self, context, quota_class, defaults=True):
"""Retrieve the quotas for the given quota class. """Retrieve the quotas for the given quota class.
@ -695,7 +825,7 @@ class QuotaEngine(object):
quota_class, defaults=defaults) quota_class, defaults=defaults)
def get_project_quotas(self, context, project_id, quota_class=None, def get_project_quotas(self, context, project_id, quota_class=None,
defaults=True, usages=True, parent_project_id=None): defaults=True, usages=True):
"""Retrieve the quotas for the given project. """Retrieve the quotas for the given project.
:param context: The request context, for access checks. :param context: The request context, for access checks.
@ -709,17 +839,12 @@ class QuotaEngine(object):
specific value for the resource. specific value for the resource.
:param usages: If True, the current in_use, reserved and :param usages: If True, the current in_use, reserved and
allocated counts will also be returned. allocated counts will also be returned.
:param parent_project_id: The id of the current project's parent,
if any.
""" """
return self._driver.get_project_quotas(context, self.resources, return self._driver.get_project_quotas(context, self.resources,
project_id, project_id,
quota_class=quota_class, quota_class=quota_class,
defaults=defaults, defaults=defaults,
usages=usages, usages=usages)
parent_project_id=
parent_project_id)
def count(self, context, resource, *args, **kwargs): def count(self, context, resource, *args, **kwargs):
"""Count a resource. """Count a resource.

View File

@ -12,20 +12,42 @@
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations # License for the specific language governing permissions and limitations
# under the License. # under the License.
import webob
from oslo_config import cfg
from oslo_log import log as logging from oslo_log import log as logging
from keystoneclient.auth.identity.generic import token
from keystoneclient import client
from keystoneclient import exceptions
from keystoneclient import session
from cinder import exception from cinder import exception
from cinder.i18n import _LW from cinder.i18n import _, _LW
from cinder import quota
CONF = cfg.CONF
LOG = logging.getLogger(__name__) LOG = logging.getLogger(__name__)
QUOTAS = quota.QUOTAS
class GenericProjectInfo(object):
"""Abstraction layer for Keystone V2 and V3 project objects"""
def __init__(self, project_id, project_keystone_api_version,
project_parent_id=None,
project_subtree=None,
project_parent_tree=None):
self.id = project_id
self.keystone_api_version = project_keystone_api_version
self.parent_id = project_parent_id
self.subtree = project_subtree
self.parents = project_parent_tree
def get_volume_type_reservation(ctxt, volume, type_id, def get_volume_type_reservation(ctxt, volume, type_id,
reserve_vol_type_only=False): reserve_vol_type_only=False):
from cinder import quota
QUOTAS = quota.QUOTAS
# Reserve quotas for the given volume type # Reserve quotas for the given volume type
try: try:
reserve_opts = {'volumes': 1, 'gigabytes': volume['size']} reserve_opts = {'volumes': 1, 'gigabytes': volume['size']}
@ -78,3 +100,104 @@ def get_volume_type_reservation(ctxt, volume, type_id,
raise exception.VolumeLimitExceeded( raise exception.VolumeLimitExceeded(
allowed=quotas[over]) allowed=quotas[over])
return reservations return reservations
def get_project_hierarchy(context, project_id, subtree_as_ids=False):
"""A Helper method to get the project hierarchy.
Along with hierarchical multitenancy in keystone API v3, projects can be
hierarchically organized. Therefore, we need to know the project
hierarchy, if any, in order to do nested quota operations properly.
"""
try:
keystone = _keystone_client(context)
generic_project = GenericProjectInfo(project_id, keystone.version)
if keystone.version == 'v3':
project = keystone.projects.get(project_id,
subtree_as_ids=subtree_as_ids)
generic_project.parent_id = project.parent_id
generic_project.subtree = (
project.subtree if subtree_as_ids else None)
except exceptions.NotFound:
msg = (_("Tenant ID: %s does not exist.") % project_id)
raise webob.exc.HTTPNotFound(explanation=msg)
return generic_project
def get_parent_project_id(context, project_id):
return get_project_hierarchy(context, project_id).parent_id
def get_all_projects(context):
# Right now this would have to be done as cloud admin with Keystone v3
return _keystone_client(context, (3, 0)).projects.list()
def get_all_root_project_ids(context):
project_list = get_all_projects(context)
# Find every project which does not have a parent, meaning it is the
# root of the tree
project_roots = [project.id for project in project_list
if not project.parent_id]
return project_roots
def validate_setup_for_nested_quota_use(ctxt, resources,
nested_quota_driver,
fix_allocated_quotas=False):
"""Validates the setup supports using nested quotas.
Ensures that Keystone v3 or greater is being used, that the current
user is of the cloud admin role, 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).
:param resources: the quota resources to validate
:param nested_quota_driver: nested quota driver used to validate each tree
:param fix_allocated_quotas: if True, parent projects "allocated" total
will be calculated based on the existing child limits and the DB will
be updated. If False, an exception is raised reporting any parent
allocated quotas are currently incorrect.
"""
try:
project_roots = get_all_root_project_ids(ctxt)
# Now that we've got the roots of each tree, validate the trees
# to ensure that each is setup logically for nested quotas
for root in project_roots:
root_proj = get_project_hierarchy(ctxt, root,
subtree_as_ids=True)
nested_quota_driver.validate_nested_setup(
ctxt,
resources,
{root_proj.id: root_proj.subtree},
fix_allocated_quotas=fix_allocated_quotas
)
except exceptions.VersionNotAvailable:
msg = _("Keystone version 3 or greater must be used to get nested "
"quota support.")
raise exception.CinderException(message=msg)
except exceptions.Forbidden:
msg = _("Must run this command as cloud admin using "
"a Keystone policy.json which allows cloud "
"admin to list and get any project.")
raise exception.CinderException(message=msg)
def _keystone_client(context, version=(3, 0)):
"""Creates and returns an instance of a generic keystone client.
:param context: The request context
:param version: version of Keystone to request
:return: keystoneclient.client.Client object
"""
auth_plugin = token.Token(
auth_url=CONF.keystone_authtoken.auth_uri,
token=context.auth_token,
project_id=context.project_id)
client_session = session.Session(auth=auth_plugin)
return client.Client(auth_url=CONF.keystone_authtoken.auth_uri,
session=client_session, version=version)

View File

@ -29,10 +29,10 @@ import webob.exc
from cinder.api.contrib import quotas from cinder.api.contrib import quotas
from cinder import context from cinder import context
from cinder import db from cinder import db
from cinder import quota
from cinder import test from cinder import test
from cinder.tests.unit import test_db_api from cinder.tests.unit import test_db_api
from keystoneclient import exceptions
from keystonemiddleware import auth_token from keystonemiddleware import auth_token
from oslo_config import cfg from oslo_config import cfg
from oslo_config import fixture as config_fixture from oslo_config import fixture as config_fixture
@ -43,7 +43,7 @@ CONF = cfg.CONF
def make_body(root=True, gigabytes=1000, snapshots=10, def make_body(root=True, gigabytes=1000, snapshots=10,
volumes=10, backups=10, backup_gigabytes=1000, volumes=10, backups=10, backup_gigabytes=1000,
tenant_id='foo', per_volume_gigabytes=-1): tenant_id='foo', per_volume_gigabytes=-1, is_child=False):
resources = {'gigabytes': gigabytes, resources = {'gigabytes': gigabytes,
'snapshots': snapshots, 'snapshots': snapshots,
'volumes': volumes, 'volumes': volumes,
@ -52,10 +52,18 @@ def make_body(root=True, gigabytes=1000, snapshots=10,
'per_volume_gigabytes': per_volume_gigabytes, } 'per_volume_gigabytes': per_volume_gigabytes, }
# need to consider preexisting volume types as well # need to consider preexisting volume types as well
volume_types = db.volume_type_get_all(context.get_admin_context()) volume_types = db.volume_type_get_all(context.get_admin_context())
for volume_type in volume_types:
resources['gigabytes_' + volume_type] = -1 if not is_child:
resources['snapshots_' + volume_type] = -1 for volume_type in volume_types:
resources['volumes_' + volume_type] = -1 resources['gigabytes_' + volume_type] = -1
resources['snapshots_' + volume_type] = -1
resources['volumes_' + volume_type] = -1
elif per_volume_gigabytes < 0:
# In the case that we're dealing with a child project, we aren't
# allowing -1 limits for the time being, so hack this to some large
# enough value for the tests that it's essentially unlimited
# TODO(mc_nair): remove when -1 limits for child projects are allowed
resources['per_volume_gigabytes'] = 10000
if tenant_id: if tenant_id:
resources['id'] = tenant_id resources['id'] = tenant_id
@ -75,7 +83,7 @@ def make_subproject_body(root=True, gigabytes=0, snapshots=0,
per_volume_gigabytes=per_volume_gigabytes) per_volume_gigabytes=per_volume_gigabytes)
class QuotaSetsControllerTest(test.TestCase): class QuotaSetsControllerTestBase(test.TestCase):
class FakeProject(object): class FakeProject(object):
@ -85,15 +93,30 @@ class QuotaSetsControllerTest(test.TestCase):
self.subtree = None self.subtree = None
def setUp(self): def setUp(self):
super(QuotaSetsControllerTest, self).setUp() super(QuotaSetsControllerTestBase, self).setUp()
self.controller = quotas.QuotaSetsController() self.controller = quotas.QuotaSetsController()
self.req = mock.Mock() self.req = mock.Mock()
self.req.environ = {'cinder.context': context.get_admin_context()} self.req.environ = {'cinder.context': context.get_admin_context()}
self.req.environ['cinder.context'].is_admin = True self.req.environ['cinder.context'].is_admin = True
self.req.params = {}
self._create_project_hierarchy() self._create_project_hierarchy()
get_patcher = mock.patch('cinder.quota_utils.get_project_hierarchy',
self._get_project)
get_patcher.start()
self.addCleanup(get_patcher.stop)
def _list_projects(context):
return self.project_by_id.values()
list_patcher = mock.patch('cinder.quota_utils.get_all_projects',
_list_projects)
list_patcher.start()
self.addCleanup(list_patcher.stop)
self.auth_url = 'http://localhost:5000' self.auth_url = 'http://localhost:5000'
self.fixture = self.useFixture(config_fixture.Config(auth_token.CONF)) self.fixture = self.useFixture(config_fixture.Config(auth_token.CONF))
self.fixture.config(auth_uri=self.auth_url, group='keystone_authtoken') self.fixture.config(auth_uri=self.auth_url, group='keystone_authtoken')
@ -127,111 +150,24 @@ class QuotaSetsControllerTest(test.TestCase):
def _get_project(self, context, id, subtree_as_ids=False): def _get_project(self, context, id, subtree_as_ids=False):
return self.project_by_id.get(id, self.FakeProject()) return self.project_by_id.get(id, self.FakeProject())
@mock.patch('keystoneclient.client.Client')
@mock.patch('keystoneclient.session.Session')
def test_keystone_client_instantiation(self, ksclient_session,
ksclient_class):
context = self.req.environ['cinder.context']
self.controller._keystone_client(context)
ksclient_class.assert_called_once_with(auth_url=self.auth_url,
session=ksclient_session())
@mock.patch('keystoneclient.client.Client') class QuotaSetsControllerTest(QuotaSetsControllerTestBase):
def test_get_project_keystoneclient_v2(self, ksclient_class): def setUp(self):
context = self.req.environ['cinder.context'] super(QuotaSetsControllerTest, self).setUp()
keystoneclient = ksclient_class.return_value fixture = self.useFixture(config_fixture.Config(quota.CONF))
keystoneclient.version = 'v2.0' fixture.config(quota_driver="cinder.quota.DbQuotaDriver")
expected_project = self.controller.GenericProjectInfo( quotas.QUOTAS = quota.VolumeTypeQuotaEngine()
context.project_id, 'v2.0') self.controller = quotas.QuotaSetsController()
project = self.controller._get_project(context, context.project_id)
self.assertEqual(expected_project.__dict__, project.__dict__)
@mock.patch('keystoneclient.client.Client')
def test_get_project_keystoneclient_v3(self, ksclient_class):
context = self.req.environ['cinder.context']
keystoneclient = ksclient_class.return_value
keystoneclient.version = 'v3'
returned_project = self.FakeProject(context.project_id, 'bar')
del returned_project.subtree
keystoneclient.projects.get.return_value = returned_project
expected_project = self.controller.GenericProjectInfo(
context.project_id, 'v3', 'bar')
project = self.controller._get_project(context, context.project_id)
self.assertEqual(expected_project.__dict__, project.__dict__)
@mock.patch('keystoneclient.client.Client')
def test_get_project_keystoneclient_v3_with_subtree(self, ksclient_class):
context = self.req.environ['cinder.context']
keystoneclient = ksclient_class.return_value
keystoneclient.version = 'v3'
returned_project = self.FakeProject(context.project_id, 'bar')
subtree_dict = {'baz': {'quux': None}}
returned_project.subtree = subtree_dict
keystoneclient.projects.get.return_value = returned_project
expected_project = self.controller.GenericProjectInfo(
context.project_id, 'v3', 'bar', subtree_dict)
project = self.controller._get_project(context, context.project_id,
subtree_as_ids=True)
keystoneclient.projects.get.assert_called_once_with(
context.project_id, subtree_as_ids=True)
self.assertEqual(expected_project.__dict__, project.__dict__)
def test_defaults(self): def test_defaults(self):
self.controller._get_project = mock.Mock()
self.controller._get_project.side_effect = self._get_project
result = self.controller.defaults(self.req, 'foo') result = self.controller.defaults(self.req, 'foo')
self.assertDictMatch(make_body(), result) self.assertDictMatch(make_body(), result)
def test_subproject_defaults(self):
self.controller._get_project = mock.Mock()
self.controller._get_project.side_effect = self._get_project
context = self.req.environ['cinder.context']
context.project_id = self.B.id
result = self.controller.defaults(self.req, self.B.id)
expected = make_subproject_body(tenant_id=self.B.id)
self.assertDictMatch(expected, result)
def test_show(self): def test_show(self):
self.controller._get_project = mock.Mock()
self.controller._get_project.side_effect = self._get_project
result = self.controller.show(self.req, 'foo') result = self.controller.show(self.req, 'foo')
self.assertDictMatch(make_body(), result) self.assertDictMatch(make_body(), result)
def test_subproject_show(self):
self.controller._get_project = mock.Mock()
self.controller._get_project.side_effect = self._get_project
self.req.environ['cinder.context'].project_id = self.A.id
result = self.controller.show(self.req, self.B.id)
expected = make_subproject_body(tenant_id=self.B.id)
self.assertDictMatch(expected, result)
def test_subproject_show_in_hierarchy(self):
self.controller._get_project = mock.Mock()
self.controller._get_project.side_effect = self._get_project
# An user scoped to a root project in an hierarchy can see its children
# quotas.
self.req.environ['cinder.context'].project_id = self.A.id
result = self.controller.show(self.req, self.D.id)
expected = make_subproject_body(tenant_id=self.D.id)
self.assertDictMatch(expected, result)
# An user scoped to a parent project can see its immediate children
# quotas.
self.req.environ['cinder.context'].project_id = self.B.id
result = self.controller.show(self.req, self.D.id)
expected = make_subproject_body(tenant_id=self.D.id)
self.assertDictMatch(expected, result)
def test_subproject_show_target_project_equals_to_context_project(self):
self.controller._get_project = mock.Mock()
self.controller._get_project.side_effect = self._get_project
self.req.environ['cinder.context'].project_id = self.B.id
result = self.controller.show(self.req, self.B.id)
expected = make_subproject_body(tenant_id=self.B.id)
self.assertDictMatch(expected, result)
def test_show_not_authorized(self): def test_show_not_authorized(self):
self.controller._get_project = mock.Mock()
self.controller._get_project.side_effect = self._get_project
self.req.environ['cinder.context'].is_admin = False self.req.environ['cinder.context'].is_admin = False
self.req.environ['cinder.context'].user_id = 'bad_user' self.req.environ['cinder.context'].user_id = 'bad_user'
self.req.environ['cinder.context'].project_id = 'bad_project' self.req.environ['cinder.context'].project_id = 'bad_project'
@ -239,29 +175,14 @@ class QuotaSetsControllerTest(test.TestCase):
self.req, 'foo') self.req, 'foo')
def test_show_non_admin_user(self): def test_show_non_admin_user(self):
self.controller._get_project = mock.Mock()
self.controller._get_project.side_effect = exceptions.Forbidden
self.controller._get_quotas = mock.Mock(side_effect= self.controller._get_quotas = mock.Mock(side_effect=
self.controller._get_quotas) self.controller._get_quotas)
result = self.controller.show(self.req, 'foo') result = self.controller.show(self.req, 'foo')
self.assertDictMatch(make_body(), result) self.assertDictMatch(make_body(), result)
self.controller._get_quotas.assert_called_with( self.controller._get_quotas.assert_called_with(
self.req.environ['cinder.context'], 'foo', False, self.req.environ['cinder.context'], 'foo', False)
parent_project_id=None)
def test_subproject_show_not_authorized(self):
self.controller._get_project = mock.Mock()
self.controller._get_project.side_effect = self._get_project
self.req.environ['cinder.context'].project_id = self.B.id
self.assertRaises(webob.exc.HTTPForbidden, self.controller.show,
self.req, self.C.id)
self.req.environ['cinder.context'].project_id = self.B.id
self.assertRaises(webob.exc.HTTPForbidden, self.controller.show,
self.req, self.A.id)
def test_update(self): def test_update(self):
self.controller._get_project = mock.Mock()
self.controller._get_project.side_effect = self._get_project
body = make_body(gigabytes=2000, snapshots=15, body = make_body(gigabytes=2000, snapshots=15,
volumes=5, backups=5, tenant_id=None) volumes=5, backups=5, tenant_id=None)
result = self.controller.update(self.req, 'foo', body) result = self.controller.update(self.req, 'foo', body)
@ -271,65 +192,9 @@ class QuotaSetsControllerTest(test.TestCase):
result = self.controller.update(self.req, 'foo', body) result = self.controller.update(self.req, 'foo', body)
self.assertDictMatch(body, result) self.assertDictMatch(body, result)
def test_update_subproject(self): def test_update_subproject_not_in_hierarchy_non_nested(self):
self.controller._get_project = mock.Mock() # When not using nested quotas, the hierarchy should not be considered
self.controller._get_project.side_effect = self._get_project # for an update
# Update the project A quota.
self.req.environ['cinder.context'].project_id = self.A.id
body = make_body(gigabytes=2000, snapshots=15,
volumes=5, backups=5, tenant_id=None)
result = self.controller.update(self.req, self.A.id, body)
self.assertDictMatch(body, result)
# Update the quota of B to be equal to its parent quota
self.req.environ['cinder.context'].project_id = self.A.id
body = make_body(gigabytes=2000, snapshots=15,
volumes=5, backups=5, tenant_id=None)
result = self.controller.update(self.req, self.B.id, body)
self.assertDictMatch(body, result)
# Try to update the quota of C, it will not be allowed, since the
# project A doesn't have free quota available.
self.req.environ['cinder.context'].project_id = self.A.id
body = make_body(gigabytes=2000, snapshots=15,
volumes=5, backups=5, tenant_id=None)
self.assertRaises(webob.exc.HTTPBadRequest, self.controller.update,
self.req, self.C.id, body)
# Successfully update the quota of D.
self.req.environ['cinder.context'].project_id = self.A.id
body = make_body(gigabytes=1000, snapshots=7,
volumes=3, backups=3, tenant_id=None)
result = self.controller.update(self.req, self.D.id, body)
self.assertDictMatch(body, result)
# An admin of B can also update the quota of D, since D is its an
# immediate child.
self.req.environ['cinder.context'].project_id = self.B.id
body = make_body(gigabytes=1500, snapshots=10,
volumes=4, backups=4, tenant_id=None)
result = self.controller.update(self.req, self.D.id, body)
def test_update_subproject_repetitive(self):
self.controller._get_project = mock.Mock()
self.controller._get_project.side_effect = self._get_project
# Update the project A volumes quota.
self.req.environ['cinder.context'].project_id = self.A.id
body = make_body(gigabytes=2000, snapshots=15,
volumes=10, backups=5, tenant_id=None)
result = self.controller.update(self.req, self.A.id, body)
self.assertDictMatch(body, result)
# Update the quota of B to be equal to its parent quota
# three times should be successful, the quota will not be
# allocated to 'allocated' value of parent project
for i in range(0, 3):
self.req.environ['cinder.context'].project_id = self.A.id
body = make_body(gigabytes=2000, snapshots=15,
volumes=10, backups=5, tenant_id=None)
result = self.controller.update(self.req, self.B.id, body)
self.assertDictMatch(body, result)
def test_update_subproject_not_in_hierarchy(self):
self.controller._get_project = mock.Mock()
self.controller._get_project.side_effect = self._get_project
# Create another project hierarchy
E = self.FakeProject(id=uuid.uuid4().hex, parent_id=None) E = self.FakeProject(id=uuid.uuid4().hex, parent_id=None)
F = self.FakeProject(id=uuid.uuid4().hex, parent_id=E.id) F = self.FakeProject(id=uuid.uuid4().hex, parent_id=E.id)
E.subtree = {F.id: F.subtree} E.subtree = {F.id: F.subtree}
@ -342,50 +207,19 @@ class QuotaSetsControllerTest(test.TestCase):
volumes=5, backups=5, tenant_id=None) volumes=5, backups=5, tenant_id=None)
result = self.controller.update(self.req, self.A.id, body) result = self.controller.update(self.req, self.A.id, body)
self.assertDictMatch(body, result) self.assertDictMatch(body, result)
# Try to update the quota of F, it will not be allowed, since the # Try to update the quota of F, it will be allowed even though
# project E doesn't belongs to the project hierarchy of A. # project E doesn't belong to the project hierarchy of A, because
# we are NOT using the nested quota driver
self.req.environ['cinder.context'].project_id = self.A.id self.req.environ['cinder.context'].project_id = self.A.id
body = make_body(gigabytes=2000, snapshots=15, body = make_body(gigabytes=2000, snapshots=15,
volumes=5, backups=5, tenant_id=None) volumes=5, backups=5, tenant_id=None)
self.assertRaises(webob.exc.HTTPForbidden, self.controller.update, self.controller.update(self.req, F.id, body)
self.req, F.id, body)
def test_update_subproject_with_not_root_context_project(self):
self.controller._get_project = mock.Mock()
self.controller._get_project.side_effect = self._get_project
# Update the project A quota.
self.req.environ['cinder.context'].project_id = self.A.id
body = make_body(gigabytes=2000, snapshots=15,
volumes=5, backups=5, tenant_id=None)
result = self.controller.update(self.req, self.A.id, body)
self.assertDictMatch(body, result)
# Try to update the quota of B, it will not be allowed, since the
# project in the context (B) is not a root project.
self.req.environ['cinder.context'].project_id = self.B.id
body = make_body(gigabytes=2000, snapshots=15,
volumes=5, backups=5, tenant_id=None)
self.assertRaises(webob.exc.HTTPForbidden, self.controller.update,
self.req, self.B.id, body)
def test_update_subproject_quota_when_parent_has_default_quotas(self):
self.controller._get_project = mock.Mock()
self.controller._get_project.side_effect = self._get_project
# Since the quotas of the project A were not updated, it will have
# default quotas.
self.req.environ['cinder.context'].project_id = self.A.id
# Update the project B quota.
expected = make_body(gigabytes=1000, snapshots=10,
volumes=5, backups=5, tenant_id=None)
result = self.controller.update(self.req, self.B.id, expected)
self.assertDictMatch(expected, result)
@mock.patch( @mock.patch(
'cinder.api.openstack.wsgi.Controller.validate_string_length') 'cinder.api.openstack.wsgi.Controller.validate_string_length')
@mock.patch( @mock.patch(
'cinder.api.openstack.wsgi.Controller.validate_integer') 'cinder.api.openstack.wsgi.Controller.validate_integer')
def test_update_limit(self, mock_validate_integer, mock_validate): def test_update_limit(self, mock_validate_integer, mock_validate):
self.controller._get_project = mock.Mock()
self.controller._get_project.side_effect = self._get_project
mock_validate_integer.return_value = 10 mock_validate_integer.return_value = 10
body = {'quota_set': {'volumes': 10}} body = {'quota_set': {'volumes': 10}}
@ -401,22 +235,16 @@ class QuotaSetsControllerTest(test.TestCase):
self.req, 'foo', body) self.req, 'foo', body)
def test_update_invalid_value_key_value(self): def test_update_invalid_value_key_value(self):
self.controller._get_project = mock.Mock()
self.controller._get_project.side_effect = self._get_project
body = {'quota_set': {'gigabytes': "should_be_int"}} body = {'quota_set': {'gigabytes': "should_be_int"}}
self.assertRaises(webob.exc.HTTPBadRequest, self.controller.update, self.assertRaises(webob.exc.HTTPBadRequest, self.controller.update,
self.req, 'foo', body) self.req, 'foo', body)
def test_update_invalid_type_key_value(self): def test_update_invalid_type_key_value(self):
self.controller._get_project = mock.Mock()
self.controller._get_project.side_effect = self._get_project
body = {'quota_set': {'gigabytes': None}} body = {'quota_set': {'gigabytes': None}}
self.assertRaises(webob.exc.HTTPBadRequest, self.controller.update, self.assertRaises(webob.exc.HTTPBadRequest, self.controller.update,
self.req, 'foo', body) self.req, 'foo', body)
def test_update_multi_value_with_bad_data(self): def test_update_multi_value_with_bad_data(self):
self.controller._get_project = mock.Mock()
self.controller._get_project.side_effect = self._get_project
orig_quota = self.controller.show(self.req, 'foo') orig_quota = self.controller.show(self.req, 'foo')
body = make_body(gigabytes=2000, snapshots=15, volumes="should_be_int", body = make_body(gigabytes=2000, snapshots=15, volumes="should_be_int",
backups=5, tenant_id=None) backups=5, tenant_id=None)
@ -427,8 +255,6 @@ class QuotaSetsControllerTest(test.TestCase):
self.assertDictMatch(orig_quota, new_quota) self.assertDictMatch(orig_quota, new_quota)
def test_update_bad_quota_limit(self): def test_update_bad_quota_limit(self):
self.controller._get_project = mock.Mock()
self.controller._get_project.side_effect = self._get_project
body = {'quota_set': {'gigabytes': -1000}} body = {'quota_set': {'gigabytes': -1000}}
self.assertRaises(webob.exc.HTTPBadRequest, self.controller.update, self.assertRaises(webob.exc.HTTPBadRequest, self.controller.update,
self.req, 'foo', body) self.req, 'foo', body)
@ -437,8 +263,6 @@ class QuotaSetsControllerTest(test.TestCase):
self.req, 'foo', body) self.req, 'foo', body)
def test_update_no_admin(self): def test_update_no_admin(self):
self.controller._get_project = mock.Mock()
self.controller._get_project.side_effect = self._get_project
self.req.environ['cinder.context'].is_admin = False self.req.environ['cinder.context'].is_admin = False
self.req.environ['cinder.context'].project_id = 'foo' self.req.environ['cinder.context'].project_id = 'foo'
self.req.environ['cinder.context'].user_id = 'foo_user' self.req.environ['cinder.context'].user_id = 'foo_user'
@ -468,8 +292,6 @@ class QuotaSetsControllerTest(test.TestCase):
db.quota_usage_get_all_by_project(ctxt, 'foo')) db.quota_usage_get_all_by_project(ctxt, 'foo'))
def test_update_lower_than_existing_resources_when_skip_false(self): def test_update_lower_than_existing_resources_when_skip_false(self):
self.controller._get_project = mock.Mock()
self.controller._get_project.side_effect = self._get_project
self._commit_quota_reservation() self._commit_quota_reservation()
body = {'quota_set': {'volumes': 0}, body = {'quota_set': {'volumes': 0},
'skip_validation': 'false'} 'skip_validation': 'false'}
@ -481,8 +303,6 @@ class QuotaSetsControllerTest(test.TestCase):
self.req, 'foo', body) self.req, 'foo', body)
def test_update_lower_than_existing_resources_when_skip_true(self): def test_update_lower_than_existing_resources_when_skip_true(self):
self.controller._get_project = mock.Mock()
self.controller._get_project.side_effect = self._get_project
self._commit_quota_reservation() self._commit_quota_reservation()
body = {'quota_set': {'volumes': 0}, body = {'quota_set': {'volumes': 0},
'skip_validation': 'true'} 'skip_validation': 'true'}
@ -491,8 +311,6 @@ class QuotaSetsControllerTest(test.TestCase):
result['quota_set']['volumes']) result['quota_set']['volumes'])
def test_update_lower_than_existing_resources_without_skip_argument(self): def test_update_lower_than_existing_resources_without_skip_argument(self):
self.controller._get_project = mock.Mock()
self.controller._get_project.side_effect = self._get_project
self._commit_quota_reservation() self._commit_quota_reservation()
body = {'quota_set': {'volumes': 0}} body = {'quota_set': {'volumes': 0}}
result = self.controller.update(self.req, 'foo', body) result = self.controller.update(self.req, 'foo', body)
@ -500,8 +318,6 @@ class QuotaSetsControllerTest(test.TestCase):
result['quota_set']['volumes']) result['quota_set']['volumes'])
def test_delete(self): def test_delete(self):
self.controller._get_project = mock.Mock()
self.controller._get_project.side_effect = self._get_project
result_show = self.controller.show(self.req, 'foo') result_show = self.controller.show(self.req, 'foo')
self.assertDictMatch(make_body(), result_show) self.assertDictMatch(make_body(), result_show)
@ -516,9 +332,7 @@ class QuotaSetsControllerTest(test.TestCase):
result_show_after = self.controller.show(self.req, 'foo') result_show_after = self.controller.show(self.req, 'foo')
self.assertDictMatch(result_show, result_show_after) self.assertDictMatch(result_show, result_show_after)
def test_subproject_delete(self): def test_delete_with_allocated_quota_different_from_zero(self):
self.controller._get_project = mock.Mock()
self.controller._get_project.side_effect = self._get_project
self.req.environ['cinder.context'].project_id = self.A.id self.req.environ['cinder.context'].project_id = self.A.id
body = make_body(gigabytes=2000, snapshots=15, body = make_body(gigabytes=2000, snapshots=15,
@ -539,6 +353,365 @@ class QuotaSetsControllerTest(test.TestCase):
result_show_after = self.controller.show(self.req, self.A.id) result_show_after = self.controller.show(self.req, self.A.id)
self.assertDictMatch(result_show, result_show_after) self.assertDictMatch(result_show, result_show_after)
def test_delete_no_admin(self):
self.req.environ['cinder.context'].is_admin = False
self.assertRaises(webob.exc.HTTPForbidden, self.controller.delete,
self.req, 'foo')
def test_subproject_show_not_using_nested_quotas(self):
# Current roles say for non-nested quotas, an admin should be able to
# see anyones quota
self.req.environ['cinder.context'].project_id = self.B.id
self.controller.show(self.req, self.C.id)
self.controller.show(self.req, self.A.id)
class QuotaSetControllerValidateNestedQuotaSetup(QuotaSetsControllerTestBase):
"""Validates the setup before using NestedQuota driver.
Test case validates flipping on NestedQuota driver after using the
non-nested quota driver for some time.
"""
def _create_project_hierarchy(self):
"""Sets an environment used for nested quotas tests.
Create a project hierarchy such as follows:
+-----------------+
| |
| A G E |
| / \ \ |
| B C F |
| / |
| D |
+-----------------+
"""
super(QuotaSetControllerValidateNestedQuotaSetup,
self)._create_project_hierarchy()
# Project A, B, C, D are already defined by parent test class
self.E = self.FakeProject(id=uuid.uuid4().hex, parent_id=None)
self.F = self.FakeProject(id=uuid.uuid4().hex, parent_id=self.E.id)
self.G = self.FakeProject(id=uuid.uuid4().hex, parent_id=None)
self.E.subtree = {self.F.id: self.F.subtree}
self.project_by_id.update({self.E.id: self.E, self.F.id: self.F,
self.G.id: self.G})
def test_validate_nested_quotas_no_in_use_vols(self):
# Update the project A quota.
self.req.environ['cinder.context'].project_id = self.A.id
quota = {'volumes': 5}
body = {'quota_set': quota}
self.controller.update(self.req, self.A.id, body)
quota['volumes'] = 3
self.controller.update(self.req, self.B.id, body)
# Allocated value for quota A is borked, because update was done
# without nested quota driver
self.assertRaises(webob.exc.HTTPBadRequest,
self.controller.validate_setup_for_nested_quota_use,
self.req)
# Fix the allocated values in DB
self.req.params['fix_allocated_quotas'] = True
self.controller.validate_setup_for_nested_quota_use(
self.req)
self.req.params['fix_allocated_quotas'] = False
# Ensure that we've properly fixed the allocated quotas
self.controller.validate_setup_for_nested_quota_use(self.req)
# Over-allocate the quotas between children
self.controller.update(self.req, self.C.id, body)
# This is we should fail because the child limits are too big
self.assertRaises(webob.exc.HTTPBadRequest,
self.controller.validate_setup_for_nested_quota_use,
self.req)
quota['volumes'] = 1
self.controller.update(self.req, self.C.id, body)
# Make sure we're validating all hierarchy trees
self.req.environ['cinder.context'].project_id = self.E.id
quota['volumes'] = 1
self.controller.update(self.req, self.E.id, body)
quota['volumes'] = 3
self.controller.update(self.req, self.F.id, body)
self.assertRaises(
webob.exc.HTTPBadRequest,
self.controller.validate_setup_for_nested_quota_use,
self.req)
# Put quotas in a good state
quota['volumes'] = 1
self.controller.update(self.req, self.F.id, body)
self.req.params['fix_allocated_quotas'] = True
self.controller.validate_setup_for_nested_quota_use(self.req)
def _fake_quota_usage_get_all_by_project(self, context, project_id):
proj_vals = {
self.A.id: {'in_use': 1},
self.B.id: {'in_use': 1},
self.D.id: {'in_use': 0},
self.C.id: {'in_use': 3},
self.E.id: {'in_use': 0},
self.F.id: {'in_use': 0},
self.G.id: {'in_use': 0},
}
return {'volumes': proj_vals[project_id]}
@mock.patch('cinder.db.quota_usage_get_all_by_project')
def test_validate_nested_quotas_in_use_vols(self, mock_usage):
mock_usage.side_effect = self._fake_quota_usage_get_all_by_project
# Update the project A quota.
self.req.environ['cinder.context'].project_id = self.A.id
quota_limit = {'volumes': 7}
body = {'quota_set': quota_limit}
self.controller.update(self.req, self.A.id, body)
quota_limit['volumes'] = 3
self.controller.update(self.req, self.B.id, body)
quota_limit['volumes'] = 3
self.controller.update(self.req, self.C.id, body)
self.req.params['fix_allocated_quotas'] = True
self.controller.validate_setup_for_nested_quota_use(self.req)
quota_limit['volumes'] = 6
self.controller.update(self.req, self.A.id, body)
# Should fail because the one in_use volume of 'A'
self.assertRaises(
webob.exc.HTTPBadRequest,
self.controller.validate_setup_for_nested_quota_use,
self.req)
@mock.patch('cinder.db.quota_usage_get_all_by_project')
def test_validate_nested_quotas_quota_borked(self, mock_usage):
mock_usage.side_effect = self._fake_quota_usage_get_all_by_project
# Update the project A quota.
self.req.environ['cinder.context'].project_id = self.A.id
quota_limit = {'volumes': 7}
body = {'quota_set': quota_limit}
self.controller.update(self.req, self.A.id, body)
# Other quotas would default to 0 but already have some limit being
# used
self.assertRaises(
webob.exc.HTTPBadRequest,
self.controller.validate_setup_for_nested_quota_use,
self.req)
def test_validate_nested_quota_negative_limits(self):
# When we're validating, update the allocated values since we've
# been updating child limits
self.req.params['fix_allocated_quotas'] = True
self.controller.validate_setup_for_nested_quota_use(self.req)
# Update the project A quota.
self.req.environ['cinder.context'].project_id = self.A.id
quota_limit = {'volumes': -1}
body = {'quota_set': quota_limit}
self.controller.update(self.req, self.A.id, body)
quota_limit['volumes'] = 4
self.controller.update(self.req, self.B.id, body)
self.controller.validate_setup_for_nested_quota_use(self.req)
quota_limit['volumes'] = -1
self.controller.update(self.req, self.F.id, body)
# Should not work because can't have a child with negative limits
self.assertRaises(
webob.exc.HTTPBadRequest,
self.controller.validate_setup_for_nested_quota_use,
self.req)
class QuotaSetsControllerNestedQuotasTest(QuotaSetsControllerTestBase):
def setUp(self):
super(QuotaSetsControllerNestedQuotasTest, self).setUp()
fixture = self.useFixture(config_fixture.Config(quota.CONF))
fixture.config(quota_driver="cinder.quota.NestedDbQuotaDriver")
quotas.QUOTAS = quota.VolumeTypeQuotaEngine()
self.controller = quotas.QuotaSetsController()
def test_subproject_defaults(self):
context = self.req.environ['cinder.context']
context.project_id = self.B.id
result = self.controller.defaults(self.req, self.B.id)
expected = make_subproject_body(tenant_id=self.B.id)
self.assertDictMatch(expected, result)
def test_subproject_show(self):
self.req.environ['cinder.context'].project_id = self.A.id
result = self.controller.show(self.req, self.B.id)
expected = make_subproject_body(tenant_id=self.B.id)
self.assertDictMatch(expected, result)
def test_subproject_show_in_hierarchy(self):
# A user scoped to a root project in a hierarchy can see its children
# quotas.
self.req.environ['cinder.context'].project_id = self.A.id
result = self.controller.show(self.req, self.D.id)
expected = make_subproject_body(tenant_id=self.D.id)
self.assertDictMatch(expected, result)
# A user scoped to a parent project can see its immediate children
# quotas.
self.req.environ['cinder.context'].project_id = self.B.id
result = self.controller.show(self.req, self.D.id)
expected = make_subproject_body(tenant_id=self.D.id)
self.assertDictMatch(expected, result)
def test_subproject_show_target_project_equals_to_context_project(
self):
self.req.environ['cinder.context'].project_id = self.B.id
result = self.controller.show(self.req, self.B.id)
expected = make_subproject_body(tenant_id=self.B.id)
self.assertDictMatch(expected, result)
def test_subproject_show_not_authorized(self):
self.req.environ['cinder.context'].project_id = self.B.id
self.assertRaises(webob.exc.HTTPForbidden, self.controller.show,
self.req, self.C.id)
self.req.environ['cinder.context'].project_id = self.B.id
self.assertRaises(webob.exc.HTTPForbidden, self.controller.show,
self.req, self.A.id)
def test_update_subproject_not_in_hierarchy(self):
# Create another project hierarchy
E = self.FakeProject(id=uuid.uuid4().hex, parent_id=None)
F = self.FakeProject(id=uuid.uuid4().hex, parent_id=E.id)
E.subtree = {F.id: F.subtree}
self.project_by_id[E.id] = E
self.project_by_id[F.id] = F
# Update the project A quota.
self.req.environ['cinder.context'].project_id = self.A.id
body = make_body(gigabytes=2000, snapshots=15,
volumes=5, backups=5, tenant_id=None)
result = self.controller.update(self.req, self.A.id, body)
self.assertDictMatch(body, result)
# Try to update the quota of F, it will not be allowed, since the
# project E doesn't belongs to the project hierarchy of A.
self.req.environ['cinder.context'].project_id = self.A.id
body = make_body(gigabytes=2000, snapshots=15,
volumes=5, backups=5, tenant_id=None)
self.assertRaises(webob.exc.HTTPForbidden,
self.controller.update, self.req, F.id, body)
def test_update_subproject(self):
# Update the project A quota.
self.req.environ['cinder.context'].project_id = self.A.id
body = make_body(gigabytes=2000, snapshots=15,
volumes=5, backups=5, tenant_id=None)
result = self.controller.update(self.req, self.A.id, body)
self.assertDictMatch(body, result)
# Update the quota of B to be equal to its parent quota
self.req.environ['cinder.context'].project_id = self.A.id
body = make_body(gigabytes=2000, snapshots=15,
volumes=5, backups=5, tenant_id=None, is_child=True)
result = self.controller.update(self.req, self.B.id, body)
self.assertDictMatch(body, result)
# Try to update the quota of C, it will not be allowed, since the
# project A doesn't have free quota available.
self.req.environ['cinder.context'].project_id = self.A.id
body = make_body(gigabytes=2000, snapshots=15,
volumes=5, backups=5, tenant_id=None, is_child=True)
self.assertRaises(webob.exc.HTTPBadRequest, self.controller.update,
self.req, self.C.id, body)
# Successfully update the quota of D.
self.req.environ['cinder.context'].project_id = self.A.id
body = make_body(gigabytes=1000, snapshots=7,
volumes=3, backups=3, tenant_id=None, is_child=True)
result = self.controller.update(self.req, self.D.id, body)
self.assertDictMatch(body, result)
# An admin of B can also update the quota of D, since D is its
# immediate child.
self.req.environ['cinder.context'].project_id = self.B.id
body = make_body(gigabytes=1500, snapshots=10,
volumes=4, backups=4, tenant_id=None, is_child=True)
self.controller.update(self.req, self.D.id, body)
def test_update_subproject_negative_limit(self):
# Should not be able to set a negative limit for a child project (will
# require further fixes to allow for this)
self.req.environ['cinder.context'].project_id = self.A.id
body = make_body(volumes=-1, is_child=True)
self.assertRaises(webob.exc.HTTPBadRequest,
self.controller.update, self.req, self.B.id, body)
def test_update_subproject_repetitive(self):
# Update the project A volumes quota.
self.req.environ['cinder.context'].project_id = self.A.id
body = make_body(gigabytes=2000, snapshots=15,
volumes=10, backups=5, tenant_id=None)
result = self.controller.update(self.req, self.A.id, body)
self.assertDictMatch(body, result)
# Update the quota of B to be equal to its parent quota
# three times should be successful, the quota will not be
# allocated to 'allocated' value of parent project
for i in range(0, 3):
self.req.environ['cinder.context'].project_id = self.A.id
body = make_body(gigabytes=2000, snapshots=15,
volumes=10, backups=5, tenant_id=None,
is_child=True)
result = self.controller.update(self.req, self.B.id, body)
self.assertDictMatch(body, result)
def test_update_subproject_with_not_root_context_project(self):
# Update the project A quota.
self.req.environ['cinder.context'].project_id = self.A.id
body = make_body(gigabytes=2000, snapshots=15,
volumes=5, backups=5, tenant_id=None)
result = self.controller.update(self.req, self.A.id, body)
self.assertDictMatch(body, result)
# Try to update the quota of B, it will not be allowed, since the
# project in the context (B) is not a root project.
self.req.environ['cinder.context'].project_id = self.B.id
body = make_body(gigabytes=2000, snapshots=15,
volumes=5, backups=5, tenant_id=None)
self.assertRaises(webob.exc.HTTPForbidden, self.controller.update,
self.req, self.B.id, body)
def test_update_subproject_quota_when_parent_has_default_quotas(self):
# Since the quotas of the project A were not updated, it will have
# default quotas.
self.req.environ['cinder.context'].project_id = self.A.id
# Update the project B quota.
expected = make_body(gigabytes=1000, snapshots=10,
volumes=5, backups=5, tenant_id=None,
is_child=True)
result = self.controller.update(self.req, self.B.id, expected)
self.assertDictMatch(expected, result)
def test_subproject_delete(self):
self.req.environ['cinder.context'].project_id = self.A.id
body = make_body(gigabytes=2000, snapshots=15,
volumes=5, backups=5,
backup_gigabytes=1000, tenant_id=None, is_child=True)
result_update = self.controller.update(self.req, self.A.id, body)
self.assertDictMatch(body, result_update)
# Set usage param to True in order to see get allocated values.
self.req.params = {'usage': 'True'}
result_show = self.controller.show(self.req, self.A.id)
result_update = self.controller.update(self.req, self.B.id, body)
self.assertDictMatch(body, result_update)
self.controller.delete(self.req, self.B.id)
result_show_after = self.controller.show(self.req, self.A.id)
self.assertDictMatch(result_show, result_show_after)
def test_subproject_delete_not_considering_default_quotas(self): def test_subproject_delete_not_considering_default_quotas(self):
"""Test delete subprojects' quotas won't consider default quotas. """Test delete subprojects' quotas won't consider default quotas.
@ -551,8 +724,6 @@ class QuotaSetsControllerTest(test.TestCase):
updating the allocated values of the parent project. Thus, the delete updating the allocated values of the parent project. Thus, the delete
operation should succeed. operation should succeed.
""" """
self.controller._get_project = mock.Mock()
self.controller._get_project.side_effect = self._get_project
self.req.environ['cinder.context'].project_id = self.A.id self.req.environ['cinder.context'].project_id = self.A.id
body = {'quota_set': {'volumes': 5}} body = {'quota_set': {'volumes': 5}}
@ -567,35 +738,20 @@ class QuotaSetsControllerTest(test.TestCase):
self.controller.delete(self.req, self.B.id) self.controller.delete(self.req, self.B.id)
def test_delete_with_allocated_quota_different_from_zero(self): def test_subproject_delete_with_child_present(self):
self.controller._get_project = mock.Mock() # Update the project A quota.
self.controller._get_project.side_effect = self._get_project
self.req.environ['cinder.context'].project_id = self.A.id self.req.environ['cinder.context'].project_id = self.A.id
body = make_body(volumes=5)
self.controller.update(self.req, self.A.id, body)
body = make_body(gigabytes=2000, snapshots=15, # Allocate some of that quota to a child project
volumes=5, backups=5, body = make_body(volumes=3, is_child=True)
backup_gigabytes=1000, tenant_id=None) self.controller.update(self.req, self.B.id, body)
result_update = self.controller.update(self.req, self.A.id, body)
self.assertDictMatch(body, result_update)
# Set usage param to True in order to see get allocated values. # Deleting 'A' should be disallowed since 'B' is using some of that
self.req.params = {'usage': 'True'} # quota
result_show = self.controller.show(self.req, self.A.id) self.assertRaises(webob.exc.HTTPBadRequest, self.controller.delete,
self.req, self.A.id)
result_update = self.controller.update(self.req, self.B.id, body)
self.assertDictMatch(body, result_update)
self.controller.delete(self.req, self.B.id)
result_show_after = self.controller.show(self.req, self.A.id)
self.assertDictMatch(result_show, result_show_after)
def test_delete_no_admin(self):
self.controller._get_project = mock.Mock()
self.controller._get_project.side_effect = self._get_project
self.req.environ['cinder.context'].is_admin = False
self.assertRaises(webob.exc.HTTPForbidden, self.controller.delete,
self.req, 'foo')
class QuotaSerializerTest(test.TestCase): class QuotaSerializerTest(test.TestCase):

View File

@ -20,6 +20,7 @@ import datetime
import mock import mock
from oslo_config import cfg from oslo_config import cfg
from oslo_config import fixture as config_fixture
from oslo_utils import timeutils from oslo_utils import timeutils
import six import six
@ -37,6 +38,8 @@ from cinder import test
import cinder.tests.unit.image.fake import cinder.tests.unit.image.fake
from cinder import volume from cinder import volume
from keystonemiddleware import auth_token
CONF = cfg.CONF CONF = cfg.CONF
@ -358,11 +361,9 @@ class FakeDriver(object):
return resources return resources
def get_project_quotas(self, context, resources, project_id, def get_project_quotas(self, context, resources, project_id,
quota_class=None, defaults=True, usages=True, quota_class=None, defaults=True, usages=True):
parent_project_id=None):
self.called.append(('get_project_quotas', context, resources, self.called.append(('get_project_quotas', context, resources,
project_id, quota_class, defaults, usages, project_id, quota_class, defaults, usages))
parent_project_id))
return resources return resources
def limit_check(self, context, resources, values, project_id=None): def limit_check(self, context, resources, values, project_id=None):
@ -613,7 +614,6 @@ class QuotaEngineTestCase(test.TestCase):
def test_get_project_quotas(self): def test_get_project_quotas(self):
context = FakeContext(None, None) context = FakeContext(None, None)
driver = FakeDriver() driver = FakeDriver()
parent_project_id = None
quota_obj = self._make_quota_obj(driver) quota_obj = self._make_quota_obj(driver)
result1 = quota_obj.get_project_quotas(context, 'test_project') result1 = quota_obj.get_project_quotas(context, 'test_project')
result2 = quota_obj.get_project_quotas(context, 'test_project', result2 = quota_obj.get_project_quotas(context, 'test_project',
@ -628,33 +628,26 @@ class QuotaEngineTestCase(test.TestCase):
'test_project', 'test_project',
None, None,
True, True,
True, True),
parent_project_id),
('get_project_quotas', ('get_project_quotas',
context, context,
quota_obj.resources, quota_obj.resources,
'test_project', 'test_project',
'test_class', 'test_class',
False, False,
False, False), ], driver.called)
parent_project_id), ], driver.called)
self.assertEqual(quota_obj.resources, result1) self.assertEqual(quota_obj.resources, result1)
self.assertEqual(quota_obj.resources, result2) self.assertEqual(quota_obj.resources, result2)
def test_get_subproject_quotas(self): def test_get_subproject_quotas(self):
context = FakeContext(None, None) context = FakeContext(None, None)
driver = FakeDriver() driver = FakeDriver()
parent_project_id = 'test_parent_project_id'
quota_obj = self._make_quota_obj(driver) quota_obj = self._make_quota_obj(driver)
result1 = quota_obj.get_project_quotas(context, 'test_project', result1 = quota_obj.get_project_quotas(context, 'test_project')
parent_project_id=
parent_project_id)
result2 = quota_obj.get_project_quotas(context, 'test_project', result2 = quota_obj.get_project_quotas(context, 'test_project',
quota_class='test_class', quota_class='test_class',
defaults=False, defaults=False,
usages=False, usages=False)
parent_project_id=
parent_project_id)
self.assertEqual([ self.assertEqual([
('get_project_quotas', ('get_project_quotas',
@ -663,16 +656,14 @@ class QuotaEngineTestCase(test.TestCase):
'test_project', 'test_project',
None, None,
True, True,
True, True),
parent_project_id),
('get_project_quotas', ('get_project_quotas',
context, context,
quota_obj.resources, quota_obj.resources,
'test_project', 'test_project',
'test_class', 'test_class',
False, False,
False, False), ], driver.called)
parent_project_id), ], driver.called)
self.assertEqual(quota_obj.resources, result1) self.assertEqual(quota_obj.resources, result1)
self.assertEqual(quota_obj.resources, result2) self.assertEqual(quota_obj.resources, result2)
@ -886,9 +877,9 @@ class VolumeTypeQuotaEngineTestCase(test.TestCase):
db.volume_type_destroy(ctx, vtype2['id']) db.volume_type_destroy(ctx, vtype2['id'])
class DbQuotaDriverTestCase(test.TestCase): class DbQuotaDriverBaseTestCase(test.TestCase):
def setUp(self): def setUp(self):
super(DbQuotaDriverTestCase, self).setUp() super(DbQuotaDriverBaseTestCase, self).setUp()
self.flags(quota_volumes=10, self.flags(quota_volumes=10,
quota_snapshots=10, quota_snapshots=10,
@ -900,7 +891,21 @@ class DbQuotaDriverTestCase(test.TestCase):
max_age=0, max_age=0,
) )
self.driver = quota.DbQuotaDriver() # These can be used for expected defaults for child/non-child
self._default_quotas_non_child = dict(
volumes=10,
snapshots=10,
gigabytes=1000,
backups=10,
backup_gigabytes=1000,
per_volume_gigabytes=-1)
self._default_quotas_child = dict(
volumes=0,
snapshots=0,
gigabytes=0,
backups=0,
backup_gigabytes=0,
per_volume_gigabytes=0)
self.calls = [] self.calls = []
@ -909,38 +914,6 @@ class DbQuotaDriverTestCase(test.TestCase):
self.mock_utcnow = patcher.start() self.mock_utcnow = patcher.start()
self.mock_utcnow.return_value = datetime.datetime.utcnow() self.mock_utcnow.return_value = datetime.datetime.utcnow()
def test_get_defaults(self):
# Use our pre-defined resources
self._stub_quota_class_get_default()
self._stub_volume_type_get_all()
result = self.driver.get_defaults(None, quota.QUOTAS.resources)
self.assertEqual(
dict(
volumes=10,
snapshots=10,
gigabytes=1000,
backups=10,
backup_gigabytes=1000,
per_volume_gigabytes=-1), result)
def test_subproject_get_defaults(self):
# Test subproject default values.
self._stub_volume_type_get_all()
parent_project_id = 'test_parent_project_id'
result = self.driver.get_defaults(None,
quota.QUOTAS.resources,
parent_project_id)
self.assertEqual(
dict(
volumes=0,
snapshots=0,
gigabytes=0,
backups=0,
backup_gigabytes=0,
per_volume_gigabytes=0), result)
def _stub_quota_class_get_default(self): def _stub_quota_class_get_default(self):
# Stub out quota_class_get_default # Stub out quota_class_get_default
def fake_qcgd(context): def fake_qcgd(context):
@ -967,6 +940,37 @@ class DbQuotaDriverTestCase(test.TestCase):
backup_gigabytes=500) backup_gigabytes=500)
self.stubs.Set(db, 'quota_class_get_all_by_name', fake_qcgabn) self.stubs.Set(db, 'quota_class_get_all_by_name', fake_qcgabn)
def _stub_allocated_get_all_by_project(self, allocated_quota=False):
def fake_qagabp(context, project_id):
self.calls.append('quota_allocated_get_all_by_project')
if allocated_quota:
return dict(project_id=project_id, volumes=3)
return dict(project_id=project_id)
self.stubs.Set(db, 'quota_allocated_get_all_by_project', fake_qagabp)
class DbQuotaDriverTestCase(DbQuotaDriverBaseTestCase):
def setUp(self):
super(DbQuotaDriverTestCase, self).setUp()
self.driver = quota.DbQuotaDriver()
def test_get_defaults(self):
# Use our pre-defined resources
self._stub_quota_class_get_default()
self._stub_volume_type_get_all()
result = self.driver.get_defaults(None, quota.QUOTAS.resources)
self.assertEqual(
dict(
volumes=10,
snapshots=10,
gigabytes=1000,
backups=10,
backup_gigabytes=1000,
per_volume_gigabytes=-1), result)
def test_get_class_quotas(self): def test_get_class_quotas(self):
self._stub_quota_class_get_all_by_name() self._stub_quota_class_get_all_by_name()
self._stub_volume_type_get_all() self._stub_volume_type_get_all()
@ -1017,33 +1021,6 @@ class DbQuotaDriverTestCase(test.TestCase):
self._stub_quota_class_get_all_by_name() self._stub_quota_class_get_all_by_name()
self._stub_quota_class_get_default() self._stub_quota_class_get_default()
def _stub_get_by_subproject(self):
def fake_qgabp(context, project_id):
self.calls.append('quota_get_all_by_project')
self.assertEqual('test_project', project_id)
return dict(volumes=10, gigabytes=50, reserved=0)
def fake_qugabp(context, project_id):
self.calls.append('quota_usage_get_all_by_project')
self.assertEqual('test_project', project_id)
return dict(volumes=dict(in_use=2, reserved=0),
gigabytes=dict(in_use=10, reserved=0))
self.stubs.Set(db, 'quota_get_all_by_project', fake_qgabp)
self.stubs.Set(db, 'quota_usage_get_all_by_project', fake_qugabp)
self._stub_quota_class_get_all_by_name()
def _stub_allocated_get_all_by_project(self, allocated_quota=False):
def fake_qagabp(context, project_id):
self.calls.append('quota_allocated_get_all_by_project')
self.assertEqual('test_project', project_id)
if allocated_quota:
return dict(project_id=project_id, volumes=3)
return dict(project_id=project_id)
self.stubs.Set(db, 'quota_allocated_get_all_by_project', fake_qagabp)
def test_get_project_quotas(self): def test_get_project_quotas(self):
self._stub_get_by_project() self._stub_get_by_project()
self._stub_volume_type_get_all() self._stub_volume_type_get_all()
@ -1115,45 +1092,6 @@ class DbQuotaDriverTestCase(test.TestCase):
allocated=0) allocated=0)
), result) ), result)
def test_get_subproject_quotas(self):
self._stub_get_by_subproject()
self._stub_volume_type_get_all()
self._stub_allocated_get_all_by_project(allocated_quota=True)
parent_project_id = 'test_parent_project_id'
result = self.driver.get_project_quotas(
FakeContext('test_project', None),
quota.QUOTAS.resources, 'test_project',
parent_project_id=parent_project_id)
self.assertEqual(['quota_get_all_by_project',
'quota_usage_get_all_by_project',
'quota_allocated_get_all_by_project', ], self.calls)
self.assertEqual(dict(volumes=dict(limit=10,
in_use=2,
reserved=0,
allocated=3, ),
snapshots=dict(limit=0,
in_use=0,
reserved=0,
allocated=0, ),
gigabytes=dict(limit=50,
in_use=10,
reserved=0,
allocated=0, ),
backups=dict(limit=0,
in_use=0,
reserved=0,
allocated=0, ),
backup_gigabytes=dict(limit=0,
in_use=0,
reserved=0,
allocated=0, ),
per_volume_gigabytes=dict(in_use=0,
limit=0,
reserved=0,
allocated=0)
), result)
def test_get_project_quotas_alt_context_no_class(self): def test_get_project_quotas_alt_context_no_class(self):
self._stub_get_by_project() self._stub_get_by_project()
self._stub_volume_type_get_all() self._stub_volume_type_get_all()
@ -1423,6 +1361,231 @@ class DbQuotaDriverTestCase(test.TestCase):
self.calls) self.calls)
class NestedDbQuotaDriverBaseTestCase(DbQuotaDriverBaseTestCase):
def setUp(self):
super(NestedDbQuotaDriverBaseTestCase, self).setUp()
self.context = context.RequestContext('user_id',
'project_id',
is_admin=True,
auth_token="fake_token")
self.auth_url = 'http://localhost:5000'
self._child_proj_id = 'child_id'
self._non_child_proj_id = 'non_child_id'
keystone_mock = mock.Mock()
keystone_mock.version = 'v3'
class FakeProject(object):
def __init__(self, parent_id):
self.parent_id = parent_id
def fake_get_project(project_id, subtree_as_ids=False):
# Enable imitation of projects with and without parents
if project_id == self._child_proj_id:
return FakeProject('parent_id')
else:
return FakeProject(None)
keystone_mock.projects.get.side_effect = fake_get_project
def _keystone_mock(self):
return keystone_mock
keystone_patcher = mock.patch('cinder.quota_utils._keystone_client',
_keystone_mock)
keystone_patcher.start()
self.addCleanup(keystone_patcher.stop)
self.fixture = self.useFixture(config_fixture.Config(auth_token.CONF))
self.fixture.config(auth_uri=self.auth_url, group='keystone_authtoken')
self.driver = quota.NestedDbQuotaDriver()
def _stub_get_by_subproject(self):
def fake_qgabp(context, project_id):
self.calls.append('quota_get_all_by_project')
return dict(volumes=10, gigabytes=50, reserved=0)
def fake_qugabp(context, project_id):
self.calls.append('quota_usage_get_all_by_project')
return dict(volumes=dict(in_use=2, reserved=0),
gigabytes=dict(in_use=10, reserved=0))
self.stubs.Set(db, 'quota_get_all_by_project', fake_qgabp)
self.stubs.Set(db, 'quota_usage_get_all_by_project', fake_qugabp)
self._stub_quota_class_get_all_by_name()
class NestedDbQuotaDriverTestCase(NestedDbQuotaDriverBaseTestCase):
def test_get_defaults(self):
self._stub_volume_type_get_all()
# Test for child project defaults
result = self.driver.get_defaults(self.context,
quota.QUOTAS.resources,
self._child_proj_id)
self.assertEqual(self._default_quotas_child, result)
# Test for non-child project defaults
result = self.driver.get_defaults(self.context,
quota.QUOTAS.resources,
self._non_child_proj_id)
self.assertEqual(self._default_quotas_non_child, result)
def test_subproject_enforce_defaults(self):
# Non-child defaults should allow volume to get created
self.driver.reserve(self.context,
quota.QUOTAS.resources,
{'volumes': 1, 'gigabytes': 1},
project_id=self._non_child_proj_id)
# Child defaults should not allow volume to be created
self.assertRaises(exception.OverQuota,
self.driver.reserve, self.context,
quota.QUOTAS.resources,
{'volumes': 1, 'gigabytes': 1},
project_id=self._child_proj_id)
def test_get_subproject_quotas(self):
self._stub_get_by_subproject()
self._stub_volume_type_get_all()
self._stub_allocated_get_all_by_project(allocated_quota=True)
result = self.driver.get_project_quotas(
self.context,
quota.QUOTAS.resources, self._child_proj_id)
self.assertEqual(['quota_get_all_by_project',
'quota_usage_get_all_by_project',
'quota_allocated_get_all_by_project', ], self.calls)
self.assertEqual(dict(volumes=dict(limit=10,
in_use=2,
reserved=0,
allocated=3, ),
snapshots=dict(limit=0,
in_use=0,
reserved=0,
allocated=0, ),
gigabytes=dict(limit=50,
in_use=10,
reserved=0,
allocated=0, ),
backups=dict(limit=0,
in_use=0,
reserved=0,
allocated=0, ),
backup_gigabytes=dict(limit=0,
in_use=0,
reserved=0,
allocated=0, ),
per_volume_gigabytes=dict(in_use=0,
limit=0,
reserved=0,
allocated=0)
), result)
class NestedQuotaValidation(NestedDbQuotaDriverBaseTestCase):
def setUp(self):
super(NestedQuotaValidation, self).setUp()
"""
Quota hierarchy setup like so
+-----------+
| |
| A |
| / \ |
| B C |
| / |
| D |
+-----------+
"""
self.project_tree = {'A': {'B': {'D': None}, 'C': None}}
self.proj_vals = {
'A': {'limit': 7, 'in_use': 1, 'alloc': 6},
'B': {'limit': 3, 'in_use': 1, 'alloc': 2},
'D': {'limit': 2, 'in_use': 0},
'C': {'limit': 3, 'in_use': 3},
}
# Just using one resource currently for simplicity of test
self.resources = {'volumes': quota.ReservableResource(
'volumes', '_sync_volumes', 'quota_volumes')}
to_patch = [('cinder.db.quota_allocated_get_all_by_project',
self._fake_quota_allocated_get_all_by_project),
('cinder.db.quota_get_all_by_project',
self._fake_quota_get_all_by_project),
('cinder.db.quota_usage_get_all_by_project',
self._fake_quota_usage_get_all_by_project)]
for patch_path, patch_obj in to_patch:
patcher = mock.patch(patch_path, patch_obj)
patcher.start()
self.addCleanup(patcher.stop)
def _fake_quota_get_all_by_project(self, context, project_id):
return {'volumes': self.proj_vals[project_id]['limit']}
def _fake_quota_usage_get_all_by_project(self, context, project_id):
return {'volumes': self.proj_vals[project_id]}
def _fake_quota_allocated_get_all_by_project(self, context, project_id):
ret = {'project_id': project_id}
proj_val = self.proj_vals[project_id]
if 'alloc' in proj_val:
ret['volumes'] = proj_val['alloc']
return ret
def test_validate_nested_quotas(self):
self.driver.validate_nested_setup(self.context,
self.resources, self.project_tree)
# Fail because 7 - 2 < 3 + 3
self.proj_vals['A']['in_use'] = 2
self.assertRaises(exception.InvalidNestedQuotaSetup,
self.driver.validate_nested_setup,
self.context,
self.resources, self.project_tree)
self.proj_vals['A']['in_use'] = 1
# Fail because 7 - 1 < 3 + 7
self.proj_vals['C']['limit'] = 7
self.assertRaises(exception.InvalidNestedQuotaSetup,
self.driver.validate_nested_setup,
self.context,
self.resources, self.project_tree)
self.proj_vals['C']['limit'] = 3
# Fail because 3 < 4
self.proj_vals['D']['limit'] = 4
self.assertRaises(exception.InvalidNestedQuotaSetup,
self.driver.validate_nested_setup,
self.context,
self.resources, self.project_tree)
self.proj_vals['D']['limit'] = 2
def test_validate_nested_quotas_negative_child_limit(self):
self.proj_vals['B']['limit'] = -1
self.assertRaises(
exception.InvalidNestedQuotaSetup,
self.driver.validate_nested_setup,
self.context, self.resources, self.project_tree)
def test_validate_nested_quotas_usage_over_limit(self):
self.proj_vals['D']['in_use'] = 5
self.assertRaises(exception.InvalidNestedQuotaSetup,
self.driver.validate_nested_setup,
self.context, self.resources, self.project_tree)
def test_validate_nested_quota_bad_allocated_quotas(self):
self.proj_vals['A']['alloc'] = 5
self.proj_vals['B']['alloc'] = 8
self.assertRaises(exception.InvalidNestedQuotaSetup,
self.driver.validate_nested_setup,
self.context, self.resources, self.project_tree)
class FakeSession(object): class FakeSession(object):
def begin(self): def begin(self):
return self return self

View File

@ -0,0 +1,110 @@
# Copyright 2016 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.
import mock
from cinder import context
from cinder import exception
from cinder import quota_utils
from cinder import test
from keystoneclient import exceptions
from keystonemiddleware import auth_token
from oslo_config import cfg
from oslo_config import fixture as config_fixture
CONF = cfg.CONF
class QuotaUtilsTest(test.TestCase):
class FakeProject(object):
def __init__(self, id='foo', parent_id=None):
self.id = id
self.parent_id = parent_id
self.subtree = None
def setUp(self):
super(QuotaUtilsTest, self).setUp()
self.auth_url = 'http://localhost:5000'
self.context = context.RequestContext('fake_user', 'fake_proj_id')
self.fixture = self.useFixture(config_fixture.Config(auth_token.CONF))
self.fixture.config(auth_uri=self.auth_url, group='keystone_authtoken')
@mock.patch('keystoneclient.client.Client')
@mock.patch('keystoneclient.session.Session')
def test_keystone_client_instantiation(self, ksclient_session,
ksclient_class):
quota_utils._keystone_client(self.context)
ksclient_class.assert_called_once_with(auth_url=self.auth_url,
session=ksclient_session(),
version=(3, 0))
@mock.patch('keystoneclient.client.Client')
def test_get_project_keystoneclient_v2(self, ksclient_class):
keystoneclient = ksclient_class.return_value
keystoneclient.version = 'v2.0'
expected_project = quota_utils.GenericProjectInfo(
self.context.project_id, 'v2.0')
project = quota_utils.get_project_hierarchy(
self.context, self.context.project_id)
self.assertEqual(expected_project.__dict__, project.__dict__)
@mock.patch('keystoneclient.client.Client')
def test_get_project_keystoneclient_v3(self, ksclient_class):
keystoneclient = ksclient_class.return_value
keystoneclient.version = 'v3'
returned_project = self.FakeProject(self.context.project_id, 'bar')
del returned_project.subtree
keystoneclient.projects.get.return_value = returned_project
expected_project = quota_utils.GenericProjectInfo(
self.context.project_id, 'v3', 'bar')
project = quota_utils.get_project_hierarchy(
self.context, self.context.project_id)
self.assertEqual(expected_project.__dict__, project.__dict__)
@mock.patch('keystoneclient.client.Client')
def test_get_project_keystoneclient_v3_with_subtree(self, ksclient_class):
keystoneclient = ksclient_class.return_value
keystoneclient.version = 'v3'
returned_project = self.FakeProject(self.context.project_id, 'bar')
subtree_dict = {'baz': {'quux': None}}
returned_project.subtree = subtree_dict
keystoneclient.projects.get.return_value = returned_project
expected_project = quota_utils.GenericProjectInfo(
self.context.project_id, 'v3', 'bar', subtree_dict)
project = quota_utils.get_project_hierarchy(
self.context, self.context.project_id, subtree_as_ids=True)
keystoneclient.projects.get.assert_called_once_with(
self.context.project_id, subtree_as_ids=True)
self.assertEqual(expected_project.__dict__, project.__dict__)
@mock.patch('cinder.quota_utils._keystone_client')
def test_validate_nested_projects_with_keystone_v2(self, _keystone_client):
_keystone_client.side_effect = exceptions.VersionNotAvailable
self.assertRaises(exception.CinderException,
quota_utils.validate_setup_for_nested_quota_use,
self.context, [], None)
@mock.patch('cinder.quota_utils._keystone_client')
def test_validate_nested_projects_non_cloud_admin(self, _keystone_client):
# Covers not cloud admin or using old policy.json
_keystone_client.side_effect = exceptions.Forbidden
self.assertRaises(exception.CinderException,
quota_utils.validate_setup_for_nested_quota_use,
self.context, [], None)

View File

@ -39,6 +39,7 @@
"volume_extension:quotas:update": "rule:admin_api", "volume_extension:quotas:update": "rule:admin_api",
"volume_extension:quotas:delete": "rule:admin_api", "volume_extension:quotas:delete": "rule:admin_api",
"volume_extension:quota_classes": "rule:admin_api", "volume_extension:quota_classes": "rule:admin_api",
"volume_extension:quota_classes:validate_setup_for_nested_quota_use": "rule: admin_api",
"volume_extension:volume_admin_actions:reset_status": "rule:admin_api", "volume_extension:volume_admin_actions:reset_status": "rule:admin_api",
"volume_extension:snapshot_admin_actions:reset_status": "rule:admin_api", "volume_extension:snapshot_admin_actions:reset_status": "rule:admin_api",

View File

@ -0,0 +1,12 @@
---
features:
- Split nested quota support into a separate driver. In
order to use nested quotas, change the following config
``quota_driver = cinder.quota.NestedDbQuotaDriver`` after
running the following admin API
"os-quota-sets/validate_setup_for_nested_quota_use" command
to ensure the existing quota values make sense to nest.
upgrade:
- Nested quotas will no longer be used by default, but can be
configured by setting
``quota_driver = cinder.quota.NestedDbQuotaDriver``