Skip to content
Find file
181 lines (143 sloc) 6.02 KB
#!/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
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# See the License for the specific language governing permissions and
# limitations under the License.
import datetime
import json
import logging
import os
from pprint import pformat
import jinja2
import webapp2
from google.appengine.api import app_identity
from google.appengine.ext import vendor
from googleapiclient import discovery
from oauth2client.client import GoogleCredentials
compute ='compute','v1', credentials=GoogleCredentials.get_application_default())
SAMPLE_NAME = 'Instance timeout helper'
# In DRY_RUN mode, deletes are only logged. Set this to False after you've
# double-checked the status page and you're ready to enable the deletes.
'DRY_RUN': True,
# Be careful, this application could delete all instances in this project.
# Your project id can be found on the overview tab of the Google APIs
# Console:
'GCE_PROJECT_ID': app_identity.get_application_id(),
# Instances created with these tags will never be deleted.
'SAFE_TAGS': ['production', 'safetag'],
# Instances are deleted after they have been running for TIMEOUT minutes.
'TIMEOUT': 60 * 8, # in minutes, defaulting to 8 hours
CONFIG['SAFE_TAGS'] = [t.lower() for t in CONFIG['SAFE_TAGS']]
# Obtain App Engine AppAssertion credentials and authorize HTTP connection.
# Build object for the 'v1' version of the GCE API.
jinja_environment = jinja2.Environment(
def annotate_instances(instances):
"""loops through the instances and adds exclusion, age and timeout"""
for inst in instances:
# set _excluded
excluded = False
tags = inst.get('tags', {}).get('items', [])
inst['_tags'] = tags
for tag in tags:
if tag.lower() in CONFIG['SAFE_TAGS']:
excluded = True
inst['_excluded'] = excluded
# set _age_minutes and _timeout_expired
# _timeout_expired is never True for _excluded inst
creation = parse_iso8601tz(inst['creationTimestamp'])
now =
delta = now - creation
age_minutes = (delta.days * 24 * 60) + (delta.seconds / 60)
inst['_age_minutes'] = age_minutes
# >= comparison because seconds are truncated above.
if not inst['_excluded'] and age_minutes >= CONFIG['TIMEOUT']:
inst['_timeout_expired'] = True
inst['_timeout_expired'] = False
def list_instances():
"""returns a list of dictionaries containing GCE instance data"""
request = compute.instances().aggregatedList(project=CONFIG['GCE_PROJECT_ID'])
response = request.execute()
zones = response.get('items', {})
instances = []
for zone in zones.values():
for instance in zone.get('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'] = json.dumps(instances, indent=4, sort_keys=True)
template = jinja_environment.get_template('index.html')
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']]'delete cron: %s instance%s to delete',
len(instances), '' if len(instances) == 1 else 's')
for instance in instances:
name = instance['name']
zone = instance['zone'].split('/')[-1]
if CONFIG['DRY_RUN']:"DRY_RUN, not deleted: %s", name)
else:"DELETE: %s", name)
request = compute.instances().delete(
response = request.execute()
class DeleteHandler(webapp2.RequestHandler):
"""delete handler - HTTP endpoint for the GAE cron job"""
def get(self):
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:
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],
# parse the timezone offset separately
delta = datetime.timedelta(minutes=int(date_string[-2:]),
if date_string[-6] == '-':
# add the delta to return to UTC time
dt = dt + delta
dt = dt - delta
return dt
Jump to Line
Something went wrong with that request. Please try again.