sync_repo_from_koji.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359
  1. #!/bin/env python
  2. from __future__ import print_function
  3. import argparse
  4. import re
  5. import os
  6. import sys
  7. import subprocess
  8. import glob
  9. import shutil
  10. import tempfile
  11. import atexit
  12. from datetime import datetime
  13. RELEASE_VERSIONS = [
  14. '7.6',
  15. '8.0',
  16. '8.1',
  17. '8.2',
  18. '8.3',
  19. ]
  20. DEV_VERSIONS = [
  21. ]
  22. VERSIONS = DEV_VERSIONS + RELEASE_VERSIONS
  23. # Not used, just here as memory and in the unlikely case we might need to update their repos again
  24. DEAD_TAGS = [
  25. 'v7.6-base',
  26. 'v7.6-updates',
  27. 'v7.6-testing',
  28. 'v8.0-base',
  29. 'v8.0-updates',
  30. 'v8.0-testing',
  31. 'v8.1-base',
  32. 'v8.1-updates',
  33. 'v8.1-testing',
  34. ]
  35. TAGS = [
  36. 'v8.2-base',
  37. 'v8.2-updates',
  38. 'v8.2-candidates',
  39. 'v8.2-testing',
  40. 'v8.2-ci',
  41. 'v8.2-incoming',
  42. 'v8.2-lab',
  43. 'v8.3-base',
  44. 'v8.3-updates',
  45. 'v8.3-candidates',
  46. 'v8.3-testing',
  47. 'v8.3-ci',
  48. 'v8.3-incoming',
  49. 'v8.3-lab',
  50. ]
  51. # tags in which we only keep the latest build for each package
  52. RELEASE_TAGS = [
  53. 'v7.6-base',
  54. 'v8.0-base',
  55. 'v8.1-base',
  56. 'v8.2-base',
  57. 'v8.3-base',
  58. ]
  59. # tags for which we want to export a stripped repo for offline updates
  60. OFFLINE_TAGS = [
  61. 'v8.2-updates',
  62. 'v8.2-v-linstor',
  63. 'v8.3-updates',
  64. 'v8.3-v-linstor',
  65. ]
  66. # Additional "user" tags. For them, repos are generated at a different place.
  67. # Initialized empty: user tags are autodetected based on their name
  68. U_TAGS = []
  69. # Additional V-tags (V stands for "vates" or for "vendor"). For them, repos also are generated at a different place.
  70. # Initialized empty: V-tags are autodetected based on their name
  71. V_TAGS = []
  72. KOJI_ROOT_DIR = '/mnt/koji'
  73. KEY_ID = "3fd3ac9e"
  74. DEVNULL = open(os.devnull, 'w')
  75. def version_from_tag(tag):
  76. matches = re.match(r'v(\d+\.\d+)', tag)
  77. return matches.group(1)
  78. def repo_name_from_tag(tag):
  79. version = version_from_tag(tag)
  80. name = tag[len("v%s-" % version):]
  81. if name.startswith('u-') or name.startswith('v-'):
  82. name = name[2:]
  83. return name
  84. def build_path_to_version(parent_dir, tag):
  85. version = version_from_tag(tag)
  86. major = version.split('.')[0]
  87. return os.path.join(parent_dir, major, version)
  88. def build_path_to_repo(parent_dir, tag):
  89. return os.path.join(build_path_to_version(parent_dir, tag), repo_name_from_tag(tag))
  90. def sign_rpm(rpm):
  91. # create temporary work directory
  92. tmpdir = tempfile.mkdtemp(prefix=rpm)
  93. current_dir = os.getcwd()
  94. try:
  95. os.chdir(tmpdir)
  96. # download from koji
  97. subprocess.check_call(['koji', 'download-build', '--debuginfo', '--noprogress', '--rpm', rpm])
  98. # sign: requires a sign-rpm executable or alias in the PATH
  99. subprocess.check_call(['sign-rpm', rpm], stdout=DEVNULL)
  100. # import signature
  101. subprocess.check_call(['koji', 'import-sig', rpm])
  102. finally:
  103. # clean up
  104. os.chdir(current_dir)
  105. shutil.rmtree(tmpdir)
  106. def write_repo(tag, dest_dir, tmp_root_dir):
  107. version = version_from_tag(tag)
  108. repo_name = repo_name_from_tag(tag)
  109. # Hack for 7.6 because koji only handles its updates and updates_testing repos:
  110. if version == '7.6':
  111. if repo_name == 'testing':
  112. repo_name = 'updates_testing'
  113. elif repo_name != 'updates':
  114. raise Exception("Fatal: koji should not have any changes outside testing and updates for 7.6!")
  115. path_to_repo = build_path_to_repo(dest_dir, tag)
  116. path_to_tmp_repo = build_path_to_repo(tmp_root_dir, tag)
  117. # remove temporary repo if exists
  118. if os.path.isdir(path_to_tmp_repo):
  119. shutil.rmtree(path_to_tmp_repo)
  120. # create empty structure
  121. print("\n-- Copy the RPMs from %s to %s" % (KOJI_ROOT_DIR, path_to_tmp_repo))
  122. for d in ['x86_64/Packages', 'Source/SPackages']:
  123. os.makedirs(os.path.join(path_to_tmp_repo, d))
  124. print("Link to latest dist-repo: %s" % os.readlink('%s/repos-dist/%s/latest' % (KOJI_ROOT_DIR, tag)))
  125. # copy RPMs from koji
  126. for f in glob.glob('%s/repos-dist/%s/latest/x86_64/Packages/*/*.rpm' % (KOJI_ROOT_DIR, tag)):
  127. shutil.copy(f, os.path.join(path_to_tmp_repo, 'x86_64', 'Packages'))
  128. # and source RPMs
  129. for f in glob.glob('%s/repos-dist/%s/latest/src/Packages/*/*.rpm' % (KOJI_ROOT_DIR, tag)):
  130. shutil.copy(f, os.path.join(path_to_tmp_repo, 'Source', 'SPackages'))
  131. # generate repodata and sign
  132. for path in [os.path.join(path_to_tmp_repo, 'x86_64'), os.path.join(path_to_tmp_repo, 'Source')]:
  133. print("\n-- Generate repodata for %s" % path)
  134. subprocess.check_call(['createrepo_c', path], stdout=DEVNULL)
  135. subprocess.check_call(['sign-file', os.path.join(path, 'repodata', 'repomd.xml')], stdout=DEVNULL)
  136. # Synchronize to our final repository:
  137. # - add new RPMs
  138. # - remove RPMs that are not present anymore (for tags in RELEASE_TAGS)
  139. # - do NOT change the creation nor modification stamps for existing RPMs that have not been modified
  140. # (and usually there's no reason why they would have been modified without changing names)
  141. # => using -c and omitting -t
  142. # - sync updated repodata
  143. print("\n-- Syncing to final repository %s" % path_to_repo)
  144. if not os.path.exists(path_to_repo):
  145. os.makedirs(path_to_repo)
  146. subprocess.check_call(['rsync', '-crlpi', '--delete-after', path_to_tmp_repo + '/', path_to_repo])
  147. print()
  148. shutil.rmtree(path_to_tmp_repo)
  149. def sign_unsigned_rpms(tag):
  150. # get list of RPMs not signed by us by comparing the list that is signed with the full list
  151. # all RPMs for the tag
  152. output = subprocess.check_output(['koji', 'list-tagged', tag, '--rpms'])
  153. rpms = set(output.strip().splitlines())
  154. # only signed RPMs
  155. # koji list-tagged v7.6-base --sigs | grep "^3fd3ac9e" | cut -c 10-
  156. signed_rpms = set()
  157. output = subprocess.check_output(['koji', 'list-tagged', tag, '--sigs'])
  158. for line in output.strip().splitlines():
  159. try:
  160. key, rpm = line.split(' ')
  161. except:
  162. # couldn't unpack values... no signature.
  163. continue
  164. if key == KEY_ID:
  165. signed_rpms.add(rpm)
  166. # diff and sort
  167. unsigned_rpms = sorted(list(rpms.difference(signed_rpms)))
  168. if unsigned_rpms:
  169. print("\nSigning unsigned RPMs first\n")
  170. for rpm in unsigned_rpms:
  171. sign_rpm(rpm + '.rpm')
  172. for rpm in unsigned_rpms:
  173. if rpm.endswith('.src'):
  174. nvr = rpm[:-4]
  175. # write signed file to koji's own repositories
  176. subprocess.check_call(['koji', 'write-signed-rpm', KEY_ID, nvr])
  177. def atexit_remove_lock(lock_file):
  178. os.unlink(lock_file)
  179. def main():
  180. parser = argparse.ArgumentParser(description='Detect package changes in koji and update repository')
  181. parser.add_argument('dest_dir', help='root directory of the destination repository')
  182. parser.add_argument('u_dest_dir', help='root directory of the destination repository for user tags')
  183. parser.add_argument('v_dest_dir', help='root directory of the destination repository for V-tags')
  184. parser.add_argument('data_dir', help='directory where the script will write or read data from')
  185. parser.add_argument('--quiet', action='store_true',
  186. help='do not output anything unless there are changes to be considered')
  187. parser.add_argument('--modify-stable-base', action='store_true',
  188. help='allow modifying the base repository of a stable release')
  189. args = parser.parse_args()
  190. dest_dir = args.dest_dir
  191. u_dest_dir = args.u_dest_dir
  192. v_dest_dir = args.v_dest_dir
  193. data_dir = args.data_dir
  194. tmp_root_dir = os.path.join(data_dir, 'tmproot')
  195. quiet = args.quiet
  196. lock_file = os.path.join(data_dir, 'lock')
  197. if os.path.exists(lock_file):
  198. print("Lock file %s already exists. Aborting." % lock_file)
  199. return
  200. else:
  201. open(lock_file, 'w').close()
  202. atexit.register(atexit_remove_lock, lock_file)
  203. global U_TAGS, V_TAGS
  204. U_TAGS += subprocess.check_output(['koji', 'list-tags', 'v*.*-u-*']).strip().splitlines()
  205. V_TAGS += subprocess.check_output(['koji', 'list-tags', 'v*.*-v-*']).strip().splitlines()
  206. def dest_dir_for_tag(tag):
  207. if tag in U_TAGS:
  208. return u_dest_dir
  209. if tag in V_TAGS:
  210. return v_dest_dir
  211. return dest_dir
  212. def offline_repo_dir():
  213. return os.path.join(v_dest_dir, 'offline')
  214. for version in VERSIONS:
  215. for tag in TAGS + U_TAGS + V_TAGS:
  216. if version_from_tag(tag) != version:
  217. continue
  218. needs_update = False
  219. # get current list of packages from koji for this tag
  220. tag_builds_koji = subprocess.check_output(['koji', 'list-tagged', '--quiet', tag])
  221. # read latest known list of builds in the tag if exists
  222. tag_builds_filepath = os.path.join(data_dir, "%s-builds.txt" % tag)
  223. if os.path.exists(tag_builds_filepath):
  224. with open(tag_builds_filepath, 'r') as f:
  225. tag_builds_txt = f.read()
  226. if tag_builds_koji != tag_builds_txt:
  227. needs_update = True
  228. else:
  229. needs_update = True
  230. msgs = ["\n*** %s" % tag]
  231. if needs_update:
  232. msgs.append("Repository update needed")
  233. if tag in RELEASE_TAGS and version not in DEV_VERSIONS:
  234. if args.modify_stable_base:
  235. msgs.append("Modification of base repository for stable release %s " % version
  236. + "allowed through the --modify-stable-base switch.")
  237. else:
  238. if not quiet:
  239. msgs.append("Not modifying base repository for stable release %s..." % version)
  240. print('\n'.join(msgs))
  241. continue
  242. print('\n'.join(msgs))
  243. # sign RPMs in the tag if needed
  244. sign_unsigned_rpms(tag)
  245. # export the RPMs from koji
  246. print("\n-- Make koji write the repository for tag %s" % tag)
  247. with_non_latest = [] if tag in RELEASE_TAGS else ['--non-latest']
  248. sys.stdout.flush()
  249. subprocess.check_call(['koji', 'dist-repo', tag, '3fd3ac9e', '--with-src', '--noinherit'] + with_non_latest)
  250. # write repository to the appropriate destination directory for the tag
  251. write_repo(tag, dest_dir_for_tag(tag), tmp_root_dir)
  252. if tag in OFFLINE_TAGS:
  253. print("\n-- Make koji write the offline repository for tag %s" % tag)
  254. # Also generate a stripped repo for offline updates
  255. sys.stdout.flush()
  256. subprocess.check_call(['koji', 'dist-repo', tag, '3fd3ac9e', '--noinherit'])
  257. write_repo(tag, offline_repo_dir(), tmp_root_dir)
  258. # Wrap it up in a tarball
  259. offline_repo_path = build_path_to_repo(offline_repo_dir(), tag)
  260. offline_repo_path_parent = os.path.dirname(offline_repo_path)
  261. offline_tarball_path_prefix = os.path.join(
  262. offline_repo_path_parent,
  263. "xcpng-%s-offline-%s" % (version.replace('.', '_'), repo_name_from_tag(tag))
  264. )
  265. offline_tarball = "%s-%s.tar" % (offline_tarball_path_prefix, datetime.now().strftime("%Y%m%d"))
  266. print("\n-- Generate offline update tarball: %s" % offline_tarball)
  267. subprocess.check_call(['rm', '-f', offline_tarball])
  268. subprocess.check_call([
  269. 'tar',
  270. '-cf', offline_tarball,
  271. '-C', offline_repo_path_parent,
  272. os.path.basename(offline_repo_path)
  273. ])
  274. # Point the "latest" symlink at the tarball
  275. latest_symlink = "%s-latest.tar" % offline_tarball_path_prefix
  276. if os.path.exists(latest_symlink):
  277. os.unlink(latest_symlink)
  278. # relative symlink
  279. os.symlink(os.path.basename(offline_tarball), latest_symlink)
  280. # And remove older tarballs
  281. tarballs = glob.glob("%s-*.tar" % offline_tarball_path_prefix)
  282. tarballs.remove(latest_symlink)
  283. tarballs_sorted_by_mtime = sorted(tarballs, key=os.path.getmtime, reverse=True)
  284. # Remove all but the latest three tarballs
  285. for old_tarball in tarballs_sorted_by_mtime[3:]:
  286. print("Removing old tarball: %s" % old_tarball)
  287. os.remove(old_tarball)
  288. # update data
  289. with open(tag_builds_filepath, 'w') as f:
  290. f.write(tag_builds_koji)
  291. elif not quiet:
  292. print('\n'.join(msgs))
  293. print("Already up to date")
  294. if __name__ == "__main__":
  295. main()