]> review.fuel-infra Code Review - openstack-build/heat-build.git/commitdiff
api : Add ReST actions POST method
authorSteven Hardy <shardy@redhat.com>
Fri, 10 May 2013 17:29:04 +0000 (18:29 +0100)
committerSteven Hardy <shardy@redhat.com>
Wed, 26 Jun 2013 15:24:34 +0000 (16:24 +0100)
Initial support for an actions subpath, which will be the access-point
for non-lifecycle operations on stacks, e.g suspend/resume

This adds support for a  POST method for /actions subpath, so that requests for
specific actions may be made, using a similar interface to that provided by
nova for the admin actions extension.  So a body of {'suspend': None}
will suspend the stack, see http://api.openstack.org/api-ref.html

blueprint: stack-suspend-resume
Change-Id: I180345c818cbfa38ac8c17caba2c0f64adabf6be

heat/api/openstack/v1/__init__.py
heat/api/openstack/v1/actions.py [new file with mode: 0644]
heat/tests/test_api_openstack_v1.py

index f9d0eb1b6e513ce08bb20afb775aed8409e1d398..2f65ad8189c7ef514fbabe218249ac6ef75fb466 100644 (file)
@@ -21,6 +21,7 @@ gettext.install('heat', unicode=1)
 from heat.api.openstack.v1 import stacks
 from heat.api.openstack.v1 import resources
 from heat.api.openstack.v1 import events
+from heat.api.openstack.v1 import actions
 from heat.common import wsgi
 
 from heat.openstack.common import log as logging
@@ -71,12 +72,16 @@ class API(wsgi.Router):
             stack_mapper.connect("stack_lookup",
                                  r"/stacks/{stack_name:arn\x3A.*}",
                                  action="lookup")
-            subpaths = ['resources', 'events', 'template']
+            subpaths = ['resources', 'events', 'template', 'actions']
             path = "{path:%s}" % '|'.join(subpaths)
             stack_mapper.connect("stack_lookup_subpath",
                                  "/stacks/{stack_name}/" + path,
                                  action="lookup",
                                  conditions={'method': 'GET'})
+            stack_mapper.connect("stack_lookup_subpath_post",
+                                 "/stacks/{stack_name}/" + path,
+                                 action="lookup",
+                                 conditions={'method': 'POST'})
             stack_mapper.connect("stack_show",
                                  "/stacks/{stack_name}/{stack_id}",
                                  action="show",
@@ -140,4 +145,14 @@ class API(wsgi.Router):
                               action="show",
                               conditions={'method': 'GET'})
 
+        # Actions
+        actions_resource = actions.create_resource(conf)
+        with mapper.submapper(controller=actions_resource,
+                              path_prefix=stack_path) as ac_mapper:
+
+            ac_mapper.connect("action_stack",
+                              "/actions",
+                              action="action",
+                              conditions={'method': 'POST'})
+
         super(API, self).__init__(mapper)
