mirror of
https://github.com/ansible-collections/community.general.git
synced 2024-09-14 20:13:21 +02:00
[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
|
from ansible.module_utils._text import to_bytes
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def get_stack_events(cfn, stack_name):
|
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.'''
|
'''This event data was never correct, it worked as a side effect. So the v2.3 format is different.'''
|
||||||
ret = {'events':[], 'log':[]}
|
ret = {'events':[], 'log':[]}
|
||||||
|
@ -297,36 +296,28 @@ def create_stack(module, stack_params, cfn):
|
||||||
|
|
||||||
def list_changesets(cfn, stack_name):
|
def list_changesets(cfn, stack_name):
|
||||||
res = cfn.list_change_sets(StackName=stack_name)
|
res = cfn.list_change_sets(StackName=stack_name)
|
||||||
changesets = []
|
return [cs['ChangeSetName'] for cs in res['Summaries']]
|
||||||
for cs in res['Summaries']:
|
|
||||||
changesets.append(cs['ChangeSetName'])
|
|
||||||
return changesets
|
|
||||||
|
|
||||||
def create_changeset(module, stack_params, cfn):
|
def create_changeset(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:
|
||||||
module.fail_json(msg="Either 'template' or 'template_url' is required.")
|
module.fail_json(msg="Either 'template' or 'template_url' is required.")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if not 'ChangeSetName' in stack_params:
|
changeset_name = build_changeset_name(stack_params)
|
||||||
# Determine ChangeSetName using hash of parameters.
|
stack_params['ChangeSetName'] = changeset_name
|
||||||
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']
|
|
||||||
|
|
||||||
# Determine if this changeset already exists
|
# Determine if this changeset already exists
|
||||||
pending_changesets = list_changesets(cfn, stack_params['StackName'])
|
pending_changesets = list_changesets(cfn, stack_params['StackName'])
|
||||||
if changeset_name in pending_changesets:
|
if changeset_name in pending_changesets:
|
||||||
warning = 'WARNING: '+str(len(pending_changesets))+' pending changeset(s) exist(s) for this stack!'
|
warning = 'WARNING: %d pending changeset(s) exist(s) for this stack!' % len(pending_changesets)
|
||||||
result = dict(changed=False, output='ChangeSet ' + changeset_name + ' already exists.', warnings=[warning])
|
result = dict(changed=False, output='ChangeSet %s already exists.' % changeset_name, warnings=[warning])
|
||||||
else:
|
else:
|
||||||
cs = cfn.create_change_set(**stack_params)
|
cs = cfn.create_change_set(**stack_params)
|
||||||
result = stack_operation(cfn, stack_params['StackName'], 'UPDATE')
|
result = stack_operation(cfn, stack_params['StackName'], 'UPDATE')
|
||||||
result['warnings'] = [('Created changeset named ' + changeset_name + ' for stack ' + stack_params['StackName']),
|
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 ' + cs['Id']),
|
'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!')]
|
'NOTE that dependencies on this stack might fail due to pending changes!']
|
||||||
except Exception as err:
|
except Exception as err:
|
||||||
error_msg = boto_exception(err)
|
error_msg = boto_exception(err)
|
||||||
if 'No updates are to be performed.' in error_msg:
|
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.'}
|
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):
|
def get_stack_facts(cfn, stack_name):
|
||||||
try:
|
try:
|
||||||
stack_response = cfn.describe_stacks(StackName=stack_name)
|
stack_response = cfn.describe_stacks(StackName=stack_name)
|
||||||
stack_info = stack_response['Stacks'][0]
|
stack_info = stack_response['Stacks'][0]
|
||||||
#except AmazonCloudFormationException as e:
|
|
||||||
except (botocore.exceptions.ValidationError,botocore.exceptions.ClientError) as err:
|
except (botocore.exceptions.ValidationError,botocore.exceptions.ClientError) as err:
|
||||||
error_msg = boto_exception(err)
|
error_msg = boto_exception(err)
|
||||||
if 'does not exist' in error_msg:
|
if 'does not exist' in error_msg:
|
||||||
|
@ -451,6 +480,7 @@ def main():
|
||||||
module = AnsibleModule(
|
module = AnsibleModule(
|
||||||
argument_spec=argument_spec,
|
argument_spec=argument_spec,
|
||||||
mutually_exclusive=[['template_url', 'template']],
|
mutually_exclusive=[['template_url', 'template']],
|
||||||
|
supports_check_mode=True
|
||||||
)
|
)
|
||||||
if not HAS_BOTO3:
|
if not HAS_BOTO3:
|
||||||
module.fail_json(msg='boto3 and botocore are required for this module')
|
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'])
|
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 state == 'present':
|
||||||
if not stack_info:
|
if not stack_info:
|
||||||
result = create_stack(module, stack_params, cfn)
|
result = create_stack(module, stack_params, cfn)
|
||||||
|
|
Loading…
Add table
Reference in a new issue