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