You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@mesos.apache.org by an...@apache.org on 2018/05/24 22:27:22 UTC

[4/6] mesos git commit: Ported all support scripts to Python 3.

http://git-wip-us.apache.org/repos/asf/mesos/blob/960df5c4/support/python3/push-commits.py
----------------------------------------------------------------------
diff --git a/support/python3/push-commits.py b/support/python3/push-commits.py
new file mode 100755
index 0000000..82a7004
--- /dev/null
+++ b/support/python3/push-commits.py
@@ -0,0 +1,158 @@
+#!/usr/bin/env python3
+#
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you 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.
+
+"""
+This script is typically used by Mesos committers to push a locally applied
+review chain to ASF git repo and mark the reviews as submitted on ASF
+ReviewBoard.
+
+Example Usage:
+
+> git checkout master
+> git pull origin
+> ./support/python3/apply-reviews.py -c -r 1234
+> ./support/python3/push-commits.py
+"""
+
+# TODO(vinod): Also post the commit message to the corresponding ASF JIRA
+# tickets and resolve them if necessary.
+
+import argparse
+import os
+import re
+import sys
+
+from subprocess import check_output
+
+REVIEWBOARD_URL = 'https://reviews.apache.org'
+
+
+def get_reviews(revision_range):
+    """Return the list of reviews found in the commits in the revision range."""
+    reviews = [] # List of (review id, commit log) tuples
+
+    rev_list = check_output(['git',
+                             'rev-list',
+                             '--reverse',
+                             revision_range]).strip().split('\n')
+    for rev in rev_list:
+        commit_log = check_output(['git',
+                                   '--no-pager',
+                                   'show',
+                                   '--no-color',
+                                   '--no-patch',
+                                   rev]).strip()
+
+        pos = commit_log.find('Review: ')
+        if pos != -1:
+            pattern = re.compile('Review: ({url})$'.format(
+                url=os.path.join(REVIEWBOARD_URL, 'r', '[0-9]+')))
+            match = pattern.search(commit_log.strip().strip('/'))
+            if match is None:
+                print("\nInvalid ReviewBoard URL: '{}'".format(
+                    commit_log[pos:]))
+                sys.exit(1)
+
+            url = match.group(1)
+            reviews.append((os.path.basename(url), commit_log))
+
+    return reviews
+
+
+def close_reviews(reviews, options):
+    """Mark the given reviews as submitted on ReviewBoard."""
+    for review_id, commit_log in reviews:
+        print('Closing review', review_id)
+        if not options['dry_run']:
+            check_output(['rbt',
+                          'close',
+                          '--description',
+                          commit_log,
+                          review_id])
+
+
+def parse_options():
+    """Return a dictionary of options parsed from command line arguments."""
+    parser = argparse.ArgumentParser()
+
+    parser.add_argument('-n',
+                        '--dry-run',
+                        action='store_true',
+                        help='Perform a dry run.')
+
+    args = parser.parse_args()
+
+    options = {}
+    options['dry_run'] = args.dry_run
+
+    return options
+
+
+def main():
+    """Main function to push the commits in this branch as review requests."""
+    options = parse_options()
+
+    current_branch_ref = check_output(['git', 'symbolic-ref', 'HEAD']).strip()
+    current_branch = current_branch_ref.replace('refs/heads/', '', 1)
+
+    if current_branch != 'master':
+        print('Please run this script from master branch')
+        sys.exit(1)
+
+    remote_tracking_branch = check_output(['git',
+                                           'rev-parse',
+                                           '--abbrev-ref',
+                                           'master@{upstream}']).strip()
+
+    merge_base = check_output([
+        'git',
+        'merge-base',
+        remote_tracking_branch,
+        'master']).strip()
+
+    if merge_base == current_branch_ref:
+        print('No new commits found to push')
+        sys.exit(1)
+
+    reviews = get_reviews(merge_base + ".." + current_branch_ref)
+
+    # Push the current branch to remote master.
+    remote = check_output(['git',
+                           'config',
+                           '--get',
+                           'branch.master.remote']).strip()
+
+    print('Pushing commits to', remote)
+
+    if options['dry_run']:
+        check_output(['git',
+                      'push',
+                      '--dry-run',
+                      remote,
+                      'master:master'])
+    else:
+        check_output(['git',
+                      'push',
+                      remote,
+                      'master:master'])
+
+    # Now mark the reviews as submitted.
+    close_reviews(reviews, options)
+
+if __name__ == '__main__':
+    main()

