]> review.fuel-infra Code Review - openstack-build/neutron-build.git/commitdiff
add parent/sub-resource support into Quantum API framework
authorOleg Bondarev <obondarev@mirantis.com>
Tue, 4 Dec 2012 15:15:09 +0000 (19:15 +0400)
committerOleg Bondarev <obondarev@mirantis.com>
Thu, 13 Dec 2012 12:07:53 +0000 (16:07 +0400)
- 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

quantum/api/v2/base.py
quantum/api/v2/router.py
quantum/tests/unit/test_api_v2.py

index 1cdaaa06d164d0729d3acdace9faba2e997deefa..0ddb9c1574ffec45ea837e7643527e2b77a7f8c9 100644 (file)
@@ -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
index f9d2ced2d3a4efd09960b373bc41134869ae9ef8..77e0070063b19d37d7048913ff353802a1e32a5e 100644 (file)
@@ -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()))
+
+        for resource in SUB_RESOURCES:
+            _map_resource(SUB_RESOURCES[resource]['collection_name'], resource,
                           attributes.RESOURCE_ATTRIBUTE_MAP.get(
-                              resources[resource], dict()))
+                              SUB_RESOURCES[resource]['collection_name'],
+                              dict()),
+                          SUB_RESOURCES[resource]['parent'])
 
         super(APIRouter, self).__init__(mapper)
index 7e7e7900280c02f1be339deb88394f0b546ba593..2731388a90b04360216eedc5e3d7d4e7af4ea98f 100644 (file)
@@ -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