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
This commit is contained in:
Mark McClain 2013-01-05 19:28:35 -05:00
parent 2a10cd2b6c
commit 31f09ab2ec
11 changed files with 84 additions and 161 deletions

View File

@ -36,27 +36,6 @@ from quantum import wsgi
LOG = logging.getLogger('quantum.api.extensions') 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): class PluginInterface(object):
__metaclass__ = ABCMeta __metaclass__ = ABCMeta
@ -559,9 +538,6 @@ class PluginAwareExtensionManager(ExtensionManager):
alias in plugin.supported_extension_aliases) alias in plugin.supported_extension_aliases)
for plugin in self.plugins.values()) for plugin in self.plugins.values())
plugin_provider = cfg.CONF.core_plugin 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: if not supports_extension:
LOG.warn(_("extension %s not supported by any of loaded plugins"), LOG.warn(_("extension %s not supported by any of loaded plugins"),
alias) alias)
@ -581,11 +557,6 @@ class PluginAwareExtensionManager(ExtensionManager):
@classmethod @classmethod
def get_instance(cls): def get_instance(cls):
if cls._instance is None: 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(), cls._instance = cls(get_extensions_path(),
QuantumManager.get_service_plugins()) QuantumManager.get_service_plugins())
return cls._instance return cls._instance

View File

@ -15,8 +15,22 @@
# License for the specific language governing permissions and limitations # License for the specific language governing permissions and limitations
# under the License. # under the License.
import sqlalchemy as sa
from quantum.common import exceptions 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): class DbQuotaDriver(object):
@ -38,17 +52,15 @@ class DbQuotaDriver(object):
:return dict: from resource name to dict of name and limit :return dict: from resource name to dict of name and limit
""" """
quotas = {} # init with defaults
tenant_quotas = context.session.query( tenant_quota = dict((key, resource.default)
quotav2_model.Quota).filter_by(tenant_id=tenant_id).all() for key, resource in resources.items())
tenant_quotas_dict = {}
for _quota in tenant_quotas: # update with tenant specific limits
tenant_quotas_dict[_quota['resource']] = _quota['limit'] q_qry = context.session.query(Quota).filter_by(tenant_id=tenant_id)
for key, resource in resources.items(): tenant_quota.update((q['resource'], q['limit']) for q in q_qry.all())
quotas[key] = dict(
name=key, return tenant_quota
limit=tenant_quotas_dict.get(key, resource.default))
return quotas
@staticmethod @staticmethod
def delete_tenant_quota(context, tenant_id): 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. Atfer deletion, this tenant will use default quota values in conf.
""" """
with context.session.begin(): with context.session.begin():
tenant_quotas = context.session.query( tenant_quotas = context.session.query(Quota).filter_by(
quotav2_model.Quota).filter_by(tenant_id=tenant_id).all() tenant_id=tenant_id).all()
for quota in tenant_quotas: for quota in tenant_quotas:
context.session.delete(quota) context.session.delete(quota)
@ -74,22 +86,38 @@ class DbQuotaDriver(object):
resourcekey2: ... resourcekey2: ...
""" """
_quotas = context.session.query(quotav2_model.Quota).all() tenant_default = dict((key, resource.default)
quotas = {} for key, resource in resources.items())
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 all_tenant_quotas = {}
for tenant_quotas_dict in quotas.itervalues():
for key, resource in resources.items(): for quota in context.session.query(Quota).all():
tenant_quotas_dict[key] = tenant_quotas_dict.get( tenant_id = quota['tenant_id']
key, resource.default)
return quotas.itervalues() # 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): def _get_quotas(self, context, tenant_id, resources, keys):
""" """

View File

@ -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)

View File

