]> git.decadent.org.uk Git - dak.git/blob - dak/process_unchecked.py
miscellaneous fixups from testing
[dak.git] / dak / process_unchecked.py
1 #!/usr/bin/env python
2
3 """
4 Checks Debian packages from Incoming
5 @contact: Debian FTP Master <ftpmaster@debian.org>
6 @copyright: 2000, 2001, 2002, 2003, 2004, 2005, 2006  James Troup <james@nocrew.org>
7 @copyright: 2009  Joerg Jaspert <joerg@debian.org>
8 @copyright: 2009  Mark Hymers <mhy@debian.org>
9 @license: GNU General Public License version 2 or later
10 """
11
12 # This program is free software; you can redistribute it and/or modify
13 # it under the terms of the GNU General Public License as published by
14 # the Free Software Foundation; either version 2 of the License, or
15 # (at your option) any later version.
16
17 # This program is distributed in the hope that it will be useful,
18 # but WITHOUT ANY WARRANTY; without even the implied warranty of
19 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
20 # GNU General Public License for more details.
21
22 # You should have received a copy of the GNU General Public License
23 # along with this program; if not, write to the Free Software
24 # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
25
26 # Originally based on dinstall by Guy Maor <maor@debian.org>
27
28 ################################################################################
29
30 # Computer games don't affect kids. I mean if Pacman affected our generation as
31 # kids, we'd all run around in a darkened room munching pills and listening to
32 # repetitive music.
33 #         -- Unknown
34
35 ################################################################################
36
37 import commands
38 import errno
39 import fcntl
40 import os
41 import re
42 import shutil
43 import stat
44 import sys
45 import time
46 import traceback
47 import tarfile
48 import apt_inst
49 import apt_pkg
50 from debian_bundle import deb822
51
52 from daklib.dbconn import *
53 from daklib.binary import Binary
54 from daklib import daklog
55 from daklib.queue import *
56 from daklib import utils
57 from daklib.textutils import fix_maintainer
58 from daklib.dak_exceptions import *
59 from daklib.regexes import re_default_answer
60 from daklib.summarystats import SummaryStats
61 from daklib.holding import Holding
62 from daklib.config import Config
63
64 from types import *
65
66 ################################################################################
67
68
69 ################################################################################
70
71 # Globals
72 Options = None
73 Logger = None
74
75 ###############################################################################
76
77 def init():
78     global Options
79
80     apt_pkg.init()
81     cnf = Config()
82
83     Arguments = [('a',"automatic","Dinstall::Options::Automatic"),
84                  ('h',"help","Dinstall::Options::Help"),
85                  ('n',"no-action","Dinstall::Options::No-Action"),
86                  ('p',"no-lock", "Dinstall::Options::No-Lock"),
87                  ('s',"no-mail", "Dinstall::Options::No-Mail"),
88                  ('d',"directory", "Dinstall::Options::Directory", "HasArg")]
89
90     for i in ["automatic", "help", "no-action", "no-lock", "no-mail",
91               "override-distribution", "version", "directory"]:
92         cnf["Dinstall::Options::%s" % (i)] = ""
93
94     changes_files = apt_pkg.ParseCommandLine(cnf.Cnf, Arguments, sys.argv)
95     Options = cnf.SubTree("Dinstall::Options")
96
97     if Options["Help"]:
98         usage()
99
100     # If we have a directory flag, use it to find our files
101     if cnf["Dinstall::Options::Directory"] != "":
102         # Note that we clobber the list of files we were given in this case
103         # so warn if the user has done both
104         if len(changes_files) > 0:
105             utils.warn("Directory provided so ignoring files given on command line")
106
107         changes_files = utils.get_changes_files(cnf["Dinstall::Options::Directory"])
108
109     return changes_files
110
111 ################################################################################
112
113 def usage (exit_code=0):
114     print """Usage: dinstall [OPTION]... [CHANGES]...
115   -a, --automatic           automatic run
116   -h, --help                show this help and exit.
117   -n, --no-action           don't do anything
118   -p, --no-lock             don't check lockfile !! for cron.daily only !!
119   -s, --no-mail             don't send any mail
120   -V, --version             display the version number and exit"""
121     sys.exit(exit_code)
122
123 ################################################################################
124
125 def action(u):
126     cnf = Config()
127     # changes["distribution"] may not exist in corner cases
128     # (e.g. unreadable changes files)
129     if not u.pkg.changes.has_key("distribution") or not isinstance(u.pkg.changes["distribution"], DictType):
130         u.pkg.changes["distribution"] = {}
131
132     (summary, short_summary) = u.build_summaries()
133
134     # q-unapproved hax0ring
135     queue_info = {
136          "New": { "is": is_new, "process": acknowledge_new },
137          "Autobyhand" : { "is" : is_autobyhand, "process": do_autobyhand },
138          "Byhand" : { "is": is_byhand, "process": do_byhand },
139          "OldStableUpdate" : { "is": is_oldstableupdate,
140                                "process": do_oldstableupdate },
141          "StableUpdate" : { "is": is_stableupdate, "process": do_stableupdate },
142          "Unembargo" : { "is": is_unembargo, "process": queue_unembargo },
143          "Embargo" : { "is": is_embargo, "process": queue_embargo },
144     }
145
146     queues = [ "New", "Autobyhand", "Byhand" ]
147     if cnf.FindB("Dinstall::SecurityQueueHandling"):
148         queues += [ "Unembargo", "Embargo" ]
149     else:
150         queues += [ "OldStableUpdate", "StableUpdate" ]
151
152     (prompt, answer) = ("", "XXX")
153     if Options["No-Action"] or Options["Automatic"]:
154         answer = 'S'
155
156     queuekey = ''
157
158     pi = u.package_info()
159
160     if len(u.rejects) > 0:
161         if u.upload_too_new():
162             print "SKIP (too new)\n" + pi,
163             prompt = "[S]kip, Quit ?"
164         else:
165             print "REJECT\n" + pi
166             prompt = "[R]eject, Skip, Quit ?"
167             if Options["Automatic"]:
168                 answer = 'R'
169     else:
170         qu = None
171         for q in queues:
172             if queue_info[q]["is"](u):
173                 qu = q
174                 break
175         if qu:
176             print "%s for %s\n%s%s" % ( qu.upper(), ", ".join(u.pkg.changes["distribution"].keys()), pi, summary)
177             queuekey = qu[0].upper()
178             if queuekey in "RQSA":
179                 queuekey = "D"
180                 prompt = "[D]ivert, Skip, Quit ?"
181             else:
182                 prompt = "[%s]%s, Skip, Quit ?" % (queuekey, qu[1:].lower())
183             if Options["Automatic"]:
184                 answer = queuekey
185         else:
186             print "ACCEPT\n" + pi + summary,
187             prompt = "[A]ccept, Skip, Quit ?"
188             if Options["Automatic"]:
189                 answer = 'A'
190
191     while prompt.find(answer) == -1:
192         answer = utils.our_raw_input(prompt)
193         m = re_default_answer.match(prompt)
194         if answer == "":
195             answer = m.group(1)
196         answer = answer[:1].upper()
197
198     if answer == 'R':
199         os.chdir(u.pkg.directory)
200         u.do_reject(0, pi)
201     elif answer == 'A':
202         u.accept(summary, short_summary)
203         u.check_override()
204         u.remove()
205     elif answer == queuekey:
206         queue_info[qu]["process"](u, summary, short_summary)
207         u.remove()
208     elif answer == 'Q':
209         sys.exit(0)
210
211 ################################################################################
212
213 def package_to_suite(u, suite):
214     if not u.pkg.changes["distribution"].has_key(suite):
215         return False
216
217     if not u.pkg.changes["architecture"].has_key("source"):
218         s = DBConn().session()
219         q = s.query(SrcAssociation.sa_id)
220         q = q.join(Suite).filter_by(suite_name=suite)
221         q = q.join(DBSource).filter_by(source=u.pkg.changes['source'])
222         q = q.filter_by(version=u.pkg.changes['version']).limit(1)
223
224         if q.count() < 1:
225             return False
226
227     return True
228
229 def package_to_queue(u, summary, short_summary, queue, perms=0660, build=True, announce=None):
230     cnf = Config()
231     dir = cnf["Dir::Queue::%s" % queue]
232
233     print "Moving to %s holding area" % queue.upper()
234     Logger.log(["Moving to %s" % queue, u.pkg.changes_file])
235
236     u.pkg.write_dot_dak(dir)
237     u.move_to_dir(dir, perms=perms)
238     if build:
239         get_queue(queue.lower()).autobuild_upload(u.pkg, dir)
240
241     # Check for override disparities
242     u.check_override()
243
244     # Send accept mail, announce to lists and close bugs
245     if announce and not cnf["Dinstall::Options::No-Mail"]:
246         template = os.path.join(cnf["Dir::Templates"], announce)
247         u.update_subst()
248         u.Subst["__SUITE__"] = ""
249         mail_message = utils.TemplateSubst(u.Subst, template)
250         utils.send_mail(mail_message)
251         u.announce(short_summary, True)
252
253 ################################################################################
254
255 def is_unembargo(u):
256     session = DBConn().session()
257     cnf = Config()
258
259     q = session.execute("SELECT package FROM disembargo WHERE package = :source AND version = :version", u.pkg.changes)
260     if q.rowcount > 0:
261         return True
262
263     oldcwd = os.getcwd()
264     os.chdir(cnf["Dir::Queue::Disembargo"])
265     disdir = os.getcwd()
266     os.chdir(oldcwd)
267
268     if u.pkg.directory == disdir:
269         if u.pkg.changes["architecture"].has_key("source"):
270             if not Options["No-Action"]:
271                 session.execute("INSERT INTO disembargo (package, version) VALUES (:package, :version)", u.pkg.changes)
272                 session.commit()
273
274             return True
275
276     return False
277
278 def queue_unembargo(u, summary, short_summary):
279     return package_to_queue(u, summary, short_summary, "Unembargoed",
280                             perms=0660, build=True, announce='process-unchecked.accepted')
281
282 ################################################################################
283
284 def is_embargo(u):
285     # if embargoed queues are enabled always embargo
286     return True
287
288 def queue_embargo(u, summary, short_summary):
289     return package_to_queue(u, summary, short_summary, "Unembargoed",
290                             perms=0660, build=True, announce='process-unchecked.accepted')
291
292 ################################################################################
293
294 def is_stableupdate(u):
295     return package_to_suite(u, 'proposed-updates')
296
297 def do_stableupdate(u, summary, short_summary):
298     return package_to_queue(u, summary, short_summary, "ProposedUpdates",
299                             perms=0664, build=False, announce=None)
300
301 ################################################################################
302
303 def is_oldstableupdate(u):
304     return package_to_suite(u, 'oldstable-proposed-updates')
305
306 def do_oldstableupdate(u, summary, short_summary):
307     return package_to_queue(u, summary, short_summary, "OldProposedUpdates",
308                             perms=0664, build=False, announce=None)
309
310 ################################################################################
311
312 def is_autobyhand(u):
313     cnf = Config()
314
315     all_auto = 1
316     any_auto = 0
317     for f in u.pkg.files.keys():
318         if u.pkg.files[f].has_key("byhand"):
319             any_auto = 1
320
321             # filename is of form "PKG_VER_ARCH.EXT" where PKG, VER and ARCH
322             # don't contain underscores, and ARCH doesn't contain dots.
323             # further VER matches the .changes Version:, and ARCH should be in
324             # the .changes Architecture: list.
325             if f.count("_") < 2:
326                 all_auto = 0
327                 continue
328
329             (pckg, ver, archext) = f.split("_", 2)
330             if archext.count(".") < 1 or u.pkg.changes["version"] != ver:
331                 all_auto = 0
332                 continue
333
334             ABH = cnf.SubTree("AutomaticByHandPackages")
335             if not ABH.has_key(pckg) or \
336               ABH["%s::Source" % (pckg)] != u.pkg.changes["source"]:
337                 print "not match %s %s" % (pckg, u.pkg.changes["source"])
338                 all_auto = 0
339                 continue
340
341             (arch, ext) = archext.split(".", 1)
342             if arch not in u.pkg.changes["architecture"]:
343                 all_auto = 0
344                 continue
345
346             u.pkg.files[f]["byhand-arch"] = arch
347             u.pkg.files[f]["byhand-script"] = ABH["%s::Script" % (pckg)]
348
349     return any_auto and all_auto
350
351 def do_autobyhand(u, summary, short_summary):
352     print "Attempting AUTOBYHAND."
353     byhandleft = True
354     for f, entry in u.pkg.files.items():
355         byhandfile = f
356
357         if not entry.has_key("byhand"):
358             continue
359
360         if not entry.has_key("byhand-script"):
361             byhandleft = True
362             continue
363
364         os.system("ls -l %s" % byhandfile)
365
366         result = os.system("%s %s %s %s %s" % (
367                 entry["byhand-script"],
368                 byhandfile,
369                 u.pkg.changes["version"],
370                 entry["byhand-arch"],
371                 os.path.abspath(u.pkg.changes_file)))
372
373         if result == 0:
374             os.unlink(byhandfile)
375             del entry
376         else:
377             print "Error processing %s, left as byhand." % (f)
378             byhandleft = True
379
380     if byhandleft:
381         do_byhand(u, summary, short_summary)
382     else:
383         u.accept(summary, short_summary)
384         u.check_override()
385         # XXX: We seem to be missing a u.remove() here
386         #      This might explain why we get byhand leftovers in unchecked - mhy
387
388 ################################################################################
389
390 def is_byhand(u):
391     for f in u.pkg.files.keys():
392         if u.pkg.files[f].has_key("byhand"):
393             return True
394     return False
395
396 def do_byhand(u, summary, short_summary):
397     return package_to_queue(u, summary, short_summary, "Byhand",
398                             perms=0660, build=False, announce=None)
399
400 ################################################################################
401
402 def is_new(u):
403     for f in u.pkg.files.keys():
404         if u.pkg.files[f].has_key("new"):
405             return True
406     return False
407
408 def acknowledge_new(u, summary, short_summary):
409     cnf = Config()
410
411     print "Moving to NEW holding area."
412     Logger.log(["Moving to new", u.pkg.changes_file])
413
414     u.pkg.write_dot_dak(cnf["Dir::Queue::New"])
415     u.move_to_dir(cnf["Dir::Queue::New"], perms=0640, changesperms=0644)
416
417     if not Options["No-Mail"]:
418         print "Sending new ack."
419         template = os.path.join(cnf["Dir::Templates"], 'process-unchecked.new')
420         u.Subst["__SUMMARY__"] = summary
421         new_ack_message = utils.TemplateSubst(u.Subst, template)
422         utils.send_mail(new_ack_message)
423
424 ################################################################################
425
426 # reprocess is necessary for the case of foo_1.2-1 and foo_1.2-2 in
427 # Incoming. -1 will reference the .orig.tar.gz, but -2 will not.
428 # Upload.check_dsc_against_db() can find the .orig.tar.gz but it will
429 # not have processed it during it's checks of -2.  If -1 has been
430 # deleted or otherwise not checked by 'dak process-unchecked', the
431 # .orig.tar.gz will not have been checked at all.  To get round this,
432 # we force the .orig.tar.gz into the .changes structure and reprocess
433 # the .changes file.
434
435 def process_it(changes_file):
436     global Logger
437
438     cnf = Config()
439
440     holding = Holding()
441
442     u = Upload()
443     u.pkg.changes_file = changes_file
444     u.pkg.directory = os.getcwd()
445     u.logger = Logger
446
447     # Some defaults in case we can't fully process the .changes file
448     u.pkg.changes["maintainer2047"] = cnf["Dinstall::MyEmailAddress"]
449     u.pkg.changes["changedby2047"] = cnf["Dinstall::MyEmailAddress"]
450
451     # debian-{devel-,}-changes@lists.debian.org toggles writes access based on this header
452     bcc = "X-DAK: dak process-unchecked\nX-Katie: $Revision: 1.65 $"
453     if cnf.has_key("Dinstall::Bcc"):
454         u.Subst["__BCC__"] = bcc + "\nBcc: %s" % (cnf["Dinstall::Bcc"])
455     else:
456         u.Subst["__BCC__"] = bcc
457
458     # Remember where we are so we can come back after cd-ing into the
459     # holding directory.  TODO: Fix this stupid hack
460     u.prevdir = os.getcwd()
461
462     # TODO: Figure out something better for this (or whether it's even
463     #       necessary - it seems to have been for use when we were
464     #       still doing the is_unchecked check; reprocess = 2)
465     u.reprocess = 1
466
467     try:
468         # If this is the Real Thing(tm), copy things into a private
469         # holding directory first to avoid replacable file races.
470         if not Options["No-Action"]:
471             os.chdir(cnf["Dir::Queue::Holding"])
472
473             # Absolutize the filename to avoid the requirement of being in the
474             # same directory as the .changes file.
475             holding.copy_to_holding(os.path.abspath(changes_file))
476
477             # Relativize the filename so we use the copy in holding
478             # rather than the original...
479             changespath = os.path.basename(u.pkg.changes_file)
480
481         (u.pkg.changes["fingerprint"], rejects) = utils.check_signature(changespath)
482
483         if u.pkg.changes["fingerprint"]:
484             valid_changes_p = u.load_changes(changespath)
485         else:
486             valid_changes_p = False
487             u.rejects.extend(rejects)
488
489         if valid_changes_p:
490             while u.reprocess:
491                 u.check_distributions()
492                 u.check_files(not Options["No-Action"])
493                 valid_dsc_p = u.check_dsc(not Options["No-Action"])
494                 if valid_dsc_p:
495                     u.check_source()
496                 u.check_hashes()
497                 u.check_urgency()
498                 u.check_timestamps()
499                 u.check_signed_by_key()
500
501         action(u)
502
503     except SystemExit:
504         raise
505
506     except:
507         print "ERROR"
508         traceback.print_exc(file=sys.stderr)
509
510     # Restore previous WD
511     os.chdir(u.prevdir)
512
513 ###############################################################################
514
515 def main():
516     global Options, Logger
517
518     cnf = Config()
519     changes_files = init()
520
521     # -n/--dry-run invalidates some other options which would involve things happening
522     if Options["No-Action"]:
523         Options["Automatic"] = ""
524
525     # Initialize our Holding singleton
526     holding = Holding()
527
528     # Ensure all the arguments we were given are .changes files
529     for f in changes_files:
530         if not f.endswith(".changes"):
531             utils.warn("Ignoring '%s' because it's not a .changes file." % (f))
532             changes_files.remove(f)
533
534     if changes_files == []:
535         if cnf["Dinstall::Options::Directory"] == "":
536             utils.fubar("Need at least one .changes file as an argument.")
537         else:
538             sys.exit(0)
539
540     # Check that we aren't going to clash with the daily cron job
541     if not Options["No-Action"] and os.path.exists("%s/daily.lock" % (cnf["Dir::Lock"])) and not Options["No-Lock"]:
542         utils.fubar("Archive maintenance in progress.  Try again later.")
543
544     # Obtain lock if not in no-action mode and initialize the log
545     if not Options["No-Action"]:
546         lock_fd = os.open(cnf["Dinstall::LockFile"], os.O_RDWR | os.O_CREAT)
547         try:
548             fcntl.lockf(lock_fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
549         except IOError, e:
550             if errno.errorcode[e.errno] == 'EACCES' or errno.errorcode[e.errno] == 'EAGAIN':
551                 utils.fubar("Couldn't obtain lock; assuming another 'dak process-unchecked' is already running.")
552             else:
553                 raise
554         Logger = daklog.Logger(cnf, "process-unchecked")
555
556     # Sort the .changes files so that we process sourceful ones first
557     changes_files.sort(utils.changes_compare)
558
559     # Process the changes files
560     for changes_file in changes_files:
561         print "\n" + changes_file
562         try:
563             process_it (changes_file)
564         finally:
565             if not Options["No-Action"]:
566                 holding.clean()
567
568     accept_count = SummaryStats().accept_count
569     accept_bytes = SummaryStats().accept_bytes
570
571     if accept_count:
572         sets = "set"
573         if accept_count > 1:
574             sets = "sets"
575         print "Accepted %d package %s, %s." % (accept_count, sets, utils.size_type(int(accept_bytes)))
576         Logger.log(["total",accept_count,accept_bytes])
577
578     if not Options["No-Action"]:
579         Logger.close()
580
581 ################################################################################
582
583 if __name__ == '__main__':
584     main()