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')
# 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

View File

@ -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']
tenant_default = dict((key, resource.default)
for key, resource in resources.items())
# 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()
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):
"""

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.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)}

View File

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

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 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"

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

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 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"

View File

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

View File

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

View File

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