rpmwatcher_update.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259
  1. #!/bin/env python
  2. """
  3. Update stats about XCP-ng RPMs.
  4. Prerequisites:
  5. - a directory that contains
  6. - a local CentOS repo of SRPMs, all SRPMs in the first-level directory. Directory name: centos.
  7. - a local EPEL repo of SRPMs, all SRPMs in the first-level directory. Directory name: epel.
  8. - a local XCP-ng repo of SRPMs, all SRPMs in the first-level directory. Directory name: xcp-ng/{version}, e.g. xcp-ng/8.0
  9. - a working directory: workdir
  10. - the user running the script must have access to koji through cli
  11. """
  12. from __future__ import print_function
  13. import argparse
  14. import subprocess
  15. import os
  16. import rpm
  17. import json
  18. import urllib2
  19. DEVNULL = open(os.devnull, 'w')
  20. MAIN_TAGS = ['base', 'updates', 'candidates', 'testing', 'ci']
  21. def check_dir(dirpath):
  22. if not os.path.isdir(dirpath):
  23. raise Exception("Directory %s doesn't exist" % dirpath)
  24. return dirpath
  25. def list_tags_for_version(version):
  26. tags = subprocess.check_output(['koji', 'list-tags', 'v%s*' % version]).splitlines()
  27. sorted_tags = []
  28. # first get the main tags
  29. for tag_suffix in MAIN_TAGS:
  30. t = 'v%s-%s' % (version, tag_suffix)
  31. if t in tags:
  32. sorted_tags.append(t)
  33. tags.remove(t)
  34. # # add the remaining tags
  35. # # UPDATE: not adding them anymore until we download the corresponding SRPMs and RPMs from koji
  36. # # and can add them as repos to be used by rpmwatcher_extract_deps.py
  37. # sorted_tags += sorted(tags)
  38. return sorted_tags
  39. def get_builds_for_tag(tag, latest=False):
  40. builds = []
  41. param_latest = ['--latest'] if latest else []
  42. lines = subprocess.check_output(['koji', 'list-tagged', tag, '--quiet'] + param_latest).splitlines()
  43. for line in lines:
  44. srpm_nvr = line.split()[0]
  45. builds.append(srpm_nvr)
  46. return sorted(builds)
  47. def get_info_from_srpm_file(filepath):
  48. if not os.path.exists(filepath):
  49. return {}
  50. output = subprocess.check_output(
  51. ['rpm', '-qp', filepath, '--qf', '%{name};;%{vendor};;%{summary};;%{nvr};;%{epoch};;%{version};;%{release}'],
  52. stderr=DEVNULL).split(';;')
  53. return {
  54. 'name': output[0],
  55. 'vendor': output[1],
  56. 'summary': output[2],
  57. 'nvr': output[3],
  58. 'epoch': '' if output[4] == '(none)' else output[4],
  59. 'version': output[5],
  60. 'release': output[6],
  61. }
  62. def get_latest_srpms_info_from_dir(dirpath):
  63. result = {}
  64. for filename in os.listdir(dirpath):
  65. info = get_info_from_srpm_file(os.path.join(dirpath, filename))
  66. if info['name'] not in result:
  67. result[info['name']] = info
  68. else:
  69. # keep only the newest
  70. prev_info = result[info['name']]
  71. if rpm.labelCompare((prev_info['epoch'], prev_info['version'], prev_info['release']),
  72. (info['epoch'], info['version'], info['release'])) < 0:
  73. result[info['name']] = info
  74. return result
  75. def version_release(version, release):
  76. return version + '-' + release
  77. def main():
  78. parser = argparse.ArgumentParser(description='Update stats about XCP-ng RPMs')
  79. parser.add_argument('version', help='XCP-ng 2-digit version, e.g. 8.0')
  80. parser.add_argument('basedir', help='path to the base directory where repos must be present and where '
  81. 'we\'ll output results.')
  82. args = parser.parse_args()
  83. base_dir = os.path.abspath(check_dir(args.basedir))
  84. xcp_version = args.version
  85. xcp_srpm_repo = check_dir(os.path.join(base_dir, 'xcp-ng', xcp_version))
  86. centos_srpm_repo = check_dir(os.path.join(base_dir, 'centos'))
  87. epel_srpm_repo = check_dir(os.path.join(base_dir, 'epel'))
  88. work_dir = check_dir(os.path.join(base_dir, 'workdir', xcp_version))
  89. built_by = {}
  90. built_by['centos'] = get_builds_for_tag('built-by-centos')
  91. built_by['epel'] = get_builds_for_tag('built-by-epel')
  92. built_by['xs'] = get_builds_for_tag('built-by-xs')
  93. built_by['xcp-ng'] = get_builds_for_tag('built-by-xcp-ng')
  94. # Get "manually written" information about our packages
  95. expected_headers = ['SRPM_name', 'added_by', 'import_reason', 'latest_release_URL', 'latest_release_regexp']
  96. filename = "packages_provenance.csv"
  97. provenance_csv = urllib2.urlopen(
  98. 'https://raw.githubusercontent.com/xcp-ng/xcp/master/data/%s/%s' % (xcp_version, filename)
  99. ).read()
  100. provenance_csv = [line.split(';') for line in provenance_csv.splitlines()]
  101. csv_headers = provenance_csv[0]
  102. if csv_headers != expected_headers:
  103. raise Exception("The headers in %s were different from what was expected. Expected %s, got %s."
  104. % (filename, expected_headers, csv_headers))
  105. provenance_csv = provenance_csv[1:]
  106. # If we have information from previous runs, use that to detect updated packages
  107. previous_centos_file_path = os.path.join(work_dir, 'centos-srpms.json')
  108. centos_srpms_previous = {}
  109. if os.path.exists(previous_centos_file_path):
  110. with open(previous_centos_file_path) as f:
  111. centos_srpms_previous = json.load(f)
  112. previous_epel_file_path = os.path.join(work_dir, 'epel-srpms.json')
  113. epel_srpms_previous = {}
  114. if os.path.exists(previous_epel_file_path):
  115. with open(previous_epel_file_path) as f:
  116. epel_srpms_previous = json.load(f)
  117. # Read centos and epel repos
  118. # This takes time because each SRPM will be read by rpm -qp --qf
  119. # We could speed this up a lot by parsing repodata
  120. centos_srpms = get_latest_srpms_info_from_dir(centos_srpm_repo)
  121. epel_srpms = get_latest_srpms_info_from_dir(epel_srpm_repo)
  122. tags = list_tags_for_version(xcp_version)
  123. xcp_builds = {}
  124. excluded_builds = []
  125. latest_release_by_name = {}
  126. for tag in tags:
  127. builds = get_builds_for_tag(tag, latest=True)
  128. for srpm_nvr in builds:
  129. build_info = get_info_from_srpm_file(os.path.join(xcp_srpm_repo, srpm_nvr + '.src.rpm'))
  130. if not build_info:
  131. # SRPM not present in repos
  132. excluded_builds.append(srpm_nvr)
  133. continue
  134. name = build_info['name']
  135. # Keep only the latest release for a given SRPM name
  136. if name in latest_release_by_name:
  137. prev_info = latest_release_by_name[name]
  138. is_latest = rpm.labelCompare((prev_info['epoch'], prev_info['version'], prev_info['release']),
  139. (build_info['epoch'], build_info['version'], build_info['release'])) < 0
  140. if not is_latest:
  141. # skip
  142. continue
  143. else:
  144. # remove previous one
  145. del xcp_builds[prev_info['nvr']]
  146. latest_release_by_name[name] = {
  147. 'nvr': build_info['nvr'],
  148. 'epoch': build_info['epoch'],
  149. 'version': build_info['version'],
  150. 'release': build_info['release']
  151. }
  152. build_info['koji_tag'] = tag
  153. build_info['built-by'] = 'unknown'
  154. for builder in built_by:
  155. if srpm_nvr in built_by[builder]:
  156. build_info['built-by'] = builder
  157. break
  158. # Notify about updated packages in CentOS, EPEL...
  159. if name in centos_srpms:
  160. build_info['latest-centos'] = centos_srpms[name]
  161. if name in centos_srpms_previous and centos_srpms[name]['nvr'] != centos_srpms_previous[name]['nvr']:
  162. print("Updated in CentOS: %s from %s to %s (XCP-ng: %s)"
  163. % (name,
  164. version_release(centos_srpms_previous[name]['version'], centos_srpms_previous[name]['release']),
  165. version_release(centos_srpms[name]['version'], centos_srpms[name]['release']),
  166. version_release(build_info['version'], build_info['release'])))
  167. if name in epel_srpms:
  168. build_info['latest-epel'] = epel_srpms[name]
  169. if name in epel_srpms_previous and epel_srpms[name]['nvr'] != epel_srpms_previous[name]['nvr']:
  170. print("Updated in EPEL: %s from %s to %s (XCP-ng: %s)"
  171. % (name,
  172. version_release(epel_srpms_previous[name]['version'], epel_srpms_previous[name]['release']),
  173. version_release(epel_srpms[name]['version'], epel_srpms[name]['release']),
  174. version_release(build_info['version'], build_info['release'])))
  175. # provenance
  176. srpm_name_index = csv_headers.index('SRPM_name')
  177. for row in provenance_csv:
  178. if row[srpm_name_index] == name:
  179. for i in xrange(len(csv_headers)):
  180. field_name = csv_headers[i]
  181. if field_name == 'SRPM_name':
  182. continue
  183. if field_name in build_info:
  184. raise("Key collision! I'm trying to add the '%s' key which already exists!" % field_name)
  185. build_info[field_name] = row[i]
  186. xcp_builds[srpm_nvr] = build_info
  187. # Add list of RPMs nvra for each SRPM
  188. xcp_ng_rpms_srpms = {}
  189. with open(os.path.join(work_dir, 'xcp-ng-rpms-srpms.txt')) as f:
  190. for line in f.read().splitlines():
  191. rpm_filename, srpm_filename, rpm_shortname = line.split(",")
  192. if '-debuginfo-' in rpm_filename:
  193. continue
  194. srpm_nvr = srpm_filename[:-8] # remove .src.rpm
  195. rpm_nvra = rpm_filename[:-4] # remove .rpm
  196. if srpm_nvr in xcp_builds:
  197. if 'rpms' not in xcp_builds[srpm_nvr]:
  198. xcp_builds[srpm_nvr]['rpms'] = []
  199. xcp_builds[srpm_nvr]['rpms'].append(rpm_nvra)
  200. # also populate this dict that will be useful later
  201. xcp_ng_rpms_srpms[rpm_nvra] = {'name': rpm_shortname, 'srpm_nvr': srpm_nvr}
  202. # Get the list of RPMs that are considered "extra installable packages"
  203. # rpmwatcher_extract_roles.py will need them and can't use koji since it is run within a container
  204. lines = subprocess.check_output(['koji', 'list-groups', 'V%s' % xcp_version, 'installable_extras']).splitlines()
  205. lines = lines[1:]
  206. extra_rpms = []
  207. for line in lines:
  208. extra_rpms.append(line.strip().split(':')[0])
  209. # Write files
  210. with open(os.path.join(work_dir, 'excluded_builds.txt'), 'w') as f:
  211. f.write('\n'.join(excluded_builds))
  212. with open(os.path.join(work_dir, 'xcp-ng_builds_WIP.json'), 'w') as f:
  213. f.write(json.dumps(xcp_builds, sort_keys=True, indent=4))
  214. with open(os.path.join(work_dir, 'extra_installable.txt'), 'w') as f:
  215. f.write('\n'.join(extra_rpms))
  216. with open(os.path.join(work_dir, 'xcp-ng-rpms-srpms.json'), 'w') as f:
  217. f.write(json.dumps(xcp_ng_rpms_srpms, sort_keys=True, indent=4))
  218. with open(os.path.join(work_dir, 'centos-srpms.json'), 'w') as f:
  219. f.write(json.dumps(centos_srpms, sort_keys=True, indent=4))
  220. with open(os.path.join(work_dir, 'epel-srpms.json'), 'w') as f:
  221. f.write(json.dumps(epel_srpms, sort_keys=True, indent=4))
  222. if __name__ == "__main__":
  223. main()