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