Merge "Add tables and data modules for the quotas of Karbor"
This commit is contained in:
commit
6345644393
141
karbor/db/api.py
141
karbor/db/api.py
|
@ -675,3 +675,144 @@ def purge_deleted_rows(context, age_in_days):
|
|||
:returns: number of deleted rows
|
||||
"""
|
||||
return IMPL.purge_deleted_rows(context, age_in_days=age_in_days)
|
||||
|
||||
|
||||
####################
|
||||
|
||||
|
||||
def quota_create(context, project_id, resource, limit):
|
||||
"""Create a quota for the given project and resource."""
|
||||
return IMPL.quota_create(context, project_id, resource, limit)
|
||||
|
||||
|
||||
def quota_get(context, project_id, resource):
|
||||
"""Retrieve a quota or raise if it does not exist."""
|
||||
return IMPL.quota_get(context, project_id, resource)
|
||||
|
||||
|
||||
def quota_get_all_by_project(context, project_id):
|
||||
"""Retrieve all quotas associated with a given project."""
|
||||
return IMPL.quota_get_all_by_project(context, project_id)
|
||||
|
||||
|
||||
def quota_update(context, project_id, resource, limit):
|
||||
"""Update a quota or raise if it does not exist."""
|
||||
return IMPL.quota_update(context, project_id, resource, limit)
|
||||
|
||||
|
||||
def quota_destroy(context, project_id, resource):
|
||||
"""Destroy the quota or raise if it does not exist."""
|
||||
return IMPL.quota_destroy(context, project_id, resource)
|
||||
|
||||
|
||||
###################
|
||||
|
||||
|
||||
def quota_class_create(context, class_name, resource, limit):
|
||||
"""Create a quota class for the given name and resource."""
|
||||
return IMPL.quota_class_create(context, class_name, resource, limit)
|
||||
|
||||
|
||||
def quota_class_get(context, class_name, resource):
|
||||
"""Retrieve a quota class or raise if it does not exist."""
|
||||
return IMPL.quota_class_get(context, class_name, resource)
|
||||
|
||||
|
||||
def quota_class_get_all_by_name(context, class_name):
|
||||
"""Retrieve all quotas associated with a given quota class."""
|
||||
return IMPL.quota_class_get_all_by_name(context, class_name)
|
||||
|
||||
|
||||
def quota_class_update(context, class_name, resource, limit):
|
||||
"""Update a quota class or raise if it does not exist."""
|
||||
return IMPL.quota_class_update(context, class_name, resource, limit)
|
||||
|
||||
|
||||
def quota_class_destroy(context, class_name, resource):
|
||||
"""Destroy the quota class or raise if it does not exist."""
|
||||
return IMPL.quota_class_destroy(context, class_name, resource)
|
||||
|
||||
|
||||
def quota_class_destroy_all_by_name(context, class_name):
|
||||
"""Destroy all quotas associated with a given quota class."""
|
||||
return IMPL.quota_class_destroy_all_by_name(context, class_name)
|
||||
|
||||
|
||||
###################
|
||||
|
||||
|
||||
def quota_usage_create(context, project_id, resource, in_use, reserved,
|
||||
until_refresh):
|
||||
"""Create a quota usage for the given project and resource."""
|
||||
return IMPL.quota_usage_create(context, project_id, resource,
|
||||
in_use, reserved, until_refresh)
|
||||
|
||||
|
||||
def quota_usage_get(context, project_id, resource):
|
||||
"""Retrieve a quota usage or raise if it does not exist."""
|
||||
return IMPL.quota_usage_get(context, project_id, resource)
|
||||
|
||||
|
||||
def quota_usage_get_all_by_project(context, project_id):
|
||||
"""Retrieve all usage associated with a given resource."""
|
||||
return IMPL.quota_usage_get_all_by_project(context, project_id)
|
||||
|
||||
|
||||
###################
|
||||
|
||||
|
||||
def reservation_create(context, uuid, usage, project_id, resource, delta,
|
||||
expire):
|
||||
"""Create a reservation for the given project and resource."""
|
||||
return IMPL.reservation_create(context, uuid, usage, project_id,
|
||||
resource, delta, expire)
|
||||
|
||||
|
||||
def reservation_get(context, uuid):
|
||||
"""Retrieve a reservation or raise if it does not exist."""
|
||||
return IMPL.reservation_get(context, uuid)
|
||||
|
||||
|
||||
def reservation_get_all_by_project(context, project_id):
|
||||
"""Retrieve all reservations associated with a given project."""
|
||||
return IMPL.reservation_get_all_by_project(context, project_id)
|
||||
|
||||
|
||||
def reservation_destroy(context, uuid):
|
||||
"""Destroy the reservation or raise if it does not exist."""
|
||||
return IMPL.reservation_destroy(context, uuid)
|
||||
|
||||
|
||||
###################
|
||||
|
||||
|
||||
def quota_reserve(context, resources, quotas, deltas, expire,
|
||||
until_refresh, max_age, project_id=None):
|
||||
"""Check quotas and create appropriate reservations."""
|
||||
return IMPL.quota_reserve(context, resources, quotas, deltas, expire,
|
||||
until_refresh, max_age, project_id=project_id)
|
||||
|
||||
|
||||
def reservation_commit(context, reservations, project_id=None):
|
||||
"""Commit quota reservations."""
|
||||
return IMPL.reservation_commit(context, reservations,
|
||||
project_id=project_id)
|
||||
|
||||
|
||||
def reservation_rollback(context, reservations, project_id=None):
|
||||
"""Roll back quota reservations."""
|
||||
return IMPL.reservation_rollback(context, reservations,
|
||||
project_id=project_id)
|
||||
|
||||
|
||||
def quota_destroy_all_by_project(context, project_id):
|
||||
"""Destroy all quotas associated with a given project."""
|
||||
return IMPL.quota_destroy_all_by_project(context, project_id)
|
||||
|
||||
|
||||
def reservation_expire(context):
|
||||
"""Roll back any expired reservations."""
|
||||
return IMPL.reservation_expire(context)
|
||||
|
||||
|
||||
###################
|
||||
|
|
|
@ -19,6 +19,7 @@ import six
|
|||
import sys
|
||||
import threading
|
||||
import time
|
||||
import uuid
|
||||
|
||||
from oslo_config import cfg
|
||||
from oslo_db import api as oslo_db_api
|
||||
|
@ -1962,3 +1963,506 @@ def purge_deleted_rows(context, age_in_days):
|
|||
rows_purged = result.rowcount
|
||||
LOG.info("Deleted %(row)d rows from table=%(table)s",
|
||||
{'row': rows_purged, 'table': table})
|
||||
|
||||
|
||||
###################
|
||||
|
||||
|
||||
@require_context
|
||||
def quota_get(context, project_id, resource, session=None):
|
||||
result = model_query(context, models.Quota, session=session,
|
||||
read_deleted="no").\
|
||||
filter_by(project_id=project_id).\
|
||||
filter_by(resource=resource).\
|
||||
first()
|
||||
|
||||
if not result:
|
||||
raise exception.ProjectQuotaNotFound(project_id=project_id)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
@require_context
|
||||
def quota_get_all_by_project(context, project_id):
|
||||
authorize_project_context(context, project_id)
|
||||
|
||||
rows = model_query(context, models.Quota, 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_admin_context
|
||||
def quota_create(context, project_id, resource, limit):
|
||||
quota_ref = models.Quota()
|
||||
quota_ref.project_id = project_id
|
||||
quota_ref.resource = resource
|
||||
quota_ref.hard_limit = limit
|
||||
session = get_session()
|
||||
with session.begin():
|
||||
quota_ref.save(session)
|
||||
return quota_ref
|
||||
|
||||
|
||||
@require_admin_context
|
||||
def quota_update(context, project_id, resource, limit):
|
||||
session = get_session()
|
||||
with session.begin():
|
||||
quota_ref = quota_get(context, project_id, resource, session=session)
|
||||
quota_ref.hard_limit = limit
|
||||
quota_ref.save(session=session)
|
||||
|
||||
|
||||
@require_admin_context
|
||||
def quota_destroy(context, project_id, resource):
|
||||
session = get_session()
|
||||
with session.begin():
|
||||
quota_ref = quota_get(context, project_id, resource, session=session)
|
||||
quota_ref.delete(session=session)
|
||||
|
||||
|
||||
###################
|
||||
|
||||
|
||||
@require_context
|
||||
def quota_class_get(context, class_name, resource, session=None):
|
||||
result = model_query(context, models.QuotaClass, session=session,
|
||||
read_deleted="no").\
|
||||
filter_by(class_name=class_name).\
|
||||
filter_by(resource=resource).\
|
||||
first()
|
||||
|
||||
if not result:
|
||||
raise exception.QuotaClassNotFound(class_name=class_name)
|
||||
|
||||
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.QuotaClass, 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
|
||||
|
||||
|
||||
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 exception.NotAuthorized()
|
||||
elif context.quota_class != class_name:
|
||||
raise exception.NotAuthorized()
|
||||
|
||||
|
||||
@require_admin_context
|
||||
def quota_class_create(context, class_name, resource, limit):
|
||||
quota_class_ref = models.QuotaClass()
|
||||
quota_class_ref.class_name = class_name
|
||||
quota_class_ref.resource = resource
|
||||
quota_class_ref.hard_limit = limit
|
||||
session = 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):
|
||||
session = get_session()
|
||||
with session.begin():
|
||||
quota_class_ref = quota_class_get(context, class_name, resource,
|
||||
session=session)
|
||||
quota_class_ref.hard_limit = limit
|
||||
quota_class_ref.save(session=session)
|
||||
|
||||
|
||||
@require_admin_context
|
||||
def quota_class_destroy(context, class_name, resource):
|
||||
session = get_session()
|
||||
with session.begin():
|
||||
quota_class_ref = quota_class_get(context, class_name, resource,
|
||||
session=session)
|
||||
quota_class_ref.delete(session=session)
|
||||
|
||||
|
||||
@require_admin_context
|
||||
def quota_class_destroy_all_by_name(context, class_name):
|
||||
session = get_session()
|
||||
with session.begin():
|
||||
quota_classes = model_query(context, models.QuotaClass,
|
||||
session=session, read_deleted="no").\
|
||||
filter_by(class_name=class_name).\
|
||||
all()
|
||||
|
||||
for quota_class_ref in quota_classes:
|
||||
quota_class_ref.delete(session=session)
|
||||
|
||||
|
||||
###################
|
||||
|
||||
|
||||
@require_context
|
||||
def quota_usage_get(context, project_id, resource, session=None):
|
||||
result = model_query(context, models.QuotaUsage, session=session,
|
||||
read_deleted="no").\
|
||||
filter_by(project_id=project_id).\
|
||||
filter_by(resource=resource).\
|
||||
first()
|
||||
|
||||
if not result:
|
||||
raise exception.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.QuotaUsage, 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.QuotaUsage()
|
||||
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
|
||||
if not session:
|
||||
session = get_session()
|
||||
with session.begin():
|
||||
quota_usage_ref.save(session=session)
|
||||
|
||||
return quota_usage_ref
|
||||
|
||||
|
||||
###################
|
||||
|
||||
|
||||
@require_context
|
||||
def reservation_get(context, uuid, session=None):
|
||||
result = model_query(context, models.Reservation, session=session,
|
||||
read_deleted="no").\
|
||||
filter_by(uuid=uuid).first()
|
||||
|
||||
if not result:
|
||||
raise exception.ReservationNotFound(uuid=uuid)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
@require_context
|
||||
def reservation_get_all_by_project(context, project_id):
|
||||
authorize_project_context(context, project_id)
|
||||
|
||||
rows = model_query(context, models.Reservation, read_deleted="no").\
|
||||
filter_by(project_id=project_id).all()
|
||||
|
||||
result = {'project_id': project_id}
|
||||
for row in rows:
|
||||
result.setdefault(row.resource, {})
|
||||
result[row.resource][row.uuid] = row.delta
|
||||
|
||||
return result
|
||||
|
||||
|
||||
@require_admin_context
|
||||
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
|
||||
if not session:
|
||||
session = get_session()
|
||||
with session.begin():
|
||||
reservation_ref.save(session=session)
|
||||
return reservation_ref
|
||||
|
||||
|
||||
@require_admin_context
|
||||
def reservation_destroy(context, uuid):
|
||||
session = get_session()
|
||||
with session.begin():
|
||||
reservation_ref = reservation_get(context, uuid, session=session)
|
||||
reservation_ref.delete(session=session)
|
||||
|
||||
|
||||
###################
|
||||
|
||||
|
||||
# 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.QuotaUsage,
|
||||
read_deleted="no",
|
||||
session=session).\
|
||||
filter_by(project_id=project_id).\
|
||||
with_lockmode('update').\
|
||||
all()
|
||||
return dict((row.resource, row) for row in rows)
|
||||
|
||||
|
||||
@require_context
|
||||
def quota_reserve(context, resources, quotas, deltas, expire,
|
||||
until_refresh, max_age, project_id=None):
|
||||
elevated = context.elevated()
|
||||
session = get_session()
|
||||
with session.begin():
|
||||
if project_id is None:
|
||||
project_id = context.project_id
|
||||
|
||||
# Get the current usages
|
||||
usages = _get_quota_usages(context, session, 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] = quota_usage_create(elevated,
|
||||
project_id,
|
||||
resource,
|
||||
0, 0,
|
||||
until_refresh or None,
|
||||
session=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 -
|
||||
timeutils.utcnow()).seconds >= max_age:
|
||||
refresh = True
|
||||
|
||||
# OK, refresh the usage
|
||||
if refresh:
|
||||
# Grab the sync routine
|
||||
sync = resources[resource].sync
|
||||
updates = {}
|
||||
if sync:
|
||||
updates = sync(elevated, project_id, session)
|
||||
for res, in_use in updates.items():
|
||||
# Make sure we have a destination for the usage!
|
||||
if res not in usages:
|
||||
usages[res] = 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)
|
||||
|
||||
# NOTE(Vek): We make the assumption that the sync
|
||||
# routine actually refreshes the
|
||||
# resources that it is the sync routine
|
||||
# for. We don't check, because this is
|
||||
# a best-effort mechanism.
|
||||
|
||||
# Check for deltas that would go negative
|
||||
unders = [res for res, delta in deltas.items()
|
||||
if delta < 0 and
|
||||
delta + usages[res].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 = [res for res, delta in deltas.items()
|
||||
if quotas[res] >= 0 and delta >= 0 and
|
||||
quotas[res] < delta + usages[res].total]
|
||||
|
||||
# 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=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
|
||||
|
||||
# Apply updates to the usages table
|
||||
for usage_ref in usages.values():
|
||||
usage_ref.save(session=session)
|
||||
|
||||
if unders:
|
||||
LOG.warning(_("Change will make usage less than 0 for the following "
|
||||
"resources: %(unders)s") % unders)
|
||||
if overs:
|
||||
usages = dict((k, dict(in_use=v['in_use'], reserved=v['reserved']))
|
||||
for k, v in usages.items())
|
||||
raise exception.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
|
||||
def reservation_commit(context, reservations, project_id=None):
|
||||
session = get_session()
|
||||
with session.begin():
|
||||
usages = _get_quota_usages(context, session, project_id)
|
||||
|
||||
for reservation in _quota_reservations(session, context, reservations):
|
||||
usage = usages[reservation.resource]
|
||||
if reservation.delta >= 0:
|
||||
usage.reserved -= reservation.delta
|
||||
usage.in_use += reservation.delta
|
||||
|
||||
reservation.delete(session=session)
|
||||
|
||||
for usage in usages.values():
|
||||
usage.save(session=session)
|
||||
|
||||
|
||||
@require_context
|
||||
def reservation_rollback(context, reservations, project_id=None):
|
||||
session = get_session()
|
||||
with session.begin():
|
||||
usages = _get_quota_usages(context, session, project_id)
|
||||
|
||||
for reservation in _quota_reservations(session, context, reservations):
|
||||
usage = usages[reservation.resource]
|
||||
if reservation.delta >= 0:
|
||||
usage.reserved -= reservation.delta
|
||||
|
||||
reservation.delete(session=session)
|
||||
|
||||
for usage in usages.values():
|
||||
usage.save(session=session)
|
||||
|
||||
|
||||
@require_admin_context
|
||||
def quota_destroy_all_by_project(context, project_id):
|
||||
session = get_session()
|
||||
with session.begin():
|
||||
quotas = model_query(context, models.Quota, session=session,
|
||||
read_deleted="no").\
|
||||
filter_by(project_id=project_id).\
|
||||
all()
|
||||
|
||||
for quota_ref in quotas:
|
||||
quota_ref.delete(session=session)
|
||||
|
||||
quota_usages = model_query(context, models.QuotaUsage,
|
||||
session=session, read_deleted="no").\
|
||||
filter_by(project_id=project_id).\
|
||||
all()
|
||||
|
||||
for quota_usage_ref in quota_usages:
|
||||
quota_usage_ref.delete(session=session)
|
||||
|
||||
reservations = model_query(context, models.Reservation,
|
||||
session=session, read_deleted="no").\
|
||||
filter_by(project_id=project_id).\
|
||||
all()
|
||||
|
||||
for reservation_ref in reservations:
|
||||
reservation_ref.delete(session=session)
|
||||
|
||||
|
||||
@require_admin_context
|
||||
def reservation_expire(context):
|
||||
session = get_session()
|
||||
with session.begin():
|
||||
current_time = timeutils.utcnow()
|
||||
results = model_query(context, models.Reservation, session=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=session)
|
||||
|
||||
reservation.delete(session=session)
|
||||
|
||||
|
||||
################
|
||||
|
|
|
@ -0,0 +1,84 @@
|
|||
# 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.
|
||||
|
||||
from sqlalchemy import Boolean, Column, DateTime, Integer
|
||||
from sqlalchemy import MetaData, String, Table, ForeignKey
|
||||
|
||||
|
||||
def upgrade(migrate_engine):
|
||||
meta = MetaData()
|
||||
meta.bind = migrate_engine
|
||||
|
||||
quotas = Table(
|
||||
'quotas', meta,
|
||||
Column('created_at', DateTime),
|
||||
Column('updated_at', DateTime),
|
||||
Column('deleted_at', DateTime),
|
||||
Column('deleted', Boolean),
|
||||
Column('id', Integer, primary_key=True),
|
||||
Column('project_id', String(length=255), nullable=False),
|
||||
Column('resource', String(length=255), nullable=False),
|
||||
Column('hard_limit', Integer),
|
||||
mysql_engine='InnoDB'
|
||||
)
|
||||
|
||||
quotas.create()
|
||||
|
||||
quota_classes = Table(
|
||||
'quota_classes', meta,
|
||||
Column('created_at', DateTime),
|
||||
Column('updated_at', DateTime),
|
||||
Column('deleted_at', DateTime),
|
||||
Column('deleted', Boolean),
|
||||
Column('id', Integer, primary_key=True),
|
||||
Column('class_name', String(length=255), nullable=False),
|
||||
Column('resource', String(length=255), nullable=False),
|
||||
Column('hard_limit', Integer),
|
||||
mysql_engine='InnoDB'
|
||||
)
|
||||
quota_classes.create()
|
||||
|
||||
quota_usages = Table(
|
||||
'quota_usages', meta,
|
||||
Column('created_at', DateTime),
|
||||
Column('updated_at', DateTime),
|
||||
Column('deleted_at', DateTime),
|
||||
Column('deleted', Boolean),
|
||||
Column('id', Integer, primary_key=True),
|
||||
Column('project_id', String(length=255), nullable=False),
|
||||
Column('resource', String(length=255), nullable=False),
|
||||
Column('in_use', Integer),
|
||||
Column('reserved', Integer),
|
||||
Column('until_refresh', Integer, nullable=True),
|
||||
mysql_engine='InnoDB'
|
||||
)
|
||||
|
||||
quota_usages.create()
|
||||
|
||||
reservations = Table(
|
||||
'reservations', meta,
|
||||
Column('created_at', DateTime),
|
||||
Column('updated_at', DateTime),
|
||||
Column('deleted_at', DateTime),
|
||||
Column('deleted', Boolean),
|
||||
Column('id', Integer, primary_key=True),
|
||||
Column('uuid', String(length=36), nullable=False),
|
||||
Column('usage_id', Integer, ForeignKey('quota_usages.id'),
|
||||
nullable=False),
|
||||
Column('project_id', String(length=255), index=True),
|
||||
Column('resource', String(length=255)),
|
||||
Column('delta', Integer, nullable=False),
|
||||
Column('expire', DateTime),
|
||||
mysql_engine='InnoDB'
|
||||
)
|
||||
|
||||
reservations.create()
|
|
@ -247,6 +247,77 @@ class CheckpointRecord(BASE, KarborBase):
|
|||
extend_info = Column(Text)
|
||||
|
||||
|
||||
class Quota(BASE, KarborBase):
|
||||
"""Represents a single quota override for a project.
|
||||
|
||||
If there is no row for a given project id and resource, then the
|
||||
default for the quota class is used. If there is no row for a
|
||||
given quota class and resource, then the default for the
|
||||
deployment is used. If the row is present but the hard limit is
|
||||
Null, then the resource is unlimited.
|
||||
"""
|
||||
|
||||
__tablename__ = 'quotas'
|
||||
id = Column(Integer, primary_key=True)
|
||||
|
||||
project_id = Column(String(255), index=True)
|
||||
|
||||
resource = Column(String(255))
|
||||
hard_limit = Column(Integer, nullable=True)
|
||||
|
||||
|
||||
class QuotaClass(BASE, KarborBase):
|
||||
"""Represents a single quota override for a quota class.
|
||||
|
||||
If there is no row for a given quota class and resource, then the
|
||||
default for the deployment is used. If the row is present but the
|
||||
hard limit is Null, then the resource is unlimited.
|
||||
"""
|
||||
|
||||
__tablename__ = 'quota_classes'
|
||||
id = Column(Integer, primary_key=True)
|
||||
|
||||
class_name = Column(String(255), index=True)
|
||||
|
||||
resource = Column(String(255))
|
||||
hard_limit = Column(Integer, nullable=True)
|
||||
|
||||
|
||||
class QuotaUsage(BASE, KarborBase):
|
||||
"""Represents the current usage for a given resource."""
|
||||
|
||||
__tablename__ = 'quota_usages'
|
||||
id = Column(Integer, primary_key=True)
|
||||
|
||||
project_id = Column(String(255), index=True)
|
||||
resource = Column(String(255))
|
||||
|
||||
in_use = Column(Integer)
|
||||
reserved = Column(Integer)
|
||||
|
||||
@property
|
||||
def total(self):
|
||||
return self.in_use + self.reserved
|
||||
|
||||
until_refresh = Column(Integer, nullable=True)
|
||||
|
||||
|
||||
class Reservation(BASE, KarborBase):
|
||||
"""Represents a resource reservation for quotas."""
|
||||
|
||||
__tablename__ = 'reservations'
|
||||
id = Column(Integer, primary_key=True)
|
||||
uuid = Column(String(36), nullable=False)
|
||||
|
||||
usage_id = Column(Integer, ForeignKey('quota_usages.id'), nullable=False)
|
||||
|
||||
project_id = Column(String(255), index=True)
|
||||
resource = Column(String(255))
|
||||
|
||||
delta = Column(Integer)
|
||||
expire = Column(DateTime, nullable=False)
|
||||
|
||||
|
||||
def register_models():
|
||||
"""Register Models and create metadata.
|
||||
|
||||
|
@ -265,7 +336,11 @@ def register_models():
|
|||
ScheduledOperationLog,
|
||||
Restore,
|
||||
Verification,
|
||||
CheckpointRecord)
|
||||
CheckpointRecord,
|
||||
Quota,
|
||||
QuotaClass,
|
||||
QuotaUsage,
|
||||
Reservation)
|
||||
engine = create_engine(CONF.database.connection, echo=False)
|
||||
for model in models:
|
||||
model.metadata.create_all(engine)
|
||||
|
|
|
@ -393,3 +393,35 @@ class CheckpointNotBeDeleted(KarborException):
|
|||
class GetProtectionNetworkSubResourceFailed(KarborException):
|
||||
message = _("Get protection network sub-resources of type %(type)s failed:"
|
||||
" %(reason)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(KarborException):
|
||||
message = _("Quota exceeded for resources: %(overs)s")
|
||||
|
||||
|
||||
class InvalidReservationExpiration(Invalid):
|
||||
message = _("Invalid reservation expiration %(expire)s.")
|
||||
|
|
|
@ -17,12 +17,15 @@ from datetime import timedelta
|
|||
from oslo_config import cfg
|
||||
from oslo_utils import uuidutils
|
||||
import six
|
||||
import uuid
|
||||
|
||||
from karbor import context
|
||||
from karbor import db
|
||||
from karbor import exception
|
||||
from karbor.tests import base
|
||||
|
||||
from oslo_utils import timeutils
|
||||
|
||||
|
||||
CONF = cfg.CONF
|
||||
|
||||
|
@ -707,3 +710,144 @@ class CheckpointRecordTestCase(ModelBaseTestCase):
|
|||
self.assertRaises(exception.CheckpointRecordNotFound,
|
||||
db.checkpoint_record_update,
|
||||
self.ctxt, 42, {})
|
||||
|
||||
|
||||
class QuotaDbTestCase(ModelBaseTestCase):
|
||||
"""Unit tests for karbor.db.api.quota_*."""
|
||||
|
||||
def setUp(self):
|
||||
super(QuotaDbTestCase, self).setUp()
|
||||
self.ctxt = context.get_admin_context()
|
||||
self.project_id = "586cc6ce-e286-40bd-b2b5-dd32694d9944"
|
||||
self.resource = "volume_backups"
|
||||
self.limit = 10
|
||||
|
||||
def test_quota_create(self):
|
||||
quota = db.quota_create(self.ctxt, self.project_id,
|
||||
self.resource, self.limit)
|
||||
self.assertEqual("volume_backups", quota.resource)
|
||||
db.quota_destroy(self.ctxt, self.project_id, self.resource)
|
||||
|
||||
def test_quota_get(self):
|
||||
quota = db.quota_create(self.ctxt, self.project_id,
|
||||
self.resource, self.limit)
|
||||
self._assertEqualObjects(quota, db.quota_get(
|
||||
self.ctxt, quota.project_id, quota.resource))
|
||||
db.quota_destroy(self.ctxt, self.project_id, self.resource)
|
||||
|
||||
def test_quota_destroy(self):
|
||||
quota = db.quota_create(self.ctxt, self.project_id,
|
||||
self.resource, self.limit)
|
||||
db.quota_destroy(self.ctxt, quota.project_id, quota.resource)
|
||||
self.assertRaises(exception.ProjectQuotaNotFound, db.quota_get,
|
||||
self.ctxt, quota.project_id, quota.resource)
|
||||
|
||||
def test_quota_update(self):
|
||||
quota = db.quota_create(self.ctxt, self.project_id,
|
||||
self.resource, self.limit)
|
||||
db.quota_update(self.ctxt, quota.project_id, quota.resource,
|
||||
20)
|
||||
quota = db.quota_get(self.ctxt, self.project_id, self.resource)
|
||||
self.assertEqual(20, quota.hard_limit)
|
||||
|
||||
|
||||
class QuotaClassDbTestCase(ModelBaseTestCase):
|
||||
"""Unit tests for karbor.db.api.quota_class_*."""
|
||||
|
||||
def setUp(self):
|
||||
super(QuotaClassDbTestCase, self).setUp()
|
||||
self.ctxt = context.get_admin_context()
|
||||
self.class_name = "default"
|
||||
self.resource = "volume_backups"
|
||||
self.limit = 10
|
||||
|
||||
def test_quota_class_create(self):
|
||||
quota_class = db.quota_class_create(self.ctxt, self.class_name,
|
||||
self.resource, self.limit)
|
||||
self.assertEqual("volume_backups", quota_class.resource)
|
||||
db.quota_class_destroy(self.ctxt, self.class_name, self.resource)
|
||||
|
||||
def test_quota_class_get(self):
|
||||
quota_class = db.quota_class_create(self.ctxt, self.class_name,
|
||||
self.resource, self.limit)
|
||||
self._assertEqualObjects(quota_class, db.quota_class_get(
|
||||
self.ctxt, quota_class.class_name, quota_class.resource))
|
||||
db.quota_class_destroy(self.ctxt, self.class_name, self.resource)
|
||||
|
||||
def test_quota_class_destroy(self):
|
||||
quota_class = db.quota_class_create(self.ctxt, self.class_name,
|
||||
self.resource, self.limit)
|
||||
db.quota_class_destroy(self.ctxt, self.class_name, self.resource)
|
||||
self.assertRaises(exception.QuotaClassNotFound, db.quota_class_get,
|
||||
self.ctxt, quota_class.class_name,
|
||||
quota_class.resource)
|
||||
|
||||
def test_quota_class_update(self):
|
||||
quota_class = db.quota_class_create(self.ctxt, self.class_name,
|
||||
self.resource, self.limit)
|
||||
db.quota_class_update(self.ctxt, quota_class.class_name,
|
||||
quota_class.resource, 20)
|
||||
quota_class = db.quota_class_get(self.ctxt, self.class_name,
|
||||
self.resource)
|
||||
self.assertEqual(20, quota_class.hard_limit)
|
||||
|
||||
|
||||
class QuotaUsageDbTestCase(ModelBaseTestCase):
|
||||
"""Unit tests for karbor.db.api.quota_usage_*."""
|
||||
|
||||
def setUp(self):
|
||||
super(QuotaUsageDbTestCase, self).setUp()
|
||||
self.ctxt = context.get_admin_context()
|
||||
self.project_id = "586cc6ce-e286-40bd-b2b5-dd32694d9944"
|
||||
self.resource = "volume_backups"
|
||||
self.in_use = 10
|
||||
self.reserved = 10
|
||||
self.until_refresh = 0
|
||||
|
||||
def test_quota_usage_create(self):
|
||||
quota_usage = db.quota_usage_create(
|
||||
self.ctxt, self.project_id, "volume_backups", self.in_use,
|
||||
self.reserved, self.until_refresh)
|
||||
self.assertEqual("volume_backups", quota_usage.resource)
|
||||
|
||||
def test_quota_usage_get(self):
|
||||
quota_usage = db.quota_usage_create(
|
||||
self.ctxt, self.project_id, "volume_backups_get", self.in_use,
|
||||
self.reserved, self.until_refresh)
|
||||
self._assertEqualObjects(quota_usage, db.quota_usage_get(
|
||||
self.ctxt, self.project_id, "volume_backups_get"))
|
||||
|
||||
|
||||
class ReservationDbTestCase(ModelBaseTestCase):
|
||||
"""Unit tests for karbor.db.api.reservation_*."""
|
||||
|
||||
def setUp(self):
|
||||
super(ReservationDbTestCase, self).setUp()
|
||||
self.ctxt = context.get_admin_context()
|
||||
self.project_id = "586cc6ce-e286-40bd-b2b5-dd32694d9944"
|
||||
self.resource = "volume_backups"
|
||||
self.in_use = 10
|
||||
self.reserved = 10
|
||||
self.until_refresh = 0
|
||||
|
||||
def test_reservation_create(self):
|
||||
quota_usage = db.quota_usage_create(
|
||||
self.ctxt, self.project_id, "volume_backups", self.in_use,
|
||||
self.reserved, self.until_refresh)
|
||||
reservation = db.reservation_create(
|
||||
self.ctxt, str(uuid.uuid4()), quota_usage,
|
||||
self.project_id, "volume_backups", 1,
|
||||
timeutils.utcnow())
|
||||
self.assertEqual("volume_backups", quota_usage.resource)
|
||||
self.assertEqual("volume_backups", reservation.resource)
|
||||
|
||||
def test_reservation_get(self):
|
||||
quota_usage = db.quota_usage_create(
|
||||
self.ctxt, self.project_id, "volume_backups_get", self.in_use,
|
||||
self.reserved, self.until_refresh)
|
||||
reservation = db.reservation_create(
|
||||
self.ctxt, str(uuid.uuid4()), quota_usage,
|
||||
self.project_id, "volume_backups_get", 1,
|
||||
timeutils.utcnow())
|
||||
self._assertEqualObjects(reservation, db.reservation_get(
|
||||
self.ctxt, reservation.uuid))
|
||||
|
|
Loading…
Reference in New Issue