]> review.fuel-infra Code Review - openstack-build/heat-build.git/commitdiff
ReST API: Add API for Resources
authorZane Bitter <zbitter@redhat.com>
Mon, 12 Nov 2012 16:42:37 +0000 (17:42 +0100)
committerZane Bitter <zbitter@redhat.com>
Fri, 16 Nov 2012 11:11:41 +0000 (12:11 +0100)
Change-Id: I860349d03a2d7d034c600a129aead59964930b02
Signed-off-by: Zane Bitter <zbitter@redhat.com>
docs/api.md
heat/api/openstack/v1/__init__.py
heat/api/openstack/v1/resources.py [new file with mode: 0644]
heat/api/openstack/v1/util.py
heat/tests/test_api_openstack_v1.py

index 44305a7b053edb7dae7a59bceffa50bbd3884d44..dc4262e715309dfab09cb7023f717dec02d623f7 100644 (file)
@@ -86,6 +86,7 @@ GET /v1/{tenant_id}/stacks/{stack_name}/{stack_id}/template
 
 Parameters:
 
+* `tenant_id` The unique identifier of the tenant or account
 * `stack_name` The name of the stack to look up
 * `stack_id` The unique identifier of the stack to look up
 
@@ -161,3 +162,44 @@ Parameters:
 * `template_url` The URL of the template to validate
 * `template` A JSON template to validate - this takes precendence over the `template_url` if both are supplied.
 * `keyn`, `valuen` User-defined parameters to pass to the Template
+
+List Stack Resources
+--------------------
+
+```
+GET /v1/{tenant_id}/stacks/{stack_name}/{stack_id}/resources
+```
+
+Parameters:
+
+* `tenant_id` The unique identifier of the tenant or account
+* `stack_name` The name of the stack to look up
+* `stack_id` The unique identifier of the stack to look up
+
+Get Resource
+------------
+
+```
+GET /v1/{tenant_id}/stacks/{stack_name}/{stack_id}/resources/{resource_name}
+```
+
+Parameters:
+
+* `tenant_id` The unique identifier of the tenant or account
+* `stack_name` The name of the stack to look up
+* `stack_id` The unique identifier of the stack to look up
+* `resource_name` The name of the resource in the template
+
+Get Resource Metadata
+---------------------
+
+```
+GET /v1/{tenant_id}/stacks/{stack_name}/{stack_id}/resources/{resource_name}/metadata
+```
+
+Parameters:
+
+* `tenant_id` The unique identifier of the tenant or account
+* `stack_name` The name of the stack to look up
+* `stack_id` The unique identifier of the stack to look up
+* `resource_name` The name of the resource in the template
index c138d50e571b8a65d4688825bcca74d67b0317bf..4a02ee63b5f7ecfa025ce287ae4e4d0404b7f238 100644 (file)
@@ -22,6 +22,7 @@ import gettext
 gettext.install('heat', unicode=1)
 
 from heat.api.openstack.v1 import stacks
+from heat.api.openstack.v1 import resources
 from heat.common import wsgi
 
 from webob import Request
@@ -87,4 +88,26 @@ class API(wsgi.Router):
                                  action="delete",
                                  conditions={'method': 'DELETE'})
 
+        # Resources
+        resources_resource = resources.create_resource(conf)
+        stack_path = "/{tenant_id}/stacks/{stack_name}/{stack_id}"
+        with mapper.submapper(controller=resources_resource,
+                              path_prefix=stack_path) as res_mapper:
+
+            # Resource collection
+            res_mapper.connect("resource_index",
+                               "/resources",
+                               action="index",
+                               conditions={'method': 'GET'})
+
+            # Resource data
+            res_mapper.connect("resource_show",
+                               "/resources/{resource_name}",
+                               action="show",
+                               conditions={'method': 'GET'})
+            res_mapper.connect("resource_metadata_show",
+                               "/resources/{resource_name}/metadata",
+                               action="metadata",
+                               conditions={'method': 'GET'})
+
         super(API, self).__init__(mapper)
