[cloud] Add check mode for cloudformation module (#23483)
* cloudformation: dummy check mode * cloudformation: use changesets to implement check mode * cloudformation: wait at most 5min for change set * cloudformation: handle stack creation and deletion in check mode * cloudformation: standardize output format in check mode msg is a string, meta is a list * cloudformation: use same naming convention in get_changeset as create_changeset also add comment about code duplication between said functions * Remove unused imports * PEP8 whitespace fix * Fix CI, convert success=True check to for/else
This commit is contained in:
parent
cf1a9d4d22
commit
fc7301671e
1 changed files with 59 additions and 19 deletions
|
@ -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)
|
||||
|
|
Loading…
Reference in a new issue