diff --git a/monitoring/ci-status/README.rst b/monitoring/ci-status/README.rst
new file mode 100644
index 0000000..9822cde
--- /dev/null
+++ b/monitoring/ci-status/README.rst
@@ -0,0 +1,125 @@
+CI Status Tool
+==============
+
+Used for a quick stats collection on Third Party CIs for various OpenStack
+projects.
+
+Example usage:
+--------------
+
+.. code-block:: bash
+
+    $ ./ci-status.py -v -u datera-ci \\
+            -k /home/user/.ssh/id_rsa \\
+            -c "Datera CI" -a datera-dsvm-full -t 2 \\
+            -j openstack/cinder \\
+            --failures --number-of-reports --is-reporting \\
+            --jenkins_disagreement
+
+Output:
+-------
+
+.. code-block:: text
+
+    Gerrit Query: ssh -i /home/user/.ssh/id_rsa -p 29418 dater
+    a-ci@review.openstack.org "gerrit query --format=JSON --comments --current-
+    patch-set project:openstack/cinder NOT age:2d  reviewer:Datera CI "
+
+    ##### DATERA-DSVM-FULL #####
+
+    ####### --number-of-reports arg result #######
+
+    40 results in 2 days
+
+    ###### --is-reporting arg result #######
+
+    Review: 263026 --> 2016-07-07T17:02:15+00:00
+
+    ###### --failures arg result #######
+
+    20% failures
+
+    ###### --jenkins-disagreement arg result #######
+
+    0% -1 Jenkins && +1 CI
+    20% +1 Jenkins && -1 CI
+
+Minimal usage:
+--------------
+
+.. code-block:: bash
+
+    $ ./ci-status.py -u datera-ci -k /home/user/.ssh/id_rsa \\
+            -j openstack/cinder -c "Datera CI" -a datera-dsvm-full \\
+            --is-reporting
+
+Output:
+-------
+
+.. code-block:: text
+
+    ##### DATERA-DSVM-FULL #####
+    Review: 263026 --> 2016-07-07T17:02:15+00:00
+
+Passthrough query usage:
+------------------------
+
+.. code-block:: bash
+
+    $ ./ci-status.py -u datera-ci -k /home/user/.ssh/id_rsa \\
+            -q "reviewer:{Some Body} -j openstack/cinder"
+
+Output:
+-------
+
+    Will be a large dictionary
+
+Config example:
+---------------
+
+.. code-block:: ini
+
+    # In .gerritqueryrc file in your $HOME directory
+    # (or passed in via config option)
+
+    [DEFAULT]
+    verbose=True
+    host=review.openstack.org
+    username=datera-ci
+    port=29418
+    query_project=openstack/cinder
+    keyfile=/home/user/.ssh/id_rsa
+
+    # I would not recommend putting any other flags into this config
+    # file otherwise you could introduce silent errors
+    # For example:
+
+    # Adding these fields
+    ci_account=datera-ci
+    ci_runner_name=datera-dsvm-full
+
+    # Then running this command
+    # $ ./ci-status.py -c mellanox-ci --is-reporting
+
+    # Would report a false negative for Datera. A CI
+    # will show as non-reporting if you provide the
+    # ci_account name of one CI and the ci_runner_name of
+    # a different CI.  The tool has no way to tell that
+    # these values do not belong together and will just
+    # report that the CI has not posted within the specified
+    # timeframe.
+
+The "--all" flag:
+-----------------
+
+.. code-block:: bash
+
+    # In order to use this flag, you must first run this command:
+    $ ./ci-status.py --scrape-wiki --force -j openstack/your_project
+
+
+    # It will fill your .gerritquerycache file with information about
+    # the various CIs for your desired OpenStack project
+
+    # Now you're free to run commands with the --all flag
+    $ ./ci-status -j openstack/you_project --all --is-reporting
diff --git a/monitoring/ci-status/ci-status.py b/monitoring/ci-status/ci-status.py
new file mode 100755
index 0000000..6ab7870
--- /dev/null
+++ b/monitoring/ci-status/ci-status.py
@@ -0,0 +1,760 @@
+#!/usr/bin/env python
+"""
+CI Status Tool
+
+Used for a quick stats collection on Third Party CIs for various OpenStack
+projects.
+
+Example usage:
+    $ ./ci-status.py -v -u datera-ci \\
+            -k /home/user/.ssh/id_rsa \\
+            -c "Datera CI" -a datera-dsvm-full -t 2 \\
+            -j openstack/cinder \\
+            --failures --number-of-reports --is-reporting \\
+            --jenkins_disagreement
+
+Output:
+    Gerrit Query: ssh -i /home/user/.ssh/id_rsa -p 29418 dater
+    a-ci@review.openstack.org "gerrit query --format=JSON --comments --current-
+    patch-set project:openstack/cinder NOT age:2d  reviewer:Datera CI "
+
+    ##### DATERA-DSVM-FULL #####
+
+    ####### --number-of-reports arg result #######
+
+    40 results in 2 days
+
+    ###### --is-reporting arg result #######
+
+    Review: 263026 --> 2016-07-07T17:02:15+00:00
+
+    ###### --failures arg result #######
+
+    20% failures
+
+    ###### --jenkins-disagreement arg result #######
+
+    0% -1 Jenkins && +1 CI
+    20% +1 Jenkins && -1 CI
+
+Minimal usage:
+    $ ./ci-status.py -u datera-ci -k /home/user/.ssh/id_rsa \\
+            -j openstack/cinder -c "Datera CI" -a datera-dsvm-full \\
+            --is-reporting
+
+Output:
+    ##### DATERA-DSVM-FULL #####
+    Review: 263026 --> 2016-07-07T17:02:15+00:00
+
+Passthrough query usage:
+    $ ./ci-status.py -u datera-ci -k /home/user/.ssh/id_rsa \\
+            -q "reviewer:{Some Body} -j openstack/cinder"
+
+Output:
+    Will be a large dictionary
+
+Config example:
+
+    # In .gerritqueryrc file in your $HOME directory
+    # (or passed in via config option)
+
+    [DEFAULT]
+    verbose=True
+    host=review.openstack.org
+    username=datera-ci
+    port=29418
+    query_project=openstack/cinder
+    keyfile=/home/user/.ssh/id_rsa
+
+    # I would not recommend putting any other flags into this config
+    # file otherwise you could introduce silent errors
+    # For example:
+
+        # Adding these fields
+        ci_account=datera-ci
+        ci_runner_name=datera-dsvm-full
+
+        # Then running this command
+        $ ./ci-status.py -c mellanox-ci --is-reporting
+
+        # Would report a false negative for Datera. A CI
+        # will show as non-reporting if you provide the
+        # ci_account name of one CI and the ci_runner_name of
+        # a different CI.  The tool has no way to tell that
+        # these values do not belong together and will just
+        # report that the CI has not posted within the specified
+        # timeframe.
+
+The "--all" flag:
+    # In order to use this flag, you must first run this command:
+    $ ./ci-status.py --scrape-wiki --force -j openstack/your_project
+
+    # It will fill your .gerritquerycache file with information about
+    # the various CIs for your desired OpenStack project
+
+    # Now you're free to run commands with the --all flag
+    $ ./ci-status -j openstack/you_project --all --is-reporting
+"""
+
+from __future__ import print_function, unicode_literals
+
+import re
+import sys
+import functools
+import subprocess
+import shlex
+import os.path
+import threading
+import Queue
+from urlparse import urljoin
+from StringIO import StringIO
+
+import arrow
+import simplejson as json
+import requests
+from lxml import etree
+
+from oslo_config import cfg
+
+opts = [
+    cfg.BoolOpt('verbose',
+                short='v',
+                default=False),
+    cfg.StrOpt('host',
+               short='s',
+               default=None,
+               help=('Eg. review.openstack.org')),
+    cfg.StrOpt('username',
+               short='u',
+               default=None,
+               help=('Gerrit CI username')),
+    cfg.StrOpt('keyfile',
+               short='k',
+               default=None,
+               help=('Gerrit CI ssh private keyfile')),
+    cfg.StrOpt('port',
+               short='p',
+               default=None,
+               help=('Gerrit CI ssh port')),
+    cfg.StrOpt('query-project',
+               short='j',
+               default=None,
+               help=('Gerrit CI project to query')),
+    cfg.IntOpt('time',
+               short='t',
+               default=2,
+               help=('Time in days for query, default=2')),
+    cfg.StrOpt('query',
+               short='q',
+               default='',
+               help=('Passthrough Query')),
+    cfg.StrOpt('ci-account',
+               short='c',
+               default='',
+               help=('Filter result by this CI account (name or username)'
+                     'eg. "Datera CI" or "datera-ci"')),
+    cfg.StrOpt('ci-runner-name',
+               short='a',
+               default='',
+               help=("Specific CI runner (eg. datera-dsvm-full) "
+                     "to use for filtering results.  Useful if a "
+                     "single CI account posts results for multiple "
+                     "drivers. Can only be used if '--all' is NOT used")),
+    cfg.BoolOpt('is-reporting',
+                default=False,
+                help=('Report if the CI specified by "-c/--ci-account"'
+                      'reported within the time specified by "-t/--time"')),
+    cfg.BoolOpt('number-of-reports',
+                default=False,
+                help=("Show number of reports within the last "
+                      "'-t/--time' days")),
+    cfg.BoolOpt('failures',
+                default=False,
+                help=("Show number and percentage of failures for last "
+                      "'-t/--time' days")),
+    cfg.BoolOpt('jenkins-disagreement',
+                default=False,
+                help=("Print percentage of disagreements between "
+                      "CI and Jenkins")),
+    cfg.BoolOpt('number-of-rechecks',
+                default=False,
+                help=("Print number of rechecks for a ci within the last"
+                      "'-t/--time' days.  Heavily relies on information from "
+                      "the ThirdPartyWiki.  Should only be used for "
+                      "heruistic purposes")),
+    cfg.BoolOpt('show-contacts',
+                default=False,
+                help=("Print contacts for each CI displayed")),
+    cfg.BoolOpt('contact-list',
+                default=False,
+                help=("Print all contacts in project cache")),
+    cfg.BoolOpt('contact-list-compact',
+                default=False,
+                help=("Print all contacts in project cache compactly")),
+    cfg.BoolOpt('not-reporting-list',
+                default=False,
+                help=("Print all CIs for the project that have NOT reported "
+                      "in the last '-t/--time' days")),
+    cfg.BoolOpt('scrape-wiki',
+                default=False,
+                help=("Scrapes the wiki for CIs matching the project "
+                      "specified by '-j/--query-project'.  Use '-f/--force' "
+                      "to update from wiki instead of displaying cache")),
+    cfg.BoolOpt('force',
+                short='f',
+                default=False,
+                help=("Forces cache update")),
+    cfg.BoolOpt('all',
+                default=False,
+                help=("Forces switches to be run on all detected CIs")),
+]
+
+
+CONF = cfg.ConfigOpts()
+CONF.register_opts(opts)
+CONF.register_cli_opts(opts)
+
+CONFIG_FILE_NAME = os.path.join(os.path.expanduser("~"), '.gerritqueryrc')
+CACHE_FILE_NAME = os.path.join(os.path.expanduser("~"), '.gerritquerycache')
+WIKI_BASE_URL = "http://wiki.openstack.org"
+THIRD_PARTY_WIKI_URL = urljoin(WIKI_BASE_URL, "/wiki/ThirdPartySystems")
+WORKERS = 5
+
+# From emailregex.com.  Should be fine for heuristically extracting emails
+# we're not doing any validation :)
+EMAIL_REGEX = r"([a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+)"
+
+
+def main():
+    CONF(default_config_files=[CONFIG_FILE_NAME])
+
+    nargs = CONF
+    pname = nargs.query_project.split("/")[-1]
+    gquery = functools.partial(_gquery_base,
+                               nargs.verbose,
+                               nargs.keyfile,
+                               nargs.port,
+                               nargs.username,
+                               nargs.host,
+                               nargs.query_project,
+                               nargs.time)
+
+    # ###############################################
+    # ############ Argument Handling ################
+    # ###############################################
+
+    if nargs.scrape_wiki:
+        if nargs.verbose:
+            print("\n####### --scrape-wiki result ######\n")
+        from pprint import pprint
+        pprint(get_reporting_dict(
+               pname,
+               force_update=nargs.force))
+
+    if nargs.contact_list or nargs.contact_list_compact:
+        contacts = []
+        cis = get_reporting_dict(
+                  pname,
+                  force_update=nargs.force).keys()
+        for ci in cis:
+            contacts.extend(get_email_contacts(ci, pname))
+        curr = contacts[0].split('@')[1]
+        prev = curr
+        for c in sorted(contacts, key=lambda x: x.split('@')[1]):
+            prev = curr
+            curr = c.split('@')[1]
+            if prev != curr and not nargs.contact_list_compact:
+                print()
+            print(c)
+
+    if not any((nargs.query,
+                nargs.is_reporting,
+                nargs.failures,
+                nargs.jenkins_disagreement,
+                nargs.number_of_reports,
+                nargs.number_of_rechecks,
+                nargs.show_contacts,
+                nargs.not_reporting_list)):
+        exit(0)
+
+    # Handle defaults depending on if --all is passed
+    if not nargs.all:
+        cis = [(nargs.ci_account, nargs.ci_runner_name)]
+        runner = nargs.ci_runner_name
+        results = gquery(
+                 " ".join((nargs.query,
+                           "reviewer:{}".format(nargs.ci_account))))
+    else:
+        # We don't want to hit the wiki twice this one should always
+        # be from the cache
+        cis = [(k, v['name']) for (k, v) in get_reporting_dict(
+                pname, force_update=False).items()]
+        runner = ''
+        results = gquery(
+                 " ".join((nargs.query)))
+
+    for ci, name in sorted(cis, key=lambda x: x[1]):
+        if not nargs.not_reporting_list:
+            print("\n##### {} #####".format(name.upper()))
+        if nargs.number_of_reports:
+            if nargs.verbose:
+                print("\n####### --number-of-reports arg result #######\n")
+            print_number_of_reports(results,
+                                    ci,
+                                    runner,
+                                    nargs.time)
+
+        if nargs.is_reporting:
+            if nargs.verbose:
+                print("\n###### --is-reporting arg result #######\n")
+            print_is_reporting(results,
+                               ci,
+                               runner,
+                               nargs.time)
+
+        if nargs.failures:
+            if nargs.verbose:
+                print("\n###### --failures arg result #######\n")
+            print_failure_results(results,
+                                  ci,
+                                  runner,
+                                  nargs.time)
+
+        if nargs.jenkins_disagreement:
+            if nargs.verbose:
+                print("\n###### --jenkins-disagreement arg result #######\n")
+            print_jenkins_disagreement(results,
+                                       ci,
+                                       runner,
+                                       nargs.time)
+
+        if nargs.number_of_rechecks:
+            if nargs.verbose:
+                print("\n###### --number-of-rechecks arg result #######\n")
+            print_number_of_rechecks(results,
+                                     ci,
+                                     runner,
+                                     nargs.time,
+                                     pname)
+
+        if nargs.show_contacts:
+            if nargs.verbose:
+                print("\n####### --show-contacts result ######\n")
+            print_email_contacts(ci, pname)
+
+        if nargs.query:
+            if nargs.verbose:
+                print("\n####### --query result ######\n")
+            print(gquery(nargs.query))
+
+    if nargs.not_reporting_list and nargs.all:
+        if nargs.verbose:
+            print("\n####### --not-reporting-list result ######\n")
+        not_reporting = []
+        for ci, name in cis:
+            result = get_is_reporting(results, ci, runner, nargs.time)
+            if all(result):
+                not_reporting.append(name)
+        print("These CIs have not reported in {} days".format(nargs.time))
+        for nr in not_reporting:
+            print(nr.upper())
+    # #######################################################
+
+    exit(0)
+
+
+def _base(keyfile, port, username, host):
+    return 'ssh -i {keyfile} -p {port} {username}@{host}'.format(
+        keyfile=keyfile, port=port, username=username, host=host)
+
+
+def _gquery_base(verbose, keyfile, port, username, host, project, time, query):
+    """
+    Makes a large bulk-query to gerrit that can be further filtered
+    after-the-fact so we play nice and don't hammer gerrit
+
+    The size of the query largly depends on the "time" argument.  It's used
+    to determine how many days old results can be.
+
+        Eg.  time == 1  --> Only results >= 1 day old
+
+    The time argument is also reliant on the "last modified" date, not
+    "creation date".  So a review created six months ago, but modified two
+    days ago will show up in a "time == 2" query
+    """
+    cmd = " ".join((_base(keyfile, port, username, host),
+                   ("\"gerrit query --format=JSON --comments "
+                    "--current-patch-set project:{} NOT age:{}d".format(
+                        project, time)),
+                    query,
+                    "\""))
+    if verbose:
+        print("Gerrit Query: {}".format(cmd))
+    # Strip the last result line which is just query metadata
+    result = subprocess.check_output(shlex.split(cmd)).splitlines()[:-1]
+    return [json.loads(elem) for elem in result]
+
+
+def get_ci_comments(result, ci):
+    """
+    Returns a list of comments that match the specified CI as the reviewer in
+    either name or username
+    """
+    return filter(
+        lambda x: ci.upper() in (x['reviewer'].get('username', "").upper(),
+                                 x['reviewer'].get('name', "").upper()),
+        result['comments'])
+
+
+def get_recheck_comments(result, ci_check):
+    """
+    ci_check is the recheck/rerun string for a specific ci as listed on
+    its wiki page
+
+    Makes the assumption that CI reviewer names will end with CI and
+    filters out those comments to ensure we're not accidentally counting
+    a comment made by a CI
+
+    For example: Mellanox CI n the following example post a comment
+    containing their recheck syntax, so this would be a false positive
+    recheck request if we did not filter out all CI comments:
+
+        "Build succeeded.  Cinder-ISER-ISCSI SUCCESS in 35m 11s
+         Cinder-ISER-LIO SUCCESS in 35m 10s To re-run the job post
+         'recheck cinder-mlnx' comment. For more information visit
+         https://wiki.openstack.org/wiki/ThirdPartySystems/Mellanox_CI"
+    """
+    return filter(
+        lambda x: (not x['reviewer']['name'].upper().endswith("CI") and
+                   ci_check in x['message']),
+        result['comments'])
+
+
+def most_recent_ci_comment_timestamp(result, ci):
+    """ Returns (review_id, timestamp, message) if comments exist for the CI
+        Otherwise returns (review_id, timestamp(0), '')"""
+    filtered = get_ci_comments(result, ci)
+    try:
+        body = sorted(filtered,
+                      key=lambda x: x['timestamp'],
+                      reverse=True)[0]
+
+        return (result['number'],
+                arrow.get(body['timestamp']),
+                body['message'])
+
+    except IndexError:
+        return (result['number'],
+                arrow.get(0),
+                '')
+
+# ###############################################
+# ############# Report Functions ################
+# ###############################################
+
+"""
+The 'results' argument accepted by these functions is an array of dicts
+with these keys:
+
+    ['status',
+     'topic',
+     'currentPatchSet',
+     'url',
+     'commitMessage',
+     'createdOn',
+     'number',
+     'lastUpdated',
+     'project',
+     'comments',
+     'branch',
+     'owner',
+     'open',
+     'id',
+     'subject']
+
+This array is pulled directly from the Gerrit server via the constructed
+query above.  Most of the logic is done on the 'comments' section of the
+data structure since Third-Party CI results are left as comments on gerrit
+reviews.
+
+Only the most recent comments of any CI including Jenkins are taken into
+account for printed statistics.  This might be beefed up in the future but for
+not it's the easiest implementation
+"""
+
+
+def get_number_of_reports(results, ci, runner, argtime):
+    """
+    Report generator for --number-of-reports cli option
+    """
+    count = 0
+    for result in results:
+        review, tstamp, message = most_recent_ci_comment_timestamp(
+            result, ci)
+        if (arrow.now() - tstamp <=
+                arrow.now() - arrow.now().replace(days=-argtime) and
+                runner in message):
+            count += 1
+    return count
+
+
+def get_is_reporting(results, ci, runner, argtime):
+    """
+    Report generator for --is-reporting cli option
+    """
+    if not results:
+        return None
+    rtstamp = arrow.get(0)
+    rreview = 0
+    for result in results:
+        review, tstamp, message = most_recent_ci_comment_timestamp(
+            result, ci)
+        if tstamp > rtstamp:
+            rtstamp = tstamp
+            rreview = review
+    return (rreview, rtstamp, message)
+
+
+def get_failure_results(results, ci, runner, argtime):
+    """
+    Report generator for --failure cli option
+    """
+    count = 0
+    fail_count = 0
+    for result in results:
+        review, tstamp, message = most_recent_ci_comment_timestamp(
+            result, ci)
+        if (arrow.now() - tstamp <=
+                arrow.now() - arrow.now().replace(days=-argtime) and
+                runner in message):
+            count += 1
+            if "FAILURE" in message:
+                fail_count += 1
+    return (count, fail_count)
+
+
+def get_jenkins_disagreement(results, ci, runner, argtime):
+    count = 0
+    negative_disagree_count = 0
+    positive_disagree_count = 0
+    for result in results:
+        jenkins_failure = False
+        _, jtstamp, jmessage = most_recent_ci_comment_timestamp(
+            result, 'jenkins')
+        review, tstamp, message = most_recent_ci_comment_timestamp(
+            result, ci)
+
+        # Rough way of determining that both Jenkins and CI are referring
+        # to the same patchset
+        if tstamp >= jtstamp:
+
+            # We only care if the result is within '-t' time
+            if (arrow.now() - tstamp <=
+                    arrow.now() - arrow.now().replace(days=-argtime) and
+                    runner in message):
+                count += 1
+
+                # Filter out non-voting/stylistic/unit checks
+                # 3rd party CIs don't run these checks, so we don't
+                # care if they disagree
+                jmessages = [elem for elem in jmessage.splitlines()
+                             if not any([f in elem for f in ("non-voting",
+                                                             "python",
+                                                             "pylint",
+                                                             "pep8",
+                                                             "docs",)])]
+                if any("FAILURE" in jm for jm in jmessages):
+                    jenkins_failure = True
+
+                if "FAILURE" not in message and jenkins_failure:
+                    positive_disagree_count += 1
+
+                if "FAILURE" in message and not jenkins_failure:
+                    negative_disagree_count += 1
+    return (count, negative_disagree_count, positive_disagree_count)
+
+
+def get_rechecks(results, ci, runner, argtime, project):
+    check = get_reporting_dict(project)[ci].get('retry')
+    if not check:
+        return None, None
+    rechecks = 0
+    for result in results:
+        comments = get_recheck_comments(result, check)
+        for comment in comments:
+            tstamp = arrow.get(comment['timestamp'])
+            if (arrow.now() - tstamp <=
+                    arrow.now() - arrow.now().replace(days=-argtime)):
+                rechecks += 1
+    return (rechecks, check)
+
+
+def get_email_contacts(ci, project):
+    return get_reporting_dict(project)[ci].get('contact')
+
+
+def get_reporting_dict(project, force_update=False):
+    """
+    Report generator for --scrape-wiki cli option
+    """
+    # Handle cache creation and loading
+    cache = {}
+    if os.path.exists(CACHE_FILE_NAME):
+        with open(CACHE_FILE_NAME) as f:
+            cache = json.load(f)
+    else:
+        with open(CACHE_FILE_NAME, 'w') as f:
+            json.dump(cache, f)
+
+    # By default we're just going to return the cache
+    if not force_update:
+        return cache.get(project)
+
+    # Heavy update requests depending on wiki page links
+    with requests.session() as c:
+        resp = c.get(THIRD_PARTY_WIKI_URL)
+        parser = etree.HTMLParser()
+        root = etree.parse(StringIO(resp.text), parser).getroot()
+        links = root.xpath('//a')
+
+        # Now that we have all the links, we'll spawn a bunch of
+        # worker threads and request them in parallel
+        q = Queue.Queue()
+        [q.put(urljoin(WIKI_BASE_URL, elem.attrib['href']))
+         for elem in links
+         if elem.text and elem.text.upper().endswith("CI")]
+        workers = []
+        results = {}
+        for _ in xrange(WORKERS):
+            workers.append(
+                threading.Thread(target=_link_checker,
+                                 args=(project, c, parser, q, results)))
+        for thread in workers:
+            thread.daemon = True
+            thread.start()
+
+        q.join()
+        cache[project] = results
+
+        # Truncate file since we're writing the whole cache
+        with open(CACHE_FILE_NAME, 'w') as f:
+            json.dump(cache, f)
+        return results
+
+
+# ###############################################
+# ####### CommandLine Print Functions ###########
+# ###############################################
+
+
+def print_failure_results(results, ci, runner, argtime):
+    count, fail_count = get_failure_results(results, ci, runner, argtime)
+    if count > 0:
+        print("{}% failures".format(
+            int(float(fail_count)/count * 100)))
+
+
+def print_is_reporting(results, ci, runner, argtime):
+    try:
+        rreview, rtstamp, message = \
+            get_is_reporting(results, ci, runner, argtime)
+    except ValueError:
+        print("No results found, ci: {}".format(ci))
+        return
+
+    if (arrow.now() - rtstamp <=
+            arrow.now() - arrow.now().replace(days=-argtime) and
+            runner in message):
+        print("Review: {} --> {}".format(
+            rreview, arrow.get(rtstamp)))
+    else:
+        print("{}+ days".format(argtime))
+
+
+def print_number_of_reports(results, ci, runner, argtime):
+    count = get_number_of_reports(results, ci, runner, argtime)
+    print("{} results in {} days".format(
+        count, argtime))
+
+
+def print_jenkins_disagreement(results, ci, runner, argtime):
+    count, negative_disagree_count, positive_disagree_count = \
+        get_jenkins_disagreement(results, ci, runner, argtime)
+    if count > 0:
+        print("{}% -1 Jenkins && +1 CI".format(
+            int(float(positive_disagree_count)/count * 100)))
+        print("{}% +1 Jenkins && -1 CI".format(
+            int(float(negative_disagree_count)/count * 100)))
+
+
+def print_number_of_rechecks(results, ci, runner, argtime, project):
+    recheck_count, check = get_rechecks(results, ci, runner, argtime, project)
+    if recheck_count is None:
+        print("No recheck string found on Wiki page")
+    else:
+        print("{} rechecks in {} days, recheck string: {}".format(
+            recheck_count, argtime, check))
+
+
+def print_email_contacts(ci, project):
+    print("Contact: {}".format(get_email_contacts(ci, project)))
+
+# ###############################################
+
+
+# Used for Thread workers
+def _link_checker(project, session, parser, q, results):
+    while True:
+        try:
+            link = q.get()
+            resp = session.get(link)
+            root = etree.parse(StringIO(resp.text), parser).getroot()
+            # Grab all the table data entries
+            tds = root.xpath("//td/b")
+            try:
+                # Filter for supported openstack services
+                text = [elem.tail for elem in tds
+                        if elem.text.upper() == "OPENSTACK PROGRAMS"][0]
+                # Filter for gerrit account name
+                if project.upper() in text.upper():
+                    name = [elem.tail for elem in tds
+                            if elem.text.upper() == "GERRIT ACCOUNT"][0]
+                    # Handle oddly formatted names
+                    name = name.lstrip(": ").split()[0].split("@")[0]
+                    results[name] = {}
+                    results[name]['name'] = link.split("/")[-1].upper()
+                    # Try and get a retry syntax from the page
+                    try:
+                        # Ugly stripping to handle the weird inconsistent
+                        # formats used on the wiki
+                        retry_elem = root.xpath(
+                            "//p/b")[0].tail.strip().strip(
+                                    "\"'").split("(")[0].strip().strip("\"")
+                        for char in (":", "="):
+                            if char in retry_elem:
+                                retry_elem = retry_elem.split(
+                                    char)[1].strip().strip("\"")
+                        # Any CI with this sting hasn't updated
+                        # their wiki page with a retry syntax
+                        if "please update" not in retry_elem.lower():
+                            results[name]['retry'] = retry_elem
+                    except IndexError:
+                        pass
+                    # Try and get maintaner info from the page
+                    try:
+                        contact_elem = [
+                            elem.tail for elem in tds
+                            if elem.text.upper() == "CONTACT INFORMATION"][0]
+                        emails = re.findall(EMAIL_REGEX, contact_elem)
+                        results[name]['contact'] = emails
+                    except IndexError:
+                        pass
+            except IndexError:
+                print("No 'OpenStack Programs' or 'Gerrit Account' field",
+                      link)
+            q.task_done()
+        except Queue.Empty:
+            return
+
+if __name__ == "__main__":
+    sys.exit(main())
diff --git a/monitoring/ci-status/requirements.txt b/monitoring/ci-status/requirements.txt
new file mode 100644
index 0000000..07b5fec
--- /dev/null
+++ b/monitoring/ci-status/requirements.txt
@@ -0,0 +1,5 @@
+arrow
+simplejson
+requests
+lxml
+oslo.config>=3.12.0