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