Extend Quota API to report usage statistics

Extend existing quota api to report a quota set. The quota set
will contain a set of resources and its corresponding reservation,
limits and in_use count for each tenant.

DocImpact:Documentation describing the new API as well as the new
information that it exposes.
APIImpact

Co-Authored-By: Prince Boateng<prince.a.owusu.boateng@intel.com>
Change-Id: Ief2a6a4d2d7085e2a9dcd901123bc4fe6ac7ca22
Related-bug: #1599488
This commit is contained in:
Sergey Belous 2016-10-07 14:29:07 +03:00 committed by nanaboat
parent d7e7dd451a
commit a8109af65f
13 changed files with 542 additions and 22 deletions

View File

@ -15,12 +15,15 @@
from neutron_lib.api import attributes
from neutron_lib import exceptions
from neutron_lib.plugins import constants
from neutron_lib.plugins import directory
from oslo_log import log
from neutron.common import exceptions as n_exc
from neutron.db import api as db_api
from neutron.db.quota import api as quota_api
from neutron.objects import quota as quota_obj
from neutron.quota import resource as res
LOG = log.getLogger(__name__)
@ -72,6 +75,39 @@ class DbQuotaDriver(object):
return tenant_quota
@staticmethod
@db_api.retry_if_session_inactive()
def get_detailed_tenant_quotas(context, resources, tenant_id):
"""Given a list of resources and a sepecific tenant, retrieve
the detailed quotas (limit, used, reserved).
:param context: The request context, for access checks.
:param resources: A dictionary of the registered resource keys.
:return dict: mapping resource name in dict to its correponding limit
used and reserved. Reserved currently returns default value of 0
"""
res_reserve_info = quota_api.get_reservations_for_resources(
context, tenant_id, resources.keys())
tenant_quota_ext = {}
for key, resource in resources.items():
if isinstance(resource, res.TrackedResource):
used = resource.count_used(context, tenant_id,
resync_usage=False)
else:
plugins = directory.get_plugins()
plugin = plugins.get(key, plugins[constants.CORE])
used = resource.count(context, plugin, tenant_id)
tenant_quota_ext[key] = {
'limit': resource.default,
'used': used,
'reserved': res_reserve_info.get(key, 0),
}
#update with specific tenant limits
quota_objs = quota_obj.Quota.get_objects(context, project_id=tenant_id)
for item in quota_objs:
tenant_quota_ext[item['resource']]['limit'] = item['limit']
return tenant_quota_ext
@staticmethod
@db_api.retry_if_session_inactive()
def delete_tenant_quota(context, tenant_id):

View File

@ -128,6 +128,9 @@ class QuotaSetsController(wsgi.Controller):
class Quotasv2(api_extensions.ExtensionDescriptor):
"""Quotas management support."""
extensions.register_custom_supported_check(
RESOURCE_COLLECTION, lambda: True, plugin_agnostic=True)
@classmethod
def get_name(cls):
return "Quota management support"

View File

