]> git.decadent.org.uk Git - dak.git/blob - katie
changed-by support, cc manual reject mails to ftpmaster only, use whoami from utils.
[dak.git] / katie
1 #!/usr/bin/env python
2
3 # Installs Debian packaes
4 # Copyright (C) 2000, 2001  James Troup <james@nocrew.org>
5 # $Id: katie,v 1.38 2001-04-13 20:18:41 troup Exp $
6
7 # This program is free software; you can redistribute it and/or modify
8 # it under the terms of the GNU General Public License as published by
9 # the Free Software Foundation; either version 2 of the License, or
10 # (at your option) any later version.
11
12 # This program is distributed in the hope that it will be useful,
13 # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15 # GNU General Public License for more details.
16
17 # You should have received a copy of the GNU General Public License
18 # along with this program; if not, write to the Free Software
19 # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
20
21 # Originally based almost entirely on dinstall by Guy Maor <maor@debian.org>
22
23 #########################################################################################
24
25 #    Cartman: "I'm trying to make the best of a bad situation, I don't
26 #              need to hear crap from a bunch of hippy freaks living in
27 #              denial.  Screw you guys, I'm going home."
28 #
29 #    Kyle: "But Cartman, we're trying to..."
30 #
31 #    Cartman: "uhh.. screw you guys... home."
32
33 #########################################################################################
34
35 import FCNTL, commands, fcntl, getopt, gzip, os, pg, pwd, re, shutil, stat, string, sys, tempfile, time
36 import apt_inst, apt_pkg
37 import utils, db_access
38
39 ###############################################################################
40
41 re_isanum = re.compile (r'^\d+$');
42 re_changes = re.compile (r'changes$');
43 re_default_answer = re.compile(r"\[(.*)\]");
44 re_fdnic = re.compile("\n\n");
45 re_bad_diff = re.compile("^[\-\+][\-\+][\-\+] /dev/null");
46
47 #########################################################################################
48
49 # Globals
50 Cnf = None;
51 reject_message = "";
52 changes = {};
53 dsc = {};
54 dsc_files = {};
55 files = {};
56 projectB = None;
57 new_ack_new = {};
58 new_ack_old = {};
59 install_count = 0;
60 install_bytes = 0.0;
61 reprocess = 0;
62 orig_tar_id = None;
63 orig_tar_location = "";
64 legacy_source_untouchable = {};
65 Subst = {};
66
67 #########################################################################################
68
69 def usage (exit_code):
70     print """Usage: dinstall [OPTION]... [CHANGES]...
71   -a, --automatic           automatic run
72   -D, --debug=VALUE         turn on debugging
73   -h, --help                show this help and exit.
74   -k, --ack-new             acknowledge new packages !! for cron.daily only !!
75   -m, --manual-reject=MSG   manual reject with `msg'
76   -n, --no-action           don't do anything
77   -p, --no-lock             don't check lockfile !! for cron.daily only !!
78   -u, --distribution=DIST   override distribution to `dist'
79   -v, --version             display the version number and exit"""
80     sys.exit(exit_code)
81
82 #########################################################################################
83
84 def check_signature (filename):
85     global reject_message
86
87     (result, output) = commands.getstatusoutput("gpg --emulate-md-encode-bug --batch --no-options --no-default-keyring --always-trust --keyring=%s --keyring=%s < %s >/dev/null" % (Cnf["Dinstall::PGPKeyring"], Cnf["Dinstall::GPGKeyring"], filename))
88     if (result != 0):
89         reject_message = "Rejected: GPG signature check failed on `%s'.\n%s\n" % (os.path.basename(filename), output)
90         return 0
91     return 1
92
93 #####################################################################################################################
94
95 # See if a given package is in the override table
96
97 def in_override_p (package, component, suite, binary_type, file):
98     global files;
99     
100     if binary_type == "": # must be source
101         type = "dsc";
102     else:
103         type = binary_type;
104
105     # Override suite name; used for example with proposed-updates
106     if Cnf.Find("Suite::%s::OverrideSuite" % (suite)) != "":
107         suite = Cnf["Suite::%s::OverrideSuite" % (suite)];
108
109     # Avoid <undef> on unknown distributions
110     suite_id = db_access.get_suite_id(suite);
111     if suite_id == -1:
112         return None;
113     component_id = db_access.get_component_id(component);
114     type_id = db_access.get_override_type_id(type);
115
116     # FIXME: nasty non-US speficic hack
117     if string.lower(component[:7]) == "non-us/":
118         component = component[7:];
119
120     q = projectB.query("SELECT s.section, p.priority FROM override o, section s, priority p WHERE package = '%s' AND suite = %s AND component = %s AND type = %s AND o.section = s.id AND o.priority = p.id"
121                        % (package, suite_id, component_id, type_id));
122     result = q.getresult();
123     # If checking for a source package fall back on the binary override type
124     if type == "dsc" and not result:
125         type_id = db_access.get_override_type_id("deb");
126         q = projectB.query("SELECT s.section, p.priority FROM override o, section s, priority p WHERE package = '%s' AND suite = %s AND component = %s AND type = %s AND o.section = s.id AND o.priority = p.id"
127                            % (package, suite_id, component_id, type_id));
128         result = q.getresult();
129
130     # Remember the section and priority so we can check them later if appropriate
131     if result != []:
132         files[file]["override section"] = result[0][0];
133         files[file]["override priority"] = result[0][1];
134         
135     return result;
136
137 #####################################################################################################################
138
139 def check_changes(filename):
140     global reject_message, changes, files
141
142     # Default in case we bail out
143     (changes["changedby822"], changes["maintainer822"]) = Cnf["Dinstall::MyEmailAddress"]; 
144
145     # Parse the .changes field into a dictionary
146     try:
147         changes = utils.parse_changes(filename, 0)
148     except utils.cant_open_exc:
149         reject_message = "Rejected: can't read changes file '%s'.\n" % (filename)
150         return 0;
151     except utils.changes_parse_error_exc, line:
152         reject_message = "Rejected: error parsing changes file '%s', can't grok: %s.\n" % (filename, line)
153         return 0;
154
155     # Parse the Files field from the .changes into another dictionary
156     try:
157         files = utils.build_file_list(changes, "");
158     except utils.changes_parse_error_exc, line:
159         reject_message = "Rejected: error parsing changes file '%s', can't grok: %s.\n" % (filename, line);
160
161     # Check for mandatory fields
162     for i in ("source", "binary", "architecture", "version", "distribution","maintainer", "files"):
163         if not changes.has_key(i):
164             reject_message = "Rejected: Missing field `%s' in changes file.\n" % (i)
165             return 0    # Avoid <undef> errors during later tests
166
167     # Override the Distribution: field if appropriate
168     if Cnf["Dinstall::Options::Override-Distribution"] != "":
169         reject_message = reject_message + "Warning: Distribution was overriden from %s to %s.\n" % (changes["distribution"], Cnf["Dinstall::Options::Override-Distribution"])
170         changes["distribution"] = Cnf["Dinstall::Options::Override-Distribution"]
171
172     # Split multi-value fields into a lower-level dictionary
173     for i in ("architecture", "distribution", "binary", "closes"):
174         o = changes.get(i, "")
175         if o != "":
176             del changes[i]
177         changes[i] = {}
178         for j in string.split(o):
179             changes[i][j] = 1
180
181     # Fix the Maintainer: field to be RFC822 compatible
182     (changes["maintainer822"], changes["maintainername"], changes["maintaineremail"]) = utils.fix_maintainer (changes["maintainer"])
183
184     # Fix the Changed-By: field to be RFC822 compatible; if it exists.
185     (changes["changedby822"], changes["changedbyname"], changes["changedbyemail"]) = utils.fix_maintainer(changes.get("changed-by",""));
186
187     # Ensure all the values in Closes: are numbers
188     if changes.has_key("closes"):
189         for i in changes["closes"].keys():
190             if re_isanum.match (i) == None:
191                 reject_message = reject_message + "Rejected: `%s' from Closes field isn't a number.\n" % (i)
192
193     # Map frozen to unstable if frozen doesn't exist
194     if changes["distribution"].has_key("frozen") and not Cnf.has_key("Suite::Frozen"):
195         del changes["distribution"]["frozen"]
196         changes["distribution"]["unstable"] = 1;
197         reject_message = reject_message + "Mapping frozen to unstable.\n"
198
199     # Map testing to unstable
200     if changes["distribution"].has_key("testing"):
201         del changes["distribution"]["testing"]
202         changes["distribution"]["unstable"] = 1;
203         reject_message = reject_message + "Mapping testing to unstable.\n"
204
205     # Ensure target distributions exist
206     for i in changes["distribution"].keys():
207         if not Cnf.has_key("Suite::%s" % (i)):
208             reject_message = reject_message + "Rejected: Unknown distribution `%s'.\n" % (i)
209
210     # Ensure there _is_ a target distribution
211     if changes["distribution"].keys() == []:
212         reject_message = reject_message + "Rejected: huh? Distribution field is empty in changes file.\n";
213             
214     # Map unreleased arches from stable to unstable
215     if changes["distribution"].has_key("stable"):
216         for i in changes["architecture"].keys():
217             if not Cnf.has_key("Suite::Stable::Architectures::%s" % (i)):
218                 reject_message = reject_message + "Mapping stable to unstable for unreleased arch `%s'.\n" % (i)
219                 del changes["distribution"]["stable"]
220                 changes["distribution"]["unstable"] = 1;
221     
222     # Map arches not being released from frozen to unstable
223     if changes["distribution"].has_key("frozen"):
224         for i in changes["architecture"].keys():
225             if not Cnf.has_key("Suite::Frozen::Architectures::%s" % (i)):
226                 reject_message = reject_message + "Mapping frozen to unstable for non-releasing arch `%s'.\n" % (i)
227                 del changes["distribution"]["frozen"]
228                 changes["distribution"]["unstable"] = 1;
229
230     # Handle uploads to stable
231     if changes["distribution"].has_key("stable"):
232         # If running from within proposed-updates; assume an install to stable
233         if string.find(os.getcwd(), 'proposed-updates') != -1:
234             # FIXME: should probably remove anything that != stable
235             for i in ("frozen", "unstable"):
236                 if changes["distribution"].has_key(i):
237                     reject_message = reject_message + "Removing %s from distribution list.\n"
238                     del changes["distribution"][i]
239             changes["stable upload"] = 1;
240             # If we can't find a file from the .changes; assume it's a package already in the pool and move into the pool
241             file = files.keys()[0];
242             if os.access(file, os.R_OK) == 0:
243                 pool_dir = Cnf["Dir::PoolDir"] + '/' + utils.poolify(changes["source"], files[file]["component"]);
244                 os.chdir(pool_dir);
245         # Otherwise (normal case) map stable to updates
246         else:
247             reject_message = reject_message + "Mapping stable to updates.\n";
248             del changes["distribution"]["stable"];
249             changes["distribution"]["proposed-updates"] = 1;
250
251     # chopversion = no epoch; chopversion2 = no epoch and no revision (e.g. for .orig.tar.gz comparison)
252     changes["chopversion"] = utils.re_no_epoch.sub('', changes["version"])
253     changes["chopversion2"] = utils.re_no_revision.sub('', changes["chopversion"])
254     
255     if string.find(reject_message, "Rejected:") != -1:
256         return 0
257     else: 
258         return 1
259
260 def check_files():
261     global reject_message
262     
263     archive = utils.where_am_i();
264
265     for file in files.keys():
266         # Check the file is readable
267         if os.access(file,os.R_OK) == 0:
268             reject_message = reject_message + "Rejected: Can't read `%s'.\n" % (file)
269             files[file]["type"] = "unreadable";
270             continue
271         # If it's byhand skip remaining checks
272         if files[file]["section"] == "byhand":
273             files[file]["byhand"] = 1;
274             files[file]["type"] = "byhand";
275         # Checks for a binary package...
276         elif utils.re_isadeb.match(file) != None:
277             files[file]["type"] = "deb";
278
279             # Extract package information using dpkg-deb
280             try:
281                 control = apt_pkg.ParseSection(apt_inst.debExtractControl(utils.open_file(file,"r")))
282             except:
283                 reject_message = reject_message + "Rejected: %s: debExtractControl() raised %s.\n" % (file, sys.exc_type);
284                 # Can't continue, none of the checks on control would work.
285                 continue;
286
287             # Check for mandatory fields
288             if control.Find("Package") == None:
289                 reject_message = reject_message + "Rejected: %s: No package field in control.\n" % (file)
290             if control.Find("Architecture") == None:
291                 reject_message = reject_message + "Rejected: %s: No architecture field in control.\n" % (file)
292             if control.Find("Version") == None:
293                 reject_message = reject_message + "Rejected: %s: No version field in control.\n" % (file)
294                 
295             # Ensure the package name matches the one give in the .changes
296             if not changes["binary"].has_key(control.Find("Package", "")):
297                 reject_message = reject_message + "Rejected: %s: control file lists name as `%s', which isn't in changes file.\n" % (file, control.Find("Package", ""))
298
299             # Validate the architecture
300             if not Cnf.has_key("Suite::Unstable::Architectures::%s" % (control.Find("Architecture", ""))):
301                 reject_message = reject_message + "Rejected: Unknown architecture '%s'.\n" % (control.Find("Architecture", ""))
302
303             # Check the architecture matches the one given in the .changes
304             if not changes["architecture"].has_key(control.Find("Architecture", "")):
305                 reject_message = reject_message + "Rejected: %s: control file lists arch as `%s', which isn't in changes file.\n" % (file, control.Find("Architecture", ""))
306             # Check the section & priority match those given in the .changes (non-fatal)
307             if control.Find("Section") != None and files[file]["section"] != "" and files[file]["section"] != control.Find("Section"):
308                 reject_message = reject_message + "Warning: %s control file lists section as `%s', but changes file has `%s'.\n" % (file, control.Find("Section", ""), files[file]["section"])
309             if control.Find("Priority") != None and files[file]["priority"] != "" and files[file]["priority"] != control.Find("Priority"):
310                 reject_message = reject_message + "Warning: %s control file lists priority as `%s', but changes file has `%s'.\n" % (file, control.Find("Priority", ""), files[file]["priority"])
311
312             epochless_version = utils.re_no_epoch.sub('', control.Find("Version", ""))
313
314             files[file]["package"] = control.Find("Package");
315             files[file]["architecture"] = control.Find("Architecture");
316             files[file]["version"] = control.Find("Version");
317             files[file]["maintainer"] = control.Find("Maintainer", "");
318             if file[-5:] == ".udeb":
319                 files[file]["dbtype"] = "udeb";
320             elif file[-4:] == ".deb":
321                 files[file]["dbtype"] = "deb";
322             else:
323                 reject_message = reject_message + "Rejected: %s is neither a .deb or a .udeb.\n " % (file);
324             files[file]["fullname"] = "%s_%s_%s.deb" % (control.Find("Package", ""), epochless_version, control.Find("Architecture", ""))
325             files[file]["source"] = control.Find("Source", "");
326             if files[file]["source"] == "":
327                 files[file]["source"] = files[file]["package"];
328         # Checks for a source package...
329         else:
330             m = utils.re_issource.match(file)
331             if m != None:
332                 files[file]["package"] = m.group(1)
333                 files[file]["version"] = m.group(2)
334                 files[file]["type"] = m.group(3)
335                 
336                 # Ensure the source package name matches the Source filed in the .changes
337                 if changes["source"] != files[file]["package"]:
338                     reject_message = reject_message + "Rejected: %s: changes file doesn't say %s for Source\n" % (file, files[file]["package"])
339
340                 # Ensure the source version matches the version in the .changes file
341                 if files[file]["type"] == "orig.tar.gz":
342                     changes_version = changes["chopversion2"]
343                 else:
344                     changes_version = changes["chopversion"]
345                 if changes_version != files[file]["version"]:
346                     reject_message = reject_message + "Rejected: %s: should be %s according to changes file.\n" % (file, changes_version)
347
348                 # Ensure the .changes lists source in the Architecture field
349                 if not changes["architecture"].has_key("source"):
350                     reject_message = reject_message + "Rejected: %s: changes file doesn't list `source' in Architecture field.\n" % (file)
351
352                 # Check the signature of a .dsc file
353                 if files[file]["type"] == "dsc":
354                     check_signature(file)
355
356                 files[file]["fullname"] = file
357
358             # Not a binary or source package?  Assume byhand...
359             else:
360                 files[file]["byhand"] = 1;
361                 files[file]["type"] = "byhand";
362
363         files[file]["oldfiles"] = {}
364         for suite in changes["distribution"].keys():
365             # Skip byhand
366             if files[file].has_key("byhand"):
367                 continue
368
369             if Cnf.has_key("Suite:%s::Components" % (suite)) and not Cnf.has_key("Suite::%s::Components::%s" % (suite, files[file]["component"])):
370                 reject_message = reject_message + "Rejected: unknown component `%s' for suite `%s'.\n" % (files[file]["component"], suite)
371                 continue
372
373             # See if the package is NEW
374             if not in_override_p(files[file]["package"], files[file]["component"], suite, files[file].get("dbtype",""), file):
375                 files[file]["new"] = 1
376                 
377             # Find any old binary packages
378             if files[file]["type"] == "deb":
379                 q = projectB.query("SELECT b.id, b.version, f.filename, l.path, c.name FROM binaries b, bin_associations ba, suite s, location l, component c, architecture a, files f WHERE b.package = '%s' AND s.suite_name = '%s' AND a.arch_string = '%s' AND ba.bin = b.id AND ba.suite = s.id AND b.architecture = a.id AND f.location = l.id AND l.component = c.id AND b.file = f.id"
380                                    % (files[file]["package"], suite, files[file]["architecture"]))
381                 oldfiles = q.dictresult()
382                 for oldfile in oldfiles:
383                     files[file]["oldfiles"][suite] = oldfile
384                     # Check versions [NB: per-suite only; no cross-suite checking done (yet)]
385                     if apt_pkg.VersionCompare(files[file]["version"], oldfile["version"]) != 1:
386                         reject_message = reject_message + "Rejected: %s Old version `%s' >= new version `%s'.\n" % (file, oldfile["version"], files[file]["version"])
387                 # Check for existing copies of the file
388                 if not changes.has_key("stable upload"):
389                     q = projectB.query("SELECT b.id FROM binaries b, architecture a WHERE b.package = '%s' AND b.version = '%s' AND a.arch_string = '%s' AND a.id = b.architecture" % (files[file]["package"], files[file]["version"], files[file]["architecture"]))
390                     if q.getresult() != []:
391                         reject_message = reject_message + "Rejected: can not overwrite existing copy of '%s' already in the archive.\n" % (file)
392
393             # Find any old .dsc files
394             elif files[file]["type"] == "dsc":
395                 q = projectB.query("SELECT s.id, s.version, f.filename, l.path, c.name FROM source s, src_associations sa, suite su, location l, component c, files f WHERE s.source = '%s' AND su.suite_name = '%s' AND sa.source = s.id AND sa.suite = su.id AND f.location = l.id AND l.component = c.id AND f.id = s.file"
396                                    % (files[file]["package"], suite))
397                 oldfiles = q.dictresult()
398                 if len(oldfiles) >= 1:
399                     files[file]["oldfiles"][suite] = oldfiles[0]
400
401             # Validate the component
402             component = files[file]["component"];
403             component_id = db_access.get_component_id(component);
404             if component_id == -1:
405                 reject_message = reject_message + "Rejected: file '%s' has unknown component '%s'.\n" % (file, component);
406                 continue;
407
408             # Validate the priority
409             if string.find(files[file]["priority"],'/') != -1:
410                 reject_message = reject_message + "Rejected: file '%s' has invalid priority '%s' [contains '/'].\n" % (file, files[file]["priority"]);
411             
412             # Check the md5sum & size against existing files (if any)
413             location = Cnf["Dir::PoolDir"];
414             files[file]["location id"] = db_access.get_location_id (location, component, archive);
415
416             files[file]["pool name"] = utils.poolify (changes["source"], files[file]["component"]);
417             files_id = db_access.get_files_id(files[file]["pool name"] + file, files[file]["size"], files[file]["md5sum"], files[file]["location id"]);
418             if files_id == -1:
419                 reject_message = reject_message + "Rejected: INTERNAL ERROR, get_files_id() returned multiple matches for %s.\n" % (file)
420             elif files_id == -2:
421                 reject_message = reject_message + "Rejected: md5sum and/or size mismatch on existing copy of %s.\n" % (file)
422             files[file]["files id"] = files_id
423
424             # Check for packages that have moved from one component to another
425             if files[file]["oldfiles"].has_key(suite) and files[file]["oldfiles"][suite]["name"] != files[file]["component"]:
426                 files[file]["othercomponents"] = files[file]["oldfiles"][suite]["name"];
427
428                 
429     if string.find(reject_message, "Rejected:") != -1:
430         return 0
431     else: 
432         return 1
433
434 ###############################################################################
435
436 def check_dsc ():
437     global dsc, dsc_files, reject_message, reprocess, orig_tar_id, orig_tar_location, legacy_source_untouchable;
438
439     for file in files.keys():
440         if files[file]["type"] == "dsc":
441             try:
442                 dsc = utils.parse_changes(file, 1)
443             except utils.cant_open_exc:
444                 reject_message = reject_message + "Rejected: can't read changes file '%s'.\n" % (file)
445                 return 0;
446             except utils.changes_parse_error_exc, line:
447                 reject_message = reject_message + "Rejected: error parsing changes file '%s', can't grok: %s.\n" % (file, line)
448                 return 0;
449             except utils.invalid_dsc_format_exc, line:
450                 reject_message = reject_message + "Rejected: syntax error in .dsc file '%s', line %s.\n" % (file, line)
451                 return 0;
452             try:
453                 dsc_files = utils.build_file_list(dsc, 1)
454             except utils.no_files_exc:
455                 reject_message = reject_message + "Rejected: no Files: field in .dsc file.\n";
456                 continue;
457             except utils.changes_parse_error_exc, line:
458                 reject_message = "Rejected: error parsing .dsc file '%s', can't grok: %s.\n" % (file, line);
459                 continue;
460
461             # Try and find all files mentioned in the .dsc.  This has
462             # to work harder to cope with the multiple possible
463             # locations of an .orig.tar.gz.
464             for dsc_file in dsc_files.keys():
465                 if files.has_key(dsc_file):
466                     actual_md5 = files[dsc_file]["md5sum"];
467                     actual_size = int(files[dsc_file]["size"]);
468                     found = "%s in incoming" % (dsc_file)
469                     # Check the file does not already exist in the archive
470                     if not changes.has_key("stable upload"):
471                         q = projectB.query("SELECT f.id FROM files f, location l WHERE (f.filename ~ '/%s$' OR f.filename = '%s') AND l.id = f.location" % (utils.regex_safe(dsc_file), dsc_file));
472
473                         # "It has not broken them.  It has fixed a
474                         # brokenness.  Your crappy hack exploited a
475                         # bug in the old dinstall.
476                         #
477                         # "(Come on!  I thought it was always obvious
478                         # that one just doesn't release different
479                         # files with the same name and version.)"
480                         #                        -- ajk@ on d-devel@l.d.o
481
482                         if q.getresult() != []:
483                             reject_message = reject_message + "Rejected: can not overwrite existing copy of '%s' already in the archive.\n" % (dsc_file)
484                 elif dsc_file[-12:] == ".orig.tar.gz":
485                     # Check in the pool
486                     q = projectB.query("SELECT l.path, f.filename, l.type, f.id, l.id FROM files f, location l WHERE (f.filename ~ '/%s$' OR f.filename = '%s') AND l.id = f.location" % (utils.regex_safe(dsc_file), dsc_file));
487                     ql = q.getresult();
488
489                     if ql != []:
490                         # Unfortunately, we make get more than one match
491                         # here if, for example, the package was in potato
492                         # but had a -sa upload in woody.  So we need to a)
493                         # choose the right one and b) mark all wrong ones
494                         # as excluded from the source poolification (to
495                         # avoid file overwrites).
496
497                         x = ql[0]; # default to something sane in case we don't match any or have only one
498
499                         if len(ql) > 1:
500                             for i in ql:
501                                 old_file = i[0] + i[1];
502                                 actual_md5 = apt_pkg.md5sum(utils.open_file(old_file,"r"));
503                                 actual_size = os.stat(old_file)[stat.ST_SIZE];
504                                 if actual_md5 == dsc_files[dsc_file]["md5sum"] and actual_size == int(dsc_files[dsc_file]["size"]):
505                                     x = i;
506                                 else:
507                                     legacy_source_untouchable[i[3]] = "";
508
509                         old_file = x[0] + x[1];
510                         actual_md5 = apt_pkg.md5sum(utils.open_file(old_file,"r"));
511                         actual_size = os.stat(old_file)[stat.ST_SIZE];
512                         found = old_file;
513                         suite_type = x[2];
514                         dsc_files[dsc_file]["files id"] = x[3]; # need this for updating dsc_files in install()
515                         # See install()...
516                         orig_tar_id = x[3];
517                         if suite_type == "legacy" or suite_type == "legacy-mixed":
518                             orig_tar_location = "legacy";
519                         else:
520                             orig_tar_location = x[4];
521                     else:
522                         # Not there? Check in Incoming...
523                         # [See comment above process_it() for explanation
524                         #  of why this is necessary...]
525                         if os.access(dsc_file, os.R_OK) != 0:
526                             files[dsc_file] = {};
527                             files[dsc_file]["size"] = os.stat(dsc_file)[stat.ST_SIZE];
528                             files[dsc_file]["md5sum"] = dsc_files[dsc_file]["md5sum"];
529                             files[dsc_file]["section"] = files[file]["section"];
530                             files[dsc_file]["priority"] = files[file]["priority"];
531                             files[dsc_file]["component"] = files[file]["component"];
532                             files[dsc_file]["type"] = "orig.tar.gz";
533                             reprocess = 1;
534                             return 1;
535                         else:
536                             reject_message = reject_message + "Rejected: %s refers to %s, but I can't find it in Incoming or in the pool.\n" % (file, dsc_file);
537                             continue;
538                 else:
539                     reject_message = reject_message + "Rejected: %s refers to %s, but I can't find it in Incoming.\n" % (file, dsc_file);
540                     continue;
541                 if actual_md5 != dsc_files[dsc_file]["md5sum"]:
542                     reject_message = reject_message + "Rejected: md5sum for %s doesn't match %s.\n" % (found, file);
543                 if actual_size != int(dsc_files[dsc_file]["size"]):
544                     reject_message = reject_message + "Rejected: size for %s doesn't match %s.\n" % (found, file);
545
546     if string.find(reject_message, "Rejected:") != -1:
547         return 0
548     else: 
549         return 1
550
551 ###############################################################################
552
553 # Some cunning stunt broke dpkg-source in dpkg 1.8{,.1}; detect the
554 # resulting bad source packages and reject them.
555
556 # Even more amusingly the fix in 1.8.1.1 didn't actually fix the
557 # problem just changed the symptoms.
558
559 def check_diff ():
560     global dsc, dsc_files, reject_message, reprocess;
561
562     for filename in files.keys():
563         if files[filename]["type"] == "diff.gz":
564             file = gzip.GzipFile(filename, 'r');
565             for line in file.readlines():
566                 if re_bad_diff.search(line):
567                     reject_message = reject_message + "Rejected: [dpkg-sucks] source package was produced by a broken version of dpkg-dev 1.8.x; please rebuild with >= 1.8.3 version installed.\n";
568                     break;
569
570     if string.find(reject_message, "Rejected:") != -1:
571         return 0
572     else: 
573         return 1
574
575 ###############################################################################
576
577 def check_md5sums ():
578     global reject_message;
579
580     for file in files.keys():
581         try:
582             file_handle = utils.open_file(file,"r");
583         except utils.cant_open_exc:
584             pass;
585         else:
586             if apt_pkg.md5sum(file_handle) != files[file]["md5sum"]:
587                 reject_message = reject_message + "Rejected: md5sum check failed for %s.\n" % (file);
588
589 def check_override ():
590     global Subst;
591     
592     # Only check section & priority on sourceful uploads
593     if not changes["architecture"].has_key("source"):
594         return;
595
596     summary = ""
597     for file in files.keys():
598         if not files[file].has_key("new") and files[file]["type"] == "deb":
599             section = files[file]["section"];
600             override_section = files[file]["override section"];
601             if section != override_section and section != "-":
602                 # Ignore this; it's a common mistake and not worth whining about
603                 if string.lower(section) == "non-us/main" and string.lower(override_section) == "non-us":
604                     continue;
605                 summary = summary + "%s: section is overridden from %s to %s.\n" % (file, section, override_section);
606             priority = files[file]["priority"];
607             override_priority = files[file]["override priority"];
608             if priority != override_priority and priority != "-":
609                 summary = summary + "%s: priority is overridden from %s to %s.\n" % (file, priority, override_priority);
610
611     if summary == "":
612         return;
613
614     Subst["__SUMMARY__"] = summary;
615     mail_message = utils.TemplateSubst(Subst,open(Cnf["Dir::TemplatesDir"]+"/katie.override-disparity","r").read());
616     utils.send_mail (mail_message, "")
617
618 #####################################################################################################################
619
620 # Set up the per-package template substitution mappings
621
622 def update_subst (changes_filename):
623     global Subst;
624
625     if changes.has_key("architecture"):
626         Subst["__ARCHITECTURE__"] = string.join(changes["architecture"].keys(), ' ' );
627     else:
628         Subst["__ARCHITECTURE__"] = "Unknown";
629     Subst["__CHANGES_FILENAME__"] = os.path.basename(changes_filename);
630     Subst["__FILE_CONTENTS__"] = changes.get("filecontents", "");
631
632     # For source uploads the Changed-By field wins; otherwise Maintainer wins.
633     if changes["architecture"].has_key("source") and changes["changedby822"] != "" and (changes["changedby822"] != changes["maintainer822"]):
634         Subst["__MAINTAINER_FROM__"] = changes["changedby822"];
635         Subst["__MAINTAINER_TO__"] = changes["changedby822"] + ", " + changes["maintainer822"];
636         Subst["__MAINTAINER__"] = changes["changedby"];
637     else:
638         Subst["__MAINTAINER_FROM__"] = changes["maintainer822"];
639         Subst["__MAINTAINER_TO__"] = changes["maintainer822"];
640         Subst["__MAINTAINER__"] = changes.get("maintainer", "Unknown");
641
642     Subst["__REJECT_MESSAGE__"] = reject_message;
643     Subst["__SOURCE__"] = changes.get("source", "Unknown");
644     Subst["__VERSION__"] = changes.get("version", "Unknown");
645
646 #####################################################################################################################
647
648 def action (changes_filename):
649     byhand = confirm = suites = summary = new = "";
650
651     # changes["distribution"] may not exist in corner cases
652     # (e.g. unreadable changes files)
653     if not changes.has_key("distribution"):
654         changes["distribution"] = {};
655     
656     for suite in changes["distribution"].keys():
657         if Cnf.has_key("Suite::%s::Confirm"):
658             confirm = confirm + suite + ", "
659         suites = suites + suite + ", "
660     confirm = confirm[:-2]
661     suites = suites[:-2]
662
663     for file in files.keys():
664         if files[file].has_key("byhand"):
665             byhand = 1
666             summary = summary + file + " byhand\n"
667         elif files[file].has_key("new"):
668             new = 1
669             summary = summary + "(new) %s %s %s\n" % (file, files[file]["priority"], files[file]["section"])
670             if files[file].has_key("othercomponents"):
671                 summary = summary + "WARNING: Already present in %s distribution.\n" % (files[file]["othercomponents"])
672             if files[file]["type"] == "deb":
673                 summary = summary + apt_pkg.ParseSection(apt_inst.debExtractControl(utils.open_file(file,"r")))["Description"] + '\n';
674         else:
675             files[file]["pool name"] = utils.poolify (changes["source"], files[file]["component"])
676             destination = Cnf["Dir::PoolRoot"] + files[file]["pool name"] + file
677             summary = summary + file + "\n  to " + destination + "\n"
678
679     short_summary = summary;
680
681     # This is for direport's benefit...
682     f = re_fdnic.sub("\n .\n", changes.get("changes",""));
683
684     if confirm or byhand or new:
685         summary = summary + "Changes: " + f;
686
687     summary = summary + announce (short_summary, 0)
688     
689     (prompt, answer) = ("", "XXX")
690     if Cnf["Dinstall::Options::No-Action"] or Cnf["Dinstall::Options::Automatic"]:
691         answer = 'S'
692
693     if string.find(reject_message, "Rejected") != -1:
694         try:
695             modified_time = time.time()-os.path.getmtime(changes_filename);
696         except: # i.e. ignore errors like 'file does not exist';
697             modified_time = 0;
698         if modified_time < 86400:
699             print "SKIP (too new)\n" + reject_message,;
700             prompt = "[S]kip, Manual reject, Quit ?";
701         else:
702             print "REJECT\n" + reject_message,;
703             prompt = "[R]eject, Manual reject, Skip, Quit ?";
704             if Cnf["Dinstall::Options::Automatic"]:
705                 answer = 'R';
706     elif new:
707         print "NEW to %s\n%s%s" % (suites, reject_message, summary),;
708         prompt = "[S]kip, New ack, Manual reject, Quit ?";
709         if Cnf["Dinstall::Options::Automatic"] and Cnf["Dinstall::Options::Ack-New"]:
710             answer = 'N';
711     elif byhand:
712         print "BYHAND\n" + reject_message + summary,;
713         prompt = "[I]nstall, Manual reject, Skip, Quit ?";
714     elif confirm:
715         print "CONFIRM to %s\n%s%s" % (confirm, reject_message, summary),
716         prompt = "[I]nstall, Manual reject, Skip, Quit ?";
717     else:
718         print "INSTALL\n" + reject_message + summary,;
719         prompt = "[I]nstall, Manual reject, Skip, Quit ?";
720         if Cnf["Dinstall::Options::Automatic"]:
721             answer = 'I';
722
723     while string.find(prompt, answer) == -1:
724         print prompt,;
725         answer = utils.our_raw_input()
726         m = re_default_answer.match(prompt)
727         if answer == "":
728             answer = m.group(1)
729         answer = string.upper(answer[:1])
730
731     if answer == 'R':
732         reject (changes_filename, "");
733     elif answer == 'M':
734         manual_reject (changes_filename);
735     elif answer == 'I':
736         install (changes_filename, summary, short_summary);
737     elif answer == 'N':
738         acknowledge_new (changes_filename, summary);
739     elif answer == 'Q':
740         sys.exit(0)
741
742 #####################################################################################################################
743
744 def install (changes_filename, summary, short_summary):
745     global install_count, install_bytes, Subst;
746
747     # Stable uploads are a special case
748     if changes.has_key("stable upload"):
749         stable_install (changes_filename, summary, short_summary);
750         return;
751     
752     print "Installing."
753
754     archive = utils.where_am_i();
755
756     # Begin a transaction; if we bomb out anywhere between here and the COMMIT WORK below, the DB will not be changed.
757     projectB.query("BEGIN WORK");
758
759     # Add the .dsc file to the DB
760     for file in files.keys():
761         if files[file]["type"] == "dsc":
762             package = dsc["source"]
763             version = dsc["version"]  # NB: not files[file]["version"], that has no epoch
764             maintainer = dsc["maintainer"]
765             maintainer = string.replace(maintainer, "'", "\\'")
766             maintainer_id = db_access.get_or_set_maintainer_id(maintainer);
767             filename = files[file]["pool name"] + file;
768             dsc_location_id = files[file]["location id"];
769             if not files[file]["files id"]:
770                 files[file]["files id"] = db_access.set_files_id (filename, files[file]["size"], files[file]["md5sum"], dsc_location_id)
771             projectB.query("INSERT INTO source (source, version, maintainer, file) VALUES ('%s', '%s', %d, %d)"
772                            % (package, version, maintainer_id, files[file]["files id"]))
773             
774             for suite in changes["distribution"].keys():
775                 suite_id = db_access.get_suite_id(suite);
776                 projectB.query("INSERT INTO src_associations (suite, source) VALUES (%d, currval('source_id_seq'))" % (suite_id))
777
778             # Add the source files to the DB (files and dsc_files)
779             projectB.query("INSERT INTO dsc_files (source, file) VALUES (currval('source_id_seq'), %d)" % (files[file]["files id"]));
780             for dsc_file in dsc_files.keys():
781                 filename = files[file]["pool name"] + dsc_file;
782                 # If the .orig.tar.gz is already in the pool, it's
783                 # files id is stored in dsc_files by check_dsc().
784                 files_id = dsc_files[dsc_file].get("files id", None);
785                 if files_id == None:
786                     files_id = db_access.get_files_id(filename, dsc_files[dsc_file]["size"], dsc_files[dsc_file]["md5sum"], dsc_location_id);
787                 # FIXME: needs to check for -1/-2 and or handle exception
788                 if files_id == None:
789                     files_id = db_access.set_files_id (filename, dsc_files[dsc_file]["size"], dsc_files[dsc_file]["md5sum"], dsc_location_id);
790                 projectB.query("INSERT INTO dsc_files (source, file) VALUES (currval('source_id_seq'), %d)" % (files_id));
791             
792     # Add the .deb files to the DB
793     for file in files.keys():
794         if files[file]["type"] == "deb":
795             package = files[file]["package"]
796             version = files[file]["version"]
797             maintainer = files[file]["maintainer"]
798             maintainer = string.replace(maintainer, "'", "\\'")
799             maintainer_id = db_access.get_or_set_maintainer_id(maintainer);
800             architecture = files[file]["architecture"]
801             architecture_id = db_access.get_architecture_id (architecture);
802             type = files[file]["dbtype"];
803             dsc_component = files[file]["component"]
804             source = files[file]["source"]
805             source_version = ""
806             if string.find(source, "(") != -1:
807                 m = utils.re_extract_src_version.match(source)
808                 source = m.group(1)
809                 source_version = m.group(2)
810             if not source_version:
811                 source_version = version
812             filename = files[file]["pool name"] + file;
813             if not files[file]["files id"]:
814                 files[file]["files id"] = db_access.set_files_id (filename, files[file]["size"], files[file]["md5sum"], files[file]["location id"])
815             source_id = db_access.get_source_id (source, source_version);
816             if source_id:
817                 projectB.query("INSERT INTO binaries (package, version, maintainer, source, architecture, file, type) VALUES ('%s', '%s', %d, %d, %d, %d, '%s')"
818                                % (package, version, maintainer_id, source_id, architecture_id, files[file]["files id"], type));
819             else:
820                 projectB.query("INSERT INTO binaries (package, version, maintainer, architecture, file, type) VALUES ('%s', '%s', %d, %d, %d, '%s')"
821                                % (package, version, maintainer_id, architecture_id, files[file]["files id"], type));
822             for suite in changes["distribution"].keys():
823                 suite_id = db_access.get_suite_id(suite);
824                 projectB.query("INSERT INTO bin_associations (suite, bin) VALUES (%d, currval('binaries_id_seq'))" % (suite_id));
825
826     # If the .orig.tar.gz is in a legacy directory we need to poolify
827     # it, so that apt-get source (and anything else that goes by the
828     # "Directory:" field in the Sources.gz file) works.
829     if orig_tar_id != None and orig_tar_location == "legacy":
830         q = projectB.query("SELECT DISTINCT ON (f.id) l.path, f.filename, f.id as files_id, df.source, df.id as dsc_files_id, f.size, f.md5sum FROM files f, dsc_files df, location l WHERE df.source IN (SELECT source FROM dsc_files WHERE file = %s) AND f.id = df.file AND l.id = f.location AND (l.type = 'legacy' OR l.type = 'legacy-mixed')" % (orig_tar_id));
831         qd = q.dictresult();
832         for qid in qd:
833             # Is this an old upload superseded by a newer -sa upload?  (See check_dsc() for details)
834             if legacy_source_untouchable.has_key(qid["files_id"]):
835                 continue;
836             # First move the files to the new location
837             legacy_filename = qid["path"]+qid["filename"];
838             pool_location = utils.poolify (changes["source"], files[file]["component"]);
839             pool_filename = pool_location + os.path.basename(qid["filename"]);
840             destination = Cnf["Dir::PoolDir"] + pool_location
841             utils.move(legacy_filename, destination);
842             # Then Update the DB's files table
843             q = projectB.query("UPDATE files SET filename = '%s', location = '%s' WHERE id = '%s'" % (pool_filename, dsc_location_id, qid["files_id"]));
844
845     # If this is a sourceful diff only upload that is moving non-legacy
846     # cross-component we need to copy the .orig.tar.gz into the new
847     # component too for the same reasons as above.
848     #
849     if changes["architecture"].has_key("source") and orig_tar_id != None and \
850        orig_tar_location != "legacy" and orig_tar_location != dsc_location_id:
851         q = projectB.query("SELECT l.path, f.filename, f.size, f.md5sum FROM files f, location l WHERE f.id = %s AND f.location = l.id" % (orig_tar_id));
852         ql = q.getresult()[0];
853         old_filename = ql[0] + ql[1];
854         file_size = ql[2];
855         file_md5sum = ql[3];
856         new_filename = utils.poolify (changes["source"], dsc_component) + os.path.basename(old_filename);
857         new_files_id = db_access.get_files_id(new_filename, file_size, file_md5sum, dsc_location_id);
858         if new_files_id == None:
859             utils.copy(old_filename, Cnf["Dir::PoolDir"] + new_filename);
860             new_files_id = db_access.set_files_id(new_filename, file_size, file_md5sum, dsc_location_id);
861             projectB.query("UPDATE dsc_files SET file = %s WHERE source = %s AND file = %s" % (new_files_id, source_id, orig_tar_id));
862
863     # Install the files into the pool
864     for file in files.keys():
865         if files[file].has_key("byhand"):
866             continue
867         destination = Cnf["Dir::PoolDir"] + files[file]["pool name"] + file
868         destdir = os.path.dirname(destination)
869         utils.move (file, destination)
870         install_bytes = install_bytes + float(files[file]["size"])
871
872     # Copy the .changes file across for suite which need it.
873     for suite in changes["distribution"].keys():
874         if Cnf.has_key("Suite::%s::CopyChanges" % (suite)):
875             utils.copy (changes_filename, Cnf["Dir::RootDir"] + Cnf["Suite::%s::CopyChanges" % (suite)]);
876
877     projectB.query("COMMIT WORK");
878
879     try:
880         utils.move (changes_filename, Cnf["Dir::IncomingDir"] + 'DONE/' + os.path.basename(changes_filename))
881     except:
882         sys.stderr.write("W: couldn't move changes file '%s' to DONE directory [Got %s].\n" % (os.path.basename(changes_filename), sys.exc_type));
883
884     install_count = install_count + 1;
885
886     if not Cnf["Dinstall::Options::No-Mail"]:
887         Subst["__SUITE__"] = "";
888         Subst["__SUMMARY__"] = summary;
889         mail_message = utils.TemplateSubst(Subst,open(Cnf["Dir::TemplatesDir"]+"/katie.installed","r").read());
890         utils.send_mail (mail_message, "")
891         announce (short_summary, 1)
892         check_override ();
893
894 #####################################################################################################################
895
896 def stable_install (changes_filename, summary, short_summary):
897     global install_count, install_bytes, Subst;
898     
899     print "Installing to stable."
900
901     archive = utils.where_am_i();
902
903     # Begin a transaction; if we bomb out anywhere between here and the COMMIT WORK below, the DB will not be changed.
904     projectB.query("BEGIN WORK");
905
906     # Add the .dsc file to the DB
907     for file in files.keys():
908         if files[file]["type"] == "dsc":
909             package = dsc["source"]
910             version = dsc["version"]  # NB: not files[file]["version"], that has no epoch
911             q = projectB.query("SELECT id FROM source WHERE source = '%s' AND version = '%s'" % (package, version))
912             ql = q.getresult()
913             if ql == []:
914                 sys.stderr.write("INTERNAL ERROR: couldn't find '%s' (%s) in source table.\n" % (package, version));
915                 sys.exit(1);
916             source_id = ql[0][0];
917             suite_id = db_access.get_suite_id('proposed-updates');
918             projectB.query("DELETE FROM src_associations WHERE suite = '%s' AND source = '%s'" % (suite_id, source_id));
919             suite_id = db_access.get_suite_id('stable');
920             projectB.query("INSERT INTO src_associations (suite, source) VALUES ('%s', '%s')" % (suite_id, source_id));
921                 
922     # Add the .deb files to the DB
923     for file in files.keys():
924         if files[file]["type"] == "deb":
925             package = files[file]["package"]
926             version = files[file]["version"]
927             architecture = files[file]["architecture"]
928             q = projectB.query("SELECT b.id FROM binaries b, architecture a WHERE b.package = '%s' AND b.version = '%s' AND (a.arch_string = '%s' OR a.arch_string = 'all') AND b.architecture = a.id" % (package, version, architecture))
929             ql = q.getresult()
930             if ql == []:
931                 sys.stderr.write("INTERNAL ERROR: couldn't find '%s' (%s for %s architecture) in binaries table.\n" % (package, version, architecture));
932                 sys.exit(1);
933             binary_id = ql[0][0];
934             suite_id = db_access.get_suite_id('proposed-updates');
935             projectB.query("DELETE FROM bin_associations WHERE suite = '%s' AND bin = '%s'" % (suite_id, binary_id));
936             suite_id = db_access.get_suite_id('stable');
937             projectB.query("INSERT INTO bin_associations (suite, bin) VALUES ('%s', '%s')" % (suite_id, binary_id));
938
939     projectB.query("COMMIT WORK");
940
941     utils.move (changes_filename, Cnf["Rhona::Morgue"] + os.path.basename(changes_filename));
942
943     # Update the Stable ChangeLog file
944
945     new_changelog_filename = Cnf["Dir::RootDir"] + Cnf["Suite::Stable::ChangeLogBase"] + ".ChangeLog";
946     changelog_filename = Cnf["Dir::RootDir"] + Cnf["Suite::Stable::ChangeLogBase"] + "ChangeLog";
947     if os.path.exists(new_changelog_filename):
948         os.unlink (new_changelog_filename);
949     
950     new_changelog = utils.open_file(new_changelog_filename, 'w');
951     for file in files.keys():
952         if files[file]["type"] == "deb":
953             new_changelog.write("stable/%s/binary-%s/%s\n" % (files[file]["component"], files[file]["architecture"], file));
954         elif utils.re_issource.match(file) != None:
955             new_changelog.write("stable/%s/source/%s\n" % (files[file]["component"], file));
956         else:
957             new_changelog.write("%s\n" % (file));
958     chop_changes = re_fdnic.sub("\n", changes["changes"]);
959     new_changelog.write(chop_changes + '\n\n');
960     if os.access(changelog_filename, os.R_OK) != 0:
961         changelog = utils.open_file(changelog_filename, 'r');
962         new_changelog.write(changelog.read());
963     new_changelog.close();
964     if os.access(changelog_filename, os.R_OK) != 0:
965         os.unlink(changelog_filename);
966     utils.move(new_changelog_filename, changelog_filename);
967
968     install_count = install_count + 1;
969
970     if not Cnf["Dinstall::Options::No-Mail"]:
971         Subst["__SUITE__"] = " into stable";
972         Subst["__SUMMARY__"] = summary;
973         utils.send_mail (mail_message, "")
974         announce (short_summary, 1)
975
976 #####################################################################################################################
977
978 def reject (changes_filename, manual_reject_mail_filename):
979     global Subst;
980     
981     print "Rejecting.\n"
982
983     base_changes_filename = os.path.basename(changes_filename);
984     reason_filename = re_changes.sub("reason", base_changes_filename);
985     reject_filename = "%s/REJECT/%s" % (Cnf["Dir::IncomingDir"], reason_filename);
986
987     # Move the .changes files and it's contents into REJECT/ (if we can; errors are ignored)
988     try:
989         utils.move (changes_filename, "%s/REJECT/%s" % (Cnf["Dir::IncomingDir"], base_changes_filename));
990     except:
991         sys.stderr.write("W: couldn't reject changes file '%s' [Got %s].\n" % (base_changes_filename, sys.exc_type));
992         pass;
993     for file in files.keys():
994         if os.path.exists(file):
995             try:
996                 utils.move (file, "%s/REJECT/%s" % (Cnf["Dir::IncomingDir"], file));
997             except:
998                 sys.stderr.write("W: couldn't reject file '%s' [Got %s].\n" % (file, sys.exc_type));
999                 pass;
1000
1001     # If this is not a manual rejection generate the .reason file and rejection mail message
1002     if manual_reject_mail_filename == "":
1003         if os.path.exists(reject_filename):
1004             os.unlink(reject_filename);
1005         fd = os.open(reject_filename, os.O_RDWR|os.O_CREAT|os.O_EXCL, 0644);
1006         os.write(fd, reject_message);
1007         os.close(fd);
1008         Subst["__MANUAL_REJECT_MESSAGE__"] = "";
1009         Subst["__CC__"] = "X-Katie-Rejection: automatic (moo)";
1010         reject_mail_message = utils.TemplateSubst(Subst,open(Cnf["Dir::TemplatesDir"]+"/katie.rejected","r").read());
1011     else: # Have a manual rejection file to use
1012         reject_mail_message = ""; # avoid <undef>'s
1013         
1014     # Send the rejection mail if appropriate
1015     if not Cnf["Dinstall::Options::No-Mail"]:
1016         utils.send_mail (reject_mail_message, manual_reject_mail_filename);
1017
1018 ##################################################################
1019
1020 def manual_reject (changes_filename):
1021     global Subst;
1022     
1023     # Build up the rejection email 
1024     user_email_address = utils.whoami() + " <%s@%s>" % (pwd.getpwuid(os.getuid())[0], Cnf["Dinstall::MyHost"])
1025     manual_reject_message = Cnf.get("Dinstall::Options::Manual-Reject", "")
1026
1027     Subst["__MANUAL_REJECT_MESSAGE__"] = manual_reject_message;
1028     Subst["__CC__"] = "Cc: " + Cnf["Dinstall::MyEmailAddress"];
1029     reject_mail_message = utils.TemplateSubst(Subst,open(Cnf["Dir::TemplatesDir"]+"/katie.rejected","r").read());
1030     
1031     # Write the rejection email out as the <foo>.reason file
1032     reason_filename = re_changes.sub("reason", os.path.basename(changes_filename));
1033     reject_filename = "%s/REJECT/%s" % (Cnf["Dir::IncomingDir"], reason_filename)
1034     if os.path.exists(reject_filename):
1035         os.unlink(reject_filename);
1036     fd = os.open(reject_filename, os.O_RDWR|os.O_CREAT|os.O_EXCL, 0644);
1037     os.write(fd, reject_mail_message);
1038     os.close(fd);
1039     
1040     # If we weren't given one, spawn an editor so the user can add one in
1041     if manual_reject_message == "":
1042         result = os.system("vi +6 %s" % (reject_file))
1043         if result != 0:
1044             sys.stderr.write ("vi invocation failed for `%s'!\n" % (reject_file))
1045             sys.exit(result)
1046
1047     # Then process it as if it were an automatic rejection
1048     reject (changes_filename, reject_filename)
1049
1050 #####################################################################################################################
1051  
1052 def acknowledge_new (changes_filename, summary):
1053     global new_ack_new, Subst;
1054
1055     changes_filename = os.path.basename(changes_filename);
1056
1057     new_ack_new[changes_filename] = 1;
1058
1059     if new_ack_old.has_key(changes_filename):
1060         print "Ack already sent.";
1061         return;
1062
1063     print "Sending new ack.";
1064     if not Cnf["Dinstall::Options::No-Mail"]:
1065         Subst["__SUMMARY__"] = summary;
1066         new_ack_message = utils.TemplateSubst(Subst,open(Cnf["Dir::TemplatesDir"]+"/katie.new","r").read());
1067         utils.send_mail(new_ack_message,"");
1068
1069 #####################################################################################################################
1070
1071 def announce (short_summary, action):
1072     global Subst;
1073     
1074     # Only do announcements for source uploads with a recent dpkg-dev installed
1075     if float(changes.get("format", 0)) < 1.6 or not changes["architecture"].has_key("source"):
1076         return ""
1077
1078     lists_done = {}
1079     summary = ""
1080     Subst["__SHORT_SUMMARY__"] = short_summary;
1081
1082     for dist in changes["distribution"].keys():
1083         list = Cnf.Find("Suite::%s::Announce" % (dist))
1084         if list == "" or lists_done.has_key(list):
1085             continue
1086         lists_done[list] = 1
1087         summary = summary + "Announcing to %s\n" % (list)
1088
1089         if action:
1090             Subst["__ANNOUNCE_LIST_ADDRESS__"] = list;
1091             mail_message = utils.TemplateSubst(Subst,open(Cnf["Dir::TemplatesDir"]+"/katie.announce","r").read());
1092             utils.send_mail (mail_message, "")
1093
1094     (dsc_rfc822, dsc_name, dsc_email) = utils.fix_maintainer (dsc.get("maintainer",Cnf["Dinstall::MyEmailAddress"]));
1095     bugs = changes["closes"].keys()
1096     bugs.sort()
1097     # changes["changedbyname"] == dsc_name is probably never true, but better
1098     # safe than sorry
1099     if dsc_name == changes["maintainername"] and (changes["changedbyname"] == "" or changes["changedbyname"] == dsc_name):
1100         summary = summary + "Closing bugs: "
1101         for bug in bugs:
1102             summary = summary + "%s " % (bug)
1103             if action:
1104                 Subst["__BUG_NUMBER__"] = bug;
1105                 if changes["distribution"].has_key("stable"):
1106                     Subst["__STABLE_WARNING__"] = """
1107 Note that this package is not part of the released stable Debian
1108 distribution.  It may have dependencies on other unreleased software,
1109 or other instabilities.  Please take care if you wish to install it.
1110 The update will eventually make its way into the next released Debian
1111 distribution."""
1112                 else:
1113                     Subst["__STABLE_WARNING__"] = "";
1114                 mail_message = utils.TemplateSubst(Subst,open(Cnf["Dir::TemplatesDir"]+"/katie.bug-close","r").read());
1115                 utils.send_mail (mail_message, "")
1116     else:                     # NMU
1117         summary = summary + "Setting bugs to severity fixed: "
1118         control_message = ""
1119         for bug in bugs:
1120             summary = summary + "%s " % (bug)
1121             control_message = control_message + "tag %s + fixed\n" % (bug)
1122         if action and control_message != "":
1123             Subst["__CONTROL_MESSAGE__"] = control_message;
1124             mail_message = utils.TemplateSubst(Subst,open(Cnf["Dir::TemplatesDir"]+"/katie.bug-nmu-fixed","r").read());
1125             utils.send_mail (mail_message, "")
1126     summary = summary + "\n"
1127
1128     return summary
1129
1130 ###############################################################################
1131
1132 # reprocess is necessary for the case of foo_1.2-1 and foo_1.2-2 in
1133 # Incoming. -1 will reference the .orig.tar.gz, but -2 will not.
1134 # dsccheckdistrib() can find the .orig.tar.gz but it will not have
1135 # processed it during it's checks of -2.  If -1 has been deleted or
1136 # otherwise not checked by da-install, the .orig.tar.gz will not have
1137 # been checked at all.  To get round this, we force the .orig.tar.gz
1138 # into the .changes structure and reprocess the .changes file.
1139
1140 def process_it (changes_file):
1141     global reprocess, orig_tar_id, orig_tar_location, changes, dsc, dsc_files, files, reject_message;
1142
1143     # Reset some globals
1144     reprocess = 1;
1145     changes = {};
1146     dsc = {};
1147     dsc_files = {};
1148     files = {};
1149     orig_tar_id = None;
1150     orig_tar_location = "";
1151     legacy_source_untouchable = {};
1152     reject_message = "";
1153
1154     # Absolutize the filename to avoid the requirement of being in the
1155     # same directory as the .changes file.
1156     changes_file = os.path.abspath(changes_file);
1157
1158     # And since handling of installs to stable munges with the CWD;
1159     # save and restore it.
1160     cwd = os.getcwd();
1161     
1162     check_signature (changes_file);
1163     check_changes (changes_file);
1164     while reprocess:
1165         reprocess = 0;
1166         check_files ();
1167         check_md5sums ();
1168         check_dsc ();
1169         check_diff ();
1170         
1171     update_subst(changes_file);
1172     action(changes_file);
1173
1174     # Restore CWD
1175     os.chdir(cwd);
1176
1177 ###############################################################################
1178
1179 def main():
1180     global Cnf, projectB, install_bytes, new_ack_old, Subst
1181
1182     apt_pkg.init();
1183     
1184     Cnf = apt_pkg.newConfiguration();
1185     apt_pkg.ReadConfigFileISC(Cnf,utils.which_conf_file());
1186
1187     Arguments = [('a',"automatic","Dinstall::Options::Automatic"),
1188                  ('d',"debug","Dinstall::Options::Debug", "IntVal"),
1189                  ('h',"help","Dinstall::Options::Help"),
1190                  ('k',"ack-new","Dinstall::Options::Ack-New"),
1191                  ('m',"manual-reject","Dinstall::Options::Manual-Reject", "HasArg"),
1192                  ('n',"no-action","Dinstall::Options::No-Action"),
1193                  ('p',"no-lock", "Dinstall::Options::No-Lock"),
1194                  ('s',"no-mail", "Dinstall::Options::No-Mail"),
1195                  ('u',"override-distribution", "Dinstall::Options::Override-Distribution", "HasArg"),
1196                  ('v',"version","Dinstall::Options::Version")];
1197     
1198     changes_files = apt_pkg.ParseCommandLine(Cnf,Arguments,sys.argv);
1199
1200     if Cnf["Dinstall::Options::Help"]:
1201         usage(0);
1202         
1203     if Cnf["Dinstall::Options::Version"]:
1204         print "katie version 0.0000000000";
1205         usage(0);
1206
1207     postgresql_user = None; # Default == Connect as user running program.
1208
1209     # -n/--dry-run invalidates some other options which would involve things happening
1210     if Cnf["Dinstall::Options::No-Action"]:
1211         Cnf["Dinstall::Options::Automatic"] = ""
1212         Cnf["Dinstall::Options::Ack-New"] = ""
1213         postgresql_user = Cnf["DB::ROUser"];
1214
1215     projectB = pg.connect(Cnf["DB::Name"], Cnf["DB::Host"], int(Cnf["DB::Port"]), None, None, postgresql_user);
1216
1217     db_access.init(Cnf, projectB);
1218
1219     # Check that we aren't going to clash with the daily cron job
1220
1221     if os.path.exists("%s/Archive_Maintenance_In_Progress" % (Cnf["Dir::RootDir"])) and not Cnf["Dinstall::Options::No-Lock"]:
1222         sys.stderr.write("Archive maintenance in progress.  Try again later.\n");
1223         sys.exit(2);
1224     
1225     # Obtain lock if not in no-action mode
1226
1227     if not Cnf["Dinstall::Options::No-Action"]:
1228         lock_fd = os.open(Cnf["Dinstall::LockFile"], os.O_RDWR);
1229         fcntl.lockf(lock_fd, FCNTL.F_TLOCK);
1230
1231     # Read in the list of already-acknowledged NEW packages
1232     new_ack_list = utils.open_file(Cnf["Dinstall::NewAckList"],'r');
1233     new_ack_old = {};
1234     for line in new_ack_list.readlines():
1235         new_ack_old[line[:-1]] = 1;
1236     new_ack_list.close();
1237
1238     # Initialize the substitution template mapping global
1239     Subst = {}
1240     Subst["__ADMIN_ADDRESS__"] = Cnf["Dinstall::MyAdminAddress"];
1241     Subst["__BUG_SERVER__"] = Cnf["Dinstall::BugServer"];
1242     bcc = "X-Katie: $Revision: 1.38 $"
1243     if Cnf.has_key("Dinstall::Bcc"):
1244         Subst["__BCC__"] = bcc + "\nBcc: %s" % (Cnf["Dinstall::Bcc"]);
1245     else:
1246         Subst["__BCC__"] = bcc;
1247     Subst["__DISTRO__"] = Cnf["Dinstall::MyDistribution"];
1248     Subst["__KATIE_ADDRESS__"] = Cnf["Dinstall::MyEmailAddress"];
1249
1250     # Process the changes files
1251     for changes_file in changes_files:
1252         print "\n" + changes_file;
1253         process_it (changes_file);
1254
1255     if install_count:
1256         sets = "set"
1257         if install_count > 1:
1258             sets = "sets"
1259         sys.stderr.write("Installed %d package %s, %s.\n" % (install_count, sets, utils.size_type(int(install_bytes))));
1260
1261     # Write out the list of already-acknowledged NEW packages
1262     if Cnf["Dinstall::Options::Ack-New"]:
1263         new_ack_list = utils.open_file(Cnf["Dinstall::NewAckList"],'w')
1264         for i in new_ack_new.keys():
1265             new_ack_list.write(i+'\n')
1266         new_ack_list.close()
1267     
1268             
1269 if __name__ == '__main__':
1270     main()
1271