Merge "Extend Quota API to report usage statistics"
This commit is contained in:
commit
9449a63021
@ -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):
|
||||
|
@ -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"
|
||||
|
99
neutron/extensions/quotasv2_detail.py
Normal file
99
neutron/extensions/quotasv2_detail.py
Normal 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"]
|
@ -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:
|
||||
|
@ -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"
|
||||
|
@ -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])
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
||||
|
||||
|
@ -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'])
|
||||
|
153
neutron/tests/unit/extensions/test_quotasv2_detail.py
Normal file
153
neutron/tests/unit/extensions/test_quotasv2_detail.py
Normal 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)
|
@ -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()
|
||||
|
@ -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``.
|
Loading…
Reference in New Issue
Block a user