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
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",
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)
--- /dev/null
+# 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)
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):
'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,
'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()