blob: 13537cdfe2e7a7c548ba439a513e4cd3ea398c52 [file] [log] [blame]
#!/usr/bin/env python
# Copyright (c) 2014 The Chromium Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Waterfall monitoring script.
This script checks all builders specified in the config file and sends
status email about any step failures in these builders. This also
reports a build as failure if the latest build on that builder was built
2 days back. (Number of days can be configured in the config file)
This script can be run as cronjob on a linux machine once a day and
get email notification for any waterfall specified in the config file.
Sample cronjob entry below. This entry will run the script everyday at 9 AM.
Include this in the crontab file.
0 9 * * * <Path to script> --config <Path to json file>
"""
import datetime
import json
import optparse
import sys
import time
import traceback
import urllib
from datetime import timedelta
from email.mime.text import MIMEText
from subprocess import Popen, PIPE
SUCCESS_SUBJECT = ('[CHROME TESTING]: Builder status %s: PASSED.')
FAILURE_SUBJECT = ('[CHROME TESTING]: Builder status %s: FAILED %d out of %d')
EXCEPTION_SUBJECT = ('Exception occurred running waterfall_builder_monitor.py '
'script')
def GetTimeDelta(date, days):
if isinstance(date, datetime.datetime):
return date + timedelta(days)
def GetDateFromEpochFormat(epoch_time):
last_build_date = time.localtime(epoch_time)
last_build_date = datetime.datetime(int(last_build_date.tm_year),
int(last_build_date.tm_mon),
int(last_build_date.tm_mday),
int(last_build_date.tm_hour),
int(last_build_date.tm_min),
int(last_build_date.tm_sec))
return last_build_date
def GetJSONData(json_url):
response = urllib.urlopen(json_url)
if response.getcode() == 200:
try:
data = json.loads(response.read())
except ValueError:
print 'ValueError for JSON URL: %s' % json_url
raise
else:
raise Exception('Error from URL: %s' % json_url)
response.close()
return data
def SendEmailViaSendmailCommand(sender_email, recipient_emails,
subject, email_body):
msg = MIMEText(email_body)
msg["From"] = sender_email
msg["To"] = recipient_emails
msg["Subject"] = subject
pipe = Popen(["/usr/sbin/sendmail", "-t"], stdin=PIPE)
pipe.communicate(msg.as_string())
def SendStatusEmailViaSendmailCommand(consolidated_results,
recipient_emails,
sender_email):
failure_count = 0
for result in consolidated_results:
if result['error'] != 'passed' and not result['build_too_old']:
failure_count += 1
today = str(datetime.date.today()).replace('-', '/')[5:]
if failure_count == 0:
subject = SUCCESS_SUBJECT % today
else:
subject = FAILURE_SUBJECT % (today,
failure_count,
len(consolidated_results))
email_body = ''
for result in consolidated_results:
if result['error'] != 'passed' or result['build_too_old']:
if result['build_date'] is not None:
email_body += result['platform'] + ': ' +\
result['build_link'] + ' ( Build too old: ' +\
result['build_date'] + ' ) ' +'\n\n'
else:
email_body += result['platform'] + ': ' +\
result['build_link'] + '\n\n'
SendEmailViaSendmailCommand(sender_email, recipient_emails,
subject, email_body)
def SendExceptionEmailViaSendmailCommand(exception_message_lines,
recipient_emails,
sender_email):
subject = EXCEPTION_SUBJECT
email_body = ''
email_body = '\n'.join(exception_message_lines)
SendEmailViaSendmailCommand(sender_email, recipient_emails,
subject, email_body)
class OfficialBuilderParser(object):
"""This class implements basic utility functions on a specified builder."""
def __init__(self, builder_type, build_info):
self.platform = builder_type
self.builder_info = build_info
self.builder_url = build_info['builder_url']
self.build_json_url = build_info['json_url']
self.build = self._GetLatestBuildNumber()
def _GetLatestBuildNumber(self):
json_url = self.builder_info['builds_url']
data = GetJSONData(json_url)
# Get a sorted list of all the keys in the json data.
keys = sorted(data)
return self._GetLatestCompletedBuild(keys)
def _GetLatestCompletedBuild(self, keys):
reversed_list = keys[::-1]
for build in reversed_list:
data = self._GetJSONDataForBuild(build)
if data is not None:
if 'text' in data:
return build
return None
def _GetJSONDataForBuild(self, build):
if build is None:
return build
json_url = self.build_json_url % build
return GetJSONData(json_url)
class GetBuilderStatus(OfficialBuilderParser):
def __init__(self, builder_type, build_info):
OfficialBuilderParser.__init__(self, builder_type, build_info)
def CheckForFailedSteps(self, days):
if self.build is None:
return {}
result = {'platform': self.platform,
'build_number': self.build,
'build_link': self.builder_url + self.build,
'build_date': None,
'build_too_old': False,
'error': 'unknown'}
data = self._GetJSONDataForBuild(self.build)
if data is not None:
if 'text' in data:
if 'build' in data['text'] and 'successful' in data['text']:
result['error'] = 'passed'
else:
if 'failed' in data['text'] or\
'exception' in data['text'] or\
'interrupted' in data['text']:
result['error'] = 'failed'
if 'times' in data:
old_date = GetTimeDelta(datetime.datetime.now(), days)
last_build_date = GetDateFromEpochFormat(data['times'][0])
if last_build_date < old_date:
result['build_too_old'] = True
result['build_date'] = str(last_build_date).split(' ')[0]
else:
raise Exception('There was some problem getting JSON data '
'from URL: %s' % result['build_link'])
return result
def main():
parser = optparse.OptionParser()
parser.add_option('--config', type='str',
help='Absolute path to the config file.')
(options, _) = parser.parse_args()
if not options.config:
print 'Error: missing required parameter: --config'
parser.print_help()
return 1
try:
with open(options.config, 'r') as config_file:
try:
json_data = json.loads(config_file.read())
except ValueError:
print 'ValueError for loading JSON data from : %s' % options.config
raise ValueError
old_build_days = -2
if 'old_build_days' in json_data:
old_build_days = - json_data['old_build_days']
consolidated_results = []
for key in json_data['build_info'].keys():
builder_status = GetBuilderStatus(key, json_data['build_info'][key])
builder_result = builder_status.CheckForFailedSteps(old_build_days)
consolidated_results.append(builder_result)
SendStatusEmailViaSendmailCommand(consolidated_results,
json_data['recipient_emails'],
json_data['sender_email'])
return 0
except Exception:
formatted_lines = traceback.format_exc().splitlines()
SendExceptionEmailViaSendmailCommand(formatted_lines,
json_data['recipient_emails'],
json_data['sender_email'])
return 1
if __name__ == '__main__':
sys.exit(main())