diff --git a/heat/api/openstack/v1/actions.py b/heat/api/openstack/v1/actions.py
new file mode 100644 (file)
index 0000000..acb7460
--- /dev/null
@@ -0,0 +1,69 @@
+# 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.
+
+from webob import exc
+
+from heat.api.openstack.v1 import util
+from heat.common import wsgi
+from heat.rpc import client as rpc_client
+import heat.openstack.common.rpc.common as rpc_common
+
+
+class ActionController(object):
+    """
+    WSGI controller for Actions in Heat v1 API
+    Implements the API for stack actions
+    """
+
+    ACTIONS = (SUSPEND,) = ('suspend',)
+
+    def __init__(self, options):
+        self.options = options
+        self.engine = rpc_client.EngineClient()
+
+    @util.identified_stack
+    def action(self, req, identity, body={}):
+        """
+        Performs a specified action on a stack, the body is expecting to
+        contain exactly one item whose key specifies the action
+        """
+
+        if len(body) < 1:
+            raise exc.HTTPBadRequest(_("No action specified"))
+
+        if len(body) > 1:
+            raise exc.HTTPBadRequest(_("Multiple actions specified"))
+
+        ac = body.keys()[0]
+        if ac not in self.ACTIONS:
+            raise exc.HTTPBadRequest(_("Invalid action %s specified") % ac)
+
+        if ac == self.SUSPEND:
+            try:
+                res = self.engine.stack_suspend(req.context, identity)
+            except rpc_common.RemoteError as ex:
+                return util.remote_error(ex)
+        else:
+            raise exc.HTTPInternalServerError(_("Unexpected action %s") % ac)
+
+
+def create_resource(options):
+    """
+    Actions action factory method.
+    """
+    # TODO(zaneb) handle XML based on Content-type/Accepts
+    deserializer = wsgi.JSONRequestDeserializer()
+    serializer = wsgi.JSONResponseSerializer()
+    return wsgi.Resource(ActionController(options), deserializer, serializer)
index e8c53c22ae79916dbfe96aa03754d67b4a04958a..7783a712ee9278476b42e72f542b4fdd674413fd 100644 (file)
@@ -30,6 +30,7 @@ import heat.api.openstack.v1 as api_v1
 import heat.api.openstack.v1.stacks as stacks
 import heat.api.openstack.v1.resources as resources
 import heat.api.openstack.v1.events as events
+import heat.api.openstack.v1.actions as actions
 
 
 class InstantiationDataTest(HeatTestCase):
@@ -1902,6 +1903,32 @@ class RoutesTest(HeatTestCase):
                 'path': 'template'
             })
 
+    def test_stack_post_actions(self):
+        self.assertRoute(
+            self.m,
+            '/aaaa/stacks/teststack/bbbb/actions',
+            'POST',
+            'action',
+            'ActionController',
+            {
+                'tenant_id': 'aaaa',
+                'stack_name': 'teststack',
+                'stack_id': 'bbbb',
+            })
+
+    def test_stack_post_actions_lookup_redirect(self):
+        self.assertRoute(
+            self.m,
+            '/aaaa/stacks/teststack/actions',
+            'POST',
+            'lookup',
+            'StackController',
+            {
+                'tenant_id': 'aaaa',
+                'stack_name': 'teststack',
+                'path': 'actions'
+            })
+
     def test_stack_update_delete(self):
         self.assertRoute(
             self.m,
@@ -2000,3 +2027,140 @@ class RoutesTest(HeatTestCase):
                 'resource_name': 'cccc',
                 'event_id': 'dddd'
             })
