Merge "Quota support in Mogan(part 1)"
This commit is contained in:
commit
20fd002744
@ -300,4 +300,38 @@ class InstanceIsLocked(Invalid):
|
||||
msg_fmt = _("Instance %(instance_uuid)s is locked")
|
||||
|
||||
|
||||
class InvalidReservationExpiration(Invalid):
|
||||
message = _("Invalid reservation expiration %(expire)s.")
|
||||
|
||||
|
||||
class QuotaNotFound(NotFound):
|
||||
message = _("Quota %(quota_name)s could not be found.")
|
||||
|
||||
|
||||
class ProjectQuotaNotFound(QuotaNotFound):
|
||||
message = _("Quota for project %(project_id)s could not be found.")
|
||||
|
||||
|
||||
class QuotaResourceUnknown(QuotaNotFound):
|
||||
message = _("Unknown quota resources %(unknown)s.")
|
||||
|
||||
|
||||
class OverQuota(MoganException):
|
||||
message = _("Quota exceeded for resources: %(overs)s")
|
||||
|
||||
|
||||
class QuotaAlreadyExists(MoganException):
|
||||
_msg_fmt = _("Quota with name %(name)s and project %(project_id)s already"
|
||||
" exists.")
|
||||
|
||||
|
||||
class ReservationAlreadyExists(MoganException):
|
||||
_msg_fmt = _("Reservation with name %(name)s and project %(project_id)s "
|
||||
"already exists.")
|
||||
|
||||
|
||||
class ReservationNotFound(NotFound):
|
||||
message = _("Reservation %(uuid)s could not be found.")
|
||||
|
||||
|
||||
ObjectActionError = obj_exc.ObjectActionError
|
||||
|
@ -62,7 +62,27 @@ opts = [
|
||||
opt_group = cfg.OptGroup(name='api',
|
||||
title='Options for the mogan-api service')
|
||||
|
||||
quota_opts = [
|
||||
cfg.StrOpt('quota_driver',
|
||||
help=_("Specify the quota driver which is used in Mogan "
|
||||
"service.")),
|
||||
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')),
|
||||
cfg.IntOpt('max_age',
|
||||
default=0,
|
||||
help=_('Number of seconds between subsequent usage refreshes')),
|
||||
]
|
||||
|
||||
opt_quota_group = cfg.OptGroup(name='quota',
|
||||
title='Options for the mogan quota')
|
||||
|
||||
|
||||
def register_opts(conf):
|
||||
conf.register_group(opt_group)
|
||||
conf.register_opts(opts, group=opt_group)
|
||||
conf.register_group(opt_quota_group)
|
||||
conf.register_opts(quota_opts, group=opt_quota_group)
|
||||
|
@ -123,3 +123,52 @@ class Connection(object):
|
||||
@abc.abstractmethod
|
||||
def instance_fault_get_by_instance_uuids(self, context, instance_uuids):
|
||||
"""Get all instance faults for the provided instance_uuids."""
|
||||
|
||||
@abc.abstractmethod
|
||||
def quota_get(self, context, project_id, resource_name):
|
||||
"""Get quota value of a resource"""
|
||||
|
||||
@abc.abstractmethod
|
||||
def quota_get_all(self, context, project_only=False):
|
||||
"""Get all quotas value of resources"""
|
||||
|
||||
@abc.abstractmethod
|
||||
def quota_create(self, context, values):
|
||||
"""Create a quota of a resource"""
|
||||
|
||||
@abc.abstractmethod
|
||||
def quota_destroy(self, context, project_id, resource_name):
|
||||
"""Delete a quota of a resource"""
|
||||
|
||||
@abc.abstractmethod
|
||||
def quota_update(self, context, project_id, resource_name, updates):
|
||||
"""Delete a quota of a resource"""
|
||||
|
||||
@abc.abstractmethod
|
||||
def quota_get_all_by_project(self, context, project_id):
|
||||
"""Get quota by project id"""
|
||||
|
||||
@abc.abstractmethod
|
||||
def quota_usage_get_all_by_project(self, context, project_id):
|
||||
"""Get quota usage by project id"""
|
||||
|
||||
@abc.abstractmethod
|
||||
def quota_allocated_get_all_by_project(self, context, project_id):
|
||||
"""Get quota usage by project id"""
|
||||
|
||||
@abc.abstractmethod
|
||||
def quota_reserve(self, context, resources, quotas, deltas, expire,
|
||||
until_refresh, max_age, project_id):
|
||||
"""Reserve quota of resource"""
|
||||
|
||||
@abc.abstractmethod
|
||||
def reservation_commit(self, context, reservations, project_id):
|
||||
"""Commit reservation of quota usage"""
|
||||
|
||||
@abc.abstractmethod
|
||||
def reservation_rollback(self, context, reservations, project_id):
|
||||
"""Reservation rollback"""
|
||||
|
||||
@abc.abstractmethod
|
||||
def reservation_expire(self, context):
|
||||
"""expire all reservations which has been expired"""
|
||||
|
@ -124,3 +124,55 @@ def upgrade():
|
||||
mysql_ENGINE='InnoDB',
|
||||
mysql_DEFAULT_CHARSET='UTF8'
|
||||
)
|
||||
op.create_table(
|
||||
'quotas',
|
||||
sa.Column('created_at', sa.DateTime(), nullable=True),
|
||||
sa.Column('updated_at', sa.DateTime(), nullable=True),
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('project_id', sa.String(length=36), nullable=True),
|
||||
sa.Column('resource_name', sa.String(length=255), nullable=True),
|
||||
sa.Column('hard_limit', sa.Integer(), nullable=True),
|
||||
sa.Column('allocated', sa.Integer(), nullable=True),
|
||||
sa.PrimaryKeyConstraint('id'),
|
||||
sa.UniqueConstraint('resource_name', 'project_id',
|
||||
name='uniq_quotas0resource_name'),
|
||||
mysql_ENGINE='InnoDB',
|
||||
mysql_DEFAULT_CHARSET='UTF8'
|
||||
)
|
||||
op.create_table(
|
||||
'quota_usages',
|
||||
sa.Column('created_at', sa.DateTime(), nullable=True),
|
||||
sa.Column('updated_at', sa.DateTime(), nullable=True),
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('project_id', sa.String(length=36), nullable=True),
|
||||
sa.Column('resource_name', sa.String(length=255), nullable=True),
|
||||
sa.Column('in_use', sa.Integer(), nullable=True),
|
||||
sa.Column('reserved', sa.Integer(), nullable=True),
|
||||
sa.Column('until_refresh', sa.Integer(), nullable=True),
|
||||
sa.PrimaryKeyConstraint('id'),
|
||||
sa.UniqueConstraint('resource_name', 'project_id',
|
||||
name='uniq_quotas0resource_name'),
|
||||
mysql_ENGINE='InnoDB',
|
||||
mysql_DEFAULT_CHARSET='UTF8'
|
||||
)
|
||||
op.create_table(
|
||||
'reservations',
|
||||
sa.Column('created_at', sa.DateTime(), nullable=True),
|
||||
sa.Column('updated_at', sa.DateTime(), nullable=True),
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('uuid', sa.String(length=36), nullable=True),
|
||||
sa.Column('usage_id', sa.Integer(), nullable=False),
|
||||
sa.Column('allocated_id', sa.Integer(), nullable=False),
|
||||
sa.Column('project_id', sa.String(length=36), nullable=True),
|
||||
sa.Column('resource_name', sa.String(length=255), nullable=True),
|
||||
sa.Column('delta', sa.Integer(), nullable=True),
|
||||
sa.Column('expire', sa.DateTime(), nullable=True),
|
||||
sa.ForeignKeyConstraint(['usage_id'],
|
||||
['quota_usages.id']),
|
||||
sa.ForeignKeyConstraint(['allocated_id'],
|
||||
['quotas.id']),
|
||||
sa.PrimaryKeyConstraint('id'),
|
||||
sa.UniqueConstraint('uuid', name='uniq_reservation0uuid'),
|
||||
mysql_ENGINE='InnoDB',
|
||||
mysql_DEFAULT_CHARSET='UTF8'
|
||||
)
|
||||
|
@ -20,19 +20,22 @@ import threading
|
||||
from oslo_db import exception as db_exc
|
||||
from oslo_db.sqlalchemy import enginefacade
|
||||
from oslo_db.sqlalchemy import utils as sqlalchemyutils
|
||||
from oslo_log import log as logging
|
||||
from oslo_utils import strutils
|
||||
from oslo_utils import timeutils
|
||||
from oslo_utils import uuidutils
|
||||
from sqlalchemy.orm.exc import NoResultFound
|
||||
from sqlalchemy.orm import joinedload
|
||||
from sqlalchemy.sql.expression import desc
|
||||
|
||||
from mogan.common import exception
|
||||
from mogan.common.i18n import _
|
||||
from mogan.common.i18n import _, _LW
|
||||
from mogan.db import api
|
||||
from mogan.db.sqlalchemy import models
|
||||
|
||||
|
||||
_CONTEXT = threading.local()
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def get_backend():
|
||||
@ -109,6 +112,7 @@ class Connection(api.Connection):
|
||||
"""SqlAlchemy connection."""
|
||||
|
||||
def __init__(self):
|
||||
self.QUOTA_SYNC_FUNCTIONS = {'_sync_instances': self._sync_instances}
|
||||
pass
|
||||
|
||||
def instance_type_create(self, context, values):
|
||||
@ -346,6 +350,325 @@ class Connection(api.Connection):
|
||||
|
||||
return output
|
||||
|
||||
def quota_get(self, context, project_id, resource_name):
|
||||
query = model_query(
|
||||
context,
|
||||
models.Quota).filter_by(project_id=project_id,
|
||||
resource_name=resource_name)
|
||||
try:
|
||||
return query.one()
|
||||
except NoResultFound:
|
||||
raise exception.QuotaNotFound(quota_name=resource_name)
|
||||
|
||||
def quota_create(self, context, values):
|
||||
quota = models.Quota()
|
||||
quota.update(values)
|
||||
|
||||
with _session_for_write() as session:
|
||||
try:
|
||||
session.add(quota)
|
||||
session.flush()
|
||||
except db_exc.DBDuplicateEntry:
|
||||
project_id = values['project_id']
|
||||
raise exception.QuotaAlreadyExists(name=values['name'],
|
||||
project_id=project_id)
|
||||
return quota
|
||||
|
||||
def quota_get_all(self, context, project_only):
|
||||
return model_query(context, models.Quota, project_only=project_only)
|
||||
|
||||
def quota_destroy(self, context, project_id, resource_name):
|
||||
with _session_for_write():
|
||||
query = model_query(context, models.Quota)
|
||||
query = query.filter_by(project_id=project_id,
|
||||
resource_name=resource_name)
|
||||
|
||||
count = query.delete()
|
||||
if count != 1:
|
||||
raise exception.QuotaNotFound(quota_name=resource_name)
|
||||
|
||||
def _do_update_quota(self, context, project_id, resource_name, updates):
|
||||
with _session_for_write():
|
||||
query = model_query(context, models.Quota)
|
||||
query = query.filter_by(project_id=project_id,
|
||||
resource_name=resource_name)
|
||||
try:
|
||||
ref = query.with_lockmode('update').one()
|
||||
except NoResultFound:
|
||||
raise exception.QuotaNotFound(quota_name=resource_name)
|
||||
|
||||
ref.update(updates)
|
||||
return ref
|
||||
|
||||
def quota_update(self, context, project_id, resource_name, updates):
|
||||
if 'resource_name' in updates or 'project_id' in updates:
|
||||
msg = _("Cannot overwrite resource_name/project_id for "
|
||||
"an existing Quota.")
|
||||
raise exception.InvalidParameterValue(err=msg)
|
||||
try:
|
||||
return self._do_update_quota(context, project_id, resource_name,
|
||||
updates)
|
||||
except db_exc.DBDuplicateEntry:
|
||||
pass
|
||||
|
||||
def quota_get_all_by_project(self, context, project_id):
|
||||
return model_query(context, models.Quota, project_id=project_id)
|
||||
|
||||
def quota_usage_get_all_by_project(self, context, project_id):
|
||||
rows = model_query(context, models.QuotaUsage,
|
||||
project_id=project_id)
|
||||
result = {'project_id': project_id}
|
||||
for row in rows:
|
||||
result[row.resource_name] = dict(in_use=row.in_use,
|
||||
reserved=row.reserved)
|
||||
return result
|
||||
|
||||
def quota_allocated_get_all_by_project(self, context, project_id):
|
||||
rows = model_query(context, models.Quota,
|
||||
project_id=project_id)
|
||||
result = {'project_id': project_id}
|
||||
for row in rows:
|
||||
result[row.resource_name] = row.allocated
|
||||
return result
|
||||
|
||||
def _get_quota_usages(self, context, project_id):
|
||||
# Broken out for testability
|
||||
rows = model_query(context, models.QuotaUsage,
|
||||
project_id=project_id).\
|
||||
order_by(models.QuotaUsage.id.asc()).\
|
||||
with_lockmode('update').all()
|
||||
return {row.resource_name: row for row in rows}
|
||||
|
||||
def quota_allocated_update(self, context, project_id, resource, allocated):
|
||||
with _session_for_write():
|
||||
quota_ref = self.quota_get(context, project_id, resource)
|
||||
quota_ref.update({'allocated': allocated})
|
||||
return quota_ref
|
||||
|
||||
def _quota_usage_create(self, context, project_id, resource, in_use,
|
||||
reserved, until_refresh, session=None):
|
||||
quota_usage_ref = models.QuotaUsage()
|
||||
quota_usage_ref.project_id = project_id
|
||||
quota_usage_ref.resource_name = resource
|
||||
quota_usage_ref.in_use = in_use
|
||||
quota_usage_ref.reserved = reserved
|
||||
quota_usage_ref.until_refresh = until_refresh
|
||||
try:
|
||||
session.add(quota_usage_ref)
|
||||
session.flush()
|
||||
except db_exc.DBDuplicateEntry:
|
||||
raise exception.QuotaAlreadyExists(name=resource,
|
||||
project_id=project_id)
|
||||
return quota_usage_ref
|
||||
|
||||
def _reservation_create(self, context, uuid, usage, project_id, resource,
|
||||
delta, expire, session=None, allocated_id=None):
|
||||
usage_id = usage['id'] if usage else None
|
||||
reservation_ref = models.Reservation()
|
||||
reservation_ref.uuid = uuid
|
||||
reservation_ref.usage_id = usage_id
|
||||
reservation_ref.project_id = project_id
|
||||
reservation_ref.resource_name = resource
|
||||
reservation_ref.delta = delta
|
||||
reservation_ref.expire = expire
|
||||
reservation_ref.allocated_id = allocated_id
|
||||
try:
|
||||
session.add(reservation_ref)
|
||||
session.flush()
|
||||
except db_exc.DBDuplicateEntry:
|
||||
raise exception.ReservationAlreadyExists(name=resource,
|
||||
project_id=project_id)
|
||||
return reservation_ref
|
||||
|
||||
def _sync_instances(self, context, project_id):
|
||||
query = model_query(context, models.Instance, instance=True).\
|
||||
filter_by(project_id=project_id).all()
|
||||
return {'instances': len(query) or 0}
|
||||
|
||||
def quota_reserve(self, context, resources, quotas, deltas, expire,
|
||||
until_refresh, max_age, project_id,
|
||||
is_allocated_reserve=False):
|
||||
# NOTE(wanghao): Now we still doesn't support contenxt.elevated() yet.
|
||||
# We can support it later.
|
||||
elevated = context
|
||||
with _session_for_write() as session:
|
||||
if project_id is None:
|
||||
project_id = context.project_id
|
||||
# Get the current usages
|
||||
usages = self._get_quota_usages(context, project_id)
|
||||
allocated = self.quota_allocated_get_all_by_project(context,
|
||||
project_id)
|
||||
allocated.pop('project_id')
|
||||
|
||||
# Handle usage refresh
|
||||
work = set(deltas.keys())
|
||||
while work:
|
||||
resource = work.pop()
|
||||
|
||||
# Do we need to refresh the usage?
|
||||
refresh = False
|
||||
if resource not in usages:
|
||||
usages[resource] = self._quota_usage_create(
|
||||
elevated, project_id, resource, 0, 0,
|
||||
until_refresh or None, session=session)
|
||||
refresh = True
|
||||
elif usages[resource].in_use < 0:
|
||||
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
|
||||
|
||||
# OK, refresh the usage
|
||||
if refresh:
|
||||
# Grab the sync routine
|
||||
sync = self.QUOTA_SYNC_FUNCTIONS[resources[resource].sync]
|
||||
updates = sync(elevated, project_id)
|
||||
for res, in_use in updates.items():
|
||||
# Make sure we have a destination for the usage!
|
||||
if res not in usages:
|
||||
usages[res] = self._quota_usage_create(
|
||||
elevated, project_id, res, 0, 0,
|
||||
until_refresh or None, session=session)
|
||||
|
||||
# Update the usage
|
||||
usages[res].in_use = in_use
|
||||
usages[res].until_refresh = until_refresh or None
|
||||
|
||||
# Because more than one resource may be refreshed
|
||||
# by the call to the sync routine, and we don't
|
||||
# want to double-sync, we make sure all refreshed
|
||||
# resources are dropped from the work set.
|
||||
work.discard(res)
|
||||
|
||||
# Check for deltas that would go negative
|
||||
if is_allocated_reserve:
|
||||
unders = [r for r, delta in deltas.items()
|
||||
if delta < 0 and delta + allocated.get(r, 0) < 0]
|
||||
else:
|
||||
unders = [r for r, delta in deltas.items()
|
||||
if delta < 0 and delta + usages[r].in_use < 0]
|
||||
|
||||
# Now, let's check the quotas
|
||||
overs = [r for r, delta in deltas.items()
|
||||
if quotas[r] >= 0 and delta >= 0 and
|
||||
quotas[r] < delta + usages[r].total + allocated.get(r, 0)]
|
||||
|
||||
# Create the reservations
|
||||
if not overs:
|
||||
reservations = []
|
||||
for resource, delta in deltas.items():
|
||||
usage = usages[resource]
|
||||
allocated_id = None
|
||||
if is_allocated_reserve:
|
||||
try:
|
||||
quota = self.quota_get(context, project_id,
|
||||
resource)
|
||||
except exception.ProjectQuotaNotFound:
|
||||
# If we were using the default quota, create DB
|
||||
# entry
|
||||
quota = self.quota_create(context,
|
||||
project_id,
|
||||
resource,
|
||||
quotas[resource], 0)
|
||||
# Since there's no reserved/total for allocated, update
|
||||
# allocated immediately and subtract on rollback
|
||||
# if needed
|
||||
self.quota_allocated_update(context, project_id,
|
||||
resource,
|
||||
quota.allocated + delta)
|
||||
allocated_id = quota.id
|
||||
usage = None
|
||||
reservation = self._reservation_create(
|
||||
elevated, uuidutils.generate_uuid(), usage, project_id,
|
||||
resource, delta, expire, session=session,
|
||||
allocated_id=allocated_id)
|
||||
|
||||
reservations.append(reservation)
|
||||
|
||||
# Also update the reserved quantity
|
||||
if delta > 0 and not is_allocated_reserve:
|
||||
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,
|
||||
allocated=allocated.get(k, 0))
|
||||
for k, v in usages.items()}
|
||||
raise exception.OverQuota(overs=sorted(overs), quotas=quotas,
|
||||
usages=usages)
|
||||
return reservations
|
||||
|
||||
def _dict_with_usage_id(self, usages):
|
||||
return {row.id: row for row in usages.values()}
|
||||
|
||||
def reservation_commit(self, context, reservations, project_id):
|
||||
with _session_for_write():
|
||||
usages = self._get_quota_usages(context, project_id)
|
||||
usages = self._dict_with_usage_id(usages)
|
||||
|
||||
for reservation in reservations:
|
||||
# Allocated reservations will have already been bumped
|
||||
if not reservation.allocated_id:
|
||||
usage = usages[reservation.usage_id]
|
||||
if reservation.delta >= 0:
|
||||
usage.reserved -= reservation.delta
|
||||
usage.in_use += reservation.delta
|
||||
|
||||
query = model_query(context, models.Reservation)
|
||||
query = query.filter_by(uuid=reservation.uuid)
|
||||
count = query.delete()
|
||||
if count != 1:
|
||||
raise exception.ReservationNotFound(uuid=reservation.uuid)
|
||||
|
||||
def reservation_rollback(self, context, reservations, project_id):
|
||||
with _session_for_write():
|
||||
usages = self._get_quota_usages(context, project_id)
|
||||
usages = self._dict_with_usage_id(usages)
|
||||
for reservation in reservations:
|
||||
if reservation.allocated_id:
|
||||
reservation.quota.allocated -= reservation.delta
|
||||
else:
|
||||
usage = usages[reservation.usage_id]
|
||||
if reservation.delta >= 0:
|
||||
usage.reserved -= reservation.delta
|
||||
|
||||
query = model_query(context, models.Reservation)
|
||||
query = query.filter_by(uuid=reservation.uuid)
|
||||
count = query.delete()
|
||||
if count != 1:
|
||||
raise exception.ReservationNotFound(uuid=reservation.uuid)
|
||||
|
||||
def reservation_expire(self, context):
|
||||
with _session_for_write() as session:
|
||||
current_time = timeutils.utcnow()
|
||||
results = model_query(context, models.Reservation).\
|
||||
filter(models.Reservation.expire < current_time).\
|
||||
all()
|
||||
|
||||
if results:
|
||||
for reservation in results:
|
||||
if reservation.delta >= 0:
|
||||
if reservation.allocated_id:
|
||||
reservation.quota.allocated -= reservation.delta
|
||||
reservation.quota.save(session=session)
|
||||
else:
|
||||
reservation.usage.reserved -= reservation.delta
|
||||
reservation.usage.save(session=session)
|
||||
|
||||
query = model_query(context, models.Reservation)
|
||||
query = query.filter_by(uuid=reservation.uuid)
|
||||
count = query.delete()
|
||||
if count != 1:
|
||||
uuid = reservation.uuid
|
||||
raise exception.ReservationNotFound(uuid=uuid)
|
||||
|
||||
|
||||
def _type_get_id_from_type_query(context, type_id):
|
||||
return model_query(context, models.InstanceTypes). \
|
||||
|
@ -186,3 +186,68 @@ class InstanceFault(Base):
|
||||
backref=orm.backref('instance_faults', uselist=False),
|
||||
foreign_keys=instance_uuid,
|
||||
primaryjoin='Instance.uuid == InstanceFault.instance_uuid')
|
||||
|
||||
|
||||
class Quota(Base):
|
||||
"""Represents a single quota override for a project."""
|
||||
|
||||
__tablename__ = 'quotas'
|
||||
__table_args__ = (
|
||||
schema.UniqueConstraint('resource_name', 'project_id',
|
||||
name='uniq_quotas0resource_name'),
|
||||
table_args()
|
||||
)
|
||||
id = Column(Integer, primary_key=True)
|
||||
resource_name = Column(String(255), nullable=False)
|
||||
project_id = Column(String(36), nullable=False)
|
||||
hard_limit = Column(Integer, nullable=False)
|
||||
allocated = Column(Integer, default=0)
|
||||
|
||||
|
||||
class QuotaUsage(Base):
|
||||
"""Represents the current usage for a given resource."""
|
||||
|
||||
__tablename__ = 'quota_usages'
|
||||
__table_args__ = (
|
||||
schema.UniqueConstraint('resource_name', 'project_id',
|
||||
name='uniq_quotas0resource_name'),
|
||||
table_args()
|
||||
)
|
||||
|
||||
id = Column(Integer, primary_key=True)
|
||||
project_id = Column(String(255), index=True)
|
||||
resource_name = Column(String(255))
|
||||
in_use = Column(Integer)
|
||||
reserved = Column(Integer)
|
||||
until_refresh = Column(Integer, nullable=True)
|
||||
|
||||
@property
|
||||
def total(self):
|
||||
return self.in_use + self.reserved
|
||||
|
||||
|
||||
class Reservation(Base):
|
||||
"""Represents a resource reservation for quotas."""
|
||||
|
||||
__tablename__ = 'reservations'
|
||||
__table_args__ = (
|
||||
schema.UniqueConstraint('uuid', name='uniq_reservation0uuid'),
|
||||
table_args()
|
||||
)
|
||||
|
||||
id = Column(Integer, primary_key=True)
|
||||
uuid = Column(String(36), nullable=False)
|
||||
usage_id = Column(Integer, ForeignKey('quota_usages.id'), nullable=True)
|
||||
allocated_id = Column(Integer, ForeignKey('quotas.id'), nullable=True)
|
||||
project_id = Column(String(255), index=True)
|
||||
resource_name = Column(String(255))
|
||||
delta = Column(Integer)
|
||||
expire = Column(DateTime, nullable=False)
|
||||
|
||||
usage = orm.relationship(
|
||||
"QuotaUsage", foreign_keys=usage_id,
|
||||
primaryjoin='Reservation.usage_id == QuotaUsage.id')
|
||||
|
||||
quota = orm.relationship(
|
||||
"Quota", foreign_keys=allocated_id,
|
||||
primaryjoin='Reservation.allocated_id == Quota.id')
|
||||
|
413
mogan/objects/quota.py
Normal file
413
mogan/objects/quota.py
Normal file
@ -0,0 +1,413 @@
|
||||
# Copyright 2017 Fiberhome Integration 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.
|
||||
|
||||
"""Quotas for instances."""
|
||||
|
||||
import datetime
|
||||
|
||||
from oslo_config import cfg
|
||||
from oslo_utils import importutils
|
||||
from oslo_utils import timeutils
|
||||
from oslo_versionedobjects import base as object_base
|
||||
import six
|
||||
|
||||
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 = importutils.import_object(CONF.quota.quota_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.reserver(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)
|
||||
|
||||
|
||||
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_project_quotas(self, context, resources, project_id,
|
||||
quota_class=None, defaults=True,
|
||||
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 quota_class: If project_id != context.tenant, the
|
||||
quota class cannot be determined. This
|
||||
parameter allows it to be specified. It
|
||||
will be ignored if project_id ==
|
||||
context.tenant.
|
||||
: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.
|
||||
"""
|
||||
|
||||
quotas = {}
|
||||
project_quotas = dbapi.quota_get_all_by_project(context, project_id)
|
||||
allocated_quotas = None
|
||||
if usages:
|
||||
project_usages = dbapi.quota_usage_get_all_by_project(context,
|
||||
project_id)
|
||||
allocated_quotas = dbapi.quota_allocated_get_all_by_project(
|
||||
context, project_id)
|
||||
allocated_quotas.pop('project_id')
|
||||
|
||||
for resource in resources.values():
|
||||
# Omit default/quota class values
|
||||
if not defaults and 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,
|
||||
context.quota_class, 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 dbapi.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.tenant
|
||||
|
||||
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
|
||||
|
||||
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.
|
||||
"""
|
||||
|
||||
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., "instances".
|
||||
: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 InstanceResource(BaseResource):
|
||||
"""ReservableResource for a specific instance."""
|
||||
|
||||
def __init__(self, name='instances'):
|
||||
"""Initializes a InstanceResource.
|
||||
|
||||
:param name: The kind of resource, i.e., "instances".
|
||||
"""
|
||||
super(InstanceResource, self).__init__(name, "_sync_%s" % name)
|
@ -230,6 +230,21 @@ class MigrationCheckersMixin(object):
|
||||
self.assertIn('created_at', col_names)
|
||||
self.assertIsInstance(nodes.c.provision_updated_at.type,
|
||||
sqlalchemy.types.DateTime)
|
||||
nodes = db_utils.get_table(engine, 'quotas')
|
||||
col_names = [column.name for column in nodes.c]
|
||||
self.assertIn('created_at', col_names)
|
||||
self.assertIsInstance(nodes.c.resource_name.type,
|
||||
sqlalchemy.types.String)
|
||||
nodes = db_utils.get_table(engine, 'quota_usages')
|
||||
col_names = [column.name for column in nodes.c]
|
||||
self.assertIn('created_at', col_names)
|
||||
self.assertIsInstance(nodes.c.resource_name.type,
|
||||
sqlalchemy.types.String)
|
||||
nodes = db_utils.get_table(engine, 'reservations')
|
||||
col_names = [column.name for column in nodes.c]
|
||||
self.assertIn('created_at', col_names)
|
||||
self.assertIsInstance(nodes.c.resource_name.type,
|
||||
sqlalchemy.types.String)
|
||||
|
||||
def test_upgrade_and_version(self):
|
||||
with patch_with_engine(self.engine):
|
||||
|
82
mogan/tests/unit/db/test_quota_usages.py
Normal file
82
mogan/tests/unit/db/test_quota_usages.py
Normal file
@ -0,0 +1,82 @@
|
||||
# Copyright 2018 FiberHome Telecommunication 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.
|
||||
|
||||
"""Tests for manipulating QuotaUsages via the DB API"""
|
||||
|
||||
import datetime
|
||||
|
||||
from oslo_config import cfg
|
||||
from oslo_context import context
|
||||
|
||||
from mogan.db import api as db_api
|
||||
from mogan.objects import quota
|
||||
from mogan.tests.unit.db import base
|
||||
from mogan.tests.unit.db import utils
|
||||
|
||||
|
||||
CONF = cfg.CONF
|
||||
|
||||
|
||||
class DbQuotaUsageTestCase(base.DbTestCase):
|
||||
|
||||
def setUp(self):
|
||||
super(DbQuotaUsageTestCase, self).setUp()
|
||||
self.context = context.get_admin_context()
|
||||
self.instance = quota.InstanceResource()
|
||||
self.resources = {self.instance.name: self.instance}
|
||||
self.project_id = "c18e8a1a870d4c08a0b51ced6e0b6459"
|
||||
|
||||
def test_quota_usage_reserve(self):
|
||||
utils.create_test_quota()
|
||||
dbapi = db_api.get_instance()
|
||||
r = dbapi.quota_reserve(self.context, self.resources,
|
||||
{'instances': 10},
|
||||
{'instances': 1},
|
||||
datetime.datetime(2099, 1, 1, 0, 0),
|
||||
CONF.quota.until_refresh, CONF.quota.max_age,
|
||||
project_id=self.project_id)
|
||||
self.assertEqual('instances', r[0].resource_name)
|
||||
|
||||
def test_reserve_commit(self):
|
||||
utils.create_test_quota()
|
||||
dbapi = db_api.get_instance()
|
||||
rs = dbapi.quota_reserve(self.context, self.resources,
|
||||
{'instances': 10},
|
||||
{'instances': 1},
|
||||
datetime.datetime(2099, 1, 1, 0, 0),
|
||||
CONF.quota.until_refresh, CONF.quota.max_age,
|
||||
project_id=self.project_id)
|
||||
r = dbapi.quota_usage_get_all_by_project(self.context, self.project_id)
|
||||
before_in_use = r['instances']['in_use']
|
||||
dbapi.reservation_commit(self.context, rs, self.project_id)
|
||||
r = dbapi.quota_usage_get_all_by_project(self.context, self.project_id)
|
||||
after_in_use = r['instances']['in_use']
|
||||
self.assertEqual(before_in_use + 1, after_in_use)
|
||||
|
||||
def test_reserve_rollback(self):
|
||||
utils.create_test_quota()
|
||||
dbapi = db_api.get_instance()
|
||||
rs = dbapi.quota_reserve(self.context, self.resources,
|
||||
{'instances': 10},
|
||||
{'instances': 1},
|
||||
datetime.datetime(2099, 1, 1, 0, 0),
|
||||
CONF.quota.until_refresh, CONF.quota.max_age,
|
||||
project_id=self.project_id)
|
||||
r = dbapi.quota_usage_get_all_by_project(self.context, self.project_id)
|
||||
before_in_use = r['instances']['in_use']
|
||||
dbapi.reservation_rollback(self.context, rs, self.project_id)
|
||||
r = dbapi.quota_usage_get_all_by_project(self.context, self.project_id)
|
||||
after_in_use = r['instances']['in_use']
|
||||
self.assertEqual(before_in_use, after_in_use)
|
114
mogan/tests/unit/db/test_quotas.py
Normal file
114
mogan/tests/unit/db/test_quotas.py
Normal file
@ -0,0 +1,114 @@
|
||||
# Copyright 2018 FiberHome Telecommunication 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.
|
||||
|
||||
"""Tests for manipulating Quotas via the DB API"""
|
||||
|
||||
import six
|
||||
|
||||
from mogan.common import exception
|
||||
from mogan.tests.unit.db import base
|
||||
from mogan.tests.unit.db import utils
|
||||
|
||||
|
||||
class DbQuotaTestCase(base.DbTestCase):
|
||||
|
||||
def test_quota_create(self):
|
||||
utils.create_test_quota()
|
||||
|
||||
def test_quota_get_by_project_id_and_resource_name(self):
|
||||
quota = utils.create_test_quota()
|
||||
res = self.dbapi.quota_get(self.context, quota.project_id,
|
||||
quota.resource_name)
|
||||
self.assertEqual(quota.id, res.id)
|
||||
|
||||
def test_quota_get_not_exist(self):
|
||||
self.assertRaises(exception.QuotaNotFound,
|
||||
self.dbapi.quota_get,
|
||||
self.context,
|
||||
'123', 'fake')
|
||||
|
||||
def test_quota_get_all(self):
|
||||
ids_project_1 = []
|
||||
ids_project_2 = []
|
||||
ids_project_all = []
|
||||
resource_names = ['instances', 'instances_type', 'test_resource']
|
||||
for i in range(0, 3):
|
||||
quota = utils.create_test_quota(project_id='project_1',
|
||||
resource_name=resource_names[i])
|
||||
ids_project_1.append(quota['id'])
|
||||
for i in range(3, 5):
|
||||
resource_name = resource_names[i - 3]
|
||||
quota = utils.create_test_quota(project_id='project_2',
|
||||
resource_name=resource_name)
|
||||
ids_project_2.append(quota['id'])
|
||||
ids_project_all.extend(ids_project_1)
|
||||
ids_project_all.extend(ids_project_2)
|
||||
|
||||
# Set project_only to False
|
||||
# get all quotas from all projects
|
||||
res = self.dbapi.quota_get_all(self.context, project_only=False)
|
||||
res_ids = [r.id for r in res]
|
||||
six.assertCountEqual(self, ids_project_all, res_ids)
|
||||
|
||||
# Set project_only to True
|
||||
# get quotas from current project (project_1)
|
||||
self.context.tenant = 'project_1'
|
||||
res = self.dbapi.quota_get_all(self.context, project_only=True)
|
||||
res_ids = [r.id for r in res]
|
||||
six.assertCountEqual(self, ids_project_1, res_ids)
|
||||
|
||||
# Set project_only to True
|
||||
# get quotas from current project (project_2)
|
||||
self.context.tenant = 'project_2'
|
||||
res = self.dbapi.quota_get_all(self.context, project_only=True)
|
||||
res_ids = [r.id for r in res]
|
||||
six.assertCountEqual(self, ids_project_2, res_ids)
|
||||
|
||||
def test_quota_destroy(self):
|
||||
quota = utils.create_test_quota()
|
||||
self.dbapi.quota_destroy(self.context, quota.project_id,
|
||||
quota.resource_name)
|
||||
self.assertRaises(exception.QuotaNotFound,
|
||||
self.dbapi.quota_get,
|
||||
self.context,
|
||||
quota.project_id,
|
||||
quota.resource_name)
|
||||
|
||||
def test_quota_destroy_not_exist(self):
|
||||
self.assertRaises(exception.QuotaNotFound,
|
||||
self.dbapi.quota_destroy,
|
||||
self.context,
|
||||
'123', 'fake')
|
||||
|
||||
def test_quota_update(self):
|
||||
quota = utils.create_test_quota()
|
||||
old_limit = quota.hard_limit
|
||||
new_limit = 100
|
||||
self.assertNotEqual(old_limit, new_limit)
|
||||
|
||||
res = self.dbapi.quota_update(self.context,
|
||||
quota.project_id,
|
||||
quota.resource_name,
|
||||
{'hard_limit': new_limit})
|
||||
self.assertEqual(new_limit, res.hard_limit)
|
||||
|
||||
def test_quota_update_with_invalid_parameter_value(self):
|
||||
quota = utils.create_test_quota()
|
||||
self.assertRaises(exception.InvalidParameterValue,
|
||||
self.dbapi.quota_update,
|
||||
self.context,
|
||||
quota.project_id,
|
||||
quota.resource_name,
|
||||
{'resource_name': 'instance_test'})
|
@ -68,6 +68,19 @@ def get_test_instance(**kw):
|
||||
}
|
||||
|
||||
|
||||
def get_test_quota(**kw):
|
||||
return {
|
||||
'id': kw.get('id', 123),
|
||||
'resource_name': kw.get('resource_name', 'instances'),
|
||||
'project_id': kw.get('project_id',
|
||||
'c18e8a1a870d4c08a0b51ced6e0b6459'),
|
||||
'hard_limit': kw.get('hard_limit', 10),
|
||||
'allocated': kw.get('allocated', 0),
|
||||
'created_at': kw.get('created_at'),
|
||||
'updated_at': kw.get('updated_at'),
|
||||
}
|
||||
|
||||
|
||||
def create_test_instance(context={}, **kw):
|
||||
"""Create test instance entry in DB and return Instance DB object.
|
||||
|
||||
@ -112,3 +125,22 @@ def create_test_instance_type(context={}, **kw):
|
||||
dbapi = db_api.get_instance()
|
||||
|
||||
return dbapi.instance_type_create(context, instance_type)
|
||||
|
||||
|
||||
def create_test_quota(context={}, **kw):
|
||||
"""Create test quota entry in DB and return quota DB object.
|
||||
|
||||
Function to be used to create test Quota objects in the database.
|
||||
|
||||
:param context: The request context, for access checks.
|
||||
:param kw: kwargs with overriding values for instance's attributes.
|
||||
:returns: Test Quota DB object.
|
||||
|
||||
"""
|
||||
quota = get_test_quota(**kw)
|
||||
# Let DB generate ID if it isn't specified explicitly
|
||||
if 'id' not in kw:
|
||||
del quota['id']
|
||||
dbapi = db_api.get_instance()
|
||||
|
||||
return dbapi.quota_create(context, quota)
|
||||
|
@ -389,7 +389,8 @@ expected_object_fingerprints = {
|
||||
'MyObj': '1.1-aad62eedc5a5cc8bcaf2982c285e753f',
|
||||
'FakeNode': '1.0-07813a70fee67557d8a71ad96f31cee7',
|
||||
'InstanceNic': '1.0-78744332fe105f9c1796dc5295713d9f',
|
||||
'InstanceNics': '1.0-33a2e1bb91ad4082f9f63429b77c1244'
|
||||
'InstanceNics': '1.0-33a2e1bb91ad4082f9f63429b77c1244',
|
||||
'Quota': '1.0-c8caa082f4d726cb63fdc5943f7cd186',
|
||||
}
|
||||
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user