Subversion Repositories DevTools

Rev

Rev 5809 | Blame | Compare with Previous | Last modification | View Log | RSS feed

#!/usr/bin/python
#
# Auto-start and stop EC2 instances
#
import boto.ec2, datetime, sys
from time import gmtime, strftime, strptime, sleep

#Default start stop times (7am / 7pm AWST)
DEFAULT_START_GMT = "23"
DEFAULT_STOP_GMT = "11"

#debug setting for dry-run
#IS_DRY_RUN = True
IS_DRY_RUN = False

#tagging start and stop times is used to prevent starting / stopping instances 
#multiple times an hour, but it can be turned off to reduce number of tags used 
#by script if you don't think this is an issue (eg script only runs once an hour)
#TAG_START_STOP_TIMES = False
TAG_START_STOP_TIMES = True

###################################################################################
# Shoudn't need to modify anything below here
###################################################################################

#State Code Constants
#http://docs.aws.amazon.com/cli/latest/reference/ec2/describe-instance-status.html
STATE_PENDING = 0
STATE_RUNNING = 16
STATE_SHUTTING_DOWN = 32
STATE_TERMINATED = 48
STATE_STOPPING = 64
STATE_STOPPED = 80

# This is special flag to never kill this instance. Should really only be used
# on your scripting server instance.
TAG_DATE_TIME_FORMAT = "%Y-%m-%d"
DEFAULT_NO_BOOT_TIME = "2000-01-01"
DEFAULT_NO_KILL_TIME = "2000-01-01"

TODAY_HOUR_DT_FORMAT = '%Y-%m-%d %H'
DEFAULT_AUTO_TIME = '2000-01-01 00'

TODAY_WEEKDAY_NAME = '%a'
DEFAULT_WEEKDAY_ACTION = 'MTWHFSU'

def main():
    gmtime_now = gmtime()

    #Test cases
    #Monday
    #gmtime_now = strptime("2014/11/24 10:00:00", "%Y/%m/%d %H:%M:%S")
    #Wednesday
    #gmtime_now = strptime("2014/11/26 10:00:00", "%Y/%m/%d %H:%M:%S")
    #Thursday
    #gmtime_now = strptime("2014/11/27 10:00:00", "%Y/%m/%d %H:%M:%S")
    #Friday
    #gmtime_now = strptime("2014/11/28 10:00:00", "%Y/%m/%d %H:%M:%S")
    #Saturday
    #gmtime_now = strptime("2014/11/29 10:00:00", "%Y/%m/%d %H:%M:%S")
    #Sunday
    #gmtime_now = strptime("2014/11/30 10:00:00", "%Y/%m/%d %H:%M:%S")
    
    startStopInstancesForTime(gmtime_now)

def startStopInstancesForTime(gmtime_now):
    #Starts or stops instances based on the supplied GMT time.
    #@param gmtime_now - time struct of the current time in GMT

    #AWS credentials
    region = boto.config.get('sleep_tight', 'region', 'ap-southeast-2')
    aws_key = boto.config.get('sleep_tight', 'aws_access_key_id', '0')
    aws_secret = boto.config.get('sleep_tight', 'aws_secret_access_key', '0')
    
    # Connect to EC2
    conn = boto.ec2.connect_to_region(region, aws_access_key_id=aws_key, aws_secret_access_key=aws_secret)
    
    # Get current hour
    hh = strftime("%H", gmtime_now)
    # Get Today 
    this_hour_str = strftime(TODAY_HOUR_DT_FORMAT, gmtime_now)
    this_hour_time = strptime(this_hour_str, TODAY_HOUR_DT_FORMAT)
    this_day_name = strftime(TODAY_WEEKDAY_NAME, gmtime_now)

    instances = conn.get_only_instances()
    for( instance ) in instances:
        instance_id = instance.id
        name = instance.tags.get("Name", instance_id)
        state_code = instance.state_code
        start_hour_gmt = getSetTag(instance, 'START_HOUR_GMT', DEFAULT_START_GMT)
        stop_hour_gmt = getSetTag(instance, 'STOP_HOUR_GMT', DEFAULT_STOP_GMT)
        action_days = getSetTag(instance, 'ACTION_DAYS', DEFAULT_WEEKDAY_ACTION)

        no_boot_until_time = getTimeFromInstance(instance, 'NO_BOOT_UNTIL', DEFAULT_NO_BOOT_TIME, TAG_DATE_TIME_FORMAT)
        no_kill_until_time = getTimeFromInstance(instance, 'NO_KILL_UNTIL', DEFAULT_NO_KILL_TIME, TAG_DATE_TIME_FORMAT)
        
        last_auto_start_time = 99;
        last_auto_stop_time = 99;
        
        if ( TAG_START_STOP_TIMES ):
            last_auto_start_time = getTimeFromInstance(instance, 'LAST_AUTO_START', DEFAULT_AUTO_TIME, TODAY_HOUR_DT_FORMAT) 
            last_auto_stop_time = getTimeFromInstance(instance, 'LAST_AUTO_STOP', DEFAULT_AUTO_TIME, TODAY_HOUR_DT_FORMAT)

        action_taken = "Do Nothing"
        try: 
            if ( int(start_hour_gmt) == int(stop_hour_gmt) ):
                action_taken = "Not modifying - Start (" + start_hour_gmt + ") and stop (" \
                             + stop_hour_gmt + ") are same hour."
            elif ( isNoActionDay(action_days, this_day_name) ):
                action_taken = "Not modifying - Non actionable day (" + action_days + ") " + this_day_name
            else:
                total_msg = ""

                if ( this_hour_time <= no_boot_until_time ):
                    total_msg += "No boot until " + strftime(TAG_DATE_TIME_FORMAT, no_boot_until_time)  
                elif ( int(hh) == int(start_hour_gmt) and last_auto_start_time != this_hour_time ):
                    total_msg += processStart( conn, instance, state_code, this_hour_str )
                else:
                    total_msg += "No boot action"
           
                total_msg += " - "

                if ( this_hour_time <= no_kill_until_time ):
                    total_msg += "No kill until " + strftime(TAG_DATE_TIME_FORMAT, no_kill_until_time)
                elif( int(hh) == int(stop_hour_gmt) and last_auto_stop_time != this_hour_time ):
                    total_msg += processStop( conn, instance, state_code, this_hour_str )
                else:
                    total_msg += "No kill action"
        
                if ( total_msg != "" ):
                    action_taken = total_msg

        except:
            action_taken = "Unknown Exception"
    
        print this_hour_str, ':',  instance_id, state_code, start_hour_gmt, stop_hour_gmt, " --- ", action_taken, "for:", name
   


