DB API changes for the nova-manage quota_usage_refresh command

This is the first patch in a sequence to implement the
blueprint.

The patch implements the database API changes that will be
needed by the new nova-manage quota_usage_refresh command.

A new API is added to refresh the quota usages of the
selected resources.

Another API is added to get the PER_PROJECT_QUOTAS, which
will be needed later in the patch sequence.

Implements blueprint: refresh-quotas-usage

Change-Id: Id36328b2f2016eab2d7194b88d3eee8d016786d1
This commit is contained in:
Chuck Carmack
2016-03-30 14:25:39 +00:00
parent 7ba44cbb99
commit 4a95c1c5ea
3 changed files with 184 additions and 38 deletions

View File

@@ -1082,6 +1082,13 @@ def quota_get_all_by_project(context, project_id):
return IMPL.quota_get_all_by_project(context, project_id)
def quota_get_per_project_resources():
"""Retrieve the names of resources whose quotas are calculated on a
per-project rather than a per-user basis.
"""
return IMPL.quota_get_per_project_resources()
def quota_get_all(context, project_id):
"""Retrieve all user quotas associated with a given project."""
return IMPL.quota_get_all(context, project_id)
@@ -1146,6 +1153,27 @@ def quota_usage_update(context, project_id, user_id, resource, **kwargs):
**kwargs)
def quota_usage_refresh(context, resources, keys, until_refresh, max_age,
project_id=None, user_id=None):
"""Refresh the quota usages.
:param context: The request context, for access checks.
:param resources: A dictionary of the registered resources.
:param keys: Names of the resources whose usage is to be refreshed.
:param until_refresh: The until_refresh configuration value.
:param max_age: The max_age configuration value.
:param project_id: (Optional) The project_id containing the usages
to be refreshed. Defaults to the project_id
in the context.
:param user_id: (Optional) The user_id containing the usages
to be refreshed. Defaults to the user_id
in the context.
"""
return IMPL.quota_usage_refresh(context, resources, keys, until_refresh,
max_age, project_id=project_id, user_id=user_id)
###################

View File

