diff --git a/etc/heat/policy.json b/etc/heat/policy.json index 50c5689c3..982480693 100644 --- a/etc/heat/policy.json +++ b/etc/heat/policy.json @@ -54,6 +54,7 @@ "stacks:snapshot": "rule:deny_stack_user", "stacks:show_snapshot": "rule:deny_stack_user", "stacks:delete_snapshot": "rule:deny_stack_user", + "stacks:list_snapshots": "rule:deny_stack_user", "software_configs:create": "rule:deny_stack_user", "software_configs:show": "rule:deny_stack_user", diff --git a/heat/api/openstack/v1/__init__.py b/heat/api/openstack/v1/__init__.py index ea0ca8c2a..ad2c875ef 100644 --- a/heat/api/openstack/v1/__init__.py +++ b/heat/api/openstack/v1/__init__.py @@ -134,6 +134,11 @@ class API(wsgi.Router): action="delete_snapshot", conditions={'method': 'DELETE'}) + stack_mapper.connect("stack_list_snapshots", + "/stacks/{stack_name}/{stack_id}/snapshots", + action="list_snapshots", + conditions={'method': 'GET'}) + # Resources resources_resource = resources.create_resource(conf) stack_path = "/{tenant_id}/stacks/{stack_name}/{stack_id}" diff --git a/heat/api/openstack/v1/stacks.py b/heat/api/openstack/v1/stacks.py index 29bd22c79..95ce422d8 100644 --- a/heat/api/openstack/v1/stacks.py +++ b/heat/api/openstack/v1/stacks.py @@ -414,6 +414,13 @@ class StackController(object): self.rpc_client.delete_snapshot(req.context, identity, snapshot_id) raise exc.HTTPNoContent() + @util.identified_stack + def list_snapshots(self, req, identity): + return { + 'snapshots': self.rpc_client.stack_list_snapshots( + req.context, identity) + } + class StackSerializer(serializers.JSONResponseSerializer): """Handles serialization of specific controller method responses.""" diff --git a/heat/engine/service.py b/heat/engine/service.py index 50edabdef..c204090a2 100644 --- a/heat/engine/service.py +++ b/heat/engine/service.py @@ -1100,6 +1100,12 @@ class EngineService(service.Service): self.thread_group_mgr.start_with_lock(cnxt, stack, self.engine_id, stack.check) + @request_context + def stack_list_snapshots(self, cnxt, stack_identity): + s = self._get_stack(cnxt, stack_identity) + data = db_api.snapshot_get_all(cnxt, s.id) + return [api.format_snapshot(snapshot) for snapshot in data] + @request_context def metadata_update(self, cnxt, stack_identity, resource_name, metadata): diff --git a/heat/rpc/client.py b/heat/rpc/client.py index 278f988c1..e935ad172 100644 --- a/heat/rpc/client.py +++ b/heat/rpc/client.py @@ -512,3 +512,7 @@ class EngineClient(object): return self.call(cnxt, self.make_msg('delete_snapshot', stack_identity=stack_identity, snapshot_id=snapshot_id)) + + def stack_list_snapshots(self, cnxt, stack_identity): + return self.call(cnxt, self.make_msg('stack_list_snapshots', + stack_identity=stack_identity)) diff --git a/heat/tests/test_api_openstack_v1.py b/heat/tests/test_api_openstack_v1.py index b805f0b78..1c5048428 100644 --- a/heat/tests/test_api_openstack_v1.py +++ b/heat/tests/test_api_openstack_v1.py @@ -2969,6 +2969,18 @@ class RoutesTest(HeatTestCase): 'snapshot_id': 'cccc' }) + self.assertRoute( + self.m, + '/aaaa/stacks/teststack/bbbb/snapshots', + 'GET', + 'list_snapshots', + 'StackController', + { + 'tenant_id': 'aaaa', + 'stack_name': 'teststack', + 'stack_id': 'bbbb' + }) + def test_stack_data_template(self): self.assertRoute( self.m, diff --git a/heat/tests/test_engine_service.py b/heat/tests/test_engine_service.py index cc39c2232..f7987f016 100644 --- a/heat/tests/test_engine_service.py +++ b/heat/tests/test_engine_service.py @@ -3238,3 +3238,22 @@ class SnapshotServiceTest(HeatTestCase): self.engine.show_snapshot, self.ctx, stack.identifier(), snapshot_id) self.assertEqual(ex.exc_info[0], exception.NotFound) + + def test_list_snapshots(self): + stack = self._create_stack() + self.m.ReplayAll() + snapshot = self.engine.stack_snapshot( + self.ctx, stack.identifier(), 'snap1') + self.assertIsNotNone(snapshot['id']) + self.assertEqual("IN_PROGRESS", snapshot['status']) + self.engine.thread_group_mgr.groups[stack.id].wait() + + snapshots = self.engine.stack_list_snapshots( + self.ctx, stack.identifier()) + expected = { + "id": snapshot["id"], + "name": "snap1", + "status": "COMPLETE", + "status_reason": "Stack SNAPSHOT completed successfully", + "data": stack.prepare_abandon()} + self.assertEqual([expected], snapshots) diff --git a/heat/tests/test_sqlalchemy_api.py b/heat/tests/test_sqlalchemy_api.py index f9d24036e..a994f24e9 100644 --- a/heat/tests/test_sqlalchemy_api.py +++ b/heat/tests/test_sqlalchemy_api.py @@ -1027,6 +1027,20 @@ class SqlAlchemyTest(HeatTestCase): self.assertIn(snapshot_id, six.text_type(err)) + def test_snapshot_get_all(self): + template = create_raw_template(self.ctx) + user_creds = create_user_creds(self.ctx) + stack = create_stack(self.ctx, template, user_creds) + values = {'tenant': self.ctx.tenant_id, 'status': 'IN_PROGRESS', + 'stack_id': stack.id} + snapshot = db_api.snapshot_create(self.ctx, values) + self.assertIsNotNone(snapshot) + [snapshot] = db_api.snapshot_get_all(self.ctx, stack.id) + self.assertIsNotNone(snapshot) + self.assertEqual(values['tenant'], snapshot.tenant) + self.assertEqual(values['status'], snapshot.status) + self.assertIsNotNone(snapshot.created_at) + def create_raw_template(context, **kwargs): t = template_format.parse(wp_template)