diff --git a/lib/ansible/modules/cloud/amazon/cloudformation.py b/lib/ansible/modules/cloud/amazon/cloudformation.py index ac399c2c52e..156207acadb 100644 --- a/lib/ansible/modules/cloud/amazon/cloudformation.py +++ b/lib/ansible/modules/cloud/amazon/cloudformation.py @@ -250,7 +250,6 @@ from ansible.module_utils.basic import AnsibleModule from ansible.module_utils._text import to_bytes - def get_stack_events(cfn, stack_name): '''This event data was never correct, it worked as a side effect. So the v2.3 format is different.''' ret = {'events':[], 'log':[]} @@ -297,36 +296,28 @@ def create_stack(module, stack_params, cfn): 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 + return [cs['ChangeSetName'] for cs in res['Summaries']] + 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. - json_params = json.dumps(stack_params, sort_keys=True) - - changeset_name = 'Ansible-' + stack_params['StackName'] + '-' + sha1(to_bytes(json_params, errors='surrogate_or_strict')).hexdigest() - stack_params['ChangeSetName'] = changeset_name - else: - changeset_name = stack_params['ChangeSetName'] + 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: '+str(len(pending_changesets))+' pending changeset(s) exist(s) for this stack!' - result = dict(changed=False, output='ChangeSet ' + changeset_name + ' already exists.', warnings=[warning]) + 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(**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!')] + 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 Exception as err: error_msg = boto_exception(err) if 'No updates are to be performed.' in error_msg: @@ -407,11 +398,49 @@ def stack_operation(cfn, stack_name, operation): 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) + try: + change_set = cfn.create_change_set(**stack_params) + for i in range(60): # total time 5 min + description = cfn.describe_change_set(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(ChangeSetName=change_set['Id']) + + reason = description.get('StatusReason') + + if description['Status'] == 'FAILED' and "didn't contain changes" in description['StatusReason']: + return {'changed': False, 'msg': reason, 'meta': description['StatusReason']} + return {'changed': True, 'msg': reason, 'meta': description['Changes']} + + except (botocore.exceptions.ValidationError, botocore.exceptions.ClientError) as err: + error_msg = boto_exception(err) + module.fail_json(msg=error_msg, exception=traceback.format_exc()) + + def get_stack_facts(cfn, stack_name): try: stack_response = cfn.describe_stacks(StackName=stack_name) stack_info = stack_response['Stacks'][0] - #except AmazonCloudFormationException as e: except (botocore.exceptions.ValidationError,botocore.exceptions.ClientError) as err: error_msg = boto_exception(err) if 'does not exist' in error_msg: @@ -451,6 +480,7 @@ def main(): module = AnsibleModule( argument_spec=argument_spec, mutually_exclusive=[['template_url', 'template']], + supports_check_mode=True ) if not HAS_BOTO3: module.fail_json(msg='boto3 and botocore are required for this module') @@ -509,6 +539,16 @@ def main(): stack_info = get_stack_facts(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)