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:
Walter A. Boring IV 2017-02-07 17:52:50 +00:00
parent 93f2305552
commit 26611b68ed
5 changed files with 436 additions and 71 deletions

View File

@ -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

View File

@ -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

View 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()

View 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

View File

@ -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(