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},
}
}
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
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
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
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)
+++ /dev/null
-# 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)
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.
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
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:
class PluginAwareExtensionManager(ExtensionManager):
+ _instance = None
+
def __init__(self, path, plugin):
self.plugin = plugin
super(PluginAwareExtensionManager, self).__init__(path)
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.
--- /dev/null
+# 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 {}
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
ROOTDIR = os.path.dirname(os.path.dirname(__file__))
ETCDIR = os.path.join(ROOTDIR, 'etc')
+EXTDIR = os.path.join(ROOTDIR, 'unit/extensions')
def etcdir(*p):
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)
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': []}
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):
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)