File: //lib/python3.9/site-packages/ansible_collections/amazon/aws/plugins/modules/cloudformation.py
# Copyright: (c) 2017, Ansible Project
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
from __future__ import absolute_import, division, print_function
__metaclass__ = type
DOCUMENTATION = '''
---
module: cloudformation
version_added: 1.0.0
short_description: Create or delete an AWS CloudFormation stack
description:
- Launches or updates an AWS CloudFormation stack and waits for it complete.
options:
stack_name:
description:
- Name of the CloudFormation stack.
required: true
type: str
disable_rollback:
description:
- If a stacks fails to form, rollback will remove the stack.
default: false
type: bool
on_create_failure:
description:
- Action to take upon failure of stack creation. Incompatible with the I(disable_rollback) option.
choices:
- DO_NOTHING
- ROLLBACK
- DELETE
type: str
create_timeout:
description:
- The amount of time (in minutes) that can pass before the stack status becomes CREATE_FAILED
type: int
template_parameters:
description:
- A list of hashes of all the template variables for the stack. The value can be a string or a dict.
- Dict can be used to set additional template parameter attributes like UsePreviousValue (see example).
default: {}
type: dict
state:
description:
- If I(state=present), stack will be created.
- If I(state=present) and if stack exists and template has changed, it will be updated.
- If I(state=absent), stack will be removed.
default: present
choices: [ present, absent ]
type: str
template:
description:
- The local path of the CloudFormation template.
- This must be the full path to the file, relative to the working directory. If using roles this may look
like C(roles/cloudformation/files/cloudformation-example.json).
- If I(state=present) and the stack does not exist yet, either I(template), I(template_body) or I(template_url)
must be specified (but only one of them).
- If I(state=present), the stack does exist, and neither I(template),
I(template_body) nor I(template_url) are specified, the previous template will be reused.
type: path
notification_arns:
description:
- A comma separated list of Simple Notification Service (SNS) topic ARNs to publish stack related events.
type: str
stack_policy:
description:
- The path of the file containing the CloudFormation stack policy. A policy cannot be removed once placed, but it can be modified.
for instance, allow all updates U(https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/protect-stack-resources.html#d0e9051)
type: str
stack_policy_body:
description:
- The CloudFormation stack policy in JSON. A policy cannot be removed once placed, but it can be modified.
for instance, allow all updates U(https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/protect-stack-resources.html#d0e9051)
type: json
version_added: 1.5.0
stack_policy_on_update_body:
description:
- the body of the cloudformation stack policy only applied during this update.
type: json
version_added: 1.5.0
tags:
description:
- Dictionary of tags to associate with stack and its resources during stack creation.
- Can be updated later, updating tags removes previous entries.
type: dict
template_url:
description:
- Location of file containing the template body. The URL must point to a template (max size 307,200 bytes) located in an
S3 bucket in the same region as the stack.
- If I(state=present) and the stack does not exist yet, either I(template), I(template_body) or I(template_url)
must be specified (but only one of them).
- If I(state=present), the stack does exist, and neither I(template), I(template_body) nor I(template_url) are specified,
the previous template will be reused.
type: str
create_changeset:
description:
- "If stack already exists create a changeset instead of directly applying changes. See the AWS Change Sets docs
U(https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-changesets.html)."
- "WARNING: if the stack does not exist, it will be created without changeset. If I(state=absent), the stack will be
deleted immediately with no changeset."
type: bool
default: false
changeset_name:
description:
- Name given to the changeset when creating a changeset.
- Only used when I(create_changeset=true).
- By default a name prefixed with Ansible-STACKNAME is generated based on input parameters.
See the AWS Change Sets docs for more information
U(https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-changesets.html)
type: str
role_arn:
description:
- The role that AWS CloudFormation assumes to create the stack. See the AWS CloudFormation Service Role
docs U(https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-iam-servicerole.html)
type: str
termination_protection:
description:
- Enable or disable termination protection on the stack.
type: bool
template_body:
description:
- Template body. Use this to pass in the actual body of the CloudFormation template.
- If I(state=present) and the stack does not exist yet, either I(template), I(template_body) or I(template_url)
must be specified (but only one of them).
- If I(state=present), the stack does exist, and neither I(template), I(template_body) nor I(template_url)
are specified, the previous template will be reused.
type: str
events_limit:
description:
- Maximum number of CloudFormation events to fetch from a stack when creating or updating it.
default: 200
type: int
backoff_delay:
description:
- Number of seconds to wait for the next retry.
default: 3
type: int
required: False
backoff_max_delay:
description:
- Maximum amount of time to wait between retries.
default: 30
type: int
required: False
backoff_retries:
description:
- Number of times to retry operation.
- AWS API throttling mechanism fails CloudFormation module so we have to retry a couple of times.
default: 10
type: int
required: False
capabilities:
description:
- Specify capabilities that stack template contains.
- Valid values are C(CAPABILITY_IAM), C(CAPABILITY_NAMED_IAM) and C(CAPABILITY_AUTO_EXPAND).
type: list
elements: str
default: [ CAPABILITY_IAM, CAPABILITY_NAMED_IAM ]
author:
- "James S. Martin (@jsmartin)"
extends_documentation_fragment:
- amazon.aws.aws
- amazon.aws.ec2
- amazon.aws.boto3
'''
EXAMPLES = '''
- name: create a cloudformation stack
amazon.aws.cloudformation:
stack_name: "ansible-cloudformation"
state: "present"
region: "us-east-1"
disable_rollback: true
template: "files/cloudformation-example.json"
template_parameters:
KeyName: "jmartin"
DiskType: "ephemeral"
InstanceType: "m1.small"
ClusterSize: 3
tags:
Stack: "ansible-cloudformation"
# Basic role example
- name: create a stack, specify role that cloudformation assumes
amazon.aws.cloudformation:
stack_name: "ansible-cloudformation"
state: "present"
region: "us-east-1"
disable_rollback: true
template: "roles/cloudformation/files/cloudformation-example.json"
role_arn: 'arn:aws:iam::123456789012:role/cloudformation-iam-role'
- name: delete a stack
amazon.aws.cloudformation:
stack_name: "ansible-cloudformation-old"
state: "absent"
# Create a stack, pass in template from a URL, disable rollback if stack creation fails,
# pass in some parameters to the template, provide tags for resources created
- name: create a stack, pass in the template via an URL
amazon.aws.cloudformation:
stack_name: "ansible-cloudformation"
state: present
region: us-east-1
disable_rollback: true
template_url: https://s3.amazonaws.com/my-bucket/cloudformation.template
template_parameters:
KeyName: jmartin
DiskType: ephemeral
InstanceType: m1.small
ClusterSize: 3
tags:
Stack: ansible-cloudformation
# Create a stack, passing in template body using lookup of Jinja2 template, disable rollback if stack creation fails,
# pass in some parameters to the template, provide tags for resources created
- name: create a stack, pass in the template body via lookup template
amazon.aws.cloudformation:
stack_name: "ansible-cloudformation"
state: present
region: us-east-1
disable_rollback: true
template_body: "{{ lookup('template', 'cloudformation.j2') }}"
template_parameters:
KeyName: jmartin
DiskType: ephemeral
InstanceType: m1.small
ClusterSize: 3
tags:
Stack: ansible-cloudformation
# Pass a template parameter which uses CloudFormation's UsePreviousValue attribute
# When use_previous_value is set to True, the given value will be ignored and
# CloudFormation will use the value from a previously submitted template.
# If use_previous_value is set to False (default) the given value is used.
- amazon.aws.cloudformation:
stack_name: "ansible-cloudformation"
state: "present"
region: "us-east-1"
template: "files/cloudformation-example.json"
template_parameters:
DBSnapshotIdentifier:
use_previous_value: True
value: arn:aws:rds:es-east-1:123456789012:snapshot:rds:my-db-snapshot
DBName:
use_previous_value: True
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
amazon.aws.cloudformation:
stack_name: my_stack
state: present
template_url: https://s3.amazonaws.com/my-bucket/cloudformation.template
termination_protection: true
# Configure TimeoutInMinutes before the stack status becomes CREATE_FAILED
# In this case, if disable_rollback is not set or is set to false, the stack will be rolled back.
- name: enable termination protection during stack creation
amazon.aws.cloudformation:
stack_name: my_stack
state: present
template_url: https://s3.amazonaws.com/my-bucket/cloudformation.template
create_timeout: 5
# Configure rollback behaviour on the unsuccessful creation of a stack allowing
# CloudFormation to clean up, or do nothing in the event of an unsuccessful
# deployment
# In this case, if on_create_failure is set to "DELETE", it will clean up the stack if
# it fails to create
- name: create stack which will delete on creation failure
amazon.aws.cloudformation:
stack_name: my_stack
state: present
template_url: https://s3.amazonaws.com/my-bucket/cloudformation.template
on_create_failure: DELETE
'''
RETURN = '''
events:
type: list
description: Most recent events in CloudFormation's event log. This may be from a previous run in some cases.
returned: always
sample: ["StackEvent AWS::CloudFormation::Stack stackname UPDATE_COMPLETE", "StackEvent AWS::CloudFormation::Stack stackname UPDATE_COMPLETE_CLEANUP_IN_PROGRESS"]
log:
description: Debugging logs. Useful when modifying or finding an error.
returned: always
type: list
sample: ["updating stack"]
change_set_id:
description: The ID of the stack change set if one was created
returned: I(state=present) and I(create_changeset=true)
type: str
sample: "arn:aws:cloudformation:us-east-1:123456789012:changeSet/Ansible-StackName-f4496805bd1b2be824d1e315c6884247ede41eb0"
stack_resources:
description: AWS stack resources and their status. List of dictionaries, one dict per resource.
returned: state == present
type: list
sample: [
{
"last_updated_time": "2016-10-11T19:40:14.979000+00:00",
"logical_resource_id": "CFTestSg",
"physical_resource_id": "cloudformation2-CFTestSg-16UQ4CYQ57O9F",
"resource_type": "AWS::EC2::SecurityGroup",
"status": "UPDATE_COMPLETE",
"status_reason": null
}
]
stack_outputs:
type: dict
description: A key:value dictionary of all the stack outputs currently defined. If there are no stack outputs, it is an empty dictionary.
returned: state == present
sample: {"MySg": "AnsibleModuleTestYAML-CFTestSg-C8UVS567B6NS"}
''' # NOQA
import json
import time
import traceback
import uuid
from hashlib import sha1
try:
import botocore
except ImportError:
pass # Handled by AnsibleAWSModule
from ansible.module_utils._text import to_bytes
from ansible.module_utils._text import to_native
from ansible_collections.amazon.aws.plugins.module_utils.core import AnsibleAWSModule
from ansible_collections.amazon.aws.plugins.module_utils.core import is_boto3_error_message
from ansible_collections.amazon.aws.plugins.module_utils.ec2 import AWSRetry
from ansible_collections.amazon.aws.plugins.module_utils.ec2 import ansible_dict_to_boto3_tag_list
from ansible_collections.amazon.aws.plugins.module_utils.ec2 import boto_exception
# Set a default, mostly for our integration tests. This will be overridden in
# the main() loop to match the parameters we're passed
retry_decorator = AWSRetry.jittered_backoff()
def get_stack_events(cfn, stack_name, events_limit, token_filter=None):
'''This event data was never correct, it worked as a side effect. So the v2.3 format is different.'''
ret = {'events': [], 'log': []}
try:
pg = cfn.get_paginator(
'describe_stack_events'
).paginate(
StackName=stack_name,
PaginationConfig={'MaxItems': events_limit}
)
if token_filter is not None:
events = list(retry_decorator(pg.search)(
"StackEvents[?ClientRequestToken == '{0}']".format(token_filter)
))
else:
events = list(pg.search("StackEvents[*]"))
except is_boto3_error_message('does not exist'):
ret['log'].append('Stack does not exist.')
return ret
except (botocore.exceptions.ValidationError, botocore.exceptions.ClientError) as err: # pylint: disable=duplicate-except
error_msg = boto_exception(err)
ret['log'].append('Unknown error: ' + str(error_msg))
return ret
for e in events:
eventline = 'StackEvent {ResourceType} {LogicalResourceId} {ResourceStatus}'.format(**e)
ret['events'].append(eventline)
if e['ResourceStatus'].endswith('FAILED'):
failline = '{ResourceType} {LogicalResourceId} {ResourceStatus}: {ResourceStatusReason}'.format(**e)
ret['log'].append(failline)
return ret
def create_stack(module, stack_params, cfn, events_limit):
if 'TemplateBody' not in stack_params and 'TemplateURL' not in stack_params:
module.fail_json(msg="Either 'template', 'template_body' or 'template_url' is required when the stack does not exist.")
# 'DisableRollback', 'TimeoutInMinutes', 'EnableTerminationProtection' and
# 'OnFailure' only apply on creation, not update.
if module.params.get('on_create_failure') is not None:
stack_params['OnFailure'] = module.params['on_create_failure']
else:
stack_params['DisableRollback'] = module.params['disable_rollback']
if module.params.get('create_timeout') is not None:
stack_params['TimeoutInMinutes'] = module.params['create_timeout']
if module.params.get('termination_protection') is not None:
stack_params['EnableTerminationProtection'] = bool(module.params.get('termination_protection'))
try:
response = cfn.create_stack(aws_retry=True, **stack_params)
# Use stack ID to follow stack state in case of on_create_failure = DELETE
result = stack_operation(module, cfn, response['StackId'], 'CREATE', events_limit, stack_params.get('ClientRequestToken', None))
except (botocore.exceptions.BotoCoreError, botocore.exceptions.ClientError) as err:
module.fail_json_aws(err, msg="Failed to create stack {0}".format(stack_params.get('StackName')))
if not result:
module.fail_json(msg="empty result")
return result
def list_changesets(cfn, stack_name):
res = cfn.list_change_sets(aws_retry=True, StackName=stack_name)
return [cs['ChangeSetName'] for cs in res['Summaries']]
def create_changeset(module, stack_params, cfn, events_limit):
if 'TemplateBody' not in stack_params and 'TemplateURL' not in stack_params:
module.fail_json(msg="Either 'template' or 'template_url' is required.")
if module.params['changeset_name'] is not None:
stack_params['ChangeSetName'] = module.params['changeset_name']
# changesets don't accept ClientRequestToken parameters
stack_params.pop('ClientRequestToken', None)
try:
changeset_name = build_changeset_name(stack_params)
stack_params['ChangeSetName'] = changeset_name
# Determine if this changeset already exists
pending_changesets = list_changesets(cfn, stack_params['StackName'])
if changeset_name in pending_changesets:
warning = 'WARNING: %d pending changeset(s) exist(s) for this stack!' % len(pending_changesets)
result = dict(changed=False, output='ChangeSet %s already exists.' % changeset_name, warnings=[warning])
else:
cs = cfn.create_change_set(aws_retry=True, **stack_params)
# Make sure we don't enter an infinite loop
time_end = time.time() + 600
while time.time() < time_end:
try:
newcs = cfn.describe_change_set(aws_retry=True, ChangeSetName=cs['Id'])
except botocore.exceptions.BotoCoreError as err:
module.fail_json_aws(err)
if newcs['Status'] == 'CREATE_PENDING' or newcs['Status'] == 'CREATE_IN_PROGRESS':
time.sleep(1)
elif newcs['Status'] == 'FAILED' and ("The submitted information didn't contain changes" in newcs['StatusReason']
or "No updates are to be performed" in newcs['StatusReason']):
cfn.delete_change_set(aws_retry=True, ChangeSetName=cs['Id'])
result = dict(changed=False,
output='The created Change Set did not contain any changes to this stack and was deleted.')
# a failed change set does not trigger any stack events so we just want to
# skip any further processing of result and just return it directly
return result
else:
break
# Lets not hog the cpu/spam the AWS API
time.sleep(1)
result = stack_operation(module, cfn, stack_params['StackName'], 'CREATE_CHANGESET', events_limit)
result['change_set_id'] = cs['Id']
result['warnings'] = ['Created changeset named %s for stack %s' % (changeset_name, stack_params['StackName']),
'You can execute it using: aws cloudformation execute-change-set --change-set-name %s' % cs['Id'],
'NOTE that dependencies on this stack might fail due to pending changes!']
except is_boto3_error_message('No updates are to be performed.'):
result = dict(changed=False, output='Stack is already up-to-date.')
except (botocore.exceptions.BotoCoreError, botocore.exceptions.ClientError) as err:
module.fail_json_aws(err, msg='Failed to create change set')
if not result:
module.fail_json(msg="empty result")
return result
def update_stack(module, stack_params, cfn, events_limit):
if 'TemplateBody' not in stack_params and 'TemplateURL' not in stack_params:
stack_params['UsePreviousTemplate'] = True
if module.params['stack_policy_on_update_body'] is not None:
stack_params['StackPolicyDuringUpdateBody'] = module.params['stack_policy_on_update_body']
# if the state is present and the stack already exists, we try to update it.
# AWS will tell us if the stack template and parameters are the same and
# don't need to be updated.
try:
cfn.update_stack(aws_retry=True, **stack_params)
result = stack_operation(module, cfn, stack_params['StackName'], 'UPDATE', events_limit, stack_params.get('ClientRequestToken', None))
except is_boto3_error_message('No updates are to be performed.'):
result = dict(changed=False, output='Stack is already up-to-date.')
except (botocore.exceptions.BotoCoreError, botocore.exceptions.ClientError) as err:
module.fail_json_aws(err, msg="Failed to update stack {0}".format(stack_params.get('StackName')))
if not result:
module.fail_json(msg="empty result")
return result
def update_termination_protection(module, cfn, stack_name, desired_termination_protection_state):
'''updates termination protection of a stack'''
stack = get_stack_facts(module, cfn, stack_name)
if stack:
if stack['EnableTerminationProtection'] is not desired_termination_protection_state:
try:
cfn.update_termination_protection(
aws_retry=True,
EnableTerminationProtection=desired_termination_protection_state,
StackName=stack_name)
except botocore.exceptions.ClientError as e:
module.fail_json_aws(e)
def stack_operation(module, cfn, stack_name, operation, events_limit, op_token=None):
'''gets the status of a stack while it is created/updated/deleted'''
existed = []
while True:
try:
stack = get_stack_facts(module, cfn, stack_name, raise_errors=True)
existed.append('yes')
except (botocore.exceptions.BotoCoreError, botocore.exceptions.ClientError):
# If the stack previously existed, and now can't be found then it's
# been deleted successfully.
if 'yes' in existed or operation == 'DELETE': # stacks may delete fast, look in a few ways.
ret = get_stack_events(cfn, stack_name, events_limit, op_token)
ret.update({'changed': True, 'output': 'Stack Deleted'})
return ret
else:
return {'changed': True, 'failed': True, 'output': 'Stack Not Found', 'exception': traceback.format_exc()}
ret = get_stack_events(cfn, stack_name, events_limit, op_token)
if not stack:
if 'yes' in existed or operation == 'DELETE': # stacks may delete fast, look in a few ways.
ret = get_stack_events(cfn, stack_name, events_limit, op_token)
ret.update({'changed': True, 'output': 'Stack Deleted'})
return ret
else:
ret.update({'changed': False, 'failed': True, 'output': 'Stack not found.'})
return ret
# it covers ROLLBACK_COMPLETE and UPDATE_ROLLBACK_COMPLETE
# Possible states: https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-describing-stacks.html#w1ab2c15c17c21c13
elif stack['StackStatus'].endswith('ROLLBACK_COMPLETE') and operation != 'CREATE_CHANGESET':
ret.update({'changed': True, 'failed': True, 'output': 'Problem with %s. Rollback complete' % operation})
return ret
elif stack['StackStatus'] == 'DELETE_COMPLETE' and operation == 'CREATE':
ret.update({'changed': True, 'failed': True, 'output': 'Stack create failed. Delete complete.'})
return ret
# note the ordering of ROLLBACK_COMPLETE, DELETE_COMPLETE, and COMPLETE, because otherwise COMPLETE will match all cases.
elif stack['StackStatus'].endswith('_COMPLETE'):
ret.update({'changed': True, 'output': 'Stack %s complete' % operation})
return ret
elif stack['StackStatus'].endswith('_ROLLBACK_FAILED'):
ret.update({'changed': True, 'failed': True, 'output': 'Stack %s rollback failed' % operation})
return ret
# note the ordering of ROLLBACK_FAILED and FAILED, because otherwise FAILED will match both cases.
elif stack['StackStatus'].endswith('_FAILED'):
ret.update({'changed': True, 'failed': True, 'output': 'Stack %s failed' % operation})
return ret
else:
# this can loop forever :/
time.sleep(5)
return {'failed': True, 'output': 'Failed for unknown reasons.'}
def build_changeset_name(stack_params):
if 'ChangeSetName' in stack_params:
return stack_params['ChangeSetName']
json_params = json.dumps(stack_params, sort_keys=True)
return 'Ansible-{0}-{1}'.format(
stack_params['StackName'],
sha1(to_bytes(json_params, errors='surrogate_or_strict')).hexdigest()
)
def check_mode_changeset(module, stack_params, cfn):
"""Create a change set, describe it and delete it before returning check mode outputs."""
stack_params['ChangeSetName'] = build_changeset_name(stack_params)
# changesets don't accept ClientRequestToken parameters
stack_params.pop('ClientRequestToken', None)
try:
change_set = cfn.create_change_set(aws_retry=True, **stack_params)
for _i in range(60): # total time 5 min
description = cfn.describe_change_set(aws_retry=True, ChangeSetName=change_set['Id'])
if description['Status'] in ('CREATE_COMPLETE', 'FAILED'):
break
time.sleep(5)
else:
# if the changeset doesn't finish in 5 mins, this `else` will trigger and fail
module.fail_json(msg="Failed to create change set %s" % stack_params['ChangeSetName'])
cfn.delete_change_set(aws_retry=True, ChangeSetName=change_set['Id'])
reason = description.get('StatusReason')
if description['Status'] == 'FAILED' and ("didn't contain changes" in reason or "No updates are to be performed" in reason):
return {'changed': False, 'msg': reason, 'meta': reason}
return {'changed': True, 'msg': reason, 'meta': description['Changes']}
except (botocore.exceptions.ValidationError, botocore.exceptions.ClientError) as err:
module.fail_json_aws(err)
def get_stack_facts(module, cfn, stack_name, raise_errors=False):
try:
stack_response = cfn.describe_stacks(aws_retry=True, StackName=stack_name)
stack_info = stack_response['Stacks'][0]
except is_boto3_error_message('does not exist'):
return None
except (botocore.exceptions.ValidationError, botocore.exceptions.ClientError) as err: # pylint: disable=duplicate-except
if raise_errors:
raise err
module.fail_json_aws(err, msg="Failed to describe stack")
if stack_response and stack_response.get('Stacks', None):
stacks = stack_response['Stacks']
if len(stacks):
stack_info = stacks[0]
return stack_info
def main():
argument_spec = dict(
stack_name=dict(required=True),
template_parameters=dict(required=False, type='dict', default={}),
state=dict(default='present', choices=['present', 'absent']),
template=dict(default=None, required=False, type='path'),
notification_arns=dict(default=None, required=False),
stack_policy=dict(default=None, required=False),
stack_policy_body=dict(default=None, required=False, type='json'),
stack_policy_on_update_body=dict(default=None, required=False, type='json'),
disable_rollback=dict(default=False, type='bool'),
on_create_failure=dict(default=None, required=False, choices=['DO_NOTHING', 'ROLLBACK', 'DELETE']),
create_timeout=dict(default=None, type='int'),
template_url=dict(default=None, required=False),
template_body=dict(default=None, required=False),
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'),
termination_protection=dict(default=None, type='bool'),
events_limit=dict(default=200, type='int'),
backoff_retries=dict(type='int', default=10, required=False),
backoff_delay=dict(type='int', default=3, required=False),
backoff_max_delay=dict(type='int', default=30, required=False),
capabilities=dict(type='list', elements='str', default=['CAPABILITY_IAM', 'CAPABILITY_NAMED_IAM'])
)
module = AnsibleAWSModule(
argument_spec=argument_spec,
mutually_exclusive=[['template_url', 'template', 'template_body'],
['disable_rollback', 'on_create_failure']],
supports_check_mode=True
)
invalid_capabilities = []
user_capabilities = module.params.get('capabilities')
for user_cap in user_capabilities:
if user_cap not in ['CAPABILITY_IAM', 'CAPABILITY_NAMED_IAM', 'CAPABILITY_AUTO_EXPAND']:
invalid_capabilities.append(user_cap)
if invalid_capabilities:
module.fail_json(msg="Specified capabilities are invalid : %r,"
" please check documentation for valid capabilities" % invalid_capabilities)
# collect the parameters that are passed to boto3. Keeps us from having so many scalars floating around.
stack_params = {
'Capabilities': user_capabilities,
'ClientRequestToken': to_native(uuid.uuid4()),
}
state = module.params['state']
stack_params['StackName'] = module.params['stack_name']
if module.params['template'] is not None:
with open(module.params['template'], 'r') as template_fh:
stack_params['TemplateBody'] = template_fh.read()
elif module.params['template_body'] is not None:
stack_params['TemplateBody'] = module.params['template_body']
elif module.params['template_url'] is not None:
stack_params['TemplateURL'] = module.params['template_url']
if module.params.get('notification_arns'):
stack_params['NotificationARNs'] = module.params['notification_arns'].split(',')
else:
stack_params['NotificationARNs'] = []
# can't check the policy when verifying.
if module.params['stack_policy_body'] is not None and not module.check_mode and not module.params['create_changeset']:
stack_params['StackPolicyBody'] = module.params['stack_policy_body']
elif module.params['stack_policy'] is not None and not module.check_mode and not module.params['create_changeset']:
with open(module.params['stack_policy'], 'r') as stack_policy_fh:
stack_params['StackPolicyBody'] = stack_policy_fh.read()
template_parameters = module.params['template_parameters']
stack_params['Parameters'] = []
for k, v in template_parameters.items():
if isinstance(v, dict):
# set parameter based on a dict to allow additional CFN Parameter Attributes
param = dict(ParameterKey=k)
if 'value' in v:
param['ParameterValue'] = str(v['value'])
if 'use_previous_value' in v and bool(v['use_previous_value']):
param['UsePreviousValue'] = True
param.pop('ParameterValue', None)
stack_params['Parameters'].append(param)
else:
# allow default k/v configuration to set a template parameter
stack_params['Parameters'].append({'ParameterKey': k, 'ParameterValue': str(v)})
if isinstance(module.params.get('tags'), dict):
stack_params['Tags'] = ansible_dict_to_boto3_tag_list(module.params['tags'])
if module.params.get('role_arn'):
stack_params['RoleARN'] = module.params['role_arn']
result = {}
# Wrap the cloudformation client methods that this module uses with
# automatic backoff / retry for throttling error codes
retry_decorator = AWSRetry.jittered_backoff(
retries=module.params.get('backoff_retries'),
delay=module.params.get('backoff_delay'),
max_delay=module.params.get('backoff_max_delay')
)
cfn = module.client('cloudformation', retry_decorator=retry_decorator)
stack_info = get_stack_facts(module, cfn, stack_params['StackName'])
if module.check_mode:
if state == 'absent' and stack_info:
module.exit_json(changed=True, msg='Stack would be deleted', meta=[])
elif state == 'absent' and not stack_info:
module.exit_json(changed=False, msg='Stack doesn\'t exist', meta=[])
elif state == 'present' and not stack_info:
module.exit_json(changed=True, msg='New stack would be created', meta=[])
else:
module.exit_json(**check_mode_changeset(module, stack_params, cfn))
if state == 'present':
if not stack_info:
result = create_stack(module, stack_params, cfn, module.params.get('events_limit'))
elif module.params.get('create_changeset'):
result = create_changeset(module, stack_params, cfn, module.params.get('events_limit'))
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, module.params.get('events_limit'))
# format the stack output
stack = get_stack_facts(module, cfn, stack_params['StackName'])
if stack is not None:
if result.get('stack_outputs') is None:
# always define stack_outputs, but it may be empty
result['stack_outputs'] = {}
for output in stack.get('Outputs', []):
result['stack_outputs'][output['OutputKey']] = output['OutputValue']
stack_resources = []
reslist = cfn.list_stack_resources(aws_retry=True, StackName=stack_params['StackName'])
for res in reslist.get('StackResourceSummaries', []):
stack_resources.append({
"logical_resource_id": res['LogicalResourceId'],
"physical_resource_id": res.get('PhysicalResourceId', ''),
"resource_type": res['ResourceType'],
"last_updated_time": res['LastUpdatedTimestamp'],
"status": res['ResourceStatus'],
"status_reason": res.get('ResourceStatusReason') # can be blank, apparently
})
result['stack_resources'] = stack_resources
elif state == 'absent':
# absent state is different because of the way delete_stack works.
# problem is it it doesn't give an error if stack isn't found
# so must describe the stack first
try:
stack = get_stack_facts(module, cfn, stack_params['StackName'])
if not stack:
result = {'changed': False, 'output': 'Stack not found.'}
else:
if stack_params.get('RoleARN') is None:
cfn.delete_stack(aws_retry=True, StackName=stack_params['StackName'])
else:
cfn.delete_stack(aws_retry=True, StackName=stack_params['StackName'], RoleARN=stack_params['RoleARN'])
result = stack_operation(module, cfn, stack_params['StackName'], 'DELETE', module.params.get('events_limit'),
stack_params.get('ClientRequestToken', None))
except (botocore.exceptions.BotoCoreError, botocore.exceptions.ClientError) as err:
module.fail_json_aws(err)
module.exit_json(**result)
if __name__ == '__main__':
main()