@ -20,17 +20,16 @@ import webob
from quantum.api import extensions from quantum.api import extensions
from quantum.api.v2 import base from quantum.api.v2 import base
from quantum.common import exceptions 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.manager import QuantumManager
from quantum.openstack.common import cfg from quantum.openstack.common import cfg
from quantum.openstack.common import importutils
from quantum import quota from quantum import quota
from quantum import wsgi from quantum import wsgi
RESOURCE_NAME = 'quota' RESOURCE_NAME = 'quota'
RESOURCE_COLLECTION = RESOURCE_NAME + "s" RESOURCE_COLLECTION = RESOURCE_NAME + "s"
QUOTAS = quota.QUOTAS QUOTAS = quota.QUOTAS
DB_QUOTA_DRIVER = 'quantum.extensions._quotav2_driver.DbQuotaDriver' DB_QUOTA_DRIVER = 'quantum.db.quota_db.DbQuotaDriver'
EXTENDED_ATTRIBUTES_2_0 = { EXTENDED_ATTRIBUTES_2_0 = {
RESOURCE_COLLECTION: {} RESOURCE_COLLECTION: {}
} }
@ -48,6 +47,7 @@ class QuotaSetsController(wsgi.Controller):
def __init__(self, plugin): def __init__(self, plugin):
self._resource_name = RESOURCE_NAME self._resource_name = RESOURCE_NAME
self._plugin = plugin self._plugin = plugin
self._driver = importutils.import_class(DB_QUOTA_DRIVER)
def _get_body(self, request): def _get_body(self, request):
body = self._deserialize(request.body, request.get_content_type()) body = self._deserialize(request.body, request.get_content_type())
@ -57,9 +57,8 @@ class QuotaSetsController(wsgi.Controller):
return req_body return req_body
def _get_quotas(self, request, tenant_id): 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) request.context, QUOTAS.resources, tenant_id)
return dict((k, v['limit']) for k, v in values.items())
def create(self, request, body=None): def create(self, request, body=None):
raise NotImplementedError() raise NotImplementedError()
@ -69,8 +68,7 @@ class QuotaSetsController(wsgi.Controller):
if not context.is_admin: if not context.is_admin:
raise webob.exc.HTTPForbidden() raise webob.exc.HTTPForbidden()
return {self._resource_name + "s": return {self._resource_name + "s":
quotav2_driver.DbQuotaDriver.get_all_quotas( self._driver.get_all_quotas(context, QUOTAS.resources)}
context, QUOTAS.resources)}
def tenant(self, request): def tenant(self, request):
"""Retrieve the tenant info in context.""" """Retrieve the tenant info in context."""
@ -93,37 +91,26 @@ class QuotaSetsController(wsgi.Controller):
def _check_modification_delete_privilege(self, context, tenant_id): def _check_modification_delete_privilege(self, context, tenant_id):
if not tenant_id: if not tenant_id:
raise webob.exc.HTTPBadRequest('invalid tenant') raise webob.exc.HTTPBadRequest('invalid tenant')
if (not context.is_admin): if not context.is_admin:
raise webob.exc.HTTPForbidden() raise webob.exc.HTTPForbidden()
return tenant_id return tenant_id
def delete(self, request, id): def delete(self, request, id):
tenant_id = id
tenant_id = self._check_modification_delete_privilege(request.context, tenant_id = self._check_modification_delete_privilege(request.context,
tenant_id) id)
quotav2_driver.DbQuotaDriver.delete_tenant_quota(request.context, self._driver.delete_tenant_quota(request.context, tenant_id)
tenant_id)
def update(self, request, id): def update(self, request, id):
tenant_id = id
tenant_id = self._check_modification_delete_privilege(request.context, tenant_id = self._check_modification_delete_privilege(request.context,
tenant_id) id)
req_body = self._get_body(request) req_body = self._get_body(request)
for key in req_body[self._resource_name].keys(): for key in req_body[self._resource_name].keys():
if key in QUOTAS.resources: if key in QUOTAS.resources:
value = int(req_body[self._resource_name][key]) value = int(req_body[self._resource_name][key])
with request.context.session.begin(): self._driver.update_quota_limit(request.context,
tenant_quotas = request.context.session.query( tenant_id,
quotav2_model.Quota).filter_by(tenant_id=tenant_id, key,
resource=key).all() value)
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})
return {self._resource_name: self._get_quotas(request, tenant_id)} return {self._resource_name: self._get_quotas(request, tenant_id)}

