Quota management for Nova-APIGW and Cinder-APIGW(part1)
Port the models and db access code mostly from Cinder, for Cinder has implemented for hierarchy multi-tenancy quota management The quota management and control in Tricircle is described in the design doc: https://docs.google.com/document/d/18kZZ1snMOCD9IQvUKI5NVDzSASpw-QKj7l2zNqMEd3g/ BP: https://blueprints.launchpad.net/tricircle/+spec/implement-stateless Change-Id: I90d210f2154d8f68be49cc4fc4a62f2532cb5f92 Signed-off-by: Chaoyi Huang <joehuang@huawei.com>
This commit is contained in:
parent
81b45f2c1d
commit
1da8d2a473
@ -13,6 +13,8 @@
|
|||||||
# 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 copy
|
||||||
|
|
||||||
from pecan import request
|
from pecan import request
|
||||||
|
|
||||||
import oslo_context.context as oslo_ctx
|
import oslo_context.context as oslo_ctx
|
||||||
@ -119,3 +121,13 @@ class Context(ContextBase):
|
|||||||
if not self._session:
|
if not self._session:
|
||||||
self._session = core.get_session()
|
self._session = core.get_session()
|
||||||
return self._session
|
return self._session
|
||||||
|
|
||||||
|
def elevated(self, read_deleted=None, overwrite=False):
|
||||||
|
"""Return a version of this context with admin flag set."""
|
||||||
|
ctx = copy.copy(self)
|
||||||
|
ctx.is_admin = True
|
||||||
|
|
||||||
|
if read_deleted is not None:
|
||||||
|
ctx.read_deleted = read_deleted
|
||||||
|
|
||||||
|
return ctx
|
||||||
|
@ -56,7 +56,9 @@ class BadRequest(TricircleException):
|
|||||||
|
|
||||||
|
|
||||||
class NotFound(TricircleException):
|
class NotFound(TricircleException):
|
||||||
pass
|
message = _("Resource could not be found.")
|
||||||
|
code = 404
|
||||||
|
safe = True
|
||||||
|
|
||||||
|
|
||||||
class Conflict(TricircleException):
|
class Conflict(TricircleException):
|
||||||
@ -120,3 +122,45 @@ class ResourceNotSupported(TricircleException):
|
|||||||
def __init__(self, resource, method):
|
def __init__(self, resource, method):
|
||||||
super(ResourceNotSupported, self).__init__(resource=resource,
|
super(ResourceNotSupported, self).__init__(resource=resource,
|
||||||
method=method)
|
method=method)
|
||||||
|
|
||||||
|
|
||||||
|
class Invalid(TricircleException):
|
||||||
|
message = _("Unacceptable parameters.")
|
||||||
|
code = 400
|
||||||
|
|
||||||
|
|
||||||
|
class InvalidReservationExpiration(Invalid):
|
||||||
|
message = _("Invalid reservation expiration %(expire)s.")
|
||||||
|
|
||||||
|
|
||||||
|
class InvalidQuotaValue(Invalid):
|
||||||
|
message = _("Change would make usage less than 0 for the following "
|
||||||
|
"resources: %(unders)s")
|
||||||
|
|
||||||
|
|
||||||
|
class QuotaNotFound(NotFound):
|
||||||
|
message = _("Quota could not be found")
|
||||||
|
|
||||||
|
|
||||||
|
class QuotaResourceUnknown(QuotaNotFound):
|
||||||
|
message = _("Unknown quota resources %(unknown)s.")
|
||||||
|
|
||||||
|
|
||||||
|
class ProjectQuotaNotFound(QuotaNotFound):
|
||||||
|
message = _("Quota for project %(project_id)s could not be found.")
|
||||||
|
|
||||||
|
|
||||||
|
class QuotaClassNotFound(QuotaNotFound):
|
||||||
|
message = _("Quota class %(class_name)s could not be found.")
|
||||||
|
|
||||||
|
|
||||||
|
class QuotaUsageNotFound(QuotaNotFound):
|
||||||
|
message = _("Quota usage for project %(project_id)s could not be found.")
|
||||||
|
|
||||||
|
|
||||||
|
class ReservationNotFound(QuotaNotFound):
|
||||||
|
message = _("Quota reservation %(uuid)s could not be found.")
|
||||||
|
|
||||||
|
|
||||||
|
class OverQuota(TricircleException):
|
||||||
|
message = _("Quota exceeded for resources: %(overs)s")
|
||||||
|
194
tricircle/common/quota.py
Normal file
194
tricircle/common/quota.py
Normal file
@ -0,0 +1,194 @@
|
|||||||
|
# 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, largely copy from Neutron
|
||||||
|
"""
|
||||||
|
|
||||||
|
from oslo_config import cfg
|
||||||
|
import oslo_log.log as logging
|
||||||
|
|
||||||
|
from tricircle.common import exceptions
|
||||||
|
|
||||||
|
CONF = cfg.CONF
|
||||||
|
LOG = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
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 exceptions.ProjectQuotaNotFound:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Try for the quota class
|
||||||
|
if quota_class:
|
||||||
|
try:
|
||||||
|
return driver.get_by_class(context, quota_class, self.name)
|
||||||
|
except 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[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
|
||||||
|
|
||||||
|
|
||||||
|
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)
|
@ -13,11 +13,28 @@
|
|||||||
# 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 functools
|
||||||
|
import time
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
from oslo_config import cfg
|
||||||
|
from oslo_db import exception as db_exc
|
||||||
|
from oslo_log import log as logging
|
||||||
|
from oslo_utils import timeutils
|
||||||
|
|
||||||
|
from tricircle.common.context import is_admin_context as _is_admin_context
|
||||||
|
from tricircle.common import exceptions
|
||||||
|
from tricircle.common.i18n import _
|
||||||
|
from tricircle.common.i18n import _LW
|
||||||
|
|
||||||
from tricircle.db import core
|
from tricircle.db import core
|
||||||
from tricircle.db import models
|
from tricircle.db import models
|
||||||
|
|
||||||
|
|
||||||
|
CONF = cfg.CONF
|
||||||
|
LOG = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
def create_pod(context, pod_dict):
|
def create_pod(context, pod_dict):
|
||||||
with context.session.begin():
|
with context.session.begin():
|
||||||
return core.create_resource(context, models.Pod, pod_dict)
|
return core.create_resource(context, models.Pod, pod_dict)
|
||||||
@ -166,3 +183,606 @@ def get_pod_by_name(context, pod_name):
|
|||||||
return pod
|
return pod
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
_DEFAULT_QUOTA_NAME = 'default'
|
||||||
|
|
||||||
|
|
||||||
|
def _is_user_context(context):
|
||||||
|
"""Indicates if the request context is a normal user."""
|
||||||
|
if not context:
|
||||||
|
return False
|
||||||
|
if context.is_admin:
|
||||||
|
return False
|
||||||
|
if not context.user_id or not context.project_id:
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def authorize_quota_class_context(context, class_name):
|
||||||
|
"""Ensures a request has permission to access the given quota class."""
|
||||||
|
if _is_user_context(context):
|
||||||
|
if not context.quota_class:
|
||||||
|
raise exceptions.NotAuthorized()
|
||||||
|
elif context.quota_class != class_name:
|
||||||
|
raise exceptions.NotAuthorized()
|
||||||
|
|
||||||
|
|
||||||
|
def authorize_project_context(context, project_id):
|
||||||
|
"""Ensures a request has permission to access the given project."""
|
||||||
|
if _is_user_context(context):
|
||||||
|
if not context.project_id:
|
||||||
|
raise exceptions.NotAuthorized()
|
||||||
|
elif context.project_id != project_id:
|
||||||
|
raise exceptions.NotAuthorized()
|
||||||
|
|
||||||
|
|
||||||
|
def authorize_user_context(context, user_id):
|
||||||
|
"""Ensures a request has permission to access the given user."""
|
||||||
|
if _is_user_context(context):
|
||||||
|
if not context.user_id:
|
||||||
|
raise exceptions.NotAuthorized()
|
||||||
|
elif context.user_id != user_id:
|
||||||
|
raise exceptions.NotAuthorized()
|
||||||
|
|
||||||
|
|
||||||
|
def require_admin_context(f):
|
||||||
|
"""Decorator to require admin request context.
|
||||||
|
|
||||||
|
The first argument to the wrapped function must be the context.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
def wrapper(*args, **kwargs):
|
||||||
|
if not _is_admin_context(args[0]):
|
||||||
|
raise exceptions.AdminRequired()
|
||||||
|
return f(*args, **kwargs)
|
||||||
|
return wrapper
|
||||||
|
|
||||||
|
|
||||||
|
def require_context(f):
|
||||||
|
"""Decorator to require *any* user or admin context.
|
||||||
|
|
||||||
|
This does no authorization for user or project access matching, see
|
||||||
|
:py:func:`authorize_project_context` and
|
||||||
|
:py:func:`authorize_user_context`.
|
||||||
|
|
||||||
|
The first argument to the wrapped function must be the context.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
def wrapper(*args, **kwargs):
|
||||||
|
if not _is_admin_context(args[0]) and not _is_user_context(args[0]):
|
||||||
|
raise exceptions.NotAuthorized()
|
||||||
|
return f(*args, **kwargs)
|
||||||
|
return wrapper
|
||||||
|
|
||||||
|
|
||||||
|
def _retry_on_deadlock(f):
|
||||||
|
"""Decorator to retry a DB API call if Deadlock was received."""
|
||||||
|
@functools.wraps(f)
|
||||||
|
def wrapped(*args, **kwargs):
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
return f(*args, **kwargs)
|
||||||
|
except db_exc.DBDeadlock:
|
||||||
|
LOG.warning(_LW("Deadlock detected when running "
|
||||||
|
"'%(func_name)s': Retrying..."),
|
||||||
|
dict(func_name=f.__name__))
|
||||||
|
# Retry!
|
||||||
|
time.sleep(0.5)
|
||||||
|
continue
|
||||||
|
functools.update_wrapper(wrapped, f)
|
||||||
|
return wrapped
|
||||||
|
|
||||||
|
|
||||||
|
def model_query(context, *args, **kwargs):
|
||||||
|
"""Query helper that accounts for context's `read_deleted` field.
|
||||||
|
|
||||||
|
:param context: context to query under
|
||||||
|
:param session: if present, the session to use
|
||||||
|
:param read_deleted: if present, overrides context's read_deleted field.
|
||||||
|
:param project_only: if present and context is user-type, then restrict
|
||||||
|
query to match the context's project_id.
|
||||||
|
"""
|
||||||
|
session = kwargs.get('session') or context.session
|
||||||
|
read_deleted = kwargs.get('read_deleted') or context.read_deleted
|
||||||
|
project_only = kwargs.get('project_only')
|
||||||
|
|
||||||
|
query = session.query(*args)
|
||||||
|
|
||||||
|
if read_deleted == 'no':
|
||||||
|
query = query.filter_by(deleted=False)
|
||||||
|
elif read_deleted == 'yes':
|
||||||
|
pass # omit the filter to include deleted and active
|
||||||
|
elif read_deleted == 'only':
|
||||||
|
query = query.filter_by(deleted=True)
|
||||||
|
elif read_deleted == 'int_no':
|
||||||
|
query = query.filter_by(deleted=0)
|
||||||
|
else:
|
||||||
|
raise Exception(
|
||||||
|
_("Unrecognized read_deleted value '%s'") % read_deleted)
|
||||||
|
|
||||||
|
if project_only and _is_user_context(context):
|
||||||
|
query = query.filter_by(project_id=context.project_id)
|
||||||
|
|
||||||
|
return query
|
||||||
|
|
||||||
|
|
||||||
|
@require_context
|
||||||
|
def _quota_get(context, project_id, resource, session=None):
|
||||||
|
result = model_query(context, models.Quotas, session=session,
|
||||||
|
read_deleted="no").\
|
||||||
|
filter_by(project_id=project_id).\
|
||||||
|
filter_by(resource=resource).\
|
||||||
|
first()
|
||||||
|
|
||||||
|
if not result:
|
||||||
|
raise exceptions.ProjectQuotaNotFound(project_id=project_id)
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
@require_context
|
||||||
|
def quota_get(context, project_id, resource):
|
||||||
|
return _quota_get(context, project_id, resource)
|
||||||
|
|
||||||
|
|
||||||
|
@require_context
|
||||||
|
def quota_get_all_by_project(context, project_id):
|
||||||
|
authorize_project_context(context, project_id)
|
||||||
|
|
||||||
|
rows = model_query(context, models.Quotas, read_deleted="no").\
|
||||||
|
filter_by(project_id=project_id).\
|
||||||
|
all()
|
||||||
|
|
||||||
|
result = {'project_id': project_id}
|
||||||
|
for row in rows:
|
||||||
|
result[row.resource] = row.hard_limit
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
@require_context
|
||||||
|
def quota_allocated_get_all_by_project(context, project_id):
|
||||||
|
rows = model_query(context, models.Quotas, read_deleted='no').filter_by(
|
||||||
|
project_id=project_id).all()
|
||||||
|
result = {'project_id': project_id}
|
||||||
|
for row in rows:
|
||||||
|
result[row.resource] = row.allocated
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
@require_admin_context
|
||||||
|
def quota_create(context, project_id, resource, limit, allocated=0):
|
||||||
|
quota_ref = models.Quotas()
|
||||||
|
quota_ref.project_id = project_id
|
||||||
|
quota_ref.resource = resource
|
||||||
|
quota_ref.hard_limit = limit
|
||||||
|
if allocated:
|
||||||
|
quota_ref.allocated = allocated
|
||||||
|
|
||||||
|
session = core.get_session()
|
||||||
|
with session.begin():
|
||||||
|
quota_ref.save(session)
|
||||||
|
return quota_ref
|
||||||
|
|
||||||
|
|
||||||
|
@require_admin_context
|
||||||
|
def quota_update(context, project_id, resource, limit):
|
||||||
|
with context.session.begin():
|
||||||
|
quota_ref = _quota_get(context, project_id, resource,
|
||||||
|
session=context.session)
|
||||||
|
quota_ref.hard_limit = limit
|
||||||
|
return quota_ref
|
||||||
|
|
||||||
|
|
||||||
|
@require_admin_context
|
||||||
|
def quota_allocated_update(context, project_id, resource, allocated):
|
||||||
|
with context.session.begin():
|
||||||
|
quota_ref = _quota_get(context, project_id, resource,
|
||||||
|
session=context.session)
|
||||||
|
quota_ref.allocated = allocated
|
||||||
|
return quota_ref
|
||||||
|
|
||||||
|
|
||||||
|
@require_admin_context
|
||||||
|
def quota_destroy(context, project_id, resource):
|
||||||
|
with context.session.begin():
|
||||||
|
quota_ref = _quota_get(context, project_id, resource,
|
||||||
|
session=context.session)
|
||||||
|
quota_ref.delete(session=context.session)
|
||||||
|
|
||||||
|
|
||||||
|
@require_context
|
||||||
|
def _quota_class_get(context, class_name, resource, session=None):
|
||||||
|
result = model_query(context, models.QuotaClasses, session=session,
|
||||||
|
read_deleted="no").\
|
||||||
|
filter_by(class_name=class_name).\
|
||||||
|
filter_by(resource=resource).\
|
||||||
|
first()
|
||||||
|
|
||||||
|
if not result:
|
||||||
|
raise exceptions.QuotaClassNotFound(class_name=class_name)
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
@require_context
|
||||||
|
def quota_class_get(context, class_name, resource):
|
||||||
|
return _quota_class_get(context, class_name, resource)
|
||||||
|
|
||||||
|
|
||||||
|
def quota_class_get_default(context):
|
||||||
|
rows = model_query(context, models.QuotaClasses,
|
||||||
|
read_deleted="no").\
|
||||||
|
filter_by(class_name=_DEFAULT_QUOTA_NAME).all()
|
||||||
|
|
||||||
|
result = {'class_name': _DEFAULT_QUOTA_NAME}
|
||||||
|
for row in rows:
|
||||||
|
result[row.resource] = row.hard_limit
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
@require_context
|
||||||
|
def quota_class_get_all_by_name(context, class_name):
|
||||||
|
authorize_quota_class_context(context, class_name)
|
||||||
|
|
||||||
|
rows = model_query(context, models.QuotaClasses, read_deleted="no").\
|
||||||
|
filter_by(class_name=class_name).\
|
||||||
|
all()
|
||||||
|
|
||||||
|
result = {'class_name': class_name}
|
||||||
|
for row in rows:
|
||||||
|
result[row.resource] = row.hard_limit
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
@require_admin_context
|
||||||
|
def quota_class_create(context, class_name, resource, limit):
|
||||||
|
quota_class_ref = models.QuotaClasses()
|
||||||
|
quota_class_ref.class_name = class_name
|
||||||
|
quota_class_ref.resource = resource
|
||||||
|
quota_class_ref.hard_limit = limit
|
||||||
|
|
||||||
|
session = core.get_session()
|
||||||
|
with session.begin():
|
||||||
|
quota_class_ref.save(session)
|
||||||
|
return quota_class_ref
|
||||||
|
|
||||||
|
|
||||||
|
@require_admin_context
|
||||||
|
def quota_class_update(context, class_name, resource, limit):
|
||||||
|
with context.session.begin():
|
||||||
|
quota_class_ref = _quota_class_get(context, class_name, resource,
|
||||||
|
session=context.session)
|
||||||
|
quota_class_ref.hard_limit = limit
|
||||||
|
|
||||||
|
return quota_class_ref
|
||||||
|
|
||||||
|
|
||||||
|
@require_admin_context
|
||||||
|
def quota_class_destroy(context, class_name, resource):
|
||||||
|
with context.session.begin():
|
||||||
|
quota_class_ref = _quota_class_get(context, class_name, resource,
|
||||||
|
session=context.session)
|
||||||
|
quota_class_ref.delete(session=context.session)
|
||||||
|
|
||||||
|
|
||||||
|
@require_admin_context
|
||||||
|
def quota_class_destroy_all_by_name(context, class_name):
|
||||||
|
with context.session.begin():
|
||||||
|
quota_classes = model_query(context, models.QuotaClasses,
|
||||||
|
session=context.session,
|
||||||
|
read_deleted="no").\
|
||||||
|
filter_by(class_name=class_name).\
|
||||||
|
all()
|
||||||
|
|
||||||
|
for quota_class_ref in quota_classes:
|
||||||
|
quota_class_ref.delete(session=context.session)
|
||||||
|
|
||||||
|
|
||||||
|
@require_context
|
||||||
|
def quota_usage_get(context, project_id, resource):
|
||||||
|
result = model_query(context, models.QuotaUsages, read_deleted="no").\
|
||||||
|
filter_by(project_id=project_id).\
|
||||||
|
filter_by(resource=resource).\
|
||||||
|
first()
|
||||||
|
|
||||||
|
if not result:
|
||||||
|
raise exceptions.QuotaUsageNotFound(project_id=project_id)
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
@require_context
|
||||||
|
def quota_usage_get_all_by_project(context, project_id):
|
||||||
|
authorize_project_context(context, project_id)
|
||||||
|
|
||||||
|
rows = model_query(context, models.QuotaUsages, read_deleted="no").\
|
||||||
|
filter_by(project_id=project_id).\
|
||||||
|
all()
|
||||||
|
|
||||||
|
result = {'project_id': project_id}
|
||||||
|
for row in rows:
|
||||||
|
result[row.resource] = dict(in_use=row.in_use, reserved=row.reserved)
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
@require_admin_context
|
||||||
|
def _quota_usage_create(context, project_id, resource, in_use, reserved,
|
||||||
|
until_refresh, session=None):
|
||||||
|
|
||||||
|
quota_usage_ref = models.QuotaUsages()
|
||||||
|
quota_usage_ref.project_id = project_id
|
||||||
|
quota_usage_ref.resource = resource
|
||||||
|
quota_usage_ref.in_use = in_use
|
||||||
|
quota_usage_ref.reserved = reserved
|
||||||
|
quota_usage_ref.until_refresh = until_refresh
|
||||||
|
quota_usage_ref.save(session=session)
|
||||||
|
|
||||||
|
return quota_usage_ref
|
||||||
|
|
||||||
|
|
||||||
|
def _reservation_create(context, uuid, usage, project_id, resource, delta,
|
||||||
|
expire, session=None):
|
||||||
|
reservation_ref = models.Reservation()
|
||||||
|
reservation_ref.uuid = uuid
|
||||||
|
reservation_ref.usage_id = usage['id']
|
||||||
|
reservation_ref.project_id = project_id
|
||||||
|
reservation_ref.resource = resource
|
||||||
|
reservation_ref.delta = delta
|
||||||
|
reservation_ref.expire = expire
|
||||||
|
reservation_ref.save(session=session)
|
||||||
|
|
||||||
|
return reservation_ref
|
||||||
|
|
||||||
|
|
||||||
|
# NOTE(johannes): The quota code uses SQL locking to ensure races don't
|
||||||
|
# cause under or over counting of resources. To avoid deadlocks, this
|
||||||
|
# code always acquires the lock on quota_usages before acquiring the lock
|
||||||
|
# on reservations.
|
||||||
|
|
||||||
|
def _get_quota_usages(context, session, project_id):
|
||||||
|
# Broken out for testability
|
||||||
|
rows = model_query(context, models.QuotaUsages,
|
||||||
|
read_deleted="no",
|
||||||
|
session=session).\
|
||||||
|
filter_by(project_id=project_id).\
|
||||||
|
with_lockmode('update').\
|
||||||
|
all()
|
||||||
|
return {row.resource: row for row in rows}
|
||||||
|
|
||||||
|
|
||||||
|
def _get_quota_usages_by_resource(context, session, project_id, resource):
|
||||||
|
# TODO(joehuang), add user_id as part of the filter
|
||||||
|
rows = model_query(context, models.QuotaUsages,
|
||||||
|
read_deleted="no",
|
||||||
|
session=session).\
|
||||||
|
filter_by(project_id=project_id).\
|
||||||
|
filter_by(resource=resource).\
|
||||||
|
with_lockmode('update').\
|
||||||
|
all()
|
||||||
|
return {row.resource: row for row in rows}
|
||||||
|
|
||||||
|
|
||||||
|
@require_context
|
||||||
|
@_retry_on_deadlock
|
||||||
|
def quota_reserve(context, resources, quotas, deltas, expire,
|
||||||
|
until_refresh, max_age, project_id=None):
|
||||||
|
elevated = context.elevated()
|
||||||
|
with context.session.begin():
|
||||||
|
if project_id is None:
|
||||||
|
project_id = context.project_id
|
||||||
|
|
||||||
|
# Get the current usages
|
||||||
|
usages = _get_quota_usages(context, context.session, project_id)
|
||||||
|
|
||||||
|
# Handle usage refresh
|
||||||
|
refresh = False
|
||||||
|
work = set(deltas.keys())
|
||||||
|
while work:
|
||||||
|
resource = work.pop()
|
||||||
|
|
||||||
|
# Do we need to refresh the usage?
|
||||||
|
if resource not in usages:
|
||||||
|
usages[resource] = _quota_usage_create(elevated,
|
||||||
|
project_id,
|
||||||
|
resource,
|
||||||
|
0, 0,
|
||||||
|
until_refresh or None,
|
||||||
|
session=context.session)
|
||||||
|
refresh = True
|
||||||
|
elif usages[resource].in_use < 0:
|
||||||
|
# Negative in_use count indicates a desync, so try to
|
||||||
|
# heal from that...
|
||||||
|
refresh = True
|
||||||
|
elif usages[resource].until_refresh is not None:
|
||||||
|
usages[resource].until_refresh -= 1
|
||||||
|
if usages[resource].until_refresh <= 0:
|
||||||
|
refresh = True
|
||||||
|
elif max_age and usages[resource].updated_at is not None and (
|
||||||
|
(usages[resource].updated_at -
|
||||||
|
timeutils.utcnow()).seconds >= max_age):
|
||||||
|
refresh = True
|
||||||
|
|
||||||
|
if refresh:
|
||||||
|
# refresh from the bottom pod
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Check for deltas that would go negative
|
||||||
|
unders = [r for r, delta in deltas.items()
|
||||||
|
if delta < 0 and delta + usages[r].in_use < 0]
|
||||||
|
|
||||||
|
# Now, let's check the quotas
|
||||||
|
# NOTE(Vek): We're only concerned about positive increments.
|
||||||
|
# If a project has gone over quota, we want them to
|
||||||
|
# be able to reduce their usage without any
|
||||||
|
# problems.
|
||||||
|
overs = [r for r, delta in deltas.items()
|
||||||
|
if quotas[r] >= 0 and delta >= 0 and
|
||||||
|
quotas[r] < delta + usages[r].in_use + usages[r].reserved]
|
||||||
|
|
||||||
|
# NOTE(Vek): The quota check needs to be in the transaction,
|
||||||
|
# but the transaction doesn't fail just because
|
||||||
|
# we're over quota, so the OverQuota raise is
|
||||||
|
# outside the transaction. If we did the raise
|
||||||
|
# here, our usage updates would be discarded, but
|
||||||
|
# they're not invalidated by being over-quota.
|
||||||
|
|
||||||
|
# Create the reservations
|
||||||
|
if not overs:
|
||||||
|
reservations = []
|
||||||
|
for resource, delta in deltas.items():
|
||||||
|
reservation = _reservation_create(elevated,
|
||||||
|
str(uuid.uuid4()),
|
||||||
|
usages[resource],
|
||||||
|
project_id,
|
||||||
|
resource, delta, expire,
|
||||||
|
session=context.session)
|
||||||
|
reservations.append(reservation.uuid)
|
||||||
|
|
||||||
|
# Also update the reserved quantity
|
||||||
|
# NOTE(Vek): Again, we are only concerned here about
|
||||||
|
# positive increments. Here, though, we're
|
||||||
|
# worried about the following scenario:
|
||||||
|
#
|
||||||
|
# 1) User initiates resize down.
|
||||||
|
# 2) User allocates a new instance.
|
||||||
|
# 3) Resize down fails or is reverted.
|
||||||
|
# 4) User is now over quota.
|
||||||
|
#
|
||||||
|
# To prevent this, we only update the
|
||||||
|
# reserved value if the delta is positive.
|
||||||
|
if delta > 0:
|
||||||
|
usages[resource].reserved += delta
|
||||||
|
|
||||||
|
if unders:
|
||||||
|
LOG.warning(_LW("Change will make usage less than 0 for the following "
|
||||||
|
"resources: %s"), unders)
|
||||||
|
if overs:
|
||||||
|
usages = {k: dict(in_use=v['in_use'], reserved=v['reserved'])
|
||||||
|
for k, v in usages.items()}
|
||||||
|
raise exceptions.OverQuota(overs=sorted(overs), quotas=quotas,
|
||||||
|
usages=usages)
|
||||||
|
|
||||||
|
return reservations
|
||||||
|
|
||||||
|
|
||||||
|
def _quota_reservations(session, context, reservations):
|
||||||
|
"""Return the relevant reservations."""
|
||||||
|
|
||||||
|
# Get the listed reservations
|
||||||
|
return model_query(context, models.Reservation,
|
||||||
|
read_deleted="no",
|
||||||
|
session=session).\
|
||||||
|
filter(models.Reservation.uuid.in_(reservations)).\
|
||||||
|
with_lockmode('update').\
|
||||||
|
all()
|
||||||
|
|
||||||
|
|
||||||
|
@require_context
|
||||||
|
@_retry_on_deadlock
|
||||||
|
def reservation_commit(context, reservations, project_id=None):
|
||||||
|
with context.session.begin():
|
||||||
|
usages = _get_quota_usages(context, context.session, project_id)
|
||||||
|
|
||||||
|
for reservation in _quota_reservations(context.session,
|
||||||
|
context,
|
||||||
|
reservations):
|
||||||
|
usage = usages[reservation.resource]
|
||||||
|
if reservation.delta >= 0:
|
||||||
|
usage.reserved -= reservation.delta
|
||||||
|
usage.in_use += reservation.delta
|
||||||
|
|
||||||
|
reservation.delete(session=context.session)
|
||||||
|
|
||||||
|
|
||||||
|
@require_context
|
||||||
|
@_retry_on_deadlock
|
||||||
|
def reservation_rollback(context, reservations, project_id=None):
|
||||||
|
with context.session.begin():
|
||||||
|
usages = _get_quota_usages(context, context.session, project_id)
|
||||||
|
|
||||||
|
for reservation in _quota_reservations(context.session,
|
||||||
|
context,
|
||||||
|
reservations):
|
||||||
|
usage = usages[reservation.resource]
|
||||||
|
if reservation.delta >= 0:
|
||||||
|
usage.reserved -= reservation.delta
|
||||||
|
|
||||||
|
reservation.delete(session=context.session)
|
||||||
|
|
||||||
|
|
||||||
|
def quota_destroy_by_project(*args, **kwargs):
|
||||||
|
"""Destroy all limit quotas associated with a project.
|
||||||
|
|
||||||
|
Leaves usage and reservation quotas intact.
|
||||||
|
"""
|
||||||
|
quota_destroy_all_by_project(only_quotas=True, *args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
@require_admin_context
|
||||||
|
@_retry_on_deadlock
|
||||||
|
def quota_destroy_all_by_project(context, project_id, only_quotas=False):
|
||||||
|
"""Destroy all quotas associated with a project.
|
||||||
|
|
||||||
|
This includes limit quotas, usage quotas and reservation quotas.
|
||||||
|
Optionally can only remove limit quotas and leave other types as they are.
|
||||||
|
|
||||||
|
:param context: The request context, for access checks.
|
||||||
|
:param project_id: The ID of the project being deleted.
|
||||||
|
:param only_quotas: Only delete limit quotas, leave other types intact.
|
||||||
|
"""
|
||||||
|
with context.session.begin():
|
||||||
|
quotas = model_query(context, models.Quotas, session=context.session,
|
||||||
|
read_deleted="no").\
|
||||||
|
filter_by(project_id=project_id).\
|
||||||
|
all()
|
||||||
|
|
||||||
|
for quota_ref in quotas:
|
||||||
|
quota_ref.delete(session=context.session)
|
||||||
|
|
||||||
|
if only_quotas:
|
||||||
|
return
|
||||||
|
|
||||||
|
quota_usages = model_query(context, models.QuotaUsages,
|
||||||
|
session=context.session,
|
||||||
|
read_deleted="no").\
|
||||||
|
filter_by(project_id=project_id).\
|
||||||
|
all()
|
||||||
|
|
||||||
|
for quota_usage_ref in quota_usages:
|
||||||
|
quota_usage_ref.delete(session=context.session)
|
||||||
|
|
||||||
|
reservations = model_query(context, models.Reservation,
|
||||||
|
session=context.session,
|
||||||
|
read_deleted="no").\
|
||||||
|
filter_by(project_id=project_id).\
|
||||||
|
all()
|
||||||
|
|
||||||
|
for reservation_ref in reservations:
|
||||||
|
reservation_ref.delete(session=context.session)
|
||||||
|
|
||||||
|
|
||||||
|
@require_admin_context
|
||||||
|
@_retry_on_deadlock
|
||||||
|
def reservation_expire(context):
|
||||||
|
with context.session.begin():
|
||||||
|
current_time = timeutils.utcnow()
|
||||||
|
results = model_query(context, models.Reservation,
|
||||||
|
session=context.session,
|
||||||
|
read_deleted="no").\
|
||||||
|
filter(models.Reservation.expire < current_time).\
|
||||||
|
all()
|
||||||
|
|
||||||
|
if results:
|
||||||
|
for reservation in results:
|
||||||
|
if reservation.delta >= 0:
|
||||||
|
reservation.usage.reserved -= reservation.delta
|
||||||
|
reservation.usage.save(session=context.session)
|
||||||
|
|
||||||
|
reservation.delete(session=context.session)
|
||||||
|
@ -14,13 +14,17 @@
|
|||||||
# under the License.
|
# under the License.
|
||||||
|
|
||||||
|
|
||||||
|
import threading
|
||||||
|
|
||||||
|
import sqlalchemy as sql
|
||||||
|
from sqlalchemy.ext import declarative
|
||||||
|
from sqlalchemy.inspection import inspect
|
||||||
|
|
||||||
|
|
||||||
from oslo_config import cfg
|
from oslo_config import cfg
|
||||||
import oslo_db.options as db_options
|
import oslo_db.options as db_options
|
||||||
import oslo_db.sqlalchemy.session as db_session
|
import oslo_db.sqlalchemy.session as db_session
|
||||||
from oslo_utils import strutils
|
from oslo_utils import strutils
|
||||||
import sqlalchemy as sql
|
|
||||||
from sqlalchemy.ext import declarative
|
|
||||||
from sqlalchemy.inspection import inspect
|
|
||||||
|
|
||||||
from tricircle.common import exceptions
|
from tricircle.common import exceptions
|
||||||
|
|
||||||
@ -31,6 +35,7 @@ db_opts = [
|
|||||||
]
|
]
|
||||||
cfg.CONF.register_opts(db_opts)
|
cfg.CONF.register_opts(db_opts)
|
||||||
|
|
||||||
|
_LOCK = threading.Lock()
|
||||||
_engine_facade = None
|
_engine_facade = None
|
||||||
ModelBase = declarative.declarative_base()
|
ModelBase = declarative.declarative_base()
|
||||||
|
|
||||||
@ -64,11 +69,14 @@ def _filter_query(model, query, filters):
|
|||||||
|
|
||||||
|
|
||||||
def _get_engine_facade():
|
def _get_engine_facade():
|
||||||
|
global _LOCK
|
||||||
|
with _LOCK:
|
||||||
global _engine_facade
|
global _engine_facade
|
||||||
|
|
||||||
if not _engine_facade:
|
if not _engine_facade:
|
||||||
t_connection = cfg.CONF.tricircle_db_connection
|
t_connection = cfg.CONF.tricircle_db_connection
|
||||||
_engine_facade = db_session.EngineFacade(t_connection, _conf=cfg.CONF)
|
_engine_facade = db_session.EngineFacade(t_connection,
|
||||||
|
_conf=cfg.CONF)
|
||||||
return _engine_facade
|
return _engine_facade
|
||||||
|
|
||||||
|
|
||||||
|
@ -118,14 +118,72 @@ def upgrade(migrate_engine):
|
|||||||
quotas = sql.Table(
|
quotas = sql.Table(
|
||||||
'quotas', meta,
|
'quotas', meta,
|
||||||
sql.Column('id', sql.Integer, primary_key=True),
|
sql.Column('id', sql.Integer, primary_key=True),
|
||||||
sql.Column('project_id', sql.String(255)),
|
sql.Column('project_id', sql.String(255), index=True),
|
||||||
|
sql.Column('resource', sql.String(255), nullable=False),
|
||||||
|
sql.Column('hard_limit', sql.Integer),
|
||||||
|
sql.Column('allocated', sql.Integer, default=0),
|
||||||
|
sql.Column('created_at', sql.DateTime),
|
||||||
|
sql.Column('updated_at', sql.DateTime),
|
||||||
|
sql.Column('deleted_at', sql.DateTime),
|
||||||
|
sql.Column('deleted', sql.Integer),
|
||||||
|
migrate.UniqueConstraint(
|
||||||
|
'project_id', 'resource', 'deleted',
|
||||||
|
name='uniq_quotas0project_id0resource0deleted'),
|
||||||
|
mysql_engine='InnoDB',
|
||||||
|
mysql_charset='utf8')
|
||||||
|
|
||||||
|
quota_classes = sql.Table(
|
||||||
|
'quota_classes', meta,
|
||||||
|
sql.Column('id', sql.Integer, primary_key=True),
|
||||||
|
sql.Column('class_name', sql.String(255), index=True),
|
||||||
sql.Column('resource', sql.String(255), nullable=False),
|
sql.Column('resource', sql.String(255), nullable=False),
|
||||||
sql.Column('hard_limit', sql.Integer),
|
sql.Column('hard_limit', sql.Integer),
|
||||||
sql.Column('created_at', sql.DateTime),
|
sql.Column('created_at', sql.DateTime),
|
||||||
sql.Column('updated_at', sql.DateTime),
|
sql.Column('updated_at', sql.DateTime),
|
||||||
|
sql.Column('deleted_at', sql.DateTime),
|
||||||
|
sql.Column('deleted', sql.Integer),
|
||||||
migrate.UniqueConstraint(
|
migrate.UniqueConstraint(
|
||||||
'project_id', 'resource',
|
'class_name', 'resource', 'deleted',
|
||||||
name='uniq_quotas0project_id0resource'),
|
name='uniq_quota_classes0class_name0resource0deleted'),
|
||||||
|
mysql_engine='InnoDB',
|
||||||
|
mysql_charset='utf8')
|
||||||
|
|
||||||
|
quota_usages = sql.Table(
|
||||||
|
'quota_usages', meta,
|
||||||
|
sql.Column('id', sql.Integer, primary_key=True),
|
||||||
|
sql.Column('project_id', sql.String(255), index=True),
|
||||||
|
sql.Column('user_id', sql.String(255), index=True),
|
||||||
|
sql.Column('resource', sql.String(255), nullable=False),
|
||||||
|
sql.Column('in_use', sql.Integer),
|
||||||
|
sql.Column('reserved', sql.Integer),
|
||||||
|
sql.Column('until_refresh', sql.Integer),
|
||||||
|
sql.Column('created_at', sql.DateTime),
|
||||||
|
sql.Column('updated_at', sql.DateTime),
|
||||||
|
sql.Column('deleted_at', sql.DateTime),
|
||||||
|
sql.Column('deleted', sql.Integer),
|
||||||
|
mysql_engine='InnoDB',
|
||||||
|
mysql_charset='utf8')
|
||||||
|
|
||||||
|
reservations = sql.Table(
|
||||||
|
'reservations', meta,
|
||||||
|
sql.Column('id', sql.Integer(), primary_key=True),
|
||||||
|
sql.Column('uuid', sql.String(length=36), nullable=False),
|
||||||
|
sql.Column('usage_id', sql.Integer(),
|
||||||
|
sql.ForeignKey('quota_usages.id'),
|
||||||
|
nullable=False),
|
||||||
|
sql.Column('project_id',
|
||||||
|
sql.String(length=255),
|
||||||
|
index=True),
|
||||||
|
sql.Column('resource',
|
||||||
|
sql.String(length=255)),
|
||||||
|
sql.Column('delta', sql.Integer(), nullable=False),
|
||||||
|
sql.Column('expire', sql.DateTime),
|
||||||
|
|
||||||
|
sql.Column('created_at', sql.DateTime),
|
||||||
|
sql.Column('updated_at', sql.DateTime),
|
||||||
|
sql.Column('deleted_at', sql.DateTime),
|
||||||
|
sql.Column('deleted', sql.Boolean(create_constraint=True,
|
||||||
|
name=None)),
|
||||||
mysql_engine='InnoDB',
|
mysql_engine='InnoDB',
|
||||||
mysql_charset='utf8')
|
mysql_charset='utf8')
|
||||||
|
|
||||||
@ -167,8 +225,9 @@ def upgrade(migrate_engine):
|
|||||||
|
|
||||||
tables = [aggregates, aggregate_metadata, instance_types,
|
tables = [aggregates, aggregate_metadata, instance_types,
|
||||||
instance_type_projects, instance_type_extra_specs, key_pairs,
|
instance_type_projects, instance_type_extra_specs, key_pairs,
|
||||||
quotas, volume_types, quality_of_service_specs,
|
quotas, quota_classes, quota_usages, reservations,
|
||||||
cascaded_pods_resource_routing]
|
volume_types,
|
||||||
|
quality_of_service_specs, cascaded_pods_resource_routing]
|
||||||
for table in tables:
|
for table in tables:
|
||||||
table.create()
|
table.create()
|
||||||
|
|
||||||
@ -178,6 +237,8 @@ def upgrade(migrate_engine):
|
|||||||
'references': [instance_types.c.id]},
|
'references': [instance_types.c.id]},
|
||||||
{'columns': [instance_type_extra_specs.c.instance_type_id],
|
{'columns': [instance_type_extra_specs.c.instance_type_id],
|
||||||
'references': [instance_types.c.id]},
|
'references': [instance_types.c.id]},
|
||||||
|
{'columns': [reservations.c.usage_id],
|
||||||
|
'references': [quota_usages.c.id]},
|
||||||
{'columns': [volume_types.c.qos_specs_id],
|
{'columns': [volume_types.c.qos_specs_id],
|
||||||
'references': [quality_of_service_specs.c.id]},
|
'references': [quality_of_service_specs.c.id]},
|
||||||
{'columns': [quality_of_service_specs.c.specs_id],
|
{'columns': [quality_of_service_specs.c.specs_id],
|
||||||
|
@ -13,12 +13,14 @@
|
|||||||
# License for the specific language governing permissions and limitations
|
# License for the specific language governing permissions and limitations
|
||||||
# under the License.
|
# under the License.
|
||||||
|
|
||||||
|
|
||||||
from oslo_db.sqlalchemy import models
|
|
||||||
import sqlalchemy as sql
|
import sqlalchemy as sql
|
||||||
from sqlalchemy.dialects import mysql
|
from sqlalchemy.dialects import mysql
|
||||||
|
from sqlalchemy.orm import relationship
|
||||||
from sqlalchemy import schema
|
from sqlalchemy import schema
|
||||||
|
|
||||||
|
from oslo_db.sqlalchemy import models
|
||||||
|
from oslo_utils import timeutils
|
||||||
|
|
||||||
from tricircle.db import core
|
from tricircle.db import core
|
||||||
|
|
||||||
|
|
||||||
@ -143,7 +145,26 @@ class KeyPair(core.ModelBase, core.DictBase, models.TimestampMixin):
|
|||||||
nullable=False, server_default='ssh')
|
nullable=False, server_default='ssh')
|
||||||
|
|
||||||
|
|
||||||
class Quota(core.ModelBase, core.DictBase, models.TimestampMixin):
|
# Quota part are ported from Cinder for hierarchy multi-tenancy quota control
|
||||||
|
class QuotasBase(models.ModelBase, core.DictBase,
|
||||||
|
models.TimestampMixin, models.SoftDeleteMixin):
|
||||||
|
"""QuotasBase.
|
||||||
|
|
||||||
|
provide base class for quota series tables. For it inherits from
|
||||||
|
models.ModelBase, this is different from other tables
|
||||||
|
"""
|
||||||
|
__table_args__ = {'mysql_engine': 'InnoDB'}
|
||||||
|
|
||||||
|
metadata = None
|
||||||
|
|
||||||
|
def delete(self, session):
|
||||||
|
"""Delete this object."""
|
||||||
|
self.deleted = True
|
||||||
|
self.deleted_at = timeutils.utcnow()
|
||||||
|
self.save(session=session)
|
||||||
|
|
||||||
|
|
||||||
|
class Quotas(core.ModelBase, QuotasBase):
|
||||||
"""Represents a single quota override for a project.
|
"""Represents a single quota override for a project.
|
||||||
|
|
||||||
If there is no row for a given project id and resource, then the
|
If there is no row for a given project id and resource, then the
|
||||||
@ -154,16 +175,96 @@ class Quota(core.ModelBase, core.DictBase, models.TimestampMixin):
|
|||||||
"""
|
"""
|
||||||
__tablename__ = 'quotas'
|
__tablename__ = 'quotas'
|
||||||
__table_args__ = (
|
__table_args__ = (
|
||||||
schema.UniqueConstraint('project_id', 'resource',
|
schema.UniqueConstraint(
|
||||||
name='uniq_quotas0project_id0resource'),
|
'project_id', 'resource', 'deleted',
|
||||||
)
|
name='uniq_quotas0project_id0resource0deleted'),)
|
||||||
attributes = ['id', 'project_id', 'resource', 'hard_limit',
|
attributes = ['id', 'project_id', 'resource',
|
||||||
'created_at', 'updated_at']
|
'hard_limit', 'allocated',
|
||||||
|
'created_at', 'updated_at', 'deleted_at', 'deleted']
|
||||||
|
|
||||||
id = sql.Column(sql.Integer, primary_key=True)
|
id = sql.Column(sql.Integer, primary_key=True)
|
||||||
project_id = sql.Column(sql.String(255))
|
project_id = sql.Column(sql.String(255), index=True)
|
||||||
resource = sql.Column(sql.String(255), nullable=False)
|
resource = sql.Column(sql.String(255), nullable=False)
|
||||||
hard_limit = sql.Column(sql.Integer)
|
hard_limit = sql.Column(sql.Integer)
|
||||||
|
allocated = sql.Column(sql.Integer, default=0)
|
||||||
|
|
||||||
|
|
||||||
|
class QuotaClasses(core.ModelBase, QuotasBase):
|
||||||
|
"""Quota_classes.
|
||||||
|
|
||||||
|
make default quota as a update-able quota.Mainly for the command
|
||||||
|
quota-class-show and quota-class-update
|
||||||
|
"""
|
||||||
|
__tablename__ = 'quota_classes'
|
||||||
|
__table_args__ = (
|
||||||
|
schema.UniqueConstraint(
|
||||||
|
'class_name', 'resource', 'deleted',
|
||||||
|
name='uniq_quota_classes0class_name0resource0deleted'),
|
||||||
|
)
|
||||||
|
attributes = ['id', 'class_name', 'resource', 'hard_limit',
|
||||||
|
'created_at', 'updated_at', 'deleted_at', 'deleted']
|
||||||
|
|
||||||
|
id = sql.Column(sql.Integer, primary_key=True)
|
||||||
|
class_name = sql.Column(sql.String(255), index=True)
|
||||||
|
resource = sql.Column(sql.String(255), nullable=False)
|
||||||
|
hard_limit = sql.Column(sql.Integer)
|
||||||
|
|
||||||
|
|
||||||
|
class QuotaUsages(core.ModelBase, QuotasBase):
|
||||||
|
"""Quota_uages.
|
||||||
|
|
||||||
|
store quota usages for project resource
|
||||||
|
"""
|
||||||
|
__tablename__ = 'quota_usages'
|
||||||
|
__table_args__ = ()
|
||||||
|
attributes = ['id', 'project_id', 'user_id', 'resource',
|
||||||
|
'in_use', 'reserved', 'until_refresh',
|
||||||
|
'created_at', 'updated_at', 'deleted_at', 'deleted']
|
||||||
|
|
||||||
|
id = sql.Column(sql.Integer, primary_key=True)
|
||||||
|
project_id = sql.Column(sql.String(255), index=True)
|
||||||
|
user_id = sql.Column(sql.String(255), index=True)
|
||||||
|
resource = sql.Column(sql.String(255), nullable=False)
|
||||||
|
|
||||||
|
in_use = sql.Column(sql.Integer)
|
||||||
|
reserved = sql.Column(sql.Integer, default=0)
|
||||||
|
|
||||||
|
until_refresh = sql.Column(sql.Integer, default=0)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def total(self):
|
||||||
|
return self.in_use + self.reserved
|
||||||
|
|
||||||
|
|
||||||
|
class Reservation(core.ModelBase, QuotasBase):
|
||||||
|
"""Reservation.
|
||||||
|
|
||||||
|
Represents a resource reservation for quotas
|
||||||
|
"""
|
||||||
|
__tablename__ = 'reservations'
|
||||||
|
__table_args__ = ()
|
||||||
|
attributes = ['id', 'uuid', 'usage_id', 'project_id', 'resource',
|
||||||
|
'delta', 'expire',
|
||||||
|
'created_at', 'updated_at', 'deleted_at', 'deleted']
|
||||||
|
|
||||||
|
id = sql.Column(sql.Integer, primary_key=True)
|
||||||
|
uuid = sql.Column(sql.String(36), nullable=False)
|
||||||
|
|
||||||
|
usage_id = sql.Column(sql.Integer,
|
||||||
|
sql.ForeignKey('quota_usages.id'),
|
||||||
|
nullable=False)
|
||||||
|
|
||||||
|
project_id = sql.Column(sql.String(255), index=True)
|
||||||
|
resource = sql.Column(sql.String(255))
|
||||||
|
|
||||||
|
delta = sql.Column(sql.Integer)
|
||||||
|
expire = sql.Column(sql.DateTime, nullable=False)
|
||||||
|
|
||||||
|
usage = relationship(
|
||||||
|
"QuotaUsages",
|
||||||
|
foreign_keys=usage_id,
|
||||||
|
primaryjoin='and_(Reservation.usage_id == QuotaUsages.id,'
|
||||||
|
'QuotaUsages.deleted == 0)')
|
||||||
|
|
||||||
|
|
||||||
class VolumeTypes(core.ModelBase, core.DictBase, models.TimestampMixin):
|
class VolumeTypes(core.ModelBase, core.DictBase, models.TimestampMixin):
|
||||||
|
@ -13,10 +13,14 @@
|
|||||||
# 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 datetime
|
||||||
|
import six
|
||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
from tricircle.common import context
|
from tricircle.common import context
|
||||||
|
from tricircle.common import exceptions
|
||||||
|
from tricircle.common import quota
|
||||||
|
|
||||||
from tricircle.db import api
|
from tricircle.db import api
|
||||||
from tricircle.db import core
|
from tricircle.db import core
|
||||||
from tricircle.db import models
|
from tricircle.db import models
|
||||||
@ -181,3 +185,323 @@ class APITest(unittest.TestCase):
|
|||||||
|
|
||||||
def tearDown(self):
|
def tearDown(self):
|
||||||
core.ModelBase.metadata.drop_all(core.get_engine())
|
core.ModelBase.metadata.drop_all(core.get_engine())
|
||||||
|
|
||||||
|
|
||||||
|
class QuotaApiTestCase(unittest.TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
core.initialize()
|
||||||
|
core.ModelBase.metadata.create_all(core.get_engine())
|
||||||
|
self.context = context.get_admin_context()
|
||||||
|
|
||||||
|
def _quota_reserve(self, context, project_id):
|
||||||
|
"""Create sample Quota, QuotaUsage and Reservation objects.
|
||||||
|
|
||||||
|
There is no method api.quota_usage_create(), so we have to use
|
||||||
|
api.quota_reserve() for creating QuotaUsage objects.
|
||||||
|
|
||||||
|
Returns reservations uuids.
|
||||||
|
|
||||||
|
"""
|
||||||
|
quotas = {}
|
||||||
|
resources = {}
|
||||||
|
deltas = {}
|
||||||
|
for i, resource in enumerate(('volumes', 'gigabytes')):
|
||||||
|
quota_obj = api.quota_create(context, project_id, resource, i + 1)
|
||||||
|
quotas[resource] = quota_obj.hard_limit
|
||||||
|
resources[resource] = quota.ReservableResource(resource, None)
|
||||||
|
deltas[resource] = i + 1
|
||||||
|
return api.quota_reserve(
|
||||||
|
context, resources, quotas, deltas,
|
||||||
|
datetime.datetime.utcnow(), datetime.datetime.utcnow(),
|
||||||
|
datetime.timedelta(days=1), project_id
|
||||||
|
)
|
||||||
|
|
||||||
|
def _dict_from_object(self, obj, ignored_keys):
|
||||||
|
if ignored_keys is None:
|
||||||
|
ignored_keys = []
|
||||||
|
if isinstance(obj, dict):
|
||||||
|
items = obj.items()
|
||||||
|
else:
|
||||||
|
items = obj.iteritems()
|
||||||
|
return {k: v for k, v in items
|
||||||
|
if k not in ignored_keys}
|
||||||
|
|
||||||
|
def _assertEqualObjects(self, obj1, obj2, ignored_keys=None):
|
||||||
|
obj1 = self._dict_from_object(obj1, ignored_keys)
|
||||||
|
obj2 = self._dict_from_object(obj2, ignored_keys)
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
len(obj1), len(obj2),
|
||||||
|
"Keys mismatch: %s" % six.text_type(
|
||||||
|
set(obj1.keys()) ^ set(obj2.keys())))
|
||||||
|
for key, value in obj1.items():
|
||||||
|
self.assertEqual(value, obj2[key])
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
core.ModelBase.metadata.drop_all(core.get_engine())
|
||||||
|
|
||||||
|
|
||||||
|
class DBAPIReservationTestCase(QuotaApiTestCase):
|
||||||
|
|
||||||
|
"""Tests for db.api.reservation_* methods."""
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
super(DBAPIReservationTestCase, self).setUp()
|
||||||
|
self.values = {
|
||||||
|
'uuid': 'sample-uuid',
|
||||||
|
'project_id': 'project1',
|
||||||
|
'resource': 'resource',
|
||||||
|
'delta': 42,
|
||||||
|
'expire': (datetime.datetime.utcnow() +
|
||||||
|
datetime.timedelta(days=1)),
|
||||||
|
'usage': {'id': 1}
|
||||||
|
}
|
||||||
|
|
||||||
|
def test_reservation_commit(self):
|
||||||
|
reservations = self._quota_reserve(self.context, 'project1')
|
||||||
|
expected = {'project_id': 'project1',
|
||||||
|
'volumes': {'reserved': 1, 'in_use': 0},
|
||||||
|
'gigabytes': {'reserved': 2, 'in_use': 0},
|
||||||
|
}
|
||||||
|
self.assertEqual(expected,
|
||||||
|
api.quota_usage_get_all_by_project(
|
||||||
|
self.context, 'project1'))
|
||||||
|
api.reservation_commit(self.context, reservations, 'project1')
|
||||||
|
expected = {'project_id': 'project1',
|
||||||
|
'volumes': {'reserved': 0, 'in_use': 1},
|
||||||
|
'gigabytes': {'reserved': 0, 'in_use': 2},
|
||||||
|
}
|
||||||
|
self.assertEqual(expected,
|
||||||
|
api.quota_usage_get_all_by_project(
|
||||||
|
self.context,
|
||||||
|
'project1'))
|
||||||
|
|
||||||
|
def test_reservation_rollback(self):
|
||||||
|
reservations = self._quota_reserve(self.context, 'project1')
|
||||||
|
expected = {'project_id': 'project1',
|
||||||
|
'volumes': {'reserved': 1, 'in_use': 0},
|
||||||
|
'gigabytes': {'reserved': 2, 'in_use': 0},
|
||||||
|
}
|
||||||
|
self.assertEqual(expected,
|
||||||
|
api.quota_usage_get_all_by_project(
|
||||||
|
self.context,
|
||||||
|
'project1'))
|
||||||
|
api.reservation_rollback(self.context, reservations, 'project1')
|
||||||
|
expected = {'project_id': 'project1',
|
||||||
|
'volumes': {'reserved': 0, 'in_use': 0},
|
||||||
|
'gigabytes': {'reserved': 0, 'in_use': 0},
|
||||||
|
}
|
||||||
|
self.assertEqual(expected,
|
||||||
|
api.quota_usage_get_all_by_project(
|
||||||
|
self.context,
|
||||||
|
'project1'))
|
||||||
|
|
||||||
|
def test_reservation_expire(self):
|
||||||
|
self.values['expire'] = datetime.datetime.utcnow() + \
|
||||||
|
datetime.timedelta(days=1)
|
||||||
|
self._quota_reserve(self.context, 'project1')
|
||||||
|
api.reservation_expire(self.context)
|
||||||
|
|
||||||
|
expected = {'project_id': 'project1',
|
||||||
|
'gigabytes': {'reserved': 0, 'in_use': 0},
|
||||||
|
'volumes': {'reserved': 0, 'in_use': 0}}
|
||||||
|
self.assertEqual(expected,
|
||||||
|
api.quota_usage_get_all_by_project(
|
||||||
|
self.context,
|
||||||
|
'project1'))
|
||||||
|
|
||||||
|
|
||||||
|
class DBAPIQuotaClassTestCase(QuotaApiTestCase):
|
||||||
|
|
||||||
|
"""Tests for api.api.quota_class_* methods."""
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
super(DBAPIQuotaClassTestCase, self).setUp()
|
||||||
|
self.sample_qc = api.quota_class_create(self.context, 'test_qc',
|
||||||
|
'test_resource', 42)
|
||||||
|
|
||||||
|
def test_quota_class_get(self):
|
||||||
|
qc = api.quota_class_get(self.context, 'test_qc', 'test_resource')
|
||||||
|
self._assertEqualObjects(self.sample_qc, qc)
|
||||||
|
|
||||||
|
def test_quota_class_destroy(self):
|
||||||
|
api.quota_class_destroy(self.context, 'test_qc', 'test_resource')
|
||||||
|
self.assertRaises(exceptions.QuotaClassNotFound,
|
||||||
|
api.quota_class_get, self.context,
|
||||||
|
'test_qc', 'test_resource')
|
||||||
|
|
||||||
|
def test_quota_class_get_not_found(self):
|
||||||
|
self.assertRaises(exceptions.QuotaClassNotFound,
|
||||||
|
api.quota_class_get, self.context, 'nonexistent',
|
||||||
|
'nonexistent')
|
||||||
|
|
||||||
|
def test_quota_class_get_all_by_name(self):
|
||||||
|
api.quota_class_create(self.context, 'test2', 'res1', 43)
|
||||||
|
api.quota_class_create(self.context, 'test2', 'res2', 44)
|
||||||
|
self.assertEqual({'class_name': 'test_qc', 'test_resource': 42},
|
||||||
|
api.quota_class_get_all_by_name(self.context,
|
||||||
|
'test_qc'))
|
||||||
|
self.assertEqual({'class_name': 'test2', 'res1': 43, 'res2': 44},
|
||||||
|
api.quota_class_get_all_by_name(self.context,
|
||||||
|
'test2'))
|
||||||
|
|
||||||
|
def test_quota_class_update(self):
|
||||||
|
api.quota_class_update(self.context, 'test_qc', 'test_resource', 43)
|
||||||
|
updated = api.quota_class_get(self.context, 'test_qc',
|
||||||
|
'test_resource')
|
||||||
|
self.assertEqual(43, updated['hard_limit'])
|
||||||
|
|
||||||
|
def test_quota_class_destroy_all_by_name(self):
|
||||||
|
api.quota_class_create(self.context, 'test2', 'res1', 43)
|
||||||
|
api.quota_class_create(self.context, 'test2', 'res2', 44)
|
||||||
|
api.quota_class_destroy_all_by_name(self.context, 'test2')
|
||||||
|
self.assertEqual({'class_name': 'test2'},
|
||||||
|
api.quota_class_get_all_by_name(self.context,
|
||||||
|
'test2'))
|
||||||
|
|
||||||
|
|
||||||
|
class DBAPIQuotaTestCase(QuotaApiTestCase):
|
||||||
|
|
||||||
|
"""Tests for api.api.reservation_* methods."""
|
||||||
|
|
||||||
|
def test_quota_create(self):
|
||||||
|
_quota = api.quota_create(self.context, 'project1', 'resource', 99)
|
||||||
|
self.assertEqual('resource', _quota.resource)
|
||||||
|
self.assertEqual(99, _quota.hard_limit)
|
||||||
|
self.assertEqual('project1', _quota.project_id)
|
||||||
|
|
||||||
|
def test_quota_get(self):
|
||||||
|
_quota = api.quota_create(self.context, 'project1', 'resource', 99)
|
||||||
|
quota_db = api.quota_get(self.context, 'project1', 'resource')
|
||||||
|
self._assertEqualObjects(_quota, quota_db)
|
||||||
|
|
||||||
|
def test_quota_get_all_by_project(self):
|
||||||
|
for i in range(3):
|
||||||
|
for j in range(3):
|
||||||
|
api.quota_create(self.context, 'proj%d' % i, 'res%d' % j, j)
|
||||||
|
for i in range(3):
|
||||||
|
quotas_db = api.quota_get_all_by_project(self.context,
|
||||||
|
'proj%d' % i)
|
||||||
|
self.assertEqual({'project_id': 'proj%d' % i,
|
||||||
|
'res0': 0,
|
||||||
|
'res1': 1,
|
||||||
|
'res2': 2}, quotas_db)
|
||||||
|
|
||||||
|
def test_quota_update(self):
|
||||||
|
api.quota_create(self.context, 'project1', 'resource1', 41)
|
||||||
|
api.quota_update(self.context, 'project1', 'resource1', 42)
|
||||||
|
_quota = api.quota_get(self.context, 'project1', 'resource1')
|
||||||
|
self.assertEqual(42, _quota.hard_limit)
|
||||||
|
self.assertEqual('resource1', _quota.resource)
|
||||||
|
self.assertEqual('project1', _quota.project_id)
|
||||||
|
|
||||||
|
def test_quota_update_nonexistent(self):
|
||||||
|
self.assertRaises(exceptions.ProjectQuotaNotFound,
|
||||||
|
api.quota_update,
|
||||||
|
self.context,
|
||||||
|
'project1',
|
||||||
|
'resource1',
|
||||||
|
42)
|
||||||
|
|
||||||
|
def test_quota_get_nonexistent(self):
|
||||||
|
self.assertRaises(exceptions.ProjectQuotaNotFound,
|
||||||
|
api.quota_get,
|
||||||
|
self.context,
|
||||||
|
'project1',
|
||||||
|
'resource1')
|
||||||
|
|
||||||
|
def test_quota_reserve(self):
|
||||||
|
reservations = self._quota_reserve(self.context, 'project1')
|
||||||
|
self.assertEqual(2, len(reservations))
|
||||||
|
quota_usage = api.quota_usage_get_all_by_project(self.context,
|
||||||
|
'project1')
|
||||||
|
self.assertEqual({'project_id': 'project1',
|
||||||
|
'gigabytes': {'reserved': 2, 'in_use': 0},
|
||||||
|
'volumes': {'reserved': 1, 'in_use': 0}},
|
||||||
|
quota_usage)
|
||||||
|
|
||||||
|
def test_quota_destroy(self):
|
||||||
|
api.quota_create(self.context, 'project1', 'resource1', 41)
|
||||||
|
self.assertIsNone(api.quota_destroy(self.context, 'project1',
|
||||||
|
'resource1'))
|
||||||
|
self.assertRaises(exceptions.ProjectQuotaNotFound, api.quota_get,
|
||||||
|
self.context, 'project1', 'resource1')
|
||||||
|
|
||||||
|
def test_quota_destroy_by_project(self):
|
||||||
|
# Create limits, reservations and usage for project
|
||||||
|
project = 'project1'
|
||||||
|
self._quota_reserve(self.context, project)
|
||||||
|
expected_usage = {'project_id': project,
|
||||||
|
'volumes': {'reserved': 1, 'in_use': 0},
|
||||||
|
'gigabytes': {'reserved': 2, 'in_use': 0}}
|
||||||
|
expected = {'project_id': project, 'gigabytes': 2, 'volumes': 1}
|
||||||
|
|
||||||
|
# Check that quotas are there
|
||||||
|
self.assertEqual(expected,
|
||||||
|
api.quota_get_all_by_project(self.context, project))
|
||||||
|
self.assertEqual(expected_usage,
|
||||||
|
api.quota_usage_get_all_by_project(self.context,
|
||||||
|
project))
|
||||||
|
|
||||||
|
# Destroy only the limits
|
||||||
|
api.quota_destroy_by_project(self.context, project)
|
||||||
|
|
||||||
|
# Confirm that limits have been removed
|
||||||
|
self.assertEqual({'project_id': project},
|
||||||
|
api.quota_get_all_by_project(self.context, project))
|
||||||
|
|
||||||
|
# But that usage and reservations are the same
|
||||||
|
self.assertEqual(expected_usage,
|
||||||
|
api.quota_usage_get_all_by_project(self.context,
|
||||||
|
project))
|
||||||
|
|
||||||
|
def test_quota_destroy_sqlalchemy_all_by_project_(self):
|
||||||
|
# Create limits, reservations and usage for project
|
||||||
|
project = 'project1'
|
||||||
|
self._quota_reserve(self.context, project)
|
||||||
|
expected_usage = {'project_id': project,
|
||||||
|
'volumes': {'reserved': 1, 'in_use': 0},
|
||||||
|
'gigabytes': {'reserved': 2, 'in_use': 0}}
|
||||||
|
expected = {'project_id': project, 'gigabytes': 2, 'volumes': 1}
|
||||||
|
expected_result = {'project_id': project}
|
||||||
|
|
||||||
|
# Check that quotas are there
|
||||||
|
self.assertEqual(expected,
|
||||||
|
api.quota_get_all_by_project(self.context, project))
|
||||||
|
self.assertEqual(expected_usage,
|
||||||
|
api.quota_usage_get_all_by_project(self.context,
|
||||||
|
project))
|
||||||
|
|
||||||
|
# Destroy all quotas using SQLAlchemy Implementation
|
||||||
|
api.quota_destroy_all_by_project(self.context, project,
|
||||||
|
only_quotas=False)
|
||||||
|
|
||||||
|
# Check that all quotas have been deleted
|
||||||
|
self.assertEqual(expected_result,
|
||||||
|
api.quota_get_all_by_project(self.context, project))
|
||||||
|
self.assertEqual(expected_result,
|
||||||
|
api.quota_usage_get_all_by_project(self.context,
|
||||||
|
project))
|
||||||
|
|
||||||
|
def test_quota_usage_get_nonexistent(self):
|
||||||
|
self.assertRaises(exceptions.QuotaUsageNotFound,
|
||||||
|
api.quota_usage_get,
|
||||||
|
self.context,
|
||||||
|
'p1',
|
||||||
|
'nonexitent_resource')
|
||||||
|
|
||||||
|
def test_quota_usage_get(self):
|
||||||
|
self._quota_reserve(self.context, 'p1')
|
||||||
|
quota_usage = api.quota_usage_get(self.context, 'p1', 'gigabytes')
|
||||||
|
expected = {'resource': 'gigabytes', 'project_id': 'p1',
|
||||||
|
'in_use': 0, 'reserved': 2, 'total': 2}
|
||||||
|
for key, value in expected.items():
|
||||||
|
self.assertEqual(value, quota_usage[key], key)
|
||||||
|
|
||||||
|
def test_quota_usage_get_all_by_project(self):
|
||||||
|
self._quota_reserve(self.context, 'p1')
|
||||||
|
expected = {'project_id': 'p1',
|
||||||
|
'volumes': {'in_use': 0, 'reserved': 1},
|
||||||
|
'gigabytes': {'in_use': 0, 'reserved': 2}}
|
||||||
|
self.assertEqual(expected, api.quota_usage_get_all_by_project(
|
||||||
|
self.context, 'p1'))
|
||||||
|
@ -14,6 +14,7 @@
|
|||||||
# under the License.
|
# under the License.
|
||||||
|
|
||||||
|
|
||||||
|
import datetime
|
||||||
import inspect
|
import inspect
|
||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
@ -47,6 +48,8 @@ def _get_field_value(column):
|
|||||||
return 1.0
|
return 1.0
|
||||||
elif isinstance(column.type, sql.Boolean):
|
elif isinstance(column.type, sql.Boolean):
|
||||||
return True
|
return True
|
||||||
|
elif isinstance(column.type, sql.DateTime):
|
||||||
|
return datetime.datetime.utcnow()
|
||||||
else:
|
else:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@ -222,8 +225,9 @@ class ModelsTest(unittest.TestCase):
|
|||||||
with self.context.session.begin():
|
with self.context.session.begin():
|
||||||
core.create_resource(
|
core.create_resource(
|
||||||
self.context, model_class, create_dict)
|
self.context, model_class, create_dict)
|
||||||
except Exception:
|
except Exception as e:
|
||||||
self.fail('test_resources raised Exception unexpectedly')
|
msg = str(e)
|
||||||
|
self.fail('test_resources raised Exception unexpectedly %s' % msg)
|
||||||
|
|
||||||
def test_resource_routing_unique_key(self):
|
def test_resource_routing_unique_key(self):
|
||||||
pod = {'pod_id': 'test_pod1_uuid',
|
pod = {'pod_id': 'test_pod1_uuid',
|
||||||
|
Loading…
Reference in New Issue
Block a user