]> git.decadent.org.uk Git - dak.git/blob - dak/process_unchecked.py
Merge commit 'mhy/master' into merge
[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     # changes["distribution"] may not exist in corner cases
127     # (e.g. unreadable changes files)
128     if not u.pkg.changes.has_key("distribution") or not isinstance(u.pkg.changes["distribution"], DictType):
129         u.pkg.changes["distribution"] = {}
130
131     (summary, short_summary) = u.build_summaries()
132
133     # q-unapproved hax0ring
134     queue_info = {
135          "New": { "is": is_new, "process": acknowledge_new },
136          "Autobyhand" : { "is" : is_autobyhand, "process": do_autobyhand },
137          "Byhand" : { "is": is_byhand, "process": do_byhand },
138          "OldStableUpdate" : { "is": is_oldstableupdate,
139                                "process": do_oldstableupdate },
140          "StableUpdate" : { "is": is_stableupdate, "process": do_stableupdate },
141          "Unembargo" : { "is": is_unembargo, "process": queue_unembargo },
142          "Embargo" : { "is": is_embargo, "process": queue_embargo },
143     }
144
145     queues = [ "New", "Autobyhand", "Byhand" ]
146     if cnf.FindB("Dinstall::SecurityQueueHandling"):
147         queues += [ "Unembargo", "Embargo" ]
148     else:
149         queues += [ "OldStableUpdate", "StableUpdate" ]
150
151     (prompt, answer) = ("", "XXX")
152     if Options["No-Action"] or Options["Automatic"]:
153         answer = 'S'
154
155     queuekey = ''
156
157     pi = u.package_info()
158
159     if len(u.rejects) > 0:
160         if u.upload_too_new():
161             print "SKIP (too new)\n" + pi,
162             prompt = "[S]kip, Quit ?"
163         else:
164             print "REJECT\n" + pi
165             prompt = "[R]eject, Skip, Quit ?"
166             if Options["Automatic"]:
167                 answer = 'R'
168     else:
169         qu = None
170         for q in queues:
171             if queue_info[q]["is"](u):
172                 qu = q
173                 break
174         if qu:
175             print "%s for %s\n%s%s" % ( qu.upper(), ", ".join(u.pkg.changes["distribution"].keys()), pi, summary)
176             queuekey = qu[0].upper()
177             if queuekey in "RQSA":
178                 queuekey = "D"
179                 prompt = "[D]ivert, Skip, Quit ?"
180             else:
181                 prompt = "[%s]%s, Skip, Quit ?" % (queuekey, qu[1:].lower())
182             if Options["Automatic"]:
183                 answer = queuekey
184         else:
185             print "ACCEPT\n" + pi + summary,
186             prompt = "[A]ccept, Skip, Quit ?"
187             if Options["Automatic"]:
188                 answer = 'A'
189
190     while prompt.find(answer) == -1:
191         answer = utils.our_raw_input(prompt)
192         m = re_default_answer.match(prompt)
193         if answer == "":
194             answer = m.group(1)
195         answer = answer[:1].upper()
196
197     if answer == 'R':
198         os.chdir(u.pkg.directory)
199         u.do_reject(0, pi)
200     elif answer == 'A':
201         u.accept(summary, short_summary)
202         u.check_override()
203         u.remove()
204     elif answer == queuekey:
205         queue_info[qu]["process"](u, summary, short_summary)
206         u.remove()
207     elif answer == 'Q':
208         sys.exit(0)
209
210 ################################################################################
211
212 def package_to_suite(u, suite):
213     if not u.pkg.changes["distribution"].has_key(suite):
214         return False
215
216     ret = True
217
218     if not u.pkg.changes["architecture"].has_key("source"):
219         s = DBConn().session()
220         q = s.query(SrcAssociation.sa_id)
221         q = q.join(Suite).filter_by(suite_name=suite)
222         q = q.join(DBSource).filter_by(source=u.pkg.changes['source'])
223         q = q.filter_by(version=u.pkg.changes['version']).limit(1)
224
225         if q.count() < 1:
226             ret = False
227
228         s.close()
229
230     return ret
231
232 def package_to_queue(u, summary, short_summary, queue, perms=0660, build=True, announce=None):
233     cnf = Config()
234     dir = cnf["Dir::Queue::%s" % queue]
235
236     print "Moving to %s holding area" % queue.upper()
237     Logger.log(["Moving to %s" % queue, u.pkg.changes_file])
238
239     u.pkg.write_dot_dak(dir)
240     u.move_to_dir(dir, perms=perms)
241     if build:
242         get_queue(queue.lower()).autobuild_upload(u.pkg, dir)
243
244     # Check for override disparities
245     u.check_override()
246
247     # Send accept mail, announce to lists and close bugs
248     if announce and not cnf["Dinstall::Options::No-Mail"]:
249         template = os.path.join(cnf["Dir::Templates"], announce)
250         u.update_subst()
251         u.Subst["__SUITE__"] = ""
252         mail_message = utils.TemplateSubst(u.Subst, template)
253         utils.send_mail(mail_message)
254         u.announce(short_summary, True)
255
256 ################################################################################
257
258 def is_unembargo(u):
259     session = DBConn().session()
260     cnf = Config()
261
262     q = session.execute("SELECT package FROM disembargo WHERE package = :source AND version = :version", u.pkg.changes)
263     if q.rowcount > 0:
264         session.close()
265         return True
266
267     oldcwd = os.getcwd()
268     os.chdir(cnf["Dir::Queue::Disembargo"])
269     disdir = os.getcwd()
270     os.chdir(oldcwd)
271
272     ret = False
273
274     if u.pkg.directory == disdir:
275         if u.pkg.changes["architecture"].has_key("source"):
276             if not Options["No-Action"]:
277                 session.execute("INSERT INTO disembargo (package, version) VALUES (:package, :version)", u.pkg.changes)
278                 session.commit()
279
280             ret = True
281
282     session.close()
283
284     return ret
285
286 def queue_unembargo(u, summary, short_summary):
287     return package_to_queue(u, summary, short_summary, "Unembargoed",
288                             perms=0660, build=True, announce='process-unchecked.accepted')
289
290 ################################################################################
291
292 def is_embargo(u):
293     # if embargoed queues are enabled always embargo
294     return True
295
296 def queue_embargo(u, summary, short_summary):
297     return package_to_queue(u, summary, short_summary, "Unembargoed",
298                             perms=0660, build=True, announce='process-unchecked.accepted')
299
300 ################################################################################
301
302 def is_stableupdate(u):
303     return package_to_suite(u, 'proposed-updates')
304
305 def do_stableupdate(u, summary, short_summary):
306     return package_to_queue(u, summary, short_summary, "ProposedUpdates",
307                             perms=0664, build=False, announce=None)
308
309 ################################################################################
310
311 def is_oldstableupdate(u):
312     return package_to_suite(u, 'oldstable-proposed-updates')
313
314 def do_oldstableupdate(u, summary, short_summary):
315     return package_to_queue(u, summary, short_summary, "OldProposedUpdates",
316                             perms=0664, build=False, announce=None)
317
318 ################################################################################
319
320 def is_autobyhand(u):
321     cnf = Config()
322
323     all_auto = 1
324     any_auto = 0
325     for f in u.pkg.files.keys():
326         if u.pkg.files[f].has_key("byhand"):
327             any_auto = 1
328
329             # filename is of form "PKG_VER_ARCH.EXT" where PKG, VER and ARCH
330             # don't contain underscores, and ARCH doesn't contain dots.
331             # further VER matches the .changes Version:, and ARCH should be in
332             # the .changes Architecture: list.
333             if f.count("_") < 2:
334                 all_auto = 0
335                 continue
336
337             (pckg, ver, archext) = f.split("_", 2)
338             if archext.count(".") < 1 or u.pkg.changes["version"] != ver:
339                 all_auto = 0
340                 continue
341
342             ABH = cnf.SubTree("AutomaticByHandPackages")
343             if not ABH.has_key(pckg) or \
344               ABH["%s::Source" % (pckg)] != u.pkg.changes["source"]:
345                 print "not match %s %s" % (pckg, u.pkg.changes["source"])
346                 all_auto = 0
347                 continue
348
349             (arch, ext) = archext.split(".", 1)
350             if arch not in u.pkg.changes["architecture"]:
351                 all_auto = 0
352                 continue
353
354             u.pkg.files[f]["byhand-arch"] = arch
355             u.pkg.files[f]["byhand-script"] = ABH["%s::Script" % (pckg)]
356
357     return any_auto and all_auto
358
359 def do_autobyhand(u, summary, short_summary):
360     print "Attempting AUTOBYHAND."
361     byhandleft = True
362     for f, entry in u.pkg.files.items():
363         byhandfile = f
364
365         if not entry.has_key("byhand"):
366             continue
367
368         if not entry.has_key("byhand-script"):
369             byhandleft = True
370             continue
371
372         os.system("ls -l %s" % byhandfile)
373
374         result = os.system("%s %s %s %s %s" % (
375                 entry["byhand-script"],
376                 byhandfile,
377                 u.pkg.changes["version"],
378                 entry["byhand-arch"],
379                 os.path.abspath(u.pkg.changes_file)))
380
381         if result == 0:
382             os.unlink(byhandfile)
383             del entry
384         else:
385             print "Error processing %s, left as byhand." % (f)
386             byhandleft = True
387
388     if byhandleft:
389         do_byhand(u, summary, short_summary)
390     else:
391         u.accept(summary, short_summary)
392         u.check_override()
393         # XXX: We seem to be missing a u.remove() here
394         #      This might explain why we get byhand leftovers in unchecked - mhy
395
396 ################################################################################
397
398 def is_byhand(u):
399     for f in u.pkg.files.keys():
400         if u.pkg.files[f].has_key("byhand"):
401             return True
402     return False
403
404 def do_byhand(u, summary, short_summary):
405     return package_to_queue(u, summary, short_summary, "Byhand",
406                             perms=0660, build=False, announce=None)
407
408 ################################################################################
409
410 def is_new(u):
411     for f in u.pkg.files.keys():
412         if u.pkg.files[f].has_key("new"):
413             return True
414     return False
415
416 def acknowledge_new(u, summary, short_summary):
417     cnf = Config()
418
419     print "Moving to NEW holding area."
420     Logger.log(["Moving to new", u.pkg.changes_file])
421
422     u.pkg.write_dot_dak(cnf["Dir::Queue::New"])
423     u.move_to_dir(cnf["Dir::Queue::New"], perms=0640, changesperms=0644)
424
425     if not Options["No-Mail"]:
426         print "Sending new ack."
427         template = os.path.join(cnf["Dir::Templates"], 'process-unchecked.new')
428         u.Subst["__SUMMARY__"] = summary
429         new_ack_message = utils.TemplateSubst(u.Subst, template)
430         utils.send_mail(new_ack_message)
431
432 ################################################################################
433
434 # reprocess is necessary for the case of foo_1.2-1 and foo_1.2-2 in
435 # Incoming. -1 will reference the .orig.tar.gz, but -2 will not.
436 # Upload.check_dsc_against_db() can find the .orig.tar.gz but it will
437 # not have processed it during it's checks of -2.  If -1 has been
438 # deleted or otherwise not checked by 'dak process-unchecked', the
439 # .orig.tar.gz will not have been checked at all.  To get round this,
440 # we force the .orig.tar.gz into the .changes structure and reprocess
441 # the .changes file.
442
443 def process_it(changes_file):
444     global Logger
445
446     cnf = Config()
447
448     holding = Holding()
449
450     u = Upload()
451     u.pkg.changes_file = changes_file
452     u.pkg.directory = os.getcwd()
453     u.logger = Logger
454     origchanges = os.path.join(u.pkg.directory, u.pkg.changes_file)
455
456     # Some defaults in case we can't fully process the .changes file
457     u.pkg.changes["maintainer2047"] = cnf["Dinstall::MyEmailAddress"]
458     u.pkg.changes["changedby2047"] = cnf["Dinstall::MyEmailAddress"]
459
460     # debian-{devel-,}-changes@lists.debian.org toggles writes access based on this header
461     bcc = "X-DAK: dak process-unchecked\nX-Katie: $Revision: 1.65 $"
462     if cnf.has_key("Dinstall::Bcc"):
463         u.Subst["__BCC__"] = bcc + "\nBcc: %s" % (cnf["Dinstall::Bcc"])
464     else:
465         u.Subst["__BCC__"] = bcc
466
467     # Remember where we are so we can come back after cd-ing into the
468     # holding directory.  TODO: Fix this stupid hack
469     u.prevdir = os.getcwd()
470
471     # TODO: Figure out something better for this (or whether it's even
472     #       necessary - it seems to have been for use when we were
473     #       still doing the is_unchecked check; reprocess = 2)
474     u.reprocess = 1
475
476     try:
477         # If this is the Real Thing(tm), copy things into a private
478         # holding directory first to avoid replacable file races.
479         if not Options["No-Action"]:
480             os.chdir(cnf["Dir::Queue::Holding"])
481
482             # Absolutize the filename to avoid the requirement of being in the
483             # same directory as the .changes file.
484             holding.copy_to_holding(origchanges)
485
486             # Relativize the filename so we use the copy in holding
487             # rather than the original...
488             changespath = os.path.basename(u.pkg.changes_file)
489
490         (u.pkg.changes["fingerprint"], rejects) = utils.check_signature(changespath)
491
492         if u.pkg.changes["fingerprint"]:
493             valid_changes_p = u.load_changes(changespath)
494         else:
495             valid_changes_p = False
496             u.rejects.extend(rejects)
497
498         if valid_changes_p:
499             while u.reprocess:
500                 u.check_distributions()
501                 u.check_files(not Options["No-Action"])
502                 valid_dsc_p = u.check_dsc(not Options["No-Action"])
503                 if valid_dsc_p:
504                     u.check_source()
505                 u.check_hashes()
506                 u.check_urgency()
507                 u.check_timestamps()
508                 u.check_signed_by_key()
509
510         action(u)
511
512     except SystemExit:
513         raise
514
515     except:
516         print "ERROR"
517         traceback.print_exc(file=sys.stderr)
518
519     # Restore previous WD
520     os.chdir(u.prevdir)
521
522 ###############################################################################
523
524 def main():
525     global Options, Logger
526
527     cnf = Config()
528     changes_files = init()
529
530     # -n/--dry-run invalidates some other options which would involve things happening
531     if Options["No-Action"]:
532         Options["Automatic"] = ""
533
534     # Initialize our Holding singleton
535     holding = Holding()
536
537     # Ensure all the arguments we were given are .changes files
538     for f in changes_files:
539         if not f.endswith(".changes"):
540             utils.warn("Ignoring '%s' because it's not a .changes file." % (f))
541             changes_files.remove(f)
542
543     if changes_files == []:
544         if cnf["Dinstall::Options::Directory"] == "":
545             utils.fubar("Need at least one .changes file as an argument.")
546         else:
547             sys.exit(0)
548
549     # Check that we aren't going to clash with the daily cron job
550     if not Options["No-Action"] and os.path.exists("%s/daily.lock" % (cnf["Dir::Lock"])) and not Options["No-Lock"]:
551         utils.fubar("Archive maintenance in progress.  Try again later.")
552
553     # Obtain lock if not in no-action mode and initialize the log
554     if not Options["No-Action"]:
555         lock_fd = os.open(cnf["Dinstall::LockFile"], os.O_RDWR | os.O_CREAT)
556         try:
557             fcntl.lockf(lock_fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
558         except IOError, e:
559             if errno.errorcode[e.errno] == 'EACCES' or errno.errorcode[e.errno] == 'EAGAIN':
560                 utils.fubar("Couldn't obtain lock; assuming another 'dak process-unchecked' is already running.")
561             else:
562                 raise
563         Logger = daklog.Logger(cnf, "process-unchecked")
564
565     # Sort the .changes files so that we process sourceful ones first
566     changes_files.sort(utils.changes_compare)
567
568     # Process the changes files
569     for changes_file in changes_files:
570         print "\n" + changes_file
571         try:
572             process_it (changes_file)
573         finally:
574             if not Options["No-Action"]:
575                 holding.clean()
576
577     accept_count = SummaryStats().accept_count
578     accept_bytes = SummaryStats().accept_bytes
579
580     if accept_count:
581         sets = "set"
582         if accept_count > 1:
583             sets = "sets"
584         print "Accepted %d package %s, %s." % (accept_count, sets, utils.size_type(int(accept_bytes)))
585         Logger.log(["total",accept_count,accept_bytes])
586
587     if not Options["No-Action"]:
588         Logger.close()
589
590 ################################################################################
591
592 if __name__ == '__main__':
593     main()