Index: chrome/test/chromedriver/test/waterfall_builder_monitor.py |
diff --git a/chrome/test/chromedriver/test/waterfall_builder_monitor.py b/chrome/test/chromedriver/test/waterfall_builder_monitor.py |
new file mode 100755 |
index 0000000000000000000000000000000000000000..13537cdfe2e7a7c548ba439a513e4cd3ea398c52 |
--- /dev/null |
+++ b/chrome/test/chromedriver/test/waterfall_builder_monitor.py |
@@ -0,0 +1,226 @@ |
+#!/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()) |