]> git.decadent.org.uk Git - dak.git/blob - dak/process_unchecked.py
make sure we close sessions
[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     ret = True
218
219     if not u.pkg.changes["architecture"].has_key("source"):
220         s = DBConn().session()
221         q = s.query(SrcAssociation.sa_id)
222         q = q.join(Suite).filter_by(suite_name=suite)
223         q = q.join(DBSource).filter_by(source=u.pkg.changes['source'])
224         q = q.filter_by(version=u.pkg.changes['version']).limit(1)
225
226         if q.count() < 1:
227             ret = False
228
229         s.close()
230
231     return ret
232
233 def package_to_queue(u, summary, short_summary, queue, perms=0660, build=True, announce=None):
234     cnf = Config()
235     dir = cnf["Dir::Queue::%s" % queue]
236
237     print "Moving to %s holding area" % queue.upper()
238     Logger.log(["Moving to %s" % queue, u.pkg.changes_file])
239
240     u.pkg.write_dot_dak(dir)
241     u.move_to_dir(dir, perms=perms)
242     if build:
243         get_queue(queue.lower()).autobuild_upload(u.pkg, dir)
244
245     # Check for override disparities
246     u.check_override()
247
248     # Send accept mail, announce to lists and close bugs
249     if announce and not cnf["Dinstall::Options::No-Mail"]:
250         template = os.path.join(cnf["Dir::Templates"], announce)
251         u.update_subst()
252         u.Subst["__SUITE__"] = ""
253         mail_message = utils.TemplateSubst(u.Subst, template)
254         utils.send_mail(mail_message)
255         u.announce(short_summary, True)
256
257 ################################################################################
258
259 def is_unembargo(u):
260     session = DBConn().session()
261     cnf = Config()
262
263     q = session.execute("SELECT package FROM disembargo WHERE package = :source AND version = :version", u.pkg.changes)
264     if q.rowcount > 0:
265         session.close()
266         return True
267
268     oldcwd = os.getcwd()
269     os.chdir(cnf["Dir::Queue::Disembargo"])
270     disdir = os.getcwd()
271     os.chdir(oldcwd)
272
273     ret = False
274
275     if u.pkg.directory == disdir:
276         if u.pkg.changes["architecture"].has_key("source"):
277             if not Options["No-Action"]:
278                 session.execute("INSERT INTO disembargo (package, version) VALUES (:package, :version)", u.pkg.changes)
279                 session.commit()
280
281             ret = True
282
283     session.close()
284
285     return ret
286
287 def queue_unembargo(u, summary, short_summary):
288     return package_to_queue(u, summary, short_summary, "Unembargoed",
289                             perms=0660, build=True, announce='process-unchecked.accepted')
290
291 ################################################################################
292
293 def is_embargo(u):
294     # if embargoed queues are enabled always embargo
295     return True
296
297 def queue_embargo(u, summary, short_summary):
298     return package_to_queue(u, summary, short_summary, "Unembargoed",
299                             perms=0660, build=True, announce='process-unchecked.accepted')
300
301 ################################################################################
302
303 def is_stableupdate(u):
304     return package_to_suite(u, 'proposed-updates')
305
306 def do_stableupdate(u, summary, short_summary):
307     return package_to_queue(u, summary, short_summary, "ProposedUpdates",
308                             perms=0664, build=False, announce=None)
309
310 ################################################################################
311
312 def is_oldstableupdate(u):
313     return package_to_suite(u, 'oldstable-proposed-updates')
314
315 def do_oldstableupdate(u, summary, short_summary):
316     return package_to_queue(u, summary, short_summary, "OldProposedUpdates",
317                             perms=0664, build=False, announce=None)
318
319 ################################################################################
320
321 def is_autobyhand(u):
322     cnf = Config()
323
324     all_auto = 1
325     any_auto = 0
326     for f in u.pkg.files.keys():
327         if u.pkg.files[f].has_key("byhand"):
328             any_auto = 1
329
330             # filename is of form "PKG_VER_ARCH.EXT" where PKG, VER and ARCH
331             # don't contain underscores, and ARCH doesn't contain dots.
332             # further VER matches the .changes Version:, and ARCH should be in
333             # the .changes Architecture: list.
334             if f.count("_") < 2:
335                 all_auto = 0
336                 continue
337
338             (pckg, ver, archext) = f.split("_", 2)
339             if archext.count(".") < 1 or u.pkg.changes["version"] != ver:
340                 all_auto = 0
341                 continue
342
343             ABH = cnf.SubTree("AutomaticByHandPackages")
344             if not ABH.has_key(pckg) or \
345               ABH["%s::Source" % (pckg)] != u.pkg.changes["source"]:
346                 print "not match %s %s" % (pckg, u.pkg.changes["source"])
347                 all_auto = 0
348                 continue
349
350             (arch, ext) = archext.split(".", 1)
351             if arch not in u.pkg.changes["architecture"]:
352                 all_auto = 0
353                 continue
354
355             u.pkg.files[f]["byhand-arch"] = arch
356             u.pkg.files[f]["byhand-script"] = ABH["%s::Script" % (pckg)]
357
358     return any_auto and all_auto
359
360 def do_autobyhand(u, summary, short_summary):
361     print "Attempting AUTOBYHAND."
362     byhandleft = True
363     for f, entry in u.pkg.files.items():
364         byhandfile = f
365
366         if not entry.has_key("byhand"):
367             continue
368
369         if not entry.has_key("byhand-script"):
370             byhandleft = True
371             continue
372
373         os.system("ls -l %s" % byhandfile)
374
375         result = os.system("%s %s %s %s %s" % (
376                 entry["byhand-script"],
377                 byhandfile,
378                 u.pkg.changes["version"],
379                 entry["byhand-arch"],
380                 os.path.abspath(u.pkg.changes_file)))
381
382         if result == 0:
383             os.unlink(byhandfile)
384             del entry
385         else:
386             print "Error processing %s, left as byhand." % (f)
387             byhandleft = True
388
389     if byhandleft:
390         do_byhand(u, summary, short_summary)
391     else:
392         u.accept(summary, short_summary)
393         u.check_override()
394         # XXX: We seem to be missing a u.remove() here
395         #      This might explain why we get byhand leftovers in unchecked - mhy
396
397 ################################################################################
398
399 def is_byhand(u):
400     for f in u.pkg.files.keys():
401         if u.pkg.files[f].has_key("byhand"):
402             return True
403     return False
404
405 def do_byhand(u, summary, short_summary):
406     return package_to_queue(u, summary, short_summary, "Byhand",
407                             perms=0660, build=False, announce=None)
408
409 ################################################################################
410
411 def is_new(u):
412     for f in u.pkg.files.keys():
413         if u.pkg.files[f].has_key("new"):
414             return True
415     return False
416
417 def acknowledge_new(u, summary, short_summary):
418     cnf = Config()
419
420     print "Moving to NEW holding area."
421     Logger.log(["Moving to new", u.pkg.changes_file])
422
423     u.pkg.write_dot_dak(cnf["Dir::Queue::New"])
424     u.move_to_dir(cnf["Dir::Queue::New"], perms=0640, changesperms=0644)
425
426     if not Options["No-Mail"]:
427         print "Sending new ack."
428         template = os.path.join(cnf["Dir::Templates"], 'process-unchecked.new')
429         u.Subst["__SUMMARY__"] = summary
430         new_ack_message = utils.TemplateSubst(u.Subst, template)
431         utils.send_mail(new_ack_message)
432
433 ################################################################################
434
435 # reprocess is necessary for the case of foo_1.2-1 and foo_1.2-2 in
436 # Incoming. -1 will reference the .orig.tar.gz, but -2 will not.
437 # Upload.check_dsc_against_db() can find the .orig.tar.gz but it will
438 # not have processed it during it's checks of -2.  If -1 has been
439 # deleted or otherwise not checked by 'dak process-unchecked', the
440 # .orig.tar.gz will not have been checked at all.  To get round this,
441 # we force the .orig.tar.gz into the .changes structure and reprocess
442 # the .changes file.
443
444 def process_it(changes_file):
445     global Logger
446
447     cnf = Config()
448
449     holding = Holding()
450
451     u = Upload()
452     u.pkg.changes_file = changes_file
453     u.pkg.directory = os.getcwd()
454     u.logger = Logger
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(os.path.abspath(changes_file))
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()