Import ci scoreboard tool.

This is a simple set of scripts that can listen to gerrit, track 3rd
party ci systems reviews, and display a web dashboard showing their
results.

A demo system can be found here:

http://ec2-54-67-102-119.us-west-1.compute.amazonaws.com:5000/?project
=openstack%2Fcinder&user=&timeframe=24

It is not intended to do much more than just show success/fail, and can
work nicely as a quick way to compare your systems results with others
to catch any issues you might be having.

Change-Id: I682c5426fe834a63d3e4f27ebde7d40ee7f9749b
This commit is contained in:
Patrick East 2015-05-11 13:37:37 -07:00
parent 98ff04fb0a
commit c0d0d4cac8
20 changed files with 10880 additions and 0 deletions

4
monitoring/scoreboard/.gitignore vendored Normal file
View File

@ -0,0 +1,4 @@
.idea
*.pyc
.venv
app.db

View File

@ -0,0 +1,31 @@
Very simple 3rd party CI dashboard tool
=======================================
It is two python scripts, one is a Flask app that serves up the UI and handles
REST calls. The other one monitors gerrit and records ci results in the database.
Requires:
* mongodb
* python-dev
* python-pip
* virtualenv
Setup the config files.. alter the path in config.py to match the location
of ci-scoreboard.conf. And update the ci-scoreboard.conf to have the right
values for your gerrit account, keyfile, and mongodb server.
To run the server first init things with:
`./env.sh`
Then source the virtual environment:
`source ./.venv/bin/activate`
And run the app with:
`./scoreboard_ui.py runserver`
`./scoreboard_gerrit_listener.py`

View File

@ -0,0 +1,7 @@
[scoreboard]
GERRIT_USER: some-ci-user
GERRIT_KEY: /home/ubuntu/.ssh/gerrit_key
GERRIT_HOSTNAME: review.openstack.org
GERRIT_PORT: 29418
DB_URI: mongodb://localhost:27017
LOG_FILE_LOCATION: scoreboard.log

View File

@ -0,0 +1,44 @@
import ConfigParser
CONFIG_FILE = '/etc/ci-scoreboard/ci-scoreboard.conf'
CONFIG_SECTION = 'scoreboard'
class Config:
def __init__(self):
self._cfg = ConfigParser.ConfigParser()
self._cfg.read(CONFIG_FILE)
def _value(self, option):
if self._cfg.has_option(CONFIG_SECTION, option):
return self._cfg.get(CONFIG_SECTION, option)
return None
def _int_value(self, option):
if self._cfg.has_option(CONFIG_SECTION, option):
return self._cfg.getint(CONFIG_SECTION, option)
return None
def _float_value(self, option):
if self._cfg.has_option(CONFIG_SECTION, option):
return self._cfg.getfloat(CONFIG_SECTION, option)
return None
def gerrit_user(self):
return self._value('GERRIT_USER')
def gerrit_key(self):
return self._value('GERRIT_KEY')
def gerrit_hostname(self):
return self._value('GERRIT_HOSTNAME')
def gerrit_port(self):
return self._int_value('GERRIT_PORT')
def db_uri(self):
return self._value('DB_URI')
def log_file(self):
return self._value('LOG_FILE_LOCATION')

View File

@ -0,0 +1,10 @@
import pymongo
class DBHelper:
def __init__(self, config):
self._mongo_client = pymongo.MongoClient(config.db_uri())
self._db = self._mongo_client.scoreboard
def get(self):
return self._db

16
monitoring/scoreboard/env.sh Executable file
View File

@ -0,0 +1,16 @@
#!/bin/bash
DIR=$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )
VENV=${DIR}/.venv
INSTALL_REQS=False
if [ ! -d ${VENV} ]; then
virtualenv ${VENV}
INSTALL_REQS=True
fi
source ${VENV}/bin/activate
if [ ${INSTALL_REQS} == True ]; then
pip install -r ${DIR}/requirements.txt
fi

View File

View File

@ -0,0 +1,201 @@
# Copyright 2011 OpenStack, LLC.
# Copyright 2012 Hewlett-Packard Development Company, L.P.
# Copyright 2015 Pure Storage, Inc.
#
# Licensed 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.
import threading
import json
import time
from six.moves import queue as Queue
import paramiko
import logging
import pprint
class GerritWatcher(threading.Thread):
log = logging.getLogger("gerrit.GerritWatcher")
def __init__(self, gerrit, username, hostname, port=29418, keyfile=None):
threading.Thread.__init__(self)
self.username = username
self.keyfile = keyfile
self.hostname = hostname
self.port = port
self.gerrit = gerrit
def _read(self, fd):
l = fd.readline()
data = json.loads(l)
self.log.debug("Received data from Gerrit event stream: \n%s" %
str(data))
self.gerrit.addEvent(data)
def _listen(self, stdout, stderr):
while True:
self._read(stdout)
def _run(self):
try:
client = paramiko.SSHClient()
client.load_system_host_keys()
client.set_missing_host_key_policy(paramiko.WarningPolicy())
client.connect(self.hostname,
username=self.username,
port=self.port,
key_filename=self.keyfile)
stdin, stdout, stderr = client.exec_command("gerrit stream-events")
self._listen(stdout, stderr)
ret = stdout.channel.recv_exit_status()
self.log.debug("SSH exit status: %s" % ret)
if ret:
raise Exception("Gerrit error executing stream-events")
except:
self.log.exception("Exception on ssh event stream:")
time.sleep(5)
def run(self):
while True:
self._run()
class Gerrit(object):
log = logging.getLogger("gerrit.Gerrit")
def __init__(self, hostname, username, port=29418, keyfile=None):
self.username = username
self.hostname = hostname
self.port = port
self.keyfile = keyfile
self.watcher_thread = None
self.event_queue = None
self.client = None
def startWatching(self):
self.event_queue = Queue.Queue()
self.watcher_thread = GerritWatcher(
self,
self.username,
self.hostname,
self.port,
keyfile=self.keyfile)
self.watcher_thread.daemon = True
self.watcher_thread.start()
def addEvent(self, data):
return self.event_queue.put(data)
def getEvent(self):
return self.event_queue.get()
def eventDone(self):
self.event_queue.task_done()
def review(self, project, change, message, action={}):
cmd = 'gerrit review --project %s' % project
if message:
cmd += ' --message "%s"' % message
for k, v in action.items():
if v is True:
cmd += ' --%s' % k
else:
cmd += ' --label %s=%s' % (k, v)
cmd += ' %s' % change
out, err = self._ssh(cmd)
return err
def query(self, query):
args = '--all-approvals --comments --commit-message'
args += ' --current-patch-set --dependencies --files'
args += ' --patch-sets --submit-records'
cmd = 'gerrit query --format json %s %s' % (
args, query)
out, err = self._ssh(cmd)
if not out:
return False
lines = out.split('\n')
if not lines:
return False
data = json.loads(lines[0])
if not data:
return False
self.log.debug("Received data from Gerrit query: \n%s" %
(pprint.pformat(data)))
return data
def simpleQuery(self, query):
def _query_chunk(query):
args = '--current-patch-set'
cmd = 'gerrit query --format json %s %s' % (
args, query)
out, err = self._ssh(cmd)
if not out:
return False
lines = out.split('\n')
if not lines:
return False
data = [json.loads(line) for line in lines
if "sortKey" in line]
if not data:
return False
self.log.debug("Received data from Gerrit query: \n%s" %
(pprint.pformat(data)))
return data
# gerrit returns 500 results by default, so implement paging
# for large projects like nova
alldata = []
chunk = _query_chunk(query)
while(chunk):
alldata.extend(chunk)
sortkey = "resume_sortkey:'%s'" % chunk[-1]["sortKey"]
chunk = _query_chunk("%s %s" % (query, sortkey))
return alldata
def _open(self):
client = paramiko.SSHClient()
client.load_system_host_keys()
client.set_missing_host_key_policy(paramiko.WarningPolicy())
client.connect(self.hostname,
username=self.username,
port=self.port,
key_filename=self.keyfile)
self.client = client
def _ssh(self, command):
if not self.client:
self._open()
try:
self.log.debug("SSH command:\n%s" % command)
stdin, stdout, stderr = self.client.exec_command(command)
except:
self._open()
stdin, stdout, stderr = self.client.exec_command(command)
out = stdout.read()
self.log.debug("SSH received stdout:\n%s" % out)
ret = stdout.channel.recv_exit_status()
self.log.debug("SSH exit status: %s" % ret)
err = stderr.read()
self.log.debug("SSH received stderr:\n%s" % err)
if ret:
raise Exception("Gerrit error executing %s" % command)
return (out, err)

View File

@ -0,0 +1,10 @@
import logging
def init(config):
log_file = config.log_file() or 'scoreboard.log'
logging.basicConfig(filename=log_file, level=logging.INFO)
def get(name):
return logging.getLogger(name)

View File

@ -0,0 +1,4 @@
Flask
paramiko
six
pymongo

View File

@ -0,0 +1,124 @@
#!/usr/bin/env python
import datetime
import re
import threading
import config
import db_helper
from infra import gerrit
import logger
import users
class GerritCIListener():
def __init__(self):
self.ci_user_names = []
self.cfg = config.Config()
self.db = db_helper.DBHelper(self.cfg).get()
logger.init(self.cfg)
self.log = logger.get('scoreboard-gerrit-listener')
def get_thirdparty_users(self):
# TODO: figure out how to do the authentication..
# thirdparty_group = '95d633d37a5d6b06df758e57b1370705ec071a57'
# url = 'http://review.openstack.org/groups/%s/members' % thirdparty_group
# members = eval(urllib.urlopen(url).read())
members = users.third_party_group
for account in members:
username = account[u'username']
self.ci_user_names.append(username)
def is_ci_user(self, username):
# TODO: query this from gerrit. Maybe save a copy in the db?
return (username in self.ci_user_names) or (username == u'jenkins')
def determine_result(self, event):
approvals = event.get(u'approvals', None)
if approvals:
for approval in approvals:
vote = approval.get(u'value', 0)
if int(vote) > 0:
return 'SUCCESS'
comment = event[u'comment']
if re.search('FAILURE|FAILED', comment, re.IGNORECASE):
return 'FAILURE'
elif re.search('ERROR', comment, re.IGNORECASE):
return 'ERROR'
elif re.search('NOT_REGISTERED', comment, re.IGNORECASE):
return 'NOT_REGISTERED'
elif re.search('ABORTED', comment, re.IGNORECASE):
return 'ABORTED'
elif re.search('merge failed', comment, re.IGNORECASE):
return 'MERGE FAILED'
elif re.search('SUCCESS|SUCCEEDED', comment, re.IGNORECASE):
return 'SUCCESS'
else:
return 'UNKNOWN'
def handle_gerrit_event(self, event):
# We only care about comments on reviews
if event[u'type'] == u'comment-added' and \
self.is_ci_user(event[u'author'][u'username']):
# special case for jenkins, it comments other things too, ignore those
if event[u'author'][u'username'] == u'jenkins':
if re.search('elastic|starting|merged',
event[u'comment'], re.IGNORECASE):
return
# Lazy populate account info the in the db
user_name = event[u'author'][u'username']
ci_account = self.db.ci_accounts.find_one({'_id': user_name})
if not ci_account:
ci_account = {
'_id': user_name,
'user_name_pretty': event[u'author'][u'name']
}
self.db.ci_accounts.insert(ci_account)
review_num_patchset = '%s,%s' % (event[u'change'][u'number'],
event[u'patchSet'][u'number'])
patchset = self.db.test_results.find_one({'_id': review_num_patchset})
if patchset:
self.log.info('Updating %s' % review_num_patchset)
patchset['results'][user_name] = self.determine_result(event)
self.db.test_results.save(patchset)
else:
patchset = {
'_id': review_num_patchset,
'results': {
user_name: self.determine_result(event)
},
'project': event[u'change'][u'project'],
'created': datetime.datetime.utcnow(),
}
self.log.info('Inserting %s' % review_num_patchset)
self.db.test_results.insert(patchset)
def run(self):
# TODO: Maybe split this into its own process? Its kind of annoying that
# when modifying the UI portion of the project it stops gathering data..
hostname = self.cfg.gerrit_hostname()
username = self.cfg.gerrit_user()
port = self.cfg.gerrit_port()
keyfile = self.cfg.gerrit_key()
g = gerrit.Gerrit(hostname, username, port=port, keyfile=keyfile)
g.startWatching()
self.get_thirdparty_users()
while True:
event = g.getEvent()
try:
self.handle_gerrit_event(event)
except:
self.log.exception('Failed to handle gerrit event: ')
g.eventDone()
if __name__ == '__main__':
listener = GerritCIListener()
listener.run()

View File

@ -0,0 +1,69 @@
#!/usr/bin/env python
import datetime
from bson import json_util
from flask import Flask, request, render_template, send_from_directory
import pymongo
import config
import db_helper
import logger
cfg = config.Config()
app = Flask(__name__)
app.debug = True
logger.init(cfg)
db = db_helper.DBHelper(cfg).get()
@app.route('/')
def index():
return render_template('index.html', host=request.host)
@app.route('/static/<path:path>')
def send_js(path):
# TODO: We should probably use a real webserver for this..
return send_from_directory('static', path)
@app.route('/ci-accounts', methods=['GET'])
def ci_accounts():
return json_util.dumps(db.ci_accounts.find())
@app.route('/results', methods=['GET'])
def results():
# TODO: We should have a cache for these requests
# so we don't get hammered by reloading pages
project = request.args.get('project', None)
username = request.args.get('user', None)
count = request.args.get('count', None)
start = request.args.get('start', None)
timeframe = request.args.get('timeframe', None)
return query_results(project, username, count, start, timeframe)
def query_results(project, user_name, count, start, timeframe):
query = {}
if project:
query['project'] = project
if user_name:
query['results.' + user_name] = {'$exists': True, '$ne': None}
if timeframe:
num_hours = int(timeframe)
current_time = datetime.datetime.utcnow()
start_time = current_time - datetime.timedelta(hours=num_hours)
query['created'] = {'$gt': start_time}
records = db.test_results.find(query).sort('created', pymongo.DESCENDING)
return json_util.dumps(records)
if __name__ == '__main__':
app.run(host='0.0.0.0')

Binary file not shown.

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,192 @@
@font-face {
font-family: 'Myriad Pro Regular';
font-style: normal;
font-weight: normal;
src: local('Myriad Pro Regular'), url('/static/MYRIADPRO-REGULAR.woff') format('woff');
}
body {
font-family: 'Myriad Pro Regular';
background-image: url("site_bg.png");
background-repeat: repeat-x;
background-attachment: fixed;
}
.pretty_table {
box-shadow: 10px 10px 5px #888888;
-moz-border-radius-bottomleft:5px;
-webkit-border-bottom-left-radius:5px;
border-bottom-left-radius:5px;
-moz-border-radius-bottomright:5px;
-webkit-border-bottom-right-radius:5px;
border-bottom-right-radius:5px;
-moz-border-radius-topright:5px;
-webkit-border-top-right-radius:5px;
border-top-right-radius:5px;
-moz-border-radius-topleft:5px;
-webkit-border-top-left-radius:5px;
border-top-left-radius:5px;
}
.pretty_table table {
width:100%;
height:100%;
border-spacing: 10px;
border-collapse: separate;
}
.pretty_table tr:last-child td:last-child {
-moz-border-radius-bottomright: 5px;
-webkit-border-bottom-right-radius: 5px;
border-bottom-right-radius: 5px;
}
.pretty_table tr:first-child td:first-child {
-moz-border-radius-topleft: 5px;
-webkit-border-top-left-radius: 5px;
border-top-left-radius: 5px;
}
.pretty_table tr:first-child td:last-child {
-moz-border-radius-topright: 5px;
-webkit-border-top-right-radius: 5px;
border-top-right-radius: 5px;
}
.pretty_table tr:last-child td:first-child {
-moz-border-radius-bottomleft: 5px;
-webkit-border-bottom-left-radius: 5px;
border-bottom-left-radius: 5px;
}
.pretty_table td {
vertical-align: middle;
text-align: center;
padding: 5px;
border: solid 1px black;
}
.pretty_table_header {
background-color:#5C5C5C;
text-align:center;
color:#ffffff;
border: solid 1px black;
padding: 5px;
/* white-space: nowrap; */
}
.success {
background-color: #00FF00;
}
.success:hover {
font-weight: bold;
cursor:pointer;
background-color: #33CC00;
}
.fail {
background-color: #FF3300;
}
.fail:hover {
font-weight: bold;
cursor:pointer;
background-color: #FF0000;
}
.unknown {
background-color: #A0A0A0;
}
.unknown:hover {
font-weight: bold;
cursor:pointer;
background-color: #808080;
}
.no_result {
background-color: #E0E0E0;
opacity: 0.75;
}
.scoreboard_container {
margin-top: 10px;
margin-left: auto;
margin-right: auto;
width: 90%;
}
.query_box_container {
margin-top: 15px;
margin-left: auto;
margin-right: auto;
background: #5C5C5C;
width: 90%;
border: solid 1px black;
border-radius: 5px;
-webkit-border-radius: 5px;
color: #ffffff;
}
.query_box {
padding: 10px;
margin: 0 auto;
}
.query_box_title {
font-size: 250%;
font-weight: bold;
display: inline-block;
}
.query_box form {
margin-left: 25px;
display: inline-block;
}
.query_box input {
border-radius:5px;
-webkit-border-radius:5px;
margin-left: 5px;
margin-right: 5px;
border: solid 1px black;
padding: 5px
}
.overlay_opaque {
opacity: 0.75;
background-color: #4C4C4C;
position: fixed;
width: 100%;
height: 100%;
top: 0px;
left: 0px;
z-index: 10000;
}
.overlay_title {
width: 100%;
font-size: 250%;
font-weight: bold;
text-align: center;
position: absolute;
bottom: 0;
z-index: 100001;
}
.overlay_clear {
background-color:rgba(0, 0, 0, 0.5)
position: fixed;
width: 100%;
height: 100%;
top: 0px;
left: 0px;
z-index: 100000; /* more than the opaque one */
}

View File

@ -0,0 +1,298 @@
var Scoreboard = (function () {
var board = {};
var table_div_id = null;
var table = null;
var table_header = null;
var hostname = null;
var ci_results = null;
var ci_accounts = null;
var row_cache = {};
var spinner = null;
var overlay = null;
var opaque_overlay = null;
var hide_overlay = function () {
spinner.stop();
overlay.remove();
opaque_overlay.remove();
}
var show_overlay = function () {
overlay = $(document.createElement('div'));
overlay.addClass('overlay_clear');
overlay.appendTo(document.body);
opaque_overlay = $(document.createElement('div'));
opaque_overlay.addClass('overlay_opaque');
opaque_overlay.appendTo(document.body);
title = $(document.createElement('div'));
title.addClass('overlay_title');
title.html('Building results...');
title.appendTo(overlay);
var opts = {
lines: 20, // The number of lines to draw
length: 35, // The length of each line
width: 10, // The line thickness
radius: 45, // The radius of the inner circle
corners: 1, // Corner roundness (0..1)
rotate: 0, // The rotation offset
direction: 1, // 1: clockwise, -1: counterclockwise
color: '#000', // #rgb or #rrggbb or array of colors
speed: 1, // Rounds per second
trail: 60, // Afterglow percentage
shadow: true, // Whether to render a shadow
hwaccel: true, // Whether to use hardware acceleration
className: 'spinner', // The CSS class to assign to the spinner
zIndex: 2e9, // The z-index (defaults to 2000000000)
top: '50%', // Top position relative to parent
left: '50%' // Left position relative to parent
};
spinner = new Spinner(opts).spin();
$(spinner.el).appendTo(overlay);
}
var gather_data_and_build = function () {
show_overlay();
$.ajax({
type: 'get',
url: 'results',
data: window.location.search.substring(1),
success: function(data) {
ci_results = JSON.parse(data);
get_ci_accounts()
}
});
};
var get_ci_accounts = function () {
$.ajax({
type: 'get',
url: 'ci-accounts',
success: function(data) {
parse_accounts(data);
build_table();
}
});
}
var find_ci_in_list = function (ci, list) {
for (var i = 0; i < list.length; i++) {
if (ci == list[i]._id) {
return list[i];
}
}
}
var parse_accounts = function (ci_accounts_raw) {
var all_ci_accounts = JSON.parse(ci_accounts_raw);
var ci_account_objs = {};
ci_accounts = [];
for (var patchset in ci_results) {
for (var ci in ci_results[patchset].results) {
if (!(ci in ci_account_objs)) {
ci_account_objs[ci] = true;
ci_accounts.push(find_ci_in_list(ci, all_ci_accounts));
}
}
}
}
var ci_account_header = function (user_name, user_name_pretty) {
return user_name_pretty + ' <br /> (' + user_name + ')';
};
var create_header = function () {
td = $(document.createElement('td'));
td.addClass('pretty_table_header');
return td;
};
var create_filler = function () {
td = $(document.createElement('td'));
td.addClass('no_result');
td.html('&nbsp');
return td;
};
var add_header = function (header_title) {
var td = create_header();
td.html(header_title);
td.appendTo(table_header);
};
var set_result = function(cell, result) {
var cell_class = null;
switch (result) {
case 'SUCCESS':
cell_class = 'success';
break;
case 'FAILURE':
case 'ERROR':
case 'NOT_REGISTERED':
case 'ABORTED':
cell_class = 'fail';
break;
case 'MERGE FAILED':
case 'UNKNOWN':
default:
cell_class = 'unknown';
break;
}
cell.removeClass().addClass(cell_class);
cell.html(result);
};
var handle_patchset = function(patchset) {
var result_row = null;
var ci_index = null;
// console.log(JSON.stringify(result));
var review_id_patchset = patchset._id;
// add a new row for the review number + patchset
result_row = $(document.createElement('tr'));
result_row.appendTo(table);
var label = create_header();
label.html(review_id_patchset);
label.appendTo(result_row);
for (var i = 0; i < ci_accounts.length; i++) {
var ci_account = ci_accounts[i];
var td = null;
if (ci_account._id in patchset.results) {
var result = patchset.results[ci_account._id];
td = $(document.createElement('td'));
review_patchset_split = review_id_patchset.split(',');
var url = "https://review.openstack.org/#/c/" + review_patchset_split[0] + "/" + review_patchset_split[1];
td.on('click', (function () {
// closures are weird.. scope the url so each on click is using
// the right one and not just the last url handled by the loop
var review_url = url;
return function () {
window.open(review_url, '_blank');
}
})());
td.prop('title', url);
set_result(td, result);
}
else {
td = create_filler();
}
td.appendTo(result_row);
}
}
var build_table = function () {
table = $(document.createElement('table'));
table.addClass('pretty_table');
table.attr('cellspacing', 0);
table_container = $('#' + table_div_id);
table_container.addClass('scoreboard_container');
table.appendTo(table_container);
// build a table header that will (by the time
// we're done) have row for each ci account name
table_header = $(document.createElement('tr'));
create_header().appendTo(table_header); // spacer box
table_header.appendTo(table);
for (var i = 0; i < ci_accounts.length; i++) {
var ci = ci_accounts[i]
add_header(ci_account_header(ci._id, ci.user_name_pretty));
}
// TODO: maybe process some of this in a worker thread?
// It might be nice if we can build a model and then render it
// all in one go instead of modifying the DOM so much... or at
// least do some pre-checks to build out all of the columns
// first so we don't have to keep updating them later on
//
// For now we will handle a single result at a time (later on
// we could maybe stream/pull incremental updates so the page
// is 'live').
//
// This will add each result into the table and then yield
// the main thread so the browser can render, handle events,
// and generally not lock up and be angry with us. It still
// takes a while to actually build out the table, but at least
// it will be more exciting to watch all the results pop up
// on the screen instead of just blank page.
var index = 0;
var num_results = ci_results.length;
(function handle_patchset_wrapper() {
if (index < num_results) {
handle_patchset(ci_results[index]);
index++;
window.setTimeout(handle_patchset_wrapper, 0);
} else {
hide_overlay();
}
})();
};
var add_input_to_form = function (form, label_text, input_name, starting_val) {
var label = $('<label>').text(label_text + ":");
var input = $('<input type="text">').attr({id: input_name, name: input_name});
input.appendTo(label);
if (starting_val) {
input.val(starting_val);
}
label.appendTo(form);
return input;
}
var get_param_by_name = function (name) {
name = name.replace(/[\[]/, "\\[").replace(/[\]]/, "\\]");
var regex = new RegExp("[\\?&]" + name + "=([^&#]*)"),
results = regex.exec(window.location.search);
return results === null ? "" : decodeURIComponent(results[1].replace(/\+/g, " "));
}
board.show_query_box = function (host, container) {
var qb_container = $('#' + container);
qb_container.addClass('query_box_container');
// create a div inside the container to hold the form stuff
qb_div = $(document.createElement('div'));
qb_div.addClass('query_box');
qb_div.appendTo(qb_container);
var title = $(document.createElement('div'));
title.html('3rd Party CI Scoreboard');
title.addClass('query_box_title');
title.appendTo(qb_div);
current_project = get_param_by_name('project');
current_user = get_param_by_name('user');
current_timeframe = get_param_by_name('timeframe');
var form = $(document.createElement('form'));
add_input_to_form(form, 'Project Name', 'project', current_project);
add_input_to_form(form, 'CI Account Username', 'user', current_user);
add_input_to_form(form, 'Timeframe (hours)', 'timeframe', current_timeframe);
// TODO: Implement the "start" and "count" filters so we can do pagination
submit_button = $('<input/>', { type:'submit', value:'GO!'});
submit_button.appendTo(form);
form.submit(function(){
location.href = '/' + $(this).serialize();
});
form.appendTo(qb_div);
}
board.build = function (host, container) {
hostname = host;
table_div_id = container;
gather_data_and_build();
};
return board;
})();

