Rearchitect quota checking to partially fix bug 938317.
This is a rearchitecting/rewriting of quota handling to correct the quota atomicity issues highlighted by bug 938317. Partially implements blueprint quota-refactor as well. This change is fairly substantial. To make it easier to review, it has been broken up into 3 parts. This is the first part. Change-Id: I805f5750c08de17487e59fe33fad0bed203188a6
This commit is contained in:
112
nova/db/api.py
112
nova/db/api.py
@@ -251,9 +251,10 @@ def floating_ip_create(context, values):
|
||||
return IMPL.floating_ip_create(context, values)
|
||||
|
||||
|
||||
def floating_ip_count_by_project(context, project_id):
|
||||
def floating_ip_count_by_project(context, project_id, session=None):
|
||||
"""Count floating ips used by project."""
|
||||
return IMPL.floating_ip_count_by_project(context, project_id)
|
||||
return IMPL.floating_ip_count_by_project(context, project_id,
|
||||
session=session)
|
||||
|
||||
|
||||
def floating_ip_deallocate(context, address):
|
||||
@@ -520,9 +521,10 @@ def instance_create(context, values):
|
||||
return IMPL.instance_create(context, values)
|
||||
|
||||
|
||||
def instance_data_get_for_project(context, project_id):
|
||||
def instance_data_get_for_project(context, project_id, session=None):
|
||||
"""Get (instance_count, total_cores, total_ram) for project."""
|
||||
return IMPL.instance_data_get_for_project(context, project_id)
|
||||
return IMPL.instance_data_get_for_project(context, project_id,
|
||||
session=session)
|
||||
|
||||
|
||||
def instance_destroy(context, instance_id):
|
||||
@@ -900,11 +902,6 @@ def quota_destroy(context, project_id, resource):
|
||||
return IMPL.quota_destroy(context, project_id, resource)
|
||||
|
||||
|
||||
def quota_destroy_all_by_project(context, project_id):
|
||||
"""Destroy all quotas associated with a given project."""
|
||||
return IMPL.quota_get_all_by_project(context, project_id)
|
||||
|
||||
|
||||
###################
|
||||
|
||||
|
||||
@@ -941,6 +938,93 @@ def 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 quota_usage_update(context, class_name, resource, in_use, reserved,
|
||||
until_refresh):
|
||||
"""Update a quota usage or raise if it does not exist."""
|
||||
return IMPL.quota_usage_update(context, project_id, resource,
|
||||
in_use, reserved, until_refresh)
|
||||
|
||||
|
||||
def quota_usage_destroy(context, project_id, resource):
|
||||
"""Destroy the quota usage or raise if it does not exist."""
|
||||
return IMPL.quota_usage_destroy(context, project_id, resource)
|
||||
|
||||
|
||||
###################
|
||||
|
||||
|
||||
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):
|
||||
"""Check quotas and create appropriate reservations."""
|
||||
return IMPL.quota_reserve(context, resources, quotas, deltas, expire,
|
||||
until_refresh, max_age)
|
||||
|
||||
|
||||
def reservation_commit(context, reservations):
|
||||
"""Commit quota reservations."""
|
||||
return IMPL.reservation_commit(context, reservations)
|
||||
|
||||
|
||||
def reservation_rollback(context, reservations):
|
||||
"""Roll back quota reservations."""
|
||||
return IMPL.reservation_rollback(context, reservations)
|
||||
|
||||
|
||||
def quota_destroy_all_by_project(context, project_id):
|
||||
"""Destroy all quotas associated with a given project."""
|
||||
return IMPL.quota_get_all_by_project(context, project_id)
|
||||
|
||||
|
||||
def reservation_expire(context):
|
||||
"""Roll back any expired reservations."""
|
||||
return IMPL.reservation_expire(context)
|
||||
|
||||
|
||||
###################
|
||||
|
||||
|
||||
def volume_allocate_iscsi_target(context, volume_id, host):
|
||||
"""Atomically allocate a free iscsi_target from the pool."""
|
||||
return IMPL.volume_allocate_iscsi_target(context, volume_id, host)
|
||||
@@ -956,9 +1040,10 @@ def volume_create(context, values):
|
||||
return IMPL.volume_create(context, values)
|
||||
|
||||
|
||||
def volume_data_get_for_project(context, project_id):
|
||||
def volume_data_get_for_project(context, project_id, session=None):
|
||||
"""Get (volume_count, gigabytes) for project."""
|
||||
return IMPL.volume_data_get_for_project(context, project_id)
|
||||
return IMPL.volume_data_get_for_project(context, project_id,
|
||||
session=session)
|
||||
|
||||
|
||||
def volume_destroy(context, volume_id):
|
||||
@@ -1161,9 +1246,10 @@ def security_group_destroy(context, security_group_id):
|
||||
return IMPL.security_group_destroy(context, security_group_id)
|
||||
|
||||
|
||||
def security_group_count_by_project(context, project_id):
|
||||
def security_group_count_by_project(context, project_id, session=None):
|
||||
"""Count number of security groups in a project."""
|
||||
return IMPL.security_group_count_by_project(context, project_id)
|
||||
return IMPL.security_group_count_by_project(context, project_id,
|
||||
session=session)
|
||||
|
||||
|
||||
####################
|
||||
|
||||
@@ -677,10 +677,11 @@ def floating_ip_create(context, values):
|
||||
|
||||
|
||||
@require_context
|
||||
def floating_ip_count_by_project(context, project_id):
|
||||
def floating_ip_count_by_project(context, project_id, session=None):
|
||||
authorize_project_context(context, project_id)
|
||||
# TODO(tr3buchet): why leave auto_assigned floating IPs out?
|
||||
return model_query(context, models.FloatingIp, read_deleted="no").\
|
||||
return model_query(context, models.FloatingIp, read_deleted="no",
|
||||
session=session).\
|
||||
filter_by(project_id=project_id).\
|
||||
filter_by(auto_assigned=False).\
|
||||
count()
|
||||
@@ -1295,12 +1296,13 @@ def instance_create(context, values):
|
||||
|
||||
|
||||
@require_admin_context
|
||||
def instance_data_get_for_project(context, project_id):
|
||||
def instance_data_get_for_project(context, project_id, session=None):
|
||||
result = model_query(context,
|
||||
func.count(models.Instance.id),
|
||||
func.sum(models.Instance.vcpus),
|
||||
func.sum(models.Instance.memory_mb),
|
||||
read_deleted="no").\
|
||||
read_deleted="no",
|
||||
session=session).\
|
||||
filter_by(project_id=project_id).\
|
||||
first()
|
||||
# NOTE(vish): convert None to 0
|
||||
@@ -2293,19 +2295,6 @@ def quota_destroy(context, project_id, resource):
|
||||
quota_ref.delete(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)
|
||||
|
||||
|
||||
###################
|
||||
|
||||
|
||||
@@ -2383,6 +2372,370 @@ def quota_class_destroy_all_by_name(context, class_name):
|
||||
###################
|
||||
|
||||
|
||||
@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, save=True):
|
||||
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
|
||||
|
||||
# Allow us to hold the save operation until later; keeps the
|
||||
# transaction in quota_reserve() from breaking too early
|
||||
if save:
|
||||
quota_usage_ref.save(session=session)
|
||||
|
||||
return quota_usage_ref
|
||||
|
||||
|
||||
@require_admin_context
|
||||
def quota_usage_update(context, project_id, resource, in_use, reserved,
|
||||
until_refresh, session=None):
|
||||
def do_update(session):
|
||||
quota_usage_ref = quota_usage_get(context, project_id, resource,
|
||||
session=session)
|
||||
quota_usage_ref.in_use = in_use
|
||||
quota_usage_ref.reserved = reserved
|
||||
quota_usage_ref.until_refresh = until_refresh
|
||||
quota_usage_ref.save(session=session)
|
||||
|
||||
if session:
|
||||
# Assume caller started a transaction
|
||||
do_update(session)
|
||||
else:
|
||||
session = get_session()
|
||||
with session.begin():
|
||||
do_update(session)
|
||||
|
||||
|
||||
@require_admin_context
|
||||
def quota_usage_destroy(context, project_id, resource):
|
||||
session = get_session()
|
||||
with session.begin():
|
||||
quota_usage_ref = quota_usage_get(context, project_id, resource,
|
||||
session=session)
|
||||
quota_usage_ref.delete(session=session)
|
||||
|
||||
|
||||
###################
|
||||
|
||||
|
||||
@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.QuotaUsage, 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 = usage
|
||||
reservation_ref.project_id = project_id
|
||||
reservation_ref.resource = resource
|
||||
reservation_ref.delta = delta
|
||||
reservation_ref.expire = expire
|
||||
reservation_ref.save(session=session)
|
||||
return reservation_ref
|
||||
|
||||
|
||||
@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)
|
||||
|
||||
|
||||
###################
|
||||
|
||||
|
||||
def _get_quota_usages(context, session, keys):
|
||||
# Broken out for testability
|
||||
rows = model_query(context, models.QuotaUsage,
|
||||
read_deleted="no",
|
||||
session=session).\
|
||||
filter_by(project_id=context.project_id).\
|
||||
filter(models.QuotaUsage.resource.in_(keys)).\
|
||||
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):
|
||||
elevated = context.elevated()
|
||||
session = get_session()
|
||||
with session.begin():
|
||||
# Get the current usages
|
||||
usages = _get_quota_usages(context, session, deltas.keys())
|
||||
|
||||
# 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:
|
||||
# Note we're inhibiting save...
|
||||
usages[resource] = quota_usage_create(elevated,
|
||||
context.project_id,
|
||||
resource,
|
||||
0, 0,
|
||||
until_refresh or None,
|
||||
session=session,
|
||||
save=False)
|
||||
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 -
|
||||
utils.utcnow()).seconds >= max_age:
|
||||
refresh = True
|
||||
|
||||
# OK, refresh the usage
|
||||
if refresh:
|
||||
# Grab the sync routine
|
||||
sync = resources[resource].sync
|
||||
|
||||
updates = sync(elevated, context.project_id, session)
|
||||
for res, in_use in updates.items():
|
||||
# Make sure we have a destination for the usage!
|
||||
if res not in usages:
|
||||
# Note we're inhibiting save...
|
||||
usages[res] = quota_usage_create(elevated,
|
||||
context.project_id,
|
||||
res,
|
||||
0, 0,
|
||||
until_refresh or None,
|
||||
session=session,
|
||||
save=False)
|
||||
|
||||
# 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 = [resource for resource, delta in deltas.items()
|
||||
if delta < 0 and
|
||||
delta + usages[resource].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 = [resource for resource, delta in deltas.items()
|
||||
if quotas[resource] >= 0 and delta >= 0 and
|
||||
quotas[resource] < delta + usages[resource].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 unders and not overs:
|
||||
reservations = []
|
||||
for resource, delta in deltas.items():
|
||||
reservation = reservation_create(elevated,
|
||||
str(utils.gen_uuid()),
|
||||
usages[resource],
|
||||
context.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:
|
||||
raise exception.InvalidQuotaValue(unders=sorted(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).\
|
||||
options(joinedload('usage')).\
|
||||
filter(models.Reservation.uuid.in_(reservations)).\
|
||||
with_lockmode('update').\
|
||||
all()
|
||||
|
||||
|
||||
@require_context
|
||||
def reservation_commit(context, reservations):
|
||||
session = get_session()
|
||||
with session.begin():
|
||||
for reservation in _quota_reservations(session, context, reservations):
|
||||
if reservation.delta >= 0:
|
||||
reservation.usage.reserved -= reservation.delta
|
||||
reservation.usage.in_use += reservation.delta
|
||||
|
||||
reservation.usage.save(session=session)
|
||||
reservation.delete(session=session)
|
||||
|
||||
|
||||
@require_context
|
||||
def reservation_rollback(context, reservations):
|
||||
session = get_session()
|
||||
with session.begin():
|
||||
for reservation in _quota_reservations(session, context, reservations):
|
||||
if reservation.delta >= 0:
|
||||
reservation.usage.reserved -= reservation.delta
|
||||
reservation.usage.save(session=session)
|
||||
|
||||
reservation.delete(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():
|
||||
results = model_query(context, models.Reservation, session=session,
|
||||
read_deleted="no").\
|
||||
filter(models.Reservation.expire < utils.utcnow()).\
|
||||
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)
|
||||
|
||||
|
||||
###################
|
||||
|
||||
|
||||
@require_admin_context
|
||||
def volume_allocate_iscsi_target(context, volume_id, host):
|
||||
session = get_session()
|
||||
@@ -2438,11 +2791,12 @@ def volume_create(context, values):
|
||||
|
||||
|
||||
@require_admin_context
|
||||
def volume_data_get_for_project(context, project_id):
|
||||
def volume_data_get_for_project(context, project_id, session=None):
|
||||
result = model_query(context,
|
||||
func.count(models.Volume.id),
|
||||
func.sum(models.Volume.size),
|
||||
read_deleted="no").\
|
||||
read_deleted="no",
|
||||
session=session).\
|
||||
filter_by(project_id=project_id).\
|
||||
first()
|
||||
|
||||
@@ -3010,9 +3364,10 @@ def security_group_destroy(context, security_group_id):
|
||||
|
||||
|
||||
@require_context
|
||||
def security_group_count_by_project(context, project_id):
|
||||
def security_group_count_by_project(context, project_id, session=None):
|
||||
authorize_project_context(context, project_id)
|
||||
return model_query(context, models.SecurityGroup, read_deleted="no").\
|
||||
return model_query(context, models.SecurityGroup, read_deleted="no",
|
||||
session=session).\
|
||||
filter_by(project_id=project_id).\
|
||||
count()
|
||||
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
# Copyright 2012 OpenStack LLC.
|
||||
#
|
||||
# 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
|
||||
from sqlalchemy import MetaData, Integer, String, Table, ForeignKey
|
||||
|
||||
from nova import log as logging
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def upgrade(migrate_engine):
|
||||
meta = MetaData()
|
||||
meta.bind = migrate_engine
|
||||
|
||||
# New tables
|
||||
quota_usages = Table('quota_usages', meta,
|
||||
Column('created_at', DateTime(timezone=False)),
|
||||
Column('updated_at', DateTime(timezone=False)),
|
||||
Column('deleted_at', DateTime(timezone=False)),
|
||||
Column('deleted', Boolean(create_constraint=True, name=None)),
|
||||
Column('id', Integer(), primary_key=True),
|
||||
Column('project_id',
|
||||
String(length=255, convert_unicode=True,
|
||||
assert_unicode=None, unicode_error=None,
|
||||
_warn_on_bytestring=False),
|
||||
index=True),
|
||||
Column('resource',
|
||||
String(length=255, convert_unicode=True,
|
||||
assert_unicode=None, unicode_error=None,
|
||||
_warn_on_bytestring=False)),
|
||||
Column('in_use', Integer(), nullable=False),
|
||||
Column('reserved', Integer(), nullable=False),
|
||||
Column('until_refresh', Integer(), nullable=True),
|
||||
mysql_engine='InnoDB',
|
||||
mysql_charset='utf8',
|
||||
)
|
||||
|
||||
try:
|
||||
quota_usages.create()
|
||||
except Exception:
|
||||
LOG.error(_("Table |%s| not created!"), repr(quota_usages))
|
||||
raise
|
||||
|
||||
reservations = Table('reservations', meta,
|
||||
Column('created_at', DateTime(timezone=False)),
|
||||
Column('updated_at', DateTime(timezone=False)),
|
||||
Column('deleted_at', DateTime(timezone=False)),
|
||||
Column('deleted', Boolean(create_constraint=True, name=None)),
|
||||
Column('id', Integer(), primary_key=True),
|
||||
Column('uuid',
|
||||
String(length=36, convert_unicode=True,
|
||||
assert_unicode=None, unicode_error=None,
|
||||
_warn_on_bytestring=False), nullable=False),
|
||||
Column('usage_id', Integer(), ForeignKey('quota_usages.id'),
|
||||
nullable=False),
|
||||
Column('project_id',
|
||||
String(length=255, convert_unicode=True,
|
||||
assert_unicode=None, unicode_error=None,
|
||||
_warn_on_bytestring=False),
|
||||
index=True),
|
||||
Column('resource',
|
||||
String(length=255, convert_unicode=True,
|
||||
assert_unicode=None, unicode_error=None,
|
||||
_warn_on_bytestring=False)),
|
||||
Column('delta', Integer(), nullable=False),
|
||||
Column('expire', DateTime(timezone=False)),
|
||||
mysql_engine='InnoDB',
|
||||
mysql_charset='utf8',
|
||||
)
|
||||
|
||||
try:
|
||||
reservations.create()
|
||||
except Exception:
|
||||
LOG.error(_("Table |%s| not created!"), repr(reservations))
|
||||
raise
|
||||
|
||||
|
||||
def downgrade(migrate_engine):
|
||||
meta = MetaData()
|
||||
meta.bind = migrate_engine
|
||||
|
||||
quota_usages = Table('quota_usages', meta, autoload=True)
|
||||
try:
|
||||
quota_usages.drop()
|
||||
except Exception:
|
||||
LOG.error(_("quota_usages table not dropped"))
|
||||
raise
|
||||
|
||||
reservations = Table('reservations', meta, autoload=True)
|
||||
try:
|
||||
reservations.drop()
|
||||
except Exception:
|
||||
LOG.error(_("reservations table not dropped"))
|
||||
raise
|
||||
@@ -439,6 +439,47 @@ class QuotaClass(BASE, NovaBase):
|
||||
hard_limit = Column(Integer, nullable=True)
|
||||
|
||||
|
||||
class QuotaUsage(BASE, NovaBase):
|
||||
"""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, NovaBase):
|
||||
"""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)
|
||||
usage = relationship(QuotaUsage,
|
||||
backref=backref('reservations'),
|
||||
foreign_keys=usage_id,
|
||||
primaryjoin='and_('
|
||||
'Reservation.usage_id == QuotaUsage.id,'
|
||||
'Reservation.deleted == False)')
|
||||
|
||||
project_id = Column(String(255), index=True)
|
||||
resource = Column(String(255))
|
||||
|
||||
delta = Column(Integer)
|
||||
expire = Column(DateTime, nullable=False)
|
||||
|
||||
|
||||
class Snapshot(BASE, NovaBase):
|
||||
"""Represents a block storage device that can be attached to a vm."""
|
||||
__tablename__ = 'snapshots'
|
||||
|
||||
@@ -689,10 +689,23 @@ class AccessKeyNotFound(NotFound):
|
||||
message = _("Access Key %(access_key)s could not be found.")
|
||||
|
||||
|
||||
class InvalidReservationExpiration(Invalid):
|
||||
message = _("Invalid reservation expiration %(expire)s.")
|
||||
|
||||
|
||||
class InvalidQuotaValue(Invalid):
|
||||
message = _("Change would make usage less than 0 for the following "
|
||||
"resources: %(unders)s")
|
||||
|
||||
|
||||
class QuotaNotFound(NotFound):
|
||||
message = _("Quota could not be found")
|
||||
|
||||
|
||||
class QuotaResourceUnknown(QuotaNotFound):
|
||||
message = _("Unknown quota resources %(unknown)s.")
|
||||
|
||||
|
||||
class ProjectQuotaNotFound(QuotaNotFound):
|
||||
message = _("Quota for project %(project_id)s could not be found.")
|
||||
|
||||
@@ -701,6 +714,18 @@ 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(NovaException):
|
||||
message = _("Quota exceeded for resources: %(overs)s")
|
||||
|
||||
|
||||
class SecurityGroupNotFound(NotFound):
|
||||
message = _("Security group %(security_group_id)s not found.")
|
||||
|
||||
|
||||
750
nova/quota.py
750
nova/quota.py
@@ -18,10 +18,18 @@
|
||||
|
||||
"""Quotas for instances, volumes, and floating ips."""
|
||||
|
||||
from nova import db
|
||||
from nova import flags
|
||||
from nova.openstack.common import cfg
|
||||
import datetime
|
||||
|
||||
from nova import db
|
||||
from nova import exception
|
||||
from nova import flags
|
||||
from nova import log as logging
|
||||
from nova.openstack.common import cfg
|
||||
from nova.openstack.common import importutils
|
||||
from nova import utils
|
||||
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
quota_opts = [
|
||||
cfg.IntOpt('quota_instances',
|
||||
@@ -63,6 +71,18 @@ quota_opts = [
|
||||
cfg.IntOpt('quota_key_pairs',
|
||||
default=100,
|
||||
help='number of key pairs per user'),
|
||||
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'),
|
||||
cfg.StrOpt('quota_driver',
|
||||
default='nova.quota.DbQuotaDriver',
|
||||
help='default driver to use for quota checks'),
|
||||
]
|
||||
|
||||
FLAGS = flags.FLAGS
|
||||
@@ -250,3 +270,727 @@ def allowed_injected_file_content_bytes(context, requested_bytes):
|
||||
def allowed_injected_file_path_bytes(context):
|
||||
"""Return the number of bytes allowed in an injected file path."""
|
||||
return FLAGS.quota_injected_file_path_bytes
|
||||
|
||||
|
||||
class DbQuotaDriver(object):
|
||||
"""
|
||||
Driver to perform necessary checks to enforce quotas and obtain
|
||||
quota information. The default driver utilizes the local
|
||||
database.
|
||||
"""
|
||||
|
||||
def get_by_project(self, context, project_id, resource):
|
||||
"""Get a specific quota by project."""
|
||||
|
||||
return db.quota_get(context, project_id, resource)
|
||||
|
||||
def get_by_class(self, context, quota_class, resource):
|
||||
"""Get a specific quota by quota class."""
|
||||
|
||||
return db.quota_class_get(context, quota_class, resource)
|
||||
|
||||
def get_defaults(self, context, resources):
|
||||
"""Given a list of resources, retrieve the default quotas.
|
||||
|
||||
:param context: The request context, for access checks.
|
||||
:param resources: A dictionary of the registered resources.
|
||||
"""
|
||||
|
||||
quotas = {}
|
||||
for resource in resources.values():
|
||||
quotas[resource.name] = resource.default
|
||||
|
||||
return quotas
|
||||
|
||||
def get_class_quotas(self, context, resources, quota_class,
|
||||
defaults=True):
|
||||
"""
|
||||
Given a list of resources, retrieve the quotas for the given
|
||||
quota class.
|
||||
|
||||
:param context: The request context, for access checks.
|
||||
:param resources: A dictionary of the registered resources.
|
||||
:param quota_class: The name of the quota class to return
|
||||
quotas for.
|
||||
:param defaults: If True, the default value will be reported
|
||||
if there is no specific value for the
|
||||
resource.
|
||||
"""
|
||||
|
||||
quotas = {}
|
||||
class_quotas = db.quota_class_get_all_by_name(context, quota_class)
|
||||
for resource in resources.values():
|
||||
if defaults or resource.name in class_quotas:
|
||||
quotas[resource.name] = class_quotas.get(resource.name,
|
||||
resource.default)
|
||||
|
||||
return quotas
|
||||
|
||||
def get_project_quotas(self, context, resources, project_id,
|
||||
quota_class=None, defaults=True,
|
||||
usages=True):
|
||||
"""
|
||||
Given a list of resources, retrieve the quotas for the given
|
||||
project.
|
||||
|
||||
:param context: The request context, for access checks.
|
||||
:param resources: A dictionary of the registered resources.
|
||||
:param project_id: The ID of the project to return quotas for.
|
||||
:param quota_class: If project_id != context.project_id, the
|
||||
quota class cannot be determined. This
|
||||
parameter allows it to be specified. It
|
||||
will be ignored if project_id ==
|
||||
context.project_id.
|
||||
:param defaults: If True, the quota class value (or the
|
||||
default value, if there is no value from the
|
||||
quota class) will be reported if there is no
|
||||
specific value for the resource.
|
||||
:param usages: If True, the current in_use and reserved counts
|
||||
will also be returned.
|
||||
"""
|
||||
|
||||
quotas = {}
|
||||
project_quotas = db.quota_get_all_by_project(context, project_id)
|
||||
if usages:
|
||||
project_usages = db.quota_usage_get_all_by_project(context,
|
||||
project_id)
|
||||
|
||||
# Get the quotas for the appropriate class. If the project ID
|
||||
# matches the one in the context, we use the quota_class from
|
||||
# the context, otherwise, we use the provided quota_class (if
|
||||
# any)
|
||||
if project_id == context.project_id:
|
||||
quota_class = context.quota_class
|
||||
if quota_class:
|
||||
class_quotas = db.quota_class_get_all_by_name(context, quota_class)
|
||||
else:
|
||||
class_quotas = {}
|
||||
|
||||
for resource in resources.values():
|
||||
# Omit default/quota class values
|
||||
if not defaults and resource.name not in project_quotas:
|
||||
continue
|
||||
|
||||
quotas[resource.name] = dict(
|
||||
limit=project_quotas.get(resource.name, class_quotas.get(
|
||||
resource.name, resource.default)),
|
||||
)
|
||||
|
||||
# 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),
|
||||
)
|
||||
|
||||
return quotas
|
||||
|
||||
def _get_quotas(self, context, resources, keys, has_sync):
|
||||
"""
|
||||
A helper method which retrieves the quotas for the specific
|
||||
resources 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.
|
||||
"""
|
||||
|
||||
# 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 = dict((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,
|
||||
context.project_id,
|
||||
context.quota_class, usages=False)
|
||||
|
||||
return dict((k, v['limit']) for k, v in quotas.items())
|
||||
|
||||
def limit_check(self, context, resources, values):
|
||||
"""Check simple quota limits.
|
||||
|
||||
For limits--those quotas for which there is no usage
|
||||
synchronization function--this method checks that a set of
|
||||
proposed values are permitted by the limit restriction.
|
||||
|
||||
This method will raise a QuotaResourceUnknown exception if a
|
||||
given resource is unknown or if it is not a simple limit
|
||||
resource.
|
||||
|
||||
If any of the proposed values is over the defined quota, an
|
||||
OverQuota exception will be raised with the sorted list of the
|
||||
resources which are too high. Otherwise, the method returns
|
||||
nothing.
|
||||
|
||||
:param context: The request context, for access checks.
|
||||
:param resources: A dictionary of the registered resources.
|
||||
:param values: A dictionary of the values to check against the
|
||||
quota.
|
||||
"""
|
||||
|
||||
# Ensure no value is less than zero
|
||||
unders = [key for key, val in values.items() if val < 0]
|
||||
if unders:
|
||||
raise exception.InvalidQuotaValue(unders=sorted(unders))
|
||||
|
||||
# Get the applicable quotas
|
||||
quotas = self._get_quotas(context, resources, values.keys(),
|
||||
has_sync=False)
|
||||
|
||||
# Check the quotas and construct a list of the resources that
|
||||
# would be put over limit by the desired values
|
||||
overs = [key for key, val in values.items()
|
||||
if quotas[key] >= 0 and quotas[key] < val]
|
||||
if overs:
|
||||
raise exception.OverQuota(overs=sorted(overs), quotas=quotas,
|
||||
usages={})
|
||||
|
||||
def reserve(self, context, resources, deltas, expire=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).
|
||||
"""
|
||||
|
||||
# Set up the reservation expiration
|
||||
if expire is None:
|
||||
expire = FLAGS.reservation_expire
|
||||
if isinstance(expire, (int, long)):
|
||||
expire = datetime.timedelta(seconds=expire)
|
||||
if isinstance(expire, datetime.timedelta):
|
||||
expire = utils.utcnow() + expire
|
||||
if not isinstance(expire, datetime.datetime):
|
||||
raise exception.InvalidReservationExpiration(expire=expire)
|
||||
|
||||
# Get the applicable quotas.
|
||||
# NOTE(Vek): We're not worried about races at this point.
|
||||
# Yes, the admin may be in the process of reducing
|
||||
# quotas, but that's a pretty rare thing.
|
||||
quotas = self._get_quotas(context, resources, deltas.keys(),
|
||||
has_sync=True)
|
||||
|
||||
# NOTE(Vek): Most of the work here has to be done in the DB
|
||||
# API, because we have to do it in a transaction,
|
||||
# which means access to the session. Since the
|
||||
# session isn't available outside the DBAPI, we
|
||||
# have to do the work there.
|
||||
return db.quota_reserve(context, resources, quotas, deltas, expire,
|
||||
FLAGS.until_refresh, FLAGS.max_age)
|
||||
|
||||
def commit(self, context, reservations):
|
||||
"""Commit reservations.
|
||||
|
||||
:param context: The request context, for access checks.
|
||||
:param reservations: A list of the reservation UUIDs, as
|
||||
returned by the reserve() method.
|
||||
"""
|
||||
|
||||
db.reservation_commit(context, reservations)
|
||||
|
||||
def rollback(self, context, reservations):
|
||||
"""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.
|
||||
"""
|
||||
|
||||
db.reservation_rollback(context, reservations)
|
||||
|
||||
def destroy_all_by_project(self, context, project_id):
|
||||
"""
|
||||
Destroy all quotas, usages, and reservations associated with a
|
||||
project.
|
||||
|
||||
:param context: The request context, for access checks.
|
||||
:param project_id: The ID of the project being deleted.
|
||||
"""
|
||||
|
||||
db.quota_destroy_all_by_project(context, project_id)
|
||||
|
||||
def expire(self, context):
|
||||
"""Expire reservations.
|
||||
|
||||
Explores all currently existing reservations and rolls back
|
||||
any that have expired.
|
||||
|
||||
:param context: The request context, for access checks.
|
||||
"""
|
||||
|
||||
db.reservation_expire(context)
|
||||
|
||||
|
||||
class BaseResource(object):
|
||||
"""Describe a single resource for quota checking."""
|
||||
|
||||
def __init__(self, name, flag=None):
|
||||
"""
|
||||
Initializes a Resource.
|
||||
|
||||
:param name: The name of the resource, i.e., "instances".
|
||||
:param flag: The name of the flag or configuration option
|
||||
which specifies the default value of the quota
|
||||
for this resource.
|
||||
"""
|
||||
|
||||
self.name = name
|
||||
self.flag = flag
|
||||
|
||||
def quota(self, driver, context, **kwargs):
|
||||
"""
|
||||
Given a driver and context, obtain the quota for this
|
||||
resource.
|
||||
|
||||
:param driver: A quota driver.
|
||||
:param context: The request context.
|
||||
:param project_id: The project to obtain the quota value for.
|
||||
If not provided, it is taken from the
|
||||
context. If it is given as None, no
|
||||
project-specific quota will be searched
|
||||
for.
|
||||
:param quota_class: The quota class corresponding to the
|
||||
project, or for which the quota is to be
|
||||
looked up. If not provided, it is taken
|
||||
from the context. If it is given as None,
|
||||
no quota class-specific quota will be
|
||||
searched for. Note that the quota class
|
||||
defaults to the value in the context,
|
||||
which may not correspond to the project if
|
||||
project_id is not the same as the one in
|
||||
the context.
|
||||
"""
|
||||
|
||||
# Get the project ID
|
||||
project_id = kwargs.get('project_id', context.project_id)
|
||||
|
||||
# Ditto for the quota class
|
||||
quota_class = kwargs.get('quota_class', context.quota_class)
|
||||
|
||||
# Look up the quota for the project
|
||||
if project_id:
|
||||
try:
|
||||
return driver.get_by_project(context, project_id, self.name)
|
||||
except exception.ProjectQuotaNotFound:
|
||||
pass
|
||||
|
||||
# Try for the quota class
|
||||
if quota_class:
|
||||
try:
|
||||
return driver.get_by_class(context, quota_class, self.name)
|
||||
except exception.QuotaClassNotFound:
|
||||
pass
|
||||
|
||||
# OK, return the default
|
||||
return self.default
|
||||
|
||||
@property
|
||||
def default(self):
|
||||
"""Return the default value of the quota."""
|
||||
|
||||
return FLAGS[self.flag] if self.flag else -1
|
||||
|
||||
|
||||
class ReservableResource(BaseResource):
|
||||
"""Describe a reservable resource."""
|
||||
|
||||
def __init__(self, name, sync, flag=None):
|
||||
"""
|
||||
Initializes a ReservableResource.
|
||||
|
||||
Reservable resources are those resources which directly
|
||||
correspond to objects in the database, i.e., instances, cores,
|
||||
etc. A ReservableResource must be constructed with a usage
|
||||
synchronization function, which will be called to determine the
|
||||
current counts of one or more resources.
|
||||
|
||||
The usage synchronization function will be passed three
|
||||
arguments: an admin context, the project ID, and an opaque
|
||||
session object, which should in turn be passed to the
|
||||
underlying database function. Synchronization functions
|
||||
should return a dictionary mapping resource names to the
|
||||
current in_use count for those resources; more than one
|
||||
resource and resource count may be returned. Note that
|
||||
synchronization functions may be associated with more than one
|
||||
ReservableResource.
|
||||
|
||||
:param name: The name of the resource, i.e., "instances".
|
||||
:param sync: A callable which returns a dictionary to
|
||||
resynchronize the in_use count for one or more
|
||||
resources, as described above.
|
||||
:param flag: The name of the flag or configuration option
|
||||
which specifies the default value of the quota
|
||||
for this resource.
|
||||
"""
|
||||
|
||||
super(ReservableResource, self).__init__(name, flag=flag)
|
||||
self.sync = sync
|
||||
|
||||
|
||||
class AbsoluteResource(BaseResource):
|
||||
"""Describe a non-reservable resource."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class CountableResource(AbsoluteResource):
|
||||
"""
|
||||
Describe a resource where the counts aren't based solely on the
|
||||
project ID.
|
||||
"""
|
||||
|
||||
def __init__(self, name, count, flag=None):
|
||||
"""
|
||||
Initializes a CountableResource.
|
||||
|
||||
Countable resources are those resources which directly
|
||||
correspond to objects in the database, i.e., instances, cores,
|
||||
etc., but for which a count by project ID is inappropriate. A
|
||||
CountableResource must be constructed with a counting
|
||||
function, which will be called to determine the current counts
|
||||
of the resource.
|
||||
|
||||
The counting function will be passed the context, along with
|
||||
the extra positional and keyword arguments that are passed to
|
||||
Quota.count(). It should return an integer specifying the
|
||||
count.
|
||||
|
||||
Note that this counting is not performed in a transaction-safe
|
||||
manner. This resource class is a temporary measure to provide
|
||||
required functionality, until a better approach to solving
|
||||
this problem can be evolved.
|
||||
|
||||
:param name: The name of the resource, i.e., "instances".
|
||||
:param count: A callable which returns the count of the
|
||||
resource. The arguments passed are as described
|
||||
above.
|
||||
:param flag: The name of the flag or configuration option
|
||||
which specifies the default value of the quota
|
||||
for this resource.
|
||||
"""
|
||||
|
||||
super(CountableResource, self).__init__(name, flag=flag)
|
||||
self.count = count
|
||||
|
||||
|
||||
class QuotaEngine(object):
|
||||
"""Represent the set of recognized quotas."""
|
||||
|
||||
def __init__(self, quota_driver_class=None):
|
||||
"""Initialize a Quota object."""
|
||||
|
||||
if not quota_driver_class:
|
||||
quota_driver_class = FLAGS.quota_driver
|
||||
|
||||
if isinstance(quota_driver_class, basestring):
|
||||
quota_driver_class = importutils.import_object(quota_driver_class)
|
||||
|
||||
self._resources = {}
|
||||
self._driver = quota_driver_class
|
||||
|
||||
def __contains__(self, resource):
|
||||
return resource in self._resources
|
||||
|
||||
def register_resource(self, resource):
|
||||
"""Register a resource."""
|
||||
|
||||
self._resources[resource.name] = resource
|
||||
|
||||
def register_resources(self, resources):
|
||||
"""Register a list of resources."""
|
||||
|
||||
for resource in resources:
|
||||
self.register_resource(resource)
|
||||
|
||||
def get_by_project(self, context, project_id, resource):
|
||||
"""Get a specific quota by project."""
|
||||
|
||||
return self._driver.get_by_project(context, project_id, resource)
|
||||
|
||||
def get_by_class(self, context, quota_class, resource):
|
||||
"""Get a specific quota by quota class."""
|
||||
|
||||
return self._driver.get_by_class(context, quota_class, resource)
|
||||
|
||||
def get_defaults(self, context):
|
||||
"""Retrieve the default quotas.
|
||||
|
||||
:param context: The request context, for access checks.
|
||||
"""
|
||||
|
||||
return self._driver.get_defaults(context, self._resources)
|
||||
|
||||
def get_class_quotas(self, context, quota_class, defaults=True):
|
||||
"""Retrieve the quotas for the given quota class.
|
||||
|
||||
:param context: The request context, for access checks.
|
||||
:param quota_class: The name of the quota class to return
|
||||
quotas for.
|
||||
:param defaults: If True, the default value will be reported
|
||||
if there is no specific value for the
|
||||
resource.
|
||||
"""
|
||||
|
||||
return self._driver.get_class_quotas(context, self._resources,
|
||||
quota_class, defaults=defaults)
|
||||
|
||||
def get_project_quotas(self, context, project_id, quota_class=None,
|
||||
defaults=True, usages=True):
|
||||
"""Retrieve the quotas for the given project.
|
||||
|
||||
:param context: The request context, for access checks.
|
||||
:param project_id: The ID of the project to return quotas for.
|
||||
:param quota_class: If project_id != context.project_id, the
|
||||
quota class cannot be determined. This
|
||||
parameter allows it to be specified.
|
||||
:param defaults: If True, the quota class value (or the
|
||||
default value, if there is no value from the
|
||||
quota class) will be reported if there is no
|
||||
specific value for the resource.
|
||||
:param usages: If True, the current in_use and reserved counts
|
||||
will also be returned.
|
||||
"""
|
||||
|
||||
return self._driver.get_project_quotas(context, self._resources,
|
||||
project_id,
|
||||
quota_class=quota_class,
|
||||
defaults=defaults,
|
||||
usages=usages)
|
||||
|
||||
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 limit_check(self, context, **values):
|
||||
"""Check simple quota limits.
|
||||
|
||||
For limits--those quotas for which there is no usage
|
||||
synchronization function--this method checks that a set of
|
||||
proposed values are permitted by the limit restriction. The
|
||||
values to check are given as keyword arguments, where the key
|
||||
identifies the specific quota limit to check, and the value is
|
||||
the proposed value.
|
||||
|
||||
This method will raise a QuotaResourceUnknown exception if a
|
||||
given resource is unknown or if it is not a simple limit
|
||||
resource.
|
||||
|
||||
If any of the proposed values is over the defined quota, an
|
||||
OverQuota exception will be raised with the sorted list of the
|
||||
resources which are too high. Otherwise, the method returns
|
||||
nothing.
|
||||
|
||||
:param context: The request context, for access checks.
|
||||
"""
|
||||
|
||||
return self._driver.limit_check(context, self._resources, values)
|
||||
|
||||
def reserve(self, context, expire=None, **deltas):
|
||||
"""Check quotas and reserve resources.
|
||||
|
||||
For counting quotas--those quotas for which there is a usage
|
||||
synchronization function--this method checks quotas against
|
||||
current usage and the desired deltas. The deltas are given as
|
||||
keyword arguments, and current usage and other reservations
|
||||
are factored into the quota check.
|
||||
|
||||
This method will raise a QuotaResourceUnknown exception if a
|
||||
given resource is unknown or if it does not have a usage
|
||||
synchronization function.
|
||||
|
||||
If any of the proposed values is over the defined quota, an
|
||||
OverQuota exception will be raised with the sorted list of the
|
||||
resources which are too high. Otherwise, the method returns a
|
||||
list of reservation UUIDs which were created.
|
||||
|
||||
:param context: The request context, for access checks.
|
||||
:param expire: An optional parameter specifying an expiration
|
||||
time for the reservations. If it is a simple
|
||||
number, it is interpreted as a number of
|
||||
seconds and added to the current time; if it is
|
||||
a datetime.timedelta object, it will also be
|
||||
added to the current time. A datetime.datetime
|
||||
object will be interpreted as the absolute
|
||||
expiration time. If None is specified, the
|
||||
default expiration time set by
|
||||
--default-reservation-expire will be used (this
|
||||
value will be treated as a number of seconds).
|
||||
"""
|
||||
|
||||
reservations = self._driver.reserve(context, self._resources, deltas,
|
||||
expire=expire)
|
||||
|
||||
LOG.debug(_("Created reservations %(reservations)s") % locals())
|
||||
|
||||
return reservations
|
||||
|
||||
def commit(self, context, reservations):
|
||||
"""Commit reservations.
|
||||
|
||||
:param context: The request context, for access checks.
|
||||
:param reservations: A list of the reservation UUIDs, as
|
||||
returned by the reserve() method.
|
||||
"""
|
||||
|
||||
try:
|
||||
self._driver.commit(context, reservations)
|
||||
except Exception:
|
||||
# NOTE(Vek): Ignoring exceptions here is safe, because the
|
||||
# usage resynchronization and the reservation expiration
|
||||
# mechanisms will resolve the issue. The exception is
|
||||
# logged, however, because this is less than optimal.
|
||||
LOG.exception(_("Failed to commit reservations "
|
||||
"%(reservations)s") % locals())
|
||||
|
||||
def rollback(self, context, reservations):
|
||||
"""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.
|
||||
"""
|
||||
|
||||
try:
|
||||
self._driver.rollback(context, reservations)
|
||||
except Exception:
|
||||
# NOTE(Vek): Ignoring exceptions here is safe, because the
|
||||
# usage resynchronization and the reservation expiration
|
||||
# mechanisms will resolve the issue. The exception is
|
||||
# logged, however, because this is less than optimal.
|
||||
LOG.exception(_("Failed to roll back reservations "
|
||||
"%(reservations)s") % locals())
|
||||
|
||||
def destroy_all_by_project(self, context, project_id):
|
||||
"""
|
||||
Destroy all quotas, usages, and reservations associated with a
|
||||
project.
|
||||
|
||||
:param context: The request context, for access checks.
|
||||
:param project_id: The ID of the project being deleted.
|
||||
"""
|
||||
|
||||
self._driver.destroy_all_by_project(context, project_id)
|
||||
|
||||
def expire(self, context):
|
||||
"""Expire reservations.
|
||||
|
||||
Explores all currently existing reservations and rolls back
|
||||
any that have expired.
|
||||
|
||||
:param context: The request context, for access checks.
|
||||
"""
|
||||
|
||||
self._driver.expire(context)
|
||||
|
||||
@property
|
||||
def resources(self):
|
||||
return sorted(self._resources.keys())
|
||||
|
||||
|
||||
def _sync_instances(context, project_id, session):
|
||||
return dict(zip(('instances', 'cores', 'ram'),
|
||||
db.instance_data_get_for_project(
|
||||
context, project_id, session=session)))
|
||||
|
||||
|
||||
def _sync_volumes(context, project_id, session):
|
||||
return dict(zip(('volumes', 'gigabytes'),
|
||||
db.volume_data_get_for_project(
|
||||
context, project_id, session=session)))
|
||||
|
||||
|
||||
def _sync_floating_ips(context, project_id, session):
|
||||
return dict(floating_ips=db.floating_ip_count_by_project(
|
||||
context, project_id, session=session))
|
||||
|
||||
|
||||
def _sync_security_groups(context, project_id, session):
|
||||
return dict(security_groups=db.security_group_count_by_project(
|
||||
context, project_id, session=session))
|
||||
|
||||
|
||||
QUOTAS = QuotaEngine()
|
||||
|
||||
|
||||
resources = [
|
||||
ReservableResource('instances', _sync_instances, 'quota_instances'),
|
||||
ReservableResource('cores', _sync_instances, 'quota_cores'),
|
||||
ReservableResource('ram', _sync_instances, 'quota_ram'),
|
||||
ReservableResource('volumes', _sync_volumes, 'quota_volumes'),
|
||||
ReservableResource('gigabytes', _sync_volumes, 'quota_gigabytes'),
|
||||
ReservableResource('floating_ips', _sync_floating_ips,
|
||||
'quota_floating_ips'),
|
||||
AbsoluteResource('metadata_items', 'quota_metadata_items'),
|
||||
AbsoluteResource('injected_files', 'quota_injected_files'),
|
||||
AbsoluteResource('injected_file_content_bytes',
|
||||
'quota_injected_file_content_bytes'),
|
||||
AbsoluteResource('injected_file_path_bytes',
|
||||
'quota_injected_file_path_bytes'),
|
||||
ReservableResource('security_groups', _sync_security_groups,
|
||||
'quota_security_groups'),
|
||||
CountableResource('security_group_rules',
|
||||
db.security_group_rule_count_by_group,
|
||||
'quota_security_group_rules'),
|
||||
CountableResource('key_pairs', db.key_pair_count_by_user,
|
||||
'quota_key_pairs'),
|
||||
]
|
||||
|
||||
|
||||
QUOTAS.register_resources(resources)
|
||||
|
||||
@@ -33,6 +33,7 @@ from nova.notifier import api as notifier
|
||||
from nova.openstack.common import cfg
|
||||
from nova.openstack.common import excutils
|
||||
from nova.openstack.common import importutils
|
||||
from nova import quota
|
||||
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
@@ -44,6 +45,8 @@ scheduler_driver_opt = cfg.StrOpt('scheduler_driver',
|
||||
FLAGS = flags.FLAGS
|
||||
FLAGS.register_opt(scheduler_driver_opt)
|
||||
|
||||
QUOTAS = quota.QUOTAS
|
||||
|
||||
|
||||
class SchedulerManager(manager.Manager):
|
||||
"""Chooses a host to run instances on."""
|
||||
@@ -228,3 +231,7 @@ class SchedulerManager(manager.Manager):
|
||||
'ephemeral_gb': sum(ephemeral)}
|
||||
|
||||
return {'resource': resource, 'usage': usage}
|
||||
|
||||
@manager.periodic_task
|
||||
def _expire_reservations(self, context):
|
||||
QUOTAS.expire(context)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user