CI for the TripleO project
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

tripleo-jobs-gerrit.py 9.1KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255
  1. #!/usr/bin/python
  2. import argparse
  3. import datetime
  4. import json
  5. import re
  6. import subprocess
  7. import sys
  8. # Do not include the -nv suffix in the job name here. The code will handle
  9. # reading both the voting and non-voting forms of the job if they exist.
  10. DEFAULT_JOB_NAMES = [
  11. 'tripleo-ci-centos-7-undercloud-containers',
  12. 'tripleo-ci-centos-7-containers-multinode',
  13. 'tripleo-ci-centos-7-scenario000-multinode-oooq-container-updates',
  14. 'tripleo-ci-centos-7-standalone',
  15. 'tripleo-ci-centos-7-scenario001-standalone',
  16. 'tripleo-ci-centos-7-scenario002-standalone',
  17. 'tripleo-ci-centos-7-scenario003-standalone',
  18. 'tripleo-ci-centos-7-scenario004-standalone',
  19. ]
  20. DEFAULT_PROJECTS = [
  21. 'openstack/tripleo-heat-templates',
  22. 'openstack/os-apply-config',
  23. 'openstack/os-collect-config',
  24. 'openstack/os-net-config',
  25. 'openstack/os-refresh-config',
  26. 'openstack/paunch',
  27. 'openstack/python-tripleoclient',
  28. 'openstack-infra/tripleo-ci',
  29. 'openstack/tripleo-common',
  30. 'openstack/tripleo-image-elements',
  31. 'openstack/tripleo-puppet-elements',
  32. 'openstack/tripleo-ansible',
  33. 'openstack/tripleo-repos',
  34. 'openstack/tripleo-upgrade',
  35. 'openstack/tripleo-quickstart',
  36. 'openstack/tripleo-quickstart-extras',
  37. 'openstack/tripleo-ha-utils',
  38. 'openstack/tripleo-validations',
  39. 'openstack/tripleo-ipsec',
  40. '^openstack/puppet-.*',
  41. ]
  42. COLORS = {"SUCCESS": "#008800", "FAILURE": "#FF0000", "ABORTED": "#000000"}
  43. def get_gerrit_reviews(project, status="open", branch="master", limit="30"):
  44. arr = []
  45. status_query = ''
  46. if status:
  47. status_query = 'status: %s' % status
  48. cmd = 'ssh review.opendev.org -p29418 gerrit' \
  49. ' query "%s project: %s branch: %s" --comments' \
  50. ' --format JSON limit: %s --patch-sets --current-patch-set'\
  51. % (status_query, project, branch, limit)
  52. p = subprocess.Popen([cmd], shell=True, stdin=subprocess.PIPE,
  53. stdout=subprocess.PIPE, stderr=subprocess.PIPE)
  54. stdout = p.stdout
  55. for line in stdout.readlines():
  56. review = json.loads(line)
  57. if 'project' in review:
  58. arr.append(review)
  59. return arr
  60. def get_jenkins_comment_message(review):
  61. jenkins_messages = {}
  62. for comment in review['comments']:
  63. if 'name' in comment['reviewer']:
  64. if comment['reviewer']['name'] == 'Zuul':
  65. if "NOT_REGISTERED" in comment['message']:
  66. continue
  67. # NOTE(bnemec): For some reason the experimental-tripleo
  68. # message does not include "pipeline".
  69. if ('tripleo-ci' not in comment['message']):
  70. continue
  71. jenkins_messages[comment['timestamp']] = comment['message']
  72. return jenkins_messages
  73. def process_jenkins_comment_message(message, job_names):
  74. job_results = {}
  75. for line in message.split('\n'):
  76. if line and line[0] == '-':
  77. split = line.split(" ", 6)
  78. voting_job_name = split[1]
  79. if voting_job_name.endswith('-nv'):
  80. voting_job_name = voting_job_name[:-3]
  81. if voting_job_name in job_names:
  82. if len(split) > 6:
  83. duration = " ".join(split[6].split()[:2])
  84. else:
  85. duration = ''
  86. job_results[voting_job_name] = {'log_url': split[2],
  87. 'status': split[4],
  88. 'duration': duration}
  89. return job_results
  90. def gen_html(data, html_file, table_file, stats_hours, job_names, options):
  91. fp = open(table_file, "w")
  92. fp.write('<table border="1" cellspacing="0">')
  93. fp.write("<tr class='headers'><td>&nbsp;</td>")
  94. for job_name in job_names:
  95. fp.write("<td class='headers'><b>%s</b></td>" %
  96. job_name.replace("tripleo-ci-centos-7-", ""))
  97. fp.write("</tr>")
  98. count = 0
  99. reversed_sorted_keys = [(x['id'], x['patchset']) for x in
  100. reversed(sorted(data.values(),
  101. key=lambda y: y['ts']))]
  102. passed_jobs = 0
  103. partial_jobs = 0
  104. failed_jobs = 0
  105. for key in reversed_sorted_keys:
  106. result = data[key]
  107. if count > 300:
  108. break
  109. if not result['ci_results']:
  110. continue
  111. if (count % 2) == 1:
  112. fp.write("<tr class='tr0'>")
  113. else:
  114. fp.write("<tr class='tr1'>")
  115. count += 1
  116. fp.write("<td>")
  117. fp.write(result['timestamp'])
  118. fp.write("<br/>")
  119. fp.write(result['project'])
  120. fp.write("/")
  121. fp.write(result['branch'])
  122. fp.write("<br/>")
  123. fp.write(result['status'])
  124. fp.write("</td>")
  125. job_columns = ""
  126. result_types = set()
  127. for job_name in job_names:
  128. if job_name in result['ci_results']:
  129. job_columns += "<td>"
  130. ci_result = result['ci_results'][job_name]
  131. color = COLORS.get(ci_result['status'], "#666666")
  132. result_types.add(ci_result['status'])
  133. job_columns += '<font color="%s">' % color
  134. gerrit_href = 'https://review.opendev.org/#/c/%s/%s"' % (
  135. result['url'].split('/')[-1], result['patchset']
  136. )
  137. job_columns += '<a STYLE="color : %s" href="%s">%s,%s</a>' % \
  138. (color, gerrit_href, result['url'].split('/')[-1],
  139. result['patchset'])
  140. job_columns += '<br/>%s ' % (ci_result['duration'])
  141. job_columns += '<a STYLE="text-decoration:none" '
  142. job_columns += 'href="%s">log</a>' %\
  143. ci_result['log_url']
  144. job_columns += '</font><br/>'
  145. job_columns += "</td>"
  146. else:
  147. job_columns += "<td>&nbsp;</td>"
  148. # For the purpose of these stats, let's ignore POST_FAILURE jobs
  149. result_types.discard('POST_FAILURE')
  150. if len(result_types) > 1:
  151. partial_jobs += 1
  152. elif 'FAILURE' in result_types:
  153. failed_jobs += 1
  154. else:
  155. passed_jobs += 1
  156. fp.write(job_columns)
  157. fp.write("</tr>")
  158. fp.write("<table>")
  159. fp.write("<p>Query parameters:</p>")
  160. fp.write("Branch: "+options.b+"<br/>")
  161. fp.write("Status: "+options.s+"<br/>")
  162. fp.write("Limit: "+options.l)
  163. total = passed_jobs + partial_jobs + failed_jobs
  164. fp.write("<p>Overall</p>")
  165. fp.write("Passed: %d/%d (%d %%)<br/>" % (
  166. passed_jobs,
  167. total,
  168. float(passed_jobs) / float(total) * 100
  169. ))
  170. fp.write("Partial Failures: %d/%d (%d %%)<br/>" % (
  171. partial_jobs,
  172. total,
  173. float(partial_jobs) / float(total) * 100
  174. ))
  175. fp.write("Complete Failures: %d/%d (%d %%)<br/>" % (
  176. failed_jobs,
  177. total,
  178. float(failed_jobs) / float(total) * 100
  179. ))
  180. fp.close()
  181. with open(html_file, "w") as f:
  182. f.write('<html><head/><body>')
  183. f.write(open(table_file).read())
  184. f.write("<table></body></html>")
  185. def main(args=sys.argv[1:]):
  186. parser = argparse.ArgumentParser(
  187. description=("Get details of tripleo ci jobs and generates a html "
  188. "report."))
  189. parser.add_argument('-o', default="tripleo-jobs.html", help="html file")
  190. parser.add_argument('-p', default=",".join(DEFAULT_PROJECTS),
  191. help='comma separated list of projects to use.')
  192. parser.add_argument('-j', default=",".join(DEFAULT_JOB_NAMES),
  193. help='comma separated list of jobs to monitor.')
  194. parser.add_argument('-s', default="", help="status")
  195. parser.add_argument('-b', default="master", help="branch")
  196. parser.add_argument('-l', default="30", help="limit")
  197. opts = parser.parse_args(args)
  198. job_names = opts.j.split(",")
  199. # project reviews
  200. proj_reviews = []
  201. for proj in opts.p.split(","):
  202. proj_reviews.extend(get_gerrit_reviews(proj,
  203. status=opts.s,
  204. branch=opts.b,
  205. limit=opts.l))
  206. results = {}
  207. for review in proj_reviews:
  208. for ts, message in get_jenkins_comment_message(review).iteritems():
  209. ci_results = process_jenkins_comment_message(message,
  210. job_names)
  211. patchset = str(re.search('Patch Set (.+?):', message).group(1))
  212. key = (review['id'], patchset)
  213. results.setdefault(key, {}).update({
  214. 'id': review['id'],
  215. 'ts': ts,
  216. 'status': review['status'],
  217. 'timestamp': datetime.datetime.fromtimestamp(
  218. int(ts)).strftime('%Y-%m-%d %H:%M:%S'),
  219. 'url': review['url'],
  220. 'patchset': patchset,
  221. 'project': re.sub(r'.*/', '', review['project']),
  222. 'branch': review['branch'],
  223. })
  224. results[key].setdefault(
  225. 'ci_results', {}).update(ci_results)
  226. gen_html(results, opts.o, "%s-table" % opts.o, 24, job_names, opts)
  227. if __name__ == '__main__':
  228. exit(main())