add parent/sub-resource support into Quantum API framework
- quantum.api.v2.base.Controller class now able to handle sub-resources - quantum.api.v2.router.APIRouter now able to specify sub-resources Fixes bug 1085968 Change-Id: I07f2c1f3d974f7f17d4947804bde064dd8004a84
This commit is contained in:
parent
7930531eb9
commit
9240097e11
@ -101,9 +101,14 @@ def _filters(request, attr_info):
|
||||
|
||||
|
||||
class Controller(object):
|
||||
LIST = 'list'
|
||||
SHOW = 'show'
|
||||
CREATE = 'create'
|
||||
UPDATE = 'update'
|
||||
DELETE = 'delete'
|
||||
|
||||
def __init__(self, plugin, collection, resource, attr_info,
|
||||
allow_bulk=False, member_actions=None):
|
||||
allow_bulk=False, member_actions=None, parent=None):
|
||||
if member_actions is None:
|
||||
member_actions = []
|
||||
self._plugin = plugin
|
||||
@ -117,6 +122,20 @@ class Controller(object):
|
||||
self._publisher_id = notifier_api.publisher_id('network')
|
||||
self._member_actions = member_actions
|
||||
|
||||
if parent:
|
||||
self._parent_id_name = '%s_id' % parent['member_name']
|
||||
parent_part = '_%s' % parent['member_name']
|
||||
else:
|
||||
self._parent_id_name = None
|
||||
parent_part = ''
|
||||
self._plugin_handlers = {
|
||||
self.LIST: 'get%s_%s' % (parent_part, self._collection),
|
||||
self.SHOW: 'get%s_%s' % (parent_part, self._resource)
|
||||
}
|
||||
for action in [self.CREATE, self.UPDATE, self.DELETE]:
|
||||
self._plugin_handlers[action] = '%s%s_%s' % (action, parent_part,
|
||||
self._resource)
|
||||
|
||||
def _is_native_bulk_supported(self):
|
||||
native_bulk_attr_name = ("_%s__native_bulk_support"
|
||||
% self._plugin.__class__.__name__)
|
||||
@ -152,7 +171,7 @@ class Controller(object):
|
||||
else:
|
||||
raise AttributeError
|
||||
|
||||
def _items(self, request, do_authz=False):
|
||||
def _items(self, request, do_authz=False, parent_id=None):
|
||||
"""Retrieves and formats a list of elements of the requested entity"""
|
||||
# NOTE(salvatore-orlando): The following ensures that fields which
|
||||
# are needed for authZ policy validation are not stripped away by the
|
||||
@ -160,7 +179,9 @@ class Controller(object):
|
||||
original_fields, fields_to_add = self._do_field_list(_fields(request))
|
||||
kwargs = {'filters': _filters(request, self._attr_info),
|
||||
'fields': original_fields}
|
||||
obj_getter = getattr(self._plugin, "get_%s" % self._collection)
|
||||
if parent_id:
|
||||
kwargs[self._parent_id_name] = parent_id
|
||||
obj_getter = getattr(self._plugin, self._plugin_handlers[self.LIST])
|
||||
obj_list = obj_getter(request.context, **kwargs)
|
||||
# Check authz
|
||||
if do_authz:
|
||||
@ -169,17 +190,20 @@ class Controller(object):
|
||||
# Omit items from list that should not be visible
|
||||
obj_list = [obj for obj in obj_list
|
||||
if policy.check(request.context,
|
||||
"get_%s" % self._resource,
|
||||
self._plugin_handlers[self.SHOW],
|
||||
obj,
|
||||
plugin=self._plugin)]
|
||||
return {self._collection: [self._view(obj,
|
||||
fields_to_strip=fields_to_add)
|
||||
for obj in obj_list]}
|
||||
|
||||
def _item(self, request, id, do_authz=False, field_list=None):
|
||||
def _item(self, request, id, do_authz=False, field_list=None,
|
||||
parent_id=None):
|
||||
"""Retrieves and formats a single element of the requested entity"""
|
||||
kwargs = {'fields': field_list}
|
||||
action = "get_%s" % self._resource
|
||||
action = self._plugin_handlers[self.SHOW]
|
||||
if parent_id:
|
||||
kwargs[self._parent_id_name] = parent_id
|
||||
obj_getter = getattr(self._plugin, action)
|
||||
obj = obj_getter(request.context, id, **kwargs)
|
||||
# Check authz
|
||||
@ -189,33 +213,38 @@ class Controller(object):
|
||||
policy.enforce(request.context, action, obj, plugin=self._plugin)
|
||||
return obj
|
||||
|
||||
def index(self, request):
|
||||
def index(self, request, **kwargs):
|
||||
"""Returns a list of the requested entity"""
|
||||
return self._items(request, True)
|
||||
parent_id = kwargs.get(self._parent_id_name)
|
||||
return self._items(request, True, parent_id)
|
||||
|
||||
def show(self, request, id):
|
||||
def show(self, request, id, **kwargs):
|
||||
"""Returns detailed information about the requested entity"""
|
||||
try:
|
||||
# NOTE(salvatore-orlando): The following ensures that fields
|
||||
# which are needed for authZ policy validation are not stripped
|
||||
# away by the plugin before returning.
|
||||
field_list, added_fields = self._do_field_list(_fields(request))
|
||||
parent_id = kwargs.get(self._parent_id_name)
|
||||
return {self._resource:
|
||||
self._view(self._item(request,
|
||||
id,
|
||||
do_authz=True,
|
||||
field_list=field_list),
|
||||
field_list=field_list,
|
||||
parent_id=parent_id),
|
||||
fields_to_strip=added_fields)}
|
||||
except exceptions.PolicyNotAuthorized:
|
||||
# To avoid giving away information, pretend that it
|
||||
# doesn't exist
|
||||
raise webob.exc.HTTPNotFound()
|
||||
|
||||
def _emulate_bulk_create(self, obj_creator, request, body):
|
||||
def _emulate_bulk_create(self, obj_creator, request, body, parent_id=None):
|
||||
objs = []
|
||||
try:
|
||||
for item in body[self._collection]:
|
||||
kwargs = {self._resource: item}
|
||||
if parent_id:
|
||||
kwargs[self._parent_id_name] = parent_id
|
||||
objs.append(self._view(obj_creator(request.context,
|
||||
**kwargs)))
|
||||
return objs
|
||||
@ -223,10 +252,12 @@ class Controller(object):
|
||||
# could raise any kind of exception
|
||||
except Exception as ex:
|
||||
for obj in objs:
|
||||
delete_action = "delete_%s" % self._resource
|
||||
obj_deleter = getattr(self._plugin, delete_action)
|
||||
obj_deleter = getattr(self._plugin,
|
||||
self._plugin_handlers[self.DELETE])
|
||||
try:
|
||||
obj_deleter(request.context, obj['id'])
|
||||
kwargs = ({self._parent_id_name: parent_id} if parent_id
|
||||
else {})
|
||||
obj_deleter(request.context, obj['id'], **kwargs)
|
||||
except Exception:
|
||||
# broad catch as our only purpose is to log the exception
|
||||
LOG.exception(_("Unable to undo add for "
|
||||
@ -239,8 +270,9 @@ class Controller(object):
|
||||
# it is then deleted
|
||||
raise
|
||||
|
||||
def create(self, request, body=None):
|
||||
def create(self, request, body=None, **kwargs):
|
||||
"""Creates a new instance of the requested entity"""
|
||||
parent_id = kwargs.get(self._parent_id_name)
|
||||
notifier_api.notify(request.context,
|
||||
self._publisher_id,
|
||||
self._resource + '.create.start',
|
||||
@ -249,7 +281,7 @@ class Controller(object):
|
||||
body = Controller.prepare_request_body(request.context, body, True,
|
||||
self._resource, self._attr_info,
|
||||
allow_bulk=self._allow_bulk)
|
||||
action = "create_%s" % self._resource
|
||||
action = self._plugin_handlers[self.CREATE]
|
||||
# Check authz
|
||||
try:
|
||||
if self._collection in body:
|
||||
@ -312,34 +344,37 @@ class Controller(object):
|
||||
create_result)
|
||||
return create_result
|
||||
|
||||
kwargs = {self._parent_id_name: parent_id} if parent_id else {}
|
||||
if self._collection in body and self._native_bulk:
|
||||
# plugin does atomic bulk create operations
|
||||
obj_creator = getattr(self._plugin, "%s_bulk" % action)
|
||||
objs = obj_creator(request.context, body)
|
||||
objs = obj_creator(request.context, body, **kwargs)
|
||||
return notify({self._collection: [self._view(obj)
|
||||
for obj in objs]})
|
||||
else:
|
||||
obj_creator = getattr(self._plugin, action)
|
||||
if self._collection in body:
|
||||
# Emulate atomic bulk behavior
|
||||
objs = self._emulate_bulk_create(obj_creator, request, body)
|
||||
objs = self._emulate_bulk_create(obj_creator, request,
|
||||
body, parent_id)
|
||||
return notify({self._collection: objs})
|
||||
else:
|
||||
kwargs = {self._resource: body}
|
||||
kwargs.update({self._resource: body})
|
||||
obj = obj_creator(request.context, **kwargs)
|
||||
return notify({self._resource: self._view(obj)})
|
||||
|
||||
def delete(self, request, id):
|
||||
def delete(self, request, id, **kwargs):
|
||||
"""Deletes the specified entity"""
|
||||
notifier_api.notify(request.context,
|
||||
self._publisher_id,
|
||||
self._resource + '.delete.start',
|
||||
notifier_api.INFO,
|
||||
{self._resource + '_id': id})
|
||||
action = "delete_%s" % self._resource
|
||||
action = self._plugin_handlers[self.DELETE]
|
||||
|
||||
# Check authz
|
||||
obj = self._item(request, id)
|
||||
parent_id = kwargs.get(self._parent_id_name)
|
||||
obj = self._item(request, id, parent_id=parent_id)
|
||||
try:
|
||||
policy.enforce(request.context,
|
||||
action,
|
||||
@ -351,15 +386,16 @@ class Controller(object):
|
||||
raise webob.exc.HTTPNotFound()
|
||||
|
||||
obj_deleter = getattr(self._plugin, action)
|
||||
obj_deleter(request.context, id)
|
||||
obj_deleter(request.context, id, **kwargs)
|
||||
notifier_api.notify(request.context,
|
||||
self._publisher_id,
|
||||
self._resource + '.delete.end',
|
||||
notifier_api.INFO,
|
||||
{self._resource + '_id': id})
|
||||
|
||||
def update(self, request, id, body=None):
|
||||
def update(self, request, id, body=None, **kwargs):
|
||||
"""Updates the specified entity's attributes"""
|
||||
parent_id = kwargs.get(self._parent_id_name)
|
||||
payload = body.copy()
|
||||
payload['id'] = id
|
||||
notifier_api.notify(request.context,
|
||||
@ -370,7 +406,7 @@ class Controller(object):
|
||||
body = Controller.prepare_request_body(request.context, body, False,
|
||||
self._resource, self._attr_info,
|
||||
allow_bulk=self._allow_bulk)
|
||||
action = "update_%s" % self._resource
|
||||
action = self._plugin_handlers[self.UPDATE]
|
||||
# Load object to check authz
|
||||
# but pass only attributes in the original body and required
|
||||
# by the policy engine to the policy 'brain'
|
||||
@ -378,7 +414,8 @@ class Controller(object):
|
||||
if ('required_by_policy' in value and
|
||||
value['required_by_policy'] or
|
||||
not 'default' in value)]
|
||||
orig_obj = self._item(request, id, field_list=field_list)
|
||||
orig_obj = self._item(request, id, field_list=field_list,
|
||||
parent_id=parent_id)
|
||||
orig_obj.update(body[self._resource])
|
||||
try:
|
||||
policy.enforce(request.context,
|
||||
@ -392,6 +429,8 @@ class Controller(object):
|
||||
|
||||
obj_updater = getattr(self._plugin, action)
|
||||
kwargs = {self._resource: body}
|
||||
if parent_id:
|
||||
kwargs[self._parent_id_name] = parent_id
|
||||
obj = obj_updater(request.context, id, **kwargs)
|
||||
result = {self._resource: self._view(obj)}
|
||||
notifier_api.notify(request.context,
|
||||
@ -526,9 +565,9 @@ class Controller(object):
|
||||
|
||||
|
||||
def create_resource(collection, resource, plugin, params, allow_bulk=False,
|
||||
member_actions=None):
|
||||
member_actions=None, parent=None):
|
||||
controller = Controller(plugin, collection, resource, params, allow_bulk,
|
||||
member_actions=member_actions)
|
||||
member_actions=member_actions, parent=parent)
|
||||
|
||||
# NOTE(jkoelker) To anyone wishing to add "proper" xml support
|
||||
# this is where you do it
|
||||
|
@ -30,6 +30,11 @@ from quantum import wsgi
|
||||
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
RESOURCES = {'network': 'networks',
|
||||
'subnet': 'subnets',
|
||||
'port': 'ports'}
|
||||
SUB_RESOURCES = {}
|
||||
COLLECTION_ACTIONS = ['index', 'create']
|
||||
MEMBER_ACTIONS = ['show', 'update', 'delete']
|
||||
REQUIREMENTS = {'id': attributes.UUID_PATTERN, 'format': 'xml|json'}
|
||||
@ -75,25 +80,35 @@ class APIRouter(wsgi.Router):
|
||||
col_kwargs = dict(collection_actions=COLLECTION_ACTIONS,
|
||||
member_actions=MEMBER_ACTIONS)
|
||||
|
||||
resources = {'network': 'networks',
|
||||
'subnet': 'subnets',
|
||||
'port': 'ports'}
|
||||
|
||||
def _map_resource(collection, resource, params):
|
||||
def _map_resource(collection, resource, params, parent=None):
|
||||
allow_bulk = cfg.CONF.allow_bulk
|
||||
controller = base.create_resource(collection, resource,
|
||||
plugin, params,
|
||||
allow_bulk=allow_bulk)
|
||||
allow_bulk=allow_bulk,
|
||||
parent=parent)
|
||||
path_prefix = None
|
||||
if parent:
|
||||
path_prefix = "/%s/{%s_id}/%s" % (parent['collection_name'],
|
||||
parent['member_name'],
|
||||
collection)
|
||||
mapper_kwargs = dict(controller=controller,
|
||||
requirements=REQUIREMENTS,
|
||||
path_prefix=path_prefix,
|
||||
**col_kwargs)
|
||||
return mapper.collection(collection, resource,
|
||||
**mapper_kwargs)
|
||||
|
||||
mapper.connect('index', '/', controller=Index(resources))
|
||||
for resource in resources:
|
||||
_map_resource(resources[resource], resource,
|
||||
mapper.connect('index', '/', controller=Index(RESOURCES))
|
||||
for resource in RESOURCES:
|
||||
_map_resource(RESOURCES[resource], resource,
|
||||
attributes.RESOURCE_ATTRIBUTE_MAP.get(
|
||||
resources[resource], dict()))
|
||||
RESOURCES[resource], dict()))
|
||||
|
||||
for resource in SUB_RESOURCES:
|
||||
_map_resource(SUB_RESOURCES[resource]['collection_name'], resource,
|
||||
attributes.RESOURCE_ATTRIBUTE_MAP.get(
|
||||
SUB_RESOURCES[resource]['collection_name'],
|
||||
dict()),
|
||||
SUB_RESOURCES[resource]['parent'])
|
||||
|
||||
super(APIRouter, self).__init__(mapper)
|
||||
|
@ -674,6 +674,83 @@ class JSONV2TestCase(APIv2TestBase):
|
||||
self.assertEqual(res.status_int, 400)
|
||||
|
||||
|
||||
class SubresourceTest(unittest.TestCase):
|
||||
def setUp(self):
|
||||
plugin = 'quantum.tests.unit.test_api_v2.TestSubresourcePlugin'
|
||||
QuantumManager._instance = None
|
||||
PluginAwareExtensionManager._instance = None
|
||||
args = ['--config-file', etcdir('quantum.conf.test')]
|
||||
config.parse(args=args)
|
||||
cfg.CONF.set_override('core_plugin', plugin)
|
||||
|
||||
self._plugin_patcher = mock.patch(plugin, autospec=True)
|
||||
self.plugin = self._plugin_patcher.start()
|
||||
|
||||
router.SUB_RESOURCES['dummy'] = {
|
||||
'collection_name': 'dummies',
|
||||
'parent': {'collection_name': 'networks',
|
||||
'member_name': 'network'}
|
||||
}
|
||||
|
||||
api = router.APIRouter()
|
||||
self.api = webtest.TestApp(api)
|
||||
|
||||
def tearDown(self):
|
||||
self._plugin_patcher.stop()
|
||||
self.api = None
|
||||
self.plugin = None
|
||||
cfg.CONF.reset()
|
||||
|
||||
def test_index_sub_resource(self):
|
||||
instance = self.plugin.return_value
|
||||
|
||||
self.api.get('/networks/id1/dummies')
|
||||
instance.get_network_dummies.assert_called_once_with(mock.ANY,
|
||||
filters=mock.ANY,
|
||||
fields=mock.ANY,
|
||||
network_id='id1')
|
||||
|
||||
def test_show_sub_resource(self):
|
||||
instance = self.plugin.return_value
|
||||
|
||||
dummy_id = _uuid()
|
||||
self.api.get('/networks/id1' + _get_path('dummies', id=dummy_id))
|
||||
instance.get_network_dummy.assert_called_once_with(mock.ANY,
|
||||
dummy_id,
|
||||
network_id='id1',
|
||||
fields=mock.ANY)
|
||||
|
||||
def test_create_sub_resource(self):
|
||||
instance = self.plugin.return_value
|
||||
|
||||
body = {'dummy': {'foo': 'bar', 'tenant_id': _uuid()}}
|
||||
self.api.post_json('/networks/id1/dummies', body)
|
||||
instance.create_network_dummy.assert_called_once_with(mock.ANY,
|
||||
network_id='id1',
|
||||
dummy=body)
|
||||
|
||||
def test_update_sub_resource(self):
|
||||
instance = self.plugin.return_value
|
||||
|
||||
dummy_id = _uuid()
|
||||
body = {'dummy': {'foo': 'bar', 'tenant_id': _uuid()}}
|
||||
self.api.put_json('/networks/id1' + _get_path('dummies', id=dummy_id),
|
||||
body)
|
||||
instance.update_network_dummy.assert_called_once_with(mock.ANY,
|
||||
dummy_id,
|
||||
network_id='id1',
|
||||
dummy=body)
|
||||
|
||||
def test_delete_sub_resource(self):
|
||||
instance = self.plugin.return_value
|
||||
|
||||
dummy_id = _uuid()
|
||||
self.api.delete('/networks/id1' + _get_path('dummies', id=dummy_id))
|
||||
instance.delete_network_dummy.assert_called_once_with(mock.ANY,
|
||||
dummy_id,
|
||||
network_id='id1')
|
||||
|
||||
|
||||
class V2Views(unittest.TestCase):
|
||||
def _view(self, keys, collection, resource):
|
||||
data = dict((key, 'value') for key in keys)
|
||||
@ -861,3 +938,22 @@ class ExtensionTestCase(unittest.TestCase):
|
||||
self.assertEqual(net['status'], "ACTIVE")
|
||||
self.assertEqual(net['v2attrs:something'], "123")
|
||||
self.assertFalse('v2attrs:something_else' in net)
|
||||
|
||||
|
||||
class TestSubresourcePlugin():
|
||||
def get_network_dummies(self, context, network_id,
|
||||
filters=None, fields=None):
|
||||
return []
|
||||
|
||||
def get_network_dummy(self, context, id, network_id,
|
||||
fields=None):
|
||||
return {}
|
||||
|
||||
def create_network_dummy(self, context, network_id, dummy):
|
||||
return {}
|
||||
|
||||
def update_network_dummy(self, context, id, network_id, dummy):
|
||||
return {}
|
||||
|
||||
def delete_network_dummy(self, context, id, network_id):
|
||||
return
|
||||
|
Loading…
Reference in New Issue
Block a user