From be871fe774dc16a9ce29b958add9e11eeff509f2 Mon Sep 17 00:00:00 2001 From: James Martin Date: Fri, 22 Feb 2013 15:52:23 -0500 Subject: [PATCH] CloudFormation support. --- cloudformation | 243 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 243 insertions(+) create mode 100644 cloudformation diff --git a/cloudformation b/cloudformation new file mode 100644 index 00000000000..5ab2d1a93ec --- /dev/null +++ b/cloudformation @@ -0,0 +1,243 @@ +#!/usr/bin/python -tt +# 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 . + +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 + 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 the stack will be launched in + required: true + default: null + aliases: [] + 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 present, stack will be removed. + required: true + default: null + aliases: [] + template: + description: + - the path of the cloudformation template + required: true + default: null + aliases: [] + wait_for: + description: + - Wait while the stack is being created/updated/deleted. + required: false + default: true + aliases: [] + +examples: + + 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 + +requirements: [ "boto" ] +author: James S. Martin +''' + +import boto.cloudformation.connection +import json + + +class Region: + def __init__(self, region): + self.name = region + self.endpoint = 'cloudformation.%s.amazonaws.com' % region + + +def boto_exception(err): + + if hasattr(err, 'error_message'): + error = err.error_message + elif hasattr(err, 'message'): + error = err.message + else: + error = '%s: %s' % (Exception, err) + try: + error_msg = json.loads(error) + except: + error_msg = {'Error': error} + return error_msg + + +def stack_operation(cfn, stack_name, operation): + 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 = {'changed': True, 'output': 'Stack Deleted'} + result['events'] = map(str, list(stack.describe_events())) + else: + result = {'changed': True, 'output': 'Stack Not Found'} + break + if '%s_COMPLETE' % operation == stack.stack_status: + result['changed'] = True + result['events'] = map(str, list(stack.describe_events())) + result['output'] = 'Stack %s complete' % operation + break + elif '%s_FAILED' % operation == stack.stack_status: + result['changed'] = False + result['events'] = map(str, list(stack.describe_events())) + result['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=False), + region=dict(required=True, + choices=['ap-northeast-1', 'ap-southeast-1', + 'ap-southeast-2', 'eu-west-1', + 'sa-east-1', 'us-east-1', 'us-west-1', + 'us-west-2']), + state=dict(default='present', choices=['present', 'absent']), + template=dict(default=None, required=True), + disable_rollback=dict(default=False), + wait_for=dict(default=True) + ) + ) + + wait_for = module.params['wait_for'] + state = module.params['state'] + stack_name = module.params['stack_name'] + region = Region(module.params['region']) + template_body = open(module.params['template'], 'r').read() + disable_rollback = module.params['disable_rollback'] + template_parameters = module.params['template_parameters'] + + template_parameters_tup = [(k, v) for k, v in template_parameters.items()] + stack_outputs = {} + stack_outputs[module.params['region']] = {} + stack_outputs[module.params['region']][stack_name] = {} + + try: + cfn = boto.cloudformation.connection.CloudFormationConnection( + region=region) + except boto.exception.NoAuthHandlerFound, e: + module.fail_json(msg=str(e)) + update = False + stack_events = [] + result = {} + operation = None + output = '' + + 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 error_msg['Error']['Code'] == 'AlreadyExistsException': + update = True + else: + result = {'changed': False, 'output': error_msg} + module.fail_json(**result) + if not update: + result = stack_operation(cfn, stack_name, operation) + 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 error_msg['Error']['Message'] == 'No updates are to be performed.': + output = error_msg['Error']['Message'] + result = {'changed': False, 'output': output} + if operation == 'UPDATE': + result = stack_operation(cfn, stack_name, operation) + + if state == 'present' or update: + stack = cfn.describe_stacks(stack_name)[0] + for output in stack.outputs: + stack_outputs[module.params['region']][stack_name][ + 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) + result = {'changed': False, 'output': error_msg} + module.fail_json(result) + 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 +#<> + +main()