@ -0,0 +1,99 @@
# Copyright 2017 Intel Corporation.
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from neutron_lib.api import extensions as api_extensions
from neutron_lib import exceptions as n_exc
from neutron_lib.plugins import directory
from oslo_config import cfg
from neutron._i18n import _
from neutron.api import extensions
from neutron.api.v2 import base
from neutron.api.v2 import resource
from neutron.extensions import quotasv2
from neutron.quota import resource_registry
DETAIL_QUOTAS_ACTION = 'details'
RESOURCE_NAME = 'quota'
ALIAS = RESOURCE_NAME + '_' + DETAIL_QUOTAS_ACTION
QUOTA_DRIVER = cfg.CONF.QUOTAS.quota_driver
RESOURCE_COLLECTION = RESOURCE_NAME + "s"
DB_QUOTA_DRIVER = 'neutron.db.quota.driver.DbQuotaDriver'
EXTENDED_ATTRIBUTES_2_0 = {
RESOURCE_COLLECTION: {}
}
class DetailQuotaSetsController(quotasv2.QuotaSetsController):
def _get_detailed_quotas(self, request, tenant_id):
return self._driver.get_detailed_tenant_quotas(
request.context,
resource_registry.get_all_resources(), tenant_id)
def details(self, request, id):
if id != request.context.project_id:
# Check if admin
if not request.context.is_admin:
reason = _("Only admin is authorized to access quotas for"
" another tenant")
raise n_exc.AdminRequired(reason=reason)
return {self._resource_name:
self._get_detailed_quotas(request, id)}
class Quotasv2_detail(api_extensions.ExtensionDescriptor):
"""Quota details management support."""
# Ensure new extension is not loaded with old conf driver.
extensions.register_custom_supported_check(
ALIAS, lambda: True if QUOTA_DRIVER == DB_QUOTA_DRIVER else False,
plugin_agnostic=True)
@classmethod
def get_name(cls):
return "Quota details management support"
@classmethod
def get_alias(cls):
return ALIAS
@classmethod
def get_description(cls):
return 'Expose functions for quotas usage statistics per project'
@classmethod
def get_updated(cls):
return "2017-02-10T10:00:00-00:00"
@classmethod
def get_resources(cls):
"""Returns Extension Resources."""
controller = resource.Resource(
DetailQuotaSetsController(directory.get_plugin()),
faults=base.FAULT_MAP)
return [extensions.ResourceExtension(
RESOURCE_COLLECTION,
controller,
member_actions={'details': 'GET'},
collection_actions={'tenant': 'GET'})]
def get_extended_resources(self, version):
return EXTENDED_ATTRIBUTES_2_0 if version == "2.0" else {}
def get_required_extensions(self):
return ["quotas"]

View File

