housekeeping.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345
  1. # coding: utf-8
  2. from __future__ import print_function
  3. from __future__ import absolute_import
  4. from __future__ import division
  5. from __future__ import unicode_literals
  6. import django_housekeeping as hk
  7. from django.conf import settings
  8. from django.utils.timezone import utc, now
  9. from django.db import transaction
  10. from backend.housekeeping import MakeLink
  11. import backend.models as bmodels
  12. from backend import const
  13. from . import models as kmodels
  14. from .git import GitKeyring
  15. from . import git_ops
  16. import os
  17. import os.path
  18. import time
  19. import shutil
  20. import subprocess
  21. import datetime
  22. import pipes
  23. import logging
  24. log = logging.getLogger(__name__)
  25. KEYRINGS_TMPDIR = getattr(settings, "KEYRINGS_TMPDIR", "/srv/keyring.debian.org/data/tmp_keyrings")
  26. class Keyrings(hk.Task):
  27. """
  28. Load keyrings
  29. """
  30. NAME = "keyrings"
  31. KEYID_LEN = 16
  32. def run_main(self, stage):
  33. self.dm = frozenset(kmodels.list_dm())
  34. log.info("%s: Imported %d entries from dm keyring", self.IDENTIFIER, len(self.dm))
  35. self.dd_u = frozenset(kmodels.list_dd_u())
  36. log.info("%s: Imported %d entries from dd_u keyring", self.IDENTIFIER, len(self.dd_u))
  37. self.dd_nu = frozenset(kmodels.list_dd_nu())
  38. log.info("%s: Imported %d entries from dd_nu keyring", self.IDENTIFIER, len(self.dd_nu))
  39. self.emeritus_dd = frozenset(kmodels.list_emeritus_dd())
  40. log.info("%s: Imported %d entries from emeritus_dd keyring", self.IDENTIFIER, len(self.emeritus_dd))
  41. self.removed_dd = frozenset(kmodels.list_removed_dd())
  42. log.info("%s: Imported %d entries from removed_dd keyring", self.IDENTIFIER, len(self.removed_dd))
  43. # Keep an index mapping key IDs to fingerprints and keyring type
  44. self.by_fpr = {}
  45. self.by_keyid = {}
  46. duplicate_fprs = []
  47. duplicate_keyids = []
  48. for t in ("dm", "dd_u", "dd_nu", "emeritus_dd", "removed_dd"):
  49. for fpr in getattr(self, t):
  50. record = (fpr, t)
  51. # Index by fingerprint
  52. old_rec = self.by_fpr.get(fpr, None)
  53. if old_rec is not None:
  54. log.warning("%s: duplicate fingerprint %s, found in %s and in %s", self.IDENTIFIER, fpr, old_rec[1], t)
  55. duplicate_fprs.append(fpr)
  56. else:
  57. self.by_fpr[fpr] = record
  58. # Index by key id
  59. keyid = fpr[-self.KEYID_LEN:]
  60. old_rec = self.by_keyid.get(keyid, None)
  61. if old_rec is not None:
  62. log.warning("%s: duplicate key id %s, found in %s and in %s", self.IDENTIFIER, keyid, old_rec[1], t)
  63. duplicate_keyids.append(keyid)
  64. else:
  65. self.by_keyid[keyid] = record
  66. # Ignore duplicate fingerprints for lookup purposes
  67. for fpr in duplicate_fprs:
  68. del self.by_fpr[fpr]
  69. for keyid in duplicate_keyids:
  70. del self.by_keyid[keyid]
  71. def resolve_fpr(self, fpr):
  72. """
  73. Return the keyring type given a fingerprint, or None if the fingerprint
  74. is unknown
  75. """
  76. rec = self.by_fpr.get(fpr, None)
  77. if rec is None:
  78. return None
  79. return rec[1]
  80. def resolve_keyid(self, keyid):
  81. """
  82. Return the (fingerprint, keyring type) given a key id, or (None, None)
  83. if the key id is unknown
  84. """
  85. if len(keyid) > self.KEYID_LEN:
  86. type = self.resolve_fpr(keyid)
  87. if type is None:
  88. return None, None
  89. else:
  90. return keyid, type
  91. rec = self.by_keyid.get(keyid, None)
  92. if rec is None:
  93. return None, None
  94. return rec
  95. class CheckKeyringConsistency(hk.Task):
  96. """
  97. Show entries that do not match between keyrings and our DB
  98. """
  99. DEPENDS = [Keyrings, MakeLink]
  100. def run_main(self, stage):
  101. # Index Fingerprint objects by fingerprint
  102. fingerprints_by_fpr = {}
  103. for f in bmodels.Fingerprint.objects.select_related("person").all():
  104. if f.fpr.startswith("FIXME"): continue
  105. fingerprints_by_fpr[f.fpr] = f
  106. # Index keyring status by fingerprint
  107. keyring_by_status = {
  108. const.STATUS_DM: self.hk.keyrings.dm,
  109. const.STATUS_DD_U: self.hk.keyrings.dd_u,
  110. const.STATUS_DD_NU: self.hk.keyrings.dd_nu,
  111. const.STATUS_EMERITUS_DD: self.hk.keyrings.emeritus_dd,
  112. const.STATUS_REMOVED_DD: self.hk.keyrings.removed_dd,
  113. }
  114. keyring_by_fpr = {}
  115. for status, keyring in keyring_by_status.items():
  116. for fpr in keyring:
  117. if fpr in keyring_by_fpr:
  118. log.warn("%s: fingerprint %s is both in keyring %s and in keyring %s",
  119. self.IDENTIFIER, fpr, status, keyring_by_fpr[fpr])
  120. else:
  121. keyring_by_fpr[fpr] = status
  122. self.count = 0
  123. # Fingerprints that are not in any keyring
  124. no_keyring = set(fingerprints_by_fpr.keys()) - set(keyring_by_fpr.keys())
  125. for fpr in no_keyring:
  126. f = fingerprints_by_fpr[fpr]
  127. if not f.is_active: continue
  128. if f.person.status in (const.STATUS_REMOVED_DD, const.STATUS_REMOVED_DM, const.STATUS_DC, const.STATUS_DC_GA): continue
  129. log.warn("%s: %s has status %s in the database, but the key %s is not in any keyring",
  130. self.IDENTIFIER, self.hk.link(f.person), const.ALL_STATUS_DESCS[f.person.status], fpr)
  131. self.count += 1
  132. # Fingerprints that are in some keyring
  133. both = set(fingerprints_by_fpr.keys()) & set(keyring_by_fpr.keys())
  134. for fpr in both:
  135. f = fingerprints_by_fpr[fpr]
  136. if not f.is_active: continue
  137. status = keyring_by_fpr[fpr]
  138. # Normalise dm/dm_ga
  139. pstatus = f.person.status
  140. if pstatus == const.STATUS_DM_GA: pstatus = const.STATUS_DM
  141. if pstatus != status:
  142. log.warn("%s: %s has status %s in the database, but the key is in %s keyring",
  143. self.IDENTIFIER, self.hk.link(f.person), const.ALL_STATUS_DESCS[f.person.status], status)
  144. self.count += 1
  145. # Fingerprints that are not in the DB
  146. no_db = set(keyring_by_fpr.keys()) - set(fingerprints_by_fpr.keys())
  147. for fpr in no_db:
  148. status = keyring_by_fpr[fpr]
  149. if status == const.STATUS_REMOVED_DD: continue
  150. log.warn("%s: key %s is in %s keyring, but not in our db", self.IDENTIFIER, fpr, const.ALL_STATUS_DESCS[status])
  151. self.count += 1
  152. def log_stats(self):
  153. log.warn("%s: %d mismatches between keyring and nm.debian.org databases",
  154. self.IDENTIFIER, self.count)
  155. #@transaction.atomic
  156. #def compute_display_names_from_keyring(self, **kw):
  157. # """
  158. # Update Person.display_name with data from keyrings
  159. # """
  160. # # Current display names
  161. # info = dict()
  162. # for p in bmodels.Person.objects.all():
  163. # if not p.fpr: continue
  164. # info[p.fpr] = dict(
  165. # cur=p.fullname,
  166. # pri=None, # Primary uid
  167. # deb=None, # Debian uid
  168. # )
  169. # log.info("%d entries with fingerprints", len(info))
  170. # cur_fpr = None
  171. # cur_info = None
  172. # for keyring in "debian-keyring.gpg", "debian-maintainers.gpg", "debian-nonupload.gpg", "emeritus-keyring.gpg", "removed-keys.gpg":
  173. # count = 0
  174. # for fpr, u in kmodels.uid_info(keyring):
  175. # if fpr != cur_fpr:
  176. # cur_info = info.get(fpr, None)
  177. # cur_fpr = fpr
  178. # if cur_info is not None:
  179. # # Save primary uid
  180. # cur_info["pri"] = u.name
  181. # if cur_info is not None and u.email is not None and u.email.endswith("@debian.org"):
  182. # cur_info["deb"] = u.name
  183. # count += 1
  184. # log.info("%s: %d uids checked...", keyring, count)
  185. # for fpr, i in info.iteritems():
  186. # if not i["pri"] and not i["deb"]: continue
  187. # if i["pri"]:
  188. # cand = i["pri"]
  189. # else:
  190. # cand = i["deb"]
  191. # if i["cur"] != cand:
  192. # log.info("%s: %s %r != %r", keyring, fpr, i["cur"], cand)
  193. class CleanUserKeys(hk.Task):
  194. """
  195. Remove old user keyrings
  196. """
  197. def run_main(self, stage):
  198. threshold = now() - datetime.timedelta(days=15)
  199. for key in kmodels.Key.objects.all():
  200. try:
  201. fpr = bmodels.Fingerprint.objects.get(fpr=key.fpr)
  202. except bmodels.Fingerprint.DoesNotExist:
  203. fpr = None
  204. in_use = fpr is not None and fpr.is_active and (fpr.person.pending or fpr.person.active_processes)
  205. if in_use: continue
  206. if key.key_updated < threshold:
  207. log.info("%s: removing old key %s", self.IDENTIFIER, key.fpr)
  208. key.delete()
  209. class KeyringMaint(hk.Task):
  210. """
  211. Update/regenerate the keyring with the keys of keyring-maint people
  212. """
  213. KEYRING_MAINT_MEMBERS = [
  214. {
  215. "uid": "noodles",
  216. "fpr": "0E3A94C3E83002DAB88CCA1694FA372B2DA8B985",
  217. "email": ["noodles@earth.li"],
  218. },
  219. {
  220. "uid": "gwolf",
  221. "fpr": "AB41C1C68AFD668CA045EBF8673A03E4C1DB921F",
  222. "email": ["gwolf@debian.org", "gwolf@gwolf.org"],
  223. },
  224. {
  225. "uid": "dkg",
  226. "fpr": "0EE5BE979282D80B9F7540F1CCD2ED94D21739E9",
  227. "email": ["dkg@openflows.com", "dkg@fifthhorseman.net"],
  228. },
  229. ]
  230. NAME = "keyring_maint"
  231. def run_main(self, stage):
  232. KEYRING_MAINT_KEYRING = os.path.abspath(getattr(settings, "KEYRING_MAINT_KEYRING", "data/keyring-maint.gpg"))
  233. # Get the Person entries for keyring-maint people, indexed by the email
  234. # that they use in git commits.
  235. self.persons = {}
  236. for entry in self.KEYRING_MAINT_MEMBERS:
  237. for email in entry["email"]:
  238. self.persons[email] = bmodels.Person.objects.get(uid=entry["uid"])
  239. # Regenerate the keyring in a new directory
  240. tmpdir = KEYRING_MAINT_KEYRING + ".tmp"
  241. if os.path.exists(tmpdir): shutil.rmtree(tmpdir)
  242. os.mkdir(tmpdir)
  243. cmd = ["/usr/bin/gpg", "--homedir", tmpdir, "--keyserver", kmodels.KEYSERVER, "-q", "--no-default-keyring", "--no-auto-check-trustdb", "--no-permission-warning", "--recv"]
  244. for entry in self.KEYRING_MAINT_MEMBERS:
  245. cmd.append(entry["fpr"])
  246. proc = subprocess.Popen(cmd, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
  247. stdout, stderr = proc.communicate()
  248. res = proc.wait()
  249. if res != 0:
  250. raise RuntimeError("{} returned error code {}. Stderr: {}", " ".join(pipes.quote(x) for x in cmd), res, stderr);
  251. # Remove the old directory
  252. if os.path.exists(KEYRING_MAINT_KEYRING):
  253. shutil.rmtree(KEYRING_MAINT_KEYRING)
  254. # Move the new directory to the destination place
  255. os.rename(tmpdir, KEYRING_MAINT_KEYRING)
  256. class KeyringGit(hk.Task):
  257. """
  258. Update the local keyring repository
  259. """
  260. NAME = "keyring_git"
  261. DEPENDS = [KeyringMaint]
  262. def run_main(self, stage):
  263. self.keyring = GitKeyring()
  264. self.keyring.git.fetch()
  265. class CheckKeyringLogs(hk.Task):
  266. """
  267. Import changes from the signed parts of the keyring git log
  268. """
  269. DEPENDS = [MakeLink, KeyringMaint, KeyringGit]
  270. def run_main(self, stage):
  271. """
  272. Parse changes from changelog entries after the given date (non inclusive).
  273. """
  274. gk = self.hk.keyring_git.keyring
  275. actions = list(gk.read_log("keyring_maint_import..remotes/origin/master"))
  276. for entry in actions[::-1]:
  277. if entry.parsed is None: continue
  278. try:
  279. op = git_ops.Operation.from_log_entry(entry)
  280. except git_ops.ParseError as e:
  281. log.warn("%s: commit %s: parse error: %s", self.IDENTIFIER, entry.shasum, e)
  282. break
  283. if op is None: continue
  284. try:
  285. ops = list(op.ops())
  286. except git_ops.OperationError as e:
  287. log.warn("%s: commit %s: error computing changes to apply: %s", self.IDENTIFIER, entry.shasum, e)
  288. break
  289. for op in ops:
  290. with transaction.atomic():
  291. op.execute()
  292. # Update our bookmark
  293. gk.git.update_ref("refs/heads/keyring_maint_import", entry.shasum)
  294. log.info("%s: Updating ref keyring_maint_import to commit %s", self.IDENTIFIER, entry.shasum)