ansible/cloud/cloudformation.py

314 lines
10 KiB
Python
Raw Normal View History

#!/usr/bin/python
2013-02-22 21:52:23 +01:00
# 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: "false"
choices: [ "true", "false" ]
2013-02-22 21:52:23 +01:00
aliases: []
template_parameters:
description:
- a list of hashes of all the template variables for the stack
required: false
default: {}
2013-02-22 21:52:23 +01:00
aliases: []
region:
description:
- The AWS region to use. If not specified then the value of the EC2_REGION environment variable, if any, is used.
2013-02-22 21:52:23 +01:00
required: true
default: null
aliases: ['aws_region', 'ec2_region']
2013-02-22 21:52:23 +01:00
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.
2013-04-01 21:15:40 +02:00
If state is absent, stack will be removed.
2013-02-22 21:52:23 +01:00
required: true
default: null
aliases: []
template:
description:
- the path of the cloudformation template
required: true
default: null
aliases: []
tags:
description:
- Dictionary of tags to associate with stack and it's resources during stack creation. Cannot be updated later.
Requires at least Boto version 2.6.0.
required: false
default: null
aliases: []
version_added: "1.4"
aws_secret_key:
description:
- AWS secret key. If not set then the value of the AWS_SECRET_KEY environment variable is used.
required: false
default: null
aliases: [ 'ec2_secret_key', 'secret_key' ]
version_added: "1.5"
aws_access_key:
description:
- AWS access key. If not set then the value of the AWS_ACCESS_KEY environment variable is used.
required: false
default: null
aliases: [ 'ec2_access_key', 'access_key' ]
version_added: "1.5"
region:
description:
- The AWS region to use. If not specified then the value of the EC2_REGION environment variable, if any, is used.
required: false
aliases: ['aws_region', 'ec2_region']
version_added: "1.5"
requirements: [ "boto" ]
author: James S. Martin
'''
2013-02-22 21:52:23 +01:00
EXAMPLES = '''
# Basic task example
tasks:
- name: launch ansible cloudformation example
cloudformation:
stack_name="ansible-cloudformation" state=present
region=us-east-1 disable_rollback=true
template=files/cloudformation-example.json
args:
template_parameters:
KeyName: jmartin
DiskType: ephemeral
InstanceType: m1.small
ClusterSize: 3
tags:
Stack: ansible-cloudformation
2013-02-22 21:52:23 +01:00
'''
import json
import time
2013-02-22 21:52:23 +01:00
try:
import boto
import boto.cloudformation.connection
except ImportError:
print "failed=True msg='boto required for this module'"
sys.exit(1)
2013-02-22 21:52:23 +01:00
class Region:
def __init__(self, region):
'''connects boto to the region specified in the cloudformation template'''
2013-02-22 21:52:23 +01:00
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)
2013-02-22 21:52:23 +01:00
return error
2013-02-22 21:52:23 +01:00
def boto_version_required(version_tuple):
parts = boto.Version.split('.')
boto_version = []
try:
for part in parts:
boto_version.append(int(part))
except:
boto_version.append(-1)
return tuple(boto_version) >= tuple(version_tuple)
2013-02-22 21:52:23 +01:00
def stack_operation(cfn, stack_name, operation):
'''gets the status of a stack while it is created/updated/deleted'''
2013-02-22 21:52:23 +01:00
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())))
2013-02-22 21:52:23 +01:00
else:
2013-07-01 00:50:40 +02:00
result = dict(changed= True, output='Stack Not Found')
2013-02-22 21:52:23 +01:00
break
if '%s_COMPLETE' % operation == stack.stack_status:
result = dict(changed=True,
events = map(str, list(stack.describe_events())),
2013-07-01 00:50:40 +02:00
output = 'Stack %s complete' % operation)
break
if 'ROLLBACK_COMPLETE' == stack.stack_status or '%s_ROLLBACK_COMPLETE' % operation == stack.stack_status:
result = dict(changed=True, failed=True,
events = map(str, list(stack.describe_events())),
2013-07-01 00:50:40 +02:00
output = 'Problem with %s. Rollback complete' % operation)
2013-02-22 21:52:23 +01:00
break
elif '%s_FAILED' % operation == stack.stack_status:
result = dict(changed=True, failed=True,
events = map(str, list(stack.describe_events())),
2013-07-01 00:50:40 +02:00
output = 'Stack %s failed' % operation)
2013-02-22 21:52:23 +01:00
break
else:
time.sleep(5)
return result
def main():
argument_spec = ec2_argument_spec()
argument_spec.update(dict(
2013-02-22 21:52:23 +01:00
stack_name=dict(required=True),
template_parameters=dict(required=False, type='dict', default={}),
2013-02-22 21:52:23 +01:00
state=dict(default='present', choices=['present', 'absent']),
template=dict(default=None, required=True),
disable_rollback=dict(default=False, type='bool'),
tags=dict(default=None)
2013-02-22 21:52:23 +01:00
)
)
module = AnsibleModule(
argument_spec=argument_spec,
)
2013-02-22 21:52:23 +01:00
state = module.params['state']
stack_name = module.params['stack_name']
template_body = open(module.params['template'], 'r').read()
disable_rollback = module.params['disable_rollback']
template_parameters = module.params['template_parameters']
tags = module.params['tags']
2013-02-22 21:52:23 +01:00
ec2_url, aws_access_key, aws_secret_key, region = get_ec2_creds(module)
kwargs = dict()
if tags is not None:
if not boto_version_required((2,6,0)):
module.fail_json(msg='Module parameter "tags" requires at least Boto version 2.6.0')
kwargs['tags'] = tags
# convert the template parameters ansible passes into a tuple for boto
2013-02-22 21:52:23 +01:00
template_parameters_tup = [(k, v) for k, v in template_parameters.items()]
stack_outputs = {}
try:
cf_region = Region(region)
2013-02-22 21:52:23 +01:00
cfn = boto.cloudformation.connection.CloudFormationConnection(
aws_access_key_id=aws_access_key,
aws_secret_access_key=aws_secret_key,
region=cf_region,
)
2013-02-22 21:52:23 +01:00
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
2013-02-22 21:52:23 +01:00
if state == 'present':
try:
cfn.create_stack(stack_name, parameters=template_parameters_tup,
template_body=template_body,
disable_rollback=disable_rollback,
capabilities=['CAPABILITY_IAM'],
**kwargs)
2013-02-22 21:52:23 +01:00
operation = 'CREATE'
except Exception, err:
error_msg = boto_exception(err)
if 'AlreadyExistsException' in error_msg or 'already exists' in error_msg:
2013-02-22 21:52:23 +01:00
update = True
else:
module.fail_json(msg=error_msg)
2013-02-22 21:52:23 +01:00
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.
2013-02-22 21:52:23 +01:00
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)
2013-02-22 21:52:23 +01:00
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
2013-02-22 21:52:23 +01:00
if state == 'present' or update:
stack = cfn.describe_stacks(stack_name)[0]
for output in stack.outputs:
stack_outputs[output.key] = output.value
2013-02-22 21:52:23 +01:00
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
2013-02-22 21:52:23 +01:00
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)
2013-02-22 21:52:23 +01:00
if operation == 'DELETE':
cfn.delete_stack(stack_name)
result = stack_operation(cfn, stack_name, operation)
module.exit_json(**result)
2013-12-02 21:13:49 +01:00
# import module snippets
from ansible.module_utils.basic import *
from ansible.module_utils.ec2 import *
2013-02-22 21:52:23 +01:00
main()