diff --git a/etc/heat/policy.json b/etc/heat/policy.json index 1be98e8f76..b4980d9771 100644 --- a/etc/heat/policy.json +++ b/etc/heat/policy.json @@ -41,6 +41,7 @@ "stacks:create": "rule:deny_stack_user", "stacks:delete": "rule:deny_stack_user", "stacks:detail": "rule:deny_stack_user", + "stacks:export": "rule:deny_stack_user", "stacks:generate_template": "rule:deny_stack_user", "stacks:global_index": "rule:deny_everybody", "stacks:index": "rule:deny_stack_user", diff --git a/heat/api/openstack/v1/__init__.py b/heat/api/openstack/v1/__init__.py index 8cc68a53d3..f0920391cf 100644 --- a/heat/api/openstack/v1/__init__.py +++ b/heat/api/openstack/v1/__init__.py @@ -233,6 +233,12 @@ class API(wsgi.Router): 'action': 'abandon', 'method': 'DELETE' }, + { + 'name': 'stack_export', + 'url': '/stacks/{stack_name}/{stack_id}/export', + 'action': 'export', + 'method': 'GET' + }, { 'name': 'stack_snapshot', 'url': '/stacks/{stack_name}/{stack_id}/snapshots', diff --git a/heat/api/openstack/v1/stacks.py b/heat/api/openstack/v1/stacks.py index 525e4cc652..2153c4ba62 100644 --- a/heat/api/openstack/v1/stacks.py +++ b/heat/api/openstack/v1/stacks.py @@ -527,6 +527,14 @@ class StackController(object): return self.rpc_client.abandon_stack(req.context, identity) + @util.identified_stack + def export(self, req, identity): + """Export specified stack. + + Return stack data in JSON format. + """ + return self.rpc_client.export_stack(req.context, identity) + @util.policy_enforce def validate_template(self, req, body): """Implements the ValidateTemplate API action. diff --git a/heat/tests/api/openstack_v1/test_stacks.py b/heat/tests/api/openstack_v1/test_stacks.py index e935c343d1..e243c65687 100644 --- a/heat/tests/api/openstack_v1/test_stacks.py +++ b/heat/tests/api/openstack_v1/test_stacks.py @@ -2103,6 +2103,29 @@ class StackControllerTest(tools.ControllerTest, common.HeatTestCase): self.assertEqual(403, resp.status_int) self.assertIn('403 Forbidden', six.text_type(resp)) + def test_export(self, mock_enforce): + self._mock_enforce_setup(mock_enforce, 'export', True) + identity = identifier.HeatIdentifier(self.tenant, 'wordpress', '6') + req = self._get('/stacks/%(stack_name)s/%(stack_id)s/export' % + identity) + + self.m.StubOutWithMock(rpc_client.EngineClient, 'call') + # Engine returns json data + expected = {"name": "test", "id": "123"} + rpc_client.EngineClient.call( + req.context, + ('export_stack', {'stack_identity': dict(identity)}), + version='1.22' + ).AndReturn(expected) + self.m.ReplayAll() + + ret = self.controller.export(req, + tenant_id=identity.tenant, + stack_name=identity.stack_name, + stack_id=identity.stack_id) + self.assertEqual(expected, ret) + self.m.VerifyAll() + def test_abandon(self, mock_enforce): self._mock_enforce_setup(mock_enforce, 'abandon', True) identity = identifier.HeatIdentifier(self.tenant, 'wordpress', '6')