diff --git a/heat/api/openstack/v1/resources.py b/heat/api/openstack/v1/resources.py
new file mode 100644 (file)
index 0000000..0cbaab2
--- /dev/null
@@ -0,0 +1,111 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+#
+#    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 itertools
+
+from heat.api.openstack.v1 import util
+from heat.common import wsgi
+from heat.engine import api as engine_api
+from heat.engine import identifier
+from heat.engine import rpcapi as engine_rpcapi
+import heat.openstack.common.rpc.common as rpc_common
+
+
+def format_resource(req, stack, keys=[]):
+    include_key = lambda k: k in keys if keys else True
+
+    def transform(key, value):
+        if not include_key(key):
+            return
+
+        if key == engine_api.RES_ID:
+            identity = identifier.HeatIdentifier(**value)
+            yield ('links', [util.make_link(req, identity),
+                             util.make_link(req, identity.stack(), 'stack')])
+        elif (key == engine_api.RES_STACK_NAME or
+              key == engine_api.RES_STACK_ID):
+            return
+        elif (key == engine_api.RES_METADATA):
+            return
+        else:
+            yield (key, value)
+
+    return dict(itertools.chain.from_iterable(
+        transform(k, v) for k, v in stack.items()))
+
+
+class ResourceController(object):
+    """
+    WSGI controller for Resources in Heat v1 API
+    Implements the API actions
+    """
+
+    def __init__(self, options):
+        self.options = options
+        self.engine = engine_rpcapi.EngineAPI()
+
+    @util.identified_stack
+    def index(self, req, identity):
+        """
+        Lists summary information for all resources
+        """
+
+        try:
+            res_list = self.engine.list_stack_resources(req.context,
+                                                        identity)
+        except rpc_common.RemoteError as ex:
+            return util.remote_error(ex)
+
+        return {'resources': [format_resource(req, res) for res in res_list]}
+
+    @util.identified_stack
+    def show(self, req, identity, resource_name):
+        """
+        Gets detailed information for a stack
+        """
+
+        try:
+            res = self.engine.describe_stack_resource(req.context,
+                                                      identity,
+                                                      resource_name)
+        except rpc_common.RemoteError as ex:
+            return util.remote_error(ex)
+
+        return {'resource': format_resource(req, res)}
+
+    @util.identified_stack
+    def metadata(self, req, identity, resource_name):
+        """
+        Gets detailed information for a stack
+        """
+
+        try:
+            res = self.engine.describe_stack_resource(req.context,
+                                                      identity,
+                                                      resource_name)
+        except rpc_common.RemoteError as ex:
+            return util.remote_error(ex)
+
+        return {engine_api.RES_METADATA: res[engine_api.RES_METADATA]}
+
+
+def create_resource(options):
+    """
+    Resources resource factory method.
+    """
+    # TODO(zaneb) handle XML based on Content-type/Accepts
+    deserializer = wsgi.JSONRequestDeserializer()
+    serializer = wsgi.JSONResponseSerializer()
+    return wsgi.Resource(ResourceController(options), deserializer, serializer)
index 26de4c01a0d1f6dc001ed37cc4a1747d6da7d5a8..7d367d95639d554f31142d3b8f7f690d4310fa56 100644 (file)
@@ -48,6 +48,22 @@ def identified_stack(handler):
     return handle_stack_method
 
 
+def identified_resource(handler):
+    '''
+    Decorator for a handler method that passes a resource identifier in place
+    of the various path components.
+    '''
+    @identified_stack
+    @wraps(handler)
+    def handle_stack_method(controller, stack_identity,
+                            resource_name, **kwargs):
+        resource_identity = identifier.ResourceIdentifier(stack_identity,
+                                                          resource_name)
+        return handler(controller, req, dict(resource_identity), **kwargs)
+
+    return handle_stack_method
+
+
 def make_url(req, identity):
     '''Return the URL for the supplied identity dictionary.'''
     try:
index 8d5c7eda222c1fea3edef237db7f806c743ff9ee..ae5a2c1585f415497e583bae595f95207b8c7bf3 100644 (file)
@@ -34,6 +34,7 @@ import heat.openstack.common.rpc.common as rpc_common
 from heat.common.wsgi import Request
 
 import heat.api.openstack.v1.stacks as stacks
+import heat.api.openstack.v1.resources as resources
 
 
 @attr(tag=['unit', 'api-openstack-v1'])
@@ -733,6 +734,250 @@ class StackControllerTest(ControllerTest, unittest.TestCase):
         self.m.VerifyAll()
 
 
