From dd07d11ae51755d5f383c34ebb5378611d9aa18c Mon Sep 17 00:00:00 2001 From: Wouter de Geus Date: Wed, 21 Jun 2017 22:05:17 +0200 Subject: [PATCH] [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 --- .../modules/cloud/amazon/cloudformation.py | 64 +++++++++++++++++++ 1 file changed, 64 insertions(+) diff --git a/lib/ansible/modules/cloud/amazon/cloudformation.py b/lib/ansible/modules/cloud/amazon/cloudformation.py index e6321ea033..88897337c5 100644 --- a/lib/ansible/modules/cloud/amazon/cloudformation.py +++ b/lib/ansible/modules/cloud/amazon/cloudformation.py @@ -93,6 +93,23 @@ options: present, the stack does exist, and neither 'template' nor 'template_url' are specified, the previous template will be reused. required: false 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: description: - (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 from ansible.module_utils.ec2 import AWSRetry from ansible.module_utils.basic import AnsibleModule +from ansible.utils.hashing import secure_hash_s import ansible.module_utils.ec2 def boto_exception(err): @@ -297,6 +315,45 @@ def create_stack(module, stack_params, cfn): 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): if 'TemplateBody' not in stack_params and 'TemplateURL' not in stack_params: stack_params['UsePreviousTemplate'] = True @@ -402,6 +459,8 @@ def main(): disable_rollback=dict(default=False, type='bool'), template_url=dict(default=None, 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), tags=dict(default=None, type='dict') ) @@ -434,6 +493,9 @@ def main(): if module.params['stack_policy'] is not None: 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'] 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 not stack_info: result = create_stack(module, stack_params, cfn) + elif create_changeset: + result = create_changeset(module, stack_params, cfn) else: result = update_stack(module, stack_params, cfn)