]> git.decadent.org.uk Git - dak.git/blob - dak/process_unchecked.py
Merge commit 'ftpmaster/master' into sqlalchemy
[dak.git] / dak / process_unchecked.py
1 #!/usr/bin/env python
2
3 """
4 Checks Debian packages from Incoming
5 @contact: Debian FTP Master <ftpmaster@debian.org>
6 @copyright: 2000, 2001, 2002, 2003, 2004, 2005, 2006  James Troup <james@nocrew.org>
7 @copyright: 2009  Joerg Jaspert <joerg@debian.org>
8 @copyright: 2009  Mark Hymers <mhy@debian.org>
9 @license: GNU General Public License version 2 or later
10 """
11
12 # This program is free software; you can redistribute it and/or modify
13 # it under the terms of the GNU General Public License as published by
14 # the Free Software Foundation; either version 2 of the License, or
15 # (at your option) any later version.
16
17 # This program is distributed in the hope that it will be useful,
18 # but WITHOUT ANY WARRANTY; without even the implied warranty of
19 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
20 # GNU General Public License for more details.
21
22 # You should have received a copy of the GNU General Public License
23 # along with this program; if not, write to the Free Software
24 # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
25
26 # Originally based on dinstall by Guy Maor <maor@debian.org>
27
28 ################################################################################
29
30 # Computer games don't affect kids. I mean if Pacman affected our generation as
31 # kids, we'd all run around in a darkened room munching pills and listening to
32 # repetitive music.
33 #         -- Unknown
34
35 ################################################################################
36
37 import commands
38 import errno
39 import fcntl
40 import os
41 import re
42 import shutil
43 import stat
44 import sys
45 import time
46 import traceback
47 import tarfile
48 import apt_inst
49 import apt_pkg
50 from debian_bundle import deb822
51
52 from daklib.dbconn import *
53 from daklib.binary import Binary
54 from daklib import daklog
55 from daklib.queue import *
56 from daklib import utils
57 from daklib.textutils import fix_maintainer
58 from daklib.dak_exceptions import *
59 from daklib.regexes import re_default_answer
60 from daklib.summarystats import SummaryStats
61 from daklib.holding import Holding
62 from daklib.config import Config
63
64 from types import *
65
66 ################################################################################
67
68
69 ################################################################################
70
71 # Globals
72 Options = None
73 Logger = None
74
75 ###############################################################################
76
77 def init():
78     global Options
79
80     apt_pkg.init()
81     cnf = Config()
82
83     Arguments = [('a',"automatic","Dinstall::Options::Automatic"),
84                  ('h',"help","Dinstall::Options::Help"),
85                  ('n',"no-action","Dinstall::Options::No-Action"),
86                  ('p',"no-lock", "Dinstall::Options::No-Lock"),
87                  ('s',"no-mail", "Dinstall::Options::No-Mail"),
88                  ('d',"directory", "Dinstall::Options::Directory", "HasArg")]
89
90     for i in ["automatic", "help", "no-action", "no-lock", "no-mail",
91               "override-distribution", "version", "directory"]:
92         cnf["Dinstall::Options::%s" % (i)] = ""
93
94     changes_files = apt_pkg.ParseCommandLine(cnf.Cnf, Arguments, sys.argv)
95     Options = cnf.SubTree("Dinstall::Options")
96
97     if Options["Help"]:
98         usage()
99
100     # If we have a directory flag, use it to find our files
101     if cnf["Dinstall::Options::Directory"] != "":
102         # Note that we clobber the list of files we were given in this case
103         # so warn if the user has done both
104         if len(changes_files) > 0:
105             utils.warn("Directory provided so ignoring files given on command line")
106
107         changes_files = utils.get_changes_files(cnf["Dinstall::Options::Directory"])
108
109     return changes_files
110
111 ################################################################################
112
113 def usage (exit_code=0):
114     print """Usage: dinstall [OPTION]... [CHANGES]...
115   -a, --automatic           automatic run
116   -h, --help                show this help and exit.
117   -n, --no-action           don't do anything
118   -p, --no-lock             don't check lockfile !! for cron.daily only !!
119   -s, --no-mail             don't send any mail
120   -V, --version             display the version number and exit"""
121     sys.exit(exit_code)
122
123 ################################################################################
124
125 def reject (str, prefix="Rejected: "):
126     global reject_message
127     if str:
128         reject_message += prefix + str + "\n"
129
130 ################################################################################
131
132 def copy_to_holding(filename):
133     global in_holding
134
135     base_filename = os.path.basename(filename)
136
137     dest = Cnf["Dir::Queue::Holding"] + '/' + base_filename
138     try:
139         fd = os.open(dest, os.O_RDWR|os.O_CREAT|os.O_EXCL, 0640)
140         os.close(fd)
141     except OSError, e:
142         # Shouldn't happen, but will if, for example, someone lists a
143         # file twice in the .changes.
144         if errno.errorcode[e.errno] == 'EEXIST':
145             reject("%s: already exists in holding area; can not overwrite." % (base_filename))
146             return
147         raise
148
149     try:
150         shutil.copy(filename, dest)
151     except IOError, e:
152         # In either case (ENOENT or EACCES) we want to remove the
153         # O_CREAT | O_EXCLed ghost file, so add the file to the list
154         # of 'in holding' even if it's not the real file.
155         if errno.errorcode[e.errno] == 'ENOENT':
156             reject("%s: can not copy to holding area: file not found." % (base_filename))
157             os.unlink(dest)
158             return
159         elif errno.errorcode[e.errno] == 'EACCES':
160             reject("%s: can not copy to holding area: read permission denied." % (base_filename))
161             os.unlink(dest)
162             return
163         raise
164
165     in_holding[base_filename] = ""
166
167 ################################################################################
168
169 def clean_holding():
170     global in_holding
171
172     cwd = os.getcwd()
173     os.chdir(Cnf["Dir::Queue::Holding"])
174     for f in in_holding.keys():
175         if os.path.exists(f):
176             if f.find('/') != -1:
177                 utils.fubar("WTF? clean_holding() got a file ('%s') with / in it!" % (f))
178             else:
179                 os.unlink(f)
180     in_holding = {}
181     os.chdir(cwd)
182
183 ################################################################################
184
185 def check_changes():
186     filename = pkg.changes_file
187
188     # Parse the .changes field into a dictionary
189     try:
190         changes.update(utils.parse_changes(filename))
191     except CantOpenError:
192         reject("%s: can't read file." % (filename))
193         return 0
194     except ParseChangesError, line:
195         reject("%s: parse error, can't grok: %s." % (filename, line))
196         return 0
197     except ChangesUnicodeError:
198         reject("%s: changes file not proper utf-8" % (filename))
199         return 0
200
201     # Parse the Files field from the .changes into another dictionary
202     try:
203         files.update(utils.build_file_list(changes))
204     except ParseChangesError, line:
205         reject("%s: parse error, can't grok: %s." % (filename, line))
206     except UnknownFormatError, format:
207         reject("%s: unknown format '%s'." % (filename, format))
208         return 0
209
210     # Check for mandatory fields
211     for i in ("source", "binary", "architecture", "version", "distribution",
212               "maintainer", "files", "changes", "description"):
213         if not changes.has_key(i):
214             reject("%s: Missing mandatory field `%s'." % (filename, i))
215             return 0    # Avoid <undef> errors during later tests
216
217     # Strip a source version in brackets from the source field
218     if re_strip_srcver.search(changes["source"]):
219         changes["source"] = re_strip_srcver.sub('', changes["source"])
220
221     # Ensure the source field is a valid package name.
222     if not re_valid_pkg_name.match(changes["source"]):
223         reject("%s: invalid source name '%s'." % (filename, changes["source"]))
224
225     # Split multi-value fields into a lower-level dictionary
226     for i in ("architecture", "distribution", "binary", "closes"):
227         o = changes.get(i, "")
228         if o != "":
229             del changes[i]
230         changes[i] = {}
231         for j in o.split():
232             changes[i][j] = 1
233
234     # Fix the Maintainer: field to be RFC822/2047 compatible
235     try:
236         (changes["maintainer822"], changes["maintainer2047"],
237          changes["maintainername"], changes["maintaineremail"]) = \
238          utils.fix_maintainer (changes["maintainer"])
239     except ParseMaintError, msg:
240         reject("%s: Maintainer field ('%s') failed to parse: %s" \
241                % (filename, changes["maintainer"], msg))
242
243     # ...likewise for the Changed-By: field if it exists.
244     try:
245         (changes["changedby822"], changes["changedby2047"],
246          changes["changedbyname"], changes["changedbyemail"]) = \
247          utils.fix_maintainer (changes.get("changed-by", ""))
248     except ParseMaintError, msg:
249         (changes["changedby822"], changes["changedby2047"],
250          changes["changedbyname"], changes["changedbyemail"]) = \
251          ("", "", "", "")
252         reject("%s: Changed-By field ('%s') failed to parse: %s" \
253                % (filename, changes["changed-by"], msg))
254
255     # Ensure all the values in Closes: are numbers
256     if changes.has_key("closes"):
257         for i in changes["closes"].keys():
258             if re_isanum.match (i) == None:
259                 reject("%s: `%s' from Closes field isn't a number." % (filename, i))
260
261
262     # chopversion = no epoch; chopversion2 = no epoch and no revision (e.g. for .orig.tar.gz comparison)
263     changes["chopversion"] = re_no_epoch.sub('', changes["version"])
264     changes["chopversion2"] = re_no_revision.sub('', changes["chopversion"])
265
266     # Check there isn't already a changes file of the same name in one
267     # of the queue directories.
268     base_filename = os.path.basename(filename)
269     for d in [ "Accepted", "Byhand", "Done", "New", "ProposedUpdates", "OldProposedUpdates" ]:
270         if os.path.exists(Cnf["Dir::Queue::%s" % (d) ]+'/'+base_filename):
271             reject("%s: a file with this name already exists in the %s directory." % (base_filename, d))
272
273     # Check the .changes is non-empty
274     if not files:
275         reject("%s: nothing to do (Files field is empty)." % (base_filename))
276         return 0
277
278     return 1
279
280 ################################################################################
281
282 def check_distributions():
283     "Check and map the Distribution field of a .changes file."
284
285     # Handle suite mappings
286     for m in Cnf.ValueList("SuiteMappings"):
287         args = m.split()
288         mtype = args[0]
289         if mtype == "map" or mtype == "silent-map":
290             (source, dest) = args[1:3]
291             if changes["distribution"].has_key(source):
292                 del changes["distribution"][source]
293                 changes["distribution"][dest] = 1
294                 if mtype != "silent-map":
295                     reject("Mapping %s to %s." % (source, dest),"")
296             if changes.has_key("distribution-version"):
297                 if changes["distribution-version"].has_key(source):
298                     changes["distribution-version"][source]=dest
299         elif mtype == "map-unreleased":
300             (source, dest) = args[1:3]
301             if changes["distribution"].has_key(source):
302                 for arch in changes["architecture"].keys():
303                     if arch not in DBConn().get_suite_architectures(source):
304                         reject("Mapping %s to %s for unreleased architecture %s." % (source, dest, arch),"")
305                         del changes["distribution"][source]
306                         changes["distribution"][dest] = 1
307                         break
308         elif mtype == "ignore":
309             suite = args[1]
310             if changes["distribution"].has_key(suite):
311                 del changes["distribution"][suite]
312                 reject("Ignoring %s as a target suite." % (suite), "Warning: ")
313         elif mtype == "reject":
314             suite = args[1]
315             if changes["distribution"].has_key(suite):
316                 reject("Uploads to %s are not accepted." % (suite))
317         elif mtype == "propup-version":
318             # give these as "uploaded-to(non-mapped) suites-to-add-when-upload-obsoletes"
319             #
320             # changes["distribution-version"] looks like: {'testing': 'testing-proposed-updates'}
321             if changes["distribution"].has_key(args[1]):
322                 changes.setdefault("distribution-version", {})
323                 for suite in args[2:]: changes["distribution-version"][suite]=suite
324
325     # Ensure there is (still) a target distribution
326     if changes["distribution"].keys() == []:
327         reject("no valid distribution.")
328
329     # Ensure target distributions exist
330     for suite in changes["distribution"].keys():
331         if not Cnf.has_key("Suite::%s" % (suite)):
332             reject("Unknown distribution `%s'." % (suite))
333
334 ################################################################################
335
336 def check_files():
337     global reprocess
338
339     archive = utils.where_am_i()
340     file_keys = files.keys()
341
342     # if reprocess is 2 we've already done this and we're checking
343     # things again for the new .orig.tar.gz.
344     # [Yes, I'm fully aware of how disgusting this is]
345     if not Options["No-Action"] and reprocess < 2:
346         cwd = os.getcwd()
347         os.chdir(pkg.directory)
348         for f in file_keys:
349             copy_to_holding(f)
350         os.chdir(cwd)
351
352     # Check there isn't already a .changes or .dak file of the same name in
353     # the proposed-updates "CopyChanges" or "CopyDotDak" storage directories.
354     # [NB: this check must be done post-suite mapping]
355     base_filename = os.path.basename(pkg.changes_file)
356     dot_dak_filename = base_filename[:-8]+".dak"
357     for suite in changes["distribution"].keys():
358         copychanges = "Suite::%s::CopyChanges" % (suite)
359         if Cnf.has_key(copychanges) and \
360                os.path.exists(Cnf[copychanges]+"/"+base_filename):
361             reject("%s: a file with this name already exists in %s" \
362                    % (base_filename, Cnf[copychanges]))
363
364         copy_dot_dak = "Suite::%s::CopyDotDak" % (suite)
365         if Cnf.has_key(copy_dot_dak) and \
366                os.path.exists(Cnf[copy_dot_dak]+"/"+dot_dak_filename):
367             reject("%s: a file with this name already exists in %s" \
368                    % (dot_dak_filename, Cnf[copy_dot_dak]))
369
370     reprocess = 0
371     has_binaries = 0
372     has_source = 0
373
374     cursor = DBConn().cursor()
375     # Check for packages that have moved from one component to another
376     # STU: this should probably be changed to not join on architecture, suite tables but instead to used their cached name->id mappings from DBConn
377     DBConn().prepare("moved_pkg_q", """
378         PREPARE moved_pkg_q(text,text,text) AS
379         SELECT c.name FROM binaries b, bin_associations ba, suite s, location l,
380                     component c, architecture a, files f
381         WHERE b.package = $1 AND s.suite_name = $2
382           AND (a.arch_string = $3 OR a.arch_string = 'all')
383           AND ba.bin = b.id AND ba.suite = s.id AND b.architecture = a.id
384           AND f.location = l.id
385           AND l.component = c.id
386           AND b.file = f.id""")
387
388     for f in file_keys:
389         # Ensure the file does not already exist in one of the accepted directories
390         for d in [ "Accepted", "Byhand", "New", "ProposedUpdates", "OldProposedUpdates", "Embargoed", "Unembargoed" ]:
391             if not Cnf.has_key("Dir::Queue::%s" % (d)): continue
392             if os.path.exists(Cnf["Dir::Queue::%s" % (d) ] + '/' + f):
393                 reject("%s file already exists in the %s directory." % (f, d))
394         if not re_taint_free.match(f):
395             reject("!!WARNING!! tainted filename: '%s'." % (f))
396         # Check the file is readable
397         if os.access(f, os.R_OK) == 0:
398             # When running in -n, copy_to_holding() won't have
399             # generated the reject_message, so we need to.
400             if Options["No-Action"]:
401                 if os.path.exists(f):
402                     reject("Can't read `%s'. [permission denied]" % (f))
403                 else:
404                     reject("Can't read `%s'. [file not found]" % (f))
405             files[f]["type"] = "unreadable"
406             continue
407         # If it's byhand skip remaining checks
408         if files[f]["section"] == "byhand" or files[f]["section"][:4] == "raw-":
409             files[f]["byhand"] = 1
410             files[f]["type"] = "byhand"
411         # Checks for a binary package...
412         elif re_isadeb.match(f):
413             has_binaries = 1
414             files[f]["type"] = "deb"
415
416             # Extract package control information
417             deb_file = utils.open_file(f)
418             try:
419                 control = apt_pkg.ParseSection(apt_inst.debExtractControl(deb_file))
420             except:
421                 reject("%s: debExtractControl() raised %s." % (f, sys.exc_type))
422                 deb_file.close()
423                 # Can't continue, none of the checks on control would work.
424                 continue
425
426             # Check for mandantory "Description:"
427             deb_file.seek ( 0 )
428             try:
429                 apt_pkg.ParseSection(apt_inst.debExtractControl(deb_file))["Description"] + '\n'
430             except:
431                 reject("%s: Missing Description in binary package" % (f))
432                 continue
433
434             deb_file.close()
435
436             # Check for mandatory fields
437             for field in [ "Package", "Architecture", "Version" ]:
438                 if control.Find(field) == None:
439                     reject("%s: No %s field in control." % (f, field))
440                     # Can't continue
441                     continue
442
443             # Ensure the package name matches the one give in the .changes
444             if not changes["binary"].has_key(control.Find("Package", "")):
445                 reject("%s: control file lists name as `%s', which isn't in changes file." % (f, control.Find("Package", "")))
446
447             # Validate the package field
448             package = control.Find("Package")
449             if not re_valid_pkg_name.match(package):
450                 reject("%s: invalid package name '%s'." % (f, package))
451
452             # Validate the version field
453             version = control.Find("Version")
454             if not re_valid_version.match(version):
455                 reject("%s: invalid version number '%s'." % (f, version))
456
457             # Ensure the architecture of the .deb is one we know about.
458             default_suite = Cnf.get("Dinstall::DefaultSuite", "Unstable")
459             architecture = control.Find("Architecture")
460             upload_suite = changes["distribution"].keys()[0]
461             if architecture not in DBConn().get_suite_architectures(default_suite) and architecture not in DBConn().get_suite_architectures(upload_suite):
462                 reject("Unknown architecture '%s'." % (architecture))
463
464             # Ensure the architecture of the .deb is one of the ones
465             # listed in the .changes.
466             if not changes["architecture"].has_key(architecture):
467                 reject("%s: control file lists arch as `%s', which isn't in changes file." % (f, architecture))
468
469             # Sanity-check the Depends field
470             depends = control.Find("Depends")
471             if depends == '':
472                 reject("%s: Depends field is empty." % (f))
473
474             # Sanity-check the Provides field
475             provides = control.Find("Provides")
476             if provides:
477                 provide = re_spacestrip.sub('', provides)
478                 if provide == '':
479                     reject("%s: Provides field is empty." % (f))
480                 prov_list = provide.split(",")
481                 for prov in prov_list:
482                     if not re_valid_pkg_name.match(prov):
483                         reject("%s: Invalid Provides field content %s." % (f, prov))
484
485
486             # Check the section & priority match those given in the .changes (non-fatal)
487             if control.Find("Section") and files[f]["section"] != "" and files[f]["section"] != control.Find("Section"):
488                 reject("%s control file lists section as `%s', but changes file has `%s'." % (f, control.Find("Section", ""), files[f]["section"]), "Warning: ")
489             if control.Find("Priority") and files[f]["priority"] != "" and files[f]["priority"] != control.Find("Priority"):
490                 reject("%s control file lists priority as `%s', but changes file has `%s'." % (f, control.Find("Priority", ""), files[f]["priority"]),"Warning: ")
491
492             files[f]["package"] = package
493             files[f]["architecture"] = architecture
494             files[f]["version"] = version
495             files[f]["maintainer"] = control.Find("Maintainer", "")
496             if f.endswith(".udeb"):
497                 files[f]["dbtype"] = "udeb"
498             elif f.endswith(".deb"):
499                 files[f]["dbtype"] = "deb"
500             else:
501                 reject("%s is neither a .deb or a .udeb." % (f))
502             files[f]["source"] = control.Find("Source", files[f]["package"])
503             # Get the source version
504             source = files[f]["source"]
505             source_version = ""
506             if source.find("(") != -1:
507                 m = re_extract_src_version.match(source)
508                 source = m.group(1)
509                 source_version = m.group(2)
510             if not source_version:
511                 source_version = files[f]["version"]
512             files[f]["source package"] = source
513             files[f]["source version"] = source_version
514
515             # Ensure the filename matches the contents of the .deb
516             m = re_isadeb.match(f)
517             #  package name
518             file_package = m.group(1)
519             if files[f]["package"] != file_package:
520                 reject("%s: package part of filename (%s) does not match package name in the %s (%s)." % (f, file_package, files[f]["dbtype"], files[f]["package"]))
521             epochless_version = re_no_epoch.sub('', control.Find("Version"))
522             #  version
523             file_version = m.group(2)
524             if epochless_version != file_version:
525                 reject("%s: version part of filename (%s) does not match package version in the %s (%s)." % (f, file_version, files[f]["dbtype"], epochless_version))
526             #  architecture
527             file_architecture = m.group(3)
528             if files[f]["architecture"] != file_architecture:
529                 reject("%s: architecture part of filename (%s) does not match package architecture in the %s (%s)." % (f, file_architecture, files[f]["dbtype"], files[f]["architecture"]))
530
531             # Check for existent source
532             source_version = files[f]["source version"]
533             source_package = files[f]["source package"]
534             if changes["architecture"].has_key("source"):
535                 if source_version != changes["version"]:
536                     reject("source version (%s) for %s doesn't match changes version %s." % (source_version, f, changes["version"]))
537             else:
538                 # Check in the SQL database
539                 if not Upload.source_exists(source_package, source_version, changes["distribution"].keys()):
540                     # Check in one of the other directories
541                     source_epochless_version = re_no_epoch.sub('', source_version)
542                     dsc_filename = "%s_%s.dsc" % (source_package, source_epochless_version)
543                     if os.path.exists(Cnf["Dir::Queue::Byhand"] + '/' + dsc_filename):
544                         files[f]["byhand"] = 1
545                     elif os.path.exists(Cnf["Dir::Queue::New"] + '/' + dsc_filename):
546                         files[f]["new"] = 1
547                     else:
548                         dsc_file_exists = 0
549                         for myq in ["Accepted", "Embargoed", "Unembargoed", "ProposedUpdates", "OldProposedUpdates"]:
550                             if Cnf.has_key("Dir::Queue::%s" % (myq)):
551                                 if os.path.exists(Cnf["Dir::Queue::"+myq] + '/' + dsc_filename):
552                                     dsc_file_exists = 1
553                                     break
554                         if not dsc_file_exists:
555                             reject("no source found for %s %s (%s)." % (source_package, source_version, f))
556             # Check the version and for file overwrites
557             reject(Upload.check_binary_against_db(f),"")
558
559             Binary(f, reject).scan_package()
560
561         # Checks for a source package...
562         else:
563             m = re_issource.match(f)
564             if m:
565                 has_source = 1
566                 files[f]["package"] = m.group(1)
567                 files[f]["version"] = m.group(2)
568                 files[f]["type"] = m.group(3)
569
570                 # Ensure the source package name matches the Source filed in the .changes
571                 if changes["source"] != files[f]["package"]:
572                     reject("%s: changes file doesn't say %s for Source" % (f, files[f]["package"]))
573
574                 # Ensure the source version matches the version in the .changes file
575                 if files[f]["type"] == "orig.tar.gz":
576                     changes_version = changes["chopversion2"]
577                 else:
578                     changes_version = changes["chopversion"]
579                 if changes_version != files[f]["version"]:
580                     reject("%s: should be %s according to changes file." % (f, changes_version))
581
582                 # Ensure the .changes lists source in the Architecture field
583                 if not changes["architecture"].has_key("source"):
584                     reject("%s: changes file doesn't list `source' in Architecture field." % (f))
585
586                 # Check the signature of a .dsc file
587                 if files[f]["type"] == "dsc":
588                     dsc["fingerprint"] = utils.check_signature(f, reject)
589
590                 files[f]["architecture"] = "source"
591
592             # Not a binary or source package?  Assume byhand...
593             else:
594                 files[f]["byhand"] = 1
595                 files[f]["type"] = "byhand"
596
597         # Per-suite file checks
598         files[f]["oldfiles"] = {}
599         for suite in changes["distribution"].keys():
600             # Skip byhand
601             if files[f].has_key("byhand"):
602                 continue
603
604             # Handle component mappings
605             for m in Cnf.ValueList("ComponentMappings"):
606                 (source, dest) = m.split()
607                 if files[f]["component"] == source:
608                     files[f]["original component"] = source
609                     files[f]["component"] = dest
610
611             # Ensure the component is valid for the target suite
612             if Cnf.has_key("Suite:%s::Components" % (suite)) and \
613                files[f]["component"] not in Cnf.ValueList("Suite::%s::Components" % (suite)):
614                 reject("unknown component `%s' for suite `%s'." % (files[f]["component"], suite))
615                 continue
616
617             # Validate the component
618             component = files[f]["component"]
619             component_id = DBConn().get_component_id(component)
620             if component_id == -1:
621                 reject("file '%s' has unknown component '%s'." % (f, component))
622                 continue
623
624             # See if the package is NEW
625             if not Upload.in_override_p(files[f]["package"], files[f]["component"], suite, files[f].get("dbtype",""), f):
626                 files[f]["new"] = 1
627
628             # Validate the priority
629             if files[f]["priority"].find('/') != -1:
630                 reject("file '%s' has invalid priority '%s' [contains '/']." % (f, files[f]["priority"]))
631
632             # Determine the location
633             location = Cnf["Dir::Pool"]
634             location_id = DBConn().get_location_id(location, component, archive)
635             if location_id == -1:
636                 reject("[INTERNAL ERROR] couldn't determine location (Component: %s, Archive: %s)" % (component, archive))
637             files[f]["location id"] = location_id
638
639             # Check the md5sum & size against existing files (if any)
640             files[f]["pool name"] = utils.poolify (changes["source"], files[f]["component"])
641             files_id = DBConn().get_files_id(files[f]["pool name"] + f, files[f]["size"], files[f]["md5sum"], files[f]["location id"])
642             if files_id == -1:
643                 reject("INTERNAL ERROR, get_files_id() returned multiple matches for %s." % (f))
644             elif files_id == -2:
645                 reject("md5sum and/or size mismatch on existing copy of %s." % (f))
646             files[f]["files id"] = files_id
647
648             # Check for packages that have moved from one component to another
649             files[f]['suite'] = suite
650             cursor.execute("""EXECUTE moved_pkg_q( %(package)s, %(suite)s, %(architecture)s )""", ( files[f] ) )
651             ql = cursor.fetchone()
652             if ql:
653                 files[f]["othercomponents"] = ql[0][0]
654
655     # If the .changes file says it has source, it must have source.
656     if changes["architecture"].has_key("source"):
657         if not has_source:
658             reject("no source found and Architecture line in changes mention source.")
659
660         if not has_binaries and Cnf.FindB("Dinstall::Reject::NoSourceOnly"):
661             reject("source only uploads are not supported.")
662
663 ###############################################################################
664
665 def check_dsc():
666     global reprocess
667
668     # Ensure there is source to check
669     if not changes["architecture"].has_key("source"):
670         return 1
671
672     # Find the .dsc
673     dsc_filename = None
674     for f in files.keys():
675         if files[f]["type"] == "dsc":
676             if dsc_filename:
677                 reject("can not process a .changes file with multiple .dsc's.")
678                 return 0
679             else:
680                 dsc_filename = f
681
682     # If there isn't one, we have nothing to do. (We have reject()ed the upload already)
683     if not dsc_filename:
684         reject("source uploads must contain a dsc file")
685         return 0
686
687     # Parse the .dsc file
688     try:
689         dsc.update(utils.parse_changes(dsc_filename, signing_rules=1))
690     except CantOpenError:
691         # if not -n copy_to_holding() will have done this for us...
692         if Options["No-Action"]:
693             reject("%s: can't read file." % (dsc_filename))
694     except ParseChangesError, line:
695         reject("%s: parse error, can't grok: %s." % (dsc_filename, line))
696     except InvalidDscError, line:
697         reject("%s: syntax error on line %s." % (dsc_filename, line))
698     except ChangesUnicodeError:
699         reject("%s: dsc file not proper utf-8." % (dsc_filename))
700
701     # Build up the file list of files mentioned by the .dsc
702     try:
703         dsc_files.update(utils.build_file_list(dsc, is_a_dsc=1))
704     except NoFilesFieldError:
705         reject("%s: no Files: field." % (dsc_filename))
706         return 0
707     except UnknownFormatError, format:
708         reject("%s: unknown format '%s'." % (dsc_filename, format))
709         return 0
710     except ParseChangesError, line:
711         reject("%s: parse error, can't grok: %s." % (dsc_filename, line))
712         return 0
713
714     # Enforce mandatory fields
715     for i in ("format", "source", "version", "binary", "maintainer", "architecture", "files"):
716         if not dsc.has_key(i):
717             reject("%s: missing mandatory field `%s'." % (dsc_filename, i))
718             return 0
719
720     # Validate the source and version fields
721     if not re_valid_pkg_name.match(dsc["source"]):
722         reject("%s: invalid source name '%s'." % (dsc_filename, dsc["source"]))
723     if not re_valid_version.match(dsc["version"]):
724         reject("%s: invalid version number '%s'." % (dsc_filename, dsc["version"]))
725
726     # Bumping the version number of the .dsc breaks extraction by stable's
727     # dpkg-source.  So let's not do that...
728     if dsc["format"] != "1.0":
729         reject("%s: incompatible 'Format' version produced by a broken version of dpkg-dev 1.9.1{3,4}." % (dsc_filename))
730
731     # Validate the Maintainer field
732     try:
733         utils.fix_maintainer (dsc["maintainer"])
734     except ParseMaintError, msg:
735         reject("%s: Maintainer field ('%s') failed to parse: %s" \
736                % (dsc_filename, dsc["maintainer"], msg))
737
738     # Validate the build-depends field(s)
739     for field_name in [ "build-depends", "build-depends-indep" ]:
740         field = dsc.get(field_name)
741         if field:
742             # Check for broken dpkg-dev lossage...
743             if field.startswith("ARRAY"):
744                 reject("%s: invalid %s field produced by a broken version of dpkg-dev (1.10.11)" % (dsc_filename, field_name.title()))
745
746             # Have apt try to parse them...
747             try:
748                 apt_pkg.ParseSrcDepends(field)
749             except:
750                 reject("%s: invalid %s field (can not be parsed by apt)." % (dsc_filename, field_name.title()))
751                 pass
752
753     # Ensure the version number in the .dsc matches the version number in the .changes
754     epochless_dsc_version = re_no_epoch.sub('', dsc["version"])
755     changes_version = files[dsc_filename]["version"]
756     if epochless_dsc_version != files[dsc_filename]["version"]:
757         reject("version ('%s') in .dsc does not match version ('%s') in .changes." % (epochless_dsc_version, changes_version))
758
759     # Ensure there is a .tar.gz in the .dsc file
760     has_tar = 0
761     for f in dsc_files.keys():
762         m = re_issource.match(f)
763         if not m:
764             reject("%s: %s in Files field not recognised as source." % (dsc_filename, f))
765             continue
766         ftype = m.group(3)
767         if ftype == "orig.tar.gz" or ftype == "tar.gz":
768             has_tar = 1
769     if not has_tar:
770         reject("%s: no .tar.gz or .orig.tar.gz in 'Files' field." % (dsc_filename))
771
772     # Ensure source is newer than existing source in target suites
773     reject(Upload.check_source_against_db(dsc_filename),"")
774
775     (reject_msg, is_in_incoming) = Upload.check_dsc_against_db(dsc_filename)
776     reject(reject_msg, "")
777     if is_in_incoming:
778         if not Options["No-Action"]:
779             copy_to_holding(is_in_incoming)
780         orig_tar_gz = os.path.basename(is_in_incoming)
781         files[orig_tar_gz] = {}
782         files[orig_tar_gz]["size"] = os.stat(orig_tar_gz)[stat.ST_SIZE]
783         files[orig_tar_gz]["md5sum"] = dsc_files[orig_tar_gz]["md5sum"]
784         files[orig_tar_gz]["sha1sum"] = dsc_files[orig_tar_gz]["sha1sum"]
785         files[orig_tar_gz]["sha256sum"] = dsc_files[orig_tar_gz]["sha256sum"]
786         files[orig_tar_gz]["section"] = files[dsc_filename]["section"]
787         files[orig_tar_gz]["priority"] = files[dsc_filename]["priority"]
788         files[orig_tar_gz]["component"] = files[dsc_filename]["component"]
789         files[orig_tar_gz]["type"] = "orig.tar.gz"
790         reprocess = 2
791
792     return 1
793
794 ################################################################################
795
796 def get_changelog_versions(source_dir):
797     """Extracts a the source package and (optionally) grabs the
798     version history out of debian/changelog for the BTS."""
799
800     # Find the .dsc (again)
801     dsc_filename = None
802     for f in files.keys():
803         if files[f]["type"] == "dsc":
804             dsc_filename = f
805
806     # If there isn't one, we have nothing to do. (We have reject()ed the upload already)
807     if not dsc_filename:
808         return
809
810     # Create a symlink mirror of the source files in our temporary directory
811     for f in files.keys():
812         m = re_issource.match(f)
813         if m:
814             src = os.path.join(source_dir, f)
815             # If a file is missing for whatever reason, give up.
816             if not os.path.exists(src):
817                 return
818             ftype = m.group(3)
819             if ftype == "orig.tar.gz" and pkg.orig_tar_gz:
820                 continue
821             dest = os.path.join(os.getcwd(), f)
822             os.symlink(src, dest)
823
824     # If the orig.tar.gz is not a part of the upload, create a symlink to the
825     # existing copy.
826     if pkg.orig_tar_gz:
827         dest = os.path.join(os.getcwd(), os.path.basename(pkg.orig_tar_gz))
828         os.symlink(pkg.orig_tar_gz, dest)
829
830     # Extract the source
831     cmd = "dpkg-source -sn -x %s" % (dsc_filename)
832     (result, output) = commands.getstatusoutput(cmd)
833     if (result != 0):
834         reject("'dpkg-source -x' failed for %s [return code: %s]." % (dsc_filename, result))
835         reject(utils.prefix_multi_line_string(output, " [dpkg-source output:] "), "")
836         return
837
838     if not Cnf.Find("Dir::Queue::BTSVersionTrack"):
839         return
840
841     # Get the upstream version
842     upstr_version = re_no_epoch.sub('', dsc["version"])
843     if re_strip_revision.search(upstr_version):
844         upstr_version = re_strip_revision.sub('', upstr_version)
845
846     # Ensure the changelog file exists
847     changelog_filename = "%s-%s/debian/changelog" % (dsc["source"], upstr_version)
848     if not os.path.exists(changelog_filename):
849         reject("%s: debian/changelog not found in extracted source." % (dsc_filename))
850         return
851
852     # Parse the changelog
853     dsc["bts changelog"] = ""
854     changelog_file = utils.open_file(changelog_filename)
855     for line in changelog_file.readlines():
856         m = re_changelog_versions.match(line)
857         if m:
858             dsc["bts changelog"] += line
859     changelog_file.close()
860
861     # Check we found at least one revision in the changelog
862     if not dsc["bts changelog"]:
863         reject("%s: changelog format not recognised (empty version tree)." % (dsc_filename))
864
865 ########################################
866
867 def check_source():
868     # Bail out if:
869     #    a) there's no source
870     # or b) reprocess is 2 - we will do this check next time when orig.tar.gz is in 'files'
871     # or c) the orig.tar.gz is MIA
872     if not changes["architecture"].has_key("source") or reprocess == 2 \
873        or pkg.orig_tar_gz == -1:
874         return
875
876     tmpdir = utils.temp_dirname()
877
878     # Move into the temporary directory
879     cwd = os.getcwd()
880     os.chdir(tmpdir)
881
882     # Get the changelog version history
883     get_changelog_versions(cwd)
884
885     # Move back and cleanup the temporary tree
886     os.chdir(cwd)
887     try:
888         shutil.rmtree(tmpdir)
889     except OSError, e:
890         if errno.errorcode[e.errno] != 'EACCES':
891             utils.fubar("%s: couldn't remove tmp dir for source tree." % (dsc["source"]))
892
893         reject("%s: source tree could not be cleanly removed." % (dsc["source"]))
894         # We probably have u-r or u-w directories so chmod everything
895         # and try again.
896         cmd = "chmod -R u+rwx %s" % (tmpdir)
897         result = os.system(cmd)
898         if result != 0:
899             utils.fubar("'%s' failed with result %s." % (cmd, result))
900         shutil.rmtree(tmpdir)
901     except:
902         utils.fubar("%s: couldn't remove tmp dir for source tree." % (dsc["source"]))
903
904 ################################################################################
905
906 # FIXME: should be a debian specific check called from a hook
907
908 def check_urgency ():
909     if changes["architecture"].has_key("source"):
910         if not changes.has_key("urgency"):
911             changes["urgency"] = Cnf["Urgency::Default"]
912         # Urgency may be followed by space & comment (policy 5.6.17)
913         changes["urgency"] = changes["urgency"].split(" ")[0].lower();
914         if changes["urgency"] not in Cnf.ValueList("Urgency::Valid"):
915             reject("%s is not a valid urgency; it will be treated as %s by testing." % (changes["urgency"], Cnf["Urgency::Default"]), "Warning: ")
916             changes["urgency"] = Cnf["Urgency::Default"]
917
918 ################################################################################
919
920 def check_hashes ():
921     utils.check_hash(".changes", files, "md5", apt_pkg.md5sum)
922     utils.check_size(".changes", files)
923     utils.check_hash(".dsc", dsc_files, "md5", apt_pkg.md5sum)
924     utils.check_size(".dsc", dsc_files)
925
926     # This is stupid API, but it'll have to do for now until
927     # we actually have proper abstraction
928     for m in utils.ensure_hashes(changes, dsc, files, dsc_files):
929         reject(m)
930
931 ################################################################################
932
933 # Sanity check the time stamps of files inside debs.
934 # [Files in the near future cause ugly warnings and extreme time
935 #  travel can cause errors on extraction]
936
937 def check_timestamps():
938     class Tar:
939         def __init__(self, future_cutoff, past_cutoff):
940             self.reset()
941             self.future_cutoff = future_cutoff
942             self.past_cutoff = past_cutoff
943
944         def reset(self):
945             self.future_files = {}
946             self.ancient_files = {}
947
948         def callback(self, Kind,Name,Link,Mode,UID,GID,Size,MTime,Major,Minor):
949             if MTime > self.future_cutoff:
950                 self.future_files[Name] = MTime
951             if MTime < self.past_cutoff:
952                 self.ancient_files[Name] = MTime
953     ####
954
955     future_cutoff = time.time() + int(Cnf["Dinstall::FutureTimeTravelGrace"])
956     past_cutoff = time.mktime(time.strptime(Cnf["Dinstall::PastCutoffYear"],"%Y"))
957     tar = Tar(future_cutoff, past_cutoff)
958     for filename in files.keys():
959         if files[filename]["type"] == "deb":
960             tar.reset()
961             try:
962                 deb_file = utils.open_file(filename)
963                 apt_inst.debExtract(deb_file,tar.callback,"control.tar.gz")
964                 deb_file.seek(0)
965                 try:
966                     apt_inst.debExtract(deb_file,tar.callback,"data.tar.gz")
967                 except SystemError, e:
968                     # If we can't find a data.tar.gz, look for data.tar.bz2 instead.
969                     if not re.search(r"Cannot f[ui]nd chunk data.tar.gz$", str(e)):
970                         raise
971                     deb_file.seek(0)
972                     apt_inst.debExtract(deb_file,tar.callback,"data.tar.bz2")
973                 deb_file.close()
974                 #
975                 future_files = tar.future_files.keys()
976                 if future_files:
977                     num_future_files = len(future_files)
978                     future_file = future_files[0]
979                     future_date = tar.future_files[future_file]
980                     reject("%s: has %s file(s) with a time stamp too far into the future (e.g. %s [%s])."
981                            % (filename, num_future_files, future_file,
982                               time.ctime(future_date)))
983                 #
984                 ancient_files = tar.ancient_files.keys()
985                 if ancient_files:
986                     num_ancient_files = len(ancient_files)
987                     ancient_file = ancient_files[0]
988                     ancient_date = tar.ancient_files[ancient_file]
989                     reject("%s: has %s file(s) with a time stamp too ancient (e.g. %s [%s])."
990                            % (filename, num_ancient_files, ancient_file,
991                               time.ctime(ancient_date)))
992             except:
993                 reject("%s: deb contents timestamp check failed [%s: %s]" % (filename, sys.exc_type, sys.exc_value))
994
995 ################################################################################
996
997 def lookup_uid_from_fingerprint(fpr):
998     """
999     Return the uid,name,isdm for a given gpg fingerprint
1000
1001     @type fpr: string
1002     @param fpr: a 40 byte GPG fingerprint
1003
1004     @return: (uid, name, isdm)
1005     """
1006     cursor = DBConn().cursor()
1007     cursor.execute( "SELECT u.uid, u.name, k.debian_maintainer FROM fingerprint f JOIN keyrings k ON (f.keyring=k.id), uid u WHERE f.uid = u.id AND f.fingerprint = '%s'" % (fpr))
1008     qs = cursor.fetchone()
1009     if qs:
1010         return qs
1011     else:
1012         return (None, None, False)
1013
1014 def check_signed_by_key():
1015     """Ensure the .changes is signed by an authorized uploader."""
1016
1017     (uid, uid_name, is_dm) = lookup_uid_from_fingerprint(changes["fingerprint"])
1018     if uid_name == None:
1019         uid_name = ""
1020
1021     # match claimed name with actual name:
1022     if uid is None:
1023         # This is fundamentally broken but need us to refactor how we get
1024         # the UIDs/Fingerprints in order for us to fix it properly
1025         uid, uid_email = changes["fingerprint"], uid
1026         may_nmu, may_sponsor = 1, 1
1027         # XXX by default new dds don't have a fingerprint/uid in the db atm,
1028         #     and can't get one in there if we don't allow nmu/sponsorship
1029     elif is_dm is False:
1030         # If is_dm is False, we allow full upload rights
1031         uid_email = "%s@debian.org" % (uid)
1032         may_nmu, may_sponsor = 1, 1
1033     else:
1034         # Assume limited upload rights unless we've discovered otherwise
1035         uid_email = uid
1036         may_nmu, may_sponsor = 0, 0
1037
1038
1039     if uid_email in [changes["maintaineremail"], changes["changedbyemail"]]:
1040         sponsored = 0
1041     elif uid_name in [changes["maintainername"], changes["changedbyname"]]:
1042         sponsored = 0
1043         if uid_name == "": sponsored = 1
1044     else:
1045         sponsored = 1
1046         if ("source" in changes["architecture"] and
1047             uid_email and utils.is_email_alias(uid_email)):
1048             sponsor_addresses = utils.gpg_get_key_addresses(changes["fingerprint"])
1049             if (changes["maintaineremail"] not in sponsor_addresses and
1050                 changes["changedbyemail"] not in sponsor_addresses):
1051                 changes["sponsoremail"] = uid_email
1052
1053     if sponsored and not may_sponsor:
1054         reject("%s is not authorised to sponsor uploads" % (uid))
1055
1056     cursor = DBConn().cursor()
1057     if not sponsored and not may_nmu:
1058         source_ids = []
1059         cursor.execute( "SELECT s.id, s.version FROM source s JOIN src_associations sa ON (s.id = sa.source) WHERE s.source = %(source)s AND s.dm_upload_allowed = 'yes'", changes )
1060
1061         highest_sid, highest_version = None, None
1062
1063         should_reject = True
1064         while True:
1065             si = cursor.fetchone()
1066             if not si:
1067                 break
1068
1069             if highest_version == None or apt_pkg.VersionCompare(si[1], highest_version) == 1:
1070                  highest_sid = si[0]
1071                  highest_version = si[1]
1072
1073         if highest_sid == None:
1074            reject("Source package %s does not have 'DM-Upload-Allowed: yes' in its most recent version" % changes["source"])
1075         else:
1076
1077             cursor.execute("SELECT m.name FROM maintainer m WHERE m.id IN (SELECT su.maintainer FROM src_uploaders su JOIN source s ON (s.id = su.source) WHERE su.source = %s)" % (highest_sid))
1078
1079             while True:
1080                 m = cursor.fetchone()
1081                 if not m:
1082                     break
1083
1084                 (rfc822, rfc2047, name, email) = utils.fix_maintainer(m[0])
1085                 if email == uid_email or name == uid_name:
1086                     should_reject=False
1087                     break
1088
1089         if should_reject == True:
1090             reject("%s is not in Maintainer or Uploaders of source package %s" % (uid, changes["source"]))
1091
1092         for b in changes["binary"].keys():
1093             for suite in changes["distribution"].keys():
1094                 suite_id = DBConn().get_suite_id(suite)
1095
1096                 cursor.execute("SELECT DISTINCT s.source FROM source s JOIN binaries b ON (s.id = b.source) JOIN bin_associations ba On (b.id = ba.bin) WHERE b.package = %(package)s AND ba.suite = %(suite)s" , {'package':b, 'suite':suite_id} )
1097                 while True:
1098                     s = cursor.fetchone()
1099                     if not s:
1100                         break
1101
1102                     if s[0] != changes["source"]:
1103                         reject("%s may not hijack %s from source package %s in suite %s" % (uid, b, s, suite))
1104
1105         for f in files.keys():
1106             if files[f].has_key("byhand"):
1107                 reject("%s may not upload BYHAND file %s" % (uid, f))
1108             if files[f].has_key("new"):
1109                 reject("%s may not upload NEW file %s" % (uid, f))
1110
1111
1112 ################################################################################
1113 ################################################################################
1114
1115 # If any file of an upload has a recent mtime then chances are good
1116 # the file is still being uploaded.
1117
1118 def upload_too_new():
1119     too_new = 0
1120     # Move back to the original directory to get accurate time stamps
1121     cwd = os.getcwd()
1122     os.chdir(pkg.directory)
1123     file_list = pkg.files.keys()
1124     file_list.extend(pkg.dsc_files.keys())
1125     file_list.append(pkg.changes_file)
1126     for f in file_list:
1127         try:
1128             last_modified = time.time()-os.path.getmtime(f)
1129             if last_modified < int(Cnf["Dinstall::SkipTime"]):
1130                 too_new = 1
1131                 break
1132         except:
1133             pass
1134     os.chdir(cwd)
1135     return too_new
1136
1137 ################################################################################
1138
1139 def action ():
1140     # changes["distribution"] may not exist in corner cases
1141     # (e.g. unreadable changes files)
1142     if not u.pkg.changes.has_key("distribution") or not isinstance(u.pkg.changes["distribution"], DictType):
1143         u.pkg.changes["distribution"] = {}
1144
1145     (summary, short_summary) = u.build_summaries()
1146
1147     # q-unapproved hax0ring
1148     queue_info = {
1149          "New": { "is": is_new, "process": acknowledge_new },
1150          "Autobyhand" : { "is" : is_autobyhand, "process": do_autobyhand },
1151          "Byhand" : { "is": is_byhand, "process": do_byhand },
1152          "OldStableUpdate" : { "is": is_oldstableupdate,
1153                                "process": do_oldstableupdate },
1154          "StableUpdate" : { "is": is_stableupdate, "process": do_stableupdate },
1155          "Unembargo" : { "is": is_unembargo, "process": queue_unembargo },
1156          "Embargo" : { "is": is_embargo, "process": queue_embargo },
1157     }
1158
1159     queues = [ "New", "Autobyhand", "Byhand" ]
1160     if cnf.FindB("Dinstall::SecurityQueueHandling"):
1161         queues += [ "Unembargo", "Embargo" ]
1162     else:
1163         queues += [ "OldStableUpdate", "StableUpdate" ]
1164
1165     (prompt, answer) = ("", "XXX")
1166     if Options["No-Action"] or Options["Automatic"]:
1167         answer = 'S'
1168
1169     queuekey = ''
1170
1171     pi = u.package_info()
1172
1173     if len(u.rejects) > 0:
1174         if u.upload_too_new():
1175             print "SKIP (too new)\n" + pi,
1176             prompt = "[S]kip, Quit ?"
1177         else:
1178             print "REJECT\n" + pi
1179             prompt = "[R]eject, Skip, Quit ?"
1180             if Options["Automatic"]:
1181                 answer = 'R'
1182     else:
1183         qu = None
1184         for q in queues:
1185             if queue_info[q]["is"](u):
1186                 qu = q
1187                 break
1188         if qu:
1189             print "%s for %s\n%s%s" % ( qu.upper(), ", ".join(u.pkg.changes["distribution"].keys()), pi, summary)
1190             queuekey = qu[0].upper()
1191             if queuekey in "RQSA":
1192                 queuekey = "D"
1193                 prompt = "[D]ivert, Skip, Quit ?"
1194             else:
1195                 prompt = "[%s]%s, Skip, Quit ?" % (queuekey, qu[1:].lower())
1196             if Options["Automatic"]:
1197                 answer = queuekey
1198         else:
1199             print "ACCEPT\n" + pi + summary,
1200             prompt = "[A]ccept, Skip, Quit ?"
1201             if Options["Automatic"]:
1202                 answer = 'A'
1203
1204     while prompt.find(answer) == -1:
1205         answer = utils.our_raw_input(prompt)
1206         m = re_default_answer.match(prompt)
1207         if answer == "":
1208             answer = m.group(1)
1209         answer = answer[:1].upper()
1210
1211     if answer == 'R':
1212         os.chdir(u.pkg.directory)
1213         u.do_reject(0, pi)
1214     elif answer == 'A':
1215         u.accept(summary, short_summary)
1216         u.check_override()
1217         u.remove()
1218     elif answer == queuekey:
1219         queue_info[qu]["process"](u, summary, short_summary)
1220         u.remove()
1221     elif answer == 'Q':
1222         sys.exit(0)
1223
1224 ################################################################################
1225
1226 def package_to_suite(u, suite):
1227     if not u.pkg.changes["distribution"].has_key(suite):
1228         return False
1229
1230     ret = True
1231
1232     if not u.pkg.changes["architecture"].has_key("source"):
1233         s = DBConn().session()
1234         q = s.query(SrcAssociation.sa_id)
1235         q = q.join(Suite).filter_by(suite_name=suite)
1236         q = q.join(DBSource).filter_by(source=u.pkg.changes['source'])
1237         q = q.filter_by(version=u.pkg.changes['version']).limit(1)
1238
1239         if q.count() < 1:
1240             ret = False
1241
1242         s.close()
1243
1244     return ret
1245
1246 def package_to_queue(u, summary, short_summary, queue, perms=0660, build=True, announce=None):
1247     cnf = Config()
1248     dir = cnf["Dir::Queue::%s" % queue]
1249
1250     print "Moving to %s holding area" % queue.upper()
1251     Logger.log(["Moving to %s" % queue, u.pkg.changes_file])
1252
1253     u.pkg.write_dot_dak(dir)
1254     u.move_to_dir(dir, perms=perms)
1255     if build:
1256         get_queue(queue.lower()).autobuild_upload(u.pkg, dir)
1257
1258     # Check for override disparities
1259     u.check_override()
1260
1261     # Send accept mail, announce to lists and close bugs
1262     if announce and not cnf["Dinstall::Options::No-Mail"]:
1263         template = os.path.join(cnf["Dir::Templates"], announce)
1264         u.update_subst()
1265         u.Subst["__SUITE__"] = ""
1266         mail_message = utils.TemplateSubst(u.Subst, template)
1267         utils.send_mail(mail_message)
1268         u.announce(short_summary, True)
1269
1270 ################################################################################
1271
1272 def is_unembargo(u):
1273     session = DBConn().session()
1274     cnf = Config()
1275
1276     q = session.execute("SELECT package FROM disembargo WHERE package = :source AND version = :version", u.pkg.changes)
1277     if q.rowcount > 0:
1278         session.close()
1279         return True
1280
1281     oldcwd = os.getcwd()
1282     os.chdir(cnf["Dir::Queue::Disembargo"])
1283     disdir = os.getcwd()
1284     os.chdir(oldcwd)
1285
1286     ret = False
1287
1288     if u.pkg.directory == disdir:
1289         if u.pkg.changes["architecture"].has_key("source"):
1290             if not Options["No-Action"]:
1291                 session.execute("INSERT INTO disembargo (package, version) VALUES (:package, :version)", u.pkg.changes)
1292                 session.commit()
1293
1294             ret = True
1295
1296     session.close()
1297
1298     return ret
1299
1300 def queue_unembargo(u, summary, short_summary):
1301     return package_to_queue(u, summary, short_summary, "Unembargoed",
1302                             perms=0660, build=True, announce='process-unchecked.accepted')
1303
1304 ################################################################################
1305
1306 def is_embargo(u):
1307     # if embargoed queues are enabled always embargo
1308     return True
1309
1310 def queue_embargo(u, summary, short_summary):
1311     return package_to_queue(u, summary, short_summary, "Unembargoed",
1312                             perms=0660, build=True, announce='process-unchecked.accepted')
1313
1314 ################################################################################
1315
1316 def is_stableupdate(u):
1317     return package_to_suite(u, 'proposed-updates')
1318
1319 def do_stableupdate(u, summary, short_summary):
1320     return package_to_queue(u, summary, short_summary, "ProposedUpdates",
1321                             perms=0664, build=False, announce=None)
1322
1323 ################################################################################
1324
1325 def is_oldstableupdate(u):
1326     return package_to_suite(u, 'oldstable-proposed-updates')
1327
1328 def do_oldstableupdate(u, summary, short_summary):
1329     return package_to_queue(u, summary, short_summary, "OldProposedUpdates",
1330                             perms=0664, build=False, announce=None)
1331
1332 ################################################################################
1333
1334 def is_autobyhand(u):
1335     cnf = Config()
1336
1337     all_auto = 1
1338     any_auto = 0
1339     for f in u.pkg.files.keys():
1340         if u.pkg.files[f].has_key("byhand"):
1341             any_auto = 1
1342
1343             # filename is of form "PKG_VER_ARCH.EXT" where PKG, VER and ARCH
1344             # don't contain underscores, and ARCH doesn't contain dots.
1345             # further VER matches the .changes Version:, and ARCH should be in
1346             # the .changes Architecture: list.
1347             if f.count("_") < 2:
1348                 all_auto = 0
1349                 continue
1350
1351             (pckg, ver, archext) = f.split("_", 2)
1352             if archext.count(".") < 1 or u.pkg.changes["version"] != ver:
1353                 all_auto = 0
1354                 continue
1355
1356             ABH = cnf.SubTree("AutomaticByHandPackages")
1357             if not ABH.has_key(pckg) or \
1358               ABH["%s::Source" % (pckg)] != u.pkg.changes["source"]:
1359                 print "not match %s %s" % (pckg, u.pkg.changes["source"])
1360                 all_auto = 0
1361                 continue
1362
1363             (arch, ext) = archext.split(".", 1)
1364             if arch not in u.pkg.changes["architecture"]:
1365                 all_auto = 0
1366                 continue
1367
1368             u.pkg.files[f]["byhand-arch"] = arch
1369             u.pkg.files[f]["byhand-script"] = ABH["%s::Script" % (pckg)]
1370
1371     return any_auto and all_auto
1372
1373 def do_autobyhand(u, summary, short_summary):
1374     print "Attempting AUTOBYHAND."
1375     byhandleft = True
1376     for f, entry in u.pkg.files.items():
1377         byhandfile = f
1378
1379         if not entry.has_key("byhand"):
1380             continue
1381
1382         if not entry.has_key("byhand-script"):
1383             byhandleft = True
1384             continue
1385
1386         os.system("ls -l %s" % byhandfile)
1387
1388         result = os.system("%s %s %s %s %s" % (
1389                 entry["byhand-script"],
1390                 byhandfile,
1391                 u.pkg.changes["version"],
1392                 entry["byhand-arch"],
1393                 os.path.abspath(u.pkg.changes_file)))
1394
1395         if result == 0:
1396             os.unlink(byhandfile)
1397             del entry
1398         else:
1399             print "Error processing %s, left as byhand." % (f)
1400             byhandleft = True
1401
1402     if byhandleft:
1403         do_byhand(u, summary, short_summary)
1404     else:
1405         u.accept(summary, short_summary)
1406         u.check_override()
1407         # XXX: We seem to be missing a u.remove() here
1408         #      This might explain why we get byhand leftovers in unchecked - mhy
1409
1410 ################################################################################
1411
1412 def is_byhand(u):
1413     for f in u.pkg.files.keys():
1414         if u.pkg.files[f].has_key("byhand"):
1415             return True
1416     return False
1417
1418 def do_byhand(u, summary, short_summary):
1419     return package_to_queue(u, summary, short_summary, "Byhand",
1420                             perms=0660, build=False, announce=None)
1421
1422 ################################################################################
1423
1424 def is_new(u):
1425     for f in u.pkg.files.keys():
1426         if u.pkg.files[f].has_key("new"):
1427             return True
1428     return False
1429
1430 def acknowledge_new(u, summary, short_summary):
1431     cnf = Config()
1432
1433     print "Moving to NEW holding area."
1434     Logger.log(["Moving to new", u.pkg.changes_file])
1435
1436     u.pkg.write_dot_dak(cnf["Dir::Queue::New"])
1437     u.move_to_dir(cnf["Dir::Queue::New"], perms=0640, changesperms=0644)
1438
1439     if not Options["No-Mail"]:
1440         print "Sending new ack."
1441         template = os.path.join(cnf["Dir::Templates"], 'process-unchecked.new')
1442         u.Subst["__SUMMARY__"] = summary
1443         new_ack_message = utils.TemplateSubst(u.Subst, template)
1444         utils.send_mail(new_ack_message)
1445
1446 ################################################################################
1447
1448 # reprocess is necessary for the case of foo_1.2-1 and foo_1.2-2 in
1449 # Incoming. -1 will reference the .orig.tar.gz, but -2 will not.
1450 # Upload.check_dsc_against_db() can find the .orig.tar.gz but it will
1451 # not have processed it during it's checks of -2.  If -1 has been
1452 # deleted or otherwise not checked by 'dak process-unchecked', the
1453 # .orig.tar.gz will not have been checked at all.  To get round this,
1454 # we force the .orig.tar.gz into the .changes structure and reprocess
1455 # the .changes file.
1456
1457 def process_it(changes_file):
1458     global Logger
1459
1460     cnf = Config()
1461
1462     holding = Holding()
1463
1464     u = Upload()
1465     u.pkg.changes_file = changes_file
1466     u.pkg.directory = os.getcwd()
1467     u.logger = Logger
1468
1469     # Some defaults in case we can't fully process the .changes file
1470     u.pkg.changes["maintainer2047"] = cnf["Dinstall::MyEmailAddress"]
1471     u.pkg.changes["changedby2047"] = cnf["Dinstall::MyEmailAddress"]
1472
1473     # debian-{devel-,}-changes@lists.debian.org toggles writes access based on this header
1474     bcc = "X-DAK: dak process-unchecked\nX-Katie: $Revision: 1.65 $"
1475     if cnf.has_key("Dinstall::Bcc"):
1476         u.Subst["__BCC__"] = bcc + "\nBcc: %s" % (cnf["Dinstall::Bcc"])
1477     else:
1478         u.Subst["__BCC__"] = bcc
1479
1480     # Remember where we are so we can come back after cd-ing into the
1481     # holding directory.  TODO: Fix this stupid hack
1482     u.prevdir = os.getcwd()
1483
1484     # TODO: Figure out something better for this (or whether it's even
1485     #       necessary - it seems to have been for use when we were
1486     #       still doing the is_unchecked check; reprocess = 2)
1487     u.reprocess = 1
1488
1489     try:
1490         # If this is the Real Thing(tm), copy things into a private
1491         # holding directory first to avoid replacable file races.
1492         if not Options["No-Action"]:
1493             os.chdir(cnf["Dir::Queue::Holding"])
1494
1495             # Absolutize the filename to avoid the requirement of being in the
1496             # same directory as the .changes file.
1497             holding.copy_to_holding(os.path.abspath(changes_file))
1498
1499             # Relativize the filename so we use the copy in holding
1500             # rather than the original...
1501             changespath = os.path.basename(u.pkg.changes_file)
1502
1503         (u.pkg.changes["fingerprint"], rejects) = utils.check_signature(changespath)
1504
1505         if u.pkg.changes["fingerprint"]:
1506             valid_changes_p = u.load_changes(changespath)
1507         else:
1508             valid_changes_p = False
1509             u.rejects.extend(rejects)
1510
1511         if valid_changes_p:
1512             while u.reprocess:
1513                 u.check_distributions()
1514                 u.check_files(not Options["No-Action"])
1515                 valid_dsc_p = u.check_dsc(not Options["No-Action"])
1516                 if valid_dsc_p:
1517                     u.check_source()
1518                 u.check_hashes()
1519                 u.check_urgency()
1520                 u.check_timestamps()
1521                 u.check_signed_by_key()
1522
1523         action(u)
1524
1525     except SystemExit:
1526         raise
1527
1528     except:
1529         print "ERROR"
1530         traceback.print_exc(file=sys.stderr)
1531
1532     # Restore previous WD
1533     os.chdir(u.prevdir)
1534
1535 ###############################################################################
1536
1537 def main():
1538     global Options, Logger
1539
1540     cnf = Config()
1541     changes_files = init()
1542
1543     # -n/--dry-run invalidates some other options which would involve things happening
1544     if Options["No-Action"]:
1545         Options["Automatic"] = ""
1546
1547     # Initialize our Holding singleton
1548     holding = Holding()
1549
1550     # Ensure all the arguments we were given are .changes files
1551     for f in changes_files:
1552         if not f.endswith(".changes"):
1553             utils.warn("Ignoring '%s' because it's not a .changes file." % (f))
1554             changes_files.remove(f)
1555
1556     if changes_files == []:
1557         if cnf["Dinstall::Options::Directory"] == "":
1558             utils.fubar("Need at least one .changes file as an argument.")
1559         else:
1560             sys.exit(0)
1561
1562     # Check that we aren't going to clash with the daily cron job
1563     if not Options["No-Action"] and os.path.exists("%s/daily.lock" % (cnf["Dir::Lock"])) and not Options["No-Lock"]:
1564         utils.fubar("Archive maintenance in progress.  Try again later.")
1565
1566     # Obtain lock if not in no-action mode and initialize the log
1567     if not Options["No-Action"]:
1568         lock_fd = os.open(cnf["Dinstall::LockFile"], os.O_RDWR | os.O_CREAT)
1569         try:
1570             fcntl.lockf(lock_fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
1571         except IOError, e:
1572             if errno.errorcode[e.errno] == 'EACCES' or errno.errorcode[e.errno] == 'EAGAIN':
1573                 utils.fubar("Couldn't obtain lock; assuming another 'dak process-unchecked' is already running.")
1574             else:
1575                 raise
1576         Logger = daklog.Logger(cnf, "process-unchecked")
1577
1578     # Sort the .changes files so that we process sourceful ones first
1579     changes_files.sort(utils.changes_compare)
1580
1581     # Process the changes files
1582     for changes_file in changes_files:
1583         print "\n" + changes_file
1584         try:
1585             process_it (changes_file)
1586         finally:
1587             if not Options["No-Action"]:
1588                 holding.clean()
1589
1590     accept_count = SummaryStats().accept_count
1591     accept_bytes = SummaryStats().accept_bytes
1592
1593     if accept_count:
1594         sets = "set"
1595         if accept_count > 1:
1596             sets = "sets"
1597         print "Accepted %d package %s, %s." % (accept_count, sets, utils.size_type(int(accept_bytes)))
1598         Logger.log(["total",accept_count,accept_bytes])
1599
1600     if not Options["No-Action"]:
1601         Logger.close()
1602
1603 ################################################################################
1604
1605 if __name__ == '__main__':
1606     main()