@ -234,27 +234,17 @@ class TrackedResource(BaseResource):
# Update quota usage
return self._resync(context, tenant_id, in_use)
def count(self, context, _plugin, tenant_id, resync_usage=True):
"""Return the current usage count for the resource.
def count_used(self, context, tenant_id, resync_usage=True):
"""Returns the current usage count for the resource.
This method will fetch aggregate information for resource usage
data, unless usage data are marked as "dirty".
In the latter case resource usage will be calculated counting
rows for tenant_id in the resource's database model.
Active reserved amount are instead always calculated by summing
amounts for matching records in the 'reservations' database model.
The _plugin and _resource parameters are unused but kept for
compatibility with the signature of the count method for
CountableResource instances.
:param context: The request context.
:param tenant_id: The ID of the tenant
:param resync_usage: Default value is set to True. Syncs
with in_use usage.
"""
# Load current usage data, setting a row-level lock on the DB
usage_info = quota_api.get_quota_usage_by_resource_and_tenant(
context, self.name, tenant_id)
# Always fetch reservations, as they are not tracked by usage counters
reservations = quota_api.get_reservations_for_resources(
context, tenant_id, [self.name])
reserved = reservations.get(self.name, 0)
# If dirty or missing, calculate actual resource usage querying
# the database and set/create usage info data
@ -287,7 +277,26 @@ class TrackedResource(BaseResource):
"Used quota:%(used)d."),
{'resource': self.name,
'used': usage_info.used})
return usage_info.used + reserved
return usage_info.used
def count_reserved(self, context, tenant_id):
"""Return the current reservation count for the resource."""
# NOTE(princenana) Current implementation of reservations
# is ephemeral and returns the default value
reservations = quota_api.get_reservations_for_resources(
context, tenant_id, [self.name])
reserved = reservations.get(self.name, 0)
return reserved
def count(self, context, _plugin, tenant_id, resync_usage=True):
"""Return the count of the resource.
The _plugin parameter is unused but kept for
compatibility with the signature of the count method for
CountableResource instances.
"""
return (self.count_used(context, tenant_id, resync_usage) +
self.count_reserved(context, tenant_id))
def _except_bulk_delete(self, delete_context):
if delete_context.mapper.class_ == self._model_class:

View File

@ -29,6 +29,7 @@ NETWORK_API_EXTENSIONS+=",project-id"
NETWORK_API_EXTENSIONS+=",provider"
NETWORK_API_EXTENSIONS+=",qos"
NETWORK_API_EXTENSIONS+=",quotas"
NETWORK_API_EXTENSIONS+=",quota_details"
NETWORK_API_EXTENSIONS+=",rbac-policies"
NETWORK_API_EXTENSIONS+=",router"
NETWORK_API_EXTENSIONS+=",router_availability_zone"

View File

@ -13,9 +13,11 @@
# License for the specific language governing permissions and limitations
# under the License.
import six
from tempest.lib.common.utils import data_utils
from tempest.lib import decorators
from tempest.lib import exceptions as lib_exc
from tempest import test
from neutron.tests.tempest.api import base
from neutron.tests.tempest import config
@ -58,6 +60,19 @@ class QuotasTestBase(base.BaseAdminNetworkTest):
except lib_exc.NotFound:
pass
def _create_network(self, project_id):
network = self.create_network(client=self.admin_client,
tenant_id=project_id)
self.addCleanup(self.admin_client.delete_network,
network['id'])
return network
def _create_port(self, **kwargs):
port = self.admin_client.create_port(**kwargs)['port']
self.addCleanup(self.admin_client.delete_port,
port['id'])
return port
class QuotasTest(QuotasTestBase):
"""Test the Neutron API of Quotas.
@ -67,6 +82,7 @@ class QuotasTest(QuotasTestBase):
list quotas for tenants who have non-default quota values
show quotas for a specified tenant
show detail quotas for a specified tenant
update quotas for a specified tenant
reset quotas to default values for a specified tenant
@ -108,3 +124,39 @@ class QuotasTest(QuotasTestBase):
non_default_quotas = self.admin_client.list_quotas()
for q in non_default_quotas['quotas']:
self.assertNotEqual(tenant_id, q['tenant_id'])
@decorators.idempotent_id('e974b5ba-090a-452c-a578-f9710151d9fc')
@decorators.attr(type='gate')
@test.requires_ext(extension="quota_details", service="network")
def test_detail_quotas(self):
tenant_id = self._create_tenant()['id']
new_quotas = {'network': {'used': 1, 'limit': 2, 'reserved': 0},
'port': {'used': 1, 'limit': 2, 'reserved': 0}}
# update quota limit for tenant
new_quota = {'network': new_quotas['network']['limit'], 'port':
new_quotas['port']['limit']}
quota_set = self._setup_quotas(tenant_id, **new_quota)
# create test resources
network = self._create_network(tenant_id)
post_body = {"network_id": network['id'],
"tenant_id": tenant_id}
self._create_port(**post_body)
# confirm from extended API quotas were changed
# as requested for tenant
quota_set = self.admin_client.show_details_quota(tenant_id)
quota_set = quota_set['quota']
for key, value in six.iteritems(new_quotas):
self.assertEqual(new_quotas[key]['limit'],
quota_set[key]['limit'])
self.assertEqual(new_quotas[key]['reserved'],
quota_set[key]['reserved'])
self.assertEqual(new_quotas[key]['used'],
quota_set[key]['used'])
# validate 'default' action for old extension
quota_limit = self.admin_client.show_quotas(tenant_id)['quota']
for key, value in six.iteritems(new_quotas):
self.assertEqual(new_quotas[key]['limit'], quota_limit[key])

View File

@ -217,11 +217,12 @@ class BaseNetworkTest(test.BaseTestCase):
pass
@classmethod
def create_network(cls, network_name=None, **kwargs):
def create_network(cls, network_name=None, client=None, **kwargs):
"""Wrapper utility that returns a test network."""
network_name = network_name or data_utils.rand_name('test-network-')
body = cls.client.create_network(name=network_name, **kwargs)
client = client or cls.client
body = client.create_network(name=network_name, **kwargs)
network = body['network']
cls.networks.append(network)
return network

View File