View File

@ -40,4 +40,4 @@ default_quota = -1
# default driver to use for quota checks # default driver to use for quota checks
# quota_driver = quantum.quota.ConfDriver # quota_driver = quantum.quota.ConfDriver
quota_driver = quantum.extensions._quotav2_driver.DbQuotaDriver quota_driver = quantum.db.quota_db.DbQuotaDriver

View File

@ -25,6 +25,7 @@ from quantum.db import db_base_plugin_v2
from quantum.db import dhcp_rpc_base from quantum.db import dhcp_rpc_base
from quantum.db import l3_db from quantum.db import l3_db
from quantum.db import l3_rpc_base from quantum.db import l3_rpc_base
from quantum.db import quota_db
from quantum.extensions import portbindings from quantum.extensions import portbindings
from quantum.extensions import providernet as provider from quantum.extensions import providernet as provider
from quantum.openstack.common import cfg from quantum.openstack.common import cfg
@ -156,7 +157,7 @@ class LinuxBridgePluginV2(db_base_plugin_v2.QuantumDbPluginV2,
# is qualified by class # is qualified by class
__native_bulk_support = True __native_bulk_support = True
supported_extension_aliases = ["provider", "router", "binding"] supported_extension_aliases = ["provider", "router", "binding", "quotas"]
network_view = "extension:provider_network:view" network_view = "extension:provider_network:view"
network_set = "extension:provider_network:set" network_set = "extension:provider_network:set"

View File

@ -43,6 +43,7 @@ from quantum.db import api as db
from quantum.db import db_base_plugin_v2 from quantum.db import db_base_plugin_v2
from quantum.db import dhcp_rpc_base from quantum.db import dhcp_rpc_base
from quantum.db import models_v2 from quantum.db import models_v2
from quantum.db import quota_db
from quantum.extensions import providernet as pnet from quantum.extensions import providernet as pnet
from quantum.openstack.common import cfg from quantum.openstack.common import cfg
from quantum.openstack.common import rpc from quantum.openstack.common import rpc
@ -127,7 +128,7 @@ class NvpPluginV2(db_base_plugin_v2.QuantumDbPluginV2):
functionality using NVP. functionality using NVP.
""" """
supported_extension_aliases = ["provider"] supported_extension_aliases = ["provider", "quotas"]
# Default controller cluster # Default controller cluster
default_cluster = None default_cluster = None

View File

@ -31,6 +31,7 @@ from quantum.db import db_base_plugin_v2
from quantum.db import dhcp_rpc_base from quantum.db import dhcp_rpc_base
from quantum.db import l3_db from quantum.db import l3_db
from quantum.db import l3_rpc_base from quantum.db import l3_rpc_base
from quantum.db import quota_db
from quantum.extensions import portbindings from quantum.extensions import portbindings
from quantum.extensions import providernet as provider from quantum.extensions import providernet as provider
from quantum.openstack.common import cfg 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 # bulk operations. Name mangling is used in order to ensure it
# is qualified by class # is qualified by class
__native_bulk_support = True __native_bulk_support = True
supported_extension_aliases = ["provider", "router", "binding"] supported_extension_aliases = ["provider", "router", "binding", "quotas"]
network_view = "extension:provider_network:view" network_view = "extension:provider_network:view"
network_set = "extension:provider_network:set" network_set = "extension:provider_network:set"

View File

@ -139,10 +139,9 @@ class BaseResource(object):
@property @property
def default(self): def default(self):
"""Return the default value of the quota.""" """Return the default value of the quota."""
if hasattr(cfg.CONF.QUOTAS, self.flag): return getattr(cfg.CONF.QUOTAS,
return cfg.CONF.QUOTAS[self.flag] self.flag,
else: cfg.CONF.QUOTAS.default_quota)
return cfg.CONF.QUOTAS.default_quota
class CountableResource(BaseResource): class CountableResource(BaseResource):

View File

@ -23,7 +23,7 @@ from quantum.common.test_lib import test_config
from quantum import context from quantum import context
from quantum.db import api as db from quantum.db import api as db
from quantum.db import l3_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.manager import QuantumManager
from quantum.openstack.common import cfg from quantum.openstack.common import cfg
from quantum.plugins.cisco.common import cisco_constants as const from quantum.plugins.cisco.common import cisco_constants as const

View File

@ -26,12 +26,6 @@ _get_path = test_api_v2._get_path
class QuotaExtensionTestCase(unittest.TestCase): class QuotaExtensionTestCase(unittest.TestCase):
def setUp(self): def setUp(self):
if getattr(self, 'testflag', 1) == 1:
self._setUp1()
else:
self._setUp2()
def _setUp1(self):
db._ENGINE = None db._ENGINE = None
db._MAKER = None db._MAKER = None
# Ensure 'stale' patched copies of the plugin are never returned # 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('core_plugin', TARGET_PLUGIN)
cfg.CONF.set_override( cfg.CONF.set_override(
'quota_driver', 'quota_driver',
'quantum.extensions._quotav2_driver.DbQuotaDriver', 'quantum.db.quota_db.DbQuotaDriver',
group='QUOTAS') group='QUOTAS')
cfg.CONF.set_override( cfg.CONF.set_override(
'quota_items', 'quota_items',
@ -62,6 +56,7 @@ class QuotaExtensionTestCase(unittest.TestCase):
self._plugin_patcher = mock.patch(TARGET_PLUGIN, autospec=True) self._plugin_patcher = mock.patch(TARGET_PLUGIN, autospec=True)
self.plugin = self._plugin_patcher.start() self.plugin = self._plugin_patcher.start()
self.plugin.return_value.supported_extension_aliases = ['quotas']
# QUOTAS will regester the items in conf when starting # QUOTAS will regester the items in conf when starting
# extra1 here is added later, so have to do it manually # extra1 here is added later, so have to do it manually
quota.QUOTAS.register_resource_by_name('extra1') quota.QUOTAS.register_resource_by_name('extra1')
@ -71,34 +66,6 @@ class QuotaExtensionTestCase(unittest.TestCase):
ext_middleware = extensions.ExtensionMiddleware(app, ext_mgr=ext_mgr) ext_middleware = extensions.ExtensionMiddleware(app, ext_mgr=ext_mgr)
self.api = webtest.TestApp(ext_middleware) 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): def tearDown(self):
self._plugin_patcher.stop() self._plugin_patcher.stop()
self.api = None self.api = None
@ -114,7 +81,7 @@ class QuotaExtensionTestCase(unittest.TestCase):
res = self.api.get(_get_path('quotas')) res = self.api.get(_get_path('quotas'))
self.assertEqual(200, res.status_int) self.assertEqual(200, res.status_int)
def test_quotas_defaul_values(self): def test_quotas_default_values(self):
tenant_id = 'tenant_id1' tenant_id = 'tenant_id1'
env = {'quantum.context': context.Context('', tenant_id)} env = {'quantum.context': context.Context('', tenant_id)}
res = self.api.get(_get_path('quotas', id=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) self.assertEqual(403, res.status_int)
def test_quotas_loaded_bad(self): def test_quotas_loaded_bad(self):
self.testflag = 2
try: try:
res = self.api.get(_get_path('quotas'), expect_errors=True) res = self.api.get(_get_path('quotas'), expect_errors=True)
self.assertEqual(404, res.status_int) self.assertEqual(404, res.status_int)
except Exception: except Exception:
pass pass
self.testflag = 1