From b9e15d0df1100d492c5bd766cc0d65f2465f9a50 Mon Sep 17 00:00:00 2001 From: Prasad Katti Date: Tue, 17 Oct 2017 05:34:45 -0700 Subject: [PATCH] Support 'termination protection' for cloudformation stacks (#31675) * Support 'termination protection' for cloudformation stacks - Pass in the stack_name and desired termination protection state to update_termination_protection * Fix for failing cloudformation unit test * Check if cfn has update_termination_protection attr * Use hasattr to test if cfn supports update_termination_protection * termination_protection shouldn't prevent update_stack call for existing stacks --- .../modules/cloud/amazon/cloudformation.py | 50 ++++++++++++++++++- 1 file changed, 48 insertions(+), 2 deletions(-) diff --git a/lib/ansible/modules/cloud/amazon/cloudformation.py b/lib/ansible/modules/cloud/amazon/cloudformation.py index f74852dcc0..9ddd19f0a4 100644 --- a/lib/ansible/modules/cloud/amazon/cloudformation.py +++ b/lib/ansible/modules/cloud/amazon/cloudformation.py @@ -118,6 +118,10 @@ options: required: false default: null version_added: "2.3" + termination_protection: + description: + - enable or disable termination protection on the stack. Only works with botocore >= 1.7.18. + version_added: "2.5" author: "James S. Martin (@jsmartin)" extends_documentation_fragment: @@ -173,6 +177,16 @@ EXAMPLES = ''' ClusterSize: 3 tags: Stack: ansible-cloudformation + +# Enable termination protection on a stack. +# If the stack already exists, this will update its termination protection +- name: enable termination protection during stack creation + cloudformation: + stack_name: my_stack + state: present + template_url: https://s3.amazonaws.com/my-bucket/cloudformation.template + termination_protection: yes + ''' RETURN = ''' @@ -256,8 +270,14 @@ def create_stack(module, stack_params, cfn): if 'TemplateBody' not in stack_params and 'TemplateURL' not in stack_params: module.fail_json(msg="Either 'template' or 'template_url' is required when the stack does not exist.") - # 'disablerollback' only applies on creation, not update. + # 'disablerollback' and 'EnableTerminationProtection' only + # apply on creation, not update. stack_params['DisableRollback'] = module.params['disable_rollback'] + if module.params.get('termination_protection') is not None: + if boto_supports_termination_protection(cfn): + stack_params['EnableTerminationProtection'] = bool(module.params.get('termination_protection')) + else: + module.fail_json(msg="termination_protection parameter requires botocore >= 1.7.18") try: cfn.create_stack(**stack_params) @@ -328,6 +348,26 @@ def update_stack(module, stack_params, cfn): return result +def update_termination_protection(module, cfn, stack_name, desired_termination_protection_state): + '''updates termination protection of a stack''' + if not boto_supports_termination_protection(cfn): + module.fail_json(msg="termination_protection parameter requires botocore >= 1.7.18") + stack = get_stack_facts(cfn, stack_name) + if stack: + if stack['EnableTerminationProtection'] is not desired_termination_protection_state: + try: + cfn.update_termination_protection( + EnableTerminationProtection=desired_termination_protection_state, + StackName=stack_name) + except botocore.exceptions.ClientError as e: + module.fail_json(msg=boto_exception(e), exception=traceback.format_exc()) + + +def boto_supports_termination_protection(cfn): + '''termination protection was added in botocore 1.7.18''' + return hasattr(cfn, "update_termination_protection") + + def stack_operation(cfn, stack_name, operation): '''gets the status of a stack while it is created/updated/deleted''' existed = [] @@ -450,7 +490,8 @@ def main(): create_changeset=dict(default=False, type='bool'), changeset_name=dict(default=None, required=False), role_arn=dict(default=None, required=False), - tags=dict(default=None, type='dict') + tags=dict(default=None, type='dict'), + termination_protection=dict(default=None, type='bool') ) ) @@ -511,6 +552,8 @@ def main(): cfn.describe_stacks = backoff_wrapper(cfn.describe_stacks) cfn.list_stack_resources = backoff_wrapper(cfn.list_stack_resources) cfn.delete_stack = backoff_wrapper(cfn.delete_stack) + if boto_supports_termination_protection(cfn): + cfn.update_termination_protection = backoff_wrapper(cfn.update_termination_protection) stack_info = get_stack_facts(cfn, stack_params['StackName']) @@ -530,6 +573,9 @@ def main(): elif module.params.get('create_changeset'): result = create_changeset(module, stack_params, cfn) else: + if module.params.get('termination_protection') is not None: + update_termination_protection(module, cfn, stack_params['StackName'], + bool(module.params.get('termination_protection'))) result = update_stack(module, stack_params, cfn) # format the stack output