1. Pior Bastida
  2. awstools

Source

awstools / scripts / cfn

#!/usr/bin/env python
# -*- coding: utf-8 -*-
# Copyright (C) 2012 Ludia Inc.
# This software is licensed as described in the file LICENSE, which
# you should have received as part of this distribution.
# Author: Pior Bastida <pbastida@socialludia.com>

import os
import pkg_resources

from argh import arg, alias, ArghParser, confirm, wrap_errors
from argh.exceptions import CommandError
import boto
from boto.exception import BotoServerError

import awstools
from awstools import find_stacks, find_one_stack, find_one_resource
from awstools.display import (format_stack_summary,
                              format_stack_summary_short,
                              format_stack_events)
from awstools.application import Applications
from awstools import cfntemplate


HELP_SN = "the name of the stack like tt-python-production"
HELP_TMPL = "force a different template file"
HELP_CFG = "path of an alternative configuration file"
HELP_SETTINGS = "path of the application settings configuration file"
HELP_CAP = "AutoScale desired capacity"
HELP_LIMITS = "Specify the MIN:MAX parameters (eg: 10:20 or :2)"


def initialize_from_cli(args):
    """
    Read the configuration and settings file and lookup for a stack_info"""

    config = awstools.read_config(args.config)

    if args.settings:
        settings = args.settings
    else:
        settings = config.get("cfn", "settings")

    settings_data = open(os.path.expanduser(settings), 'rb')

    if hasattr(args, 'stack_name'):
        apps = Applications(settings_data)
        app = apps.get(stackname=args.stack_name)
        sinfo = app.get_stack_info_from_stackname(args.stack_name)
    else:
        sinfo = None

    return config, settings, sinfo


def warn_for_live(sinfo):
    if sinfo['live'] and sinfo['Environment'] == 'production':
        if not confirm("WARNING: Updating a live stack! Are you sure? "):
            raise CommandError("Aborted")


@arg('-a', '--all', default=False)
@arg('stack_name', nargs='?', default='')
@alias('list')
def ls(args):
    stacks = find_stacks(args.stack_name, findall=args.all)
    for stack in stacks:
        yield format_stack_summary_short(stack)


@arg('stack_name', help=HELP_SN)
@arg('--template', help=HELP_TMPL)
def create(args):
    config, settings, sinfo = initialize_from_cli(args)

    # Read template
    template_path = os.path.join(
        config.get("cfn", "templatedir"),
        args.template if args.template else sinfo['template'])
    template = cfntemplate.CfnTemplate(template_path)
    parameters = cfntemplate.CfnParameters(template, sinfo)

    print("\nStack name: {args.stack_name}\n"
          "\nTemplate: {template!r}\n"
          "\nParameters:\n"
          "{parameters!r}\n".format(args=args,
                                    template=template,
                                    parameters=parameters))

    if not confirm('Confirm this creation? ', default=True):
        raise CommandError("Aborted")

    try:
        stackid = boto.connect_cloudformation().create_stack(
            args.stack_name,
            template_body=template.body,
            parameters=parameters,
            capabilities=['CAPABILITY_IAM'])
        print("StackId %s" % stackid)
    except BotoServerError as e:
        if e.error_message:
            raise CommandError("BotoServerError: " + e.error_message)
        else:
            raise e


@arg('stack_name', help=HELP_SN)
@arg('--template', help=HELP_TMPL)
def update(args):
    config, settings, sinfo = initialize_from_cli(args)

    # Read template
    template = cfntemplate.CfnTemplate(
        os.path.join(
            config.get("cfn", "templatedir"),
            args.template if args.template else sinfo['template']
        )
    )

    parameters = cfntemplate.CfnParameters(template, sinfo)

    print("\nStack name: {args.stack_name}\n"
          "\nTemplate: {template!r}\n"
          "\nParameters:\n"
          "{parameters!r}\n".format(args=args,
                                    template=template,
                                    parameters=parameters))
    warn_for_live(sinfo)

    if not confirm('Confirm the update? ', default=True):
        raise CommandError("Aborted")

    try:
        stackid = boto.connect_cloudformation().update_stack(
            args.stack_name,
            template_body=template.body,
            parameters=parameters,
            capabilities=['CAPABILITY_IAM'])
        print("StackId %s" % stackid)
    except BotoServerError as e:
        if e.error_message:
            raise CommandError("BotoServerError: " + e.error_message)
        else:
            raise e


@arg('stack_name', help=HELP_SN)
def delete(args):
    config, settings, sinfo = initialize_from_cli(args)

    stack = find_one_stack(args.stack_name)

    print(format_stack_summary(stack))

    warn_for_live(sinfo)

    if not confirm('Confirm the deletion? ', default=True):
        raise CommandError("Aborted")

    try:
        res = boto.connect_cloudformation().delete_stack(stack.stack_name)
    except BotoServerError as e:
        if e:
            raise CommandError("BotoServerError: " + e.error_message)
        else:
            raise e
    print("Result %s" % res)


@arg('stack_name', help=HELP_SN)
@wrap_errors(ValueError)
def info(args):
    stack = find_one_stack(args.stack_name, summary=False)

    yield format_stack_summary(stack) + '\n'

    for param in stack.parameters:
        yield str(param)
    yield ''

    for output in stack.outputs:
        yield str(output)
    yield ''

    yield format_stack_events(stack, limit=10) + '\n'

    for resource in stack.describe_resources():
        yield "{r}\n  {r.resource_status} {r.physical_resource_id}".format(r=resource)
    yield ''


