]> git.decadent.org.uk Git - dak.git/blob - daklib/queue.py
Merge branch 'psycopg2' into content_generation
[dak.git] / daklib / queue.py
1 #!/usr/bin/env python
2 # vim:set et sw=4:
3
4 """
5 Queue utility functions for dak
6
7 @contact: Debian FTP Master <ftpmaster@debian.org>
8 @copyright: 2001 - 2006 James Troup <james@nocrew.org>
9 @copyright: 2009  Joerg Jaspert <joerg@debian.org>
10 @license: GNU General Public License version 2 or later
11 """
12
13 # This program is free software; you can redistribute it and/or modify
14 # it under the terms of the GNU General Public License as published by
15 # the Free Software Foundation; either version 2 of the License, or
16 # (at your option) any later version.
17
18 # This program is distributed in the hope that it will be useful,
19 # but WITHOUT ANY WARRANTY; without even the implied warranty of
20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
21 # GNU General Public License for more details.
22
23 # You should have received a copy of the GNU General Public License
24 # along with this program; if not, write to the Free Software
25 # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
26
27 ###############################################################################
28
29 import cPickle
30 import errno
31 import os
32 import pg
33 import stat
34 import sys
35 import time
36 import apt_inst
37 import apt_pkg
38 import utils
39 import database
40 from dak_exceptions import *
41 from regexes import re_default_answer, re_fdnic, re_bin_only_nmu
42
43 from types import *
44
45 ###############################################################################
46
47 # Determine what parts in a .changes are NEW
48
49 def determine_new(changes, files, projectB, warn=1):
50     """
51     Determine what parts in a C{changes} file are NEW.
52
53     @type changes: Upload.Pkg.changes dict
54     @param changes: Changes dictionary
55
56     @type files: Upload.Pkg.files dict
57     @param files: Files dictionary
58
59     @type projectB: pgobject
60     @param projectB: DB handle
61
62     @type warn: bool
63     @param warn: Warn if overrides are added for (old)stable
64
65     @rtype: dict
66     @return: dictionary of NEW components.
67
68     """
69     new = {}
70
71     # Build up a list of potentially new things
72     for file_entry in files.keys():
73         f = files[file_entry]
74         # Skip byhand elements
75         if f["type"] == "byhand":
76             continue
77         pkg = f["package"]
78         priority = f["priority"]
79         section = f["section"]
80         file_type = get_type(f)
81         component = f["component"]
82
83         if file_type == "dsc":
84             priority = "source"
85         if not new.has_key(pkg):
86             new[pkg] = {}
87             new[pkg]["priority"] = priority
88             new[pkg]["section"] = section
89             new[pkg]["type"] = file_type
90             new[pkg]["component"] = component
91             new[pkg]["files"] = []
92         else:
93             old_type = new[pkg]["type"]
94             if old_type != file_type:
95                 # source gets trumped by deb or udeb
96                 if old_type == "dsc":
97                     new[pkg]["priority"] = priority
98                     new[pkg]["section"] = section
99                     new[pkg]["type"] = file_type
100                     new[pkg]["component"] = component
101         new[pkg]["files"].append(file_entry)
102         if f.has_key("othercomponents"):
103             new[pkg]["othercomponents"] = f["othercomponents"]
104
105     for suite in changes["suite"].keys():
106         suite_id = database.get_suite_id(suite)
107         for pkg in new.keys():
108             component_id = database.get_component_id(new[pkg]["component"])
109             type_id = database.get_override_type_id(new[pkg]["type"])
110             q = projectB.query("SELECT package FROM override WHERE package = '%s' AND suite = %s AND component = %s AND type = %s" % (pkg, suite_id, component_id, type_id))
111             ql = q.getresult()
112             if ql:
113                 for file_entry in new[pkg]["files"]:
114                     if files[file_entry].has_key("new"):
115                         del files[file_entry]["new"]
116                 del new[pkg]
117
118     if warn:
119         if changes["suite"].has_key("stable"):
120             print "WARNING: overrides will be added for stable!"
121             if changes["suite"].has_key("oldstable"):
122                 print "WARNING: overrides will be added for OLDstable!"
123         for pkg in new.keys():
124             if new[pkg].has_key("othercomponents"):
125                 print "WARNING: %s already present in %s distribution." % (pkg, new[pkg]["othercomponents"])
126
127     return new
128
129 ################################################################################
130
131 def get_type(file):
132     """
133     Get the file type of C{file}
134
135     @type file: dict
136     @param file: file entry
137
138     @rtype: string
139     @return: filetype
140
141     """
142     # Determine the type
143     if file.has_key("dbtype"):
144         file_type = file["dbtype"]
145     elif file["type"] in [ "orig.tar.gz", "orig.tar.bz2", "tar.gz", "tar.bz2", "diff.gz", "diff.bz2", "dsc" ]:
146         file_type = "dsc"
147     else:
148         utils.fubar("invalid type (%s) for new.  Dazed, confused and sure as heck not continuing." % (file_type))
149
150     # Validate the override type
151     type_id = database.get_override_type_id(file_type)
152     if type_id == -1:
153         utils.fubar("invalid type (%s) for new.  Say wha?" % (file_type))
154
155     return file_type
156
157 ################################################################################
158
159
160
161 def check_valid(new):
162     """
163     Check if section and priority for NEW packages exist in database.
164     Additionally does sanity checks:
165       - debian-installer packages have to be udeb (or source)
166       - non debian-installer packages can not be udeb
167       - source priority can only be assigned to dsc file types
168
169     @type new: dict
170     @param new: Dict of new packages with their section, priority and type.
171
172     """
173     for pkg in new.keys():
174         section = new[pkg]["section"]
175         priority = new[pkg]["priority"]
176         file_type = new[pkg]["type"]
177         new[pkg]["section id"] = database.get_section_id(section)
178         new[pkg]["priority id"] = database.get_priority_id(new[pkg]["priority"])
179         # Sanity checks
180         di = section.find("debian-installer") != -1
181         if (di and file_type not in ("udeb", "dsc")) or (not di and file_type == "udeb"):
182             new[pkg]["section id"] = -1
183         if (priority == "source" and file_type != "dsc") or \
184            (priority != "source" and file_type == "dsc"):
185             new[pkg]["priority id"] = -1
186
187
188 ###############################################################################
189
190 class Pkg:
191     """ Convenience wrapper to carry around all the package information """
192     def __init__(self, **kwds):
193         self.__dict__.update(kwds)
194
195     def update(self, **kwds):
196         self.__dict__.update(kwds)
197
198 ###############################################################################
199
200 class Upload:
201     """
202     Everything that has to do with an upload processed.
203
204     """
205     def __init__(self, Cnf):
206         """
207         Initialize various variables and the global substitution template mappings.
208         Also connect to the DB and initialize the Database module.
209
210         """
211         self.Cnf = Cnf
212         self.accept_count = 0
213         self.accept_bytes = 0L
214         self.reject_message = ""
215         self.pkg = Pkg(changes = {}, dsc = {}, dsc_files = {}, files = {},
216                        legacy_source_untouchable = {})
217
218         # Initialize the substitution template mapping global
219         Subst = self.Subst = {}
220         Subst["__ADMIN_ADDRESS__"] = Cnf["Dinstall::MyAdminAddress"]
221         Subst["__BUG_SERVER__"] = Cnf["Dinstall::BugServer"]
222         Subst["__DISTRO__"] = Cnf["Dinstall::MyDistribution"]
223         Subst["__DAK_ADDRESS__"] = Cnf["Dinstall::MyEmailAddress"]
224
225         self.projectB = pg.connect(Cnf["DB::Name"], Cnf["DB::Host"], int(Cnf["DB::Port"]))
226         database.init(Cnf, self.projectB)
227
228     ###########################################################################
229
230     def init_vars (self):
231         """ Reset a number of entries from our Pkg object. """
232         self.pkg.changes.clear()
233         self.pkg.dsc.clear()
234         self.pkg.files.clear()
235         self.pkg.dsc_files.clear()
236         self.pkg.legacy_source_untouchable.clear()
237         self.pkg.orig_tar_id = None
238         self.pkg.orig_tar_location = ""
239         self.pkg.orig_tar_gz = None
240
241     ###########################################################################
242
243     def update_vars (self):
244         """
245         Update our Pkg object by reading a previously created cPickle .dak dumpfile.
246         """
247         dump_filename = self.pkg.changes_file[:-8]+".dak"
248         dump_file = utils.open_file(dump_filename)
249         p = cPickle.Unpickler(dump_file)
250
251         self.pkg.changes.update(p.load())
252         self.pkg.dsc.update(p.load())
253         self.pkg.files.update(p.load())
254         self.pkg.dsc_files.update(p.load())
255         self.pkg.legacy_source_untouchable.update(p.load())
256
257         self.pkg.orig_tar_id = p.load()
258         self.pkg.orig_tar_location = p.load()
259
260         dump_file.close()
261
262     ###########################################################################
263
264
265     def dump_vars(self, dest_dir):
266         """
267         Dump our Pkg object into a cPickle file.
268
269         @type dest_dir: string
270         @param dest_dir: Path where the dumpfile should be stored
271
272         @note: This could just dump the dictionaries as is, but I'd like to avoid this so
273                there's some idea of what process-accepted & process-new use from
274                process-unchecked. (JT)
275
276         """
277
278         changes = self.pkg.changes
279         dsc = self.pkg.dsc
280         files = self.pkg.files
281         dsc_files = self.pkg.dsc_files
282         legacy_source_untouchable = self.pkg.legacy_source_untouchable
283         orig_tar_id = self.pkg.orig_tar_id
284         orig_tar_location = self.pkg.orig_tar_location
285
286         dump_filename = os.path.join(dest_dir,self.pkg.changes_file[:-8] + ".dak")
287         dump_file = utils.open_file(dump_filename, 'w')
288         try:
289             os.chmod(dump_filename, 0664)
290         except OSError, e:
291             # chmod may fail when the dumpfile is not owned by the user
292             # invoking dak (like e.g. when NEW is processed by a member
293             # of ftpteam)
294             if errno.errorcode[e.errno] == 'EPERM':
295                 perms = stat.S_IMODE(os.stat(dump_filename)[stat.ST_MODE])
296                 # security precaution, should never happen unless a weird
297                 # umask is set anywhere
298                 if perms & stat.S_IWOTH:
299                     utils.fubar("%s is world writable and chmod failed." % \
300                         (dump_filename,))
301                 # ignore the failed chmod otherwise as the file should
302                 # already have the right privileges and is just, at worst,
303                 # unreadable for world
304             else:
305                 raise
306
307         p = cPickle.Pickler(dump_file, 1)
308         d_changes = {}
309         d_dsc = {}
310         d_files = {}
311         d_dsc_files = {}
312
313         ## files
314         for file_entry in files.keys():
315             d_files[file_entry] = {}
316             for i in [ "package", "version", "architecture", "type", "size",
317                        "md5sum", "sha1sum", "sha256sum", "component",
318                        "location id", "source package", "source version",
319                        "maintainer", "dbtype", "files id", "new",
320                        "section", "priority", "othercomponents",
321                        "pool name", "original component" ]:
322                 if files[file_entry].has_key(i):
323                     d_files[file_entry][i] = files[file_entry][i]
324         ## changes
325         # Mandatory changes fields
326         for i in [ "distribution", "source", "architecture", "version",
327                    "maintainer", "urgency", "fingerprint", "changedby822",
328                    "changedby2047", "changedbyname", "maintainer822",
329                    "maintainer2047", "maintainername", "maintaineremail",
330                    "closes", "changes" ]:
331             d_changes[i] = changes[i]
332         # Optional changes fields
333         for i in [ "changed-by", "filecontents", "format", "process-new note", "adv id", "distribution-version",
334                    "sponsoremail" ]:
335             if changes.has_key(i):
336                 d_changes[i] = changes[i]
337         ## dsc
338         for i in [ "source", "version", "maintainer", "fingerprint",
339                    "uploaders", "bts changelog", "dm-upload-allowed" ]:
340             if dsc.has_key(i):
341                 d_dsc[i] = dsc[i]
342         ## dsc_files
343         for file_entry in dsc_files.keys():
344             d_dsc_files[file_entry] = {}
345             # Mandatory dsc_files fields
346             for i in [ "size", "md5sum" ]:
347                 d_dsc_files[file_entry][i] = dsc_files[file_entry][i]
348             # Optional dsc_files fields
349             for i in [ "files id" ]:
350                 if dsc_files[file_entry].has_key(i):
351                     d_dsc_files[file_entry][i] = dsc_files[file_entry][i]
352
353         for i in [ d_changes, d_dsc, d_files, d_dsc_files,
354                    legacy_source_untouchable, orig_tar_id, orig_tar_location ]:
355             p.dump(i)
356         dump_file.close()
357
358     ###########################################################################
359
360     # Set up the per-package template substitution mappings
361
362     def update_subst (self, reject_message = ""):
363         """ Set up the per-package template substitution mappings """
364
365         Subst = self.Subst
366         changes = self.pkg.changes
367         # If 'dak process-unchecked' crashed out in the right place, architecture may still be a string.
368         if not changes.has_key("architecture") or not isinstance(changes["architecture"], DictType):
369             changes["architecture"] = { "Unknown" : "" }
370         # and maintainer2047 may not exist.
371         if not changes.has_key("maintainer2047"):
372             changes["maintainer2047"] = self.Cnf["Dinstall::MyEmailAddress"]
373
374         Subst["__ARCHITECTURE__"] = " ".join(changes["architecture"].keys())
375         Subst["__CHANGES_FILENAME__"] = os.path.basename(self.pkg.changes_file)
376         Subst["__FILE_CONTENTS__"] = changes.get("filecontents", "")
377
378         # For source uploads the Changed-By field wins; otherwise Maintainer wins.
379         if changes["architecture"].has_key("source") and changes["changedby822"] != "" and (changes["changedby822"] != changes["maintainer822"]):
380             Subst["__MAINTAINER_FROM__"] = changes["changedby2047"]
381             Subst["__MAINTAINER_TO__"] = "%s, %s" % (changes["changedby2047"],
382                                                      changes["maintainer2047"])
383             Subst["__MAINTAINER__"] = changes.get("changed-by", "Unknown")
384         else:
385             Subst["__MAINTAINER_FROM__"] = changes["maintainer2047"]
386             Subst["__MAINTAINER_TO__"] = changes["maintainer2047"]
387             Subst["__MAINTAINER__"] = changes.get("maintainer", "Unknown")
388
389         if "sponsoremail" in changes:
390             Subst["__MAINTAINER_TO__"] += ", %s"%changes["sponsoremail"]
391
392         if self.Cnf.has_key("Dinstall::TrackingServer") and changes.has_key("source"):
393             Subst["__MAINTAINER_TO__"] += "\nBcc: %s@%s" % (changes["source"], self.Cnf["Dinstall::TrackingServer"])
394
395         # Apply any global override of the Maintainer field
396         if self.Cnf.get("Dinstall::OverrideMaintainer"):
397             Subst["__MAINTAINER_TO__"] = self.Cnf["Dinstall::OverrideMaintainer"]
398             Subst["__MAINTAINER_FROM__"] = self.Cnf["Dinstall::OverrideMaintainer"]
399
400         Subst["__REJECT_MESSAGE__"] = reject_message
401         Subst["__SOURCE__"] = changes.get("source", "Unknown")
402         Subst["__VERSION__"] = changes.get("version", "Unknown")
403
404     ###########################################################################
405
406     def build_summaries(self):
407         """ Build a summary of changes the upload introduces. """
408         changes = self.pkg.changes
409         files = self.pkg.files
410
411         byhand = summary = new = ""
412
413         # changes["distribution"] may not exist in corner cases
414         # (e.g. unreadable changes files)
415         if not changes.has_key("distribution") or not isinstance(changes["distribution"], DictType):
416             changes["distribution"] = {}
417
418         override_summary =""
419         file_keys = files.keys()
420         file_keys.sort()
421         for file_entry in file_keys:
422             if files[file_entry].has_key("byhand"):
423                 byhand = 1
424                 summary += file_entry + " byhand\n"
425             elif files[file_entry].has_key("new"):
426                 new = 1
427                 summary += "(new) %s %s %s\n" % (file_entry, files[file_entry]["priority"], files[file_entry]["section"])
428                 if files[file_entry].has_key("othercomponents"):
429                     summary += "WARNING: Already present in %s distribution.\n" % (files[file_entry]["othercomponents"])
430                 if files[file_entry]["type"] == "deb":
431                     deb_fh = utils.open_file(file_entry)
432                     summary += apt_pkg.ParseSection(apt_inst.debExtractControl(deb_fh))["Description"] + '\n'
433                     deb_fh.close()
434             else:
435                 files[file_entry]["pool name"] = utils.poolify (changes.get("source",""), files[file_entry]["component"])
436                 destination = self.Cnf["Dir::PoolRoot"] + files[file_entry]["pool name"] + file_entry
437                 summary += file_entry + "\n  to " + destination + "\n"
438                 if not files[file_entry].has_key("type"):
439                     files[file_entry]["type"] = "unknown"
440                 if files[file_entry]["type"] in ["deb", "udeb", "dsc"]:
441                     # (queue/unchecked), there we have override entries already, use them
442                     # (process-new), there we dont have override entries, use the newly generated ones.
443                     override_prio = files[file_entry].get("override priority", files[file_entry]["priority"])
444                     override_sect = files[file_entry].get("override section", files[file_entry]["section"])
445                     override_summary += "%s - %s %s\n" % (file_entry, override_prio, override_sect)
446
447         short_summary = summary
448
449         # This is for direport's benefit...
450         f = re_fdnic.sub("\n .\n", changes.get("changes",""))
451
452         if byhand or new:
453             summary += "Changes: " + f
454
455         summary += "\n\nOverride entries for your package:\n" + override_summary + "\n"
456
457         summary += self.announce(short_summary, 0)
458
459         return (summary, short_summary)
460
461     ###########################################################################
462
463     def close_bugs (self, summary, action):
464         """
465         Send mail to close bugs as instructed by the closes field in the changes file.
466         Also add a line to summary if any work was done.
467
468         @type summary: string
469         @param summary: summary text, as given by L{build_summaries}
470
471         @type action: bool
472         @param action: Set to false no real action will be done.
473
474         @rtype: string
475         @return: summary. If action was taken, extended by the list of closed bugs.
476
477         """
478         changes = self.pkg.changes
479         Subst = self.Subst
480         Cnf = self.Cnf
481
482         bugs = changes["closes"].keys()
483
484         if not bugs:
485             return summary
486
487         bugs.sort()
488         summary += "Closing bugs: "
489         for bug in bugs:
490             summary += "%s " % (bug)
491             if action:
492                 Subst["__BUG_NUMBER__"] = bug
493                 if changes["distribution"].has_key("stable"):
494                     Subst["__STABLE_WARNING__"] = """
495 Note that this package is not part of the released stable Debian
496 distribution.  It may have dependencies on other unreleased software,
497 or other instabilities.  Please take care if you wish to install it.
498 The update will eventually make its way into the next released Debian
499 distribution."""
500                 else:
501                     Subst["__STABLE_WARNING__"] = ""
502                     mail_message = utils.TemplateSubst(Subst,Cnf["Dir::Templates"]+"/process-unchecked.bug-close")
503                     utils.send_mail (mail_message)
504         if action:
505             self.Logger.log(["closing bugs"]+bugs)
506         summary += "\n"
507
508         return summary
509
510     ###########################################################################
511
512     def announce (self, short_summary, action):
513         """
514         Send an announce mail about a new upload.
515
516         @type short_summary: string
517         @param short_summary: Short summary text to include in the mail
518
519         @type action: bool
520         @param action: Set to false no real action will be done.
521
522         @rtype: string
523         @return: Textstring about action taken.
524
525         """
526         Subst = self.Subst
527         Cnf = self.Cnf
528         changes = self.pkg.changes
529
530         # Only do announcements for source uploads with a recent dpkg-dev installed
531         if float(changes.get("format", 0)) < 1.6 or not changes["architecture"].has_key("source"):
532             return ""
533
534         lists_done = {}
535         summary = ""
536         Subst["__SHORT_SUMMARY__"] = short_summary
537
538         for dist in changes["distribution"].keys():
539             announce_list = Cnf.Find("Suite::%s::Announce" % (dist))
540             if announce_list == "" or lists_done.has_key(announce_list):
541                 continue
542             lists_done[announce_list] = 1
543             summary += "Announcing to %s\n" % (announce_list)
544
545             if action:
546                 Subst["__ANNOUNCE_LIST_ADDRESS__"] = announce_list
547                 if Cnf.get("Dinstall::TrackingServer") and changes["architecture"].has_key("source"):
548                     Subst["__ANNOUNCE_LIST_ADDRESS__"] = Subst["__ANNOUNCE_LIST_ADDRESS__"] + "\nBcc: %s@%s" % (changes["source"], Cnf["Dinstall::TrackingServer"])
549                 mail_message = utils.TemplateSubst(Subst,Cnf["Dir::Templates"]+"/process-unchecked.announce")
550                 utils.send_mail (mail_message)
551
552         if Cnf.FindB("Dinstall::CloseBugs"):
553             summary = self.close_bugs(summary, action)
554
555         return summary
556
557     ###########################################################################
558
559     def accept (self, summary, short_summary):
560         """
561         Accept an upload.
562
563         This moves all files referenced from the .changes into the I{accepted}
564         queue, sends the accepted mail, announces to lists, closes bugs and
565         also checks for override disparities. If enabled it will write out
566         the version history for the BTS Version Tracking and will finally call
567         L{queue_build}.
568
569         @type summary: string
570         @param summary: Summary text
571
572         @type short_summary: string
573         @param short_summary: Short summary
574
575         """
576
577         Cnf = self.Cnf
578         Subst = self.Subst
579         files = self.pkg.files
580         changes = self.pkg.changes
581         changes_file = self.pkg.changes_file
582         dsc = self.pkg.dsc
583
584         print "Accepting."
585         self.Logger.log(["Accepting changes",changes_file])
586
587         self.dump_vars(Cnf["Dir::Queue::Accepted"])
588
589         # Move all the files into the accepted directory
590         utils.move(changes_file, Cnf["Dir::Queue::Accepted"])
591         file_keys = files.keys()
592         for file_entry in file_keys:
593             utils.move(file_entry, Cnf["Dir::Queue::Accepted"])
594             self.accept_bytes += float(files[file_entry]["size"])
595         self.accept_count += 1
596
597         # Send accept mail, announce to lists, close bugs and check for
598         # override disparities
599         if not Cnf["Dinstall::Options::No-Mail"]:
600             Subst["__SUITE__"] = ""
601             Subst["__SUMMARY__"] = summary
602             mail_message = utils.TemplateSubst(Subst,Cnf["Dir::Templates"]+"/process-unchecked.accepted")
603             utils.send_mail(mail_message)
604             self.announce(short_summary, 1)
605
606
607         ## Helper stuff for DebBugs Version Tracking
608         if Cnf.Find("Dir::Queue::BTSVersionTrack"):
609             # ??? once queue/* is cleared on *.d.o and/or reprocessed
610             # the conditionalization on dsc["bts changelog"] should be
611             # dropped.
612
613             # Write out the version history from the changelog
614             if changes["architecture"].has_key("source") and \
615                dsc.has_key("bts changelog"):
616
617                 (fd, temp_filename) = utils.temp_filename(Cnf["Dir::Queue::BTSVersionTrack"], prefix=".")
618                 version_history = os.fdopen(fd, 'w')
619                 version_history.write(dsc["bts changelog"])
620                 version_history.close()
621                 filename = "%s/%s" % (Cnf["Dir::Queue::BTSVersionTrack"],
622                                       changes_file[:-8]+".versions")
623                 os.rename(temp_filename, filename)
624                 os.chmod(filename, 0644)
625
626             # Write out the binary -> source mapping.
627             (fd, temp_filename) = utils.temp_filename(Cnf["Dir::Queue::BTSVersionTrack"], prefix=".")
628             debinfo = os.fdopen(fd, 'w')
629             for file_entry in file_keys:
630                 f = files[file_entry]
631                 if f["type"] == "deb":
632                     line = " ".join([f["package"], f["version"],
633                                      f["architecture"], f["source package"],
634                                      f["source version"]])
635                     debinfo.write(line+"\n")
636             debinfo.close()
637             filename = "%s/%s" % (Cnf["Dir::Queue::BTSVersionTrack"],
638                                   changes_file[:-8]+".debinfo")
639             os.rename(temp_filename, filename)
640             os.chmod(filename, 0644)
641
642         self.queue_build("accepted", Cnf["Dir::Queue::Accepted"])
643
644     ###########################################################################
645
646     def queue_build (self, queue, path):
647         """
648         Prepare queue_build database table used for incoming autobuild support.
649
650         @type queue: string
651         @param queue: queue name
652
653         @type path: string
654         @param path: path for the queue file entries/link destinations
655         """
656
657         Cnf = self.Cnf
658         Subst = self.Subst
659         files = self.pkg.files
660         changes = self.pkg.changes
661         changes_file = self.pkg.changes_file
662         dsc = self.pkg.dsc
663         file_keys = files.keys()
664
665         ## Special support to enable clean auto-building of queued packages
666         queue_id = database.get_or_set_queue_id(queue)
667
668         self.projectB.query("BEGIN WORK")
669         for suite in changes["distribution"].keys():
670             if suite not in Cnf.ValueList("Dinstall::QueueBuildSuites"):
671                 continue
672             suite_id = database.get_suite_id(suite)
673             dest_dir = Cnf["Dir::QueueBuild"]
674             if Cnf.FindB("Dinstall::SecurityQueueBuild"):
675                 dest_dir = os.path.join(dest_dir, suite)
676             for file_entry in file_keys:
677                 src = os.path.join(path, file_entry)
678                 dest = os.path.join(dest_dir, file_entry)
679                 if Cnf.FindB("Dinstall::SecurityQueueBuild"):
680                     # Copy it since the original won't be readable by www-data
681                     utils.copy(src, dest)
682                 else:
683                     # Create a symlink to it
684                     os.symlink(src, dest)
685                 # Add it to the list of packages for later processing by apt-ftparchive
686                 self.projectB.query("INSERT INTO queue_build (suite, queue, filename, in_queue) VALUES (%s, %s, '%s', 't')" % (suite_id, queue_id, dest))
687             # If the .orig.tar.gz is in the pool, create a symlink to
688             # it (if one doesn't already exist)
689             if self.pkg.orig_tar_id:
690                 # Determine the .orig.tar.gz file name
691                 for dsc_file in self.pkg.dsc_files.keys():
692                     if dsc_file.endswith(".orig.tar.gz"):
693                         filename = dsc_file
694                 dest = os.path.join(dest_dir, filename)
695                 # If it doesn't exist, create a symlink
696                 if not os.path.exists(dest):
697                     # Find the .orig.tar.gz in the pool
698                     q = self.projectB.query("SELECT l.path, f.filename from location l, files f WHERE f.id = %s and f.location = l.id" % (self.pkg.orig_tar_id))
699                     ql = q.getresult()
700                     if not ql:
701                         utils.fubar("[INTERNAL ERROR] Couldn't find id %s in files table." % (self.pkg.orig_tar_id))
702                     src = os.path.join(ql[0][0], ql[0][1])
703                     os.symlink(src, dest)
704                     # Add it to the list of packages for later processing by apt-ftparchive
705                     self.projectB.query("INSERT INTO queue_build (suite, queue, filename, in_queue) VALUES (%s, %s, '%s', 't')" % (suite_id, queue_id, dest))
706                 # if it does, update things to ensure it's not removed prematurely
707                 else:
708                     self.projectB.query("UPDATE queue_build SET in_queue = 't', last_used = NULL WHERE filename = '%s' AND suite = %s" % (dest, suite_id))
709
710         self.projectB.query("COMMIT WORK")
711
712     ###########################################################################
713
714     def check_override (self):
715         """
716         Checks override entries for validity. Mails "Override disparity" warnings,
717         if that feature is enabled.
718
719         Abandons the check if
720           - this is a non-sourceful upload
721           - override disparity checks are disabled
722           - mail sending is disabled
723
724         """
725         Subst = self.Subst
726         changes = self.pkg.changes
727         files = self.pkg.files
728         Cnf = self.Cnf
729
730         # Abandon the check if:
731         #  a) it's a non-sourceful upload
732         #  b) override disparity checks have been disabled
733         #  c) we're not sending mail
734         if not changes["architecture"].has_key("source") or \
735            not Cnf.FindB("Dinstall::OverrideDisparityCheck") or \
736            Cnf["Dinstall::Options::No-Mail"]:
737             return
738
739         summary = ""
740         file_keys = files.keys()
741         file_keys.sort()
742         for file_entry in file_keys:
743             if not files[file_entry].has_key("new") and files[file_entry]["type"] == "deb":
744                 section = files[file_entry]["section"]
745                 override_section = files[file_entry]["override section"]
746                 if section.lower() != override_section.lower() and section != "-":
747                     summary += "%s: package says section is %s, override says %s.\n" % (file_entry, section, override_section)
748                 priority = files[file_entry]["priority"]
749                 override_priority = files[file_entry]["override priority"]
750                 if priority != override_priority and priority != "-":
751                     summary += "%s: package says priority is %s, override says %s.\n" % (file_entry, priority, override_priority)
752
753         if summary == "":
754             return
755
756         Subst["__SUMMARY__"] = summary
757         mail_message = utils.TemplateSubst(Subst,self.Cnf["Dir::Templates"]+"/process-unchecked.override-disparity")
758         utils.send_mail(mail_message)
759
760     ###########################################################################
761
762     def force_reject (self, files):
763         """
764         Forcefully move files from the current directory to the
765         reject directory.  If any file already exists in the reject
766         directory it will be moved to the morgue to make way for
767         the new file.
768
769         @type files: dict
770         @param files: file dictionary
771
772         """
773
774         Cnf = self.Cnf
775
776         for file_entry in files:
777             # Skip any files which don't exist or which we don't have permission to copy.
778             if os.access(file_entry,os.R_OK) == 0:
779                 continue
780             dest_file = os.path.join(Cnf["Dir::Queue::Reject"], file_entry)
781             try:
782                 dest_fd = os.open(dest_file, os.O_RDWR|os.O_CREAT|os.O_EXCL, 0644)
783             except OSError, e:
784                 # File exists?  Let's try and move it to the morgue
785                 if errno.errorcode[e.errno] == 'EEXIST':
786                     morgue_file = os.path.join(Cnf["Dir::Morgue"],Cnf["Dir::MorgueReject"],file_entry)
787                     try:
788                         morgue_file = utils.find_next_free(morgue_file)
789                     except NoFreeFilenameError:
790                         # Something's either gone badly Pete Tong, or
791                         # someone is trying to exploit us.
792                         utils.warn("**WARNING** failed to move %s from the reject directory to the morgue." % (file_entry))
793                         return
794                     utils.move(dest_file, morgue_file, perms=0660)
795                     try:
796                         dest_fd = os.open(dest_file, os.O_RDWR|os.O_CREAT|os.O_EXCL, 0644)
797                     except OSError, e:
798                         # Likewise
799                         utils.warn("**WARNING** failed to claim %s in the reject directory." % (file_entry))
800                         return
801                 else:
802                     raise
803             # If we got here, we own the destination file, so we can
804             # safely overwrite it.
805             utils.move(file_entry, dest_file, 1, perms=0660)
806             os.close(dest_fd)
807
808     ###########################################################################
809
810     def do_reject (self, manual = 0, reject_message = ""):
811         """
812         Reject an upload. If called without a reject message or C{manual} is
813         true, spawn an editor so the user can write one.
814
815         @type manual: bool
816         @param manual: manual or automated rejection
817
818         @type reject_message: string
819         @param reject_message: A reject message
820
821         @return: 0
822
823         """
824         # If we weren't given a manual rejection message, spawn an
825         # editor so the user can add one in...
826         if manual and not reject_message:
827             (fd, temp_filename) = utils.temp_filename()
828             editor = os.environ.get("EDITOR","vi")
829             answer = 'E'
830             while answer == 'E':
831                 os.system("%s %s" % (editor, temp_filename))
832                 temp_fh = utils.open_file(temp_filename)
833                 reject_message = "".join(temp_fh.readlines())
834                 temp_fh.close()
835                 print "Reject message:"
836                 print utils.prefix_multi_line_string(reject_message,"  ",include_blank_lines=1)
837                 prompt = "[R]eject, Edit, Abandon, Quit ?"
838                 answer = "XXX"
839                 while prompt.find(answer) == -1:
840                     answer = utils.our_raw_input(prompt)
841                     m = re_default_answer.search(prompt)
842                     if answer == "":
843                         answer = m.group(1)
844                     answer = answer[:1].upper()
845             os.unlink(temp_filename)
846             if answer == 'A':
847                 return 1
848             elif answer == 'Q':
849                 sys.exit(0)
850
851         print "Rejecting.\n"
852
853         Cnf = self.Cnf
854         Subst = self.Subst
855         pkg = self.pkg
856
857         reason_filename = pkg.changes_file[:-8] + ".reason"
858         reason_filename = Cnf["Dir::Queue::Reject"] + '/' + reason_filename
859
860         # Move all the files into the reject directory
861         reject_files = pkg.files.keys() + [pkg.changes_file]
862         self.force_reject(reject_files)
863
864         # If we fail here someone is probably trying to exploit the race
865         # so let's just raise an exception ...
866         if os.path.exists(reason_filename):
867             os.unlink(reason_filename)
868         reason_fd = os.open(reason_filename, os.O_RDWR|os.O_CREAT|os.O_EXCL, 0644)
869
870         if not manual:
871             Subst["__REJECTOR_ADDRESS__"] = Cnf["Dinstall::MyEmailAddress"]
872             Subst["__MANUAL_REJECT_MESSAGE__"] = ""
873             Subst["__CC__"] = "X-DAK-Rejection: automatic (moo)\nX-Katie-Rejection: automatic (moo)"
874             os.write(reason_fd, reject_message)
875             reject_mail_message = utils.TemplateSubst(Subst,Cnf["Dir::Templates"]+"/queue.rejected")
876         else:
877             # Build up the rejection email
878             user_email_address = utils.whoami() + " <%s>" % (Cnf["Dinstall::MyAdminAddress"])
879
880             Subst["__REJECTOR_ADDRESS__"] = user_email_address
881             Subst["__MANUAL_REJECT_MESSAGE__"] = reject_message
882             Subst["__CC__"] = "Cc: " + Cnf["Dinstall::MyEmailAddress"]
883             reject_mail_message = utils.TemplateSubst(Subst,Cnf["Dir::Templates"]+"/queue.rejected")
884             # Write the rejection email out as the <foo>.reason file
885             os.write(reason_fd, reject_mail_message)
886
887         os.close(reason_fd)
888
889         # Send the rejection mail if appropriate
890         if not Cnf["Dinstall::Options::No-Mail"]:
891             utils.send_mail(reject_mail_message)
892
893         self.Logger.log(["rejected", pkg.changes_file])
894         return 0
895
896     ################################################################################
897
898     def source_exists (self, package, source_version, suites = ["any"]):
899         """
900         Ensure that source exists somewhere in the archive for the binary
901         upload being processed.
902           1. exact match     => 1.0-3
903           2. bin-only NMU    => 1.0-3+b1 , 1.0-3.1+b1
904
905         @type package: string
906         @param package: package source name
907
908         @type source_version: string
909         @param source_version: expected source version
910
911         @type suites: list
912         @param suites: list of suites to check in, default I{any}
913
914         @rtype: int
915         @return: returns 1 if a source with expected version is found, otherwise 0
916
917         """
918         okay = 1
919         for suite in suites:
920             if suite == "any":
921                 que = "SELECT s.version FROM source s WHERE s.source = '%s'" % \
922                     (package)
923             else:
924                 # source must exist in suite X, or in some other suite that's
925                 # mapped to X, recursively... silent-maps are counted too,
926                 # unreleased-maps aren't.
927                 maps = self.Cnf.ValueList("SuiteMappings")[:]
928                 maps.reverse()
929                 maps = [ m.split() for m in maps ]
930                 maps = [ (x[1], x[2]) for x in maps
931                                 if x[0] == "map" or x[0] == "silent-map" ]
932                 s = [suite]
933                 for x in maps:
934                     if x[1] in s and x[0] not in s:
935                         s.append(x[0])
936
937                 que = "SELECT s.version FROM source s JOIN src_associations sa ON (s.id = sa.source) JOIN suite su ON (sa.suite = su.id) WHERE s.source = '%s' AND (%s)" % (package, " OR ".join(["su.suite_name = '%s'" % a for a in s]))
938             q = self.projectB.query(que)
939
940             # Reduce the query results to a list of version numbers
941             ql = [ i[0] for i in q.getresult() ]
942
943             # Try (1)
944             if source_version in ql:
945                 continue
946
947             # Try (2)
948             orig_source_version = re_bin_only_nmu.sub('', source_version)
949             if orig_source_version in ql:
950                 continue
951
952             # No source found...
953             okay = 0
954             break
955         return okay
956
957     ################################################################################
958
959     def in_override_p (self, package, component, suite, binary_type, file):
960         """
961         Check if a package already has override entries in the DB
962
963         @type package: string
964         @param package: package name
965
966         @type component: string
967         @param component: database id of the component, as returned by L{database.get_component_id}
968
969         @type suite: int
970         @param suite: database id of the suite, as returned by L{database.get_suite_id}
971
972         @type binary_type: string
973         @param binary_type: type of the package
974
975         @type file: string
976         @param file: filename we check
977
978         @return: the database result. But noone cares anyway.
979
980         """
981         files = self.pkg.files
982
983         if binary_type == "": # must be source
984             file_type = "dsc"
985         else:
986             file_type = binary_type
987
988         # Override suite name; used for example with proposed-updates
989         if self.Cnf.Find("Suite::%s::OverrideSuite" % (suite)) != "":
990             suite = self.Cnf["Suite::%s::OverrideSuite" % (suite)]
991
992         # Avoid <undef> on unknown distributions
993         suite_id = database.get_suite_id(suite)
994         if suite_id == -1:
995             return None
996         component_id = database.get_component_id(component)
997         type_id = database.get_override_type_id(file_type)
998
999         q = self.projectB.query("SELECT s.section, p.priority FROM override o, section s, priority p WHERE package = '%s' AND suite = %s AND component = %s AND type = %s AND o.section = s.id AND o.priority = p.id"
1000                            % (package, suite_id, component_id, type_id))
1001         result = q.getresult()
1002         # If checking for a source package fall back on the binary override type
1003         if file_type == "dsc" and not result:
1004             deb_type_id = database.get_override_type_id("deb")
1005             udeb_type_id = database.get_override_type_id("udeb")
1006             q = self.projectB.query("SELECT s.section, p.priority FROM override o, section s, priority p WHERE package = '%s' AND suite = %s AND component = %s AND (type = %s OR type = %s) AND o.section = s.id AND o.priority = p.id"
1007                                % (package, suite_id, component_id, deb_type_id, udeb_type_id))
1008             result = q.getresult()
1009
1010         # Remember the section and priority so we can check them later if appropriate
1011         if result:
1012             files[file]["override section"] = result[0][0]
1013             files[file]["override priority"] = result[0][1]
1014
1015         return result
1016
1017     ################################################################################
1018
1019     def reject (self, str, prefix="Rejected: "):
1020         """
1021         Add C{str} to reject_message. Adds C{prefix}, by default "Rejected: "
1022
1023         @type str: string
1024         @param str: Reject text
1025
1026         @type prefix: string
1027         @param prefix: Prefix text, default Rejected:
1028
1029         """
1030         if str:
1031             # Unlike other rejects we add new lines first to avoid trailing
1032             # new lines when this message is passed back up to a caller.
1033             if self.reject_message:
1034                 self.reject_message += "\n"
1035             self.reject_message += prefix + str
1036
1037     ################################################################################
1038
1039     def get_anyversion(self, query_result, suite):
1040         """ """
1041         anyversion=None
1042         anysuite = [suite] + self.Cnf.ValueList("Suite::%s::VersionChecks::Enhances" % (suite))
1043         for (v, s) in query_result:
1044             if s in [ x.lower() for x in anysuite ]:
1045                 if not anyversion or apt_pkg.VersionCompare(anyversion, v) <= 0:
1046                     anyversion=v
1047         return anyversion
1048
1049     ################################################################################
1050
1051     def cross_suite_version_check(self, query_result, file, new_version,
1052             sourceful=False):
1053         """
1054         Ensure versions are newer than existing packages in target
1055         suites and that cross-suite version checking rules as
1056         set out in the conf file are satisfied.
1057
1058         """
1059
1060         # Check versions for each target suite
1061         for target_suite in self.pkg.changes["distribution"].keys():
1062             must_be_newer_than = [ i.lower() for i in self.Cnf.ValueList("Suite::%s::VersionChecks::MustBeNewerThan" % (target_suite)) ]
1063             must_be_older_than = [ i.lower() for i in self.Cnf.ValueList("Suite::%s::VersionChecks::MustBeOlderThan" % (target_suite)) ]
1064             # Enforce "must be newer than target suite" even if conffile omits it
1065             if target_suite not in must_be_newer_than:
1066                 must_be_newer_than.append(target_suite)
1067             for entry in query_result:
1068                 existent_version = entry[0]
1069                 suite = entry[1]
1070                 if suite in must_be_newer_than and sourceful and \
1071                    apt_pkg.VersionCompare(new_version, existent_version) < 1:
1072                     self.reject("%s: old version (%s) in %s >= new version (%s) targeted at %s." % (file, existent_version, suite, new_version, target_suite))
1073                 if suite in must_be_older_than and \
1074                    apt_pkg.VersionCompare(new_version, existent_version) > -1:
1075                     ch = self.pkg.changes
1076                     cansave = 0
1077                     if ch.get('distribution-version', {}).has_key(suite):
1078                     # we really use the other suite, ignoring the conflicting one ...
1079                         addsuite = ch["distribution-version"][suite]
1080
1081                         add_version = self.get_anyversion(query_result, addsuite)
1082                         target_version = self.get_anyversion(query_result, target_suite)
1083
1084                         if not add_version:
1085                             # not add_version can only happen if we map to a suite
1086                             # that doesn't enhance the suite we're propup'ing from.
1087                             # so "propup-ver x a b c; map a d" is a problem only if
1088                             # d doesn't enhance a.
1089                             #
1090                             # i think we could always propagate in this case, rather
1091                             # than complaining. either way, this isn't a REJECT issue
1092                             #
1093                             # And - we really should complain to the dorks who configured dak
1094                             self.reject("%s is mapped to, but not enhanced by %s - adding anyways" % (suite, addsuite), "Warning: ")
1095                             self.pkg.changes.setdefault("propdistribution", {})
1096                             self.pkg.changes["propdistribution"][addsuite] = 1
1097                             cansave = 1
1098                         elif not target_version:
1099                             # not targets_version is true when the package is NEW
1100                             # we could just stick with the "...old version..." REJECT
1101                             # for this, I think.
1102                             self.reject("Won't propogate NEW packages.")
1103                         elif apt_pkg.VersionCompare(new_version, add_version) < 0:
1104                             # propogation would be redundant. no need to reject though.
1105                             self.reject("ignoring versionconflict: %s: old version (%s) in %s <= new version (%s) targeted at %s." % (file, existent_version, suite, new_version, target_suite), "Warning: ")
1106                             cansave = 1
1107                         elif apt_pkg.VersionCompare(new_version, add_version) > 0 and \
1108                              apt_pkg.VersionCompare(add_version, target_version) >= 0:
1109                             # propogate!!
1110                             self.reject("Propogating upload to %s" % (addsuite), "Warning: ")
1111                             self.pkg.changes.setdefault("propdistribution", {})
1112                             self.pkg.changes["propdistribution"][addsuite] = 1
1113                             cansave = 1
1114
1115                     if not cansave:
1116                         self.reject("%s: old version (%s) in %s <= new version (%s) targeted at %s." % (file, existent_version, suite, new_version, target_suite))
1117
1118     ################################################################################
1119
1120     def check_binary_against_db(self, file):
1121         """
1122
1123         """
1124         self.reject_message = ""
1125         files = self.pkg.files
1126
1127         # Ensure version is sane
1128         q = self.projectB.query("""
1129 SELECT b.version, su.suite_name FROM binaries b, bin_associations ba, suite su,
1130                                      architecture a
1131  WHERE b.package = '%s' AND (a.arch_string = '%s' OR a.arch_string = 'all')
1132    AND ba.bin = b.id AND ba.suite = su.id AND b.architecture = a.id"""
1133                                 % (files[file]["package"],
1134                                    files[file]["architecture"]))
1135         self.cross_suite_version_check(q.getresult(), file,
1136             files[file]["version"], sourceful=False)
1137
1138         # Check for any existing copies of the file
1139         q = self.projectB.query("""
1140 SELECT b.id FROM binaries b, architecture a
1141  WHERE b.package = '%s' AND b.version = '%s' AND a.arch_string = '%s'
1142    AND a.id = b.architecture"""
1143                                 % (files[file]["package"],
1144                                    files[file]["version"],
1145                                    files[file]["architecture"]))
1146         if q.getresult():
1147             self.reject("%s: can not overwrite existing copy already in the archive." % (file))
1148
1149         return self.reject_message
1150
1151     ################################################################################
1152
1153     def check_source_against_db(self, file):
1154         """
1155         """
1156         self.reject_message = ""
1157         dsc = self.pkg.dsc
1158
1159         # Ensure version is sane
1160         q = self.projectB.query("""
1161 SELECT s.version, su.suite_name FROM source s, src_associations sa, suite su
1162  WHERE s.source = '%s' AND sa.source = s.id AND sa.suite = su.id""" % (dsc.get("source")))
1163         self.cross_suite_version_check(q.getresult(), file, dsc.get("version"),
1164             sourceful=True)
1165
1166         return self.reject_message
1167
1168     ################################################################################
1169
1170
1171     def check_dsc_against_db(self, file):
1172         """
1173
1174         @warning: NB: this function can remove entries from the 'files' index [if
1175          the .orig.tar.gz is a duplicate of the one in the archive]; if
1176          you're iterating over 'files' and call this function as part of
1177          the loop, be sure to add a check to the top of the loop to
1178          ensure you haven't just tried to dereference the deleted entry.
1179
1180         """
1181         self.reject_message = ""
1182         files = self.pkg.files
1183         dsc_files = self.pkg.dsc_files
1184         legacy_source_untouchable = self.pkg.legacy_source_untouchable
1185         self.pkg.orig_tar_gz = None
1186
1187         # Try and find all files mentioned in the .dsc.  This has
1188         # to work harder to cope with the multiple possible
1189         # locations of an .orig.tar.gz.
1190         # The ordering on the select is needed to pick the newest orig
1191         # when it exists in multiple places.
1192         for dsc_file in dsc_files.keys():
1193             found = None
1194             if files.has_key(dsc_file):
1195                 actual_md5 = files[dsc_file]["md5sum"]
1196                 actual_size = int(files[dsc_file]["size"])
1197                 found = "%s in incoming" % (dsc_file)
1198                 # Check the file does not already exist in the archive
1199                 q = self.projectB.query("SELECT f.size, f.md5sum, l.path, f.filename FROM files f, location l WHERE f.filename LIKE '%%%s%%' AND l.id = f.location ORDER BY f.id DESC" % (dsc_file))
1200                 ql = q.getresult()
1201                 # Strip out anything that isn't '%s' or '/%s$'
1202                 for i in ql:
1203                     if i[3] != dsc_file and i[3][-(len(dsc_file)+1):] != '/'+dsc_file:
1204                         ql.remove(i)
1205
1206                 # "[dak] has not broken them.  [dak] has fixed a
1207                 # brokenness.  Your crappy hack exploited a bug in
1208                 # the old dinstall.
1209                 #
1210                 # "(Come on!  I thought it was always obvious that
1211                 # one just doesn't release different files with
1212                 # the same name and version.)"
1213                 #                        -- ajk@ on d-devel@l.d.o
1214
1215                 if ql:
1216                     # Ignore exact matches for .orig.tar.gz
1217                     match = 0
1218                     if dsc_file.endswith(".orig.tar.gz"):
1219                         for i in ql:
1220                             if files.has_key(dsc_file) and \
1221                                int(files[dsc_file]["size"]) == int(i[0]) and \
1222                                files[dsc_file]["md5sum"] == i[1]:
1223                                 self.reject("ignoring %s, since it's already in the archive." % (dsc_file), "Warning: ")
1224                                 del files[dsc_file]
1225                                 self.pkg.orig_tar_gz = i[2] + i[3]
1226                                 match = 1
1227
1228                     if not match:
1229                         self.reject("can not overwrite existing copy of '%s' already in the archive." % (dsc_file))
1230             elif dsc_file.endswith(".orig.tar.gz"):
1231                 # Check in the pool
1232                 q = self.projectB.query("SELECT l.path, f.filename, l.type, f.id, l.id FROM files f, location l WHERE f.filename LIKE '%%%s%%' AND l.id = f.location" % (dsc_file))
1233                 ql = q.getresult()
1234                 # Strip out anything that isn't '%s' or '/%s$'
1235                 for i in ql:
1236                     if i[1] != dsc_file and i[1][-(len(dsc_file)+1):] != '/'+dsc_file:
1237                         ql.remove(i)
1238
1239                 if ql:
1240                     # Unfortunately, we may get more than one match here if,
1241                     # for example, the package was in potato but had an -sa
1242                     # upload in woody.  So we need to choose the right one.
1243
1244                     # default to something sane in case we don't match any or have only one
1245                     x = ql[0]
1246
1247                     if len(ql) > 1:
1248                         for i in ql:
1249                             old_file = i[0] + i[1]
1250                             old_file_fh = utils.open_file(old_file)
1251                             actual_md5 = apt_pkg.md5sum(old_file_fh)
1252                             old_file_fh.close()
1253                             actual_size = os.stat(old_file)[stat.ST_SIZE]
1254                             if actual_md5 == dsc_files[dsc_file]["md5sum"] and actual_size == int(dsc_files[dsc_file]["size"]):
1255                                 x = i
1256                             else:
1257                                 legacy_source_untouchable[i[3]] = ""
1258
1259                     old_file = x[0] + x[1]
1260                     old_file_fh = utils.open_file(old_file)
1261                     actual_md5 = apt_pkg.md5sum(old_file_fh)
1262                     old_file_fh.close()
1263                     actual_size = os.stat(old_file)[stat.ST_SIZE]
1264                     found = old_file
1265                     suite_type = x[2]
1266                     # need this for updating dsc_files in install()
1267                     dsc_files[dsc_file]["files id"] = x[3]
1268                     # See install() in process-accepted...
1269                     self.pkg.orig_tar_id = x[3]
1270                     self.pkg.orig_tar_gz = old_file
1271                     if suite_type == "legacy" or suite_type == "legacy-mixed":
1272                         self.pkg.orig_tar_location = "legacy"
1273                     else:
1274                         self.pkg.orig_tar_location = x[4]
1275                 else:
1276                     # Not there? Check the queue directories...
1277
1278                     in_unchecked = os.path.join(self.Cnf["Dir::Queue::Unchecked"],dsc_file)
1279                     # See process_it() in 'dak process-unchecked' for explanation of this
1280                     # in_unchecked check dropped by ajt 2007-08-28, how did that
1281                     # ever make sense?
1282                     if os.path.exists(in_unchecked) and False:
1283                         return (self.reject_message, in_unchecked)
1284                     else:
1285                         for directory in [ "Accepted", "New", "Byhand", "ProposedUpdates", "OldProposedUpdates" ]:
1286                             in_otherdir = os.path.join(self.Cnf["Dir::Queue::%s" % (directory)],dsc_file)
1287                             if os.path.exists(in_otherdir):
1288                                 in_otherdir_fh = utils.open_file(in_otherdir)
1289                                 actual_md5 = apt_pkg.md5sum(in_otherdir_fh)
1290                                 in_otherdir_fh.close()
1291                                 actual_size = os.stat(in_otherdir)[stat.ST_SIZE]
1292                                 found = in_otherdir
1293                                 self.pkg.orig_tar_gz = in_otherdir
1294
1295                     if not found:
1296                         self.reject("%s refers to %s, but I can't find it in the queue or in the pool." % (file, dsc_file))
1297                         self.pkg.orig_tar_gz = -1
1298                         continue
1299             else:
1300                 self.reject("%s refers to %s, but I can't find it in the queue." % (file, dsc_file))
1301                 continue
1302             if actual_md5 != dsc_files[dsc_file]["md5sum"]:
1303                 self.reject("md5sum for %s doesn't match %s." % (found, file))
1304             if actual_size != int(dsc_files[dsc_file]["size"]):
1305                 self.reject("size for %s doesn't match %s." % (found, file))
1306
1307         return (self.reject_message, None)
1308
1309     def do_query(self, query):
1310         """
1311         Executes a database query. Writes statistics / timing to stderr.
1312
1313         @type query: string
1314         @param query: database query string, passed unmodified
1315
1316         @return: db result
1317
1318         @warning: The query is passed B{unmodified}, so be careful what you use this for.
1319         """
1320         sys.stderr.write("query: \"%s\" ... " % (query))
1321         before = time.time()
1322         r = self.projectB.query(query)
1323         time_diff = time.time()-before
1324         sys.stderr.write("took %.3f seconds.\n" % (time_diff))
1325         return r