Implements data-driven views and extended attributes.
The quantum/api/v2/views.py module is replaced by is_visible properties in the RESOURCE_ATTRIBUTE_MAP defined in quantum/api/v2/attributes.py. Extensions are given the ability to add extended attribute descriptions to this map during initialization, allowing extended attributes to be implemented similarly to core attributes in plugins. Resolves bug 1023111. Change-Id: Ic6e224d5d841b6a1d4d1c762d7306adaf91f7a2d
This commit is contained in:
parent
b7f852d4c5
commit
4ff0723479
@ -141,56 +141,87 @@ validators = {'type:boolean': _validate_boolean,
|
||||
RESOURCE_ATTRIBUTE_MAP = {
|
||||
'networks': {
|
||||
'id': {'allow_post': False, 'allow_put': False,
|
||||
'validate': {'type:regex': UUID_PATTERN}},
|
||||
'name': {'allow_post': True, 'allow_put': True},
|
||||
'subnets': {'allow_post': True, 'allow_put': True, 'default': []},
|
||||
'validate': {'type:regex': UUID_PATTERN},
|
||||
'is_visible': True},
|
||||
'name': {'allow_post': True, 'allow_put': True,
|
||||
'is_visible': True},
|
||||
'subnets': {'allow_post': True, 'allow_put': True,
|
||||
'default': [],
|
||||
'is_visible': True},
|
||||
'admin_state_up': {'allow_post': True, 'allow_put': True,
|
||||
'default': True, 'convert_to': convert_to_boolean,
|
||||
'validate': {'type:boolean': None}},
|
||||
'status': {'allow_post': False, 'allow_put': False},
|
||||
'default': True,
|
||||
'convert_to': convert_to_boolean,
|
||||
'validate': {'type:boolean': None},
|
||||
'is_visible': True},
|
||||
'status': {'allow_post': False, 'allow_put': False,
|
||||
'is_visible': True},
|
||||
'tenant_id': {'allow_post': True, 'allow_put': False,
|
||||
'required_by_policy': True},
|
||||
'required_by_policy': True,
|
||||
'is_visible': True},
|
||||
'mac_ranges': {'allow_post': False, 'allow_put': False,
|
||||
'is_visible': True},
|
||||
},
|
||||
'ports': {
|
||||
'id': {'allow_post': False, 'allow_put': False,
|
||||
'validate': {'type:regex': UUID_PATTERN}},
|
||||
'validate': {'type:regex': UUID_PATTERN},
|
||||
'is_visible': True},
|
||||
'network_id': {'allow_post': True, 'allow_put': False,
|
||||
'validate': {'type:regex': UUID_PATTERN}},
|
||||
'validate': {'type:regex': UUID_PATTERN},
|
||||
'is_visible': True},
|
||||
'admin_state_up': {'allow_post': True, 'allow_put': True,
|
||||
'default': True, 'convert_to': convert_to_boolean,
|
||||
'validate': {'type:boolean': None}},
|
||||
'default': True,
|
||||
'convert_to': convert_to_boolean,
|
||||
'validate': {'type:boolean': None},
|
||||
'is_visible': True},
|
||||
'mac_address': {'allow_post': True, 'allow_put': False,
|
||||
'default': ATTR_NOT_SPECIFIED,
|
||||
'validate': {'type:mac_address': None}},
|
||||
'validate': {'type:mac_address': None},
|
||||
'is_visible': True},
|
||||
'fixed_ips': {'allow_post': True, 'allow_put': True,
|
||||
'default': ATTR_NOT_SPECIFIED},
|
||||
'default': ATTR_NOT_SPECIFIED,
|
||||
'is_visible': True},
|
||||
'host_routes': {'allow_post': True, 'allow_put': True,
|
||||
'default': ATTR_NOT_SPECIFIED},
|
||||
'device_id': {'allow_post': True, 'allow_put': True, 'default': ''},
|
||||
'default': ATTR_NOT_SPECIFIED,
|
||||
'is_visible': False},
|
||||
'device_id': {'allow_post': True, 'allow_put': True,
|
||||
'default': '',
|
||||
'is_visible': True},
|
||||
'tenant_id': {'allow_post': True, 'allow_put': False,
|
||||
'required_by_policy': True},
|
||||
'required_by_policy': True,
|
||||
'is_visible': True},
|
||||
'status': {'allow_post': False, 'allow_put': False,
|
||||
'is_visible': True},
|
||||
},
|
||||
'subnets': {
|
||||
'id': {'allow_post': False, 'allow_put': False,
|
||||
'validate': {'type:regex': UUID_PATTERN}},
|
||||
'validate': {'type:regex': UUID_PATTERN},
|
||||
'is_visible': True},
|
||||
'ip_version': {'allow_post': True, 'allow_put': False,
|
||||
'convert_to': int,
|
||||
'validate': {'type:values': [4, 6]}},
|
||||
'validate': {'type:values': [4, 6]},
|
||||
'is_visible': True},
|
||||
'network_id': {'allow_post': True, 'allow_put': False,
|
||||
'validate': {'type:regex': UUID_PATTERN}},
|
||||
'validate': {'type:regex': UUID_PATTERN},
|
||||
'is_visible': True},
|
||||
'cidr': {'allow_post': True, 'allow_put': False,
|
||||
'validate': {'type:subnet': None}},
|
||||
'validate': {'type:subnet': None},
|
||||
'is_visible': True},
|
||||
'gateway_ip': {'allow_post': True, 'allow_put': True,
|
||||
'default': ATTR_NOT_SPECIFIED,
|
||||
'validate': {'type:ip_address': None}},
|
||||
'validate': {'type:ip_address': None},
|
||||
'is_visible': True},
|
||||
#TODO(salvatore-orlando): Enable PUT on allocation_pools
|
||||
'allocation_pools': {'allow_post': True, 'allow_put': False,
|
||||
'default': ATTR_NOT_SPECIFIED},
|
||||
'default': ATTR_NOT_SPECIFIED,
|
||||
'is_visible': True},
|
||||
'dns_namesevers': {'allow_post': True, 'allow_put': True,
|
||||
'default': ATTR_NOT_SPECIFIED},
|
||||
'default': ATTR_NOT_SPECIFIED,
|
||||
'is_visible': False},
|
||||
'additional_host_routes': {'allow_post': True, 'allow_put': True,
|
||||
'default': ATTR_NOT_SPECIFIED},
|
||||
'default': ATTR_NOT_SPECIFIED,
|
||||
'is_visible': False},
|
||||
'tenant_id': {'allow_post': True, 'allow_put': False,
|
||||
'required_by_policy': True},
|
||||
'required_by_policy': True,
|
||||
'is_visible': True},
|
||||
}
|
||||
}
|
||||
|
@ -18,7 +18,6 @@ import webob.exc
|
||||
|
||||
from quantum.api.v2 import attributes
|
||||
from quantum.api.v2 import resource as wsgi_resource
|
||||
from quantum.api.v2 import views
|
||||
from quantum.common import exceptions
|
||||
from quantum.common import utils
|
||||
from quantum import policy
|
||||
@ -112,7 +111,18 @@ class Controller(object):
|
||||
self._policy_attrs = [name for (name, info) in self._attr_info.items()
|
||||
if 'required_by_policy' in info
|
||||
and info['required_by_policy']]
|
||||
self._view = getattr(views, self._resource)
|
||||
|
||||
def _is_visible(self, attr):
|
||||
attr_val = self._attr_info.get(attr)
|
||||
return attr_val and attr_val['is_visible']
|
||||
|
||||
def _view(self, data, fields_to_strip=None):
|
||||
# make sure fields_to_strip is iterable
|
||||
if not fields_to_strip:
|
||||
fields_to_strip = []
|
||||
return dict(item for item in data.iteritems()
|
||||
if self._is_visible(item[0])
|
||||
and not item[0] in fields_to_strip)
|
||||
|
||||
def _do_field_list(self, original_fields):
|
||||
fields_to_add = None
|
||||
|
@ -23,6 +23,7 @@ import webob.exc
|
||||
|
||||
from quantum.api.v2 import attributes
|
||||
from quantum.api.v2 import base
|
||||
from quantum.extensions import extensions
|
||||
from quantum import manager
|
||||
from quantum.openstack.common import cfg
|
||||
from quantum import wsgi
|
||||
@ -69,6 +70,9 @@ class APIRouter(wsgi.Router):
|
||||
mapper = routes_mapper.Mapper()
|
||||
plugin = manager.QuantumManager.get_plugin()
|
||||
|
||||
ext_mgr = extensions.PluginAwareExtensionManager.get_instance()
|
||||
ext_mgr.extend_resources("2.0", attributes.RESOURCE_ATTRIBUTE_MAP)
|
||||
|
||||
col_kwargs = dict(collection_actions=COLLECTION_ACTIONS,
|
||||
member_actions=MEMBER_ACTIONS)
|
||||
|
||||
|
@ -1,44 +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.
|
||||
|
||||
|
||||
def resource(data, keys, fields_to_strip=None):
|
||||
"""Formats the specified entity"""
|
||||
# make sure fields_to_strip is iterable
|
||||
if not fields_to_strip:
|
||||
fields_to_strip = []
|
||||
return dict(item for item in data.iteritems()
|
||||
if item[0] in keys and not item[0] in fields_to_strip)
|
||||
|
||||
|
||||
def port(port_data, fields_to_strip=None):
|
||||
"""Represents a view for a port object"""
|
||||
keys = ('id', 'network_id', 'mac_address', 'fixed_ips',
|
||||
'device_id', 'admin_state_up', 'tenant_id', 'status')
|
||||
return resource(port_data, keys, fields_to_strip)
|
||||
|
||||
|
||||
def network(network_data, fields_to_strip=None):
|
||||
"""Represents a view for a network object"""
|
||||
keys = ('id', 'name', 'subnets', 'admin_state_up', 'status',
|
||||
'tenant_id', 'mac_ranges')
|
||||
return resource(network_data, keys, fields_to_strip)
|
||||
|
||||
|
||||
def subnet(subnet_data, fields_to_strip=None):
|
||||
"""Represents a view for a subnet object"""
|
||||
keys = ('id', 'network_id', 'tenant_id', 'gateway_ip', 'ip_version',
|
||||
'cidr', 'allocation_pools')
|
||||
return resource(subnet_data, keys, fields_to_strip)
|
@ -132,6 +132,20 @@ class ExtensionDescriptor(object):
|
||||
request_exts = []
|
||||
return request_exts
|
||||
|
||||
def get_extended_attributes(self, version):
|
||||
"""Map describing extended attributes for core resources.
|
||||
|
||||
Extended attributes are implemented by a core plugin similarly
|
||||
to the attributes defined in the core, and can appear in
|
||||
request and response messages. Their names are scoped with the
|
||||
extension's prefix. The core API version is passed to this
|
||||
function, which must return a
|
||||
map[<resource_name>][<attribute_name>][<attribute_property>]
|
||||
specifying the extended resource attribute properties required
|
||||
by that API version.
|
||||
"""
|
||||
return {}
|
||||
|
||||
def get_plugin_interface(self):
|
||||
"""
|
||||
Returns an abstract class which defines contract for the plugin.
|
||||
@ -340,9 +354,7 @@ class ExtensionMiddleware(wsgi.Middleware):
|
||||
def plugin_aware_extension_middleware_factory(global_config, **local_config):
|
||||
"""Paste factory."""
|
||||
def _factory(app):
|
||||
extensions_path = get_extensions_path()
|
||||
ext_mgr = PluginAwareExtensionManager(extensions_path,
|
||||
QuantumManager.get_plugin())
|
||||
ext_mgr = PluginAwareExtensionManager.get_instance()
|
||||
return ExtensionMiddleware(app, ext_mgr=ext_mgr)
|
||||
return _factory
|
||||
|
||||
@ -398,6 +410,18 @@ class ExtensionManager(object):
|
||||
pass
|
||||
return request_exts
|
||||
|
||||
def extend_resources(self, version, attr_map):
|
||||
"""Extend resources with additional attributes."""
|
||||
for ext in self.extensions.itervalues():
|
||||
try:
|
||||
extended_attrs = ext.get_extended_attributes(version)
|
||||
for resource, resource_attrs in extended_attrs.iteritems():
|
||||
attr_map[resource].update(resource_attrs)
|
||||
except AttributeError:
|
||||
# Extensions aren't required to have extended
|
||||
# attributes
|
||||
pass
|
||||
|
||||
def _check_extension(self, extension):
|
||||
"""Checks for required methods in extension objects."""
|
||||
try:
|
||||
@ -467,6 +491,8 @@ class ExtensionManager(object):
|
||||
|
||||
class PluginAwareExtensionManager(ExtensionManager):
|
||||
|
||||
_instance = None
|
||||
|
||||
def __init__(self, path, plugin):
|
||||
self.plugin = plugin
|
||||
super(PluginAwareExtensionManager, self).__init__(path)
|
||||
@ -502,6 +528,13 @@ class PluginAwareExtensionManager(ExtensionManager):
|
||||
extension.get_alias()))
|
||||
return plugin_has_interface
|
||||
|
||||
@classmethod
|
||||
def get_instance(cls):
|
||||
if cls._instance is None:
|
||||
cls._instance = cls(get_extensions_path(),
|
||||
QuantumManager.get_plugin())
|
||||
return cls._instance
|
||||
|
||||
|
||||
class RequestExtension(object):
|
||||
"""Extend requests and responses of core Quantum OpenStack API controllers.
|
||||
|
48
quantum/tests/unit/extensions/v2attributes.py
Normal file
48
quantum/tests/unit/extensions/v2attributes.py
Normal file
@ -0,0 +1,48 @@
|
||||
# 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.
|
||||
|
||||
EXTENDED_ATTRIBUTES_2_0 = {
|
||||
'networks': {
|
||||
'v2attrs:something': {'allow_post': False,
|
||||
'allow_put': False,
|
||||
'is_visible': True},
|
||||
'v2attrs:something_else': {'allow_post': True,
|
||||
'allow_put': False,
|
||||
'is_visible': False},
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class V2attributes(object):
|
||||
def get_name(self):
|
||||
return "V2 Extended Attributes Example"
|
||||
|
||||
def get_alias(self):
|
||||
return "v2attrs"
|
||||
|
||||
def get_description(self):
|
||||
return "Demonstrates extended attributes on V2 core resources"
|
||||
|
||||
def get_namespace(self):
|
||||
return "http://docs.openstack.org/ext/examples/v2attributes/api/v1.0"
|
||||
|
||||
def get_updated(self):
|
||||
return "2012-07-18T10:00:00-00:00"
|
||||
|
||||
def get_extended_attributes(self, version):
|
||||
if version == "2.0":
|
||||
return EXTENDED_ATTRIBUTES_2_0
|
||||
else:
|
||||
return {}
|
@ -23,12 +23,13 @@ import webtest
|
||||
from webob import exc
|
||||
|
||||
from quantum.api.v2 import attributes
|
||||
from quantum.api.v2 import base
|
||||
from quantum.api.v2 import resource as wsgi_resource
|
||||
from quantum.api.v2 import router
|
||||
from quantum.api.v2 import views
|
||||
from quantum.common import config
|
||||
from quantum.common import exceptions as q_exc
|
||||
from quantum import context
|
||||
from quantum.extensions.extensions import PluginAwareExtensionManager
|
||||
from quantum.manager import QuantumManager
|
||||
from quantum.openstack.common import cfg
|
||||
|
||||
@ -41,6 +42,7 @@ def _uuid():
|
||||
|
||||
ROOTDIR = os.path.dirname(os.path.dirname(__file__))
|
||||
ETCDIR = os.path.join(ROOTDIR, 'etc')
|
||||
EXTDIR = os.path.join(ROOTDIR, 'unit/extensions')
|
||||
|
||||
|
||||
def etcdir(*p):
|
||||
@ -133,6 +135,8 @@ class APIv2TestBase(unittest.TestCase):
|
||||
plugin = 'quantum.quantum_plugin_base_v2.QuantumPluginBaseV2'
|
||||
# Ensure 'stale' patched copies of the plugin are never returned
|
||||
QuantumManager._instance = None
|
||||
# Ensure existing ExtensionManager is not used
|
||||
PluginAwareExtensionManager._instance = None
|
||||
# Create the default configurations
|
||||
args = ['--config-file', etcdir('quantum.conf.test')]
|
||||
config.parse(args=args)
|
||||
@ -602,6 +606,26 @@ class JSONV2TestCase(APIv2TestBase):
|
||||
self.assertEqual(port['network_id'], net_id)
|
||||
self.assertEqual(port['mac_address'], 'ca:fe:de:ad:be:ef')
|
||||
|
||||
def test_create_return_extra_attr(self):
|
||||
net_id = _uuid()
|
||||
data = {'network': {'name': 'net1', 'admin_state_up': True,
|
||||
'tenant_id': _uuid()}}
|
||||
return_value = {'subnets': [], 'status': "ACTIVE",
|
||||
'id': net_id, 'v2attrs:something': "123"}
|
||||
return_value.update(data['network'].copy())
|
||||
|
||||
instance = self.plugin.return_value
|
||||
instance.create_network.return_value = return_value
|
||||
|
||||
res = self.api.post_json(_get_path('networks'), data)
|
||||
|
||||
self.assertEqual(res.status_int, exc.HTTPCreated.code)
|
||||
self.assertTrue('network' in res.json)
|
||||
net = res.json['network']
|
||||
self.assertEqual(net['id'], net_id)
|
||||
self.assertEqual(net['status'], "ACTIVE")
|
||||
self.assertFalse('v2attrs:something' in net)
|
||||
|
||||
def test_fields(self):
|
||||
return_value = {'name': 'net1', 'admin_state_up': True,
|
||||
'subnets': []}
|
||||
@ -704,33 +728,30 @@ class JSONV2TestCase(APIv2TestBase):
|
||||
|
||||
|
||||
class V2Views(unittest.TestCase):
|
||||
def _view(self, keys, func):
|
||||
def _view(self, keys, collection, resource):
|
||||
data = dict((key, 'value') for key in keys)
|
||||
data['fake'] = 'value'
|
||||
res = func(data)
|
||||
attr_info = attributes.RESOURCE_ATTRIBUTE_MAP[collection]
|
||||
controller = base.Controller(None, collection, resource, attr_info)
|
||||
res = controller._view(data)
|
||||
self.assertTrue('fake' not in res)
|
||||
for key in keys:
|
||||
self.assertTrue(key in res)
|
||||
|
||||
def test_resource(self):
|
||||
res = views.resource({'one': 1, 'two': 2}, ['one'])
|
||||
self.assertTrue('one' in res)
|
||||
self.assertTrue('two' not in res)
|
||||
|
||||
def test_network(self):
|
||||
keys = ('id', 'name', 'subnets', 'admin_state_up', 'status',
|
||||
'tenant_id', 'mac_ranges')
|
||||
self._view(keys, views.network)
|
||||
self._view(keys, 'networks', 'network')
|
||||
|
||||
def test_port(self):
|
||||
keys = ('id', 'network_id', 'mac_address', 'fixed_ips',
|
||||
'device_id', 'admin_state_up', 'tenant_id', 'status')
|
||||
self._view(keys, views.port)
|
||||
self._view(keys, 'ports', 'port')
|
||||
|
||||
def test_subnet(self):
|
||||
keys = ('id', 'network_id', 'tenant_id', 'gateway_ip',
|
||||
'ip_version', 'cidr')
|
||||
self._view(keys, views.subnet)
|
||||
self._view(keys, 'subnets', 'subnet')
|
||||
|
||||
|
||||
class QuotaTest(APIv2TestBase):
|
||||
@ -770,3 +791,74 @@ class QuotaTest(APIv2TestBase):
|
||||
res = self.api.post_json(
|
||||
_get_path('networks'), initial_input)
|
||||
self.assertEqual(res.status_int, exc.HTTPCreated.code)
|
||||
|
||||
|
||||
class ExtensionTestCase(unittest.TestCase):
|
||||
# NOTE(jkoelker) This potentially leaks the mock object if the setUp
|
||||
# raises without being caught. Using unittest2
|
||||
# or dropping 2.6 support so we can use addCleanup
|
||||
# will get around this.
|
||||
def setUp(self):
|
||||
plugin = 'quantum.quantum_plugin_base_v2.QuantumPluginBaseV2'
|
||||
|
||||
# Ensure 'stale' patched copies of the plugin are never returned
|
||||
QuantumManager._instance = None
|
||||
|
||||
# Ensure existing ExtensionManager is not used
|
||||
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', etcdir('quantum.conf.test')]
|
||||
config.parse(args=args)
|
||||
|
||||
# Update the plugin and extensions path
|
||||
cfg.CONF.set_override('core_plugin', plugin)
|
||||
cfg.CONF.set_override('api_extensions_path', EXTDIR)
|
||||
|
||||
self._plugin_patcher = mock.patch(plugin, autospec=True)
|
||||
self.plugin = self._plugin_patcher.start()
|
||||
|
||||
# Instantiate mock plugin and enable the V2attributes extension
|
||||
QuantumManager.get_plugin().supported_extension_aliases = ["v2attrs"]
|
||||
|
||||
api = router.APIRouter()
|
||||
self.api = webtest.TestApp(api)
|
||||
|
||||
def tearDown(self):
|
||||
self._plugin_patcher.stop()
|
||||
self.api = None
|
||||
self.plugin = None
|
||||
cfg.CONF.reset()
|
||||
|
||||
# Restore the global RESOURCE_ATTRIBUTE_MAP
|
||||
attributes.RESOURCE_ATTRIBUTE_MAP = self.saved_attr_map
|
||||
|
||||
def test_extended_create(self):
|
||||
net_id = _uuid()
|
||||
data = {'network': {'name': 'net1', 'admin_state_up': True,
|
||||
'tenant_id': _uuid(), 'subnets': [],
|
||||
'v2attrs:something_else': "abc"}}
|
||||
return_value = {'subnets': [], 'status': "ACTIVE",
|
||||
'id': net_id,
|
||||
'v2attrs:something': "123"}
|
||||
return_value.update(data['network'].copy())
|
||||
|
||||
instance = self.plugin.return_value
|
||||
instance.create_network.return_value = return_value
|
||||
|
||||
res = self.api.post_json(_get_path('networks'), data)
|
||||
|
||||
instance.create_network.assert_called_with(mock.ANY,
|
||||
network=data)
|
||||
self.assertEqual(res.status_int, exc.HTTPCreated.code)
|
||||
self.assertTrue('network' in res.json)
|
||||
net = res.json['network']
|
||||
self.assertEqual(net['id'], net_id)
|
||||
self.assertEqual(net['status'], "ACTIVE")
|
||||
self.assertEqual(net['v2attrs:something'], "123")
|
||||
self.assertFalse('v2attrs:something_else' in net)
|
||||
|
Loading…
Reference in New Issue
Block a user