@arg('stack_name', help=HELP_SN)
@wrap_errors(ValueError)
def outputs(args):
    stack = find_one_stack(args.stack_name, summary=False)

    yield format_stack_summary(stack) + '\n'

    for output in stack.outputs:
        yield str(output)


@arg('stack_name', help=HELP_SN)
@wrap_errors(ValueError)
def resources(args):
    stack = find_one_stack(args.stack_name, summary=False)

    yield format_stack_summary(stack) + '\n'

    tmpl = "  ".join([
                     "{r.logical_resource_id:<24}",
                     "{r.physical_resource_id:<60}",
                     "[{r.resource_status}] {r.resource_type}"
                     ])

    for resource in stack.describe_resources():
        yield tmpl.format(r=resource)
    yield ''


@arg('stack_name', help=HELP_SN)
@wrap_errors(ValueError)
def events(args):
    stack = find_one_stack(args.stack_name, summary=False)
    yield format_stack_summary(stack) + '\n'
    yield format_stack_events(stack) + '\n'


def activities(args):
    stacks = find_stacks(None, findall=True)
    for stack in stacks:
        if stack.stack_status.endswith('_COMPLETE'):
            continue
        yield format_stack_summary_short(stack)


def update_asg_capacity(asg, desired=None, minlimit=None, maxlimit=None):
    print("Current limits: %s:%s" % (asg.min_size, asg.max_size))
    print("Current desired capacity %s" % desired)

    changed = False

    if not minlimit is None:
        if asg.min_size != minlimit:
            print("Updating min_size (set to %s)" % minlimit)
            asg.min_size = minlimit
            changed = True

    if not maxlimit is None:
        if asg.max_size != maxlimit:
            print("Updating max_size (set to %s)" % maxlimit)
            asg.max_size = maxlimit
            changed = True

    if not desired is None:
        if asg.desired_capacity != desired:
            print("Updating desired_capacity to %s" % desired)
            asg.desired_capacity = desired
            changed = True

    if changed:
        asg.update()

    print("Success")


@arg('stack_name', help=HELP_SN)
@arg('limits', help=HELP_LIMITS)
@wrap_errors([ValueError, BotoServerError])
def setlimit(args):
    """
    Change the min and/or max parameters of the AutoScale in a stack
    """
    config, settings, sinfo = initialize_from_cli(args)
    stack = find_one_stack(args.stack_name, summary=False)
    print(format_stack_summary(stack))

    warn_for_live(sinfo)

    asg = find_one_resource(stack, 'AWS::AutoScaling::AutoScalingGroup')
    print("AutoScale ID: %s" % asg.name)

    try:
        limits = args.limits.split(':')
        minlimit = int(limits[0]) if limits[0] else None
        maxlimit = int(limits[1]) if limits[1] else None
    except:
        raise ValueError("Invalid limit format ")

    update_asg_capacity(
        asg,
        minlimit=minlimit,
        maxlimit=maxlimit,
    )


@arg('stack_name', help=HELP_SN)
@arg('capacity', type=int, help=HELP_CAP)
@wrap_errors([BotoServerError])
def setcapacity(args):
    """
    Change the "desired_capacity" parameter of the AutoScale in a stack
    """
    config, settings, sinfo = initialize_from_cli(args)
    stack = find_one_stack(args.stack_name, summary=False)
    print(format_stack_summary(stack))

    warn_for_live(sinfo)

    asg = find_one_resource(stack, 'AWS::AutoScaling::AutoScalingGroup')
    print("AutoScale ID: %s" % asg.name)

    update_asg_capacity(
        asg,
        desired=0,
    )


@arg('stack_name', help=HELP_SN)
@wrap_errors([ValueError, BotoServerError])
def shutdown(args):
    """
    Shutdown the stack: force the AutoScale to shut all instances down
    """
    config, settings, sinfo = initialize_from_cli(args)
    stack = find_one_stack(args.stack_name, summary=False)
    print(format_stack_summary(stack))

    warn_for_live(sinfo)

    asg = find_one_resource(stack, 'AWS::AutoScaling::AutoScalingGroup')
    print("AutoScale ID: %s" % asg.name)

    update_asg_capacity(
        asg,
        desired=0,
        minlimit=0,
        maxlimit=0,
    )


@arg('stack_name', help=HELP_SN)
@wrap_errors([ValueError, BotoServerError])
def startup(args):
    """
    Startup the stack: set AutoScale to start the instances up
    """
    config, settings, sinfo = initialize_from_cli(args)
    stack = find_one_stack(args.stack_name, summary=False)
    print(format_stack_summary(stack))

    warn_for_live(sinfo)

    asg = find_one_resource(stack, 'AWS::AutoScaling::AutoScalingGroup')
    print("AutoScale ID: %s" % asg.name)

    try:
        minlimit = int(sinfo['AutoScaleMinSize'])
        maxlimit = int(sinfo['AutoScaleMaxSize'])
        desired = int(sinfo['AutoScaleDesiredCapacity'])
    except:
        raise ValueError("Invalid AutoScale information in stack definition")

    update_asg_capacity(
        asg,
        desired=desired,
        minlimit=minlimit,
        maxlimit=maxlimit,
    )


def main():
    parser = ArghParser(version=pkg_resources.get_distribution("awstools").version)
    parser.add_argument('--config', default=None, help=HELP_CFG)
    parser.add_argument('--settings', default=None, help=HELP_SETTINGS)
    parser.add_commands([
                        ls,
                        create,
                        update,
                        delete,
                        info,
                        outputs,
                        resources,
                        events,
                        activities,
                        setlimit,
                        setcapacity,
                        shutdown,
                        startup,
                        ])
    parser.dispatch(completion=False)

if __name__ == '__main__':
    main()