Adds the new cireporter script used by Cinder team
This patch creates the cireporter script which is used by the Cinder team. This script is used to help determine which Vendor CI's are working and reporting over a release cycle. This patch refactors some of the code in the lastcomment.py into a common file that contains the Job and Comment classes used by lastcomment and cireporter. Also updated the ci.yaml. Change-Id: I0bde0f539d0e3752963594ef60c3ed460c918116
This commit is contained in:
parent
93f2305552
commit
26611b68ed
@ -73,4 +73,34 @@ To generate a html report for cinder's third party CI accounts on http://localho
|
||||
./lastcomment.py -f ci.yaml -c 100 --json lastcomment.json
|
||||
python -m SimpleHTTPServer
|
||||
|
||||
CI Reporter
|
||||
===========
|
||||
|
||||
Script that produces statistics and a report of CI systems and their jobs.
|
||||
It can output in plain text or json to stdout or a file.
|
||||
|
||||
Help
|
||||
----
|
||||
./cireporter.py -h
|
||||
|
||||
Generate A Report
|
||||
-----------------
|
||||
|
||||
To generate a plain text report that includes stats for all Cinder CI's
|
||||
|
||||
./cireporter.py -i cinder.yaml -c 250
|
||||
|
||||
Other Uses
|
||||
----------
|
||||
|
||||
To see the latest Cinder Jenkins stats
|
||||
|
||||
./cireporter.py -p openstack/cinder -n Jenkins -c 250
|
||||
|
||||
To generate a report as json output to stdout
|
||||
|
||||
./cireporter.py -p openstack/cinder -n Jenkins -c 250 -j
|
||||
|
||||
To generate a report as json and write it to a file
|
||||
|
||||
./cireporter.py -p openstack/cinder -n Jenkins -c 250 -j -o foo.json
|
||||
|
@ -11,12 +11,13 @@ openstack/cinder:
|
||||
- Blockbridge EPS CI
|
||||
- CloudByte CI
|
||||
- Coho Data Storage CI
|
||||
- EMC CorpHD CI
|
||||
- Dell Storage CI
|
||||
- EMC Unity CI
|
||||
- ITRI DISCO CI
|
||||
- Vedams DotHillDriver CI
|
||||
- Vedams- HPMSA FCISCSIDriver CI
|
||||
- EMC CorpHD CI
|
||||
- EMC ScaleIO CI
|
||||
- EMC Unity CI
|
||||
- EMC VNX CI
|
||||
- EMC XIO CI
|
||||
- EMC VMAX CI
|
||||
|
264
monitoring/lastcomment-scoreboard/cireporter.py
Executable file
264
monitoring/lastcomment-scoreboard/cireporter.py
Executable file
@ -0,0 +1,264 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
"""Generate a report on CI comments and jobs."""
|
||||
|
||||
import argparse
|
||||
import calendar
|
||||
import datetime
|
||||
import json
|
||||
import yaml
|
||||
import pdb
|
||||
|
||||
import requests
|
||||
|
||||
import comment
|
||||
|
||||
|
||||
def query_gerrit(name, count, project, quiet=False):
|
||||
"""Query gerrit and fetch the comments."""
|
||||
# Include review messages in query
|
||||
search = "reviewer:\"%s\"" % name
|
||||
if project:
|
||||
search = search + (" AND project:\"%s\"" % project)
|
||||
query = ("https://review.openstack.org/changes/?q=%s&"
|
||||
"o=MESSAGES&o=DETAILED_ACCOUNTS" % search)
|
||||
r = requests.get(query)
|
||||
try:
|
||||
changes = json.loads(r.text[4:])
|
||||
except ValueError:
|
||||
if not quiet:
|
||||
print("query: '%s' failed with:\n%s" % (query, r.text))
|
||||
return []
|
||||
|
||||
comments = []
|
||||
for change in changes:
|
||||
for date, message in comment.get_comments(change, name):
|
||||
if date is None:
|
||||
# no comments from reviewer yet. This can happen since
|
||||
# 'Uploaded patch set X.' is considered a comment.
|
||||
continue
|
||||
comments.append(comment.Comment(date, change['_number'],
|
||||
change['subject'], message))
|
||||
return sorted(comments, key=lambda comment: comment.date,
|
||||
reverse=True)[0:count]
|
||||
|
||||
|
||||
def get_votes(comments):
|
||||
"""Get the stats for all of the jobs in all comments."""
|
||||
last_success = None
|
||||
votes = {'success': 0, 'failure': 0}
|
||||
for cmt in comments:
|
||||
if cmt.jobs:
|
||||
for job in cmt.jobs:
|
||||
if job.result == "SUCCESS":
|
||||
if job.name not in votes:
|
||||
votes[job.name] = {'success': 1, 'failure': 0,
|
||||
'last_success': cmt}
|
||||
elif votes[job.name]['success'] == 0:
|
||||
votes[job.name]['success'] += 1
|
||||
votes[job.name]['last_success'] = cmt
|
||||
else:
|
||||
votes[job.name]['success'] += 1
|
||||
|
||||
votes['success'] += 1
|
||||
if not last_success:
|
||||
last_success = cmt
|
||||
elif job.result == 'FAILURE':
|
||||
if job.name not in votes:
|
||||
votes[job.name] = {'success': 0, 'failure': 1,
|
||||
'last_success': None}
|
||||
else:
|
||||
votes[job.name]['failure'] += 1
|
||||
|
||||
votes['failure'] += 1
|
||||
else:
|
||||
# We got something other than
|
||||
# SUCCESS or FAILURE
|
||||
# for now, mark it as a failure
|
||||
if job.name not in votes:
|
||||
votes[job.name] = {'success': 0, 'failure': 1,
|
||||
'last_success': None}
|
||||
else:
|
||||
votes[job.name]['failure'] += 1
|
||||
votes['failure'] += 1
|
||||
#print("Job %(name)s result = %(result)s" %
|
||||
# {'name': job.name,
|
||||
# 'result': job.result})
|
||||
return votes, last_success
|
||||
|
||||
|
||||
def generate_report(name, count, project, quiet=False):
|
||||
"""Process all of the comments and generate the stats."""
|
||||
result = {'name': name, 'project': project}
|
||||
last_success = None
|
||||
|
||||
comments = query_gerrit(name, count, project)
|
||||
if not comments:
|
||||
print("No comments found. CI SYSTEM UNKNOWN")
|
||||
return
|
||||
|
||||
votes, last_success = get_votes(comments)
|
||||
result['last_seen'] = {'date': epoch(comments[0].date),
|
||||
'age': str(comments[0].age()),
|
||||
'url': comments[0].url()}
|
||||
last = len(comments) - 1
|
||||
result['first_seen'] = {'date': epoch(comments[last].date),
|
||||
'age': str(comments[last].age()),
|
||||
'url': comments[last].url()}
|
||||
if not quiet:
|
||||
print(" first seen: %s (%s old) %s" % (comments[last].date,
|
||||
comments[last].age(),
|
||||
comments[last].url()))
|
||||
print(" last seen: %s (%s old) %s" % (comments[0].date,
|
||||
comments[0].age(),
|
||||
comments[0].url()))
|
||||
if last_success:
|
||||
result['last_success'] = {'date': epoch(comments[0].date),
|
||||
'age': str(comments[0].age()),
|
||||
'url': comments[0].url()}
|
||||
if not quiet:
|
||||
print(" last success: %s (%s old) %s" % (last_success.date,
|
||||
last_success.age(),
|
||||
last_success.url()))
|
||||
else:
|
||||
result['last_success'] = None
|
||||
if not quiet:
|
||||
print(" last success: None")
|
||||
|
||||
result['jobs'] = []
|
||||
jobs = dict.fromkeys(votes, 0)
|
||||
jobs.pop('success', None)
|
||||
jobs.pop('failure', None)
|
||||
for job in jobs:
|
||||
reported_comments = votes[job]['success'] + votes[job]['failure']
|
||||
if votes[job]['failure'] == 0:
|
||||
success_rate = 100
|
||||
else:
|
||||
success_rate = int(votes[job]['success'] /
|
||||
float(reported_comments) * 100)
|
||||
|
||||
if not quiet:
|
||||
print(" Job %(job_name)s %(success_rate)s%% success out of "
|
||||
"%(comments)s comments S=%(success)s, F=%(failures)s"
|
||||
% {'success_rate': success_rate,
|
||||
'job_name': job,
|
||||
'comments': reported_comments,
|
||||
'success': votes[job]['success'],
|
||||
'failures': votes[job]['failure']})
|
||||
|
||||
# Only print the job's last success rate if the succes rate
|
||||
# is low enough to warrant showing it.
|
||||
if votes[job]['last_success'] and success_rate <= 60 and not quiet:
|
||||
print(" last success: %s (%s old) %s" %
|
||||
(votes[job]['last_success'].date,
|
||||
votes[job]['last_success'].age(),
|
||||
votes[job]['last_success'].url()))
|
||||
|
||||
job_entry = {'name': job, 'success_rate': success_rate,
|
||||
'num_success': votes[job]['success'],
|
||||
'num_failures': votes[job]['failure'],
|
||||
'comments': reported_comments,
|
||||
}
|
||||
if votes[job]['last_success']:
|
||||
job_entry['last_success'] = {
|
||||
'date': epoch(votes[job]['last_success'].date),
|
||||
'age': str(votes[job]['last_success'].age()),
|
||||
'url': votes[job]['last_success'].url()}
|
||||
else:
|
||||
job_entry['last_success'] = None
|
||||
|
||||
result['jobs'].append(job_entry)
|
||||
|
||||
total = votes['success'] + votes['failure']
|
||||
if total > 0:
|
||||
success_rate = int(votes['success'] / float(total) * 100)
|
||||
result['success_rate'] = success_rate
|
||||
if not quiet:
|
||||
print("Overall success rate: %s%% of %s comments" %
|
||||
(success_rate, len(comments)))
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def epoch(timestamp):
|
||||
return int(calendar.timegm(timestamp.timetuple()))
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description='list most recent comment by '
|
||||
'reviewer')
|
||||
parser.add_argument('-n', '--name',
|
||||
default="Jenkins",
|
||||
help='unique gerrit name of the reviewer')
|
||||
parser.add_argument('-c', '--count',
|
||||
default=10,
|
||||
type=int,
|
||||
help='unique gerrit name of the reviewer')
|
||||
parser.add_argument('-i', '--input',
|
||||
default=None,
|
||||
help='yaml file containing list of names to search on'
|
||||
'project: name'
|
||||
' (overwrites -p and -n)')
|
||||
parser.add_argument('-o', '--output',
|
||||
default=None,
|
||||
help='Write the output to a file. Defaults to stdout.')
|
||||
parser.add_argument('-j', '--json',
|
||||
default=False,
|
||||
action="store_true",
|
||||
help=("Generate report output in json format."))
|
||||
parser.add_argument('-p', '--project',
|
||||
help='only list hits for a specific project')
|
||||
|
||||
args = parser.parse_args()
|
||||
names = {args.project: [args.name]}
|
||||
|
||||
quiet = False
|
||||
if args.json and not args.output:
|
||||
# if we are writing json data to stdout
|
||||
# we shouldn't be outputting anything else
|
||||
quiet = True
|
||||
|
||||
if args.input:
|
||||
with open(args.input) as f:
|
||||
names = yaml.load(f)
|
||||
|
||||
if args.json:
|
||||
if not quiet:
|
||||
print "generating report %s" % args.json
|
||||
print "report is over last %s comments" % args.count
|
||||
report = {}
|
||||
timestamp = epoch(datetime.datetime.utcnow())
|
||||
report['timestamp'] = timestamp
|
||||
report['rows'] = []
|
||||
|
||||
for project in names:
|
||||
if not quiet:
|
||||
print 'Checking project: %s' % project
|
||||
for name in names[project]:
|
||||
if name != 'Jenkins':
|
||||
url = ("https://wiki.openstack.org/wiki/ThirdPartySystems/%s" %
|
||||
name.replace(" ", "_"))
|
||||
if not quiet:
|
||||
print 'Checking name: %s - %s' % (name, url)
|
||||
else:
|
||||
if not quiet:
|
||||
print('Checking name: %s' % name)
|
||||
try:
|
||||
report_result = generate_report(name, args.count, project,
|
||||
quiet)
|
||||
if args.json:
|
||||
report['rows'].append(report_result)
|
||||
except Exception as e:
|
||||
print e
|
||||
pass
|
||||
|
||||
if args.json:
|
||||
if not args.output:
|
||||
print(json.dumps(report))
|
||||
else:
|
||||
with open(args.output, 'w') as f:
|
||||
json.dump(report, f)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
119
monitoring/lastcomment-scoreboard/comment.py
Executable file
119
monitoring/lastcomment-scoreboard/comment.py
Executable file
@ -0,0 +1,119 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
"""Contains the Comments and Job classes."""
|
||||
|
||||
import datetime
|
||||
|
||||
TIME_FORMAT = "%Y-%m-%d %H:%M:%S"
|
||||
|
||||
|
||||
class Job(object):
|
||||
"""This class describes a job that was discovered in a comment."""
|
||||
|
||||
name = None
|
||||
time = None
|
||||
url = None
|
||||
|
||||
# SUCCESS or FAILURE
|
||||
result = None
|
||||
|
||||
# the raw job message line
|
||||
message = ''
|
||||
|
||||
def __init__(self, name, time, url, result=None, message=None):
|
||||
self.name = name
|
||||
self.time = time
|
||||
self.url = url
|
||||
self.result = result
|
||||
self.message = message
|
||||
|
||||
def __str__(self):
|
||||
return ("%s result='%s' in %s Logs = '%s' " % (
|
||||
self.name,
|
||||
self.result,
|
||||
self.time,
|
||||
self.url))
|
||||
|
||||
@staticmethod
|
||||
def parse(job_str):
|
||||
"""Parse out the raw job string and build the job obj."""
|
||||
job_split = job_str.split()
|
||||
job_name = job_split[1]
|
||||
if 'http://' in job_name or 'ftp://' in job_name:
|
||||
# we found a bogus entry w/o a name.
|
||||
return None
|
||||
|
||||
url = job_split[2]
|
||||
result = job_split[4]
|
||||
time = None
|
||||
if result == 'SUCCESS' or result == 'FAILURE':
|
||||
if 'in' in job_str and job_split[5] == 'in':
|
||||
time = " ".join(job_split[6:])
|
||||
|
||||
return Job(job_name, time, url, result, job_str)
|
||||
|
||||
|
||||
class Comment(object):
|
||||
"""Class that describes a gerrit Comment."""
|
||||
|
||||
date = None
|
||||
number = None
|
||||
subject = None
|
||||
now = None
|
||||
|
||||
def __init__(self, date, number, subject, message):
|
||||
super(Comment, self).__init__()
|
||||
self.date = date
|
||||
self.number = number
|
||||
self.subject = subject
|
||||
self.message = message
|
||||
self.now = datetime.datetime.utcnow().replace(microsecond=0)
|
||||
self.jobs = []
|
||||
|
||||
self._vote()
|
||||
|
||||
def _vote(self):
|
||||
"""Try and parse the job out of the comment message."""
|
||||
for line in self.message.splitlines():
|
||||
if line.startswith("* ") or line.startswith("- "):
|
||||
job = Job.parse(line)
|
||||
self.jobs.append(job)
|
||||
|
||||
|
||||
def __str__(self):
|
||||
return ("%s (%s old) %s '%s' " % (
|
||||
self.date.strftime(TIME_FORMAT),
|
||||
self.age(),
|
||||
self.url(), self.subject))
|
||||
|
||||
def age(self):
|
||||
return self.now - self.date
|
||||
|
||||
def url(self):
|
||||
return "https://review.openstack.org/%s" % self.number
|
||||
|
||||
def __le__(self, other):
|
||||
# self < other
|
||||
return self.date < other.date
|
||||
|
||||
def __repr__(self):
|
||||
# for sorting
|
||||
return repr((self.date, self.number))
|
||||
|
||||
|
||||
def get_comments(change, name):
|
||||
"""Generator that returns all comments by name on a given change."""
|
||||
body = None
|
||||
for message in change['messages']:
|
||||
if 'author' in message and message['author']['name'] == name:
|
||||
if (message['message'].startswith("Uploaded patch set") and
|
||||
len(message['message'].split()) is 4):
|
||||
# comment is auto created from posting a new patch
|
||||
continue
|
||||
date = message['date']
|
||||
body = message['message']
|
||||
# https://review.openstack.org/Documentation/rest-api.html#timestamp
|
||||
# drop nanoseconds
|
||||
date = date.split('.')[0]
|
||||
date = datetime.datetime.strptime(date, TIME_FORMAT)
|
||||
yield date, body
|
@ -9,61 +9,10 @@ import datetime
|
||||
import json
|
||||
import sys
|
||||
import yaml
|
||||
import pdb
|
||||
|
||||
import requests
|
||||
|
||||
|
||||
TIME_FORMAT = "%Y-%m-%d %H:%M:%S"
|
||||
|
||||
|
||||
class Comment(object):
|
||||
date = None
|
||||
number = None
|
||||
subject = None
|
||||
now = None
|
||||
|
||||
def __init__(self, date, number, subject, message):
|
||||
super(Comment, self).__init__()
|
||||
self.date = date
|
||||
self.number = number
|
||||
self.subject = subject
|
||||
self.message = message
|
||||
self.now = datetime.datetime.utcnow().replace(microsecond=0)
|
||||
|
||||
def __str__(self):
|
||||
return ("%s (%s old) https://review.openstack.org/%s '%s' " % (
|
||||
self.date.strftime(TIME_FORMAT),
|
||||
self.age(),
|
||||
self.number, self.subject))
|
||||
|
||||
def age(self):
|
||||
return self.now - self.date
|
||||
|
||||
def __le__(self, other):
|
||||
# self < other
|
||||
return self.date < other.date
|
||||
|
||||
def __repr__(self):
|
||||
# for sorting
|
||||
return repr((self.date, self.number))
|
||||
|
||||
|
||||
def get_comments(change, name):
|
||||
"""Generator that returns all comments by name on a given change."""
|
||||
body = None
|
||||
for message in change['messages']:
|
||||
if 'author' in message and message['author']['name'] == name:
|
||||
if (message['message'].startswith("Uploaded patch set") and
|
||||
len(message['message'].split()) is 4):
|
||||
# comment is auto created from posting a new patch
|
||||
continue
|
||||
date = message['date']
|
||||
body = message['message']
|
||||
# https://review.openstack.org/Documentation/rest-api.html#timestamp
|
||||
# drop nanoseconds
|
||||
date = date.split('.')[0]
|
||||
date = datetime.datetime.strptime(date, TIME_FORMAT)
|
||||
yield date, body
|
||||
import comment
|
||||
|
||||
|
||||
def query_gerrit(name, count, project):
|
||||
@ -82,30 +31,27 @@ def query_gerrit(name, count, project):
|
||||
|
||||
comments = []
|
||||
for change in changes:
|
||||
for date, message in get_comments(change, name):
|
||||
for date, message in comment.get_comments(change, name):
|
||||
if date is None:
|
||||
# no comments from reviewer yet. This can happen since
|
||||
# 'Uploaded patch set X.' is considered a comment.
|
||||
continue
|
||||
comments.append(Comment(date, change['_number'],
|
||||
change['subject'], message))
|
||||
|
||||
comments.append(comment.Comment(date, change['_number'],
|
||||
change['subject'], message))
|
||||
return sorted(comments, key=lambda comment: comment.date,
|
||||
reverse=True)[0:count]
|
||||
|
||||
|
||||
def vote(comment, success, failure, log=False):
|
||||
for line in comment.message.splitlines():
|
||||
def vote(cmt, success, failure, log=False):
|
||||
for line in cmt.message.splitlines():
|
||||
if line.startswith("* ") or line.startswith("- "):
|
||||
job = line.split(' ')[1]
|
||||
if " : SUCCESS" in line:
|
||||
if job.result == 'SUCCESS':
|
||||
success[job] += 1
|
||||
if log:
|
||||
print line
|
||||
if " : FAILURE" in line:
|
||||
elif job.result == 'FAILRE':
|
||||
failure[job] += 1
|
||||
if log:
|
||||
print line
|
||||
|
||||
if log:
|
||||
print line
|
||||
|
||||
|
||||
def generate_report(name, count, project):
|
||||
@ -122,8 +68,8 @@ def generate_report(name, count, project):
|
||||
print "last seen: %s (%s old)" % (comments[0].date, comments[0].age())
|
||||
result['last'] = epoch(comments[0].date)
|
||||
|
||||
for comment in comments:
|
||||
vote(comment, success, failure)
|
||||
for cmt in comments:
|
||||
vote(cmt, success, failure)
|
||||
|
||||
total = sum(success.values()) + sum(failure.values())
|
||||
if total > 0:
|
||||
@ -214,7 +160,12 @@ def main():
|
||||
for project in names:
|
||||
print 'Checking project: %s' % project
|
||||
for name in names[project]:
|
||||
print 'Checking name: %s' % name
|
||||
if name != 'Jenkins':
|
||||
url = ("https://wiki.openstack.org/wiki/ThirdPartySystems/%s" %
|
||||
name.replace(" ", "_"))
|
||||
print 'Checking name: %s - %s' % (name, url)
|
||||
else:
|
||||
print('Checking name: %s' % name)
|
||||
try:
|
||||
if args.json:
|
||||
report['rows'].append(generate_report(
|
||||
|
Loading…
x
Reference in New Issue
Block a user