7ebd4904b9
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
204 lines
8.0 KiB
Python
204 lines
8.0 KiB
Python
# Copyright 2013 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 webob
|
|
|
|
from oslo_config import cfg
|
|
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.i18n import _, _LW
|
|
|
|
CONF = cfg.CONF
|
|
|
|
|
|
LOG = logging.getLogger(__name__)
|
|
|
|
|
|
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,
|
|
reserve_vol_type_only=False):
|
|
from cinder import quota
|
|
QUOTAS = quota.QUOTAS
|
|
# Reserve quotas for the given volume type
|
|
try:
|
|
reserve_opts = {'volumes': 1, 'gigabytes': volume['size']}
|
|
QUOTAS.add_volume_type_opts(ctxt,
|
|
reserve_opts,
|
|
type_id)
|
|
# If reserve_vol_type_only is True, just reserve volume_type quota,
|
|
# not volume quota.
|
|
if reserve_vol_type_only:
|
|
reserve_opts.pop('volumes')
|
|
reserve_opts.pop('gigabytes')
|
|
# Note that usually the project_id on the volume will be the same as
|
|
# the project_id in the context. But, if they are different then the
|
|
# reservations must be recorded against the project_id that owns the
|
|
# volume.
|
|
project_id = volume['project_id']
|
|
reservations = QUOTAS.reserve(ctxt,
|
|
project_id=project_id,
|
|
**reserve_opts)
|
|
except exception.OverQuota as e:
|
|
overs = e.kwargs['overs']
|
|
usages = e.kwargs['usages']
|
|
quotas = e.kwargs['quotas']
|
|
|
|
def _consumed(name):
|
|
return (usages[name]['reserved'] + usages[name]['in_use'])
|
|
|
|
for over in overs:
|
|
if 'gigabytes' in over:
|
|
s_size = volume['size']
|
|
d_quota = quotas[over]
|
|
d_consumed = _consumed(over)
|
|
LOG.warning(
|
|
_LW("Quota exceeded for %(s_pid)s, tried to create "
|
|
"%(s_size)sG volume - (%(d_consumed)dG of "
|
|
"%(d_quota)dG already consumed)"),
|
|
{'s_pid': ctxt.project_id,
|
|
's_size': s_size,
|
|
'd_consumed': d_consumed,
|
|
'd_quota': d_quota})
|
|
raise exception.VolumeSizeExceedsAvailableQuota(
|
|
requested=s_size, quota=d_quota, consumed=d_consumed)
|
|
elif 'volumes' in over:
|
|
LOG.warning(
|
|
_LW("Quota exceeded for %(s_pid)s, tried to create "
|
|
"volume (%(d_consumed)d volumes "
|
|
"already consumed)"),
|
|
{'s_pid': ctxt.project_id,
|
|
'd_consumed': _consumed(over)})
|
|
raise exception.VolumeLimitExceeded(
|
|
allowed=quotas[over])
|
|
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)
|