From 7893cc7b8d02dbaf9afcc2eecfa5c561e901323a Mon Sep 17 00:00:00 2001 From: Vijendar Komalla Date: Fri, 15 Nov 2013 14:17:36 -0600 Subject: [PATCH] heat engine changes for abandon-stack abandon-stack operation is similar to delete-stack, but actual resources would not be deleted. Only stack and resource data would be deleted from heat database. And also abandon-stack operation would return the stack/resources data in json format. Implements: blueprint abandon-stack Change-Id: Ibb386255887d4de54e9a3dc95254a7151475cf3a --- heat/engine/parser.py | 15 +++++++++++ heat/engine/resource.py | 15 +++++++++++ heat/engine/service.py | 22 +++++++++++++++- heat/engine/stack_resource.py | 6 +++++ heat/rpc/client.py | 12 +++++++++ heat/tests/test_engine_service.py | 25 ++++++++++++++++++ heat/tests/test_parser.py | 43 +++++++++++++++++++++++++++++++ heat/tests/test_resource.py | 23 +++++++++++++++++ heat/tests/test_stack_resource.py | 25 ++++++++++++++++++ 9 files changed, 185 insertions(+), 1 deletion(-) diff --git a/heat/engine/parser.py b/heat/engine/parser.py index 00450ae4eb..584b3f32f5 100644 --- a/heat/engine/parser.py +++ b/heat/engine/parser.py @@ -660,6 +660,21 @@ class Stack(collections.Mapping): self.clients.nova().availability_zones.list(detailed=False)] return self._zones + def set_deletion_policy(self, policy): + for res in self.resources.values(): + res.set_deletion_policy(policy) + + def get_abandon_data(self): + return { + 'name': self.name, + 'id': self.id, + 'action': self.action, + 'status': self.status, + 'template': self.t.t, + 'resources': dict((res.name, res.get_abandon_data()) + for res in self.resources.values()) + } + def resolve_static_data(self, snippet): return resolve_static_data(self.t, self, self.parameters, snippet) diff --git a/heat/engine/resource.py b/heat/engine/resource.py index f00695430a..28a5aebc08 100644 --- a/heat/engine/resource.py +++ b/heat/engine/resource.py @@ -420,6 +420,21 @@ class Resource(object): self.name) return self._do_action(action, self.properties.validate) + def set_deletion_policy(self, policy): + self.t['DeletionPolicy'] = policy + + def get_abandon_data(self): + return { + 'name': self.name, + 'resource_id': self.resource_id, + 'type': self.type(), + 'action': self.action, + 'status': self.status, + 'metadata': self.metadata, + 'resource_data': dict((r.key, r.value) + for r in db_api.resource_data_get_all(self)) + } + def update(self, after, before=None): ''' update the resource. Subclasses should provide a handle_update() method diff --git a/heat/engine/service.py b/heat/engine/service.py index 3dcd6d6225..be6d0c3837 100644 --- a/heat/engine/service.py +++ b/heat/engine/service.py @@ -446,13 +446,33 @@ class EngineService(service.Service): stack = parser.Stack.load(cnxt, stack=st) + self._delete_stack_on_thread(st, stack) + + def _delete_stack_on_thread(self, st, stack): # Kill any pending threads by calling ThreadGroup.stop() if st.id in self.stg: self.stg[st.id].stop() del self.stg[st.id] # use the service ThreadGroup for deletes self.tg.add_thread(stack.delete) - return None + + @request_context + def abandon_stack(self, cnxt, stack_identity): + """ + The abandon_stack method abandons a given stack. + :param cnxt: RPC context. + :param stack_identity: Name of the stack you want to abandon. + """ + st = self._get_stack(cnxt, stack_identity) + logger.info(_('abandoning stack %s') % st.name) + stack = parser.Stack.load(cnxt, stack=st) + + # Get stack details before deleting it. + stack_info = stack.get_abandon_data() + # Set deletion policy to 'Retain' for all resources in the stack. + stack.set_deletion_policy(resource.RETAIN) + self._delete_stack_on_thread(st, stack) + return stack_info def list_resource_types(self, cnxt): """ diff --git a/heat/engine/stack_resource.py b/heat/engine/stack_resource.py index ea68ae64ed..1b32ff267f 100644 --- a/heat/engine/stack_resource.py +++ b/heat/engine/stack_resource.py @@ -246,6 +246,12 @@ class StackResource(resource.Resource): return done + def set_deletion_policy(self, policy): + self.nested().set_deletion_policy(policy) + + def get_abandon_data(self): + return self.nested().get_abandon_data() + def get_output(self, op): ''' Return the specified Output value from the nested stack. diff --git a/heat/rpc/client.py b/heat/rpc/client.py index 83bd150f19..fc2c1cb800 100644 --- a/heat/rpc/client.py +++ b/heat/rpc/client.py @@ -163,6 +163,18 @@ class EngineClient(heat.openstack.common.rpc.proxy.RpcProxy): self.make_msg('delete_stack', stack_identity=stack_identity)) + def abandon_stack(self, ctxt, stack_identity): + """ + The abandon_stack method deletes a given stack but + resources would not be deleted. + + :param ctxt: RPC context. + :param stack_identity: Name of the stack you want to abandon. + """ + return self.call(ctxt, + self.make_msg('abandon_stack', + stack_identity=stack_identity)) + def list_resource_types(self, ctxt): """ Get a list of valid resource types. diff --git a/heat/tests/test_engine_service.py b/heat/tests/test_engine_service.py index 56c25b6bbd..5dd9b16af6 100644 --- a/heat/tests/test_engine_service.py +++ b/heat/tests/test_engine_service.py @@ -1154,6 +1154,31 @@ class StackServiceTest(HeatTestCase): filters ) + @stack_context('service_abandon_stack') + def test_abandon_stack(self): + self.m.StubOutWithMock(parser.Stack, 'load') + parser.Stack.load(self.ctx, + stack=mox.IgnoreArg()).AndReturn(self.stack) + expected_res = { + u'WebServer': { + 'action': 'CREATE', + 'metadata': {}, + 'name': u'WebServer', + 'resource_data': {}, + 'resource_id': 9999, + 'status': 'COMPLETE', + 'type': u'AWS::EC2::Instance'}} + self.m.ReplayAll() + ret = self.eng.abandon_stack(self.ctx, self.stack.identifier()) + self.assertEqual(6, len(ret)) + self.assertEqual('CREATE', ret['action']) + self.assertEqual('COMPLETE', ret['status']) + self.assertEqual('service_abandon_stack', ret['name']) + self.assertTrue('id' in ret) + self.assertEqual(expected_res, ret['resources']) + self.assertEqual(self.stack.t.t, ret['template']) + self.m.VerifyAll() + def test_stack_describe_nonexistent(self): non_exist_identifier = identifier.HeatIdentifier( self.ctx.tenant_id, 'wibble', diff --git a/heat/tests/test_parser.py b/heat/tests/test_parser.py index 63123cb8fe..016cca0fad 100644 --- a/heat/tests/test_parser.py +++ b/heat/tests/test_parser.py @@ -766,6 +766,49 @@ class StackTest(HeatTestCase): self.assertTrue(identifier.stack_id) self.assertFalse(identifier.path) + @utils.stack_delete_after + def test_get_stack_abandon_data(self): + tpl = {'Resources': + {'A': {'Type': 'GenericResourceType'}, + 'B': {'Type': 'GenericResourceType'}}} + resources = '''{'A': {'status': 'COMPLETE', 'name': 'A', + 'resource_data': {}, 'resource_id': None, 'action': 'INIT', + 'type': 'GenericResourceType', 'metadata': {}}, + 'B': {'status': 'COMPLETE', 'name': 'B', 'resource_data': {}, + 'resource_id': None, 'action': 'INIT', 'type': 'GenericResourceType', + 'metadata': {}}}''' + self.stack = parser.Stack(self.ctx, 'stack_details_test', + parser.Template(tpl)) + self.stack.store() + info = self.stack.get_abandon_data() + self.assertEqual(None, info['action']) + self.assertTrue('id' in info) + self.assertEqual('stack_details_test', info['name']) + self.assertTrue(resources, info['resources']) + self.assertEqual(None, info['status']) + self.assertEqual(tpl, info['template']) + + @utils.stack_delete_after + def test_set_stack_res_deletion_policy(self): + tpl = {'Resources': + {'A': {'Type': 'GenericResourceType'}, + 'B': {'Type': 'GenericResourceType'}}} + resources = '''{'A': {'status': 'COMPLETE', 'name': 'A', + 'resource_data': {}, 'resource_id': None, 'action': 'INIT', + 'type': 'GenericResourceType', 'metadata': {}}, + 'B': {'status': 'COMPLETE', 'name': 'B', 'resource_data': {}, + 'resource_id': None, 'action': 'INIT', 'type': 'GenericResourceType', + 'metadata': {}}}''' + stack = parser.Stack(self.ctx, + 'stack_details_test', + parser.Template(tpl)) + stack.store() + stack.set_deletion_policy(resource.RETAIN) + self.assertEqual(resource.RETAIN, + stack.resources['A'].t['DeletionPolicy']) + self.assertEqual(resource.RETAIN, + stack.resources['B'].t['DeletionPolicy']) + @utils.stack_delete_after def test_set_param_id(self): self.stack = parser.Stack(self.ctx, 'param_arn_test', diff --git a/heat/tests/test_resource.py b/heat/tests/test_resource.py index 2f49225f00..d9a61cd421 100644 --- a/heat/tests/test_resource.py +++ b/heat/tests/test_resource.py @@ -82,6 +82,29 @@ class ResourceTest(HeatTestCase): self.assertEqual(res.state, (res.CREATE, res.COMPLETE)) self.assertEqual(res.status_reason, 'wibble') + def test_set_deletion_policy(self): + tmpl = {'Type': 'Foo'} + res = generic_rsrc.GenericResource('test_resource', tmpl, self.stack) + res.set_deletion_policy(resource.RETAIN) + self.assertEqual(resource.RETAIN, res.t['DeletionPolicy']) + res.set_deletion_policy(resource.DELETE) + self.assertEqual(resource.DELETE, res.t['DeletionPolicy']) + + def test_get_abandon_data(self): + tmpl = {'Type': 'Foo'} + res = generic_rsrc.GenericResource('test_resource', tmpl, self.stack) + expected = { + 'action': 'INIT', + 'metadata': {}, + 'name': 'test_resource', + 'resource_data': {}, + 'resource_id': None, + 'status': 'COMPLETE', + 'type': 'Foo' + } + actual = res.get_abandon_data() + self.assertEqual(expected, actual) + def test_state_set_invalid(self): tmpl = {'Type': 'Foo'} res = generic_rsrc.GenericResource('test_resource', tmpl, self.stack) diff --git a/heat/tests/test_stack_resource.py b/heat/tests/test_stack_resource.py index ea0d96127f..4550de4b92 100644 --- a/heat/tests/test_stack_resource.py +++ b/heat/tests/test_stack_resource.py @@ -114,6 +114,31 @@ class StackResourceTest(HeatTestCase): self.assertEqual(self.templ, self.stack.t.t) self.assertEqual(self.stack.id, self.parent_resource.resource_id) + @utils.stack_delete_after + def test_set_deletion_policy(self): + self.parent_resource.create_with_template(self.templ, + {"KeyName": "key"}) + self.stack = self.parent_resource.nested() + self.parent_resource.set_deletion_policy(resource.RETAIN) + for res in self.stack.resources.values(): + self.assertEqual(resource.RETAIN, res.t['DeletionPolicy']) + + @utils.stack_delete_after + def test_get_abandon_data(self): + self.parent_resource.create_with_template(self.templ, + {"KeyName": "key"}) + ret = self.parent_resource.get_abandon_data() + # check abandoned data contains all the necessary information. + # (no need to check stack/resource IDs, because they are + # randomly generated uuids) + self.assertEqual(6, len(ret)) + self.assertEqual('CREATE', ret['action']) + self.assertTrue('name' in ret) + self.assertTrue('id' in ret) + self.assertTrue('resources' in ret) + self.assertEqual(template_format.parse(param_template), + ret['template']) + @utils.stack_delete_after def test_create_with_template_validates(self): """