bbd8205da2
When stack creation is rolled back, final status is not CREATE_ROLLBACK_COMPLETE but ROLLBACK_COMPLETE.
269 lines
8.9 KiB
Python
269 lines
8.9 KiB
Python
#!/usr/bin/python
|
|
# This file is part of Ansible
|
|
#
|
|
# Ansible is free software: you can redistribute it and/or modify
|
|
# it under the terms of the GNU General Public License as published by
|
|
# the Free Software Foundation, either version 3 of the License, or
|
|
# (at your option) any later version.
|
|
#
|
|
# Ansible is distributed in the hope that it will be useful,
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
# GNU General Public License for more details.
|
|
#
|
|
# You should have received a copy of the GNU General Public License
|
|
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
DOCUMENTATION = '''
|
|
---
|
|
module: cloudformation
|
|
short_description: create a AWS CloudFormation stack
|
|
description:
|
|
- Launches an AWS CloudFormation stack and waits for it complete.
|
|
version_added: "1.1"
|
|
options:
|
|
stack_name:
|
|
description:
|
|
- name of the cloudformation stack
|
|
required: true
|
|
default: null
|
|
aliases: []
|
|
disable_rollback:
|
|
description:
|
|
- If a stacks fails to form, rollback will remove the stack
|
|
required: false
|
|
default: "no"
|
|
choices: [ "yes", "no" ]
|
|
aliases: []
|
|
template_parameters:
|
|
description:
|
|
- a list of hashes of all the template variables for the stack
|
|
required: true
|
|
default: null
|
|
aliases: []
|
|
region:
|
|
description:
|
|
- The AWS region to use. If not specified then the value of the EC2_REGION environment variable, if any, is used.
|
|
required: true
|
|
default: null
|
|
aliases: ['aws_region', 'ec2_region']
|
|
state:
|
|
description:
|
|
- If state is "present", stack will be created. If state is "present" and if stack exists and template has changed, it will be updated.
|
|
If state is absent, stack will be removed.
|
|
required: true
|
|
default: null
|
|
aliases: []
|
|
template:
|
|
description:
|
|
- the path of the cloudformation template
|
|
required: true
|
|
default: null
|
|
aliases: []
|
|
|
|
requirements: [ "boto" ]
|
|
author: James S. Martin
|
|
'''
|
|
|
|
EXAMPLES = '''
|
|
# Basic task example
|
|
tasks:
|
|
- name: launch ansible cloudformation example
|
|
action: cloudformation >
|
|
stack_name="ansible-cloudformation" state=present
|
|
region=us-east-1 disable_rollback=yes
|
|
template=files/cloudformation-example.json
|
|
args:
|
|
template_parameters:
|
|
KeyName: jmartin
|
|
DiskType: ephemeral
|
|
InstanceType: m1.small
|
|
ClusterSize: 3
|
|
'''
|
|
|
|
import json
|
|
import time
|
|
|
|
try:
|
|
import boto.cloudformation.connection
|
|
except ImportError:
|
|
print "failed=True msg='boto required for this module'"
|
|
sys.exit(1)
|
|
|
|
AWS_REGIONS = ['ap-northeast-1',
|
|
'ap-southeast-1',
|
|
'ap-southeast-2',
|
|
'eu-west-1',
|
|
'sa-east-1',
|
|
'us-east-1',
|
|
'us-west-1',
|
|
'us-west-2']
|
|
|
|
class Region:
|
|
def __init__(self, region):
|
|
'''connects boto to the region specified in the cloudformation template'''
|
|
self.name = region
|
|
self.endpoint = 'cloudformation.%s.amazonaws.com' % region
|
|
|
|
|
|
def boto_exception(err):
|
|
'''generic error message handler'''
|
|
if hasattr(err, 'error_message'):
|
|
error = err.error_message
|
|
elif hasattr(err, 'message'):
|
|
error = err.message
|
|
else:
|
|
error = '%s: %s' % (Exception, err)
|
|
|
|
return error
|
|
|
|
|
|
def stack_operation(cfn, stack_name, operation):
|
|
'''gets the status of a stack while it is created/updated/deleted'''
|
|
existed = []
|
|
result = {}
|
|
operation_complete = False
|
|
while operation_complete == False:
|
|
try:
|
|
stack = cfn.describe_stacks(stack_name)[0]
|
|
existed.append('yes')
|
|
except:
|
|
if 'yes' in existed:
|
|
result = dict(changed=True,
|
|
output='Stack Deleted',
|
|
events=map(str, list(stack.describe_events())))
|
|
else:
|
|
result = dict(changed= True, output='Stack Not Found')
|
|
break
|
|
if '%s_COMPLETE' % operation == stack.stack_status:
|
|
result = dict(changed=True,
|
|
events = map(str, list(stack.describe_events())),
|
|
output = 'Stack %s complete' % operation)
|
|
break
|
|
if 'ROLLBACK_COMPLETE' == stack.stack_status or '%s_ROLLBACK_COMPLETE' % operation == stack.stack_status:
|
|
result = dict(changed=True,
|
|
events = map(str, list(stack.describe_events())),
|
|
output = 'Problem with %s. Rollback complete' % operation)
|
|
break
|
|
elif '%s_FAILED' % operation == stack.stack_status:
|
|
result = dict(changed=False,
|
|
events = map(str, list(stack.describe_events())),
|
|
output = 'Stack %s failed' % operation)
|
|
break
|
|
else:
|
|
time.sleep(5)
|
|
return result
|
|
|
|
|
|
def main():
|
|
module = AnsibleModule(
|
|
argument_spec=dict(
|
|
stack_name=dict(required=True),
|
|
template_parameters=dict(required=True),
|
|
region=dict(aliases=['aws_region', 'ec2_region'], required=True, choices=AWS_REGIONS),
|
|
state=dict(default='present', choices=['present', 'absent']),
|
|
template=dict(default=None, required=True),
|
|
disable_rollback=dict(default=False)
|
|
)
|
|
)
|
|
|
|
state = module.params['state']
|
|
stack_name = module.params['stack_name']
|
|
r = module.params['region']
|
|
template_body = open(module.params['template'], 'r').read()
|
|
disable_rollback = module.params['disable_rollback']
|
|
template_parameters = module.params['template_parameters']
|
|
|
|
if not r:
|
|
if 'AWS_REGION' in os.environ:
|
|
r = os.environ['AWS_REGION']
|
|
elif 'EC2_REGION' in os.environ:
|
|
r = os.environ['EC2_REGION']
|
|
|
|
|
|
|
|
# convert the template parameters ansible passes into a tuple for boto
|
|
template_parameters_tup = [(k, v) for k, v in template_parameters.items()]
|
|
stack_outputs = {}
|
|
|
|
try:
|
|
region = Region(r)
|
|
cfn = boto.cloudformation.connection.CloudFormationConnection(
|
|
region=region)
|
|
except boto.exception.NoAuthHandlerFound, e:
|
|
module.fail_json(msg=str(e))
|
|
update = False
|
|
result = {}
|
|
operation = None
|
|
|
|
# if state is present we are going to ensure that the stack is either
|
|
# created or updated
|
|
if state == 'present':
|
|
try:
|
|
cfn.create_stack(stack_name, parameters=template_parameters_tup,
|
|
template_body=template_body,
|
|
disable_rollback=disable_rollback,
|
|
capabilities=['CAPABILITY_IAM'])
|
|
operation = 'CREATE'
|
|
except Exception, err:
|
|
error_msg = boto_exception(err)
|
|
if 'AlreadyExistsException' in error_msg:
|
|
update = True
|
|
else:
|
|
module.fail_json(msg=error_msg)
|
|
if not update:
|
|
result = stack_operation(cfn, stack_name, operation)
|
|
|
|
# if the state is present and the stack already exists, we try to update it
|
|
# AWS will tell us if the stack template and parameters are the same and
|
|
# don't need to be updated.
|
|
if update:
|
|
try:
|
|
cfn.update_stack(stack_name, parameters=template_parameters_tup,
|
|
template_body=template_body,
|
|
disable_rollback=disable_rollback,
|
|
capabilities=['CAPABILITY_IAM'])
|
|
operation = 'UPDATE'
|
|
except Exception, 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 operation == 'UPDATE':
|
|
result = stack_operation(cfn, stack_name, operation)
|
|
|
|
# check the status of the stack while we are creating/updating it.
|
|
# and get the outputs of the stack
|
|
|
|
if state == 'present' or update:
|
|
stack = cfn.describe_stacks(stack_name)[0]
|
|
for output in stack.outputs:
|
|
stack_outputs[output.key] = output.value
|
|
result['stack_outputs'] = stack_outputs
|
|
|
|
# absent state is different because of the way delete_stack works.
|
|
# problem is it it doesn't give an error if stack isn't found
|
|
# so must describe the stack first
|
|
|
|
if state == 'absent':
|
|
try:
|
|
cfn.describe_stacks(stack_name)
|
|
operation = 'DELETE'
|
|
except Exception, err:
|
|
error_msg = boto_exception(err)
|
|
if 'Stack:%s does not exist' % stack_name in error_msg:
|
|
result = dict(changed=False, output='Stack not found.')
|
|
else:
|
|
module.fail_json(msg=error_msg)
|
|
if operation == 'DELETE':
|
|
cfn.delete_stack(stack_name)
|
|
result = stack_operation(cfn, stack_name, operation)
|
|
|
|
module.exit_json(**result)
|
|
|
|
# this is magic, see lib/ansible/module_common.py
|
|
#<<INCLUDE_ANSIBLE_MODULE_COMMON>>
|
|
|
|
main()
|