]> git.decadent.org.uk Git - dak.git/blob - dak/process_unchecked.py
Merge commit 'lamby/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.Subst["__SUMMARY__"] = summary
431         new_ack_message = utils.TemplateSubst(u.Subst, template)
432         utils.send_mail(new_ack_message)
433
434 ################################################################################
435
436 # reprocess is necessary for the case of foo_1.2-1 and foo_1.2-2 in
437 # Incoming. -1 will reference the .orig.tar.gz, but -2 will not.
438 # Upload.check_dsc_against_db() can find the .orig.tar.gz but it will
439 # not have processed it during it's checks of -2.  If -1 has been
440 # deleted or otherwise not checked by 'dak process-unchecked', the
441 # .orig.tar.gz will not have been checked at all.  To get round this,
442 # we force the .orig.tar.gz into the .changes structure and reprocess
443 # the .changes file.
444
445 def process_it(changes_file):
446     global Logger
447
448     cnf = Config()
449
450     holding = Holding()
451
452     u = Upload()
453     u.pkg.changes_file = changes_file
454     u.pkg.directory = os.getcwd()
455     u.logger = Logger
456     origchanges = os.path.join(u.pkg.directory, u.pkg.changes_file)
457
458     # Some defaults in case we can't fully process the .changes file
459     u.pkg.changes["maintainer2047"] = cnf["Dinstall::MyEmailAddress"]
460     u.pkg.changes["changedby2047"] = cnf["Dinstall::MyEmailAddress"]
461
462     # debian-{devel-,}-changes@lists.debian.org toggles writes access based on this header
463     bcc = "X-DAK: dak process-unchecked\nX-Katie: $Revision: 1.65 $"
464     if cnf.has_key("Dinstall::Bcc"):
465         u.Subst["__BCC__"] = bcc + "\nBcc: %s" % (cnf["Dinstall::Bcc"])
466     else:
467         u.Subst["__BCC__"] = bcc
468
469     # Remember where we are so we can come back after cd-ing into the
470     # holding directory.  TODO: Fix this stupid hack
471     u.prevdir = os.getcwd()
472
473     # TODO: Figure out something better for this (or whether it's even
474     #       necessary - it seems to have been for use when we were
475     #       still doing the is_unchecked check; reprocess = 2)
476     u.reprocess = 1
477
478     try:
479         # If this is the Real Thing(tm), copy things into a private
480         # holding directory first to avoid replacable file races.
481         if not Options["No-Action"]:
482             os.chdir(cnf["Dir::Queue::Holding"])
483
484             # Absolutize the filename to avoid the requirement of being in the
485             # same directory as the .changes file.
486             holding.copy_to_holding(origchanges)
487
488             # Relativize the filename so we use the copy in holding
489             # rather than the original...
490             changespath = os.path.basename(u.pkg.changes_file)
491
492         (u.pkg.changes["fingerprint"], rejects) = utils.check_signature(changespath)
493
494         if u.pkg.changes["fingerprint"]:
495             valid_changes_p = u.load_changes(changespath)
496         else:
497             valid_changes_p = False
498             u.rejects.extend(rejects)
499
500         if valid_changes_p:
501             while u.reprocess:
502                 u.check_distributions()
503                 u.check_files(not Options["No-Action"])
504                 valid_dsc_p = u.check_dsc(not Options["No-Action"])
505                 if valid_dsc_p:
506                     u.check_source()
507                 u.check_hashes()
508                 u.check_urgency()
509                 u.check_timestamps()
510                 u.check_signed_by_key()
511
512         action(u)
513
514     except SystemExit:
515         raise
516
517     except:
518         print "ERROR"
519         traceback.print_exc(file=sys.stderr)
520
521     # Restore previous WD
522     os.chdir(u.prevdir)
523
524 ###############################################################################
525
526 def main():
527     global Options, Logger
528
529     cnf = Config()
530     changes_files = init()
531
532     # -n/--dry-run invalidates some other options which would involve things happening
533     if Options["No-Action"]:
534         Options["Automatic"] = ""
535
536     # Initialize our Holding singleton
537     holding = Holding()
538
539     # Ensure all the arguments we were given are .changes files
540     for f in changes_files:
541         if not f.endswith(".changes"):
542             utils.warn("Ignoring '%s' because it's not a .changes file." % (f))
543             changes_files.remove(f)
544
545     if changes_files == []:
546         if cnf["Dinstall::Options::Directory"] == "":
547             utils.fubar("Need at least one .changes file as an argument.")
548         else:
549             sys.exit(0)
550
551     # Check that we aren't going to clash with the daily cron job
552     if not Options["No-Action"] and os.path.exists("%s/daily.lock" % (cnf["Dir::Lock"])) and not Options["No-Lock"]:
553         utils.fubar("Archive maintenance in progress.  Try again later.")
554
555     # Obtain lock if not in no-action mode and initialize the log
556     if not Options["No-Action"]:
557         lock_fd = os.open(cnf["Dinstall::LockFile"], os.O_RDWR | os.O_CREAT)
558         try:
559             fcntl.lockf(lock_fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
560         except IOError, e:
561             if errno.errorcode[e.errno] == 'EACCES' or errno.errorcode[e.errno] == 'EAGAIN':
562                 utils.fubar("Couldn't obtain lock; assuming another 'dak process-unchecked' is already running.")
563             else:
564                 raise
565         Logger = daklog.Logger(cnf, "process-unchecked")
566
567     # Sort the .changes files so that we process sourceful ones first
568     changes_files.sort(utils.changes_compare)
569
570     # Process the changes files
571     for changes_file in changes_files:
572         print "\n" + changes_file
573         try:
574             process_it (changes_file)
575         finally:
576             if not Options["No-Action"]:
577                 holding.clean()
578
579     accept_count = SummaryStats().accept_count
580     accept_bytes = SummaryStats().accept_bytes
581
582     if accept_count:
583         sets = "set"
584         if accept_count > 1:
585             sets = "sets"
586         print "Accepted %d package %s, %s." % (accept_count, sets, utils.size_type(int(accept_bytes)))
587         Logger.log(["total",accept_count,accept_bytes])
588
589     if not Options["No-Action"]:
590         Logger.close()
591
592 ################################################################################
593
594 if __name__ == '__main__':
595     main()