This repository has been archived by the owner on Apr 27, 2021. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 16
/
main.py
157 lines (127 loc) · 5.28 KB
/
main.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
#!/usr/bin/env python
#
# Copyright 2012 Google Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import datetime
import httplib2
import logging
from pprint import pformat
import jinja2
import webapp2
from apiclient.discovery import build
from google.appengine.api import lib_config
from google.appengine.api import memcache
from oauth2client.appengine import AppAssertionCredentials
SAMPLE_NAME = 'Instance timeout helper'
# To make configuration changes, update these values in settings.py
CONFIG_DEFAULTS = {
'DRY_RUN': True,
'GCE_PROJECT_ID': 'replace-with-your-compute-engine-project-id',
'SAFE_TAGS': ['production', 'safetag'],
'TIMEOUT': 60 * 8, # in minutes, defaulting to 8 hours
}
registry = lib_config.LibConfigRegistry('settings')
config = registry.register('main', CONFIG_DEFAULTS)
config.SAFE_TAGS = [t.lower() for t in config.SAFE_TAGS]
# Obtain App Engine AppAssertion credentials and authorize HTTP connection.
# https://developers.google.com/appengine/docs/python/appidentity/overview
credentials = AppAssertionCredentials(
scope='https://www.googleapis.com/auth/compute')
HTTP = credentials.authorize(httplib2.Http(memcache))
# Build object for the 'v1beta13' version of the GCE API.
# https://developers.google.com/compute/docs/reference/v1beta13/
compute = build('compute', 'v1beta13', http=HTTP)
jinja_environment = jinja2.Environment(
loader=jinja2.FileSystemLoader('templates'))
def annotate_instances(instances):
"""loops through the instances and adds exclusion, age and timeout"""
for instance in instances:
# set _excluded
excluded = False
for tag in instance.get('tags', []):
if tag.lower() in config.SAFE_TAGS:
excluded = True
break
instance['_excluded'] = excluded
# set _age_hours and _timeout_expired
# _timeout_expired is never True for _excluded inst
creation = parse_iso8601tz(instance['creationTimestamp'])
now = datetime.datetime.now()
delta = now - creation
instance['_age_hours'] = delta.seconds / 60
if delta.seconds > config.TIMEOUT * 60 and not instance['_excluded']:
instance['_timeout_expired'] = True
else:
instance['_timeout_expired'] = False
def list_instances():
"""returns a list of dictionaries containing GCE instance data"""
request = compute.instances().list(project=config.GCE_PROJECT_ID)
response = request.execute()
instances = response.get('items', [])
annotate_instances(instances)
return instances
class MainHandler(webapp2.RequestHandler):
"""index handler, displays app configuration and instance data"""
def get(self):
instances = list_instances()
data = {}
data['config'] = config
data['title'] = SAMPLE_NAME
data['instances'] = instances
data['raw_instances'] = pformat(instances)
template = jinja_environment.get_template('index.html')
self.response.out.write(template.render(data))
def delete_expired_instances():
"""logs all expired instances, calls delete API when not DRY_RUN"""
instances = list_instances()
# filter instances, keep only expired instances
instances = [i for i in instances if i['_timeout_expired']]
logging.info("delete cron: %s instance%s to delete",
len(instances), '' if len(instances) == 1 else 's')
for instance in instances:
name = instance['name']
if config.DRY_RUN:
logging.info("DRY_RUN, not deleted: %s", name)
else:
logging.info("DELETE: %s", name)
request = compute.instances().delete(project=config.GCE_PROJECT_ID,
instance=name)
response = request.execute()
logging.info(response)
class DeleteHandler(webapp2.RequestHandler):
"""delete handler - HTTP endpoint for the GAE cron job"""
def get(self):
delete_expired_instances()
app = webapp2.WSGIApplication([
('/cron/delete', DeleteHandler),
('/', MainHandler),
], debug=True)
# ------------------------------------------------
# helpers
def parse_iso8601tz(date_string):
"""return a datetime object for a string in ISO 8601 format.
This function parses strings in exactly this format:
'2012-12-26T13:31:47.823-08:00'
Sadly, datetime.strptime's %z format is unavailable on many platforms,
so we can't use a single strptime() call.
"""
dt = datetime.datetime.strptime(date_string[:-6],
'%Y-%m-%dT%H:%M:%S.%f')
# parse the timezone offset separately
delta = datetime.timedelta(minutes=int(date_string[-2:]),
hours=int(date_string[-5:-3]))
if date_string[-6:-5] == u'-':
delta = delta * -1
dt = dt - delta
return dt