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