]> git.decadent.org.uk Git - dak.git/blob - daklib/queue.py
Merge commit 'mhy/master' into merge
[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 commands
40 import shutil
41 import textwrap
42 from types import *
43
44 import yaml
45
46 from dak_exceptions import *
47 from changes import *
48 from regexes import *
49 from config import Config
50 from holding import Holding
51 from dbconn import *
52 from summarystats import SummaryStats
53 from utils import parse_changes
54 from textutils import fix_maintainer
55 from binary import Binary
56
57 ###############################################################################
58
59 def get_type(f, session):
60     """
61     Get the file type of C{f}
62
63     @type f: dict
64     @param f: file entry from Changes object
65
66     @type session: SQLA Session
67     @param session: SQL Alchemy session object
68
69     @rtype: string
70     @return: filetype
71
72     """
73     # Determine the type
74     if f.has_key("dbtype"):
75         file_type = f["dbtype"]
76     elif f["type"] in [ "orig.tar.gz", "orig.tar.bz2", "tar.gz", "tar.bz2", "diff.gz", "diff.bz2", "dsc" ]:
77         file_type = "dsc"
78     else:
79         utils.fubar("invalid type (%s) for new.  Dazed, confused and sure as heck not continuing." % (file_type))
80
81     # Validate the override type
82     type_id = get_override_type(file_type, session)
83     if type_id is None:
84         utils.fubar("invalid type (%s) for new.  Say wha?" % (file_type))
85
86     return file_type
87
88 ################################################################################
89
90 # Determine what parts in a .changes are NEW
91
92 def determine_new(changes, files, warn=1):
93     """
94     Determine what parts in a C{changes} file are NEW.
95
96     @type changes: Upload.Pkg.changes dict
97     @param changes: Changes dictionary
98
99     @type files: Upload.Pkg.files dict
100     @param files: Files dictionary
101
102     @type warn: bool
103     @param warn: Warn if overrides are added for (old)stable
104
105     @rtype: dict
106     @return: dictionary of NEW components.
107
108     """
109     new = {}
110
111     session = DBConn().session()
112
113     # Build up a list of potentially new things
114     for name, f in files.items():
115         # Skip byhand elements
116         if f["type"] == "byhand":
117             continue
118         pkg = f["package"]
119         priority = f["priority"]
120         section = f["section"]
121         file_type = get_type(f, session)
122         component = f["component"]
123
124         if file_type == "dsc":
125             priority = "source"
126
127         if not new.has_key(pkg):
128             new[pkg] = {}
129             new[pkg]["priority"] = priority
130             new[pkg]["section"] = section
131             new[pkg]["type"] = file_type
132             new[pkg]["component"] = component
133             new[pkg]["files"] = []
134         else:
135             old_type = new[pkg]["type"]
136             if old_type != file_type:
137                 # source gets trumped by deb or udeb
138                 if old_type == "dsc":
139                     new[pkg]["priority"] = priority
140                     new[pkg]["section"] = section
141                     new[pkg]["type"] = file_type
142                     new[pkg]["component"] = component
143
144         new[pkg]["files"].append(name)
145
146         if f.has_key("othercomponents"):
147             new[pkg]["othercomponents"] = f["othercomponents"]
148
149     for suite in changes["suite"].keys():
150         for pkg in new.keys():
151             ql = get_override(pkg, suite, new[pkg]["component"], new[pkg]["type"], session)
152             if len(ql) > 0:
153                 for file_entry in new[pkg]["files"]:
154                     if files[file_entry].has_key("new"):
155                         del files[file_entry]["new"]
156                 del new[pkg]
157
158     if warn:
159         for s in ['stable', 'oldstable']:
160             if changes["suite"].has_key(s):
161                 print "WARNING: overrides will be added for %s!" % s
162         for pkg in new.keys():
163             if new[pkg].has_key("othercomponents"):
164                 print "WARNING: %s already present in %s distribution." % (pkg, new[pkg]["othercomponents"])
165
166     session.close()
167
168     return new
169
170 ################################################################################
171
172 def check_valid(new):
173     """
174     Check if section and priority for NEW packages exist in database.
175     Additionally does sanity checks:
176       - debian-installer packages have to be udeb (or source)
177       - non debian-installer packages can not be udeb
178       - source priority can only be assigned to dsc file types
179
180     @type new: dict
181     @param new: Dict of new packages with their section, priority and type.
182
183     """
184     for pkg in new.keys():
185         section_name = new[pkg]["section"]
186         priority_name = new[pkg]["priority"]
187         file_type = new[pkg]["type"]
188
189         section = get_section(section_name)
190         if section is None:
191             new[pkg]["section id"] = -1
192         else:
193             new[pkg]["section id"] = section.section_id
194
195         priority = get_priority(priority_name)
196         if priority is None:
197             new[pkg]["priority id"] = -1
198         else:
199             new[pkg]["priority id"] = priority.priority_id
200
201         # Sanity checks
202         di = section_name.find("debian-installer") != -1
203
204         # If d-i, we must be udeb and vice-versa
205         if     (di and file_type not in ("udeb", "dsc")) or \
206            (not di and file_type == "udeb"):
207             new[pkg]["section id"] = -1
208
209         # If dsc we need to be source and vice-versa
210         if (priority == "source" and file_type != "dsc") or \
211            (priority != "source" and file_type == "dsc"):
212             new[pkg]["priority id"] = -1
213
214 ###############################################################################
215
216 def lookup_uid_from_fingerprint(fpr, session):
217     uid = None
218     uid_name = ""
219     # This is a stupid default, but see the comments below
220     is_dm = False
221
222     user = get_uid_from_fingerprint(fpr, session)
223
224     if user is not None:
225         uid = user.uid
226         if user.name is None:
227             uid_name = ''
228         else:
229             uid_name = user.name
230
231         # Check the relevant fingerprint (which we have to have)
232         for f in user.fingerprint:
233             if f.fingerprint == fpr:
234                 is_dm = f.keyring.debian_maintainer
235                 break
236
237     return (uid, uid_name, is_dm)
238
239 ###############################################################################
240
241 # Used by Upload.check_timestamps
242 class TarTime(object):
243     def __init__(self, future_cutoff, past_cutoff):
244         self.reset()
245         self.future_cutoff = future_cutoff
246         self.past_cutoff = past_cutoff
247
248     def reset(self):
249         self.future_files = {}
250         self.ancient_files = {}
251
252     def callback(self, Kind, Name, Link, Mode, UID, GID, Size, MTime, Major, Minor):
253         if MTime > self.future_cutoff:
254             self.future_files[Name] = MTime
255         if MTime < self.past_cutoff:
256             self.ancient_files[Name] = MTime
257
258 ###############################################################################
259
260 class Upload(object):
261     """
262     Everything that has to do with an upload processed.
263
264     """
265     def __init__(self):
266         self.logger = None
267         self.pkg = Changes()
268         self.reset()
269
270     ###########################################################################
271
272     def reset (self):
273         """ Reset a number of internal variables."""
274
275         # Initialize the substitution template map
276         cnf = Config()
277         self.Subst = {}
278         self.Subst["__ADMIN_ADDRESS__"] = cnf["Dinstall::MyAdminAddress"]
279         self.Subst["__BUG_SERVER__"] = cnf["Dinstall::BugServer"]
280         self.Subst["__DISTRO__"] = cnf["Dinstall::MyDistribution"]
281         self.Subst["__DAK_ADDRESS__"] = cnf["Dinstall::MyEmailAddress"]
282
283         self.rejects = []
284         self.warnings = []
285         self.notes = []
286
287         self.pkg.reset()
288
289     def package_info(self):
290         msg = ''
291
292         if len(self.rejects) > 0:
293             msg += "Reject Reasons:\n"
294             msg += "\n".join(self.rejects)
295
296         if len(self.warnings) > 0:
297             msg += "Warnings:\n"
298             msg += "\n".join(self.warnings)
299
300         if len(self.notes) > 0:
301             msg += "Notes:\n"
302             msg += "\n".join(self.notes)
303
304         return msg
305
306     ###########################################################################
307     def update_subst(self):
308         """ Set up the per-package template substitution mappings """
309
310         cnf = Config()
311
312         # If 'dak process-unchecked' crashed out in the right place, architecture may still be a string.
313         if not self.pkg.changes.has_key("architecture") or not \
314            isinstance(self.pkg.changes["architecture"], DictType):
315             self.pkg.changes["architecture"] = { "Unknown" : "" }
316
317         # and maintainer2047 may not exist.
318         if not self.pkg.changes.has_key("maintainer2047"):
319             self.pkg.changes["maintainer2047"] = cnf["Dinstall::MyEmailAddress"]
320
321         self.Subst["__ARCHITECTURE__"] = " ".join(self.pkg.changes["architecture"].keys())
322         self.Subst["__CHANGES_FILENAME__"] = os.path.basename(self.pkg.changes_file)
323         self.Subst["__FILE_CONTENTS__"] = self.pkg.changes.get("filecontents", "")
324
325         # For source uploads the Changed-By field wins; otherwise Maintainer wins.
326         if self.pkg.changes["architecture"].has_key("source") and \
327            self.pkg.changes["changedby822"] != "" and \
328            (self.pkg.changes["changedby822"] != self.pkg.changes["maintainer822"]):
329
330             self.Subst["__MAINTAINER_FROM__"] = self.pkg.changes["changedby2047"]
331             self.Subst["__MAINTAINER_TO__"] = "%s, %s" % (self.pkg.changes["changedby2047"], self.pkg.changes["maintainer2047"])
332             self.Subst["__MAINTAINER__"] = self.pkg.changes.get("changed-by", "Unknown")
333         else:
334             self.Subst["__MAINTAINER_FROM__"] = self.pkg.changes["maintainer2047"]
335             self.Subst["__MAINTAINER_TO__"] = self.pkg.changes["maintainer2047"]
336             self.Subst["__MAINTAINER__"] = self.pkg.changes.get("maintainer", "Unknown")
337
338         if "sponsoremail" in self.pkg.changes:
339             self.Subst["__MAINTAINER_TO__"] += ", %s" % self.pkg.changes["sponsoremail"]
340
341         if cnf.has_key("Dinstall::TrackingServer") and self.pkg.changes.has_key("source"):
342             self.Subst["__MAINTAINER_TO__"] += "\nBcc: %s@%s" % (self.pkg.changes["source"], cnf["Dinstall::TrackingServer"])
343
344         # Apply any global override of the Maintainer field
345         if cnf.get("Dinstall::OverrideMaintainer"):
346             self.Subst["__MAINTAINER_TO__"] = cnf["Dinstall::OverrideMaintainer"]
347             self.Subst["__MAINTAINER_FROM__"] = cnf["Dinstall::OverrideMaintainer"]
348
349         self.Subst["__REJECT_MESSAGE__"] = self.package_info()
350         self.Subst["__SOURCE__"] = self.pkg.changes.get("source", "Unknown")
351         self.Subst["__VERSION__"] = self.pkg.changes.get("version", "Unknown")
352
353     ###########################################################################
354     def load_changes(self, filename):
355         """
356         @rtype: boolean
357         @rvalue: whether the changes file was valid or not.  We may want to
358                  reject even if this is True (see what gets put in self.rejects).
359                  This is simply to prevent us even trying things later which will
360                  fail because we couldn't properly parse the file.
361         """
362         Cnf = Config()
363         self.pkg.changes_file = filename
364
365         # Parse the .changes field into a dictionary
366         try:
367             self.pkg.changes.update(parse_changes(filename))
368         except CantOpenError:
369             self.rejects.append("%s: can't read file." % (filename))
370             return False
371         except ParseChangesError, line:
372             self.rejects.append("%s: parse error, can't grok: %s." % (filename, line))
373             return False
374         except ChangesUnicodeError:
375             self.rejects.append("%s: changes file not proper utf-8" % (filename))
376             return False
377
378         # Parse the Files field from the .changes into another dictionary
379         try:
380             self.pkg.files.update(utils.build_file_list(self.pkg.changes))
381         except ParseChangesError, line:
382             self.rejects.append("%s: parse error, can't grok: %s." % (filename, line))
383             return False
384         except UnknownFormatError, format:
385             self.rejects.append("%s: unknown format '%s'." % (filename, format))
386             return False
387
388         # Check for mandatory fields
389         for i in ("distribution", "source", "binary", "architecture",
390                   "version", "maintainer", "files", "changes", "description"):
391             if not self.pkg.changes.has_key(i):
392                 # Avoid undefined errors later
393                 self.rejects.append("%s: Missing mandatory field `%s'." % (filename, i))
394                 return False
395
396         # Strip a source version in brackets from the source field
397         if re_strip_srcver.search(self.pkg.changes["source"]):
398             self.pkg.changes["source"] = re_strip_srcver.sub('', self.pkg.changes["source"])
399
400         # Ensure the source field is a valid package name.
401         if not re_valid_pkg_name.match(self.pkg.changes["source"]):
402             self.rejects.append("%s: invalid source name '%s'." % (filename, self.pkg.changes["source"]))
403
404         # Split multi-value fields into a lower-level dictionary
405         for i in ("architecture", "distribution", "binary", "closes"):
406             o = self.pkg.changes.get(i, "")
407             if o != "":
408                 del self.pkg.changes[i]
409
410             self.pkg.changes[i] = {}
411
412             for j in o.split():
413                 self.pkg.changes[i][j] = 1
414
415         # Fix the Maintainer: field to be RFC822/2047 compatible
416         try:
417             (self.pkg.changes["maintainer822"],
418              self.pkg.changes["maintainer2047"],
419              self.pkg.changes["maintainername"],
420              self.pkg.changes["maintaineremail"]) = \
421                    fix_maintainer (self.pkg.changes["maintainer"])
422         except ParseMaintError, msg:
423             self.rejects.append("%s: Maintainer field ('%s') failed to parse: %s" \
424                    % (filename, changes["maintainer"], msg))
425
426         # ...likewise for the Changed-By: field if it exists.
427         try:
428             (self.pkg.changes["changedby822"],
429              self.pkg.changes["changedby2047"],
430              self.pkg.changes["changedbyname"],
431              self.pkg.changes["changedbyemail"]) = \
432                    fix_maintainer (self.pkg.changes.get("changed-by", ""))
433         except ParseMaintError, msg:
434             self.pkg.changes["changedby822"] = ""
435             self.pkg.changes["changedby2047"] = ""
436             self.pkg.changes["changedbyname"] = ""
437             self.pkg.changes["changedbyemail"] = ""
438
439             self.rejects.append("%s: Changed-By field ('%s') failed to parse: %s" \
440                    % (filename, changes["changed-by"], msg))
441
442         # Ensure all the values in Closes: are numbers
443         if self.pkg.changes.has_key("closes"):
444             for i in self.pkg.changes["closes"].keys():
445                 if re_isanum.match (i) == None:
446                     self.rejects.append(("%s: `%s' from Closes field isn't a number." % (filename, i)))
447
448         # chopversion = no epoch; chopversion2 = no epoch and no revision (e.g. for .orig.tar.gz comparison)
449         self.pkg.changes["chopversion"] = re_no_epoch.sub('', self.pkg.changes["version"])
450         self.pkg.changes["chopversion2"] = re_no_revision.sub('', self.pkg.changes["chopversion"])
451
452         # Check there isn't already a changes file of the same name in one
453         # of the queue directories.
454         base_filename = os.path.basename(filename)
455         for d in [ "Accepted", "Byhand", "Done", "New", "ProposedUpdates", "OldProposedUpdates" ]:
456             if os.path.exists(os.path.join(Cnf["Dir::Queue::%s" % (d) ], base_filename)):
457                 self.rejects.append("%s: a file with this name already exists in the %s directory." % (base_filename, d))
458
459         # Check the .changes is non-empty
460         if not self.pkg.files:
461             self.rejects.append("%s: nothing to do (Files field is empty)." % (base_filename))
462             return False
463
464         # Changes was syntactically valid even if we'll reject
465         return True
466
467     ###########################################################################
468
469     def check_distributions(self):
470         "Check and map the Distribution field"
471
472         Cnf = Config()
473
474         # Handle suite mappings
475         for m in Cnf.ValueList("SuiteMappings"):
476             args = m.split()
477             mtype = args[0]
478             if mtype == "map" or mtype == "silent-map":
479                 (source, dest) = args[1:3]
480                 if self.pkg.changes["distribution"].has_key(source):
481                     del self.pkg.changes["distribution"][source]
482                     self.pkg.changes["distribution"][dest] = 1
483                     if mtype != "silent-map":
484                         self.notes.append("Mapping %s to %s." % (source, dest))
485                 if self.pkg.changes.has_key("distribution-version"):
486                     if self.pkg.changes["distribution-version"].has_key(source):
487                         self.pkg.changes["distribution-version"][source]=dest
488             elif mtype == "map-unreleased":
489                 (source, dest) = args[1:3]
490                 if self.pkg.changes["distribution"].has_key(source):
491                     for arch in self.pkg.changes["architecture"].keys():
492                         if arch not in [ a.arch_string for a in get_suite_architectures(source) ]:
493                             self.notes.append("Mapping %s to %s for unreleased architecture %s." % (source, dest, arch))
494                             del self.pkg.changes["distribution"][source]
495                             self.pkg.changes["distribution"][dest] = 1
496                             break
497             elif mtype == "ignore":
498                 suite = args[1]
499                 if self.pkg.changes["distribution"].has_key(suite):
500                     del self.pkg.changes["distribution"][suite]
501                     self.warnings.append("Ignoring %s as a target suite." % (suite))
502             elif mtype == "reject":
503                 suite = args[1]
504                 if self.pkg.changes["distribution"].has_key(suite):
505                     self.rejects.append("Uploads to %s are not accepted." % (suite))
506             elif mtype == "propup-version":
507                 # give these as "uploaded-to(non-mapped) suites-to-add-when-upload-obsoletes"
508                 #
509                 # changes["distribution-version"] looks like: {'testing': 'testing-proposed-updates'}
510                 if self.pkg.changes["distribution"].has_key(args[1]):
511                     self.pkg.changes.setdefault("distribution-version", {})
512                     for suite in args[2:]:
513                         self.pkg.changes["distribution-version"][suite] = suite
514
515         # Ensure there is (still) a target distribution
516         if len(self.pkg.changes["distribution"].keys()) < 1:
517             self.rejects.append("No valid distribution remaining.")
518
519         # Ensure target distributions exist
520         for suite in self.pkg.changes["distribution"].keys():
521             if not Cnf.has_key("Suite::%s" % (suite)):
522                 self.rejects.append("Unknown distribution `%s'." % (suite))
523
524     ###########################################################################
525
526     def binary_file_checks(self, f, session):
527         cnf = Config()
528         entry = self.pkg.files[f]
529
530         # Extract package control information
531         deb_file = utils.open_file(f)
532         try:
533             control = apt_pkg.ParseSection(apt_inst.debExtractControl(deb_file))
534         except:
535             self.rejects.append("%s: debExtractControl() raised %s." % (f, sys.exc_type))
536             deb_file.close()
537             # Can't continue, none of the checks on control would work.
538             return
539
540         # Check for mandantory "Description:"
541         deb_file.seek(0)
542         try:
543             apt_pkg.ParseSection(apt_inst.debExtractControl(deb_file))["Description"] + '\n'
544         except:
545             self.rejects.append("%s: Missing Description in binary package" % (f))
546             return
547
548         deb_file.close()
549
550         # Check for mandatory fields
551         for field in [ "Package", "Architecture", "Version" ]:
552             if control.Find(field) == None:
553                 # Can't continue
554                 self.rejects.append("%s: No %s field in control." % (f, field))
555                 return
556
557         # Ensure the package name matches the one give in the .changes
558         if not self.pkg.changes["binary"].has_key(control.Find("Package", "")):
559             self.rejects.append("%s: control file lists name as `%s', which isn't in changes file." % (f, control.Find("Package", "")))
560
561         # Validate the package field
562         package = control.Find("Package")
563         if not re_valid_pkg_name.match(package):
564             self.rejects.append("%s: invalid package name '%s'." % (f, package))
565
566         # Validate the version field
567         version = control.Find("Version")
568         if not re_valid_version.match(version):
569             self.rejects.append("%s: invalid version number '%s'." % (f, version))
570
571         # Ensure the architecture of the .deb is one we know about.
572         default_suite = cnf.get("Dinstall::DefaultSuite", "Unstable")
573         architecture = control.Find("Architecture")
574         upload_suite = self.pkg.changes["distribution"].keys()[0]
575
576         if      architecture not in [a.arch_string for a in get_suite_architectures(default_suite, session)] \
577             and architecture not in [a.arch_string for a in get_suite_architectures(upload_suite, session)]:
578             self.rejects.append("Unknown architecture '%s'." % (architecture))
579
580         # Ensure the architecture of the .deb is one of the ones
581         # listed in the .changes.
582         if not self.pkg.changes["architecture"].has_key(architecture):
583             self.rejects.append("%s: control file lists arch as `%s', which isn't in changes file." % (f, architecture))
584
585         # Sanity-check the Depends field
586         depends = control.Find("Depends")
587         if depends == '':
588             self.rejects.append("%s: Depends field is empty." % (f))
589
590         # Sanity-check the Provides field
591         provides = control.Find("Provides")
592         if provides:
593             provide = re_spacestrip.sub('', provides)
594             if provide == '':
595                 self.rejects.append("%s: Provides field is empty." % (f))
596             prov_list = provide.split(",")
597             for prov in prov_list:
598                 if not re_valid_pkg_name.match(prov):
599                     self.rejects.append("%s: Invalid Provides field content %s." % (f, prov))
600
601         # Check the section & priority match those given in the .changes (non-fatal)
602         if     control.Find("Section") and entry["section"] != "" \
603            and entry["section"] != control.Find("Section"):
604             self.warnings.append("%s control file lists section as `%s', but changes file has `%s'." % \
605                                 (f, control.Find("Section", ""), entry["section"]))
606         if control.Find("Priority") and entry["priority"] != "" \
607            and entry["priority"] != control.Find("Priority"):
608             self.warnings.append("%s control file lists priority as `%s', but changes file has `%s'." % \
609                                 (f, control.Find("Priority", ""), entry["priority"]))
610
611         entry["package"] = package
612         entry["architecture"] = architecture
613         entry["version"] = version
614         entry["maintainer"] = control.Find("Maintainer", "")
615
616         if f.endswith(".udeb"):
617             self.pkg.files[f]["dbtype"] = "udeb"
618         elif f.endswith(".deb"):
619             self.pkg.files[f]["dbtype"] = "deb"
620         else:
621             self.rejects.append("%s is neither a .deb or a .udeb." % (f))
622
623         entry["source"] = control.Find("Source", entry["package"])
624
625         # Get the source version
626         source = entry["source"]
627         source_version = ""
628
629         if source.find("(") != -1:
630             m = re_extract_src_version.match(source)
631             source = m.group(1)
632             source_version = m.group(2)
633
634         if not source_version:
635             source_version = self.pkg.files[f]["version"]
636
637         entry["source package"] = source
638         entry["source version"] = source_version
639
640         # Ensure the filename matches the contents of the .deb
641         m = re_isadeb.match(f)
642
643         #  package name
644         file_package = m.group(1)
645         if entry["package"] != file_package:
646             self.rejects.append("%s: package part of filename (%s) does not match package name in the %s (%s)." % \
647                                 (f, file_package, entry["dbtype"], entry["package"]))
648         epochless_version = re_no_epoch.sub('', control.Find("Version"))
649
650         #  version
651         file_version = m.group(2)
652         if epochless_version != file_version:
653             self.rejects.append("%s: version part of filename (%s) does not match package version in the %s (%s)." % \
654                                 (f, file_version, entry["dbtype"], epochless_version))
655
656         #  architecture
657         file_architecture = m.group(3)
658         if entry["architecture"] != file_architecture:
659             self.rejects.append("%s: architecture part of filename (%s) does not match package architecture in the %s (%s)." % \
660                                 (f, file_architecture, entry["dbtype"], entry["architecture"]))
661
662         # Check for existent source
663         source_version = entry["source version"]
664         source_package = entry["source package"]
665         if self.pkg.changes["architecture"].has_key("source"):
666             if source_version != self.pkg.changes["version"]:
667                 self.rejects.append("source version (%s) for %s doesn't match changes version %s." % \
668                                     (source_version, f, self.pkg.changes["version"]))
669         else:
670             # Check in the SQL database
671             if not source_exists(source_package, source_version, self.pkg.changes["distribution"].keys(), session):
672                 # Check in one of the other directories
673                 source_epochless_version = re_no_epoch.sub('', source_version)
674                 dsc_filename = "%s_%s.dsc" % (source_package, source_epochless_version)
675                 if os.path.exists(os.path.join(cnf["Dir::Queue::Byhand"], dsc_filename)):
676                     entry["byhand"] = 1
677                 elif os.path.exists(os.path.join(cnf["Dir::Queue::New"], dsc_filename)):
678                     entry["new"] = 1
679                 else:
680                     dsc_file_exists = False
681                     for myq in ["Accepted", "Embargoed", "Unembargoed", "ProposedUpdates", "OldProposedUpdates"]:
682                         if cnf.has_key("Dir::Queue::%s" % (myq)):
683                             if os.path.exists(os.path.join(cnf["Dir::Queue::" + myq], dsc_filename)):
684                                 dsc_file_exists = True
685                                 break
686
687                     if not dsc_file_exists:
688                         self.rejects.append("no source found for %s %s (%s)." % (source_package, source_version, f))
689
690         # Check the version and for file overwrites
691         self.check_binary_against_db(f, session)
692
693         # Temporarily disable contents generation until we change the table storage layout
694         #b = Binary(f)
695         #b.scan_package()
696         #if len(b.rejects) > 0:
697         #    for j in b.rejects:
698         #        self.rejects.append(j)
699
700     def source_file_checks(self, f, session):
701         entry = self.pkg.files[f]
702
703         m = re_issource.match(f)
704         if not m:
705             return
706
707         entry["package"] = m.group(1)
708         entry["version"] = m.group(2)
709         entry["type"] = m.group(3)
710
711         # Ensure the source package name matches the Source filed in the .changes
712         if self.pkg.changes["source"] != entry["package"]:
713             self.rejects.append("%s: changes file doesn't say %s for Source" % (f, entry["package"]))
714
715         # Ensure the source version matches the version in the .changes file
716         if entry["type"] == "orig.tar.gz":
717             changes_version = self.pkg.changes["chopversion2"]
718         else:
719             changes_version = self.pkg.changes["chopversion"]
720
721         if changes_version != entry["version"]:
722             self.rejects.append("%s: should be %s according to changes file." % (f, changes_version))
723
724         # Ensure the .changes lists source in the Architecture field
725         if not self.pkg.changes["architecture"].has_key("source"):
726             self.rejects.append("%s: changes file doesn't list `source' in Architecture field." % (f))
727
728         # Check the signature of a .dsc file
729         if entry["type"] == "dsc":
730             # check_signature returns either:
731             #  (None, [list, of, rejects]) or (signature, [])
732             (self.pkg.dsc["fingerprint"], rejects) = utils.check_signature(f)
733             for j in rejects:
734                 self.rejects.append(j)
735
736         entry["architecture"] = "source"
737
738     def per_suite_file_checks(self, f, suite, session):
739         cnf = Config()
740         entry = self.pkg.files[f]
741         archive = utils.where_am_i()
742
743         # Skip byhand
744         if entry.has_key("byhand"):
745             return
746
747         # Check we have fields we need to do these checks
748         oktogo = True
749         for m in ['component', 'package', 'priority', 'size', 'md5sum']:
750             if not entry.has_key(m):
751                 self.rejects.append("file '%s' does not have field %s set" % (f, m))
752                 oktogo = False
753
754         if not oktogo:
755             return
756
757         # Handle component mappings
758         for m in cnf.ValueList("ComponentMappings"):
759             (source, dest) = m.split()
760             if entry["component"] == source:
761                 entry["original component"] = source
762                 entry["component"] = dest
763
764         # Ensure the component is valid for the target suite
765         if cnf.has_key("Suite:%s::Components" % (suite)) and \
766            entry["component"] not in cnf.ValueList("Suite::%s::Components" % (suite)):
767             self.rejects.append("unknown component `%s' for suite `%s'." % (entry["component"], suite))
768             return
769
770         # Validate the component
771         if not get_component(entry["component"], session):
772             self.rejects.append("file '%s' has unknown component '%s'." % (f, component))
773             return
774
775         # See if the package is NEW
776         if not self.in_override_p(entry["package"], entry["component"], suite, entry.get("dbtype",""), f, session):
777             entry["new"] = 1
778
779         # Validate the priority
780         if entry["priority"].find('/') != -1:
781             self.rejects.append("file '%s' has invalid priority '%s' [contains '/']." % (f, entry["priority"]))
782
783         # Determine the location
784         location = cnf["Dir::Pool"]
785         l = get_location(location, entry["component"], archive, session)
786         if l is None:
787             self.rejects.append("[INTERNAL ERROR] couldn't determine location (Component: %s, Archive: %s)" % (component, archive))
788             entry["location id"] = -1
789         else:
790             entry["location id"] = l.location_id
791
792         # Check the md5sum & size against existing files (if any)
793         entry["pool name"] = utils.poolify(self.pkg.changes["source"], entry["component"])
794
795         found, poolfile = check_poolfile(os.path.join(entry["pool name"], f),
796                                          entry["size"], entry["md5sum"], entry["location id"])
797
798         if found is None:
799             self.rejects.append("INTERNAL ERROR, get_files_id() returned multiple matches for %s." % (f))
800         elif found is False and poolfile is not None:
801             self.rejects.append("md5sum and/or size mismatch on existing copy of %s." % (f))
802         else:
803             if poolfile is None:
804                 entry["files id"] = None
805             else:
806                 entry["files id"] = poolfile.file_id
807
808         # Check for packages that have moved from one component to another
809         entry['suite'] = suite
810         res = get_binary_components(self.pkg.files[f]['package'], suite, entry["architecture"], session)
811         if res.rowcount > 0:
812             entry["othercomponents"] = res.fetchone()[0]
813
814     def check_files(self, action=True):
815         archive = utils.where_am_i()
816         file_keys = self.pkg.files.keys()
817         holding = Holding()
818         cnf = Config()
819
820         # XXX: As far as I can tell, this can no longer happen - see
821         #      comments by AJ in old revisions - mhy
822         # if reprocess is 2 we've already done this and we're checking
823         # things again for the new .orig.tar.gz.
824         # [Yes, I'm fully aware of how disgusting this is]
825         if action and self.reprocess < 2:
826             cwd = os.getcwd()
827             os.chdir(self.pkg.directory)
828             for f in file_keys:
829                 ret = holding.copy_to_holding(f)
830                 if ret is not None:
831                     # XXX: Should we bail out here or try and continue?
832                     self.rejects.append(ret)
833
834             os.chdir(cwd)
835
836         # Check there isn't already a .changes or .dak file of the same name in
837         # the proposed-updates "CopyChanges" or "CopyDotDak" storage directories.
838         # [NB: this check must be done post-suite mapping]
839         base_filename = os.path.basename(self.pkg.changes_file)
840         dot_dak_filename = base_filename[:-8] + ".dak"
841
842         for suite in self.pkg.changes["distribution"].keys():
843             copychanges = "Suite::%s::CopyChanges" % (suite)
844             if cnf.has_key(copychanges) and \
845                    os.path.exists(os.path.join(cnf[copychanges], base_filename)):
846                 self.rejects.append("%s: a file with this name already exists in %s" \
847                            % (base_filename, cnf[copychanges]))
848
849             copy_dot_dak = "Suite::%s::CopyDotDak" % (suite)
850             if cnf.has_key(copy_dot_dak) and \
851                    os.path.exists(os.path.join(cnf[copy_dot_dak], dot_dak_filename)):
852                 self.rejects.append("%s: a file with this name already exists in %s" \
853                            % (dot_dak_filename, Cnf[copy_dot_dak]))
854
855         self.reprocess = 0
856         has_binaries = False
857         has_source = False
858
859         session = DBConn().session()
860
861         for f, entry in self.pkg.files.items():
862             # Ensure the file does not already exist in one of the accepted directories
863             for d in [ "Accepted", "Byhand", "New", "ProposedUpdates", "OldProposedUpdates", "Embargoed", "Unembargoed" ]:
864                 if not cnf.has_key("Dir::Queue::%s" % (d)): continue
865                 if os.path.exists(cnf["Dir::Queue::%s" % (d) ] + '/' + f):
866                     self.rejects.append("%s file already exists in the %s directory." % (f, d))
867
868             if not re_taint_free.match(f):
869                 self.rejects.append("!!WARNING!! tainted filename: '%s'." % (f))
870
871             # Check the file is readable
872             if os.access(f, os.R_OK) == 0:
873                 # When running in -n, copy_to_holding() won't have
874                 # generated the reject_message, so we need to.
875                 if action:
876                     if os.path.exists(f):
877                         self.rejects.append("Can't read `%s'. [permission denied]" % (f))
878                     else:
879                         self.rejects.append("Can't read `%s'. [file not found]" % (f))
880                 entry["type"] = "unreadable"
881                 continue
882
883             # If it's byhand skip remaining checks
884             if entry["section"] == "byhand" or entry["section"][:4] == "raw-":
885                 entry["byhand"] = 1
886                 entry["type"] = "byhand"
887
888             # Checks for a binary package...
889             elif re_isadeb.match(f):
890                 has_binaries = True
891                 entry["type"] = "deb"
892
893                 # This routine appends to self.rejects/warnings as appropriate
894                 self.binary_file_checks(f, session)
895
896             # Checks for a source package...
897             elif re_issource.match(f):
898                 has_source = True
899
900                 # This routine appends to self.rejects/warnings as appropriate
901                 self.source_file_checks(f, session)
902
903             # Not a binary or source package?  Assume byhand...
904             else:
905                 entry["byhand"] = 1
906                 entry["type"] = "byhand"
907
908             # Per-suite file checks
909             entry["oldfiles"] = {}
910             for suite in self.pkg.changes["distribution"].keys():
911                 self.per_suite_file_checks(f, suite, session)
912
913         session.close()
914
915         # If the .changes file says it has source, it must have source.
916         if self.pkg.changes["architecture"].has_key("source"):
917             if not has_source:
918                 self.rejects.append("no source found and Architecture line in changes mention source.")
919
920             if not has_binaries and cnf.FindB("Dinstall::Reject::NoSourceOnly"):
921                 self.rejects.append("source only uploads are not supported.")
922
923     ###########################################################################
924     def check_dsc(self, action=True):
925         """Returns bool indicating whether or not the source changes are valid"""
926         # Ensure there is source to check
927         if not self.pkg.changes["architecture"].has_key("source"):
928             return True
929
930         # Find the .dsc
931         dsc_filename = None
932         for f, entry in self.pkg.files.items():
933             if entry["type"] == "dsc":
934                 if dsc_filename:
935                     self.rejects.append("can not process a .changes file with multiple .dsc's.")
936                     return False
937                 else:
938                     dsc_filename = f
939
940         # If there isn't one, we have nothing to do. (We have reject()ed the upload already)
941         if not dsc_filename:
942             self.rejects.append("source uploads must contain a dsc file")
943             return False
944
945         # Parse the .dsc file
946         try:
947             self.pkg.dsc.update(utils.parse_changes(dsc_filename, signing_rules=1))
948         except CantOpenError:
949             # if not -n copy_to_holding() will have done this for us...
950             if not action:
951                 self.rejects.append("%s: can't read file." % (dsc_filename))
952         except ParseChangesError, line:
953             self.rejects.append("%s: parse error, can't grok: %s." % (dsc_filename, line))
954         except InvalidDscError, line:
955             self.rejects.append("%s: syntax error on line %s." % (dsc_filename, line))
956         except ChangesUnicodeError:
957             self.rejects.append("%s: dsc file not proper utf-8." % (dsc_filename))
958
959         # Build up the file list of files mentioned by the .dsc
960         try:
961             self.pkg.dsc_files.update(utils.build_file_list(self.pkg.dsc, is_a_dsc=1))
962         except NoFilesFieldError:
963             self.rejects.append("%s: no Files: field." % (dsc_filename))
964             return False
965         except UnknownFormatError, format:
966             self.rejects.append("%s: unknown format '%s'." % (dsc_filename, format))
967             return False
968         except ParseChangesError, line:
969             self.rejects.append("%s: parse error, can't grok: %s." % (dsc_filename, line))
970             return False
971
972         # Enforce mandatory fields
973         for i in ("format", "source", "version", "binary", "maintainer", "architecture", "files"):
974             if not self.pkg.dsc.has_key(i):
975                 self.rejects.append("%s: missing mandatory field `%s'." % (dsc_filename, i))
976                 return False
977
978         # Validate the source and version fields
979         if not re_valid_pkg_name.match(self.pkg.dsc["source"]):
980             self.rejects.append("%s: invalid source name '%s'." % (dsc_filename, self.pkg.dsc["source"]))
981         if not re_valid_version.match(self.pkg.dsc["version"]):
982             self.rejects.append("%s: invalid version number '%s'." % (dsc_filename, self.pkg.dsc["version"]))
983
984         # Bumping the version number of the .dsc breaks extraction by stable's
985         # dpkg-source.  So let's not do that...
986         if self.pkg.dsc["format"] != "1.0":
987             self.rejects.append("%s: incompatible 'Format' version produced by a broken version of dpkg-dev 1.9.1{3,4}." % (dsc_filename))
988
989         # Validate the Maintainer field
990         try:
991             # We ignore the return value
992             fix_maintainer(self.pkg.dsc["maintainer"])
993         except ParseMaintError, msg:
994             self.rejects.append("%s: Maintainer field ('%s') failed to parse: %s" \
995                                  % (dsc_filename, self.pkg.dsc["maintainer"], msg))
996
997         # Validate the build-depends field(s)
998         for field_name in [ "build-depends", "build-depends-indep" ]:
999             field = self.pkg.dsc.get(field_name)
1000             if field:
1001                 # Check for broken dpkg-dev lossage...
1002                 if field.startswith("ARRAY"):
1003                     self.rejects.append("%s: invalid %s field produced by a broken version of dpkg-dev (1.10.11)" % \
1004                                         (dsc_filename, field_name.title()))
1005
1006                 # Have apt try to parse them...
1007                 try:
1008                     apt_pkg.ParseSrcDepends(field)
1009                 except:
1010                     self.rejects.append("%s: invalid %s field (can not be parsed by apt)." % (dsc_filename, field_name.title()))
1011
1012         # Ensure the version number in the .dsc matches the version number in the .changes
1013         epochless_dsc_version = re_no_epoch.sub('', self.pkg.dsc["version"])
1014         changes_version = self.pkg.files[dsc_filename]["version"]
1015
1016         if epochless_dsc_version != self.pkg.files[dsc_filename]["version"]:
1017             self.rejects.append("version ('%s') in .dsc does not match version ('%s') in .changes." % (epochless_dsc_version, changes_version))
1018
1019         # Ensure there is a .tar.gz in the .dsc file
1020         has_tar = False
1021         for f in self.pkg.dsc_files.keys():
1022             m = re_issource.match(f)
1023             if not m:
1024                 self.rejects.append("%s: %s in Files field not recognised as source." % (dsc_filename, f))
1025                 continue
1026             ftype = m.group(3)
1027             if ftype == "orig.tar.gz" or ftype == "tar.gz":
1028                 has_tar = True
1029
1030         if not has_tar:
1031             self.rejects.append("%s: no .tar.gz or .orig.tar.gz in 'Files' field." % (dsc_filename))
1032
1033         # Ensure source is newer than existing source in target suites
1034         session = DBConn().session()
1035         self.check_source_against_db(dsc_filename, session)
1036         self.check_dsc_against_db(dsc_filename, session)
1037         session.close()
1038
1039         return True
1040
1041     ###########################################################################
1042
1043     def get_changelog_versions(self, source_dir):
1044         """Extracts a the source package and (optionally) grabs the
1045         version history out of debian/changelog for the BTS."""
1046
1047         cnf = Config()
1048
1049         # Find the .dsc (again)
1050         dsc_filename = None
1051         for f in self.pkg.files.keys():
1052             if self.pkg.files[f]["type"] == "dsc":
1053                 dsc_filename = f
1054
1055         # If there isn't one, we have nothing to do. (We have reject()ed the upload already)
1056         if not dsc_filename:
1057             return
1058
1059         # Create a symlink mirror of the source files in our temporary directory
1060         for f in self.pkg.files.keys():
1061             m = re_issource.match(f)
1062             if m:
1063                 src = os.path.join(source_dir, f)
1064                 # If a file is missing for whatever reason, give up.
1065                 if not os.path.exists(src):
1066                     return
1067                 ftype = m.group(3)
1068                 if ftype == "orig.tar.gz" and self.pkg.orig_tar_gz:
1069                     continue
1070                 dest = os.path.join(os.getcwd(), f)
1071                 os.symlink(src, dest)
1072
1073         # If the orig.tar.gz is not a part of the upload, create a symlink to the
1074         # existing copy.
1075         if self.pkg.orig_tar_gz:
1076             dest = os.path.join(os.getcwd(), os.path.basename(self.pkg.orig_tar_gz))
1077             os.symlink(self.pkg.orig_tar_gz, dest)
1078
1079         # Extract the source
1080         cmd = "dpkg-source -sn -x %s" % (dsc_filename)
1081         (result, output) = commands.getstatusoutput(cmd)
1082         if (result != 0):
1083             self.rejects.append("'dpkg-source -x' failed for %s [return code: %s]." % (dsc_filename, result))
1084             self.rejects.append(utils.prefix_multi_line_string(output, " [dpkg-source output:] "), "")
1085             return
1086
1087         if not cnf.Find("Dir::Queue::BTSVersionTrack"):
1088             return
1089
1090         # Get the upstream version
1091         upstr_version = re_no_epoch.sub('', self.pkg.dsc["version"])
1092         if re_strip_revision.search(upstr_version):
1093             upstr_version = re_strip_revision.sub('', upstr_version)
1094
1095         # Ensure the changelog file exists
1096         changelog_filename = "%s-%s/debian/changelog" % (self.pkg.dsc["source"], upstr_version)
1097         if not os.path.exists(changelog_filename):
1098             self.rejects.append("%s: debian/changelog not found in extracted source." % (dsc_filename))
1099             return
1100
1101         # Parse the changelog
1102         self.pkg.dsc["bts changelog"] = ""
1103         changelog_file = utils.open_file(changelog_filename)
1104         for line in changelog_file.readlines():
1105             m = re_changelog_versions.match(line)
1106             if m:
1107                 self.pkg.dsc["bts changelog"] += line
1108         changelog_file.close()
1109
1110         # Check we found at least one revision in the changelog
1111         if not self.pkg.dsc["bts changelog"]:
1112             self.rejects.append("%s: changelog format not recognised (empty version tree)." % (dsc_filename))
1113
1114     def check_source(self):
1115         # XXX: I'm fairly sure reprocess == 2 can never happen
1116         #      AJT disabled the is_incoming check years ago - mhy
1117         #      We should probably scrap or rethink the whole reprocess thing
1118         # Bail out if:
1119         #    a) there's no source
1120         # or b) reprocess is 2 - we will do this check next time when orig.tar.gz is in 'files'
1121         # or c) the orig.tar.gz is MIA
1122         if not self.pkg.changes["architecture"].has_key("source") or self.reprocess == 2 \
1123            or self.pkg.orig_tar_gz == -1:
1124             return
1125
1126         tmpdir = utils.temp_dirname()
1127
1128         # Move into the temporary directory
1129         cwd = os.getcwd()
1130         os.chdir(tmpdir)
1131
1132         # Get the changelog version history
1133         self.get_changelog_versions(cwd)
1134
1135         # Move back and cleanup the temporary tree
1136         os.chdir(cwd)
1137
1138         try:
1139             shutil.rmtree(tmpdir)
1140         except OSError, e:
1141             if e.errno != errno.EACCES:
1142                 print "foobar"
1143                 utils.fubar("%s: couldn't remove tmp dir for source tree." % (self.pkg.dsc["source"]))
1144
1145             self.rejects.append("%s: source tree could not be cleanly removed." % (self.pkg.dsc["source"]))
1146             # We probably have u-r or u-w directories so chmod everything
1147             # and try again.
1148             cmd = "chmod -R u+rwx %s" % (tmpdir)
1149             result = os.system(cmd)
1150             if result != 0:
1151                 utils.fubar("'%s' failed with result %s." % (cmd, result))
1152             shutil.rmtree(tmpdir)
1153         except Exception, e:
1154             print "foobar2 (%s)" % e
1155             utils.fubar("%s: couldn't remove tmp dir for source tree." % (self.pkg.dsc["source"]))
1156
1157     ###########################################################################
1158     def ensure_hashes(self):
1159         # Make sure we recognise the format of the Files: field in the .changes
1160         format = self.pkg.changes.get("format", "0.0").split(".", 1)
1161         if len(format) == 2:
1162             format = int(format[0]), int(format[1])
1163         else:
1164             format = int(float(format[0])), 0
1165
1166         # We need to deal with the original changes blob, as the fields we need
1167         # might not be in the changes dict serialised into the .dak anymore.
1168         orig_changes = utils.parse_deb822(self.pkg.changes['filecontents'])
1169
1170         # Copy the checksums over to the current changes dict.  This will keep
1171         # the existing modifications to it intact.
1172         for field in orig_changes:
1173             if field.startswith('checksums-'):
1174                 self.pkg.changes[field] = orig_changes[field]
1175
1176         # Check for unsupported hashes
1177         for j in utils.check_hash_fields(".changes", self.pkg.changes):
1178             self.rejects.append(j)
1179
1180         for j in utils.check_hash_fields(".dsc", self.pkg.dsc):
1181             self.rejects.append(j)
1182
1183         # We have to calculate the hash if we have an earlier changes version than
1184         # the hash appears in rather than require it exist in the changes file
1185         for hashname, hashfunc, version in utils.known_hashes:
1186             # TODO: Move _ensure_changes_hash into this class
1187             for j in utils._ensure_changes_hash(self.pkg.changes, format, version, self.pkg.files, hashname, hashfunc):
1188                 self.rejects.append(j)
1189             if "source" in self.pkg.changes["architecture"]:
1190                 # TODO: Move _ensure_dsc_hash into this class
1191                 for j in utils._ensure_dsc_hash(self.pkg.dsc, self.pkg.dsc_files, hashname, hashfunc):
1192                     self.rejects.append(j)
1193
1194     def check_hashes(self):
1195         for m in utils.check_hash(".changes", self.pkg.files, "md5", apt_pkg.md5sum):
1196             self.rejects.append(m)
1197
1198         for m in utils.check_size(".changes", self.pkg.files):
1199             self.rejects.append(m)
1200
1201         for m in utils.check_hash(".dsc", self.pkg.dsc_files, "md5", apt_pkg.md5sum):
1202             self.rejects.append(m)
1203
1204         for m in utils.check_size(".dsc", self.pkg.dsc_files):
1205             self.rejects.append(m)
1206
1207         self.ensure_hashes()
1208
1209     ###########################################################################
1210     def check_urgency(self):
1211         cnf = Config()
1212         if self.pkg.changes["architecture"].has_key("source"):
1213             if not self.pkg.changes.has_key("urgency"):
1214                 self.pkg.changes["urgency"] = cnf["Urgency::Default"]
1215             self.pkg.changes["urgency"] = self.pkg.changes["urgency"].lower()
1216             if self.pkg.changes["urgency"] not in cnf.ValueList("Urgency::Valid"):
1217                 self.warnings.append("%s is not a valid urgency; it will be treated as %s by testing." % \
1218                                      (self.pkg.changes["urgency"], cnf["Urgency::Default"]))
1219                 self.pkg.changes["urgency"] = cnf["Urgency::Default"]
1220
1221     ###########################################################################
1222
1223     # Sanity check the time stamps of files inside debs.
1224     # [Files in the near future cause ugly warnings and extreme time
1225     #  travel can cause errors on extraction]
1226
1227     def check_timestamps(self):
1228         Cnf = Config()
1229
1230         future_cutoff = time.time() + int(Cnf["Dinstall::FutureTimeTravelGrace"])
1231         past_cutoff = time.mktime(time.strptime(Cnf["Dinstall::PastCutoffYear"],"%Y"))
1232         tar = TarTime(future_cutoff, past_cutoff)
1233
1234         for filename, entry in self.pkg.files.items():
1235             if entry["type"] == "deb":
1236                 tar.reset()
1237                 try:
1238                     deb_file = utils.open_file(filename)
1239                     apt_inst.debExtract(deb_file, tar.callback, "control.tar.gz")
1240                     deb_file.seek(0)
1241                     try:
1242                         apt_inst.debExtract(deb_file, tar.callback, "data.tar.gz")
1243                     except SystemError, e:
1244                         # If we can't find a data.tar.gz, look for data.tar.bz2 instead.
1245                         if not re.search(r"Cannot f[ui]nd chunk data.tar.gz$", str(e)):
1246                             raise
1247                         deb_file.seek(0)
1248                         apt_inst.debExtract(deb_file,tar.callback,"data.tar.bz2")
1249
1250                     deb_file.close()
1251
1252                     future_files = tar.future_files.keys()
1253                     if future_files:
1254                         num_future_files = len(future_files)
1255                         future_file = future_files[0]
1256                         future_date = tar.future_files[future_file]
1257                         self.rejects.append("%s: has %s file(s) with a time stamp too far into the future (e.g. %s [%s])."
1258                                % (filename, num_future_files, future_file, time.ctime(future_date)))
1259
1260                     ancient_files = tar.ancient_files.keys()
1261                     if ancient_files:
1262                         num_ancient_files = len(ancient_files)
1263                         ancient_file = ancient_files[0]
1264                         ancient_date = tar.ancient_files[ancient_file]
1265                         self.rejects.append("%s: has %s file(s) with a time stamp too ancient (e.g. %s [%s])."
1266                                % (filename, num_ancient_files, ancient_file, time.ctime(ancient_date)))
1267                 except:
1268                     self.rejects.append("%s: deb contents timestamp check failed [%s: %s]" % (filename, sys.exc_type, sys.exc_value))
1269
1270     ###########################################################################
1271     def check_transition(self, session):
1272         cnf = Config()
1273
1274         sourcepkg = self.pkg.changes["source"]
1275
1276         # No sourceful upload -> no need to do anything else, direct return
1277         # We also work with unstable uploads, not experimental or those going to some
1278         # proposed-updates queue
1279         if "source" not in self.pkg.changes["architecture"] or \
1280            "unstable" not in self.pkg.changes["distribution"]:
1281             return
1282
1283         # Also only check if there is a file defined (and existant) with
1284         # checks.
1285         transpath = cnf.get("Dinstall::Reject::ReleaseTransitions", "")
1286         if transpath == "" or not os.path.exists(transpath):
1287             return
1288
1289         # Parse the yaml file
1290         sourcefile = file(transpath, 'r')
1291         sourcecontent = sourcefile.read()
1292         try:
1293             transitions = yaml.load(sourcecontent)
1294         except yaml.YAMLError, msg:
1295             # This shouldn't happen, there is a wrapper to edit the file which
1296             # checks it, but we prefer to be safe than ending up rejecting
1297             # everything.
1298             utils.warn("Not checking transitions, the transitions file is broken: %s." % (msg))
1299             return
1300
1301         # Now look through all defined transitions
1302         for trans in transitions:
1303             t = transitions[trans]
1304             source = t["source"]
1305             expected = t["new"]
1306
1307             # Will be None if nothing is in testing.
1308             current = get_source_in_suite(source, "testing", session)
1309             if current is not None:
1310                 compare = apt_pkg.VersionCompare(current.version, expected)
1311
1312             if current is None or compare < 0:
1313                 # This is still valid, the current version in testing is older than
1314                 # the new version we wait for, or there is none in testing yet
1315
1316                 # Check if the source we look at is affected by this.
1317                 if sourcepkg in t['packages']:
1318                     # The source is affected, lets reject it.
1319
1320                     rejectmsg = "%s: part of the %s transition.\n\n" % (
1321                         sourcepkg, trans)
1322
1323                     if current is not None:
1324                         currentlymsg = "at version %s" % (current.version)
1325                     else:
1326                         currentlymsg = "not present in testing"
1327
1328                     rejectmsg += "Transition description: %s\n\n" % (t["reason"])
1329
1330                     rejectmsg += "\n".join(textwrap.wrap("""Your package
1331 is part of a testing transition designed to get %s migrated (it is
1332 currently %s, we need version %s).  This transition is managed by the
1333 Release Team, and %s is the Release-Team member responsible for it.
1334 Please mail debian-release@lists.debian.org or contact %s directly if you
1335 need further assistance.  You might want to upload to experimental until this
1336 transition is done."""
1337                             % (source, currentlymsg, expected,t["rm"], t["rm"])))
1338
1339                     self.rejects.append(rejectmsg)
1340                     return
1341
1342     ###########################################################################
1343     def check_signed_by_key(self):
1344         """Ensure the .changes is signed by an authorized uploader."""
1345         session = DBConn().session()
1346
1347         self.check_transition(session)
1348
1349         (uid, uid_name, is_dm) = lookup_uid_from_fingerprint(self.pkg.changes["fingerprint"], session=session)
1350
1351         # match claimed name with actual name:
1352         if uid is None:
1353             # This is fundamentally broken but need us to refactor how we get
1354             # the UIDs/Fingerprints in order for us to fix it properly
1355             uid, uid_email = self.pkg.changes["fingerprint"], uid
1356             may_nmu, may_sponsor = 1, 1
1357             # XXX by default new dds don't have a fingerprint/uid in the db atm,
1358             #     and can't get one in there if we don't allow nmu/sponsorship
1359         elif is_dm is False:
1360             # If is_dm is False, we allow full upload rights
1361             uid_email = "%s@debian.org" % (uid)
1362             may_nmu, may_sponsor = 1, 1
1363         else:
1364             # Assume limited upload rights unless we've discovered otherwise
1365             uid_email = uid
1366             may_nmu, may_sponsor = 0, 0
1367
1368         if uid_email in [self.pkg.changes["maintaineremail"], self.pkg.changes["changedbyemail"]]:
1369             sponsored = 0
1370         elif uid_name in [self.pkg.changes["maintainername"], self.pkg.changes["changedbyname"]]:
1371             sponsored = 0
1372             if uid_name == "": sponsored = 1
1373         else:
1374             sponsored = 1
1375             if ("source" in self.pkg.changes["architecture"] and
1376                 uid_email and utils.is_email_alias(uid_email)):
1377                 sponsor_addresses = utils.gpg_get_key_addresses(self.pkg.changes["fingerprint"])
1378                 if (self.pkg.changes["maintaineremail"] not in sponsor_addresses and
1379                     self.pkg.changes["changedbyemail"] not in sponsor_addresses):
1380                     self.pkg.changes["sponsoremail"] = uid_email
1381
1382         if sponsored and not may_sponsor:
1383             self.rejects.append("%s is not authorised to sponsor uploads" % (uid))
1384
1385         if not sponsored and not may_nmu:
1386             should_reject = True
1387             highest_sid, highest_version = None, None
1388
1389             # XXX: This reimplements in SQLA what existed before but it's fundamentally fucked
1390             #      It ignores higher versions with the dm_upload_allowed flag set to false
1391             #      I'm keeping the existing behaviour for now until I've gone back and
1392             #      checked exactly what the GR says - mhy
1393             for si in get_sources_from_name(source=self.pkg.changes['source'], dm_upload_allowed=True, session=session):
1394                 if highest_version is None or apt_pkg.VersionCompare(si.version, highest_version) == 1:
1395                      highest_sid = si.source_id
1396                      highest_version = si.version
1397
1398             if highest_sid is None:
1399                 self.rejects.append("Source package %s does not have 'DM-Upload-Allowed: yes' in its most recent version" % self.pkg.changes["source"])
1400             else:
1401                 for sup in session.query(SrcUploader).join(DBSource).filter_by(source_id=highest_sid):
1402                     (rfc822, rfc2047, name, email) = sup.maintainer.get_split_maintainer()
1403                     if email == uid_email or name == uid_name:
1404                         should_reject = False
1405                         break
1406
1407             if should_reject is True:
1408                 self.rejects.append("%s is not in Maintainer or Uploaders of source package %s" % (uid, self.pkg.changes["source"]))
1409
1410             for b in self.pkg.changes["binary"].keys():
1411                 for suite in self.pkg.changes["distribution"].keys():
1412                     q = session.query(DBSource)
1413                     q = q.join(DBBinary).filter_by(package=b)
1414                     q = q.join(BinAssociation).join(Suite).filter_by(suite_name=suite)
1415
1416                     for s in q.all():
1417                         if s.source != self.pkg.changes["source"]:
1418                             self.rejects.append("%s may not hijack %s from source package %s in suite %s" % (uid, b, s, suite))
1419
1420             for f in self.pkg.files.keys():
1421                 if self.pkg.files[f].has_key("byhand"):
1422                     self.rejects.append("%s may not upload BYHAND file %s" % (uid, f))
1423                 if self.pkg.files[f].has_key("new"):
1424                     self.rejects.append("%s may not upload NEW file %s" % (uid, f))
1425
1426         session.close()
1427
1428     ###########################################################################
1429     def build_summaries(self):
1430         """ Build a summary of changes the upload introduces. """
1431
1432         (byhand, new, summary, override_summary) = self.pkg.file_summary()
1433
1434         short_summary = summary
1435
1436         # This is for direport's benefit...
1437         f = re_fdnic.sub("\n .\n", self.pkg.changes.get("changes", ""))
1438
1439         if byhand or new:
1440             summary += "Changes: " + f
1441
1442         summary += "\n\nOverride entries for your package:\n" + override_summary + "\n"
1443
1444         summary += self.announce(short_summary, 0)
1445
1446         return (summary, short_summary)
1447
1448     ###########################################################################
1449
1450     def close_bugs(self, summary, action):
1451         """
1452         Send mail to close bugs as instructed by the closes field in the changes file.
1453         Also add a line to summary if any work was done.
1454
1455         @type summary: string
1456         @param summary: summary text, as given by L{build_summaries}
1457
1458         @type action: bool
1459         @param action: Set to false no real action will be done.
1460
1461         @rtype: string
1462         @return: summary. If action was taken, extended by the list of closed bugs.
1463
1464         """
1465
1466         template = os.path.join(Config()["Dir::Templates"], 'process-unchecked.bug-close')
1467
1468         bugs = self.pkg.changes["closes"].keys()
1469
1470         if not bugs:
1471             return summary
1472
1473         bugs.sort()
1474         summary += "Closing bugs: "
1475         for bug in bugs:
1476             summary += "%s " % (bug)
1477             if action:
1478                 self.update_subst()
1479                 self.Subst["__BUG_NUMBER__"] = bug
1480                 if self.pkg.changes["distribution"].has_key("stable"):
1481                     self.Subst["__STABLE_WARNING__"] = """
1482 Note that this package is not part of the released stable Debian
1483 distribution.  It may have dependencies on other unreleased software,
1484 or other instabilities.  Please take care if you wish to install it.
1485 The update will eventually make its way into the next released Debian
1486 distribution."""
1487                 else:
1488                     self.Subst["__STABLE_WARNING__"] = ""
1489                 mail_message = utils.TemplateSubst(self.Subst, template)
1490                 utils.send_mail(mail_message)
1491
1492                 # Clear up after ourselves
1493                 del self.Subst["__BUG_NUMBER__"]
1494                 del self.Subst["__STABLE_WARNING__"]
1495
1496         if action and self.logger:
1497             self.logger.log(["closing bugs"] + bugs)
1498
1499         summary += "\n"
1500
1501         return summary
1502
1503     ###########################################################################
1504
1505     def announce(self, short_summary, action):
1506         """
1507         Send an announce mail about a new upload.
1508
1509         @type short_summary: string
1510         @param short_summary: Short summary text to include in the mail
1511
1512         @type action: bool
1513         @param action: Set to false no real action will be done.
1514
1515         @rtype: string
1516         @return: Textstring about action taken.
1517
1518         """
1519
1520         cnf = Config()
1521         announcetemplate = os.path.join(cnf["Dir::Templates"], 'process-unchecked.announce')
1522
1523         # Only do announcements for source uploads with a recent dpkg-dev installed
1524         if float(self.pkg.changes.get("format", 0)) < 1.6 or not \
1525            self.pkg.changes["architecture"].has_key("source"):
1526             return ""
1527
1528         lists_done = {}
1529         summary = ""
1530
1531         self.Subst["__SHORT_SUMMARY__"] = short_summary
1532
1533         for dist in self.pkg.changes["distribution"].keys():
1534             announce_list = cnf.Find("Suite::%s::Announce" % (dist))
1535             if announce_list == "" or lists_done.has_key(announce_list):
1536                 continue
1537
1538             lists_done[announce_list] = 1
1539             summary += "Announcing to %s\n" % (announce_list)
1540
1541             if action:
1542                 self.update_subst()
1543                 self.Subst["__ANNOUNCE_LIST_ADDRESS__"] = announce_list
1544                 if cnf.get("Dinstall::TrackingServer") and \
1545                    self.pkg.changes["architecture"].has_key("source"):
1546                     trackingsendto = "Bcc: %s@%s" % (self.pkg.changes["source"], cnf["Dinstall::TrackingServer"])
1547                     self.Subst["__ANNOUNCE_LIST_ADDRESS__"] += "\n" + trackingsendto
1548
1549                 mail_message = utils.TemplateSubst(self.Subst, announcetemplate)
1550                 utils.send_mail(mail_message)
1551
1552                 del self.Subst["__ANNOUNCE_LIST_ADDRESS__"]
1553
1554         if cnf.FindB("Dinstall::CloseBugs"):
1555             summary = self.close_bugs(summary, action)
1556
1557         del self.Subst["__SHORT_SUMMARY__"]
1558
1559         return summary
1560
1561     ###########################################################################
1562
1563     def accept (self, summary, short_summary, targetdir=None):
1564         """
1565         Accept an upload.
1566
1567         This moves all files referenced from the .changes into the I{accepted}
1568         queue, sends the accepted mail, announces to lists, closes bugs and
1569         also checks for override disparities. If enabled it will write out
1570         the version history for the BTS Version Tracking and will finally call
1571         L{queue_build}.
1572
1573         @type summary: string
1574         @param summary: Summary text
1575
1576         @type short_summary: string
1577         @param short_summary: Short summary
1578
1579         """
1580
1581         cnf = Config()
1582         stats = SummaryStats()
1583
1584         accepttemplate = os.path.join(cnf["Dir::Templates"], 'process-unchecked.accepted')
1585
1586         if targetdir is None:
1587             targetdir = cnf["Dir::Queue::Accepted"]
1588
1589         print "Accepting."
1590         if self.logger:
1591             self.logger.log(["Accepting changes", self.pkg.changes_file])
1592
1593         self.pkg.write_dot_dak(targetdir)
1594
1595         # Move all the files into the accepted directory
1596         utils.move(self.pkg.changes_file, targetdir)
1597
1598         for name, entry in sorted(self.pkg.files.items()):
1599             utils.move(name, targetdir)
1600             stats.accept_bytes += float(entry["size"])
1601
1602         stats.accept_count += 1
1603
1604         # Send accept mail, announce to lists, close bugs and check for
1605         # override disparities
1606         if not cnf["Dinstall::Options::No-Mail"]:
1607             self.update_subst()
1608             self.Subst["__SUITE__"] = ""
1609             self.Subst["__SUMMARY__"] = summary
1610             mail_message = utils.TemplateSubst(self.Subst, accepttemplate)
1611             utils.send_mail(mail_message)
1612             self.announce(short_summary, 1)
1613
1614         ## Helper stuff for DebBugs Version Tracking
1615         if cnf.Find("Dir::Queue::BTSVersionTrack"):
1616             # ??? once queue/* is cleared on *.d.o and/or reprocessed
1617             # the conditionalization on dsc["bts changelog"] should be
1618             # dropped.
1619
1620             # Write out the version history from the changelog
1621             if self.pkg.changes["architecture"].has_key("source") and \
1622                self.pkg.dsc.has_key("bts changelog"):
1623
1624                 (fd, temp_filename) = utils.temp_filename(cnf["Dir::Queue::BTSVersionTrack"], prefix=".")
1625                 version_history = os.fdopen(fd, 'w')
1626                 version_history.write(self.pkg.dsc["bts changelog"])
1627                 version_history.close()
1628                 filename = "%s/%s" % (cnf["Dir::Queue::BTSVersionTrack"],
1629                                       self.pkg.changes_file[:-8]+".versions")
1630                 os.rename(temp_filename, filename)
1631                 os.chmod(filename, 0644)
1632
1633             # Write out the binary -> source mapping.
1634             (fd, temp_filename) = utils.temp_filename(cnf["Dir::Queue::BTSVersionTrack"], prefix=".")
1635             debinfo = os.fdopen(fd, 'w')
1636             for name, entry in sorted(self.pkg.files.items()):
1637                 if entry["type"] == "deb":
1638                     line = " ".join([entry["package"], entry["version"],
1639                                      entry["architecture"], entry["source package"],
1640                                      entry["source version"]])
1641                     debinfo.write(line+"\n")
1642             debinfo.close()
1643             filename = "%s/%s" % (cnf["Dir::Queue::BTSVersionTrack"],
1644                                   self.pkg.changes_file[:-8]+".debinfo")
1645             os.rename(temp_filename, filename)
1646             os.chmod(filename, 0644)
1647
1648         # Its is Cnf["Dir::Queue::Accepted"] here, not targetdir!
1649         # <Ganneff> we do call queue_build too
1650         # <mhy> well yes, we'd have had to if we were inserting into accepted
1651         # <Ganneff> now. thats database only.
1652         # <mhy> urgh, that's going to get messy
1653         # <Ganneff> so i make the p-n call to it *also* using accepted/
1654         # <mhy> but then the packages will be in the queue_build table without the files being there
1655         # <Ganneff> as the buildd queue is only regenerated whenever unchecked runs
1656         # <mhy> ah, good point
1657         # <Ganneff> so it will work out, as unchecked move it over
1658         # <mhy> that's all completely sick
1659         # <Ganneff> yes
1660
1661         # This routine returns None on success or an error on failure
1662         res = get_queue('accepted').autobuild_upload(self.pkg, cnf["Dir::Queue::Accepted"])
1663         if res:
1664             utils.fubar(res)
1665
1666
1667     def check_override(self):
1668         """
1669         Checks override entries for validity. Mails "Override disparity" warnings,
1670         if that feature is enabled.
1671
1672         Abandons the check if
1673           - override disparity checks are disabled
1674           - mail sending is disabled
1675         """
1676
1677         cnf = Config()
1678
1679         # Abandon the check if:
1680         #  a) override disparity checks have been disabled
1681         #  b) we're not sending mail
1682         if not cnf.FindB("Dinstall::OverrideDisparityCheck") or \
1683            cnf["Dinstall::Options::No-Mail"]:
1684             return
1685
1686         summary = self.pkg.check_override()
1687
1688         if summary == "":
1689             return
1690
1691         overridetemplate = os.path.join(cnf["Dir::Templates"], 'process-unchecked.override-disparity')
1692
1693         self.update_subst()
1694         self.Subst["__SUMMARY__"] = summary
1695         mail_message = utils.TemplateSubst(self.Subst, overridetemplate)
1696         utils.send_mail(mail_message)
1697         del self.Subst["__SUMMARY__"]
1698
1699     ###########################################################################
1700
1701     def remove(self, dir=None):
1702         """
1703         Used (for instance) in p-u to remove the package from unchecked
1704         """
1705         if dir is None:
1706             os.chdir(self.pkg.directory)
1707         else:
1708             os.chdir(dir)
1709
1710         for f in self.pkg.files.keys():
1711             os.unlink(f)
1712         os.unlink(self.pkg.changes_file)
1713
1714     ###########################################################################
1715
1716     def move_to_dir (self, dest, perms=0660, changesperms=0664):
1717         """
1718         Move files to dest with certain perms/changesperms
1719         """
1720         utils.move(self.pkg.changes_file, dest, perms=changesperms)
1721         for f in self.pkg.files.keys():
1722             utils.move(f, dest, perms=perms)
1723
1724     ###########################################################################
1725
1726     def force_reject(self, reject_files):
1727         """
1728         Forcefully move files from the current directory to the
1729         reject directory.  If any file already exists in the reject
1730         directory it will be moved to the morgue to make way for
1731         the new file.
1732
1733         @type files: dict
1734         @param files: file dictionary
1735
1736         """
1737
1738         cnf = Config()
1739
1740         for file_entry in reject_files:
1741             # Skip any files which don't exist or which we don't have permission to copy.
1742             if os.access(file_entry, os.R_OK) == 0:
1743                 continue
1744
1745             dest_file = os.path.join(cnf["Dir::Queue::Reject"], file_entry)
1746
1747             try:
1748                 dest_fd = os.open(dest_file, os.O_RDWR | os.O_CREAT | os.O_EXCL, 0644)
1749             except OSError, e:
1750                 # File exists?  Let's try and move it to the morgue
1751                 if e.errno == errno.EEXIST:
1752                     morgue_file = os.path.join(cnf["Dir::Morgue"], cnf["Dir::MorgueReject"], file_entry)
1753                     try:
1754                         morgue_file = utils.find_next_free(morgue_file)
1755                     except NoFreeFilenameError:
1756                         # Something's either gone badly Pete Tong, or
1757                         # someone is trying to exploit us.
1758                         utils.warn("**WARNING** failed to move %s from the reject directory to the morgue." % (file_entry))
1759                         return
1760                     utils.move(dest_file, morgue_file, perms=0660)
1761                     try:
1762                         dest_fd = os.open(dest_file, os.O_RDWR|os.O_CREAT|os.O_EXCL, 0644)
1763                     except OSError, e:
1764                         # Likewise
1765                         utils.warn("**WARNING** failed to claim %s in the reject directory." % (file_entry))
1766                         return
1767                 else:
1768                     raise
1769             # If we got here, we own the destination file, so we can
1770             # safely overwrite it.
1771             utils.move(file_entry, dest_file, 1, perms=0660)
1772             os.close(dest_fd)
1773
1774     ###########################################################################
1775     def do_reject (self, manual=0, reject_message="", note=""):
1776         """
1777         Reject an upload. If called without a reject message or C{manual} is
1778         true, spawn an editor so the user can write one.
1779
1780         @type manual: bool
1781         @param manual: manual or automated rejection
1782
1783         @type reject_message: string
1784         @param reject_message: A reject message
1785
1786         @return: 0
1787
1788         """
1789         # If we weren't given a manual rejection message, spawn an
1790         # editor so the user can add one in...
1791         if manual and not reject_message:
1792             (fd, temp_filename) = utils.temp_filename()
1793             temp_file = os.fdopen(fd, 'w')
1794             if len(note) > 0:
1795                 for line in note:
1796                     temp_file.write(line)
1797             temp_file.close()
1798             editor = os.environ.get("EDITOR","vi")
1799             answer = 'E'
1800             while answer == 'E':
1801                 os.system("%s %s" % (editor, temp_filename))
1802                 temp_fh = utils.open_file(temp_filename)
1803                 reject_message = "".join(temp_fh.readlines())
1804                 temp_fh.close()
1805                 print "Reject message:"
1806                 print utils.prefix_multi_line_string(reject_message,"  ",include_blank_lines=1)
1807                 prompt = "[R]eject, Edit, Abandon, Quit ?"
1808                 answer = "XXX"
1809                 while prompt.find(answer) == -1:
1810                     answer = utils.our_raw_input(prompt)
1811                     m = re_default_answer.search(prompt)
1812                     if answer == "":
1813                         answer = m.group(1)
1814                     answer = answer[:1].upper()
1815             os.unlink(temp_filename)
1816             if answer == 'A':
1817                 return 1
1818             elif answer == 'Q':
1819                 sys.exit(0)
1820
1821         print "Rejecting.\n"
1822
1823         cnf = Config()
1824
1825         reason_filename = self.pkg.changes_file[:-8] + ".reason"
1826         reason_filename = os.path.join(cnf["Dir::Queue::Reject"], reason_filename)
1827
1828         # Move all the files into the reject directory
1829         reject_files = self.pkg.files.keys() + [self.pkg.changes_file]
1830         self.force_reject(reject_files)
1831
1832         # If we fail here someone is probably trying to exploit the race
1833         # so let's just raise an exception ...
1834         if os.path.exists(reason_filename):
1835             os.unlink(reason_filename)
1836         reason_fd = os.open(reason_filename, os.O_RDWR|os.O_CREAT|os.O_EXCL, 0644)
1837
1838         rej_template = os.path.join(cnf["Dir::Templates"], "queue.rejected")
1839
1840         self.update_subst()
1841         if not manual:
1842             self.Subst["__REJECTOR_ADDRESS__"] = cnf["Dinstall::MyEmailAddress"]
1843             self.Subst["__MANUAL_REJECT_MESSAGE__"] = ""
1844             self.Subst["__CC__"] = "X-DAK-Rejection: automatic (moo)\nX-Katie-Rejection: automatic (moo)"
1845             os.write(reason_fd, reject_message)
1846             reject_mail_message = utils.TemplateSubst(self.Subst, rej_template)
1847         else:
1848             # Build up the rejection email
1849             user_email_address = utils.whoami() + " <%s>" % (cnf["Dinstall::MyAdminAddress"])
1850             self.Subst["__REJECTOR_ADDRESS__"] = user_email_address
1851             self.Subst["__MANUAL_REJECT_MESSAGE__"] = reject_message
1852             self.Subst["__CC__"] = "Cc: " + cnf["Dinstall::MyEmailAddress"]
1853             reject_mail_message = utils.TemplateSubst(self.Subst, rej_template)
1854             # Write the rejection email out as the <foo>.reason file
1855             os.write(reason_fd, reject_mail_message)
1856
1857         del self.Subst["__REJECTOR_ADDRESS__"]
1858         del self.Subst["__MANUAL_REJECT_MESSAGE__"]
1859         del self.Subst["__CC__"]
1860
1861         os.close(reason_fd)
1862
1863         # Send the rejection mail if appropriate
1864         if not cnf["Dinstall::Options::No-Mail"]:
1865             utils.send_mail(reject_mail_message)
1866
1867         if self.logger:
1868             self.logger.log(["rejected", self.pkg.changes_file])
1869
1870         return 0
1871
1872     ################################################################################
1873     def in_override_p(self, package, component, suite, binary_type, file, session):
1874         """
1875         Check if a package already has override entries in the DB
1876
1877         @type package: string
1878         @param package: package name
1879
1880         @type component: string
1881         @param component: database id of the component
1882
1883         @type suite: int
1884         @param suite: database id of the suite
1885
1886         @type binary_type: string
1887         @param binary_type: type of the package
1888
1889         @type file: string
1890         @param file: filename we check
1891
1892         @return: the database result. But noone cares anyway.
1893
1894         """
1895
1896         cnf = Config()
1897
1898         if binary_type == "": # must be source
1899             file_type = "dsc"
1900         else:
1901             file_type = binary_type
1902
1903         # Override suite name; used for example with proposed-updates
1904         if cnf.Find("Suite::%s::OverrideSuite" % (suite)) != "":
1905             suite = cnf["Suite::%s::OverrideSuite" % (suite)]
1906
1907         result = get_override(package, suite, component, file_type, session)
1908
1909         # If checking for a source package fall back on the binary override type
1910         if file_type == "dsc" and len(result) < 1:
1911             result = get_override(package, suite, component, ['deb', 'udeb'], session)
1912
1913         # Remember the section and priority so we can check them later if appropriate
1914         if len(result) > 0:
1915             result = result[0]
1916             self.pkg.files[file]["override section"] = result.section.section
1917             self.pkg.files[file]["override priority"] = result.priority.priority
1918             return result
1919
1920         return None
1921
1922     ################################################################################
1923     def get_anyversion(self, sv_list, suite):
1924         """
1925         @type sv_list: list
1926         @param sv_list: list of (suite, version) tuples to check
1927
1928         @type suite: string
1929         @param suite: suite name
1930
1931         Description: TODO
1932         """
1933         Cnf = Config()
1934         anyversion = None
1935         anysuite = [suite] + Cnf.ValueList("Suite::%s::VersionChecks::Enhances" % (suite))
1936         for (s, v) in sv_list:
1937             if s in [ x.lower() for x in anysuite ]:
1938                 if not anyversion or apt_pkg.VersionCompare(anyversion, v) <= 0:
1939                     anyversion = v
1940
1941         return anyversion
1942
1943     ################################################################################
1944
1945     def cross_suite_version_check(self, sv_list, file, new_version, sourceful=False):
1946         """
1947         @type sv_list: list
1948         @param sv_list: list of (suite, version) tuples to check
1949
1950         @type file: string
1951         @param file: XXX
1952
1953         @type new_version: string
1954         @param new_version: XXX
1955
1956         Ensure versions are newer than existing packages in target
1957         suites and that cross-suite version checking rules as
1958         set out in the conf file are satisfied.
1959         """
1960
1961         cnf = Config()
1962
1963         # Check versions for each target suite
1964         for target_suite in self.pkg.changes["distribution"].keys():
1965             must_be_newer_than = [ i.lower() for i in cnf.ValueList("Suite::%s::VersionChecks::MustBeNewerThan" % (target_suite)) ]
1966             must_be_older_than = [ i.lower() for i in cnf.ValueList("Suite::%s::VersionChecks::MustBeOlderThan" % (target_suite)) ]
1967
1968             # Enforce "must be newer than target suite" even if conffile omits it
1969             if target_suite not in must_be_newer_than:
1970                 must_be_newer_than.append(target_suite)
1971
1972             for (suite, existent_version) in sv_list:
1973                 vercmp = apt_pkg.VersionCompare(new_version, existent_version)
1974
1975                 if suite in must_be_newer_than and sourceful and vercmp < 1:
1976                     self.rejects.append("%s: old version (%s) in %s >= new version (%s) targeted at %s." % (file, existent_version, suite, new_version, target_suite))
1977
1978                 if suite in must_be_older_than and vercmp > -1:
1979                     cansave = 0
1980
1981                     if self.pkg.changes.get('distribution-version', {}).has_key(suite):
1982                         # we really use the other suite, ignoring the conflicting one ...
1983                         addsuite = self.pkg.changes["distribution-version"][suite]
1984
1985                         add_version = self.get_anyversion(sv_list, addsuite)
1986                         target_version = self.get_anyversion(sv_list, target_suite)
1987
1988                         if not add_version:
1989                             # not add_version can only happen if we map to a suite
1990                             # that doesn't enhance the suite we're propup'ing from.
1991                             # so "propup-ver x a b c; map a d" is a problem only if
1992                             # d doesn't enhance a.
1993                             #
1994                             # i think we could always propagate in this case, rather
1995                             # than complaining. either way, this isn't a REJECT issue
1996                             #
1997                             # And - we really should complain to the dorks who configured dak
1998                             self.warnings.append("%s is mapped to, but not enhanced by %s - adding anyways" % (suite, addsuite))
1999                             self.pkg.changes.setdefault("propdistribution", {})
2000                             self.pkg.changes["propdistribution"][addsuite] = 1
2001                             cansave = 1
2002                         elif not target_version:
2003                             # not targets_version is true when the package is NEW
2004                             # we could just stick with the "...old version..." REJECT
2005                             # for this, I think.
2006                             self.rejects.append("Won't propogate NEW packages.")
2007                         elif apt_pkg.VersionCompare(new_version, add_version) < 0:
2008                             # propogation would be redundant. no need to reject though.
2009                             self.warnings.append("ignoring versionconflict: %s: old version (%s) in %s <= new version (%s) targeted at %s." % (file, existent_version, suite, new_version, target_suite))
2010                             cansave = 1
2011                         elif apt_pkg.VersionCompare(new_version, add_version) > 0 and \
2012                              apt_pkg.VersionCompare(add_version, target_version) >= 0:
2013                             # propogate!!
2014                             self.warnings.append("Propogating upload to %s" % (addsuite))
2015                             self.pkg.changes.setdefault("propdistribution", {})
2016                             self.pkg.changes["propdistribution"][addsuite] = 1
2017                             cansave = 1
2018
2019                     if not cansave:
2020                         self.reject.append("%s: old version (%s) in %s <= new version (%s) targeted at %s." % (file, existent_version, suite, new_version, target_suite))
2021
2022     ################################################################################
2023     def check_binary_against_db(self, file, session):
2024         # Ensure version is sane
2025         q = session.query(BinAssociation)
2026         q = q.join(DBBinary).filter(DBBinary.package==self.pkg.files[file]["package"])
2027         q = q.join(Architecture).filter(Architecture.arch_string.in_([self.pkg.files[file]["architecture"], 'all']))
2028
2029         self.cross_suite_version_check([ (x.suite.suite_name, x.binary.version) for x in q.all() ],
2030                                        file, self.pkg.files[file]["version"], sourceful=False)
2031
2032         # Check for any existing copies of the file
2033         q = session.query(DBBinary).filter_by(package=self.pkg.files[file]["package"])
2034         q = q.filter_by(version=self.pkg.files[file]["version"])
2035         q = q.join(Architecture).filter_by(arch_string=self.pkg.files[file]["architecture"])
2036
2037         if q.count() > 0:
2038             self.rejects.append("%s: can not overwrite existing copy already in the archive." % (file))
2039
2040     ################################################################################
2041
2042     def check_source_against_db(self, file, session):
2043         """
2044         """
2045         source = self.pkg.dsc.get("source")
2046         version = self.pkg.dsc.get("version")
2047
2048         # Ensure version is sane
2049         q = session.query(SrcAssociation)
2050         q = q.join(DBSource).filter(DBSource.source==source)
2051
2052         self.cross_suite_version_check([ (x.suite.suite_name, x.source.version) for x in q.all() ],
2053                                        file, version, sourceful=True)
2054
2055     ################################################################################
2056     def check_dsc_against_db(self, file, session):
2057         """
2058
2059         @warning: NB: this function can remove entries from the 'files' index [if
2060          the .orig.tar.gz is a duplicate of the one in the archive]; if
2061          you're iterating over 'files' and call this function as part of
2062          the loop, be sure to add a check to the top of the loop to
2063          ensure you haven't just tried to dereference the deleted entry.
2064
2065         """
2066
2067         Cnf = Config()
2068         self.pkg.orig_tar_gz = None
2069
2070         # Try and find all files mentioned in the .dsc.  This has
2071         # to work harder to cope with the multiple possible
2072         # locations of an .orig.tar.gz.
2073         # The ordering on the select is needed to pick the newest orig
2074         # when it exists in multiple places.
2075         for dsc_name, dsc_entry in self.pkg.dsc_files.items():
2076             found = None
2077             if self.pkg.files.has_key(dsc_name):
2078                 actual_md5 = self.pkg.files[dsc_name]["md5sum"]
2079                 actual_size = int(self.pkg.files[dsc_name]["size"])
2080                 found = "%s in incoming" % (dsc_name)
2081
2082                 # Check the file does not already exist in the archive
2083                 ql = get_poolfile_like_name(dsc_name, session)
2084
2085                 # Strip out anything that isn't '%s' or '/%s$'
2086                 for i in ql:
2087                     if not i.filename.endswith(dsc_name):
2088                         ql.remove(i)
2089
2090                 # "[dak] has not broken them.  [dak] has fixed a
2091                 # brokenness.  Your crappy hack exploited a bug in
2092                 # the old dinstall.
2093                 #
2094                 # "(Come on!  I thought it was always obvious that
2095                 # one just doesn't release different files with
2096                 # the same name and version.)"
2097                 #                        -- ajk@ on d-devel@l.d.o
2098
2099                 if len(ql) > 0:
2100                     # Ignore exact matches for .orig.tar.gz
2101                     match = 0
2102                     if dsc_name.endswith(".orig.tar.gz"):
2103                         for i in ql:
2104                             if self.pkg.files.has_key(dsc_name) and \
2105                                int(self.pkg.files[dsc_name]["size"]) == int(i.filesize) and \
2106                                self.pkg.files[dsc_name]["md5sum"] == i.md5sum:
2107                                 self.warnings.append("ignoring %s, since it's already in the archive." % (dsc_name))
2108                                 # TODO: Don't delete the entry, just mark it as not needed
2109                                 # This would fix the stupidity of changing something we often iterate over
2110                                 # whilst we're doing it
2111                                 del self.pkg.files[dsc_name]
2112                                 self.pkg.orig_tar_gz = os.path.join(i.location.path, i.filename)
2113                                 match = 1
2114
2115                     if not match:
2116                         self.rejects.append("can not overwrite existing copy of '%s' already in the archive." % (dsc_name))
2117
2118             elif dsc_name.endswith(".orig.tar.gz"):
2119                 # Check in the pool
2120                 ql = get_poolfile_like_name(dsc_name, session)
2121
2122                 # Strip out anything that isn't '%s' or '/%s$'
2123                 # TODO: Shouldn't we just search for things which end with our string explicitly in the SQL?
2124                 for i in ql:
2125                     if not i.filename.endswith(dsc_name):
2126                         ql.remove(i)
2127
2128                 if len(ql) > 0:
2129                     # Unfortunately, we may get more than one match here if,
2130                     # for example, the package was in potato but had an -sa
2131                     # upload in woody.  So we need to choose the right one.
2132
2133                     # default to something sane in case we don't match any or have only one
2134                     x = ql[0]
2135
2136                     if len(ql) > 1:
2137                         for i in ql:
2138                             old_file = os.path.join(i.location.path, i.filename)
2139                             old_file_fh = utils.open_file(old_file)
2140                             actual_md5 = apt_pkg.md5sum(old_file_fh)
2141                             old_file_fh.close()
2142                             actual_size = os.stat(old_file)[stat.ST_SIZE]
2143                             if actual_md5 == dsc_entry["md5sum"] and actual_size == int(dsc_entry["size"]):
2144                                 x = i
2145
2146                     old_file = os.path.join(i.location.path, i.filename)
2147                     old_file_fh = utils.open_file(old_file)
2148                     actual_md5 = apt_pkg.md5sum(old_file_fh)
2149                     old_file_fh.close()
2150                     actual_size = os.stat(old_file)[stat.ST_SIZE]
2151                     found = old_file
2152                     suite_type = x.location.archive_type
2153                     # need this for updating dsc_files in install()
2154                     dsc_entry["files id"] = x.file_id
2155                     # See install() in process-accepted...
2156                     self.pkg.orig_tar_id = x.file_id
2157                     self.pkg.orig_tar_gz = old_file
2158                     self.pkg.orig_tar_location = x.location.location_id
2159                 else:
2160                     # TODO: Record the queues and info in the DB so we don't hardcode all this crap
2161                     # Not there? Check the queue directories...
2162                     for directory in [ "Accepted", "New", "Byhand", "ProposedUpdates", "OldProposedUpdates", "Embargoed", "Unembargoed" ]:
2163                         if not Cnf.has_key("Dir::Queue::%s" % (directory)):
2164                             continue
2165                         in_otherdir = os.path.join(Cnf["Dir::Queue::%s" % (directory)], dsc_name)
2166                         if os.path.exists(in_otherdir):
2167                             in_otherdir_fh = utils.open_file(in_otherdir)
2168                             actual_md5 = apt_pkg.md5sum(in_otherdir_fh)
2169                             in_otherdir_fh.close()
2170                             actual_size = os.stat(in_otherdir)[stat.ST_SIZE]
2171                             found = in_otherdir
2172                             self.pkg.orig_tar_gz = in_otherdir
2173
2174                     if not found:
2175                         self.rejects.append("%s refers to %s, but I can't find it in the queue or in the pool." % (file, dsc_name))
2176                         self.pkg.orig_tar_gz = -1
2177                         continue
2178             else:
2179                 self.rejects.append("%s refers to %s, but I can't find it in the queue." % (file, dsc_name))
2180                 continue
2181             if actual_md5 != dsc_entry["md5sum"]:
2182                 self.rejects.append("md5sum for %s doesn't match %s." % (found, file))
2183             if actual_size != int(dsc_entry["size"]):
2184                 self.rejects.append("size for %s doesn't match %s." % (found, file))
2185
2186     ################################################################################
2187     def accepted_checks(self, overwrite_checks, session):
2188         # Recheck anything that relies on the database; since that's not
2189         # frozen between accept and our run time when called from p-a.
2190
2191         # overwrite_checks is set to False when installing to stable/oldstable
2192
2193         propogate={}
2194         nopropogate={}
2195
2196         # Find the .dsc (again)
2197         dsc_filename = None
2198         for f in self.pkg.files.keys():
2199             if self.pkg.files[f]["type"] == "dsc":
2200                 dsc_filename = f
2201
2202         for checkfile in self.pkg.files.keys():
2203             # The .orig.tar.gz can disappear out from under us is it's a
2204             # duplicate of one in the archive.
2205             if not self.pkg.files.has_key(checkfile):
2206                 continue
2207
2208             entry = self.pkg.files[checkfile]
2209
2210             # Check that the source still exists
2211             if entry["type"] == "deb":
2212                 source_version = entry["source version"]
2213                 source_package = entry["source package"]
2214                 if not self.pkg.changes["architecture"].has_key("source") \
2215                    and not source_exists(source_package, source_version,  self.pkg.changes["distribution"].keys()):
2216                     self.rejects.append("no source found for %s %s (%s)." % (source_package, source_version, checkfile))
2217
2218             # Version and file overwrite checks
2219             if overwrite_checks:
2220                 if entry["type"] == "deb":
2221                     self.check_binary_against_db(checkfile, session)
2222                 elif entry["type"] == "dsc":
2223                     self.check_source_against_db(checkfile, session)
2224                     self.check_dsc_against_db(dsc_filename, session)
2225
2226             # propogate in the case it is in the override tables:
2227             for suite in self.pkg.changes.get("propdistribution", {}).keys():
2228                 if self.in_override_p(entry["package"], entry["component"], suite, entry.get("dbtype",""), checkfile, session):
2229                     propogate[suite] = 1
2230                 else:
2231                     nopropogate[suite] = 1
2232
2233         for suite in propogate.keys():
2234             if suite in nopropogate:
2235                 continue
2236             self.pkg.changes["distribution"][suite] = 1
2237
2238         for checkfile in self.pkg.files.keys():
2239             # Check the package is still in the override tables
2240             for suite in self.pkg.changes["distribution"].keys():
2241                 if not self.in_override_p(entry["package"], entry["component"], suite, entry.get("dbtype",""), checkfile, session):
2242                     self.rejects.append("%s is NEW for %s." % (checkfile, suite))
2243
2244     ################################################################################
2245     # This is not really a reject, but an unaccept, but since a) the code for
2246     # that is non-trivial (reopen bugs, unannounce etc.), b) this should be
2247     # extremely rare, for now we'll go with whining at our admin folks...
2248
2249     def do_unaccept(self):
2250         cnf = Config()
2251
2252         self.update_subst()
2253         self.Subst["__REJECTOR_ADDRESS__"] = cnf["Dinstall::MyEmailAddress"]
2254         self.Subst["__REJECT_MESSAGE__"] = self.package_info()
2255         self.Subst["__CC__"] = "Cc: " + cnf["Dinstall::MyEmailAddress"]
2256         self.Subst["__BCC__"] = "X-DAK: dak process-accepted\nX-Katie: $Revision: 1.18 $"
2257         if cnf.has_key("Dinstall::Bcc"):
2258             self.Subst["__BCC__"] += "\nBcc: %s" % (cnf["Dinstall::Bcc"])
2259
2260         template = os.path.join(cnf["Dir::Templates"], "process-accepted.unaccept")
2261
2262         reject_mail_message = utils.TemplateSubst(self.Subst, template)
2263
2264         # Write the rejection email out as the <foo>.reason file
2265         reason_filename = os.path.basename(self.pkg.changes_file[:-8]) + ".reason"
2266         reject_filename = os.path.join(cnf["Dir::Queue::Reject"], reason_filename)
2267
2268         # If we fail here someone is probably trying to exploit the race
2269         # so let's just raise an exception ...
2270         if os.path.exists(reject_filename):
2271             os.unlink(reject_filename)
2272
2273         fd = os.open(reject_filename, os.O_RDWR|os.O_CREAT|os.O_EXCL, 0644)
2274         os.write(fd, reject_mail_message)
2275         os.close(fd)
2276
2277         utils.send_mail(reject_mail_message)
2278
2279         del self.Subst["__REJECTOR_ADDRESS__"]
2280         del self.Subst["__REJECT_MESSAGE__"]
2281         del self.Subst["__CC__"]
2282
2283     ################################################################################
2284     # If any file of an upload has a recent mtime then chances are good
2285     # the file is still being uploaded.
2286
2287     def upload_too_new(self):
2288         cnf = Config()
2289         too_new = False
2290         # Move back to the original directory to get accurate time stamps
2291         cwd = os.getcwd()
2292         os.chdir(self.pkg.directory)
2293         file_list = self.pkg.files.keys()
2294         file_list.extend(self.pkg.dsc_files.keys())
2295         file_list.append(self.pkg.changes_file)
2296         for f in file_list:
2297             try:
2298                 last_modified = time.time()-os.path.getmtime(f)
2299                 if last_modified < int(cnf["Dinstall::SkipTime"]):
2300                     too_new = True
2301                     break
2302             except:
2303                 pass
2304
2305         os.chdir(cwd)
2306         return too_new