| 4719 |
marundel |
1 |
#!/usr/bin/python
|
|
|
2 |
#
|
|
|
3 |
# Auto-start and stop EC2 instances
|
|
|
4 |
#
|
|
|
5 |
import boto.ec2, datetime, sys
|
|
|
6 |
from time import gmtime, strftime, strptime, sleep
|
|
|
7 |
|
| 5808 |
bflanaga |
8 |
#Default start stop times (7am / 7pm AWST)
|
|
|
9 |
DEFAULT_START_GMT = "23"
|
|
|
10 |
DEFAULT_STOP_GMT = "11"
|
| 4719 |
marundel |
11 |
|
|
|
12 |
#debug setting for dry-run
|
|
|
13 |
#IS_DRY_RUN = True
|
|
|
14 |
IS_DRY_RUN = False
|
|
|
15 |
|
| 5809 |
bflanaga |
16 |
#tagging start and stop times is used to prevent starting / stopping instances
|
|
|
17 |
#multiple times an hour, but it can be turned off to reduce number of tags used
|
|
|
18 |
#by script if you don't think this is an issue (eg script only runs once an hour)
|
|
|
19 |
#TAG_START_STOP_TIMES = False
|
|
|
20 |
TAG_START_STOP_TIMES = True
|
|
|
21 |
|
| 4719 |
marundel |
22 |
###################################################################################
|
|
|
23 |
# Shoudn't need to modify anything below here
|
|
|
24 |
###################################################################################
|
|
|
25 |
|
|
|
26 |
#State Code Constants
|
|
|
27 |
#http://docs.aws.amazon.com/cli/latest/reference/ec2/describe-instance-status.html
|
|
|
28 |
STATE_PENDING = 0
|
|
|
29 |
STATE_RUNNING = 16
|
|
|
30 |
STATE_SHUTTING_DOWN = 32
|
|
|
31 |
STATE_TERMINATED = 48
|
|
|
32 |
STATE_STOPPING = 64
|
|
|
33 |
STATE_STOPPED = 80
|
|
|
34 |
|
|
|
35 |
# This is special flag to never kill this instance. Should really only be used
|
|
|
36 |
# on your scripting server instance.
|
|
|
37 |
TAG_DATE_TIME_FORMAT = "%Y-%m-%d"
|
| 5808 |
bflanaga |
38 |
DEFAULT_NO_BOOT_TIME = "2000-01-01"
|
|
|
39 |
DEFAULT_NO_KILL_TIME = "2000-01-01"
|
| 4719 |
marundel |
40 |
|
|
|
41 |
TODAY_HOUR_DT_FORMAT = '%Y-%m-%d %H'
|
|
|
42 |
DEFAULT_AUTO_TIME = '2000-01-01 00'
|
|
|
43 |
|
|
|
44 |
TODAY_WEEKDAY_NAME = '%a'
|
| 5808 |
bflanaga |
45 |
DEFAULT_WEEKDAY_ACTION = 'MTWHFSU'
|
| 4719 |
marundel |
46 |
|
|
|
47 |
def main():
|
|
|
48 |
gmtime_now = gmtime()
|
|
|
49 |
|
|
|
50 |
#Test cases
|
|
|
51 |
#Monday
|
|
|
52 |
#gmtime_now = strptime("2014/11/24 10:00:00", "%Y/%m/%d %H:%M:%S")
|
|
|
53 |
#Wednesday
|
|
|
54 |
#gmtime_now = strptime("2014/11/26 10:00:00", "%Y/%m/%d %H:%M:%S")
|
|
|
55 |
#Thursday
|
|
|
56 |
#gmtime_now = strptime("2014/11/27 10:00:00", "%Y/%m/%d %H:%M:%S")
|
|
|
57 |
#Friday
|
|
|
58 |
#gmtime_now = strptime("2014/11/28 10:00:00", "%Y/%m/%d %H:%M:%S")
|
|
|
59 |
#Saturday
|
|
|
60 |
#gmtime_now = strptime("2014/11/29 10:00:00", "%Y/%m/%d %H:%M:%S")
|
|
|
61 |
#Sunday
|
|
|
62 |
#gmtime_now = strptime("2014/11/30 10:00:00", "%Y/%m/%d %H:%M:%S")
|
|
|
63 |
|
|
|
64 |
startStopInstancesForTime(gmtime_now)
|
|
|
65 |
|
|
|
66 |
def startStopInstancesForTime(gmtime_now):
|
|
|
67 |
#Starts or stops instances based on the supplied GMT time.
|
|
|
68 |
#@param gmtime_now - time struct of the current time in GMT
|
|
|
69 |
|
|
|
70 |
#AWS credentials
|
|
|
71 |
region = boto.config.get('sleep_tight', 'region', 'ap-southeast-2')
|
|
|
72 |
aws_key = boto.config.get('sleep_tight', 'aws_access_key_id', '0')
|
|
|
73 |
aws_secret = boto.config.get('sleep_tight', 'aws_secret_access_key', '0')
|
|
|
74 |
|
|
|
75 |
# Connect to EC2
|
|
|
76 |
conn = boto.ec2.connect_to_region(region, aws_access_key_id=aws_key, aws_secret_access_key=aws_secret)
|
|
|
77 |
|
|
|
78 |
# Get current hour
|
|
|
79 |
hh = strftime("%H", gmtime_now)
|
|
|
80 |
# Get Today
|
|
|
81 |
this_hour_str = strftime(TODAY_HOUR_DT_FORMAT, gmtime_now)
|
|
|
82 |
this_hour_time = strptime(this_hour_str, TODAY_HOUR_DT_FORMAT)
|
|
|
83 |
this_day_name = strftime(TODAY_WEEKDAY_NAME, gmtime_now)
|
|
|
84 |
|
|
|
85 |
instances = conn.get_only_instances()
|
|
|
86 |
for( instance ) in instances:
|
|
|
87 |
instance_id = instance.id
|
|
|
88 |
name = instance.tags.get("Name", instance_id)
|
|
|
89 |
state_code = instance.state_code
|
|
|
90 |
start_hour_gmt = getSetTag(instance, 'START_HOUR_GMT', DEFAULT_START_GMT)
|
|
|
91 |
stop_hour_gmt = getSetTag(instance, 'STOP_HOUR_GMT', DEFAULT_STOP_GMT)
|
| 5795 |
marundel |
92 |
action_days = getSetTag(instance, 'ACTION_DAYS', DEFAULT_WEEKDAY_ACTION)
|
| 4719 |
marundel |
93 |
|
|
|
94 |
no_boot_until_time = getTimeFromInstance(instance, 'NO_BOOT_UNTIL', DEFAULT_NO_BOOT_TIME, TAG_DATE_TIME_FORMAT)
|
|
|
95 |
no_kill_until_time = getTimeFromInstance(instance, 'NO_KILL_UNTIL', DEFAULT_NO_KILL_TIME, TAG_DATE_TIME_FORMAT)
|
| 5809 |
bflanaga |
96 |
|
| 5821 |
bflanaga |
97 |
last_auto_start_time = 99;
|
|
|
98 |
last_auto_stop_time = 99;
|
|
|
99 |
|
| 5809 |
bflanaga |
100 |
if ( TAG_START_STOP_TIMES ):
|
|
|
101 |
last_auto_start_time = getTimeFromInstance(instance, 'LAST_AUTO_START', DEFAULT_AUTO_TIME, TODAY_HOUR_DT_FORMAT)
|
|
|
102 |
last_auto_stop_time = getTimeFromInstance(instance, 'LAST_AUTO_STOP', DEFAULT_AUTO_TIME, TODAY_HOUR_DT_FORMAT)
|
| 4719 |
marundel |
103 |
|
|
|
104 |
action_taken = "Do Nothing"
|
|
|
105 |
try:
|
|
|
106 |
if ( int(start_hour_gmt) == int(stop_hour_gmt) ):
|
|
|
107 |
action_taken = "Not modifying - Start (" + start_hour_gmt + ") and stop (" \
|
|
|
108 |
+ stop_hour_gmt + ") are same hour."
|
|
|
109 |
elif ( isNoActionDay(action_days, this_day_name) ):
|
| 5808 |
bflanaga |
110 |
action_taken = "Not modifying - Non actionable day (" + action_days + ") " + this_day_name
|
| 4719 |
marundel |
111 |
else:
|
|
|
112 |
total_msg = ""
|
|
|
113 |
|
| 5795 |
marundel |
114 |
if ( this_hour_time <= no_boot_until_time ):
|
| 5808 |
bflanaga |
115 |
total_msg += "No boot until " + strftime(TAG_DATE_TIME_FORMAT, no_boot_until_time)
|
| 4719 |
marundel |
116 |
elif ( int(hh) == int(start_hour_gmt) and last_auto_start_time != this_hour_time ):
|
|
|
117 |
total_msg += processStart( conn, instance, state_code, this_hour_str )
|
| 5795 |
marundel |
118 |
else:
|
|
|
119 |
total_msg += "No boot action"
|
| 4719 |
marundel |
120 |
|
| 5808 |
bflanaga |
121 |
total_msg += " - "
|
|
|
122 |
|
| 5795 |
marundel |
123 |
if ( this_hour_time <= no_kill_until_time ):
|
| 5808 |
bflanaga |
124 |
total_msg += "No kill until " + strftime(TAG_DATE_TIME_FORMAT, no_kill_until_time)
|
| 4719 |
marundel |
125 |
elif( int(hh) == int(stop_hour_gmt) and last_auto_stop_time != this_hour_time ):
|
|
|
126 |
total_msg += processStop( conn, instance, state_code, this_hour_str )
|
| 5795 |
marundel |
127 |
else:
|
| 5808 |
bflanaga |
128 |
total_msg += "No kill action"
|
| 4719 |
marundel |
129 |
|
|
|
130 |
if ( total_msg != "" ):
|
|
|
131 |
action_taken = total_msg
|
|
|
132 |
|
|
|
133 |
except:
|
|
|
134 |
action_taken = "Unknown Exception"
|
|
|
135 |
|
| 5808 |
bflanaga |
136 |
print this_hour_str, ':', instance_id, state_code, start_hour_gmt, stop_hour_gmt, " --- ", action_taken, "for:", name
|
| 4719 |
marundel |
137 |
|
|
|
138 |
|
|
|
139 |
|
|
|
140 |
def getSetTag(instance, tag, default_value):
|
|
|
141 |
#Attempts to retrieve the the tag from the instance.
|
|
|
142 |
#If the tag doesn't exist then it will create the tag on the instance
|
|
|
143 |
#with the value set to the default.
|
|
|
144 |
#@param instance - boto.ec2.instance.Instance
|
|
|
145 |
#@param tag - string for the tag to lookup
|
|
|
146 |
#@param default_value - default string if tag does not exist
|
|
|
147 |
tag_missing_default = "SOME_DEFAULT_NEVER_RTNED"
|
|
|
148 |
the_tag = instance.tags.get(tag, tag_missing_default)
|
|
|
149 |
if ( the_tag == tag_missing_default ):
|
|
|
150 |
#the tag didnt exist copy callers default for return
|
|
|
151 |
#create the tag with the callers default
|
|
|
152 |
the_tag = default_value
|
|
|
153 |
try:
|
|
|
154 |
instance.add_tag(tag, default_value)
|
|
|
155 |
except:
|
|
|
156 |
doNothing = True
|
|
|
157 |
#else the_tag is set to the instances tag.
|
|
|
158 |
|
|
|
159 |
return the_tag
|
|
|
160 |
|
|
|
161 |
|
|
|
162 |
def getTimeFromInstance(instance, tag, default_time_str, time_format):
|
|
|
163 |
#Safely reads the tag from the instance and converts it to a time structure
|
|
|
164 |
#in the case the tag is incorrectly formatted, or missing it will use the
|
|
|
165 |
#default supplied as the output date
|
|
|
166 |
#@param instance - boto.ec2.instance.Instance
|
|
|
167 |
#@param tag - string for the tag to lookup
|
|
|
168 |
#@param default_time_str - string of the date to use if the tag is missing
|
|
|
169 |
# or incorrectly formed
|
|
|
170 |
#@param time_format - string of the date format the tag and default_time_str
|
|
|
171 |
# are expected to conform to.
|
|
|
172 |
time_str = getSetTag(instance, tag, default_time_str)
|
|
|
173 |
try:
|
|
|
174 |
time_as_struct = strptime(time_str, time_format)
|
|
|
175 |
except:
|
|
|
176 |
time_as_struct = strptime(default_time_str, time_format)
|
|
|
177 |
|
|
|
178 |
return time_as_struct
|
|
|
179 |
|
|
|
180 |
|
|
|
181 |
def isNoActionDay( action_days, this_day_name ):
|
|
|
182 |
#Determines if this day name is present in action days string
|
|
|
183 |
#@param action_days - the days to return true for, is a string with the
|
|
|
184 |
# first letter of the days permitted. H = Thursday, U = Sunday
|
|
|
185 |
#@param this_day_name - the current day of the week as three letters strftime(%a, gmtime)
|
|
|
186 |
noAction = True
|
|
|
187 |
switch_dict = {
|
|
|
188 |
'Mon': 'M',
|
|
|
189 |
'Tue': 'T',
|
|
|
190 |
'Wed': 'W',
|
|
|
191 |
'Thu': 'H',
|
|
|
192 |
'Fri': 'F',
|
|
|
193 |
'Sat': 'S',
|
|
|
194 |
'Sun': 'U'
|
|
|
195 |
}
|
|
|
196 |
|
|
|
197 |
try:
|
|
|
198 |
noAction = switch_dict[this_day_name] not in action_days
|
|
|
199 |
except:
|
|
|
200 |
print 'Unknown day', this_day_name
|
|
|
201 |
|
|
|
202 |
return noAction
|
|
|
203 |
|
|
|
204 |
|
|
|
205 |
def processStart( conn, instance, state_code, this_hour_str ):
|
|
|
206 |
#Contains logic on whether to start instance based on current state.
|
|
|
207 |
#Additional it will actually start the instance if determined to do so.
|
|
|
208 |
#@param instance - oto.ec2.instance.Instance
|
|
|
209 |
#@param state_code - instance's current state
|
|
|
210 |
#@param this_hour_str - string indicating current GMT time in TODAY_HOUR_DT_FORMAT
|
|
|
211 |
#@returns - string indicating the action taken.
|
|
|
212 |
action_taken = "Unknown - start"
|
|
|
213 |
|
|
|
214 |
if ( state_code == STATE_RUNNING ):
|
|
|
215 |
action_taken = "Starting - no action already running"
|
|
|
216 |
elif ( state_code == STATE_STOPPED ):
|
|
|
217 |
action_taken = "Starting instance"
|
|
|
218 |
try:
|
|
|
219 |
conn.start_instances(instance_ids=instance.id, dry_run=IS_DRY_RUN)
|
| 5809 |
bflanaga |
220 |
if ( TAG_START_STOP_TIMES ):
|
|
|
221 |
#tag the instance so we don't potentially attempt to start it again this hour
|
|
|
222 |
instance.add_tag('LAST_AUTO_START', this_hour_str)
|
| 4719 |
marundel |
223 |
except boto.exception.EC2ResponseError, e:
|
|
|
224 |
print "Except while starting -", e
|
|
|
225 |
#TODO start the instance and poss set elastip ip
|
|
|
226 |
else:
|
|
|
227 |
action_taken = "Starting - ERROR state_code"
|
|
|
228 |
|
|
|
229 |
return action_taken
|
|
|
230 |
|
|
|
231 |
|
|
|
232 |
def processStop( conn, instance, state_code, this_hour_str ):
|
|
|
233 |
#Contains logic on whether to stop instance based on current state.
|
|
|
234 |
#Additional it will actually stop the instance if determined to do so.
|
|
|
235 |
#@param instance - oto.ec2.instance.Instance
|
|
|
236 |
#@param state_code - instance's current state
|
|
|
237 |
#@param this_hour_str - string indicating current GMT time in TODAY_HOUR_DT_FORMAT
|
|
|
238 |
#@returns - string indicating the action taken.
|
|
|
239 |
action_taken = "Unknown - stop"
|
|
|
240 |
|
|
|
241 |
if ( state_code in ( STATE_SHUTTING_DOWN, STATE_TERMINATED, STATE_STOPPING, STATE_STOPPED ) ):
|
|
|
242 |
action_taken = "Stopping - no action already shutting/shutdown"
|
|
|
243 |
elif ( state_code == STATE_RUNNING ):
|
|
|
244 |
action_taken = "Stopping instance"
|
|
|
245 |
try:
|
|
|
246 |
conn.stop_instances(instance_ids=instance.id, dry_run=IS_DRY_RUN)
|
| 5809 |
bflanaga |
247 |
if ( TAG_START_STOP_TIMES ):
|
|
|
248 |
#tag the instance so we don't potentially attempt to stop it again this hour
|
|
|
249 |
instance.add_tag('LAST_AUTO_STOP', this_hour_str)
|
| 4719 |
marundel |
250 |
except boto.exception.EC2ResponseError, e:
|
|
|
251 |
print "Except while stopping -", e
|
|
|
252 |
else:
|
|
|
253 |
action_taken = "Stopping - ERROR state_code"
|
|
|
254 |
|
|
|
255 |
return action_taken
|
|
|
256 |
|
|
|
257 |
|
|
|
258 |
|
|
|
259 |
#Jump to main.
|
|
|
260 |
if __name__ == '__main__':
|
|
|
261 |
main()
|
|
|
262 |
|