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