@ -124,7 +124,13 @@ class NetworkClientJSON(service_client.RestClient):
# list of field's name. An example:
# {'fields': ['id', 'name']}
plural = self.pluralize(resource_name)
uri = '%s/%s' % (self.get_uri(plural), resource_id)
if 'details_quotas' in plural:
details, plural = plural.split('_')
uri = '%s/%s/%s' % (self.get_uri(plural),
resource_id, details)
else:
uri = '%s/%s' % (self.get_uri(plural), resource_id)
if fields:
uri += '?' + urlparse.urlencode(fields, doseq=1)
resp, body = self.get(uri)

View File

@ -51,7 +51,8 @@ EXTDIR = os.path.join(base.ROOTDIR, 'unit/extensions')
_uuid = uuidutils.generate_uuid
def _get_path(resource, id=None, action=None, fmt=None):
def _get_path(resource, id=None, action=None,
fmt=None, endpoint=None):
path = '/%s' % resource
if id is not None:
@ -63,6 +64,9 @@ def _get_path(resource, id=None, action=None, fmt=None):
if fmt is not None:
path = path + '.%s' % fmt
if endpoint is not None:
path = path + '/%s' % endpoint
return path

View File

@ -18,14 +18,26 @@ from neutron_lib import exceptions as lib_exc
from neutron.common import exceptions
from neutron.db import db_base_plugin_v2 as base_plugin
from neutron.db.quota import api as quota_api
from neutron.db.quota import driver
from neutron.objects import quota as quota_obj
from neutron.quota import resource
from neutron.tests import base
from neutron.tests.unit import quota as test_quota
from neutron.tests.unit import testlib_api
DB_PLUGIN_KLASS = 'neutron.db.db_base_plugin_v2.NeutronDbPluginV2'
def _count_resource(context, plugin, resource, tenant_id):
"""A fake counting function to determine current used counts"""
if resource[-1] == 's':
resource = resource[:-1]
result = quota_obj.QuotaUsage.get_object_dirty_protected(
context, resource=resource)
return 0 if not result else result.in_use
class FakePlugin(base_plugin.NeutronDbPluginV2, driver.DbQuotaDriver):
"""A fake plugin class containing all DB methods."""
@ -46,6 +58,28 @@ class TestResource(object):
return self.fake_count
class TestTrackedResource(resource.TrackedResource):
"""Describes a test tracked resource for detailed quota checking"""
def __init__(self, name, model_class, flag=None,
plural_name=None):
super(TestTrackedResource, self).__init__(
name, model_class, flag=flag, plural_name=None)
@property
def default(self):
return self.flag
class TestCountableResource(resource.CountableResource):
"""Describes a test countable resource for detailed quota checking"""
def __init__(self, name, count, flag=-1, plural_name=None):
super(TestCountableResource, self).__init__(
name, count, flag=flag, plural_name=None)
@property
def default(self):
return self.flag
PROJECT = 'prj_test'
RESOURCE = 'res_test'
ALT_RESOURCE = 'res_test_meh'
@ -227,3 +261,48 @@ class TestDbQuotaDriver(testlib_api.SqlTestCase,
resources,
deltas,
self.plugin)
def test_get_detailed_tenant_quotas_resource(self):
res = {RESOURCE: TestTrackedResource(RESOURCE, test_quota.MehModel)}
self.plugin.update_quota_limit(self.context, PROJECT, RESOURCE, 6)
quota_driver = driver.DbQuotaDriver()
quota_driver.make_reservation(self.context, PROJECT, res,
{RESOURCE: 1}, self.plugin)
quota_api.set_quota_usage(self.context, RESOURCE, PROJECT, 2)
detailed_quota = self.plugin.get_detailed_tenant_quotas(self.context,
res, PROJECT)
self.assertEqual(6, detailed_quota[RESOURCE]['limit'])
self.assertEqual(2, detailed_quota[RESOURCE]['used'])
self.assertEqual(1, detailed_quota[RESOURCE]['reserved'])
def test_get_detailed_tenant_quotas_multiple_resource(self):
project_1 = 'prj_test_1'
resource_1 = 'res_test_1'
resource_2 = 'res_test_2'
resources = {resource_1:
TestTrackedResource(resource_1, test_quota.MehModel),
resource_2:
TestCountableResource(resource_2, _count_resource)}
self.plugin.update_quota_limit(self.context, project_1, resource_1, 6)
self.plugin.update_quota_limit(self.context, project_1, resource_2, 9)
quota_driver = driver.DbQuotaDriver()
quota_driver.make_reservation(self.context, project_1,
resources,
{resource_1: 1, resource_2: 7},
self.plugin)
quota_api.set_quota_usage(self.context, resource_1, project_1, 2)
quota_api.set_quota_usage(self.context, resource_2, project_1, 3)
detailed_quota = self.plugin.get_detailed_tenant_quotas(self.context,
resources,
project_1)
self.assertEqual(6, detailed_quota[resource_1]['limit'])
self.assertEqual(1, detailed_quota[resource_1]['reserved'])
self.assertEqual(2, detailed_quota[resource_1]['used'])
self.assertEqual(9, detailed_quota[resource_2]['limit'])
self.assertEqual(7, detailed_quota[resource_2]['reserved'])
self.assertEqual(3, detailed_quota[resource_2]['used'])

