[cloud] Add ChangeSet support to cloudformation module (#23490) (#24497)

*  * Implements Change Sets on updating a cloudformation stack when create_changeset=true (#23490)

*  * Silence test complaints ;)

*  * Added optional changeset_name parameter.
 * Check if changeset with the requested name already exist.
 * Documentation fix

*  * Added warning when cloudformation stack has pending changesets.
 * Fix documentation
This commit is contained in:
Wouter de Geus 2017-06-21 22:05:17 +02:00 committed by Ryan Brown
parent 7bed60ba5f
commit dd07d11ae5

View file

@ -93,6 +93,23 @@ options:
present, the stack does exist, and neither 'template' nor 'template_url' are specified, the previous template will be reused. present, the stack does exist, and neither 'template' nor 'template_url' are specified, the previous template will be reused.
required: false required: false
version_added: "2.0" version_added: "2.0"
create_changeset:
description:
- "If stack already exists create a changeset instead of directly applying changes.
See the AWS Change Sets docs U(http://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 the state is absent, the stack will be deleted immediately with no
changeset."
required: false
default: false
version_added: "2.4"
changeset_name:
description:
- Name given to the changeset when creating a changeset, only used when create_changeset is true. By default a name prefixed with Ansible-STACKNAME
is generated based on input parameters.
See the AWS Change Sets docs U(http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-changesets.html)
required: false
default: null
version_added: "2.4"
template_format: template_format:
description: description:
- (deprecated) For local templates, allows specification of json or yaml format. Templates are now passed raw to CloudFormation regardless of format. - (deprecated) For local templates, allows specification of json or yaml format. Templates are now passed raw to CloudFormation regardless of format.
@ -239,6 +256,7 @@ except ImportError:
# import a class, otherwise we'll use a fully qualified path # import a class, otherwise we'll use a fully qualified path
from ansible.module_utils.ec2 import AWSRetry from ansible.module_utils.ec2 import AWSRetry
from ansible.module_utils.basic import AnsibleModule from ansible.module_utils.basic import AnsibleModule
from ansible.utils.hashing import secure_hash_s
import ansible.module_utils.ec2 import ansible.module_utils.ec2
def boto_exception(err): def boto_exception(err):
@ -297,6 +315,45 @@ def create_stack(module, stack_params, cfn):
return result return result
def list_changesets(cfn, stack_name):
res = cfn.list_change_sets(StackName=stack_name)
changesets = []
for cs in res['Summaries']:
changesets.append(cs['ChangeSetName'])
return changesets
def create_changeset(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.")
try:
if not 'ChangeSetName' in stack_params:
# Determine ChangeSetName using hash of parameters.
changeset_name = 'Ansible-' + stack_params['StackName'] + '-' + secure_hash_s(json.dumps(stack_params, sort_keys=True))
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: '+str(len(pending_changesets))+' pending changeset(s) exist(s) for this stack!'
result = dict(changed=False, output='ChangeSet ' + changeset_name + ' already exists.', warnings=[warning])
else:
cs = cfn.create_change_set(**stack_params)
result = stack_operation(cfn, stack_params['StackName'], 'UPDATE')
result['warnings'] = [('Created changeset named ' + changeset_name + ' for stack ' + stack_params['StackName']),
('You can execute it using: aws cloudformation execute-change-set --change-set-name ' + cs['Id']),
('NOTE that dependencies on this stack might fail due to pending changes!')]
except Exception as err:
error_msg = boto_exception(err)
if 'No updates are to be performed.' in error_msg:
result = dict(changed=False, output='Stack is already up-to-date.')
else:
module.fail_json(msg=error_msg)
if not result:
module.fail_json(msg="empty result")
return result
def update_stack(module, stack_params, cfn): def update_stack(module, stack_params, cfn):
if 'TemplateBody' not in stack_params and 'TemplateURL' not in stack_params: if 'TemplateBody' not in stack_params and 'TemplateURL' not in stack_params:
stack_params['UsePreviousTemplate'] = True stack_params['UsePreviousTemplate'] = True
@ -402,6 +459,8 @@ def main():
disable_rollback=dict(default=False, type='bool'), disable_rollback=dict(default=False, type='bool'),
template_url=dict(default=None, required=False), template_url=dict(default=None, required=False),
template_format=dict(default=None, choices=['json', 'yaml'], required=False), template_format=dict(default=None, choices=['json', 'yaml'], required=False),
create_changeset=dict(default=False, type='bool'),
changeset_name=dict(default=None, required=False),
role_arn=dict(default=None, required=False), role_arn=dict(default=None, required=False),
tags=dict(default=None, type='dict') tags=dict(default=None, type='dict')
) )
@ -434,6 +493,9 @@ def main():
if module.params['stack_policy'] is not None: if module.params['stack_policy'] is not None:
stack_params['StackPolicyBody'] = open(module.params['stack_policy'], 'r').read() stack_params['StackPolicyBody'] = open(module.params['stack_policy'], 'r').read()
if module.params['changeset_name'] is not None:
stack_params['ChangeSetName'] = module.params['changeset_name']
template_parameters = module.params['template_parameters'] template_parameters = module.params['template_parameters']
stack_params['Parameters'] = [{'ParameterKey':k, 'ParameterValue':str(v)} for k, v in template_parameters.items()] stack_params['Parameters'] = [{'ParameterKey':k, 'ParameterValue':str(v)} for k, v in template_parameters.items()]
@ -456,6 +518,8 @@ def main():
if state == 'present': if state == 'present':
if not stack_info: if not stack_info:
result = create_stack(module, stack_params, cfn) result = create_stack(module, stack_params, cfn)
elif create_changeset:
result = create_changeset(module, stack_params, cfn)
else: else:
result = update_stack(module, stack_params, cfn) result = update_stack(module, stack_params, cfn)