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