http://git-wip-us.apache.org/repos/asf/mesos/blob/960df5c4/support/python3/test-upgrade.py
----------------------------------------------------------------------
diff --git a/support/python3/test-upgrade.py b/support/python3/test-upgrade.py
new file mode 100755
index 0000000..a1745bd
--- /dev/null
+++ b/support/python3/test-upgrade.py
@@ -0,0 +1,254 @@
+#!/usr/bin/env python3
+#
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you 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.
+
+"""Script to test the upgrade path between two versions of Mesos."""
+
+import argparse
+import os
+import subprocess
+import sys
+import tempfile
+import time
+
+DEFAULT_PRINCIPAL = 'foo'
+DEFAULT_SECRET = 'bar'
+
+
+class Process(object):
+    """
+    Helper class to keep track of process lifecycles.
+
+    This class allows to start processes, capture their
+    output, and check their liveness during delays/sleep.
+    """
+
+    def __init__(self, args, environment=None):
+        """Initialize the Process."""
+        outfile = tempfile.mktemp()
+        fout = open(outfile, 'w')
+        print('Run %s, output: %s' % (args, outfile))
+
+        # TODO(nnielsen): Enable glog verbose logging.
+        self.process = subprocess.Popen(args,
+                                        stdout=fout,
+                                        stderr=subprocess.STDOUT,
+                                        env=environment)
+
+    def sleep(self, seconds):
+        """
+        Poll the process for the specified number of seconds.
+
+        If the process ends during that time, this method returns the process's
+        return value. If the process is still running after that time period,
+        this method returns `True`.
+        """
+        poll_time = 0.1
+        while seconds > 0:
+            seconds -= poll_time
+            time.sleep(poll_time)
+            poll = self.process.poll()
+            if poll != None:
+                return poll
+        return True
+
+    def __del__(self):
+        """Kill the Process."""
+        if self.process.poll() is None:
+            self.process.kill()
+
+
+class Agent(Process):
+    """Class representing an agent process."""
+
+    def __init__(self, path, work_dir, credfile):
+        """Initialize a Mesos agent by running mesos-slave.sh."""
+        Process.__init__(self, [os.path.join(path, 'bin', 'mesos-slave.sh'),
+                                '--master=127.0.0.1:5050',
+                                '--credential=' + credfile,
+                                '--work_dir=' + work_dir,
+                                '--resources=disk:2048;mem:2048;cpus:2'])
+
+
+class Master(Process):
+    """Class representing a master process."""
+
+    def __init__(self, path, work_dir, credfile):
+        """Initialize a Mesos master by running mesos-master.sh."""
+        Process.__init__(self, [os.path.join(path, 'bin', 'mesos-master.sh'),
+                                '--ip=127.0.0.1',
+                                '--work_dir=' + work_dir,
+                                '--authenticate',
+                                '--credentials=' + credfile,
+                                '--roles=test'])
+
+
+# TODO(greggomann): Add support for multiple frameworks.
+class Framework(Process):
+    """Class representing a framework instance (the test-framework for now)."""
+
+    def __init__(self, path):
+        """Initialize a framework."""
+        # The test-framework can take these parameters as environment variables,
+        # but not as command-line parameters.
+        environment = {
+            # In Mesos 0.28.0, the `MESOS_BUILD_DIR` environment variable in the
+            # test framework was changed to `MESOS_HELPER_DIR`, and the '/src'
+            # subdirectory was added to the variable's path. Both are included
+            # here for backwards compatibility.
+            'MESOS_BUILD_DIR': path,
+            'MESOS_HELPER_DIR': os.path.join(path, 'src'),
+            # MESOS_AUTHENTICATE is deprecated in favor of
+            # MESOS_AUTHENTICATE_FRAMEWORKS, although 0.28.x still expects
+            # previous one, therefore adding both.
+            'MESOS_AUTHENTICATE': '1',
+            'MESOS_AUTHENTICATE_FRAMEWORKS': '1',
+            'DEFAULT_PRINCIPAL': DEFAULT_PRINCIPAL,
+            'DEFAULT_SECRET': DEFAULT_SECRET
+        }
+
+        Process.__init__(self, [os.path.join(path, 'src', 'test-framework'),
+                                '--master=127.0.0.1:5050'], environment)
+
+
+def version(path):
+    """Get the Mesos version from the built executables."""
+    mesos_master_path = os.path.join(path, 'bin', 'mesos-master.sh')
+    process = subprocess.Popen([mesos_master_path, '--version'],
+                               stdout=subprocess.PIPE,
+                               stderr=subprocess.PIPE)
+    output, _ = process.communicate()
+    return_code = process.returncode
+    if return_code != 0:
+        return False
+
+    return output[:-1]
+
+
+def create_master(master_version, build_path, work_dir, credfile):
+    """Create a master using a specific version."""
+    print('##### Starting %s master #####' % master_version)
+    master = Master(build_path, work_dir, credfile)
+    if not master.sleep(0.5):
+        print('%s master exited prematurely' % master_version)
+        sys.exit(1)
+    return master
+
+
+def create_agent(agent_version, build_path, work_dir, credfile):
+    """Create an agent using a specific version."""
+    print('##### Starting %s agent #####' % agent_version)
+    agent = Agent(build_path, work_dir, credfile)
+    if not agent.sleep(0.5):
+        print('%s agent exited prematurely' % agent_version)
+        sys.exit(1)
+    return agent
+
+
+def test_framework(framework_version, build_path):
+    """Run a version of the test framework on a specified version of Mesos."""
+    print('##### Starting %s framework #####' % framework_version)
+    print('Waiting for %s framework to complete (10 sec max)...' % (
+        framework_version))
+    framework = Framework(build_path)
+    if framework.sleep(10) != 0:
+        print('%s framework failed' % framework_version)
+        sys.exit(1)
+
+
+# TODO(nnielsen): Add support for zookeeper and failover of master.
+# TODO(nnielsen): Add support for testing scheduler live upgrade/failover.
+def main():
+    """Main function to test the upgrade between two Mesos builds."""
+    parser = argparse.ArgumentParser(
+        description='Test upgrade path between two mesos builds')
+    parser.add_argument('--prev',
+                        type=str,
+                        help='Build path to mesos version to upgrade from',
+                        required=True)
+
+    parser.add_argument('--next',
+                        type=str,
+                        help='Build path to mesos version to upgrade to',
+                        required=True)
+    args = parser.parse_args()
+
+    # Get the version strings from the built executables.
+    prev_version = version(args.prev)
+    next_version = version(args.__next__)
+
+    if not prev_version or not next_version:
+        print('Could not get mesos version numbers')
+        sys.exit(1)
+
+    # Write credentials to temporary file.
+    credfile = tempfile.mktemp()
+    with open(credfile, 'w') as fout:
+        fout.write(DEFAULT_PRINCIPAL + ' ' + DEFAULT_SECRET)
+
+    # Create a work directory for the master.
+    master_work_dir = tempfile.mkdtemp()
+
+    # Create a work directory for the agent.
+    agent_work_dir = tempfile.mkdtemp()
+
+    print('Running upgrade test from %s to %s' % (prev_version, next_version))
+
+    print("""\
++--------------+----------------+----------------+---------------+
+| Test case    |   Framework    |     Master     |     Agent     |
++--------------+----------------+----------------+---------------+
+|    #1        |  %s\t| %s\t | %s\t |
+|    #2        |  %s\t| %s\t | %s\t |
+|    #3        |  %s\t| %s\t | %s\t |
+|    #4        |  %s\t| %s\t | %s\t |
++--------------+----------------+----------------+---------------+
+
+NOTE: live denotes that master process keeps running from previous case.
+    """ % (prev_version, prev_version, prev_version,
+           prev_version, next_version, prev_version,
+           prev_version, next_version, next_version,
+           next_version, next_version, next_version))
+
+    # Test case 1.
+    master = create_master(prev_version, args.prev, master_work_dir, credfile)
+    agent = create_agent(prev_version, args.prev, agent_work_dir, credfile)
+    test_framework(prev_version, args.prev)
+
+    # Test case 2.
+    # NOTE: Need to stop and start the agent because standalone detector does
+    # not detect master failover.
+    agent.process.kill()
+    master.process.kill()
+    master = create_master(next_version, args.__next__, master_work_dir,
+                           credfile)
+    agent = create_agent(prev_version, args.prev, agent_work_dir, credfile)
+    test_framework(prev_version, args.prev)
+
+    # Test case 3.
+    agent.process.kill()
+    agent = create_agent(next_version, args.__next__, agent_work_dir, credfile)
+    test_framework(prev_version, args.prev)
+
+    # Test case 4.
+    test_framework(next_version, args.__next__)
+
+    # Tests passed.
+    sys.exit(0)
+
+if __name__ == '__main__':
+    main()

