445 lines
17 KiB
Python
445 lines
17 KiB
Python
# Copyright 2017 Fiberhome Integration Technologies Co.,LTD
|
|
# All Rights Reserved.
|
|
# Copyright 2010 United States Government as represented by the
|
|
# Administrator of the National Aeronautics and Space Administration.
|
|
# 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.
|
|
|
|
'''
|
|
Quotas for servers.
|
|
Leverages cinder/quota.py
|
|
'''
|
|
|
|
import datetime
|
|
|
|
from oslo_config import cfg
|
|
from oslo_utils import timeutils
|
|
from oslo_versionedobjects import base as object_base
|
|
import six
|
|
from stevedore import driver
|
|
|
|
from mogan.common import exception
|
|
from mogan.db import api as dbapi
|
|
from mogan.objects import base
|
|
from mogan.objects import fields as object_fields
|
|
|
|
|
|
CONF = cfg.CONF
|
|
|
|
|
|
@base.MoganObjectRegistry.register
|
|
class Quota(base.MoganObject, object_base.VersionedObjectDictCompat):
|
|
# Version 1.0: Initial version
|
|
VERSION = '1.0'
|
|
|
|
dbapi = dbapi.get_instance()
|
|
|
|
fields = {
|
|
'id': object_fields.IntegerField(),
|
|
'project_id': object_fields.UUIDField(nullable=True),
|
|
'resource_name': object_fields.StringField(nullable=True),
|
|
'hard_limit': object_fields.IntegerField(nullable=True),
|
|
'allocated': object_fields.IntegerField(default=0),
|
|
}
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
super(Quota, self).__init__(*args, **kwargs)
|
|
self.quota_driver = driver.DriverManager(
|
|
'mogan.quota.backend_driver', CONF.quota.quota_driver,
|
|
invoke_on_load=True).driver
|
|
self._resources = {}
|
|
|
|
@property
|
|
def resources(self):
|
|
return self._resources
|
|
|
|
@staticmethod
|
|
def _from_db_object_list(db_objects, cls, context):
|
|
"""Converts a list of database entities to a list of formal objects."""
|
|
return [Quota._from_db_object(cls(context), obj)
|
|
for obj in db_objects]
|
|
|
|
@classmethod
|
|
def list(cls, context, project_only=False):
|
|
"""Return a list of Quota objects."""
|
|
db_quotas = cls.dbapi.quota_get_all(context,
|
|
project_only=project_only)
|
|
return Quota._from_db_object_list(db_quotas, cls, context)
|
|
|
|
@classmethod
|
|
def get(cls, context, project_id, resource_name):
|
|
"""Find a quota of resource and return a Quota object."""
|
|
db_quota = cls.dbapi.quota_get(context, project_id, resource_name)
|
|
quota = Quota._from_db_object(cls(context), db_quota)
|
|
return quota
|
|
|
|
def create(self, context):
|
|
"""Create a Quota record in the DB."""
|
|
values = self.obj_get_changes()
|
|
# Since we need to avoid passing False down to the DB layer
|
|
# (which uses an integer), we can always default it to zero here.
|
|
values['deleted'] = 0
|
|
|
|
db_quota = self.dbapi.quota_create(context, values)
|
|
self._from_db_object(self, db_quota)
|
|
|
|
def destroy(self, context, project_id, resource_name):
|
|
"""Delete the Quota from the DB."""
|
|
self.dbapi.quota_destroy(context, project_id, resource_name)
|
|
self.obj_reset_changes()
|
|
|
|
def save(self, context, project_id, resource_name):
|
|
"""Save updates to this Quota."""
|
|
updates = self.obj_get_changes()
|
|
self.dbapi.quota_update(context, project_id, resource_name, updates)
|
|
self.obj_reset_changes()
|
|
|
|
def refresh(self, context, project_id, resource_name):
|
|
"""Refresh the object by re-fetching from the DB."""
|
|
current = self.__class__.get(context, project_id, resource_name)
|
|
self.obj_refresh(current)
|
|
self.obj_reset_changes()
|
|
|
|
def reserve(self, context, expire=None, project_id=None, **deltas):
|
|
"""reserve the Quota."""
|
|
return self.quota_driver.reserve(context, self.resources, deltas,
|
|
expire=expire, project_id=project_id)
|
|
|
|
def commit(self, context, reservations, project_id=None):
|
|
self.quota_driver.commit(context, reservations, project_id=project_id)
|
|
|
|
def rollback(self, context, reservations, project_id=None):
|
|
self.quota_driver.rollback(context, reservations,
|
|
project_id=project_id)
|
|
|
|
def expire(self, context):
|
|
return self.quota_driver.expire(context)
|
|
|
|
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 exception.QuotaResourceUnknown(unknown=[resource])
|
|
|
|
return res.count(context, *args, **kwargs)
|
|
|
|
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_quota_limit_and_usage(self, context, resources, project_id):
|
|
return self.quota_driver.get_project_quotas(context, resources,
|
|
project_id, usages=True)
|
|
|
|
|
|
class DbQuotaDriver(object):
|
|
|
|
"""Driver to perform check to enforcement of quotas.
|
|
|
|
Also allows to obtain quota information.
|
|
The default driver utilizes the local database.
|
|
"""
|
|
|
|
dbapi = dbapi.get_instance()
|
|
|
|
def get_project_quotas(self, context, resources, project_id, usages=True):
|
|
"""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 usages: If True, the current in_use, reserved and allocated
|
|
counts will also be returned.
|
|
"""
|
|
|
|
quotas = {}
|
|
project_quotas = {}
|
|
res = self.dbapi.quota_get_all_by_project(context, project_id)
|
|
for p_quota in res:
|
|
project_quotas[p_quota.resource_name] = p_quota.hard_limit
|
|
if project_quotas == {}:
|
|
servers_hard_limit = CONF.quota.servers_hard_limit
|
|
self.dbapi.quota_create(context, {'resource_name': 'servers',
|
|
'project_id': project_id,
|
|
'hard_limit': servers_hard_limit,
|
|
'allocated': 0})
|
|
project_quotas['servers'] = servers_hard_limit
|
|
kpairs_hard_limit = CONF.quota.keypairs_hard_limit
|
|
self.dbapi.quota_create(context, {'resource_name': 'keypairs',
|
|
'project_id': project_id,
|
|
'hard_limit': kpairs_hard_limit,
|
|
'allocated': 0})
|
|
project_quotas['keypairs'] = kpairs_hard_limit
|
|
allocated_quotas = None
|
|
if usages:
|
|
project_usages = self.dbapi.quota_usage_get_all_by_project(
|
|
context, project_id)
|
|
allocated_quotas = self.dbapi.quota_allocated_get_all_by_project(
|
|
context, project_id)
|
|
allocated_quotas.pop('project_id')
|
|
|
|
for resource in resources.values():
|
|
if resource.name not in project_quotas:
|
|
continue
|
|
|
|
quota_val = project_quotas.get(resource.name)
|
|
if quota_val is None:
|
|
raise exception.QuotaNotFound(quota_name=resource.name)
|
|
quotas[resource.name] = {'limit': quota_val}
|
|
|
|
# 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 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):
|
|
"""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.
|
|
"""
|
|
|
|
# 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 exception.QuotaResourceUnknown(unknown=sorted(unknown))
|
|
|
|
# Grab and return the quotas (without usages)
|
|
quotas = self.get_project_quotas(context, sub_resources,
|
|
project_id, usages=False)
|
|
|
|
return {k: v['limit'] for k, v in quotas.items()}
|
|
|
|
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 exception.InvalidReservationExpiration(expire=expire)
|
|
|
|
# If project_id is None, then we use the project_id in context
|
|
if project_id is None:
|
|
project_id = context.tenant
|
|
|
|
# Get the applicable quotas.
|
|
quotas = self._get_quotas(context, resources, deltas.keys(),
|
|
has_sync=True, project_id=project_id)
|
|
|
|
return self._reserve(context, resources, quotas, deltas, expire,
|
|
project_id)
|
|
|
|
def _reserve(self, context, resources, quotas, deltas, expire, project_id):
|
|
return self.dbapi.quota_reserve(context, resources, quotas, deltas,
|
|
expire,
|
|
CONF.quota.until_refresh,
|
|
CONF.quota.max_age,
|
|
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.tenant
|
|
|
|
self.dbapi.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.tenant
|
|
|
|
self.dbapi.reservation_rollback(context, reservations,
|
|
project_id=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.dbapi.reservation_expire(context)
|
|
|
|
|
|
class BaseResource(object):
|
|
"""Describe a single resource for quota checking."""
|
|
|
|
def __init__(self, name, sync, count=None):
|
|
"""Initializes a Resource.
|
|
|
|
:param name: The name of the resource, i.e., "servers".
|
|
:param sync: A dbapi methods name which returns a dictionary
|
|
to resynchronize the in_use count for one or more
|
|
resources, as described above.
|
|
"""
|
|
self.name = name
|
|
self.sync = sync
|
|
self.count = count
|
|
|
|
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.
|
|
"""
|
|
|
|
# Get the project ID
|
|
project_id = kwargs.get('project_id', context.tenant)
|
|
|
|
# Look up the quota for the project
|
|
if project_id:
|
|
try:
|
|
return driver.get_by_project(context, project_id, self.name)
|
|
except exception.ProjectQuotaNotFound:
|
|
pass
|
|
return -1
|
|
|
|
|
|
class ServerResource(BaseResource):
|
|
"""ReservableResource for a specific server."""
|
|
|
|
def __init__(self):
|
|
"""Initializes a ServerResource.
|
|
|
|
"""
|
|
self.name = 'servers'
|
|
super(ServerResource, self).__init__(self.name,
|
|
"_sync_%s" % self.name)
|
|
|
|
|
|
class KeyPairResource(BaseResource):
|
|
"""ReservableResource for a specific keypair."""
|
|
|
|
def __init__(self):
|
|
"""Initializes a KeyPairResource.
|
|
|
|
"""
|
|
self.name = 'keypairs'
|
|
super(KeyPairResource, self).__init__(self.name,
|
|
"_sync_%s" % self.name)
|