+@attr(tag=['unit', 'api-openstack-v1', 'ResourceController'])
+@attr(speed='fast')
+class ResourceControllerTest(ControllerTest, unittest.TestCase):
+    '''
+    Tests the API class which acts as the WSGI controller,
+    the endpoint processing API requests after they are routed
+    '''
+
+    def setUp(self):
+        # Create WSGI controller instance
+        class DummyConfig():
+            bind_port = 8004
+        cfgopts = DummyConfig()
+        self.controller = resources.ResourceController(options=cfgopts)
+
+    def test_index(self):
+        res_name = 'WikiDatabase'
+        stack_identity = identifier.HeatIdentifier(self.tenant,
+                                                   'wordpress', '1')
+        res_identity = identifier.ResourceIdentifier(stack_identity,
+                                                     res_name)
+
+        req = self._get(stack_identity._tenant_path() + '/resources')
+
+        engine_resp = [
+            {
+                u'resource_identity': dict(res_identity),
+                u'stack_name': stack_identity.stack_name,
+                u'logical_resource_id': res_name,
+                u'resource_status_reason': None,
+                u'updated_time': u'2012-07-23T13:06:00Z',
+                u'stack_identity': stack_identity,
+                u'resource_status': u'CREATE_COMPLETE',
+                u'physical_resource_id':
+                    u'a3455d8c-9f88-404d-a85b-5315293e67de',
+                u'resource_type': u'AWS::EC2::Instance',
+            }
+        ]
+        self.m.StubOutWithMock(rpc, 'call')
+        rpc.call(req.context, self.topic,
+                 {'method': 'list_stack_resources',
+                  'args': {'stack_identity': stack_identity},
+                  'version': self.api_version},
+                 None).AndReturn(engine_resp)
+        self.m.ReplayAll()
+
+        result = self.controller.index(req, tenant_id=self.tenant,
+                                       stack_name=stack_identity.stack_name,
+                                       stack_id=stack_identity.stack_id)
+
+        expected = {
+            'resources': [
+                {
+                    'links': [
+                        {'href': self._url(res_identity), 'rel': 'self'},
+                        {'href': self._url(stack_identity), 'rel': 'stack'},
+                    ],
+                    u'logical_resource_id': res_name,
+                    u'resource_status_reason': None,
+                    u'updated_time': u'2012-07-23T13:06:00Z',
+                    u'resource_status': u'CREATE_COMPLETE',
+                    u'physical_resource_id':
+                        u'a3455d8c-9f88-404d-a85b-5315293e67de',
+                    u'resource_type': u'AWS::EC2::Instance',
+                }
+            ]
+        }
+
+        self.assertEqual(result, expected)
+        self.m.VerifyAll()
+
+    def test_index_nonexist(self):
+        stack_identity = identifier.HeatIdentifier(self.tenant,
+                                                   'rubbish', '1')
+
+        req = self._get(stack_identity._tenant_path() + '/resources')
+
+        self.m.StubOutWithMock(rpc, 'call')
+        rpc.call(req.context, self.topic,
+                 {'method': 'list_stack_resources',
+                  'args': {'stack_identity': stack_identity},
+                  'version': self.api_version},
+                 None).AndRaise(rpc_common.RemoteError("AttributeError"))
+        self.m.ReplayAll()
+
+        self.assertRaises(webob.exc.HTTPNotFound,
+                          self.controller.index,
+                          req, tenant_id=self.tenant,
+                          stack_name=stack_identity.stack_name,
+                          stack_id=stack_identity.stack_id)
+        self.m.VerifyAll()
+
+    def test_show(self):
+        res_name = 'WikiDatabase'
+        stack_identity = identifier.HeatIdentifier(self.tenant,
+                                                   'wordpress', '6')
+        res_identity = identifier.ResourceIdentifier(stack_identity,
+                                                     res_name)
+
+        req = self._get(stack_identity._tenant_path())
+
+        engine_resp = {
+            u'description': u'',
+            u'resource_identity': dict(res_identity),
+            u'stack_name': stack_identity.stack_name,
+            u'logical_resource_id': res_name,
+            u'resource_status_reason': None,
+            u'updated_time': u'2012-07-23T13:06:00Z',
+            u'stack_identity': dict(stack_identity),
+            u'resource_status': u'CREATE_COMPLETE',
+            u'physical_resource_id':
+                u'a3455d8c-9f88-404d-a85b-5315293e67de',
+            u'resource_type': u'AWS::EC2::Instance',
+            u'metadata': {u'ensureRunning': u'true'}
+        }
+        self.m.StubOutWithMock(rpc, 'call')
+        rpc.call(req.context, self.topic,
+                 {'method': 'describe_stack_resource',
+                  'args': {'stack_identity': stack_identity,
+                           'resource_name': res_name},
+                  'version': self.api_version},
+                 None).AndReturn(engine_resp)
+        self.m.ReplayAll()
+
+        result = self.controller.show(req, tenant_id=self.tenant,
+                                      stack_name=stack_identity.stack_name,
+                                      stack_id=stack_identity.stack_id,
+                                      resource_name=res_name)
+
+        expected = {
+            'resource': {
+                'links': [
+                    {'href': self._url(res_identity), 'rel': 'self'},
+                    {'href': self._url(stack_identity), 'rel': 'stack'},
+                ],
+                u'description': u'',
+                u'logical_resource_id': res_name,
+                u'resource_status_reason': None,
+                u'updated_time': u'2012-07-23T13:06:00Z',
+                u'resource_status': u'CREATE_COMPLETE',
+                u'physical_resource_id':
+                    u'a3455d8c-9f88-404d-a85b-5315293e67de',
+                u'resource_type': u'AWS::EC2::Instance',
+            }
+        }
+
+        self.assertEqual(result, expected)
+        self.m.VerifyAll()
+
+    def test_show_nonexist(self):
+        res_name = 'WikiDatabase'
+        stack_identity = identifier.HeatIdentifier(self.tenant,
+                                                   'rubbish', '1')
+        res_identity = identifier.ResourceIdentifier(stack_identity,
+                                                     res_name)
+
+        req = self._get(res_identity._tenant_path())
+
+        self.m.StubOutWithMock(rpc, 'call')
+        rpc.call(req.context, self.topic,
+                 {'method': 'describe_stack_resource',
+                  'args': {'stack_identity': stack_identity,
+                           'resource_name': res_name},
+                  'version': self.api_version},
+                 None).AndRaise(rpc_common.RemoteError("AttributeError"))
+        self.m.ReplayAll()
+
+        self.assertRaises(webob.exc.HTTPNotFound,
+                          self.controller.show,
+                          req, tenant_id=self.tenant,
+                          stack_name=stack_identity.stack_name,
+                          stack_id=stack_identity.stack_id,
+                          resource_name=res_name)
+        self.m.VerifyAll()
+
+    def test_show(self):
+        res_name = 'WikiDatabase'
+        stack_identity = identifier.HeatIdentifier(self.tenant,
+                                                   'wordpress', '6')
+        res_identity = identifier.ResourceIdentifier(stack_identity,
+                                                     res_name)
+
+        req = self._get(stack_identity._tenant_path())
+
+        engine_resp = {
+            u'description': u'',
+            u'resource_identity': dict(res_identity),
+            u'stack_name': stack_identity.stack_name,
+            u'logical_resource_id': res_name,
+            u'resource_status_reason': None,
+            u'updated_time': u'2012-07-23T13:06:00Z',
+            u'stack_identity': dict(stack_identity),
+            u'resource_status': u'CREATE_COMPLETE',
+            u'physical_resource_id':
+                u'a3455d8c-9f88-404d-a85b-5315293e67de',
+            u'resource_type': u'AWS::EC2::Instance',
+            u'metadata': {u'ensureRunning': u'true'}
+        }
+        self.m.StubOutWithMock(rpc, 'call')
+        rpc.call(req.context, self.topic,
+                 {'method': 'describe_stack_resource',
+                  'args': {'stack_identity': stack_identity,
+                           'resource_name': res_name},
+                  'version': self.api_version},
+                 None).AndReturn(engine_resp)
+        self.m.ReplayAll()
+
+        result = self.controller.metadata(req, tenant_id=self.tenant,
+                                          stack_name=stack_identity.stack_name,
+                                          stack_id=stack_identity.stack_id,
+                                          resource_name=res_name)
+
+        expected = {'metadata': {u'ensureRunning': u'true'}}
+
+        self.assertEqual(result, expected)
+        self.m.VerifyAll()
+
+    def test_metadata_show_nonexist(self):
+        res_name = 'WikiDatabase'
+        stack_identity = identifier.HeatIdentifier(self.tenant,
+                                                   'rubbish', '1')
+        res_identity = identifier.ResourceIdentifier(stack_identity,
+                                                     res_name)
+
+        req = self._get(res_identity._tenant_path() + '/metadata')
+
+        self.m.StubOutWithMock(rpc, 'call')
+        rpc.call(req.context, self.topic,
+                 {'method': 'describe_stack_resource',
+                  'args': {'stack_identity': stack_identity,
+                           'resource_name': res_name},
+                  'version': self.api_version},
+                 None).AndRaise(rpc_common.RemoteError("AttributeError"))
+        self.m.ReplayAll()
+
+        self.assertRaises(webob.exc.HTTPNotFound,
+                          self.controller.metadata,
+                          req, tenant_id=self.tenant,
+                          stack_name=stack_identity.stack_name,
+                          stack_id=stack_identity.stack_id,
+                          resource_name=res_name)
+        self.m.VerifyAll()
+
+
 if __name__ == '__main__':
     sys.argv.append(__file__)
     nose.main()