Repository: ansible/ansible

Issue:
Allow enabling termination protection for CloudFormation stack
<!---
Verify first that your issue/request is not already reported on GitHub.
Also test if the latest release, and master branch are affected too.
-->

##### ISSUE TYPE
 - Feature Idea

##### COMPONENT NAME
- CloudFormation

##### ANSIBLE VERSION
- Latest Stable (2017-10-09)

##### ENVIRONMENT
- AWS

##### SUMMARY
I would like to be able to enable **Termination Protection** for a CloudFormation stack as specified in Official Documentation
 - http://docs.aws.amazon.com/cli/latest/reference/cloudformation/update-termination-protection.html
 - http://docs.aws.amazon.com/cli/latest/reference/cloudformation/create-stack.html
 - https://aws.amazon.com/blogs/mt/use-aws-cloudformation-stack-termination-protection-and-rollback-triggers-to-maintain-infrastructure-availability/

##### EXPECTED RESULTS
I expect to be able to create a new cloudformation stack (or update an existing one) and enable the **Termination Protection** option.

##### ACTUAL RESULTS
This feature appears to not exist at the present time.


Discussion:
cc @jsmartin @ryansb @tedder
[click here for bot help](https://github.com/ansible/ansibullbot/blob/master/ISSUE_HELP.md)
<!--- boilerplate: notify --->
`needs_contributor`.

Resolving patch:
diff --git a/lib/ansible/modules/cloud/amazon/cloudformation.py b/lib/ansible/modules/cloud/amazon/cloudformation.py
--- a/lib/ansible/modules/cloud/amazon/cloudformation.py
+++ b/lib/ansible/modules/cloud/amazon/cloudformation.py
@@ -116,6 +116,10 @@
     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:
@@ -197,6 +201,16 @@
       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 = '''
@@ -280,8 +294,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)
@@ -351,6 +371,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 = []
@@ -473,7 +513,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')
     )
     )
 
@@ -536,6 +577,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'])
 
@@ -555,6 +598,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


Test patch: