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:
parent
98ff04fb0a
commit
c0d0d4cac8
4
monitoring/scoreboard/.gitignore
vendored
Normal file
4
monitoring/scoreboard/.gitignore
vendored
Normal file
@ -0,0 +1,4 @@
|
||||
.idea
|
||||
*.pyc
|
||||
.venv
|
||||
app.db
|
31
monitoring/scoreboard/README.md
Normal file
31
monitoring/scoreboard/README.md
Normal 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`
|
||||
|
7
monitoring/scoreboard/ci-scoreboard.conf
Normal file
7
monitoring/scoreboard/ci-scoreboard.conf
Normal 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
|
44
monitoring/scoreboard/config.py
Normal file
44
monitoring/scoreboard/config.py
Normal 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')
|
10
monitoring/scoreboard/db_helper.py
Normal file
10
monitoring/scoreboard/db_helper.py
Normal 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
16
monitoring/scoreboard/env.sh
Executable 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
|
0
monitoring/scoreboard/infra/__init__.py
Normal file
0
monitoring/scoreboard/infra/__init__.py
Normal file
201
monitoring/scoreboard/infra/gerrit.py
Normal file
201
monitoring/scoreboard/infra/gerrit.py
Normal 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)
|
10
monitoring/scoreboard/logger.py
Normal file
10
monitoring/scoreboard/logger.py
Normal 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)
|
4
monitoring/scoreboard/requirements.txt
Normal file
4
monitoring/scoreboard/requirements.txt
Normal file
@ -0,0 +1,4 @@
|
||||
Flask
|
||||
paramiko
|
||||
six
|
||||
pymongo
|
124
monitoring/scoreboard/scoreboard_gerrit_listener.py
Executable file
124
monitoring/scoreboard/scoreboard_gerrit_listener.py
Executable 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()
|
69
monitoring/scoreboard/scoreboard_ui.py
Executable file
69
monitoring/scoreboard/scoreboard_ui.py
Executable 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')
|
BIN
monitoring/scoreboard/static/MYRIADPRO-REGULAR.woff
Normal file
BIN
monitoring/scoreboard/static/MYRIADPRO-REGULAR.woff
Normal file
Binary file not shown.
9205
monitoring/scoreboard/static/jquery-2.1.3.js
vendored
Normal file
9205
monitoring/scoreboard/static/jquery-2.1.3.js
vendored
Normal file
File diff suppressed because it is too large
Load Diff
4
monitoring/scoreboard/static/jquery-2.1.3.min.js
vendored
Normal file
4
monitoring/scoreboard/static/jquery-2.1.3.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
192
monitoring/scoreboard/static/scoreboard.css
Normal file
192
monitoring/scoreboard/static/scoreboard.css
Normal 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 */
|
||||
}
|
||||
|
298
monitoring/scoreboard/static/scoreboard.js
Normal file
298
monitoring/scoreboard/static/scoreboard.js
Normal 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(' ');
|
||||
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;
|
||||
})();
|
BIN
monitoring/scoreboard/static/site_bg.png
Normal file
BIN
monitoring/scoreboard/static/site_bg.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 137 KiB |
18
monitoring/scoreboard/templates/index.html
Normal file
18
monitoring/scoreboard/templates/index.html
Normal 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>
|
643
monitoring/scoreboard/users.py
Normal file
643
monitoring/scoreboard/users.py
Normal 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": []
|
||||
}
|
||||
]
|
Loading…
Reference in New Issue
Block a user