blob: f14951b753ffbc1a939d1991d7bb901741efe388 [file] [log] [blame]
#!/usr/bin/env python2.7
# Provides a tool to verify Mesos reviews that are submitted to
# Review Board.
import atexit
import json
import os
import subprocess
import sys
import urllib
import urllib2
from datetime import datetime, timedelta
REVIEWBOARD_URL = "https://reviews.apache.org"
REVIEW_SIZE = 1000000 # 1 MB in bytes.
# TODO(vinod): Use 'argparse' module.
# Get the user and password from command line.
if len(sys.argv) < 3:
print "Usage: ./verify-reviews.py <user> <password> [num-reviews] [query-params]"
sys.exit(1)
USER = sys.argv[1]
PASSWORD = sys.argv[2]
# Number of reviews to verify.
NUM_REVIEWS = -1 # All possible reviews.
if len(sys.argv) >= 4:
NUM_REVIEWS = int(sys.argv[3])
# Unless otherwise specified consider pending review requests to Mesos updated
# since 03/01/2014.
GROUP = "mesos"
LAST_UPDATED = "2014-03-01T00:00:00"
QUERY_PARAMS = "?to-groups=%s&status=pending&last-updated-from=%s" \
% (GROUP, LAST_UPDATED)
if len(sys.argv) >= 5:
QUERY_PARAMS = sys.argv[4]
class ReviewError(Exception):
pass
def shell(command):
print command
return subprocess.check_output(
command, stderr=subprocess.STDOUT, shell=True)
HEAD = shell("git rev-parse HEAD")
def api(url, data=None):
try:
auth_handler = urllib2.HTTPBasicAuthHandler()
auth_handler.add_password(
realm="Web API",
uri="reviews.apache.org",
user=USER,
passwd=PASSWORD)
opener = urllib2.build_opener(auth_handler)
urllib2.install_opener(opener)
return json.loads(urllib2.urlopen(url, data=data).read())
except urllib2.HTTPError as e:
print "Error handling URL %s: %s (%s)" % (url, e.reason, e.read())
exit(1)
except urllib2.URLError as e:
print "Error handling URL %s: %s" % (url, e.reason)
exit(1)
def apply_review(review_id):
print "Applying review %s" % review_id
shell("./support/apply-review.sh -n -r %s" % review_id)
def apply_reviews(review_request, reviews):
# If there are no reviewers specified throw an error.
if not review_request["target_people"]:
raise ReviewError("No reviewers specified. Please find a reviewer by"
" asking on JIRA or the mailing list.")
# If there is a circular dependency throw an error.`
if review_request["id"] in reviews:
raise ReviewError("Circular dependency detected for review %s."
"Please fix the 'depends_on' field."
% review_request["id"])
else:
reviews.append(review_request["id"])
# First recursively apply the dependent reviews.
for review in review_request["depends_on"]:
review_url = review["href"]
print "Dependent review: %s " % review_url
apply_reviews(api(review_url)["review_request"], reviews)
# Now apply this review if not yet submitted.
if review_request["status"] != "submitted":
apply_review(review_request["id"])
def post_review(review_request, message):
print "Posting review: %s" % message
review_url = review_request["links"]["reviews"]["href"]
data = urllib.urlencode({'body_top' : message, 'public' : 'true'})
api(review_url, data)
@atexit.register
def cleanup():
try:
shell("git clean -fd")
shell("git reset --hard %s" % HEAD)
except subprocess.CalledProcessError as e:
print "Failed command: %s\n\nError: %s" % (e.cmd, e.output)
def verify_review(review_request):
print "Verifying review %s" % review_request["id"]
build_output = "build_" + str(review_request["id"])
try:
# Recursively apply the review and its dependents.
reviews = []
apply_reviews(review_request, reviews)
reviews.reverse() # Reviews are applied in the reverse order.
# Launch docker build script.
# TODO(jojy): Launch 'docker_build.sh' in subprocess so that
# verifications can be run in parallel for various configurations.
configuration = ("export "
"OS='ubuntu:14.04' "
"BUILDTOOL='autotools' "
"COMPILER='gcc' "
"CONFIGURATION='--verbose' "
"ENVIRONMENT='GLOG_v=1 MESOS_VERBOSE=1'")
command = "%s; ./support/docker_build.sh" % configuration
# `tee` the output so that the console can log the whole build output.
# `pipefail` ensures that the exit status of the build command is
# preserved even after tee'ing.
subprocess.check_call(['bash', '-c', 'set -o pipefail; %s 2>&1 | tee %s'
% (command, build_output)])
# Success!
post_review(
review_request,
"Patch looks great!\n\n" \
"Reviews applied: %s\n\n" \
"Passed command: %s" % (reviews, command))
except subprocess.CalledProcessError as e:
# If we are here because the docker build command failed, read the
# output from `build_output` file. For all other command failures read
# the output from `e.output`.
output = open(build_output).read() if os.path.exists(build_output) else e.output
# Truncate the output when posting the review as it can be very large.
output = output if len(output) <= REVIEW_SIZE else "...<truncated>...\n" + output[-REVIEW_SIZE:]
output = output + "\nFull log: " + os.path.join(os.environ['BUILD_URL'], 'console')
post_review(
review_request,
"Bad patch!\n\n" \
"Reviews applied: %s\n\n" \
"Failed command: %s\n\n" \
"Error:\n%s" % (reviews, e.cmd, output))
except ReviewError as e:
post_review(
review_request,
"Bad review!\n\n" \
"Reviews applied: %s\n\n" \
"Error:\n%s" % (reviews, e.args[0]))
# Clean up.
cleanup()
# Returns true if this review request needs to be verified.
def needs_verification(review_request):
print "Checking if review: %s needs verification" % review_request["id"]
# Skip if the review blocks another review.
if review_request["blocks"]:
print "Skipping blocking review %s" % review_request["id"]
return False
diffs_url = review_request["links"]["diffs"]["href"]
diffs = api(diffs_url)
if len(diffs["diffs"]) == 0: # No diffs attached!
print "Skipping review %s as it has no diffs" % review_request["id"]
return False
RB_DATE_FORMAT = "%Y-%m-%dT%H:%M:%SZ"
# Get the timestamp of the latest diff.
timestamp = diffs["diffs"][-1]["timestamp"]
diff_time = datetime.strptime(timestamp, RB_DATE_FORMAT)
print "Latest diff timestamp: %s" % diff_time
# Get the timestamp of the latest review from this script.
reviews_url = review_request["links"]["reviews"]["href"]
reviews = api(reviews_url + "?max-results=200")
review_time = None
for review in reversed(reviews["reviews"]):
if review["links"]["user"]["title"] == USER:
timestamp = review["timestamp"]
review_time = datetime.strptime(timestamp, RB_DATE_FORMAT)
print "Latest review timestamp: %s" % review_time
break
# TODO: Apply this check recursively up the dependency chain.
changes_url = review_request["links"]["changes"]["href"]
changes = api(changes_url)
dependency_time = None
for change in changes["changes"]:
if "depends_on" in change["fields_changed"]:
timestamp = change["timestamp"]
dependency_time = datetime.strptime(timestamp, RB_DATE_FORMAT)
print "Latest dependency change timestamp: %s" % dependency_time
break
# Needs verification if there is a new diff, or if the dependencies changed,
# after the last time it was verified.
return not review_time or review_time < diff_time or \
(dependency_time and review_time < dependency_time)
if __name__=="__main__":
review_requests_url = "%s/api/review-requests/%s" % (REVIEWBOARD_URL, QUERY_PARAMS)
review_requests = api(review_requests_url)
num_reviews = 0
for review_request in reversed(review_requests["review_requests"]):
if (NUM_REVIEWS == -1 or num_reviews < NUM_REVIEWS) and \
needs_verification(review_request):
verify_review(review_request)
num_reviews += 1