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