http://git-wip-us.apache.org/repos/asf/mesos/blob/960df5c4/support/python3/verify-reviews.py
----------------------------------------------------------------------
diff --git a/support/python3/verify-reviews.py b/support/python3/verify-reviews.py
new file mode 100755
index 0000000..2e92590
--- /dev/null
+++ b/support/python3/verify-reviews.py
@@ -0,0 +1,318 @@
+#!/usr/bin/env python3
+#
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you 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.
+
+"""
+This script is used to build and test (verify) reviews that are posted
+to ReviewBoard. The script is intended for use by automated "ReviewBots"
+that are run on ASF infrastructure (or by anyone that wishes to donate
+some compute power). For example, see 'support/jenkins/reviewbot.sh'.
+
+The script performs the following sequence:
+* A query grabs review IDs from Reviewboard.
+* In reverse order (most recent first), the script determines if the
+  review needs verification (if the review has been updated or changed
+  since the last run through this script).
+* For each review that needs verification:
+  * The review is applied (via 'support/python3/apply-reviews.py').
+  * Mesos is built and unit tests are run.
+  * The result is posted to ReviewBoard.
+"""
+
+import atexit
+import json
+import os
+import platform
+import subprocess
+import sys
+import urllib.error
+import urllib.parse
+import urllib.request
+
+from datetime import datetime
+
+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):
+    """Exception returned by post_review()."""
+    pass
+
+
+def shell(command):
+    """Run a 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):
+    """Call the ReviewBoard API."""
+    try:
+        auth_handler = urllib.request.HTTPBasicAuthHandler()
+        auth_handler.add_password(
+            realm="Web API",
+            uri="reviews.apache.org",
+            user=USER,
+            passwd=PASSWORD)
+
+        opener = urllib.request.build_opener(auth_handler)
+        urllib.request.install_opener(opener)
+
+        return json.loads(urllib.request.urlopen(url, data=data).read())
+    except urllib.error.HTTPError as err:
+        print("Error handling URL %s: %s (%s)" % (url, err.reason, err.read()))
+        exit(1)
+    except urllib.error.URLError as err:
+        print("Error handling URL %s: %s" % (url, err.reason))
+        exit(1)
+
+
+def apply_review(review_id):
+    """Apply a review using the script apply-reviews.py."""
+    print("Applying review %s" % review_id)
+    shell("python support/python3/apply-reviews.py -n -r %s" % review_id)
+
+
+def apply_reviews(review_request, reviews):
+    """Apply multiple reviews at once."""
+    # 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):
+    """Post a review on the review board."""
+    print("Posting review: %s" % message)
+
+    review_url = review_request["links"]["reviews"]["href"]
+    data = urllib.parse.urlencode({'body_top': message, 'public': 'true'})
+    api(review_url, data)
+
+
+@atexit.register
+def cleanup():
+    """Clean the git repository."""
+    try:
+        shell("git clean -fd")
+        shell("git reset --hard %s" % HEAD)
+    except subprocess.CalledProcessError as err:
+        print("Failed command: %s\n\nError: %s" % (err.cmd, err.output))
+
+
+def verify_review(review_request):
+    """Verify a review."""
+    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.
+
+        command = ""
+        if platform.system() == 'Windows':
+            command = "support\\windows-build.bat"
+
+            # There is no equivalent to `tee` on Windows.
+            subprocess.check_call(
+                ['cmd', '/c', '%s 2>&1 > %s' % (command, build_output)])
+        else:
+            # 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 "
+                             "--disable-libtool-wrappers' "
+                             "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 ispreserved 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 err:
+        # 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`.
+        if os.path.exists(build_output):
+            output = open(build_output).read()
+        else:
+            output = err.output
+
+        if platform.system() == 'Windows':
+            # We didn't output anything during the build (because `tee`
+            # doesn't exist), so we print the output to stdout upon error.
+
+            # Pylint raises a no-member error on that line due to a bug
+            # fixed in pylint 1.7.
+            # TODO(ArmandGrillet): Remove this once pylint updated to >= 1.7.
+            # pylint: disable=no-member
+            sys.stdout.buffer.write(output)
+
+        # Truncate the output when posting the review as it can be very large.
+        if len(output) > REVIEW_SIZE:
+            output = "...<truncated>...\n" + output[-REVIEW_SIZE:]
+
+        output += "\nFull log: "
+        output += urllib.parse.urljoin(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, err.cmd, output))
+    except ReviewError as err:
+        post_review(
+            review_request,
+            "Bad review!\n\n" \
+            "Reviews applied: %s\n\n" \
+            "Error:\n%s" % (reviews, err.args[0]))
+
+    # Clean up.
+    cleanup()
+
+
+def needs_verification(review_request):
+    """Return True if this review request needs to be verified."""
+    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 not diffs["diffs"]:  # No diffs attached!
+        print("Skipping review %s as it has no diffs" % review_request["id"])
+        return False
+
+    # Get the timestamp of the latest diff.
+    timestamp = diffs["diffs"][-1]["timestamp"]
+    rb_date_format = "%Y-%m-%dT%H:%M:%SZ"
+    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)
+
+
+def main():
+    """Main function to verify the submitted reviews."""
+    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
+
+if __name__ == '__main__':
+    main()