You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1615 lines
67 KiB
1615 lines
67 KiB
# Copyright 2015 Huawei Technologies Co., Ltd.
|
|
# 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.
|
|
|
|
"""
|
|
Routines for configuring tricircle, copy and modify from Cinder
|
|
"""
|
|
|
|
import datetime
|
|
import six
|
|
|
|
from oslo_config import cfg
|
|
import oslo_log.log as logging
|
|
from oslo_log import versionutils
|
|
from oslo_utils import importutils
|
|
from oslo_utils import timeutils
|
|
|
|
from keystoneclient import exceptions as k_exceptions
|
|
|
|
from tricircle.common import client
|
|
from tricircle.common import constants as cons
|
|
from tricircle.common import exceptions as t_exceptions
|
|
from tricircle.common.i18n import _
|
|
from tricircle.common.i18n import _LE
|
|
from tricircle.common import utils
|
|
from tricircle.db import api as db_api
|
|
|
|
quota_opts = [
|
|
cfg.IntOpt('quota_instances',
|
|
default=10,
|
|
help='Number of instances allowed per project'),
|
|
cfg.IntOpt('quota_cores',
|
|
default=20,
|
|
help='Number of instance cores allowed per project'),
|
|
cfg.IntOpt('quota_ram',
|
|
default=50 * 1024,
|
|
help='Megabytes of instance RAM allowed per project'),
|
|
cfg.IntOpt('quota_floating_ips',
|
|
default=10,
|
|
help='Number of floating IPs allowed per project'),
|
|
cfg.IntOpt('quota_fixed_ips',
|
|
default=-1,
|
|
help='Number of fixed IPs allowed per project (this should be '
|
|
'at least the number of instances allowed)'),
|
|
cfg.IntOpt('quota_metadata_items',
|
|
default=128,
|
|
help='Number of metadata items allowed per instance'),
|
|
cfg.IntOpt('quota_injected_files',
|
|
default=5,
|
|
help='Number of injected files allowed'),
|
|
cfg.IntOpt('quota_injected_file_content_bytes',
|
|
default=10 * 1024,
|
|
help='Number of bytes allowed per injected file'),
|
|
cfg.IntOpt('quota_injected_file_path_length',
|
|
default=255,
|
|
help='Length of injected file path'),
|
|
cfg.IntOpt('quota_security_groups',
|
|
default=10,
|
|
help='Number of security groups per project'),
|
|
cfg.IntOpt('quota_security_group_rules',
|
|
default=20,
|
|
help='Number of security rules per security group'),
|
|
cfg.IntOpt('quota_key_pairs',
|
|
default=100,
|
|
help='Number of key pairs per user'),
|
|
cfg.IntOpt('quota_server_groups',
|
|
default=10,
|
|
help='Number of server groups per project'),
|
|
cfg.IntOpt('quota_server_group_members',
|
|
default=10,
|
|
help='Number of servers per server group'),
|
|
|
|
cfg.IntOpt('quota_volumes',
|
|
default=10,
|
|
help='Number of volumes allowed per project'),
|
|
cfg.IntOpt('quota_snapshots',
|
|
default=10,
|
|
help='Number of volume snapshots allowed per project'),
|
|
cfg.IntOpt('quota_consistencygroups',
|
|
default=10,
|
|
help='Number of consistencygroups allowed per project'),
|
|
cfg.IntOpt('quota_gigabytes',
|
|
default=1000,
|
|
help='Total amount of storage, in gigabytes, allowed '
|
|
'for volumes and snapshots per project'),
|
|
cfg.IntOpt('quota_backups',
|
|
default=10,
|
|
help='Number of volume backups allowed per project'),
|
|
cfg.IntOpt('quota_backup_gigabytes',
|
|
default=1000,
|
|
help='Total amount of storage, in gigabytes, allowed '
|
|
'for backups per project'),
|
|
cfg.IntOpt('per_volume_size_limit',
|
|
default=-1,
|
|
help='Max size allowed per volume, in gigabytes'),
|
|
|
|
cfg.IntOpt('reservation_expire',
|
|
default=86400,
|
|
help='Number of seconds until a reservation expires'),
|
|
cfg.IntOpt('until_refresh',
|
|
default=0,
|
|
help='Count of reservations until usage is refreshed. This '
|
|
'defaults to 0(off) to avoid additional load but it is '
|
|
'useful to turn on to help keep quota usage up to date '
|
|
'and reduce the impact of out of sync usage issues.'),
|
|
cfg.IntOpt('max_age',
|
|
default=0,
|
|
help='Number of seconds between subsequent usage refreshes. '
|
|
'This defaults to 0(off) to avoid additional load but it '
|
|
'is useful to turn on to help keep quota usage up to date '
|
|
'and reduce the impact of out of sync usage issues. '
|
|
'Note that quotas are not updated on a periodic task, '
|
|
'they will update on a new reservation if max_age has '
|
|
'passed since the last reservation'),
|
|
cfg.StrOpt('quota_driver',
|
|
default='tricircle.common.quota.DbQuotaDriver',
|
|
help='Default driver to use for quota checks'),
|
|
cfg.BoolOpt('use_default_quota_class',
|
|
default=True,
|
|
help='Enables or disables use of default quota class '
|
|
'with default quota.'), ]
|
|
|
|
LOG = logging.getLogger(__name__)
|
|
|
|
CONF = cfg.CONF
|
|
quota_group = cfg.OptGroup(name='quota', title='quota defaults options')
|
|
cfg.CONF.register_group(quota_group)
|
|
CONF.register_opts(quota_opts, quota_group)
|
|
|
|
NON_QUOTA_KEYS = ['tenant_id', 'id']
|
|
DEFAULT_PROJECT = 'default'
|
|
|
|
|
|
class BaseResource(object):
|
|
"""Describe a single resource for quota checking."""
|
|
|
|
def __init__(self, name, flag=None, parent_project_id=None):
|
|
"""Initializes a Resource.
|
|
|
|
:param name: The name of the resource, i.e., "volumes".
|
|
:param flag: The name of the flag or configuration option
|
|
which specifies the default value of the quota
|
|
for this resource.
|
|
:param parent_project_id: The id of the current project's parent,
|
|
if any.
|
|
"""
|
|
|
|
self.name = name
|
|
self.flag = flag
|
|
self.parent_project_id = parent_project_id
|
|
|
|
def quota(self, driver, context, **kwargs):
|
|
"""Given a driver and context, obtain the quota for this resource.
|
|
|
|
:param driver: A quota driver.
|
|
:param context: The request context.
|
|
:param project_id: The project to obtain the quota value for.
|
|
If not provided, it is taken from the
|
|
context. If it is given as None, no
|
|
project-specific quota will be searched
|
|
for.
|
|
:param quota_class: The quota class corresponding to the
|
|
project, or for which the quota is to be
|
|
looked up. If not provided, it is taken
|
|
from the context. If it is given as None,
|
|
no quota class-specific quota will be
|
|
searched for. Note that the quota class
|
|
defaults to the value in the context,
|
|
which may not correspond to the project if
|
|
project_id is not the same as the one in
|
|
the context.
|
|
"""
|
|
|
|
# Get the project ID
|
|
project_id = kwargs.get('project_id', context.project_id)
|
|
|
|
# Ditto for the quota class
|
|
quota_class = kwargs.get('quota_class', context.quota_class)
|
|
|
|
# Look up the quota for the project
|
|
if project_id:
|
|
try:
|
|
return driver.get_by_project(context, project_id, self.name)
|
|
except t_exceptions.ProjectQuotaNotFound:
|
|
pass
|
|
|
|
# Try for the quota class
|
|
if quota_class:
|
|
try:
|
|
return driver.get_by_class(context, quota_class, self.name)
|
|
except t_exceptions.QuotaClassNotFound:
|
|
pass
|
|
|
|
# OK, return the default
|
|
return driver.get_default(context, self,
|
|
parent_project_id=self.parent_project_id)
|
|
|
|
@property
|
|
def default(self):
|
|
"""Return the default value of the quota."""
|
|
|
|
if self.parent_project_id:
|
|
return 0
|
|
|
|
return CONF['quota'][self.flag] if self.flag else -1
|
|
|
|
|
|
class ReservableResource(BaseResource):
|
|
"""Describe a reservable resource."""
|
|
|
|
def __init__(self, name, sync, flag=None):
|
|
"""Initializes a ReservableResource.
|
|
|
|
Reservable resources are those resources which directly
|
|
correspond to objects in the database, i.e., volumes, gigabytes,
|
|
etc. A ReservableResource must be constructed with a usage
|
|
synchronization function, which will be called to determine the
|
|
current counts of one or more resources.
|
|
|
|
The usage synchronization function will be passed three
|
|
arguments: an admin context, the project ID, and an opaque
|
|
session object, which should in turn be passed to the
|
|
underlying database function. Synchronization functions
|
|
should return a dictionary mapping resource names to the
|
|
current in_use count for those resources; more than one
|
|
resource and resource count may be returned. Note that
|
|
synchronization functions may be associated with more than one
|
|
ReservableResource.
|
|
|
|
:param name: The name of the resource, i.e., "volumes".
|
|
:param sync: A dbapi methods name which returns a dictionary
|
|
to resynchronize the in_use count for one or more
|
|
resources, as described above.
|
|
:param flag: The name of the flag or configuration option
|
|
which specifies the default value of the quota
|
|
for this resource.
|
|
"""
|
|
|
|
super(ReservableResource, self).__init__(name, flag=flag)
|
|
if sync:
|
|
self.sync = sync
|
|
|
|
|
|
class AbsoluteResource(BaseResource):
|
|
"""Describe a non-reservable resource."""
|
|
|
|
pass
|
|
|
|
|
|
class CountableResource(AbsoluteResource):
|
|
"""Describe a resource where counts aren't based only on the project ID."""
|
|
|
|
def __init__(self, name, count, flag=None):
|
|
"""Initializes a CountableResource.
|
|
|
|
Countable resources are those resources which directly
|
|
correspond to objects in the database, i.e., volumes, gigabytes,
|
|
etc., but for which a count by project ID is inappropriate. A
|
|
CountableResource must be constructed with a counting
|
|
function, which will be called to determine the current counts
|
|
of the resource.
|
|
|
|
The counting function will be passed the context, along with
|
|
the extra positional and keyword arguments that are passed to
|
|
Quota.count(). It should return an integer specifying the
|
|
count.
|
|
|
|
Note that this counting is not performed in a transaction-safe
|
|
manner. This resource class is a temporary measure to provide
|
|
required functionality, until a better approach to solving
|
|
this problem can be evolved.
|
|
|
|
:param name: The name of the resource, i.e., "volumes".
|
|
:param count: A callable which returns the count of the
|
|
resource. The arguments passed are as described
|
|
above.
|
|
:param flag: The name of the flag or configuration option
|
|
which specifies the default value of the quota
|
|
for this resource.
|
|
"""
|
|
|
|
super(CountableResource, self).__init__(name, flag=flag)
|
|
self.count = count
|
|
|
|
|
|
# TODO(joehuang) support volume_type based quota in the future
|
|
|
|
class VolumeTypeResource(ReservableResource):
|
|
"""ReservableResource for a specific volume type."""
|
|
|
|
def __init__(self, part_name, volume_type):
|
|
"""Initializes a VolumeTypeResource.
|
|
|
|
:param part_name: The kind of resource, i.e., "volumes".
|
|
:param volume_type: The volume type for this resource.
|
|
"""
|
|
|
|
self.volume_type_name = volume_type['name']
|
|
self.volume_type_id = volume_type['id']
|
|
name = "%s_%s" % (part_name, self.volume_type_name)
|
|
super(VolumeTypeResource, self).__init__(name, "_sync_%s" % part_name)
|
|
|
|
|
|
class DbQuotaDriver(object):
|
|
|
|
"""Driver to perform check to enforcement of quotas.
|
|
|
|
Also allows to obtain quota information.
|
|
The default driver utilizes the local database.
|
|
"""
|
|
|
|
def get_by_project(self, context, project_id, resource_name):
|
|
"""Get a specific quota by project."""
|
|
|
|
return db_api.quota_get(context, project_id, resource_name)
|
|
|
|
def get_by_class(self, context, quota_class, resource_name):
|
|
"""Get a specific quota by quota class."""
|
|
|
|
return db_api.quota_class_get(context, quota_class, resource_name)
|
|
|
|
def get_default(self, context, resource, parent_project_id=None):
|
|
"""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_api.quota_class_get_default(context)
|
|
default_quota_value = 0 if parent_project_id else resource.default
|
|
return default_quotas.get(resource.name, default_quota_value)
|
|
|
|
def get_defaults(self, context, resources, parent_project_id=None):
|
|
"""Given a list of resources, retrieve the default quotas.
|
|
|
|
Use the class quotas named `_DEFAULT_QUOTA_NAME` as default quotas,
|
|
if it exists.
|
|
|
|
:param context: The request context, for access checks.
|
|
:param resources: A dictionary of the registered resources.
|
|
:param parent_project_id: The id of the current project's parent,
|
|
if any.
|
|
"""
|
|
|
|
quotas = {}
|
|
default_quotas = {}
|
|
if CONF.quota.use_default_quota_class and not parent_project_id:
|
|
default_quotas = db_api.quota_class_get_default(context)
|
|
|
|
for resource in resources.values():
|
|
if default_quotas:
|
|
if resource.name not in default_quotas:
|
|
versionutils.report_deprecated_feature(LOG, _(
|
|
"Default quota for resource: %(res)s is set "
|
|
"by the default quota flag: quota_%(res)s, "
|
|
"it is now deprecated. Please use the "
|
|
"default quota class for default "
|
|
"quota.") % {'res': resource.name})
|
|
quotas[resource.name] = default_quotas.get(resource.name,
|
|
(0 if parent_project_id
|
|
else resource.default))
|
|
return quotas
|
|
|
|
def get_class_quotas(self, context, resources, quota_class,
|
|
defaults=True):
|
|
"""Given list of resources, retrieve the quotas for given quota class.
|
|
|
|
:param context: The request context, for access checks.
|
|
:param resources: A dictionary of the registered resources.
|
|
:param quota_class: The name of the quota class to return
|
|
quotas for.
|
|
:param defaults: If True, the default value will be reported
|
|
if there is no specific value for the
|
|
resource.
|
|
"""
|
|
|
|
quotas = {}
|
|
default_quotas = {}
|
|
class_quotas = db_api.quota_class_get_all_by_name(context, quota_class)
|
|
if defaults:
|
|
default_quotas = db_api.quota_class_get_default(context)
|
|
for resource in resources.values():
|
|
if resource.name in class_quotas:
|
|
quotas[resource.name] = class_quotas[resource.name]
|
|
continue
|
|
|
|
if defaults:
|
|
quotas[resource.name] = default_quotas.get(resource.name,
|
|
resource.default)
|
|
|
|
return quotas
|
|
|
|
def get_project_quotas(self, context, resources, project_id,
|
|
quota_class=None, defaults=True,
|
|
usages=True, parent_project_id=None):
|
|
"""Retrieve quotas for a project.
|
|
|
|
Given a list of resources, retrieve the quotas for the given
|
|
project.
|
|
|
|
:param context: The request context, for access checks.
|
|
:param resources: A dictionary of the registered resources.
|
|
:param project_id: The ID of the project to return quotas for.
|
|
:param quota_class: If project_id != context.project_id, the
|
|
quota class cannot be determined. This
|
|
parameter allows it to be specified. It
|
|
will be ignored if project_id ==
|
|
context.project_id.
|
|
:param defaults: If True, the quota class value (or the
|
|
default value, if there is no value from the
|
|
quota class) will be reported if there is no
|
|
specific value for the resource.
|
|
:param usages: If True, the current in_use, reserved and allocated
|
|
counts will also be returned.
|
|
:param parent_project_id: The id of the current project's parent,
|
|
if any.
|
|
"""
|
|
|
|
quotas = {}
|
|
project_quotas = db_api.quota_get_all_by_project(context, project_id)
|
|
if usages:
|
|
project_usages = db_api.quota_usage_get_all_by_project(context,
|
|
project_id)
|
|
allocated_quotas = db_api.quota_allocated_get_all_by_project(
|
|
context, project_id)
|
|
allocated_quotas.pop('project_id')
|
|
|
|
# Get the quotas for the appropriate class. If the project ID
|
|
# matches the one in the context, we use the quota_class from
|
|
# the context, otherwise, we use the provided quota_class (if
|
|
# any)
|
|
if project_id == context.project_id:
|
|
quota_class = context.quota_class
|
|
if quota_class:
|
|
class_quotas = db_api.quota_class_get_all_by_name(context,
|
|
quota_class)
|
|
else:
|
|
class_quotas = {}
|
|
|
|
default_quotas = self.get_defaults(context, resources,
|
|
parent_project_id=parent_project_id)
|
|
|
|
for resource in resources.values():
|
|
# Omit default/quota class values
|
|
if not defaults and resource.name not in project_quotas:
|
|
continue
|
|
|
|
quotas[resource.name] = dict(
|
|
limit=project_quotas.get(
|
|
resource.name,
|
|
class_quotas.get(resource.name,
|
|
default_quotas[resource.name])),
|
|
)
|
|
|
|
# Include usages if desired. This is optional because one
|
|
# internal consumer of this interface wants to access the
|
|
# usages directly from inside a transaction.
|
|
if usages:
|
|
usage = project_usages.get(resource.name, {})
|
|
quotas[resource.name].update(
|
|
in_use=usage.get('in_use', 0),
|
|
reserved=usage.get('reserved', 0), )
|
|
|
|
if parent_project_id or allocated_quotas:
|
|
quotas[resource.name].update(
|
|
allocated=allocated_quotas.get(resource.name, 0), )
|
|
|
|
return quotas
|
|
|
|
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.
|
|
|
|
This specific resource is identified by keys, and which apply to the
|
|
current context.
|
|
|
|
:param context: The request context, for access checks.
|
|
:param resources: A dictionary of the registered resources.
|
|
:param keys: A list of the desired quotas to retrieve.
|
|
:param has_sync: If True, indicates that the resource must
|
|
have a sync attribute; if False, indicates
|
|
that the resource must NOT have a sync
|
|
attribute.
|
|
:param project_id: Specify the project_id if current context
|
|
is admin and admin wants to impact on
|
|
common user's tenant.
|
|
:param parent_project_id: The id of the current project's parent,
|
|
if any.
|
|
"""
|
|
|
|
# Filter resources
|
|
if has_sync:
|
|
sync_filt = lambda x: hasattr(x, 'sync')
|
|
else:
|
|
sync_filt = lambda x: not hasattr(x, 'sync')
|
|
desired = set(keys)
|
|
sub_resources = {k: v for k, v in resources.items()
|
|
if k in desired and sync_filt(v)}
|
|
|
|
# Make sure we accounted for all of them...
|
|
if len(keys) != len(sub_resources):
|
|
unknown = desired - set(sub_resources.keys())
|
|
raise t_exceptions.QuotaResourceUnknown(unknown=sorted(unknown))
|
|
|
|
# Grab and return the quotas (without usages)
|
|
quotas = self.get_project_quotas(context, sub_resources,
|
|
project_id,
|
|
context.quota_class, usages=False,
|
|
parent_project_id=parent_project_id)
|
|
|
|
return {k: v['limit'] for k, v in quotas.items()}
|
|
|
|
def limit_check(self, context, resources, values, project_id=None):
|
|
"""Check simple quota limits.
|
|
|
|
For limits--those quotas for which there is no usage
|
|
synchronization function--this method checks that a set of
|
|
proposed values are permitted by the limit restriction.
|
|
|
|
This method will raise a QuotaResourceUnknown exception if a
|
|
given resource is unknown or if it is not a simple limit
|
|
resource.
|
|
|
|
If any of the proposed values is over the defined quota, an
|
|
OverQuota exception will be raised with the sorted list of the
|
|
resources which are too high. Otherwise, the method returns
|
|
nothing.
|
|
|
|
:param context: The request context, for access checks.
|
|
:param resources: A dictionary of the registered resources.
|
|
:param values: A dictionary of the values to check against the
|
|
quota.
|
|
:param project_id: Specify the project_id if current context
|
|
is admin and admin wants to impact on
|
|
common user's tenant.
|
|
"""
|
|
|
|
# Ensure no value is less than zero
|
|
unders = [key for key, val in values.items() if val < 0]
|
|
if unders:
|
|
raise t_exceptions.InvalidQuotaValue(unders=sorted(unders))
|
|
|
|
# If project_id is None, then we use the project_id in context
|
|
if project_id is None:
|
|
project_id = context.project_id
|
|
|
|
# Get the applicable quotas
|
|
quotas = self._get_quotas(context, resources, values.keys(),
|
|
has_sync=False, project_id=project_id)
|
|
# Check the quotas and construct a list of the resources that
|
|
# would be put over limit by the desired values
|
|
overs = [key for key, val in values.items()
|
|
if quotas[key] >= 0 and quotas[key] < val]
|
|
if overs:
|
|
raise t_exceptions.OverQuota(overs=sorted(overs), quotas=quotas,
|
|
usages={})
|
|
|
|
def reserve(self, context, resources, deltas, expire=None,
|
|
project_id=None):
|
|
"""Check quotas and reserve resources.
|
|
|
|
For counting quotas--those quotas for which there is a usage
|
|
synchronization function--this method checks quotas against
|
|
current usage and the desired deltas.
|
|
|
|
This method will raise a QuotaResourceUnknown exception if a
|
|
given resource is unknown or if it does not have a usage
|
|
synchronization function.
|
|
|
|
If any of the proposed values is over the defined quota, an
|
|
OverQuota exception will be raised with the sorted list of the
|
|
resources which are too high. Otherwise, the method returns a
|
|
list of reservation UUIDs which were created.
|
|
|
|
:param context: The request context, for access checks.
|
|
:param resources: A dictionary of the registered resources.
|
|
:param deltas: A dictionary of the proposed delta changes.
|
|
:param expire: An optional parameter specifying an expiration
|
|
time for the reservations. If it is a simple
|
|
number, it is interpreted as a number of
|
|
seconds and added to the current time; if it is
|
|
a datetime.timedelta object, it will also be
|
|
added to the current time. A datetime.datetime
|
|
object will be interpreted as the absolute
|
|
expiration time. If None is specified, the
|
|
default expiration time set by
|
|
--default-reservation-expire will be used (this
|
|
value will be treated as a number of seconds).
|
|
:param project_id: Specify the project_id if current context
|
|
is admin and admin wants to impact on
|
|
common user's tenant.
|
|
"""
|
|
|
|
# Set up the reservation expiration
|
|
if expire is None:
|
|
expire = CONF.quota.reservation_expire
|
|
if isinstance(expire, six.integer_types):
|
|
expire = datetime.timedelta(seconds=expire)
|
|
if isinstance(expire, datetime.timedelta):
|
|
expire = timeutils.utcnow() + expire
|
|
if not isinstance(expire, datetime.datetime):
|
|
raise t_exceptions.InvalidReservationExpiration(expire=expire)
|
|
|
|
# If project_id is None, then we use the project_id in context
|
|
if project_id is None:
|
|
project_id = context.project_id
|
|
|
|
# Get the applicable quotas.
|
|
# NOTE(Vek): We're not worried about races at this point.
|
|
# Yes, the admin may be in the process of reducing
|
|
# quotas, but that's a pretty rare thing.
|
|
|
|
# NOTE(joehuang): in Tricircle, no embeded sync function here,
|
|
# so set has_sync=False.
|
|
quotas = self._get_quotas(context, resources, deltas.keys(),
|
|
has_sync=False, project_id=project_id)
|
|
|
|
# NOTE(Vek): Most of the work here has to be done in the DB
|
|
# API, because we have to do it in a transaction,
|
|
# which means access to the session. Since the
|
|
# session isn't available outside the DBAPI, we
|
|
# have to do the work there.
|
|
return db_api.quota_reserve(context, resources, quotas, deltas,
|
|
expire, CONF.quota.until_refresh,
|
|
CONF.quota.max_age,
|
|
project_id=project_id)
|
|
|
|
def commit(self, context, reservations, project_id=None):
|
|
"""Commit reservations.
|
|
|
|
:param context: The request context, for access checks.
|
|
:param reservations: A list of the reservation UUIDs, as
|
|
returned by the reserve() method.
|
|
:param project_id: Specify the project_id if current context
|
|
is admin and admin wants to impact on
|
|
common user's tenant.
|
|
"""
|
|
# If project_id is None, then we use the project_id in context
|
|
if project_id is None:
|
|
project_id = context.project_id
|
|
|
|
db_api.reservation_commit(context, reservations, project_id=project_id)
|
|
|
|
def rollback(self, context, reservations, project_id=None):
|
|
"""Roll back reservations.
|
|
|
|
:param context: The request context, for access checks.
|
|
:param reservations: A list of the reservation UUIDs, as
|
|
returned by the reserve() method.
|
|
:param project_id: Specify the project_id if current context
|
|
is admin and admin wants to impact on
|
|
common user's tenant.
|
|
"""
|
|
# If project_id is None, then we use the project_id in context
|
|
if project_id is None:
|
|
project_id = context.project_id
|
|
|
|
db_api.reservation_rollback(context, reservations,
|
|
project_id=project_id)
|
|
|
|
def destroy_by_project(self, context, project_id):
|
|
"""Destroy all limit quotas associated with a project.
|
|
|
|
Leave usage and reservation quotas intact.
|
|
|
|
:param context: The request context, for access checks.
|
|
:param project_id: The ID of the project being deleted.
|
|
"""
|
|
db_api.quota_destroy_by_project(context, project_id)
|
|
|
|
def expire(self, context):
|
|
"""Expire reservations.
|
|
|
|
Explores all currently existing reservations and rolls back
|
|
any that have expired.
|
|
|
|
:param context: The request context, for access checks.
|
|
"""
|
|
|
|
db_api.reservation_expire(context)
|
|
|
|
|
|
class QuotaEngine(object):
|
|
"""Represent the set of recognized quotas."""
|
|
|
|
def __init__(self, quota_driver_class=None):
|
|
"""Initialize a Quota object."""
|
|
|
|
if not quota_driver_class:
|
|
quota_driver_class = CONF.quota.quota_driver
|
|
|
|
if isinstance(quota_driver_class, six.string_types):
|
|
quota_driver_class = importutils.import_object(quota_driver_class)
|
|
|
|
self._resources = {}
|
|
self._driver = quota_driver_class
|
|
|
|
def __contains__(self, resource):
|
|
return resource in self.resources
|
|
|
|
def register_resource(self, resource):
|
|
"""Register a resource."""
|
|
|
|
self._resources[resource.name] = resource
|
|
|
|
def register_resources(self, resources):
|
|
"""Register a list of resources."""
|
|
|
|
for resource in resources:
|
|
self.register_resource(resource)
|
|
|
|
def get_by_project(self, context, project_id, resource_name):
|
|
"""Get a specific quota by project."""
|
|
|
|
return self._driver.get_by_project(context, project_id, resource_name)
|
|
|
|
def get_by_class(self, context, quota_class, resource_name):
|
|
"""Get a specific quota by quota class."""
|
|
|
|
return self._driver.get_by_class(context, quota_class, resource_name)
|
|
|
|
def get_default(self, context, resource, parent_project_id=None):
|
|
"""Get a specific default quota for a resource.
|
|
|
|
:param parent_project_id: The id of the current project's parent,
|
|
if any.
|
|
"""
|
|
|
|
return self._driver.get_default(context, resource,
|
|
parent_project_id=parent_project_id)
|
|
|
|
def get_defaults(self, context, parent_project_id=None):
|
|
"""Retrieve the default quotas.
|
|
|
|
:param context: The request context, for access checks.
|
|
:param parent_project_id: The id of the current project's parent,
|
|
if any.
|
|
"""
|
|
|
|
return self._driver.get_defaults(context, self.resources,
|
|
parent_project_id)
|
|
|
|
def get_class_quotas(self, context, quota_class, defaults=True):
|
|
"""Retrieve the quotas for the given quota class.
|
|
|
|
:param context: The request context, for access checks.
|
|
:param quota_class: The name of the quota class to return
|
|
quotas for.
|
|
:param defaults: If True, the default value will be reported
|
|
if there is no specific value for the
|
|
resource.
|
|
"""
|
|
|
|
return self._driver.get_class_quotas(context, self.resources,
|
|
quota_class, defaults=defaults)
|
|
|
|
def get_project_quotas(self, context, project_id, quota_class=None,
|
|
defaults=True, usages=True, parent_project_id=None):
|
|
"""Retrieve the quotas for the given project.
|
|
|
|
:param context: The request context, for access checks.
|
|
:param project_id: The ID of the project to return quotas for.
|
|
:param quota_class: If project_id != context.project_id, the
|
|
quota class cannot be determined. This
|
|
parameter allows it to be specified.
|
|
:param defaults: If True, the quota class value (or the
|
|
default value, if there is no value from the
|
|
quota class) will be reported if there is no
|
|
specific value for the resource.
|
|
:param usages: If True, the current in_use, reserved and
|
|
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,
|
|
project_id,
|
|
quota_class=quota_class,
|
|
defaults=defaults,
|
|
usages=usages,
|
|
parent_project_id=parent_project_id)
|
|
|
|
def count(self, context, resource, *args, **kwargs):
|
|
"""Count a resource.
|
|
|
|
For countable resources, invokes the count() function and
|
|
returns its result. Arguments following the context and
|
|
resource are passed directly to the count function declared by
|
|
the resource.
|
|
|
|
:param context: The request context, for access checks.
|
|
:param resource: The name of the resource, as a string.
|
|
"""
|
|
|
|
# Get the resource
|
|
res = self.resources.get(resource)
|
|
if not res or not hasattr(res, 'count'):
|
|
raise t_exceptions.QuotaResourceUnknown(unknown=[resource])
|
|
|
|
# TODO(joehuang): count will be calculated from bottom quota usage
|
|
if res.count:
|
|
return res.count(context, *args, **kwargs)
|
|
else:
|
|
return 0
|
|
|
|
def limit_check(self, context, project_id=None, **values):
|
|
"""Check simple quota limits.
|
|
|
|
For limits--those quotas for which there is no usage
|
|
synchronization function--this method checks that a set of
|
|
proposed values are permitted by the limit restriction. The
|
|
values to check are given as keyword arguments, where the key
|
|
identifies the specific quota limit to check, and the value is
|
|
the proposed value.
|
|
|
|
This method will raise a QuotaResourceUnknown exception if a
|
|
given resource is unknown or if it is not a simple limit
|
|
resource.
|
|
|
|
If any of the proposed values is over the defined quota, an
|
|
OverQuota exception will be raised with the sorted list of the
|
|
resources which are too high. Otherwise, the method returns
|
|
nothing.
|
|
|
|
:param context: The request context, for access checks.
|
|
:param project_id: Specify the project_id if current context
|
|
is admin and admin wants to impact on
|
|
common user's tenant.
|
|
"""
|
|
|
|
return self._driver.limit_check(context, self.resources, values,
|
|
project_id=project_id)
|
|
|
|
def reserve(self, context, expire=None, project_id=None, **deltas):
|
|
"""Check quotas and reserve resources.
|
|
|
|
For counting quotas--those quotas for which there is a usage
|
|
synchronization function--this method checks quotas against
|
|
current usage and the desired deltas. The deltas are given as
|
|
keyword arguments, and current usage and other reservations
|
|
are factored into the quota check.
|
|
|
|
This method will raise a QuotaResourceUnknown exception if a
|
|
given resource is unknown or if it does not have a usage
|
|
synchronization function.
|
|
|
|
If any of the proposed values is over the defined quota, an
|
|
OverQuota exception will be raised with the sorted list of the
|
|
resources which are too high. Otherwise, the method returns a
|
|
list of reservation UUIDs which were created.
|
|
|
|
:param context: The request context, for access checks.
|
|
:param expire: An optional parameter specifying an expiration
|
|
time for the reservations. If it is a simple
|
|
number, it is interpreted as a number of
|
|
seconds and added to the current time; if it is
|
|
a datetime.timedelta object, it will also be
|
|
added to the current time. A datetime.datetime
|
|
object will be interpreted as the absolute
|
|
expiration time. If None is specified, the
|
|
default expiration time set by
|
|
--default-reservation-expire will be used (this
|
|
value will be treated as a number of seconds).
|
|
:param project_id: Specify the project_id if current context
|
|
is admin and admin wants to impact on
|
|
common user's tenant.
|
|
"""
|
|
|
|
reservations = self._driver.reserve(context, self.resources, deltas,
|
|
expire=expire,
|
|
project_id=project_id)
|
|
|
|
LOG.debug("Created reservations %s", reservations)
|
|
|
|
return reservations
|
|
|
|
def commit(self, context, reservations, project_id=None):
|
|
"""Commit reservations.
|
|
|
|
:param context: The request context, for access checks.
|
|
:param reservations: A list of the reservation UUIDs, as
|
|
returned by the reserve() method.
|
|
:param project_id: Specify the project_id if current context
|
|
is admin and admin wants to impact on
|
|
common user's tenant.
|
|
"""
|
|
|
|
try:
|
|
self._driver.commit(context, reservations, project_id=project_id)
|
|
except Exception as e:
|
|
# NOTE(Vek): Ignoring exceptions here is safe, because the
|
|
# usage resynchronization and the reservation expiration
|
|
# mechanisms will resolve the issue. The exception is
|
|
# logged, however, because this is less than optimal.
|
|
|
|
msg = str(e)
|
|
LOG.exception(_LE("Failed to commit reservations"
|
|
" %(reservations)s, exception %(msg)s"),
|
|
{'reservations': reservations, 'msg': msg})
|
|
|
|
def rollback(self, context, reservations, project_id=None):
|
|
"""Roll back reservations.
|
|
|
|
:param context: The request context, for access checks.
|
|
:param reservations: A list of the reservation UUIDs, as
|
|
returned by the reserve() method.
|
|
:param project_id: Specify the project_id if current context
|
|
is admin and admin wants to impact on
|
|
common user's tenant.
|
|
"""
|
|
|
|
try:
|
|
self._driver.rollback(context, reservations, project_id=project_id)
|
|
except Exception as e:
|
|
# NOTE(Vek): Ignoring exceptions here is safe, because the
|
|
# usage resynchronization and the reservation expiration
|
|
# mechanisms will resolve the issue. The exception is
|
|
# logged, however, because this is less than optimal.
|
|
msg = str(e)
|
|
LOG.exception(_LE("Failed to roll back reservations"
|
|
" %(reservations)s, exception %(msg)s"),
|
|
{'reservations': reservations, 'msg': msg})
|
|
|
|
def destroy_by_project(self, context, project_id):
|
|
"""Destroy all quota limits associated with a project.
|
|
|
|
:param context: The request context, for access checks.
|
|
:param project_id: The ID of the project being deleted.
|
|
"""
|
|
|
|
self._driver.destroy_by_project(context, project_id)
|
|
|
|
def expire(self, context):
|
|
"""Expire reservations.
|
|
|
|
Explores all currently existing reservations and rolls back
|
|
any that have expired.
|
|
|
|
:param context: The request context, for access checks.
|
|
"""
|
|
|
|
self._driver.expire(context)
|
|
|
|
def add_volume_type_opts(self, context, opts, volume_type_id):
|
|
"""Add volume type resource options.
|
|
|
|
Adds elements to the opts hash for volume type quotas.
|
|
If a resource is being reserved ('gigabytes', etc) and the volume
|
|
type is set up for its own quotas, these reservations are copied
|
|
into keys for 'gigabytes_<volume type name>', etc.
|
|
|
|
:param context: The request context, for access checks.
|
|
:param opts: The reservations options hash.
|
|
:param volume_type_id: The volume type id for this reservation.
|
|
"""
|
|
if not volume_type_id:
|
|
return
|
|
|
|
# NOTE(jdg): set inactive to True in volume_type_get, as we
|
|
# may be operating on a volume that was created with a type
|
|
# that has since been deleted.
|
|
|
|
# quota based on volume_type is not supported currently
|
|
# volume_type = db_api.volume_type_get(context, volume_type_id, True)
|
|
|
|
# for quota in ('volumes', 'gigabytes', 'snapshots'):
|
|
# if quota in opts:
|
|
# vtype_quota = "%s_%s" % (quota, volume_type['name'])
|
|
# opts[vtype_quota] = opts[quota]
|
|
|
|
@property
|
|
def resource_names(self):
|
|
return sorted(self.resources.keys())
|
|
|
|
@property
|
|
def resources(self):
|
|
return self._resources
|
|
|
|
|
|
class AllQuotaEngine(QuotaEngine):
|
|
"""Represent the set of all quotas."""
|
|
|
|
@property
|
|
def resources(self):
|
|
"""Fetches all possible quota resources."""
|
|
|
|
result = {}
|
|
|
|
# Global quotas.
|
|
# Set sync_func to None for no sync function in Tricircle
|
|
reservable_argses = [
|
|
|
|
('instances', None, 'quota_instances'),
|
|
('cores', None, 'quota_cores'),
|
|
('ram', None, 'quota_ram'),
|
|
('security_groups', None, 'quota_security_groups'),
|
|
('floating_ips', None, 'quota_floating_ips'),
|
|
('fixed_ips', None, 'quota_fixed_ips'),
|
|
('server_groups', None, 'quota_server_groups'),
|
|
|
|
|
|
('volumes', None, 'quota_volumes'),
|
|
('per_volume_gigabytes', None, 'per_volume_size_limit'),
|
|
('snapshots', None, 'quota_snapshots'),
|
|
('gigabytes', None, 'quota_gigabytes'),
|
|
('backups', None, 'quota_backups'),
|
|
('backup_gigabytes', None, 'quota_backup_gigabytes'),
|
|
('consistencygroups', None, 'quota_consistencygroups')
|
|
]
|
|
|
|
absolute_argses = [
|
|
('metadata_items', 'quota_metadata_items'),
|
|
('injected_files', 'quota_injected_files'),
|
|
('injected_file_content_bytes',
|
|
'quota_injected_file_content_bytes'),
|
|
('injected_file_path_bytes',
|
|
'quota_injected_file_path_length'),
|
|
]
|
|
|
|
# TODO(joehuang), for countable, the count should be the
|
|
# value in the db but not 0 here
|
|
countable_argses = [
|
|
('security_group_rules', None, 'quota_security_group_rules'),
|
|
('key_pairs', None, 'quota_key_pairs'),
|
|
('server_group_members', None, 'quota_server_group_members'),
|
|
]
|
|
|
|
for args in reservable_argses:
|
|
resource = ReservableResource(*args)
|
|
result[resource.name] = resource
|
|
|
|
for args in absolute_argses:
|
|
resource = AbsoluteResource(*args)
|
|
result[resource.name] = resource
|
|
|
|
for args in countable_argses:
|
|
resource = CountableResource(*args)
|
|
result[resource.name] = resource
|
|
|
|
return result
|
|
|
|
def register_resource(self, resource):
|
|
raise NotImplementedError(_("Cannot register resource"))
|
|
|
|
def register_resources(self, resources):
|
|
raise NotImplementedError(_("Cannot register resources"))
|
|
|
|
|
|
QUOTAS = AllQuotaEngine()
|
|
|
|
|
|
class QuotaSetOperation(object):
|
|
"""Operation on Quota set."""
|
|
|
|
def __init__(self, target_tenant_id, user_id=None):
|
|
self.target_tenant_id = target_tenant_id
|
|
self.user_id = user_id
|
|
|
|
# used in test
|
|
def update_hierarchy(self, target_tenant_id, user_id=None):
|
|
self.target_tenant_id = target_tenant_id
|
|
self.user_id = user_id
|
|
|
|
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, tenant_id, quota_set):
|
|
"""Convert the quota object to a result dict."""
|
|
|
|
quota_set['id'] = str(tenant_id)
|
|
|
|
return dict(quota_set=quota_set)
|
|
|
|
def _keystone_client(self, context):
|
|
c = client.Client()
|
|
return c.get_keystone_client_by_context(context)
|
|
|
|
def _validate_existing_resource(self, key, value, quota_values):
|
|
if key == 'per_volume_gigabytes':
|
|
return
|
|
v = quota_values.get(key, {})
|
|
_usage = v.get('in_use', 0) + v.get('reserved', 0)
|
|
if value < _usage:
|
|
msg = _("Quota %(key)s limit %(value)d must be equal or "
|
|
"greater than existing resources"
|
|
"%(_usage)d.") % {'key': key, 'value': value,
|
|
'_usage': _usage}
|
|
LOG.error(msg=msg)
|
|
raise t_exceptions.ValidationError(msg=msg)
|
|
|
|
@staticmethod
|
|
def _validate_integer(value, name, min_value=None, max_value=None):
|
|
"""Make sure that value is a valid integer, potentially within range.
|
|
|
|
:param value: the value of the integer
|
|
:param name: the name of the integer
|
|
:param min_length: the min_length of the integer
|
|
:param max_length: the max_length of the integer
|
|
:returns: integer
|
|
"""
|
|
try:
|
|
value = int(value)
|
|
except (TypeError, ValueError, UnicodeEncodeError):
|
|
msg = _('%s must be an integer.') % name
|
|
LOG.error(msg=msg)
|
|
raise t_exceptions.ValidationError(msg=msg)
|
|
|
|
if min_value is not None and value < min_value:
|
|
msg = _('%(value_name)s must be >= '
|
|
'%(min_value)d') % {'value_name': name,
|
|
'min_value': min_value}
|
|
LOG.error(msg=msg)
|
|
raise t_exceptions.ValidationError(msg=msg)
|
|
|
|
if max_value is not None and value > max_value:
|
|
msg = _('%(value_name)s must be <= '
|
|
'%(max_value)d') % {'value_name': name,
|
|
'max_value': max_value}
|
|
LOG.error(msg=msg)
|
|
raise t_exceptions.ValidationError(msg=msg)
|
|
|
|
return value
|
|
|
|
def _validate_quota_limit(self, quota, key, project_quotas=None,
|
|
parent_project_quotas=None):
|
|
limit = self._validate_integer(quota[key], key, min_value=-1,
|
|
max_value=cons.MAX_INT)
|
|
|
|
if parent_project_quotas:
|
|
free_quota = (parent_project_quotas[key]['limit'] -
|
|
parent_project_quotas[key]['in_use'] -
|
|
parent_project_quotas[key]['reserved'] -
|
|
parent_project_quotas[key].get('allocated', 0))
|
|
|
|
current = 0
|
|
if project_quotas.get(key):
|
|
current = project_quotas[key]['limit']
|
|
|
|
if limit - current > free_quota:
|
|
msg = _("Free quota available is %s.") % free_quota
|
|
LOG.error(msg=msg)
|
|
raise t_exceptions.ValidationError(msg=msg)
|
|
|
|
return limit
|
|
|
|
def _get_quotas(self, context, id, usages=False, parent_project_id=None):
|
|
values = QUOTAS.get_project_quotas(
|
|
context, id, usages=usages,
|
|
parent_project_id=parent_project_id)
|
|
|
|
if usages:
|
|
return values
|
|
else:
|
|
return {k: v['limit'] for k, v in values.items()}
|
|
|
|
def _authorize_update_or_delete(self, context_project,
|
|
target_project_id,
|
|
parent_id):
|
|
"""Checks if update or delete are allowed in the current hierarchy.
|
|
|
|
With hierarchical projects, only the admin of the parent or the root
|
|
project has privilege to perform quota update and delete operations.
|
|
|
|
:param context_project: The project in which the user is scoped to.
|
|
:param target_project_id: The id of the project in which the
|
|
user want to perform an update or
|
|
delete operation.
|
|
:param parent_id: The parent id of the project in which the user
|
|
want to perform an update or delete operation.
|
|
"""
|
|
|
|
param_msg = _("context_project.parent_id = %(ctx_parent_id)s, "
|
|
"parent_id = %(parent_id)s, "
|
|
"context_project.id = %(ctx_project_id)s, "
|
|
"target_project_id = "
|
|
"%(target_project_id)s, ") % {
|
|
"ctx_parent_id": context_project.parent_id,
|
|
"parent_id": parent_id,
|
|
"ctx_project_id": context_project.id,
|
|
"target_project_id": target_project_id}
|
|
|
|
if context_project.parent_id and parent_id != context_project.id:
|
|
msg = _("Update and delete quota operations can only be made "
|
|
"by an admin of immediate parent or by the CLOUD admin."
|
|
"%s") % param_msg
|
|
|
|
LOG.error(msg=msg)
|
|
raise t_exceptions.HTTPForbiddenError(msg=msg)
|
|
|
|
if context_project.id != target_project_id:
|
|
if not self._is_descendant(target_project_id,
|
|
context_project.subtree):
|
|
msg = _("Update and delete quota operations can only be made "
|
|
"to projects in the same hierarchy of the project in "
|
|
"which users are scoped to."
|
|
"%s") % param_msg
|
|
LOG.error(msg=msg)
|
|
raise t_exceptions.HTTPForbiddenError(msg=msg)
|
|
else:
|
|
msg = _("Update and delete quota operations can only be made "
|
|
"by an admin of immediate parent or by the CLOUD admin."
|
|
"%s") % param_msg
|
|
LOG.error(msg=msg)
|
|
raise t_exceptions.HTTPForbiddenError(msg=msg)
|
|
|
|
def _authorize_show(self, context_project, target_project):
|
|
"""Checks if show is allowed in the current hierarchy.
|
|
|
|
With hierarchical projects, are allowed to perform quota show operation
|
|
users with admin role in, at least, one of the following projects: the
|
|
current project; the immediate parent project; or the root project.
|
|
|
|
:param context_project: The project in which the user
|
|
is scoped to.
|
|
:param target_project: The project in which the user wants
|
|
to perform a show operation.
|
|
"""
|
|
|
|
param_msg = _("target_project.parent_id = %(target_parent_id)s, "
|
|
"target_project_id = %(target_project_id)s, "
|
|
"context_project.id = %(ctx_project_id)s, "
|
|
"context_project.parent_id = %(ctx_parent_id)s, ") % {
|
|
"target_parent_id": target_project.parent_id,
|
|
"target_project_id": target_project.id,
|
|
"ctx_project_id": context_project.id,
|
|
"ctx_parent_id": context_project.parent_id}
|
|
|
|
if target_project.parent_id:
|
|
if target_project.id != context_project.id:
|
|
if not self._is_descendant(target_project.id,
|
|
context_project.subtree):
|
|
msg = _("Show operations can only be made to projects in "
|
|
"the same hierarchy of the project in which users "
|
|
"are scoped to."
|
|
"%s") % param_msg
|
|
LOG.error(msg=msg)
|
|
raise t_exceptions.HTTPForbiddenError(msg=msg)
|
|
|
|
if context_project.id != target_project.parent_id:
|
|
if context_project.parent_id:
|
|
msg = _("Only users with token scoped to immediate "
|
|
"parents or root projects are allowed to see "
|
|
"its children quotas."
|
|
"%s") % param_msg
|
|
LOG.error(msg=msg)
|
|
raise t_exceptions.HTTPForbiddenError(msg=msg)
|
|
|
|
elif context_project.parent_id:
|
|
msg = _("An user with a token scoped to a subproject is not "
|
|
"allowed to see the quota of its parents."
|
|
"%s") % param_msg
|
|
LOG.error(msg=msg)
|
|
raise t_exceptions.HTTPForbiddenError(msg=msg)
|
|
|
|
def _is_descendant(self, target_project_id, subtree):
|
|
if subtree is not None:
|
|
for key, value in subtree.items():
|
|
if key == target_project_id:
|
|
return True
|
|
if self._is_descendant(target_project_id, value):
|
|
return True
|
|
return False
|
|
|
|
def _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
|
|
|
|
# all projects in KeyStone will be put under the parent
|
|
# 'default' if not specifying the parent project id when
|
|
# creating project
|
|
if generic_project.parent_id == DEFAULT_PROJECT:
|
|
generic_project.parent_id = None
|
|
|
|
generic_project.subtree = (
|
|
project.subtree if subtree_as_ids else None)
|
|
|
|
except k_exceptions.NotFound:
|
|
msg = _("Tenant ID: %s does not exist.") % id
|
|
LOG.error(msg=msg)
|
|
raise t_exceptions.NotFound()
|
|
|
|
return generic_project
|
|
|
|
def update(self, context, **kw):
|
|
|
|
if not context.is_admin:
|
|
raise t_exceptions.AdminRequired
|
|
|
|
quota_set = kw.get('quota_set')
|
|
if not quota_set:
|
|
raise t_exceptions.InvalidInput(reason='no quota_set')
|
|
|
|
# TODO(joehuang): process is_force flag here
|
|
|
|
# Get the optional argument 'skip_validation' from body,
|
|
# if skip_validation is False, then validate existing resource.
|
|
skip_flag = kw.get('skip_validation', True)
|
|
if not utils.is_valid_boolstr(skip_flag):
|
|
msg = _("Invalid value '%s' for skip_validation.") % skip_flag
|
|
LOG.error(msg=msg)
|
|
raise t_exceptions.ValidationError(msg=msg)
|
|
|
|
skip_flag = utils.bool_from_string(skip_flag)
|
|
|
|
target_project_id = self.target_tenant_id
|
|
bad_keys = []
|
|
|
|
# NOTE(ankit): Pass #1 - In this loop for body['quota_set'].items(),
|
|
# we figure out if we have any bad keys.
|
|
for key, value in kw['quota_set'].items():
|
|
if (key not in QUOTAS and key not in NON_QUOTA_KEYS):
|
|
bad_keys.append(key)
|
|
continue
|
|
|
|
if len(bad_keys) > 0:
|
|
msg = _("Bad key(s) in quota set: %s") % ",".join(bad_keys)
|
|
LOG.error(msg=msg)
|
|
raise t_exceptions.ValidationError(msg=msg)
|
|
|
|
# Get the parent_id of the target project to verify whether we are
|
|
# dealing with hierarchical namespace or non-hierarchical namespace.
|
|
target_project = self._get_project(context, target_project_id)
|
|
parent_id = target_project.parent_id
|
|
|
|
context_project = self._get_project(context,
|
|
context.project_id,
|
|
subtree_as_ids=True)
|
|
if parent_id:
|
|
# Get the children of the project which the token is scoped to
|
|
# in order to know if the target_project is in its hierarchy.
|
|
self._authorize_update_or_delete(context_project,
|
|
target_project.id,
|
|
parent_id)
|
|
parent_project_quotas = QUOTAS.get_project_quotas(
|
|
context, parent_id)
|
|
|
|
else:
|
|
|
|
# if the target project has no parent and descendant, then
|
|
# the operation is allowed only if the context project also
|
|
# has no parent and descendant, that means only flat mode
|
|
# (current mode without hierarchy) is allowed
|
|
|
|
if not target_project.subtree and \
|
|
not context_project.parent_id and \
|
|
not context_project.subtree:
|
|
pass
|
|
elif context.project_id != target_project_id:
|
|
param_msg = _("context.project_id = %(ctx_project_id)s, "
|
|
"target_project_id = %(target_project_id)s ") % {
|
|
"ctx_project_id": context.project_id,
|
|
"target_project_id": target_project.id}
|
|
|
|
msg = _("Can not update quota for %s") % param_msg
|
|
LOG.error(msg=msg)
|
|
raise t_exceptions.HTTPForbiddenError(msg=msg)
|
|
|
|
# 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
|
|
# any of the items in the set is bad. Meanwhile we validate value
|
|
# to ensure that the value can't be lower than number of existing
|
|
# resources.
|
|
quota_values = QUOTAS.get_project_quotas(context, target_project_id,
|
|
defaults=False)
|
|
valid_quotas = {}
|
|
allocated_quotas = {}
|
|
for key in kw['quota_set'].keys():
|
|
if key in NON_QUOTA_KEYS:
|
|
continue
|
|
|
|
value = kw['quota_set'][key]
|
|
if not skip_flag:
|
|
self._validate_existing_resource(key, value, quota_values)
|
|
|
|
if parent_id:
|
|
value = self._validate_quota_limit(kw['quota_set'], key,
|
|
quota_values,
|
|
parent_project_quotas)
|
|
original_quota = 0
|
|
if quota_values.get(key):
|
|
original_quota = quota_values[key]['limit']
|
|
|
|
allocated_quotas[key] = (
|
|
parent_project_quotas[key].get('allocated', 0) + value -
|
|
original_quota)
|
|
else:
|
|
value = self._validate_quota_limit(kw['quota_set'], key)
|
|
valid_quotas[key] = value
|
|
|
|
# NOTE(ankit): Pass #3 - At this point we know that all the keys and
|
|
# values are valid and we can iterate and update them all in one shot
|
|
# without having to worry about rolling back etc as we have done
|
|
# the validation up front in the 2 loops above.
|
|
for key, value in valid_quotas.items():
|
|
try:
|
|
|
|
# TODO(joehuang) support quota by user
|
|
db_api.quota_update(context, target_project_id, key, value)
|
|
except t_exceptions.ProjectQuotaNotFound:
|
|
|
|
# TODO(joehuang) support quota by user
|
|
db_api.quota_create(context, target_project_id, key, value)
|
|
except t_exceptions.AdminRequired:
|
|
raise
|
|
|
|
# If hierarchical projects, update child's quota first
|
|
# and then parents quota. In future this needs to be an
|
|
# atomic operation.
|
|
if parent_id:
|
|
if key in allocated_quotas.keys():
|
|
try:
|
|
|
|
# TODO(joehuang) support quota by user
|
|
db_api.quota_allocated_update(context, parent_id, key,
|
|
allocated_quotas[key])
|
|
except t_exceptions.ProjectQuotaNotFound:
|
|
parent_limit = parent_project_quotas[key]['limit']
|
|
|
|
# TODO(joehuang) support quota by user
|
|
db_api.quota_create(context, parent_id, key,
|
|
parent_limit,
|
|
allocated=allocated_quotas[key])
|
|
|
|
return {'quota_set': self._get_quotas(context, target_project_id,
|
|
parent_project_id=parent_id)}
|
|
|
|
def delete(self, context):
|
|
"""Delete Quota for a particular tenant.
|
|
|
|
This works for hierarchical and non-hierarchical projects. For
|
|
hierarchical projects only immediate parent admin or the
|
|
CLOUD admin are able to perform a delete.
|
|
|
|
:param context: context for the request
|
|
:param target_tenant_id: target project id that needs to be deleted
|
|
"""
|
|
|
|
if not context.is_admin:
|
|
raise t_exceptions.AdminRequired
|
|
|
|
target_project_id = self.target_tenant_id
|
|
|
|
# Get the parent_id of the target project to verify whether we are
|
|
# dealing with hierarchical namespace or non-hierarchical namespace.
|
|
target_project = self._get_project(context, target_project_id)
|
|
parent_id = target_project.parent_id
|
|
|
|
# Get the children of the project which the token is scoped to in
|
|
# order to know if the target_project is in its hierarchy.
|
|
context_project = self._get_project(context,
|
|
context.project_id,
|
|
subtree_as_ids=True)
|
|
|
|
if parent_id:
|
|
self._authorize_update_or_delete(context_project,
|
|
target_project.id,
|
|
parent_id)
|
|
parent_project_quotas = QUOTAS.get_project_quotas(
|
|
context, parent_id, parent_project_id=parent_id)
|
|
|
|
else:
|
|
|
|
# if the target project has no parent and descendant, then
|
|
# the operation is allowed only if the context project also
|
|
# has no parent and descendant, that means only flat mode
|
|
# (current mode without hierarchy) is allowed
|
|
|
|
if not target_project.subtree and \
|
|
not context_project.parent_id and \
|
|
not context_project.subtree:
|
|
pass
|
|
elif context.project_id != target_project_id:
|
|
param_msg = _("context.project_id = %(ctx_project_id)s, "
|
|
"target_project_id = %(target_project_id)s ") % {
|
|
"ctx_project_id": context.project_id,
|
|
"target_project_id": target_project.id}
|
|
|
|
msg = _("Can not delete quota for %s") % param_msg
|
|
LOG.error(msg=msg)
|
|
raise t_exceptions.HTTPForbiddenError(msg=msg)
|
|
|
|
try:
|
|
project_quotas = QUOTAS.get_project_quotas(
|
|
context, target_project.id, usages=True,
|
|
parent_project_id=parent_id, defaults=False)
|
|
except t_exceptions.NotAuthorized:
|
|
msg = _("Not authorized to delete %s") % target_project_id
|
|
LOG.exception(msg)
|
|
raise
|
|
|
|
# If the project which is being deleted has allocated part of its
|
|
# quota to its sub_projects, then sub_projects' quotas should be
|
|
# deleted first
|
|
for key, value in project_quotas.items():
|
|
if 'allocated' in project_quotas[key].keys():
|
|
if project_quotas[key]['allocated'] != 0:
|
|
msg = _("About to delete child projects having "
|
|
"non-zero quota. This should not be performed"
|
|
" %s") % target_project_id
|
|
LOG.exception(msg)
|
|
raise t_exceptions.ChildQuotaNotZero
|
|
|
|
# Delete child quota first and later update parent's quota.
|
|
try:
|
|
# TODO(joehuang) support destroy quota by user
|
|
db_api.quota_destroy_by_project(context, target_project.id)
|
|
except t_exceptions.AdminRequired:
|
|
msg = _('Admin or tenant itself or parent tenant'
|
|
' required to delete quota'
|
|
' %s') % target_project.id
|
|
LOG.exception(msg)
|
|
raise
|
|
|
|
if parent_id:
|
|
# Update the allocated of the parent
|
|
for key, value in project_quotas.items():
|
|
project_hard_limit = project_quotas[key]['limit']
|
|
parent_allocated = parent_project_quotas[key]['allocated']
|
|
parent_allocated -= project_hard_limit
|
|
db_api.quota_allocated_update(context, parent_id, key,
|
|
parent_allocated)
|
|
|
|
def show_default_quota(self, context):
|
|
try:
|
|
project = self._get_project(context, self.target_tenant_id)
|
|
parent_id = project.parent_id
|
|
except k_exceptions.Forbidden:
|
|
# NOTE(e0ne): Keystone API v2 requires admin permissions for
|
|
# project_get method. We ignore Forbidden exception for
|
|
# non-admin users.
|
|
parent_id = self.target_tenant_id
|
|
|
|
return self._format_quota_set(self.target_tenant_id,
|
|
QUOTAS.get_defaults(
|
|
context,
|
|
parent_project_id=parent_id))
|
|
|
|
def show_detail_quota(self, context, show_usage=False):
|
|
"""Show quota for a particular tenant
|
|
|
|
This works for hierarchical and non-hierarchical projects. For
|
|
hierarchical projects admin of current project, immediate
|
|
parent of the project or the CLOUD admin are able to perform
|
|
a show.
|
|
|
|
:param context: request context
|
|
:param tenant_id: target project id that needs to be shown
|
|
:param params: whether to show usage
|
|
"""
|
|
target_project_id = self.target_tenant_id
|
|
usage = show_usage
|
|
|
|
try:
|
|
# With hierarchical projects, only the admin of the current
|
|
# project or the root project has privilege to perform quota show
|
|
# operations.
|
|
target_project = self._get_project(context, target_project_id)
|
|
context_project = self._get_project(context, context.project_id,
|
|
subtree_as_ids=True)
|
|
|
|
self._authorize_show(context_project, target_project)
|
|
parent_project_id = target_project.parent_id
|
|
except k_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:
|
|
db_api.authorize_project_context(context,
|
|
target_project_id)
|
|
except t_exceptions.NotAuthorized:
|
|
msg = _('Admin or tenant itself or parent tenant '
|
|
'required to show quota '
|
|
'tenant_id=%(tenant_id)s, '
|
|
'usage=%(usage)s') % {'tenant_id': target_project_id,
|
|
'usage': usage}
|
|
LOG.exception(msg)
|
|
raise
|
|
|
|
quotas = self._get_quotas(context, target_project_id, usage,
|
|
parent_project_id=parent_project_id)
|
|
return self._format_quota_set(target_project_id, quotas)
|