Binary file not shown.

After

Width:  |  Height:  |  Size: 137 KiB

View File

@ -0,0 +1,18 @@
<html>
<head>
<script type='text/javascript' src='/static/jquery-2.1.3.min.js'></script>
<script type='text/javascript' src='/static/scoreboard.js'></script>
<script type='text/javascript' src='http://fgnass.github.io/spin.js/spin.js'></script>
<link rel='stylesheet' type='text/css' href='/static/scoreboard.css'>
</head>
<body>
<div id='query-box'></div>
<div id='scoreboard'></div>
<script type='text/javascript'>
var HOST = '{{host}}';
Scoreboard.show_query_box(HOST, 'query-box');
Scoreboard.build(HOST, 'scoreboard');
</script>
</body>
</html>

View File

@ -0,0 +1,643 @@
# TODO: Seriously... get rid of this thing...
# email addresses have been removed
third_party_group =[
{
u"_account_id": 12040,
u"name": u"A10 Networks CI",
u"username": u"a10networks-ci",
u"avatars": []
},
{
u"_account_id": 9845,
u"name": u"Arista CI",
u"username": u"arista-test",
u"avatars": []
},
{
u"_account_id": 9787,
u"name": u"Big Switch CI",
u"username": u"bsn",
u"avatars": []
},
{
u"_account_id": 10624,
u"name": u"Brocade ADX CI",
u"username": u"pattabi-ayyasami-ci",
u"avatars": []
},
{
u"_account_id": 9753,
u"name": u"Brocade BNA CI",
u"username": u"brocade_jenkins",
u"avatars": []
},
{
u"_account_id": 13396,
u"name": u"Brocade Fibre CI",
u"username": u"brocade-fibre-ci",
u"avatars": []
},
{
u"_account_id": 13051,
u"name": u"Brocade LBaaS CI",
u"username": u"brocade-lbaas-ci",
u"avatars": []
},
{
u"_account_id": 10294,
u"name": u"Brocade VDX CI",
u"username": u"bci",
u"avatars": []
},
{
u"_account_id": 10692,
u"name": u"Brocade Vyatta CI",
u"username": u"brocade-oss-service",
u"avatars": []
},
{
u"_account_id": 14214,
u"name": u"Cisco APIC CI",
u"username": u"cisco_apic_ci",
u"avatars": []
},
{
u"_account_id": 10192,
u"name": u"Cisco CI",
u"username": u"cisco_neutron_ci",
u"avatars": []
},
{
u"_account_id": 14212,
u"name": u"Cisco Tail-f CI",
u"username": u"cisco_tailf_ci",
u"avatars": []
},
{
u"_account_id": 14208,
u"name": u"Cisco ml2 CI",
u"username": u"cisco_ml2_ci",
u"avatars": []
},
{
u"_account_id": 9897,
u"name": u"Citrix NetScaler CI",
u"username": u"NetScalerAts",
u"avatars": []
},
{
u"_account_id": 10385,
u"name": u"Citrix XenServer CI",
u"username": u"citrix_xenserver_ci",
u"avatars": []
},
{
u"_account_id": 14259,
u"name": u"CloudByte CI",
u"username": u"CloudbyteCI",
u"avatars": []
},
{
u"_account_id": 14206,
u"name": u"CloudFounders OpenvStorage CI",
u"username": u"cloudfoundersci",
u"avatars": []
},
{
u"_account_id": 10193,
u"name": u"Compass CI",
u"username": u"compass_ci",
u"avatars": []
},
{
u"_account_id": 13049,
u"name": u"Coraid CI",
u"username": u"coraid-ci",
u"avatars": []
},
{
u"_account_id": 9578,
u"name": u"DB Datasets CI",
u"username": u"turbo-hipster",
u"avatars": []
},
{
u"_account_id": 12032,
u"name": u"Datera CI",
u"username": u"datera-ci",
u"avatars": []
},
{
u"_account_id": 13141,
u"name": u"Dell AFC CI",
u"username": u"dell-afc-ci",
u"avatars": []
},
{
u"_account_id": 15249,
u"name": u"Dell Storage CI",
u"username": u"dell-storage-ci",
u"avatars": []
},
{
u"_account_id": 12779,
u"name": u"Dell StorageCenter CI",
u"username": u"dell-storagecenter-ci",
u"avatars": []
},
{
u"_account_id": 5171,
u"name": u"Deprecated Citrix CI",
u"username": u"citrixjenkins",
u"avatars": []
},
{
u"_account_id": 10240,
u"name": u"Deprecated Designate CI",
u"username": u"designate-jenkins",
u"avatars": []
},
{
u"_account_id": 13143,
u"name": u"Deprecated Nexenta ISCSI NFS CI",
u"username": u"nexenta-iscsi-nfs-ci",
u"avatars": []
},
{
u"_account_id": 11016,
u"name": u"Deprecated RAX Heat CI",
u"username": u"raxheatci",
u"avatars": []
},
{
u"_account_id": 7092,
u"name": u"Deprecated Trove CI",
u"username": u"reddwarf",
u"avatars": []
},
{
u"_account_id": 10183,
u"name": u"Docker CI",
u"username": u"docker-ci",
u"avatars": []
},
{
u"_account_id": 12016,
u"name": u"EMC VMAX CI",
u"username": u"emc-vmax-ci",
u"avatars": []
},
{
u"_account_id": 12017,
u"name": u"EMC VNX CI",
u"username": u"emc-vnx-ci",
u"avatars": []
},
{
u"_account_id": 12018,
u"name": u"EMC ViPR CI",
u"username": u"emc-vipr-ci",
u"avatars": []
},
{
u"_account_id": 12033,
u"name": u"EMC XIO CI",
u"username": u"emc-xio-ci",
u"avatars": []
},
{
u"_account_id": 9885,
u"name": u"Embrane CI",
u"username": u"eci",
u"avatars": []
},
{
u"_account_id": 10387,
u"name": u"Freescale CI",
u"username": u"freescale-ci",
u"avatars": []
},
{
u"_account_id": 10808,
u"name": u"Fuel Bot",
u"username": u"fuel-watcher",
u"avatars": []
},
{
u"_account_id": 8971,
u"name": u"Fuel CI",
u"username": u"fuel-ci",
u"avatars": []
},
{
u"_account_id": 12489,
u"name": u"Fusion-io CI",
u"username": u"fusionio-ci",
u"avatars": []
},
{
u"_account_id": 12368,
u"name": u"GlobalLogic CI",
u"username": u"globallogic-ci",
u"avatars": []
},
{
u"_account_id": 12778,
u"name": u"HDS HNAS CI",
u"username": u"hds-hnas-ci",
u"avatars": []
},
{
u"_account_id": 11811,
u"name": u"HP Storage CI",
u"username": u"hp-cinder-ci",
u"avatars": []
},
{
u"_account_id": 10503,
u"name": u"Huawei ML2 CI",
u"username": u"huawei-ci",
u"avatars": []
},
{
u"_account_id": 13628,
u"name": u"Huawei Volume CI",
u"username": u"huawei-volume-ci",
u"avatars": []
},
{
u"_account_id": 12251,
u"name": u"IB IPAM CI",
u"username": u"ib-ipam-ci",
u"avatars": []
},
{
u"_account_id": 9751,
u"name": u"IBM DB2 CI",
u"username": u"ibmdb2",
u"avatars": []
},
{
u"_account_id": 12499,
u"name": u"IBM DS8000 CI",
u"username": u"ibm-ds8k-ci",
u"avatars": []
},
{
u"_account_id": 12540,
u"name": u"IBM DS8000 CI",
u"username": u"ed3ed3",
u"avatars": []
},
{
u"_account_id": 12370,
u"name": u"IBM FlashSystem CI",
u"username": u"ibm-flashsystem-ci",
u"avatars": []
},
{
u"_account_id": 12491,
u"name": u"IBM GPFS CI",
u"username": u"ibm-gpfs-ci",
u"avatars": []
},
{
u"_account_id": 12492,
u"name": u"IBM NAS CI",
u"username": u"ibm-ibmnas-ci",
u"avatars": []
},
{
u"_account_id": 10118,
u"name": u"IBM PowerKVM CI",
u"username": u"powerkvm",
u"avatars": []
},
{
u"_account_id": 9752,
u"name": u"IBM PowerVC CI",
u"username": u"ibmpwrvc",
u"avatars": []
},
{
u"_account_id": 9846,
u"name": u"IBM SDN-VE CI",
u"username": u"ibmsdnve",
u"avatars": []
},
{
u"_account_id": 12202,
u"name": u"IBM Storwize CI",
u"username": u"ibm-storwize-ci",
u"avatars": []
},
{
u"_account_id": 12493,
u"name": u"IBM XIV CI",
u"username": u"ibm-xiv-ci",
u"avatars": []
},
{
u"_account_id": 10623,
u"name": u"IBM ZVM CI",
u"username": u"ibm-zvm-ci",
u"avatars": []
},
{
u"_account_id": 12081,
u"name": u"IBM xCAT CI",
u"username": u"ibm-xcat-ci",
u"avatars": []
},
{
u"_account_id": 14571,
u"name": u"Intel Networking CI",
u"username": u"intel-networking-ci",
u"avatars": []
},
{
u"_account_id": 11103,
u"name": u"Intel PCI CI",
u"username": u"intelotccloud",
u"avatars": []
},
{
u"_account_id": 10307,
u"name": u"Jay Pipes Testing CI",
u"username": u"jaypipes-testing",
u"avatars": []
},
{
u"_account_id": 10867,
u"name": u"LVS CI",
u"username": u"lvstest",
u"avatars": []
},
{
u"_account_id": 10712,
u"name": u"MagnetoDB CI",
u"username": u"jenkins-magnetodb",
u"avatars": []
},
{
u"_account_id": 9732,
u"name": u"Mellanox CI",
u"username": u"mellanox",
u"avatars": []
},
{
u"_account_id": 10121,
u"name": u"Metaplugin CI",
u"username": u"metaplugintest",
u"avatars": []
},
{
u"_account_id": 5170,
u"name": u"Microsoft Hyper-V CI",
u"username": u"hyper-v-ci",
u"avatars": []
},
{
u"_account_id": 13394,
u"name": u"Microsoft iSCSI CI",
u"username": u"microsoft-iscsi-ci",
u"avatars": []
},
{
u"_account_id": 9925,
u"name": u"Midokura CI",
u"username": u"midokura",
u"avatars": []
},
{
u"_account_id": 7821,
u"name": u"Murano CI",
u"username": u"murano-ci",
u"avatars": []
},
{
u"_account_id": 10116,
u"name": u"NEC CI",
u"username": u"nec-openstack-ci",
u"avatars": []
},
{
u"_account_id": 10621,
u"name": u"NetApp CI",
u"username": u"netapp-ci",
u"avatars": []
},
{
u"_account_id": 13395,
u"name": u"NetApp NAS CI",
u"username": u"netapp-nas-ci",
u"avatars": []
},
{
u"_account_id": 15239,
u"name": u"Nexenta CI check",
u"username": u"hodos",
u"avatars": []
},
{
u"_account_id": 13627,
u"name": u"Nexenta iSCSI CI",
u"username": u"nexenta-iscsi-ci",
u"avatars": []
},
{
u"_account_id": 7246,
u"name": u"Nicira Bot",
u"username": u"nicirabot",
u"avatars": []
},
{
u"_account_id": 11611,
u"name": u"Nimble Storage CI",
u"username": u"nimbleci",
u"avatars": []
},
{
u"_account_id": 10184,
u"name": u"Nuage CI",
u"username": u"nuage-ci",
u"avatars": []
},
{
u"_account_id": 10153,
u"name": u"One Convergence CI",
u"username": u"oneconvergence",
u"avatars": []
},
{
u"_account_id": 12250,
u"name": u"Open Attic CI",
u"username": u"open-attic-ci",
u"avatars": []
},
{
u"_account_id": 9682,
u"name": u"OpenContrail CI",
u"username": u"contrail",
u"avatars": []
},
{
u"_account_id": 10386,
u"name": u"OpenDaylight CI",
u"username": u"odl-jenkins",
u"avatars": []
},
{
u"_account_id": 13144,
u"name": u"Oracle ZFSSA CI",
u"username": u"oracle-zfssa-ci",
u"avatars": []
},
{
u"_account_id": 10117,
u"name": u"PLUMgrid CI",
u"username": u"plumgrid-ci",
u"avatars": []
},
{
u"_account_id": 12780,
u"name": u"ProphetStor CI",
u"username": u"prophetstor-ci",
u"avatars": []
},
{
u"_account_id": 7360,
u"name": u"Puppet CI",
u"username": u"puppet-openstack-ci-user",
u"avatars": []
},
{
u"_account_id": 9133,
u"name": u"Puppet Ceph CI",
u"username": u"puppetceph",
u"avatars": []
},
{
u"_account_id": 12369,
u"name": u"Pure Storage CI",
u"username": u"purestorage-cinder-ci",
u"avatars": []
},
{
u"_account_id": 9828,
u"name": u"Radware CI",
u"username": u"radware3rdpartytesting",
u"avatars": []
},
{
u"_account_id": 9009,
u"name": u"Red Hat CI",
u"username": u"redhatci",
u"avatars": []
},
{
u"_account_id": 9681,
u"name": u"Ryu CI",
u"username": u"neutronryu",
u"avatars": []
},
{
u"_account_id": 12494,
u"name": u"SUSE Cloud CI",
u"username": u"suse-cloud-ci",
u"avatars": []
},
{
u"_account_id": 12034,
u"name": u"Sacharya CI",
u"username": u"sacharya-ci",
u"avatars": []
},
{
u"_account_id": 7213,
u"name": u"Sahara Hadoop Cluster CI",
u"username": u"savanna-ci",
u"avatars": []
},
{
u"_account_id": 12249,
u"name": u"Scality CI",
u"username": u"scality-ci",
u"avatars": []
},
{
u"_account_id": 2166,
u"name": u"SmokeStack CI",
u"username": u"smokestack",
u"avatars": []
},
{
u"_account_id": 12019,
u"name": u"Snabb NFV CI",
u"username": u"snabb-nfv-ci",
u"avatars": []
},
{
u"_account_id": 10622,
u"name": u"SolidFire CI",
u"username": u"sfci",
u"avatars": []
},
{
u"_account_id": 13052,
u"name": u"SwiftStack Cluster CI",
u"username": u"swiftstack-cluster-ci",
u"avatars": []
},
{
u"_account_id": 11258,
u"name": u"THSTACK CI",
u"username": u"thstack-ci",
u"avatars": []
},
{
u"_account_id": 9695,
u"name": u"Tail-f NCS CI",
u"username": u"tailfncs",
u"avatars": []
},
{
u"_account_id": 13050,
u"name": u"VMWare Congress CI",
u"username": u"vmware-congress-ci",
u"avatars": []
},
{
u"_account_id": 9008,
u"name": u"VMware NSX CI",
u"username": u"vmwareminesweeper",
u"avatars": []
},
{
u"_account_id": 7584,
u"name": u"Vanilla Bot",
u"username": u"vanillabot",
u"avatars": []
},
{
u"_account_id": 9884,
u"name": u"Wherenow.org CI",
u"username": u"wherenowjenkins",
u"avatars": []
},
{
u"_account_id": 13142,
u"name": u"X-IO ISE CI",
u"username": u"x-io-ise-ci",
u"avatars": []
},
{
u"_account_id": 10119,
u"name": u"vArmour CI",
u"username": u"varmourci",
u"avatars": []
}
]