From 31f09ab2ec394f98c6d2d37f9588a37a0d9eaf92 Mon Sep 17 00:00:00 2001 From: Mark McClain Date: Sat, 5 Jan 2013 19:28:35 -0500 Subject: [PATCH] refactor QuotaV2 import to match to other exts fixes bug 1096486 The previous code used a special extension loading mechanism to selectively load the Quota model is the plugin matched and object path. This was intended to load models required by plugins, but this loading actually occurred after the db schema was created, so the model was not always loaded. This fix refactors the code to make the QuotaV2 ext behave similarly to the other extensions ensuring the models are loaded prior to database schema creation. Change-Id: Id7d1f7ddee69bfc4419df375366319dedc3dc439 --- quantum/api/extensions.py | 29 ------ .../_quotav2_driver.py => db/quota_db.py} | 88 ++++++++++++------- quantum/extensions/_quotav2_model.py | 30 ------- quantum/extensions/quotasv2.py | 39 +++----- .../tests/unit/v2/quantumv2.conf.cisco.test | 2 +- .../plugins/linuxbridge/lb_quantum_plugin.py | 3 +- .../nicira/nicira_nvp_plugin/QuantumPlugin.py | 3 +- .../plugins/openvswitch/ovs_quantum_plugin.py | 3 +- quantum/quota.py | 7 +- .../tests/unit/cisco/test_network_plugin.py | 2 +- .../tests/unit/test_quota_per_tenant_ext.py | 41 +-------- 11 files changed, 85 insertions(+), 162 deletions(-) rename quantum/{extensions/_quotav2_driver.py => db/quota_db.py} (68%) delete mode 100644 quantum/extensions/_quotav2_model.py diff --git a/quantum/api/extensions.py b/quantum/api/extensions.py index 07c8cff5c..4ae379b8f 100644 --- a/quantum/api/extensions.py +++ b/quantum/api/extensions.py @@ -36,27 +36,6 @@ from quantum import wsgi LOG = logging.getLogger('quantum.api.extensions') -# Besides the supported_extension_aliases in plugin class, -# we also support register enabled extensions here so that we -# can load some mandatory files (such as db models) before initialize plugin -ENABLED_EXTS = { - 'quantum.plugins.linuxbridge.lb_quantum_plugin.LinuxBridgePluginV2': - { - 'ext_alias': ["quotas"], - 'ext_db_models': ['quantum.extensions._quotav2_model.Quota'], - }, - 'quantum.plugins.openvswitch.ovs_quantum_plugin.OVSQuantumPluginV2': - { - 'ext_alias': ["quotas"], - 'ext_db_models': ['quantum.extensions._quotav2_model.Quota'], - }, - 'quantum.plugins.nicira.nicira_nvp_plugin.QuantumPlugin.NvpPluginV2': - { - 'ext_alias': ["quotas"], - 'ext_db_models': ['quantum.extensions._quotav2_model.Quota'], - }, -} - class PluginInterface(object): __metaclass__ = ABCMeta @@ -559,9 +538,6 @@ class PluginAwareExtensionManager(ExtensionManager): alias in plugin.supported_extension_aliases) for plugin in self.plugins.values()) plugin_provider = cfg.CONF.core_plugin - if not supports_extension and plugin_provider in ENABLED_EXTS: - supports_extension = (alias in - ENABLED_EXTS[plugin_provider]['ext_alias']) if not supports_extension: LOG.warn(_("extension %s not supported by any of loaded plugins"), alias) @@ -581,11 +557,6 @@ class PluginAwareExtensionManager(ExtensionManager): @classmethod def get_instance(cls): if cls._instance is None: - plugin_provider = cfg.CONF.core_plugin - if plugin_provider in ENABLED_EXTS: - for model in ENABLED_EXTS[plugin_provider]['ext_db_models']: - LOG.debug('loading model %s', model) - model_class = importutils.import_class(model) cls._instance = cls(get_extensions_path(), QuantumManager.get_service_plugins()) return cls._instance diff --git a/quantum/extensions/_quotav2_driver.py b/quantum/db/quota_db.py similarity index 68% rename from quantum/extensions/_quotav2_driver.py rename to quantum/db/quota_db.py index d8abada75..4e73fb659 100644 --- a/quantum/extensions/_quotav2_driver.py +++ b/quantum/db/quota_db.py @@ -15,8 +15,22 @@ # License for the specific language governing permissions and limitations # under the License. +import sqlalchemy as sa + from quantum.common import exceptions -from quantum.extensions import _quotav2_model as quotav2_model +from quantum.db import model_base +from quantum.db import models_v2 + + +class Quota(model_base.BASEV2, models_v2.HasId): + """Represent a single quota override for a tenant. + + If there is no row for a given tenant id and resource, then the + default for the quota class is used. + """ + tenant_id = sa.Column(sa.String(255), index=True) + resource = sa.Column(sa.String(255)) + limit = sa.Column(sa.Integer) class DbQuotaDriver(object): @@ -38,17 +52,15 @@ class DbQuotaDriver(object): :return dict: from resource name to dict of name and limit """ - quotas = {} - tenant_quotas = context.session.query( - quotav2_model.Quota).filter_by(tenant_id=tenant_id).all() - tenant_quotas_dict = {} - for _quota in tenant_quotas: - tenant_quotas_dict[_quota['resource']] = _quota['limit'] - for key, resource in resources.items(): - quotas[key] = dict( - name=key, - limit=tenant_quotas_dict.get(key, resource.default)) - return quotas + # init with defaults + tenant_quota = dict((key, resource.default) + for key, resource in resources.items()) + + # update with tenant specific limits + q_qry = context.session.query(Quota).filter_by(tenant_id=tenant_id) + tenant_quota.update((q['resource'], q['limit']) for q in q_qry.all()) + + return tenant_quota @staticmethod def delete_tenant_quota(context, tenant_id): @@ -57,8 +69,8 @@ class DbQuotaDriver(object): Atfer deletion, this tenant will use default quota values in conf. """ with context.session.begin(): - tenant_quotas = context.session.query( - quotav2_model.Quota).filter_by(tenant_id=tenant_id).all() + tenant_quotas = context.session.query(Quota).filter_by( + tenant_id=tenant_id).all() for quota in tenant_quotas: context.session.delete(quota) @@ -74,22 +86,38 @@ class DbQuotaDriver(object): resourcekey2: ... """ - _quotas = context.session.query(quotav2_model.Quota).all() - quotas = {} - tenant_quotas_dict = {} - for _quota in _quotas: - tenant_id = _quota['tenant_id'] - if tenant_id not in quotas: - quotas[tenant_id] = {'tenant_id': tenant_id} - tenant_quotas_dict = quotas[tenant_id] - tenant_quotas_dict[_quota['resource']] = _quota['limit'] - - # we complete the quotas according to input resources - for tenant_quotas_dict in quotas.itervalues(): - for key, resource in resources.items(): - tenant_quotas_dict[key] = tenant_quotas_dict.get( - key, resource.default) - return quotas.itervalues() + tenant_default = dict((key, resource.default) + for key, resource in resources.items()) + + all_tenant_quotas = {} + + for quota in context.session.query(Quota).all(): + tenant_id = quota['tenant_id'] + + # avoid setdefault() because only want to copy when actually req'd + tenant_quota = all_tenant_quotas.get(tenant_id) + if tenant_quota is None: + tenant_quota = tenant_default.copy() + tenant_quota['tenant_id'] = tenant_id + all_tenant_quotas[tenant_id] = tenant_quota + + tenant_quota[quota['resource']] = quota['limit'] + + return all_tenant_quotas.itervalues() + + @staticmethod + def update_quota_limit(context, tenant_id, resource, limit): + with context.session.begin(): + tenant_quota = context.session.query(Quota).filter_by( + tenant_id=tenant_id, resource=resource).first() + + if tenant_quota: + tenant_quota.update({'limit': limit}) + else: + tenant_quota = Quota(tenant_id=tenant_id, + resource=resource, + limit=limit) + context.session.add(tenant_quota) def _get_quotas(self, context, tenant_id, resources, keys): """ diff --git a/quantum/extensions/_quotav2_model.py b/quantum/extensions/_quotav2_model.py deleted file mode 100644 index 474fa228b..000000000 --- a/quantum/extensions/_quotav2_model.py +++ /dev/null @@ -1,30 +0,0 @@ -# Copyright (c) 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. - -import sqlalchemy as sa - -from quantum.db import model_base -from quantum.db import models_v2 - - -class Quota(model_base.BASEV2, models_v2.HasId): - """Represent a single quota override for a tenant. - - If there is no row for a given tenant id and resource, then the - default for the quota class is used. - """ - tenant_id = sa.Column(sa.String(255), index=True) - resource = sa.Column(sa.String(255)) - limit = sa.Column(sa.Integer) diff --git a/quantum/extensions/quotasv2.py b/quantum/extensions/quotasv2.py index ddc35d361..6c7cd3b17 100644 --- a/quantum/extensions/quotasv2.py +++ b/quantum/extensions/quotasv2.py @@ -20,17 +20,16 @@ import webob from quantum.api import extensions from quantum.api.v2 import base from quantum.common import exceptions -from quantum.extensions import _quotav2_driver as quotav2_driver -from quantum.extensions import _quotav2_model as quotav2_model from quantum.manager import QuantumManager from quantum.openstack.common import cfg +from quantum.openstack.common import importutils from quantum import quota from quantum import wsgi RESOURCE_NAME = 'quota' RESOURCE_COLLECTION = RESOURCE_NAME + "s" QUOTAS = quota.QUOTAS -DB_QUOTA_DRIVER = 'quantum.extensions._quotav2_driver.DbQuotaDriver' +DB_QUOTA_DRIVER = 'quantum.db.quota_db.DbQuotaDriver' EXTENDED_ATTRIBUTES_2_0 = { RESOURCE_COLLECTION: {} } @@ -48,6 +47,7 @@ class QuotaSetsController(wsgi.Controller): def __init__(self, plugin): self._resource_name = RESOURCE_NAME self._plugin = plugin + self._driver = importutils.import_class(DB_QUOTA_DRIVER) def _get_body(self, request): body = self._deserialize(request.body, request.get_content_type()) @@ -57,9 +57,8 @@ class QuotaSetsController(wsgi.Controller): return req_body def _get_quotas(self, request, tenant_id): - values = quotav2_driver.DbQuotaDriver.get_tenant_quotas( + return self._driver.get_tenant_quotas( request.context, QUOTAS.resources, tenant_id) - return dict((k, v['limit']) for k, v in values.items()) def create(self, request, body=None): raise NotImplementedError() @@ -69,8 +68,7 @@ class QuotaSetsController(wsgi.Controller): if not context.is_admin: raise webob.exc.HTTPForbidden() return {self._resource_name + "s": - quotav2_driver.DbQuotaDriver.get_all_quotas( - context, QUOTAS.resources)} + self._driver.get_all_quotas(context, QUOTAS.resources)} def tenant(self, request): """Retrieve the tenant info in context.""" @@ -93,37 +91,26 @@ class QuotaSetsController(wsgi.Controller): def _check_modification_delete_privilege(self, context, tenant_id): if not tenant_id: raise webob.exc.HTTPBadRequest('invalid tenant') - if (not context.is_admin): + if not context.is_admin: raise webob.exc.HTTPForbidden() return tenant_id def delete(self, request, id): - tenant_id = id tenant_id = self._check_modification_delete_privilege(request.context, - tenant_id) - quotav2_driver.DbQuotaDriver.delete_tenant_quota(request.context, - tenant_id) + id) + self._driver.delete_tenant_quota(request.context, tenant_id) def update(self, request, id): - tenant_id = id tenant_id = self._check_modification_delete_privilege(request.context, - tenant_id) + id) req_body = self._get_body(request) for key in req_body[self._resource_name].keys(): if key in QUOTAS.resources: value = int(req_body[self._resource_name][key]) - with request.context.session.begin(): - tenant_quotas = request.context.session.query( - quotav2_model.Quota).filter_by(tenant_id=tenant_id, - resource=key).all() - if not tenant_quotas: - quota = quotav2_model.Quota(tenant_id=tenant_id, - resource=key, - limit=value) - request.context.session.add(quota) - else: - quota = tenant_quotas[0] - quota.update({'limit': value}) + self._driver.update_quota_limit(request.context, + tenant_id, + key, + value) return {self._resource_name: self._get_quotas(request, tenant_id)} diff --git a/quantum/plugins/cisco/tests/unit/v2/quantumv2.conf.cisco.test b/quantum/plugins/cisco/tests/unit/v2/quantumv2.conf.cisco.test index 56511a739..dc855a7cc 100644 --- a/quantum/plugins/cisco/tests/unit/v2/quantumv2.conf.cisco.test +++ b/quantum/plugins/cisco/tests/unit/v2/quantumv2.conf.cisco.test @@ -40,4 +40,4 @@ default_quota = -1 # default driver to use for quota checks # quota_driver = quantum.quota.ConfDriver -quota_driver = quantum.extensions._quotav2_driver.DbQuotaDriver +quota_driver = quantum.db.quota_db.DbQuotaDriver diff --git a/quantum/plugins/linuxbridge/lb_quantum_plugin.py b/quantum/plugins/linuxbridge/lb_quantum_plugin.py index 7c946d000..4f47a19ea 100644 --- a/quantum/plugins/linuxbridge/lb_quantum_plugin.py +++ b/quantum/plugins/linuxbridge/lb_quantum_plugin.py @@ -25,6 +25,7 @@ from quantum.db import db_base_plugin_v2 from quantum.db import dhcp_rpc_base from quantum.db import l3_db from quantum.db import l3_rpc_base +from quantum.db import quota_db from quantum.extensions import portbindings from quantum.extensions import providernet as provider from quantum.openstack.common import cfg @@ -156,7 +157,7 @@ class LinuxBridgePluginV2(db_base_plugin_v2.QuantumDbPluginV2, # is qualified by class __native_bulk_support = True - supported_extension_aliases = ["provider", "router", "binding"] + supported_extension_aliases = ["provider", "router", "binding", "quotas"] network_view = "extension:provider_network:view" network_set = "extension:provider_network:set" diff --git a/quantum/plugins/nicira/nicira_nvp_plugin/QuantumPlugin.py b/quantum/plugins/nicira/nicira_nvp_plugin/QuantumPlugin.py index 024157648..85ac3035d 100644 --- a/quantum/plugins/nicira/nicira_nvp_plugin/QuantumPlugin.py +++ b/quantum/plugins/nicira/nicira_nvp_plugin/QuantumPlugin.py @@ -43,6 +43,7 @@ from quantum.db import api as db from quantum.db import db_base_plugin_v2 from quantum.db import dhcp_rpc_base from quantum.db import models_v2 +from quantum.db import quota_db from quantum.extensions import providernet as pnet from quantum.openstack.common import cfg from quantum.openstack.common import rpc @@ -127,7 +128,7 @@ class NvpPluginV2(db_base_plugin_v2.QuantumDbPluginV2): functionality using NVP. """ - supported_extension_aliases = ["provider"] + supported_extension_aliases = ["provider", "quotas"] # Default controller cluster default_cluster = None diff --git a/quantum/plugins/openvswitch/ovs_quantum_plugin.py b/quantum/plugins/openvswitch/ovs_quantum_plugin.py index 9ce325d6a..6722f4676 100644 --- a/quantum/plugins/openvswitch/ovs_quantum_plugin.py +++ b/quantum/plugins/openvswitch/ovs_quantum_plugin.py @@ -31,6 +31,7 @@ from quantum.db import db_base_plugin_v2 from quantum.db import dhcp_rpc_base from quantum.db import l3_db from quantum.db import l3_rpc_base +from quantum.db import quota_db from quantum.extensions import portbindings from quantum.extensions import providernet as provider from quantum.openstack.common import cfg @@ -194,7 +195,7 @@ class OVSQuantumPluginV2(db_base_plugin_v2.QuantumDbPluginV2, # bulk operations. Name mangling is used in order to ensure it # is qualified by class __native_bulk_support = True - supported_extension_aliases = ["provider", "router", "binding"] + supported_extension_aliases = ["provider", "router", "binding", "quotas"] network_view = "extension:provider_network:view" network_set = "extension:provider_network:set" diff --git a/quantum/quota.py b/quantum/quota.py index 22efcc981..e4bc01006 100644 --- a/quantum/quota.py +++ b/quantum/quota.py @@ -139,10 +139,9 @@ class BaseResource(object): @property def default(self): """Return the default value of the quota.""" - if hasattr(cfg.CONF.QUOTAS, self.flag): - return cfg.CONF.QUOTAS[self.flag] - else: - return cfg.CONF.QUOTAS.default_quota + return getattr(cfg.CONF.QUOTAS, + self.flag, + cfg.CONF.QUOTAS.default_quota) class CountableResource(BaseResource): diff --git a/quantum/tests/unit/cisco/test_network_plugin.py b/quantum/tests/unit/cisco/test_network_plugin.py index 8e3f9fc6f..4ccf0f543 100644 --- a/quantum/tests/unit/cisco/test_network_plugin.py +++ b/quantum/tests/unit/cisco/test_network_plugin.py @@ -23,7 +23,7 @@ from quantum.common.test_lib import test_config from quantum import context from quantum.db import api as db from quantum.db import l3_db -from quantum.extensions import _quotav2_model as quotav2_model +from quantum.db import quota_db from quantum.manager import QuantumManager from quantum.openstack.common import cfg from quantum.plugins.cisco.common import cisco_constants as const diff --git a/quantum/tests/unit/test_quota_per_tenant_ext.py b/quantum/tests/unit/test_quota_per_tenant_ext.py index 917b1bbed..440cca8a2 100644 --- a/quantum/tests/unit/test_quota_per_tenant_ext.py +++ b/quantum/tests/unit/test_quota_per_tenant_ext.py @@ -26,12 +26,6 @@ _get_path = test_api_v2._get_path class QuotaExtensionTestCase(unittest.TestCase): def setUp(self): - if getattr(self, 'testflag', 1) == 1: - self._setUp1() - else: - self._setUp2() - - def _setUp1(self): db._ENGINE = None db._MAKER = None # Ensure 'stale' patched copies of the plugin are never returned @@ -53,7 +47,7 @@ class QuotaExtensionTestCase(unittest.TestCase): cfg.CONF.set_override('core_plugin', TARGET_PLUGIN) cfg.CONF.set_override( 'quota_driver', - 'quantum.extensions._quotav2_driver.DbQuotaDriver', + 'quantum.db.quota_db.DbQuotaDriver', group='QUOTAS') cfg.CONF.set_override( 'quota_items', @@ -62,6 +56,7 @@ class QuotaExtensionTestCase(unittest.TestCase): self._plugin_patcher = mock.patch(TARGET_PLUGIN, autospec=True) self.plugin = self._plugin_patcher.start() + self.plugin.return_value.supported_extension_aliases = ['quotas'] # QUOTAS will regester the items in conf when starting # extra1 here is added later, so have to do it manually quota.QUOTAS.register_resource_by_name('extra1') @@ -71,34 +66,6 @@ class QuotaExtensionTestCase(unittest.TestCase): ext_middleware = extensions.ExtensionMiddleware(app, ext_mgr=ext_mgr) self.api = webtest.TestApp(ext_middleware) - def _setUp2(self): - db._ENGINE = None - db._MAKER = None - # Ensure 'stale' patched copies of the plugin are never returned - manager.QuantumManager._instance = None - - # Ensure existing ExtensionManager is not used - extensions.PluginAwareExtensionManager._instance = None - - # Save the global RESOURCE_ATTRIBUTE_MAP - self.saved_attr_map = {} - for resource, attrs in attributes.RESOURCE_ATTRIBUTE_MAP.iteritems(): - self.saved_attr_map[resource] = attrs.copy() - - # Create the default configurations - args = ['--config-file', test_extensions.etcdir('quantum.conf.test')] - config.parse(args=args) - - # Update the plugin and extensions path - cfg.CONF.set_override('core_plugin', TARGET_PLUGIN) - self._plugin_patcher = mock.patch(TARGET_PLUGIN, autospec=True) - self.plugin = self._plugin_patcher.start() - ext_mgr = extensions.PluginAwareExtensionManager.get_instance() - l2network_db_v2.initialize() - app = config.load_paste_app('extensions_test_app') - ext_middleware = extensions.ExtensionMiddleware(app, ext_mgr=ext_mgr) - self.api = webtest.TestApp(ext_middleware) - def tearDown(self): self._plugin_patcher.stop() self.api = None @@ -114,7 +81,7 @@ class QuotaExtensionTestCase(unittest.TestCase): res = self.api.get(_get_path('quotas')) self.assertEqual(200, res.status_int) - def test_quotas_defaul_values(self): + def test_quotas_default_values(self): tenant_id = 'tenant_id1' env = {'quantum.context': context.Context('', tenant_id)} res = self.api.get(_get_path('quotas', id=tenant_id), @@ -181,10 +148,8 @@ class QuotaExtensionTestCase(unittest.TestCase): self.assertEqual(403, res.status_int) def test_quotas_loaded_bad(self): - self.testflag = 2 try: res = self.api.get(_get_path('quotas'), expect_errors=True) self.assertEqual(404, res.status_int) except Exception: pass - self.testflag = 1 -- 2.45.2