+
+
+class ActionControllerTest(ControllerTest, HeatTestCase):
+    '''
+    Tests the API class which acts as the WSGI controller,
+    the endpoint processing API requests after they are routed
+    '''
+
+    def setUp(self):
+        super(ActionControllerTest, self).setUp()
+        # Create WSGI controller instance
+
+        class DummyConfig():
+            bind_port = 8004
+
+        cfgopts = DummyConfig()
+        self.controller = actions.ActionController(options=cfgopts)
+
+    def test_action_suspend(self):
+        res_name = 'WikiDatabase'
+        stack_identity = identifier.HeatIdentifier(self.tenant,
+                                                   'wordpress', '1')
+        body = {'suspend': None}
+        req = self._post(stack_identity._tenant_path() + '/actions',
+                         data=json.dumps(body))
+
+        self.m.StubOutWithMock(rpc, 'call')
+        rpc.call(req.context, self.topic,
+                 {'namespace': None,
+                  'method': 'stack_suspend',
+                  'args': {'stack_identity': stack_identity},
+                  'version': self.api_version},
+                 None).AndReturn(None)
+        self.m.ReplayAll()
+
+        result = self.controller.action(req, tenant_id=self.tenant,
+                                        stack_name=stack_identity.stack_name,
+                                        stack_id=stack_identity.stack_id,
+                                        body=body)
+        self.assertEqual(result, None)
+        self.m.VerifyAll()
+
+    def test_action_badaction(self):
+        res_name = 'WikiDatabase'
+        stack_identity = identifier.HeatIdentifier(self.tenant,
+                                                   'wordpress', '1')
+        body = {'notallowed': None}
+        req = self._post(stack_identity._tenant_path() + '/actions',
+                         data=json.dumps(body))
+
+        self.m.ReplayAll()
+
+        self.assertRaises(webob.exc.HTTPBadRequest, self.controller.action,
+                          req, tenant_id=self.tenant,
+                          stack_name=stack_identity.stack_name,
+                          stack_id=stack_identity.stack_id,
+                          body=body)
+        self.m.VerifyAll()
+
+    def test_action_badaction_empty(self):
+        res_name = 'WikiDatabase'
+        stack_identity = identifier.HeatIdentifier(self.tenant,
+                                                   'wordpress', '1')
+        body = {}
+        req = self._post(stack_identity._tenant_path() + '/actions',
+                         data=json.dumps(body))
+
+        self.m.ReplayAll()
+
+        self.assertRaises(webob.exc.HTTPBadRequest, self.controller.action,
+                          req, tenant_id=self.tenant,
+                          stack_name=stack_identity.stack_name,
+                          stack_id=stack_identity.stack_id,
+                          body=body)
+        self.m.VerifyAll()
+
+    def test_action_badaction_multiple(self):
+        res_name = 'WikiDatabase'
+        stack_identity = identifier.HeatIdentifier(self.tenant,
+                                                   'wordpress', '1')
+        body = {'one': None, 'two': None}
+        req = self._post(stack_identity._tenant_path() + '/actions',
+                         data=json.dumps(body))
+
+        self.m.ReplayAll()
+
+        self.assertRaises(webob.exc.HTTPBadRequest, self.controller.action,
+                          req, tenant_id=self.tenant,
+                          stack_name=stack_identity.stack_name,
+                          stack_id=stack_identity.stack_id,
+                          body=body)
+        self.m.VerifyAll()
+
+    def test_action_rmt_aterr(self):
+        res_name = 'WikiDatabase'
+        stack_identity = identifier.HeatIdentifier(self.tenant,
+                                                   'wordpress', '1')
+        body = {'suspend': None}
+        req = self._post(stack_identity._tenant_path() + '/actions',
+                         data=json.dumps(body))
+
+        self.m.StubOutWithMock(rpc, 'call')
+        rpc.call(req.context, self.topic,
+                 {'namespace': None,
+                  'method': 'stack_suspend',
+                  'args': {'stack_identity': stack_identity},
+                  'version': self.api_version},
+                 None).AndRaise(rpc_common.RemoteError("AttributeError"))
+        self.m.ReplayAll()
+
+        self.assertRaises(webob.exc.HTTPBadRequest, self.controller.action,
+                          req, tenant_id=self.tenant,
+                          stack_name=stack_identity.stack_name,
+                          stack_id=stack_identity.stack_id,
+                          body=body)
+
+        self.m.VerifyAll()
+
+    def test_action_badaction_ise(self):
+        res_name = 'WikiDatabase'
+        stack_identity = identifier.HeatIdentifier(self.tenant,
+                                                   'wordpress', '1')
+        body = {'oops': None}
+        req = self._post(stack_identity._tenant_path() + '/actions',
+                         data=json.dumps(body))
+
+        self.m.ReplayAll()
+
+        self.controller.ACTIONS = (SUSPEND, NEW) = ('suspend', 'oops')
+
+        self.assertRaises(webob.exc.HTTPInternalServerError,
+                          self.controller.action,
+                          req, tenant_id=self.tenant,
+                          stack_name=stack_identity.stack_name,
+                          stack_id=stack_identity.stack_id,
+                          body=body)
+        self.m.VerifyAll()