@ -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.
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
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 )