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