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