View File

@ -0,0 +1,153 @@
# Copyright 2017 Intel Corporation.
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import mock
from oslo_config import cfg
import webtest
from neutron_lib import context
from neutron.api import extensions
from neutron.api.v2 import router
from neutron.common import config
from neutron.conf import quota as qconf
from neutron import quota
from neutron.tests import tools
from neutron.tests.unit.api.v2 import test_base
from neutron.tests.unit import testlib_api
DEFAULT_QUOTAS_ACTION = 'details'
TARGET_PLUGIN = 'neutron.plugins.ml2.plugin.Ml2Plugin'
_get_path = test_base._get_path
class DetailQuotaExtensionTestCase(testlib_api.WebTestCase):
def setUp(self):
super(DetailQuotaExtensionTestCase, self).setUp()
# Ensure existing ExtensionManager is not used
extensions.PluginAwareExtensionManager._instance = None
self.useFixture(tools.AttributeMapMemento())
# Create the default configurations
self.config_parse()
# Update the plugin and extensions path
self.setup_coreplugin('ml2')
quota.QUOTAS = quota.QuotaEngine()
self._plugin_patcher = mock.patch(TARGET_PLUGIN, autospec=True)
self.plugin = self._plugin_patcher.start()
self.plugin.return_value.supported_extension_aliases = \
['quotas', 'quota_details']
# QUOTAS will register the items in conf when starting
ext_mgr = extensions.PluginAwareExtensionManager.get_instance()
app = config.load_paste_app('extensions_test_app')
ext_middleware = extensions.ExtensionMiddleware(app, ext_mgr=ext_mgr)
self.api = webtest.TestApp(ext_middleware)
# Initialize the router for the core API in order to ensure core quota
# resources are registered
router.APIRouter()
class DetailQuotaExtensionDbTestCase(DetailQuotaExtensionTestCase):
fmt = 'json'
def test_show_detail_quotas(self):
tenant_id = 'tenant_id1'
env = {'neutron.context': context.Context('', tenant_id,
is_admin=True)}
res = self.api.get(_get_path('quotas', id=tenant_id,
fmt=self.fmt,
endpoint=DEFAULT_QUOTAS_ACTION),
extra_environ=env)
self.assertEqual(200, res.status_int)
quota = self.deserialize(res)
self.assertEqual(0, quota['quota']['network']['reserved'])
self.assertEqual(0, quota['quota']['subnet']['reserved'])
self.assertEqual(0, quota['quota']['port']['reserved'])
self.assertEqual(0, quota['quota']['network']['used'])
self.assertEqual(0, quota['quota']['subnet']['used'])
self.assertEqual(0, quota['quota']['port']['used'])
self.assertEqual(qconf.DEFAULT_QUOTA_NETWORK,
quota['quota']['network']['limit'])
self.assertEqual(qconf.DEFAULT_QUOTA_SUBNET,
quota['quota']['subnet']['limit'])
self.assertEqual(qconf.DEFAULT_QUOTA_PORT,
quota['quota']['port']['limit'])
def test_detail_quotas_negative_limit_value(self):
cfg.CONF.set_override(
'quota_port', -666, group='QUOTAS')
cfg.CONF.set_override(
'quota_network', -10, group='QUOTAS')
cfg.CONF.set_override(
'quota_subnet', -50, group='QUOTAS')
tenant_id = 'tenant_id1'
env = {'neutron.context': context.Context('', tenant_id,
is_admin=True)}
res = self.api.get(_get_path('quotas', id=tenant_id,
fmt=self.fmt,
endpoint=DEFAULT_QUOTAS_ACTION),
extra_environ=env)
self.assertEqual(200, res.status_int)
quota = self.deserialize(res)
self.assertEqual(0, quota['quota']['network']['reserved'])
self.assertEqual(0, quota['quota']['subnet']['reserved'])
self.assertEqual(0, quota['quota']['port']['reserved'])
self.assertEqual(0, quota['quota']['network']['used'])
self.assertEqual(0, quota['quota']['subnet']['used'])
self.assertEqual(0, quota['quota']['port']['used'])
self.assertEqual(qconf.DEFAULT_QUOTA,
quota['quota']['network']['limit'])
self.assertEqual(qconf.DEFAULT_QUOTA,
quota['quota']['subnet']['limit'])
self.assertEqual(qconf.DEFAULT_QUOTA,
quota['quota']['port']['limit'])
def test_show_detail_quotas_with_admin(self):
tenant_id = 'tenant_id1'
env = {'neutron.context': context.Context('', tenant_id + '2',
is_admin=True)}
res = self.api.get(_get_path('quotas', id=tenant_id,
fmt=self.fmt,
endpoint=DEFAULT_QUOTAS_ACTION),
extra_environ=env)
self.assertEqual(200, res.status_int)
quota = self.deserialize(res)
self.assertEqual(0, quota['quota']['network']['reserved'])
self.assertEqual(0, quota['quota']['subnet']['reserved'])
self.assertEqual(0, quota['quota']['port']['reserved'])
self.assertEqual(0, quota['quota']['network']['used'])
self.assertEqual(0, quota['quota']['subnet']['used'])
self.assertEqual(0, quota['quota']['port']['used'])
self.assertEqual(qconf.DEFAULT_QUOTA_NETWORK,
quota['quota']['network']['limit'])
self.assertEqual(qconf.DEFAULT_QUOTA_SUBNET,
quota['quota']['subnet']['limit'])
self.assertEqual(qconf.DEFAULT_QUOTA_PORT,
quota['quota']['port']['limit'])
def test_detail_quotas_without_admin_forbidden_returns_403(self):
tenant_id = 'tenant_id1'
env = {'neutron.context': context.Context('', tenant_id,
is_admin=False)}
res = self.api.get(_get_path('quotas', id=tenant_id,
fmt=self.fmt,
endpoint=DEFAULT_QUOTAS_ACTION),
extra_environ=env, expect_errors=True)
self.assertEqual(403, res.status_int)

