queue_rss.py 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242
  1. #!/usr/bin/python
  2. # Generate two rss feeds for a directory with .changes file
  3. # License: GPL v2 or later
  4. # Author: Filippo Giunchedi <filippo@debian.org>
  5. # Version: 0.5
  6. from __future__ import print_function
  7. import cgi
  8. import os
  9. import os.path
  10. import cPickle
  11. import re
  12. import sys
  13. import time
  14. from optparse import OptionParser
  15. from datetime import datetime
  16. from email.utils import parseaddr
  17. import PyRSS2Gen
  18. from debian.deb822 import Changes
  19. inrss_filename = "NEW_in.rss"
  20. outrss_filename = "NEW_out.rss"
  21. db_filename = "status.db"
  22. parser = OptionParser()
  23. parser.set_defaults(queuedir="queue", outdir="out", datadir="status",
  24. logdir="log", max_entries="30")
  25. parser.add_option("-q", "--queuedir", dest="queuedir",
  26. help="The queue dir (%default)")
  27. parser.add_option("-o", "--outdir", dest="outdir",
  28. help="The output directory (%default)")
  29. parser.add_option("-d", "--datadir", dest="datadir",
  30. help="The data dir (%default)")
  31. parser.add_option("-l", "--logdir", dest="logdir",
  32. help="The ACCEPT/REJECT dak log dir (%default)")
  33. parser.add_option("-m", "--max-entries", dest="max_entries", type="int",
  34. help="Max number of entries to keep (%default)")
  35. class Status:
  36. def __init__(self):
  37. self.feed_in = PyRSS2Gen.RSS2(
  38. title="Packages entering NEW",
  39. link="https://ftp-master.debian.org/new.html",
  40. description="Debian packages entering the NEW queue")
  41. self.feed_out = PyRSS2Gen.RSS2(
  42. title="Packages leaving NEW",
  43. link="https://ftp-master.debian.org/new.html",
  44. description="Debian packages leaving the NEW queue")
  45. self.queue = {}
  46. def purge_old_items(feed, max):
  47. """ Purge RSSItem from feed, no more than max. """
  48. if feed.items is None or len(feed.items) == 0:
  49. return False
  50. feed.items = feed.items[:max]
  51. return True
  52. def parse_changes(fname):
  53. """ Parse a .changes file named fname.
  54. Return {fname: parsed} """
  55. m = Changes(open(fname))
  56. wanted_fields = set(['Source', 'Version', 'Architecture', 'Distribution',
  57. 'Date', 'Changed-By', 'Description', 'Changes'])
  58. if not set(m.keys()).issuperset(wanted_fields):
  59. return None
  60. return {os.path.basename(fname): m}
  61. def parse_queuedir(dir):
  62. """ Parse dir for .changes files.
  63. Return a dictionary {filename: parsed_file}"""
  64. if not os.path.exists(dir):
  65. return None
  66. res = {}
  67. for fname in os.listdir(dir):
  68. if not fname.endswith(".changes"):
  69. continue
  70. parsed = parse_changes(os.path.join(dir, fname))
  71. if parsed:
  72. res.update(parsed)
  73. return res
  74. def parse_leave_reason(fname):
  75. """ Parse a dak log file fname for ACCEPT/REJECT reason from process-new.
  76. Return a dictionary {filename: reason}"""
  77. reason_re = re.compile(r".+\|process-new\|(.+)\|NEW (ACCEPT|REJECT)\|(\S+)")
  78. try:
  79. f = open(fname)
  80. except IOError as e:
  81. print("Can't open %s: %s" % (fname, e), file=sys.stderr)
  82. return {}
  83. res = {}
  84. for l in f.readlines():
  85. m = reason_re.search(l)
  86. if m:
  87. res[m.group(3)] = (m.group(2), m.group(1))
  88. f.close()
  89. return res
  90. def add_rss_item(status, msg, direction):
  91. if direction == "in":
  92. feed = status.feed_in
  93. title = "%s %s entered NEW" % (msg['Source'], msg['Version'])
  94. pubdate = msg['Date']
  95. elif direction == "out":
  96. feed = status.feed_out
  97. if 'Leave-Reason' in msg:
  98. title = "%s %s left NEW (%s)" % (msg['Source'], msg['Version'],
  99. msg['Leave-Reason'][0])
  100. else:
  101. title = "%s %s left NEW" % (msg['Source'], msg['Version'])
  102. pubdate = datetime.utcnow()
  103. else:
  104. return False
  105. description = "<pre>Description: %s\nChanges: %s\n</pre>" % \
  106. (cgi.escape(msg['Description']),
  107. cgi.escape(msg['Changes']))
  108. link = "https://ftp-master.debian.org/new/%s_%s.html" % \
  109. (msg['Source'], msg['Version'])
  110. guid = msg['Checksums-Sha256'][0]['sha256']
  111. if 'Processed-By' in msg:
  112. author = msg['Processed-By']
  113. else:
  114. changedby = parseaddr(msg['Changed-By'])
  115. author = "%s (%s)" % (changedby[1], changedby[0])
  116. feed.items.insert(0,
  117. PyRSS2Gen.RSSItem(
  118. title,
  119. pubDate=pubdate,
  120. description=description,
  121. author=cgi.escape(author),
  122. link=link,
  123. guid=guid
  124. )
  125. )
  126. def update_feeds(curqueue, status, settings):
  127. # inrss -> append all items in curqueue not in status.queue
  128. # outrss -> append all items in status.queue not in curqueue
  129. leave_reason = None
  130. # logfile from dak's process-new
  131. reason_log = os.path.join(settings.logdir, time.strftime("%Y-%m"))
  132. for (name, parsed) in curqueue.items():
  133. if name not in status.queue:
  134. # new package
  135. add_rss_item(status, parsed, "in")
  136. for (name, parsed) in status.queue.items():
  137. if name not in curqueue:
  138. # removed package, try to find out why
  139. if leave_reason is None:
  140. leave_reason = parse_leave_reason(reason_log)
  141. if leave_reason and name in leave_reason:
  142. parsed['Leave-Reason'] = leave_reason[name][0]
  143. parsed['Processed-By'] = leave_reason[name][1] + "@debian.org"
  144. add_rss_item(status, parsed, "out")
  145. if __name__ == "__main__":
  146. (settings, args) = parser.parse_args()
  147. if not os.path.exists(settings.outdir):
  148. print("Outdir '%s' does not exists" % settings.outdir, file=sys.stderr)
  149. parser.print_help()
  150. sys.exit(1)
  151. if not os.path.exists(settings.datadir):
  152. print("Datadir '%s' does not exists" % settings.datadir, file=sys.stderr)
  153. parser.print_help()
  154. sys.exit(1)
  155. status_db = os.path.join(settings.datadir, db_filename)
  156. try:
  157. status = cPickle.load(open(status_db))
  158. except IOError:
  159. status = Status()
  160. current_queue = parse_queuedir(settings.queuedir)
  161. update_feeds(current_queue, status, settings)
  162. purge_old_items(status.feed_in, settings.max_entries)
  163. purge_old_items(status.feed_out, settings.max_entries)
  164. feed_in_file = os.path.join(settings.outdir, inrss_filename)
  165. feed_out_file = os.path.join(settings.outdir, outrss_filename)
  166. try:
  167. status.feed_in.write_xml(open(feed_in_file, "w+"), "utf-8")
  168. status.feed_out.write_xml(open(feed_out_file, "w+"), "utf-8")
  169. except IOError as why:
  170. print("Unable to write feeds:", why, file=sys.stderr)
  171. sys.exit(1)
  172. status.queue = current_queue
  173. try:
  174. cPickle.dump(status, open(status_db, "w+"))
  175. except IOError as why:
  176. print("Unable to save status:", why, file=sys.stderr)
  177. sys.exit(1)
  178. # vim:et:ts=4