@@ -3416,6 +3416,10 @@ def quota_get_all(context, project_id):
return result
def quota_get_per_project_resources():
return PER_PROJECT_QUOTAS
@main_context_manager.writer
def quota_create(context, project_id, resource, limit, user_id=None):
per_user = user_id and resource not in PER_PROJECT_QUOTAS
@@ -3735,6 +3739,53 @@ def _refresh_quota_usages(quota_usage, until_refresh, in_use):
quota_usage.until_refresh = until_refresh or None
def _refresh_quota_usages_if_needed(user_usages, context, resources, keys,
project_id, user_id, until_refresh,
max_age, force_refresh=False):
elevated = context.elevated()
# Handle usage refresh
work = set(keys)
while work:
resource = work.pop()
# Do we need to refresh the usage?
created = _create_quota_usage_if_missing(user_usages, resource,
until_refresh, project_id,
user_id, context.session)
refresh = force_refresh
if not refresh:
refresh = created or \
_is_quota_refresh_needed(user_usages[resource], max_age)
# OK, refresh the usage
if refresh:
# Grab the sync routine
sync = QUOTA_SYNC_FUNCTIONS[resources[resource].sync]
updates = sync(elevated, project_id, user_id)
for res, in_use in updates.items():
# Make sure we have a destination for the usage!
_create_quota_usage_if_missing(user_usages, res,
until_refresh, project_id,
user_id, context.session)
_refresh_quota_usages(user_usages[res], until_refresh,
in_use)
# 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.
def _calculate_overquota(project_quotas, user_quotas, deltas,
project_usages, user_usages):
"""Checks if any resources will go over quota based on the request.
@@ -3779,11 +3830,8 @@ def _calculate_overquota(project_quotas, user_quotas, deltas,
@require_context
@oslo_db_api.wrap_db_retry(max_retries=5, retry_on_deadlock=True)
@main_context_manager.writer
def quota_reserve(context, resources, project_quotas, user_quotas, deltas,
expire, until_refresh, max_age, project_id=None,
user_id=None):
elevated = context.elevated()
def quota_usage_refresh(context, resources, keys, until_refresh, max_age,
project_id=None, user_id=None):
if project_id is None:
project_id = context.project_id
if user_id is None:
@@ -3793,43 +3841,30 @@ def quota_reserve(context, resources, project_quotas, user_quotas, deltas,
project_usages, user_usages = _get_project_user_quota_usages(
context, project_id, user_id)
# Handle usage refresh
work = set(deltas.keys())
while work:
resource = work.pop()
# Force refresh of the usages
_refresh_quota_usages_if_needed(user_usages, context, resources, keys,
project_id, user_id, until_refresh,
max_age, force_refresh=True)
# Do we need to refresh the usage?
created = _create_quota_usage_if_missing(user_usages, resource,
until_refresh, project_id,
user_id, context.session)
refresh = created or _is_quota_refresh_needed(
user_usages[resource], max_age)
# OK, refresh the usage
if refresh:
# Grab the sync routine
sync = QUOTA_SYNC_FUNCTIONS[resources[resource].sync]
@require_context
@oslo_db_api.wrap_db_retry(max_retries=5, retry_on_deadlock=True)
@main_context_manager.writer
def quota_reserve(context, resources, project_quotas, user_quotas, deltas,
expire, until_refresh, max_age, project_id=None,
user_id=None):
if project_id is None:
project_id = context.project_id
if user_id is None:
user_id = context.user_id
updates = sync(elevated, project_id, user_id)
for res, in_use in updates.items():
# Make sure we have a destination for the usage!
_create_quota_usage_if_missing(user_usages, res,
until_refresh, project_id,
user_id, context.session)
_refresh_quota_usages(user_usages[res], until_refresh,
in_use)
# Get the current usages
project_usages, user_usages = _get_project_user_quota_usages(
context, project_id, user_id)
# 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.
_refresh_quota_usages_if_needed(user_usages, context, resources,
deltas.keys(), project_id, user_id,
until_refresh, max_age)
# Check for deltas that would go negative
unders = [res for res, delta in deltas.items()

View File

@@ -1616,6 +1616,89 @@ class InstanceSystemMetadataTestCase(test.TestCase):
{'key': 'value'}, True)
class RefreshUsageTestCase(test.TestCase):
"""Tests for the db.api.quota_usage_refresh method. """
def setUp(self):
super(RefreshUsageTestCase, self).setUp()
self.ctxt = context.get_admin_context()
self.project_id = 'project1'
self.user_id = 'user1'
def _quota_refresh(self, keys):
"""Refresh the in_use count on the QuotaUsage objects.
The QuotaUsage objects are created if they don't exist.
"""
def get_sync(resource, usage):
def sync(elevated, project_id, user_id):
return {resource: usage}
return sync
resources = {}
for i in range(4):
resource = 'resource%d' % i
if i == 2:
# test for project level resources
resource = 'fixed_ips'
if i == 3:
# test for project level resources
resource = 'floating_ips'
sync_name = '_sync_%s' % resource
resources[resource] = quota.ReservableResource(
resource, sync_name, 'quota_res_%d' % i)
setattr(sqlalchemy_api, sync_name, get_sync(resource, i + 1))
sqlalchemy_api.QUOTA_SYNC_FUNCTIONS[sync_name] = getattr(
sqlalchemy_api, sync_name)
db.quota_usage_refresh(self.ctxt, resources, keys,
until_refresh=3,
max_age=0,
project_id=self.project_id,
user_id=self.user_id)
def _compare_resource_usages(self, keys, expected, project_id,
user_id = None):
for key in keys:
actual = db.quota_usage_get(self.ctxt, project_id, key, user_id)
self.assertEqual(expected['project_id'], actual.project_id)
self.assertEqual(expected['user_id'], actual.user_id)
self.assertEqual(key, actual.resource)
self.assertEqual(expected[key]['in_use'], actual.in_use)
self.assertEqual(expected[key]['reserved'], actual.reserved)
self.assertEqual(expected[key]['until_refresh'],
actual.until_refresh)
def test_refresh_created_project_usages(self):
# The refresh will create the usages and then sync
# in_use from 0 to 3 for fixed_ips and 0 to 4 for floating_ips.
keys = ['fixed_ips', 'floating_ips']
self._quota_refresh(keys)
expected = {'project_id': self.project_id,
# User ID will be none for per-project resources
'user_id': None,
'fixed_ips': {'in_use': 3, 'reserved': 0,
'until_refresh': 3},
'floating_ips': {'in_use': 4, 'reserved': 0,
'until_refresh': 3}}
self._compare_resource_usages(keys, expected, self.project_id,
self.user_id)
def test_refresh_created_user_usages(self):
# The refresh will create the usages and then sync
# in_use from 0 to 1 for resource0 and 0 to 2 for resource1.
keys = ['resource0', 'resource1']
self._quota_refresh(keys)
expected = {'project_id': self.project_id,
'user_id': self.user_id,
'resource0': {'in_use': 1, 'reserved': 0,
'until_refresh': 3},
'resource1': {'in_use': 2, 'reserved': 0,
'until_refresh': 3}}
self._compare_resource_usages(keys, expected, self.project_id,
self.user_id)
class ReservationTestCase(test.TestCase, ModelsObjectComparatorMixin):
"""Tests for db.api.reservation_* methods."""