View File

@ -129,6 +129,25 @@ class TestTrackedResource(testlib_api.SqlTestCase):
# count() always resyncs with the db
self.assertEqual(2, res.count(self.context, None, self.tenant_id))
def test_count_reserved(self):
res = self._create_resource()
quota_api.create_reservation(self.context, self.tenant_id,
{res.name: 1})
self.assertEqual(1, res.count_reserved(self.context, self.tenant_id))
def test_count_used_first_call_with_dirty_false(self):
quota_api.set_quota_usage(
self.context, self.resource, self.tenant_id, in_use=1)
res = self._create_resource()
self._add_data()
# explicitly set dirty flag to False
quota_api.set_all_quota_usage_dirty(
self.context, self.resource, dirty=False)
# Expect correct count_used to be returned
# anyway since the first call to
# count_used() always resyncs with the db
self.assertEqual(2, res.count_used(self.context, self.tenant_id))
def _test_count(self):
res = self._create_resource()
quota_api.set_quota_usage(
@ -148,6 +167,18 @@ class TestTrackedResource(testlib_api.SqlTestCase):
None,
self.tenant_id))
def test_count_used_with_dirty_false(self):
res = self._test_count()
res.count_used(self.context, self.tenant_id)
# At this stage count_used has been invoked,
# and the dirty flag should be false. Another invocation
# of count_used should not query the model class
set_quota = 'neutron.db.quota.api.set_quota_usage'
with mock.patch(set_quota) as mock_set_quota:
self.assertEqual(0, mock_set_quota.call_count)
self.assertEqual(2, res.count_used(self.context,
self.tenant_id))
def test_count_with_dirty_true_resync(self):
res = self._test_count()
# Expect correct count to be returned, which also implies
@ -157,6 +188,14 @@ class TestTrackedResource(testlib_api.SqlTestCase):
self.tenant_id,
resync_usage=True))
def test_count_used_with_dirty_true_resync(self):
res = self._test_count()
# Expect correct count_used to be returned, which also implies
# set_quota_usage has been invoked with the correct parameters
self.assertEqual(2, res.count_used(self.context,
self.tenant_id,
resync_usage=True))
def test_count_with_dirty_true_resync_calls_set_quota_usage(self):
res = self._test_count()
set_quota_usage = 'neutron.db.quota.api.set_quota_usage'
@ -169,6 +208,18 @@ class TestTrackedResource(testlib_api.SqlTestCase):
mock_set_quota_usage.assert_called_once_with(
self.context, self.resource, self.tenant_id, in_use=2)
def test_count_used_with_dirty_true_resync_calls_set_quota_usage(self):
res = self._test_count()
set_quota_usage = 'neutron.db.quota.api.set_quota_usage'
with mock.patch(set_quota_usage) as mock_set_quota_usage:
quota_api.set_quota_usage_dirty(self.context,
self.resource,
self.tenant_id)
res.count_used(self.context, self.tenant_id,
resync_usage=True)
mock_set_quota_usage.assert_called_once_with(
self.context, self.resource, self.tenant_id, in_use=2)
def test_count_with_dirty_true_no_usage_info(self):
res = self._create_resource()
self._add_data()
@ -176,6 +227,13 @@ class TestTrackedResource(testlib_api.SqlTestCase):
# count to be returned
self.assertEqual(2, res.count(self.context, None, self.tenant_id))
def test_count_used_with_dirty_true_no_usage_info(self):
res = self._create_resource()
self._add_data()
# Invoke count_used without having usage info in DB - Expect correct
# count_used to be returned
self.assertEqual(2, res.count_used(self.context, self.tenant_id))
def test_count_with_dirty_true_no_usage_info_calls_set_quota_usage(self):
res = self._create_resource()
self._add_data()
@ -188,6 +246,19 @@ class TestTrackedResource(testlib_api.SqlTestCase):
mock_set_quota_usage.assert_called_once_with(
self.context, self.resource, self.tenant_id, in_use=2)
def test_count_used_with_dirty_true_no_usage_info_calls_set_quota_usage(
self):
res = self._create_resource()
self._add_data()
set_quota_usage = 'neutron.db.quota.api.set_quota_usage'
with mock.patch(set_quota_usage) as mock_set_quota_usage:
quota_api.set_quota_usage_dirty(self.context,
self.resource,
self.tenant_id)
res.count_used(self.context, self.tenant_id, resync_usage=True)
mock_set_quota_usage.assert_called_once_with(
self.context, self.resource, self.tenant_id, in_use=2)
def test_add_delete_data_triggers_event(self):
res = self._create_resource()
other_res = self._create_other_resource()

View File

@ -0,0 +1,6 @@
---
features:
- |
Implements a new extension, ``quota_details`` which extends existing quota API
to show detailed information for a specified tenant. The new API shows
details such as ``limits``, ``used``, ``reserved``.