def getSetTag(instance, tag, default_value):
    #Attempts to retrieve the the tag from the instance.
    #If the tag doesn't exist then it will create the tag on the instance
    #with the value set to the default.
    #@param instance - boto.ec2.instance.Instance
    #@param tag - string for the tag to lookup
    #@param default_value - default string if tag does not exist
    tag_missing_default = "SOME_DEFAULT_NEVER_RTNED"
    the_tag = instance.tags.get(tag, tag_missing_default)
    if ( the_tag == tag_missing_default ):
        #the tag didnt exist copy callers default for return
        #create the tag with the callers default
        the_tag = default_value
        try:
            instance.add_tag(tag, default_value)
        except:
            doNothing = True
    #else the_tag is set to the instances tag.

    return the_tag
    

def getTimeFromInstance(instance, tag, default_time_str, time_format):
    #Safely reads the tag from the instance and converts it to a time structure
    #in the case the tag is incorrectly formatted, or missing it will use the 
    #default supplied as the output date
    #@param instance - boto.ec2.instance.Instance
    #@param tag - string for the tag to lookup
    #@param default_time_str - string of the date to use if the tag is missing
    #           or incorrectly formed
    #@param time_format - string of the date format the tag and default_time_str 
    #           are expected to conform to.
    time_str = getSetTag(instance, tag, default_time_str)
    try:
        time_as_struct = strptime(time_str, time_format)
    except:
        time_as_struct = strptime(default_time_str, time_format)

    return time_as_struct
 

def isNoActionDay( action_days, this_day_name ):
    #Determines if this day name is present in action days string
    #@param action_days - the days to return true for, is a string with the 
    #                     first letter of the days permitted. H = Thursday, U = Sunday
    #@param this_day_name - the current day of the week as three letters strftime(%a, gmtime)
    noAction = True
    switch_dict = {
        'Mon': 'M',
        'Tue': 'T',
        'Wed': 'W',
        'Thu': 'H',
        'Fri': 'F',
        'Sat': 'S',
        'Sun': 'U'
    }

    try:
       noAction = switch_dict[this_day_name] not in action_days
    except:
        print 'Unknown day', this_day_name 
     
    return noAction

    
def processStart( conn, instance, state_code, this_hour_str ):
    #Contains logic on whether to start instance based on current state.
    #Additional it will actually start the instance if determined to do so.
    #@param instance - oto.ec2.instance.Instance
    #@param state_code - instance's current state
    #@param this_hour_str - string indicating current GMT time in TODAY_HOUR_DT_FORMAT
    #@returns - string indicating the action taken.
    action_taken = "Unknown - start"

    if ( state_code == STATE_RUNNING ):
        action_taken = "Starting - no action already running"
    elif ( state_code == STATE_STOPPED ):
        action_taken = "Starting instance"
        try:
            conn.start_instances(instance_ids=instance.id, dry_run=IS_DRY_RUN)
            if ( TAG_START_STOP_TIMES ):
                #tag the instance so we don't potentially attempt to start it again this hour
                instance.add_tag('LAST_AUTO_START', this_hour_str)
        except boto.exception.EC2ResponseError, e:
            print "Except while starting -", e
        #TODO start the instance and poss set elastip ip
    else:
        action_taken = "Starting - ERROR state_code"

    return action_taken


def processStop( conn, instance, state_code, this_hour_str ):
    #Contains logic on whether to stop instance based on current state.
    #Additional it will actually stop the instance if determined to do so.
    #@param instance - oto.ec2.instance.Instance
    #@param state_code - instance's current state
    #@param this_hour_str - string indicating current GMT time in TODAY_HOUR_DT_FORMAT
    #@returns - string indicating the action taken.
    action_taken = "Unknown - stop"
            
    if ( state_code in ( STATE_SHUTTING_DOWN, STATE_TERMINATED, STATE_STOPPING, STATE_STOPPED ) ):
        action_taken = "Stopping - no action already shutting/shutdown"
    elif ( state_code == STATE_RUNNING ):
        action_taken = "Stopping instance"
        try:
            conn.stop_instances(instance_ids=instance.id, dry_run=IS_DRY_RUN)
            if ( TAG_START_STOP_TIMES ):
                #tag the instance so we don't potentially attempt to stop it again this hour
                instance.add_tag('LAST_AUTO_STOP', this_hour_str)
        except boto.exception.EC2ResponseError, e:
            print "Except while stopping -", e
    else:
        action_taken = "Stopping - ERROR state_code"

    return action_taken



#Jump to main.
if __name__ == '__main__':
    main()