123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438 |
- #!/bin/env python
- """
- Formats reports about our RPMs
- Prerequisites:
- - python-markdown
- - a /data directory that contains the workdir dir updated by rpmwatcher_extract_roles.py
- Note, in what follows:
- - NVR means Name Version Release
- - NVRA means Name Version Release Arch
- Those are common concepts in the RPM world.
- """
- from __future__ import print_function
- import argparse
- import os
- import json
- import codecs
- import StringIO
- import markdown
- import rpm
- import urllib
- KOJI_URL = "https://koji.xcp-ng.org"
- KOJI_BUILD_URL = KOJI_URL + "/search?match=exact&type=build&terms=%s"
- # Unless you know an exact *binary* RPM name, there's no better URL than a simple search on the name, without the version
- # They don't display information pages for SRPMs, unfortunately...
- # Also: this URL does only exact name matches, whereas if you use their search field there's
- # an implicit wildcard... But the URL is the same... Not-significant URLs...
- CENTOS_RPM_URL = "https://pkgs.org/download/%s"
- # I could use Fedora's koji for a more direct URL, but it is slow to respond to search queries
- # and I don't want to put too much burden on it (if for example a crawler tries every URL)
- EPEL_RPM_URL = "https://pkgs.org/download/%s"
- def check_dir(dirpath):
- if not os.path.isdir(dirpath):
- raise Exception("Directory %s doesn't exist" % dirpath)
- return dirpath
- def format_role(xcp_builds, xcp_rpms, role, data, max_entries=None):
- if role in ['main', 'extra']:
- # short RPM names
- details = [xcp_rpms[rpm_nvra]['name'] for rpm_nvra in data]
- label_for = ""
- elif role in ['extra_dep', 'other_dep']:
- # short RPM names
- details = [xcp_rpms[rpm_nvra]['name'] for rpm_nvra in data]
- label_for = "for "
- else:
- # short SRPM names
- details = []
- for srpm_nvr in data:
- details.append(xcp_builds[srpm_nvr]['name'])
- label_for = "for "
- if max_entries is not None:
- if len(details) > max_entries:
- details = details[:max_entries] + ['...']
- return "%s (%s%s)" % (role, label_for, ' '.join(details))
- def js_color_cell_values(values=[], color='black'):
- return """
- if (%s.indexOf(cells[i].innerHTML) >=0 ){
- cells[i].style.color = '%s';
- }
- """ % (json.dumps(values), color)
- # From https://docs.python.org/3/howto/sorting.html
- def cmp_to_key(mycmp):
- 'Convert a cmp= function into a key= function'
- class K(object):
- def __init__(self, obj, *args):
- self.obj = obj
- def __lt__(self, other):
- return mycmp(self.obj, other.obj) < 0
- def __gt__(self, other):
- return mycmp(self.obj, other.obj) > 0
- def __eq__(self, other):
- return mycmp(self.obj, other.obj) == 0
- def __le__(self, other):
- return mycmp(self.obj, other.obj) <= 0
- def __ge__(self, other):
- return mycmp(self.obj, other.obj) >= 0
- def __ne__(self, other):
- return mycmp(self.obj, other.obj) != 0
- return K
- def simplify_roles(roles):
- # Most packages have lots of roles, so we need to simplify visually
- # Rules:
- # * 'main' hides 'extra_*' and 'other_*'
- if 'main' in roles:
- for role in roles.keys():
- if role.startswith('extra_') or role.startswith('other_'):
- del roles[role]
- # * 'extra' hides 'other_*'
- if 'extra' in roles:
- for role in roles.keys():
- if role.startswith('other_'):
- del roles[role]
- # * 'xxx_builddep' hides 'xxx_builddep_dep' (same value of xxx and same SRPM)
- for role1 in ['main_builddep', 'extra_builddep', 'other_builddep']:
- if role1 in roles.keys():
- role2 = role1 + '_dep'
- if role2 in roles.keys():
- # remove from role2's SRPMs those that are in role1
- roles[role2] = [x for x in roles[role2] if x not in roles[role1]]
- # remove role2 entirely if empty
- if not roles[role2]:
- del roles[role2]
- # * anything hides something that ends in "_indirect_builddep"
- if [x for x in roles if not x.endswith('_indirect_builddep')]:
- for role in [x for x in roles if x.endswith('_indirect_builddep')]:
- del roles[role]
- # * anything that does not begin with 'other' hides 'other*'
- if [x for x in roles if not x.startswith('other')]:
- for role in roles.keys():
- if role.startswith('other'):
- del roles[role]
- def main():
- parser = argparse.ArgumentParser(description='Format reports about XCP-ng RPMs')
- parser.add_argument('version', help='XCP-ng 2-digit version, e.g. 8.0')
- parser.add_argument('basedir', help='path to the base directory where repos must be present and where '
- 'we\'ll read data from.')
- format_choices = ['csv', 'markdown', 'html']
- parser.add_argument('format', help='output format: %s.' % " or ".join(format_choices), choices=format_choices)
- args = parser.parse_args()
- format = args.format
- shorten_output = format != 'csv'
- elaborate_output = format != 'csv'
- base_dir = os.path.abspath(check_dir(args.basedir))
- xcp_version = args.version
- xcp_srpm_repo = check_dir(os.path.join(base_dir, 'xcp-ng', xcp_version))
- xcp_rpm_repo = check_dir(os.path.join(base_dir, 'xcp-ng_rpms', xcp_version))
- work_dir = check_dir(os.path.join(base_dir, 'workdir', xcp_version))
- reports_dir = os.path.join(work_dir, 'reports')
- if not os.path.exists(reports_dir):
- os.mkdir(reports_dir)
- # Read data from workdir
- with open(os.path.join(work_dir, 'xcp-ng_builds.json')) as f:
- xcp_builds = json.load(f)
- with open(os.path.join(work_dir, 'xcp-ng_rpms.json')) as f:
- xcp_rpms = json.load(f)
- role_priority = [
- 'main',
- 'main_builddep',
- 'main_builddep_dep',
- 'main_indirect_builddep',
- 'extra',
- 'extra_dep',
- 'extra_builddep',
- 'extra_builddep_dep',
- 'extra_indirect_builddep',
- 'other_builddep',
- 'other_builddep_dep',
- 'other_indirect_builddep',
- 'other_dep'
- ]
- srpm_fields_ref = {
- # key: label
- 'srpm_name': 'SRPM name',
- 'repo': 'repo',
- 'version': 'version',
- 'centos_version': 'CentOS version',
- 'epel_version': 'EPEL version',
- 'summary': 'summary',
- 'built_by': 'built by',
- 'added_by': 'added by',
- 'import_reason': 'import reason',
- 'main_role': 'main role',
- 'provenance': 'provenance',
- 'roles': 'main roles',
- 'direct_build_deps': 'direct build deps',
- 'rpms': 'rpms',
- }
- srpm_reports_ref = {
- 'roles_and_deps': [
- 'srpm_name',
- 'repo',
- 'version',
- 'built_by',
- 'added_by',
- 'import_reason',
- 'main_role',
- 'roles',
- 'direct_build_deps',
- 'rpms'
- ],
- 'versions': [
- 'srpm_name',
- 'summary',
- 'repo',
- 'version',
- 'centos_version',
- 'epel_version',
- 'built_by',
- 'added_by',
- 'import_reason',
- 'main_role',
- 'roles'
- ]
- }
- srpm_reports = {}
- for report_name in srpm_reports_ref:
- srpm_reports[report_name] = []
- # data
- for srpm_nvr, build_info in xcp_builds.iteritems():
- repo = build_info['koji_tag'][len('v%s-' % xcp_version):]
- srpm_name = build_info['name']
- summary = build_info['summary']
- built_by = build_info['built-by']
- added_by = build_info.get('added_by', '').lower()
- import_reason = build_info.get('import_reason', '')
- # roles
- simplify_roles(build_info['roles'])
- main_role = None
- roles_list = []
- for role in role_priority:
- if role in build_info['roles']:
- if main_role is None:
- main_role = role
- roles_list.append(format_role(xcp_builds, xcp_rpms, role, build_info['roles'][role],
- max_entries=5 if shorten_output else None))
- roles = "\n".join(roles_list)
- if main_role is None:
- main_role = 'None'
- # build deps are present only for packages built by XCP-ng
- direct_build_deps = ""
- if 'build-deps' in build_info:
- direct_build_deps_list = [xcp_rpms[rpm_nvra]['name'] for rpm_nvra in build_info['build-deps'][0]]
- if shorten_output and len(direct_build_deps_list) > 10:
- direct_build_deps_list = direct_build_deps_list[:10] + ['...']
- direct_build_deps = " ".join(direct_build_deps_list)
- # rpms
- rpms_list = [xcp_rpms[rpm_nvra]['name'] for rpm_nvra in build_info['rpms']]
- if shorten_output and len(rpms_list) > 10:
- rpms_list = rpms_list[:10] + ['...']
- rpms = " ".join(rpms_list)
- # versions: highest version in bold display
- # note: voluntarily avoiding epoch in version comparisons because we might have different epochs
- version = build_info['version'] + '-' + build_info['release']
- nvr_tuple = ('1', build_info['version'], build_info['release'])
- if 'latest-centos' in build_info:
- centos_version = build_info['latest-centos']['version'] + '-' + build_info['latest-centos']['release']
- centos_nvr_tuple = ('1', build_info['latest-centos']['version'], build_info['latest-centos']['release'])
- else:
- centos_version = ""
- centos_nvr_tuple = ('0', '0', '0')
- if 'latest-epel' in build_info:
- epel_version = build_info['latest-epel']['version'] + '-' + build_info['latest-epel']['release']
- epel_nvr_tuple = ('1', build_info['latest-epel']['version'], build_info['latest-epel']['release'])
- else:
- epel_version = ""
- epel_nvr_tuple = ('0', '0', '0')
- max_nvr_tuple = max([nvr_tuple, centos_nvr_tuple, epel_nvr_tuple], key=cmp_to_key(rpm.labelCompare))
- if max_nvr_tuple == nvr_tuple:
- if (centos_version or epel_version) and max_nvr_tuple not in [epel_nvr_tuple, centos_nvr_tuple]:
- version = '**%s**' % version
- elif max_nvr_tuple == centos_nvr_tuple:
- centos_version = '**%s**' % centos_version
- elif max_nvr_tuple == epel_nvr_tuple:
- epel_version = '**%s**' % epel_version
- # add data to reports
- for report_name, report in srpm_reports.iteritems():
- row = []
- for field in srpm_reports_ref[report_name]:
- if field == 'srpm_name':
- if elaborate_output:
- value = "[%s](%s)" % (srpm_name, KOJI_BUILD_URL % urllib.quote(srpm_nvr))
- else:
- value = srpm_name
- elif field == 'repo':
- value = repo
- elif field == 'version':
- value = version
- elif field == 'centos_version':
- if centos_version and elaborate_output:
- value = "[%s](%s)" % (centos_version, CENTOS_RPM_URL % srpm_name)
- else:
- value = centos_version
- elif field == 'epel_version':
- if epel_version and elaborate_output:
- value = "[%s](%s)" % (epel_version, EPEL_RPM_URL % srpm_name)
- else:
- value = epel_version
- elif field == 'summary':
- value = summary
- elif field == 'built_by':
- value = built_by
- elif field == 'added_by':
- value= added_by
- elif field == 'import_reason':
- value= import_reason
- elif field == 'main_role':
- value = main_role
- elif field == 'roles':
- value = roles
- elif field == 'direct_build_deps':
- value = direct_build_deps
- elif field == 'rpms':
- value = rpms
- else:
- raise Exception("Couldn't handle field '%s'" % field)
- row.append(value)
- report.append(row)
- # sort rows in reports
- built_by_order = [
- 'xcp-ng',
- 'centos',
- 'epel',
- 'xs',
- 'unknown'
- ]
- role_priority.append('None')
- for report_name, report in srpm_reports.iteritems():
- headers = srpm_reports_ref[report_name]
- def custom_cmp(row1, row2):
- role_index = headers.index('main_role')
- if role_priority.index(row1[role_index]) > role_priority.index(row2[role_index]):
- return 1
- if role_priority.index(row1[role_index]) < role_priority.index(row2[role_index]):
- return -1
- built_by_index = headers.index('built_by')
- if built_by_order.index(row1[built_by_index]) > built_by_order.index(row2[built_by_index]):
- return 1
- if built_by_order.index(row1[built_by_index]) < built_by_order.index(row2[built_by_index]):
- return -1
- name_index = headers.index('srpm_name')
- return cmp([row1[name_index]], [row2[name_index]])
- report.sort(cmp=custom_cmp)
- # add header
- report.insert(0, [srpm_fields_ref[field] for field in srpm_reports_ref[report_name]])
- # format and write output
- if format == 'csv':
- with codecs.open(os.path.join(reports_dir, 'report_%s.csv' % report_name), 'w', encoding='utf8') as f:
- for row in report:
- row = [field.replace('\n', ' - ') for field in row]
- f.write(';'.join(row) + '\n')
- elif format in ['markdown', 'html']:
- s = StringIO.StringIO()
- s.write(' | '.join(report[0]) + '\n')
- separator = '-'
- for i in xrange(len(report[0]) - 1):
- separator += ' | -'
- s.write(separator + '\n')
- for row in report[1:]:
- row = [field.replace('\n', '<br>') for field in row]
- s.write(' | '.join(row) + '\n')
- try:
- if format == 'markdown':
- with codecs.open(os.path.join(reports_dir, 'report_%s.md' % report_name), 'w', encoding='utf8') as f:
- f.write(s.getvalue())
- elif format == 'html':
- with codecs.open(os.path.join(reports_dir, 'report_%s.html' % report_name), 'w', encoding='utf8') as f:
- f.write("""
- <!DOCTYPE html>
- <html lang="en">
- <head>
- <meta charset="utf-8"/>
- <style>
- table {
- border-width: 1px;
- border-collapse: collapse;
- }
- td, th {
- font-size: 0.75em;
- border-width: 1px;
- border-color: silver;
- border-style: solid;
- padding: 2px;
- }
- </style>
- </head>
- """)
- f.write(markdown.markdown(s.getvalue(), extensions=['tables']))
- script = """
- <script>
- var table = document.getElementsByTagName('table')[0];
- var tbody = table.getElementsByTagName('tbody')[0];
- var cells = tbody.getElementsByTagName('td');
- for (var i=0, len=cells.length; i<len; i++){
- """
- script += js_color_cell_values([v for v in role_priority if v.startswith('main')], 'green')
- script += js_color_cell_values([v for v in role_priority if v.startswith('extra')], 'blue')
- script += js_color_cell_values([v for v in role_priority if v.startswith('other')] + ['None'], 'tomato')
- script += js_color_cell_values(['updates'], 'blue')
- script += js_color_cell_values(['candidates'], 'orangered')
- script += js_color_cell_values(['testing'], 'orangered')
- script += js_color_cell_values(['ci'], 'orangered')
- script += js_color_cell_values(['xcp-ng'], '#263740')
- script += js_color_cell_values(['centos'], 'sienna')
- script += js_color_cell_values(['epel'], 'orchid')
- script += js_color_cell_values(['xs'], 'tomato')
- script += js_color_cell_values(['unknown'], 'red')
- script += """
- }
- </script>
- </html>
- """
- f.write(script)
- else:
- raise("Unexpected.")
- finally:
- s.close()
- else:
- raise("Oops, I don't know how to handle format '%s'." % format)
- if __name__ == "__main__":
- main()
|