]> git.decadent.org.uk Git - dak.git/blob - katie.py
996ae0283fef263e5bacc98c27031e7032adc7a0
[dak.git] / katie.py
1 #!/usr/bin/env python
2
3 # Utility functions for katie
4 # Copyright (C) 2001, 2002  James Troup <james@nocrew.org>
5 # $Id: katie.py,v 1.27 2002-10-16 02:47:32 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 ###############################################################################
22
23 import cPickle, errno, os, pg, re, stat, string, sys, tempfile, time;
24 import utils, db_access;
25 import apt_inst, apt_pkg;
26
27 from types import *;
28
29 ###############################################################################
30
31 re_isanum = re.compile (r"^\d+$");
32 re_default_answer = re.compile(r"\[(.*)\]");
33 re_fdnic = re.compile("\n\n");
34 re_bin_only_nmu_of_mu = re.compile("\.\d+\.\d+$");
35 re_bin_only_nmu_of_nmu = re.compile("\.\d+$");
36
37 ###############################################################################
38
39 # Convenience wrapper to carry around all the package information in
40
41 class Pkg:
42     def __init__(self, **kwds):
43         self.__dict__.update(kwds);
44
45     def update(self, **kwds):
46         self.__dict__.update(kwds);
47
48 ###############################################################################
49
50 class nmu_p:
51     # Read in the group maintainer override file
52     def __init__ (self, Cnf):
53         self.group_maint = {};
54         self.Cnf = Cnf;
55         if Cnf.get("Dinstall::GroupOverrideFilename"):
56             filename = Cnf["Dir::Override"] + Cnf["Dinstall::GroupOverrideFilename"];
57             file = utils.open_file(filename);
58             for line in file.readlines():
59                 line = utils.re_comments.sub('', line).lower().strip();
60                 if line != "":
61                     self.group_maint[line] = 1;
62             file.close();
63
64     def is_an_nmu (self, pkg):
65         Cnf = self.Cnf;
66         changes = pkg.changes;
67         dsc = pkg.dsc;
68
69         (dsc_rfc822, dsc_name, dsc_email) = utils.fix_maintainer (dsc.get("maintainer",Cnf["Dinstall::MyEmailAddress"]).lower());
70         # changes["changedbyname"] == dsc_name is probably never true, but better safe than sorry
71         if dsc_name == changes["maintainername"].lower() and \
72            (changes["changedby822"] == "" or changes["changedbyname"].lower() == dsc_name):
73             return 0;
74
75         if dsc.has_key("uploaders"):
76             uploaders = dsc["uploaders"].lower().split(",");
77             uploadernames = {};
78             for i in uploaders:
79                 (rfc822, name, email) = utils.fix_maintainer (i.strip());
80                 uploadernames[name] = "";
81             if uploadernames.has_key(changes["changedbyname"].lower()):
82                 return 0;
83
84         # Some group maintained packages (e.g. Debian QA) are never NMU's
85         if self.group_maint.has_key(changes["maintaineremail"].lower()):
86             return 0;
87
88         return 1;
89
90 ###############################################################################
91
92 class Katie:
93
94     def __init__(self, Cnf):
95         self.Cnf = Cnf;
96         self.values = {};
97         # Read in the group-maint override file
98         self.nmu = nmu_p(Cnf);
99         self.accept_count = 0;
100         self.accept_bytes = 0L;
101         self.pkg = Pkg(changes = {}, dsc = {}, dsc_files = {}, files = {},
102                        legacy_source_untouchable = {});
103
104         # Initialize the substitution template mapping global
105         Subst = self.Subst = {};
106         Subst["__ADMIN_ADDRESS__"] = Cnf["Dinstall::MyAdminAddress"];
107         Subst["__BUG_SERVER__"] = Cnf["Dinstall::BugServer"];
108         Subst["__DISTRO__"] = Cnf["Dinstall::MyDistribution"];
109         Subst["__KATIE_ADDRESS__"] = Cnf["Dinstall::MyEmailAddress"];
110
111         self.projectB = pg.connect(Cnf["DB::Name"], Cnf["DB::Host"], int(Cnf["DB::Port"]));
112         db_access.init(Cnf, self.projectB);
113
114     ###########################################################################
115
116     def init_vars (self):
117         for i in [ "changes", "dsc", "files", "dsc_files", "legacy_source_untouchable" ]:
118             exec "self.pkg.%s.clear();" % (i);
119         self.pkg.orig_tar_id = None;
120         self.pkg.orig_tar_location = "";
121
122     ###########################################################################
123
124     def update_vars (self):
125         dump_filename = self.pkg.changes_file[:-8]+".katie";
126         dump_file = utils.open_file(dump_filename);
127         p = cPickle.Unpickler(dump_file);
128         for i in [ "changes", "dsc", "files", "dsc_files", "legacy_source_untouchable" ]:
129             exec "self.pkg.%s.update(p.load());" % (i);
130         for i in [ "orig_tar_id", "orig_tar_location" ]:
131             exec "self.pkg.%s = p.load();" % (i);
132         dump_file.close();
133
134     ###########################################################################
135
136     # This could just dump the dictionaries as is, but I'd like to avoid
137     # this so there's some idea of what katie & lisa use from jennifer
138
139     def dump_vars(self, dest_dir):
140         for i in [ "changes", "dsc", "files", "dsc_files",
141                    "legacy_source_untouchable", "orig_tar_id", "orig_tar_location" ]:
142             exec "%s = self.pkg.%s;" % (i,i);
143         dump_filename = os.path.join(dest_dir,self.pkg.changes_file[:-8] + ".katie");
144         dump_file = utils.open_file(dump_filename, 'w');
145         try:
146             os.chmod(dump_filename, 0660);
147         except OSError, e:
148             if errno.errorcode[e.errno] == 'EPERM':
149                 perms = stat.S_IMODE(os.stat(dump_filename)[stat.ST_MODE]);
150                 if perms & stat.S_IROTH:
151                     utils.fubar("%s is world readable and chmod failed." % (dump_filename));
152             else:
153                 raise;
154
155         p = cPickle.Pickler(dump_file, 1);
156         for i in [ "d_changes", "d_dsc", "d_files", "d_dsc_files" ]:
157             exec "%s = {}" % i;
158         ## files
159         for file in files.keys():
160             d_files[file] = {};
161             for i in [ "package", "version", "architecture", "type", "size",
162                        "md5sum", "component", "location id", "source package",
163                        "source version", "maintainer", "dbtype", "files id",
164                        "new", "section", "priority", "othercomponents",
165                        "pool name", "original component" ]:
166                 if files[file].has_key(i):
167                     d_files[file][i] = files[file][i];
168         ## changes
169         # Mandatory changes fields
170         for i in [ "distribution", "source", "architecture", "version", "maintainer",
171                    "urgency", "fingerprint", "changedby822", "changedbyname",
172                    "maintainername", "maintaineremail", "closes" ]:
173             d_changes[i] = changes[i];
174         # Optional changes fields
175         # FIXME: changes should be mandatory
176         for i in [ "changed-by", "maintainer822", "filecontents", "format",
177                    "changes", "lisa note" ]:
178             if changes.has_key(i):
179                 d_changes[i] = changes[i];
180         ## dsc
181         for i in [ "source", "version", "maintainer", "fingerprint", "uploaders" ]:
182             if dsc.has_key(i):
183                 d_dsc[i] = dsc[i];
184         ## dsc_files
185         for file in dsc_files.keys():
186             d_dsc_files[file] = {};
187             # Mandatory dsc_files fields
188             for i in [ "size", "md5sum" ]:
189                 d_dsc_files[file][i] = dsc_files[file][i];
190             # Optional dsc_files fields
191             for i in [ "files id" ]:
192                 if dsc_files[file].has_key(i):
193                     d_dsc_files[file][i] = dsc_files[file][i];
194
195         for i in [ d_changes, d_dsc, d_files, d_dsc_files,
196                    legacy_source_untouchable, orig_tar_id, orig_tar_location ]:
197             p.dump(i);
198         dump_file.close();
199
200     ###########################################################################
201
202     # Set up the per-package template substitution mappings
203
204     def update_subst (self, reject_message = ""):
205         Subst = self.Subst;
206         changes = self.pkg.changes;
207         # If jennifer crashed out in the right place, architecture may still be a string.
208         if not changes.has_key("architecture") or not isinstance(changes["architecture"], DictType):
209             changes["architecture"] = { "Unknown" : "" };
210         # and maintainer822 may not exist.
211         if not changes.has_key("maintainer822"):
212             changes["maintainer822"] = self.Cnf["Dinstall::MyEmailAddress"];
213
214         Subst["__ARCHITECTURE__"] = " ".join(changes["architecture"].keys());
215         Subst["__CHANGES_FILENAME__"] = os.path.basename(self.pkg.changes_file);
216         Subst["__FILE_CONTENTS__"] = changes.get("filecontents", "");
217
218         # For source uploads the Changed-By field wins; otherwise Maintainer wins.
219         if changes["architecture"].has_key("source") and changes["changedby822"] != "" and (changes["changedby822"] != changes["maintainer822"]):
220             Subst["__MAINTAINER_FROM__"] = changes["changedby822"];
221             Subst["__MAINTAINER_TO__"] = changes["changedby822"] + ", " + changes["maintainer822"];
222             Subst["__MAINTAINER__"] = changes.get("changed-by", "Unknown");
223         else:
224             Subst["__MAINTAINER_FROM__"] = changes["maintainer822"];
225             Subst["__MAINTAINER_TO__"] = changes["maintainer822"];
226             Subst["__MAINTAINER__"] = changes.get("maintainer", "Unknown");
227         if self.Cnf.has_key("Dinstall::TrackingServer") and changes.has_key("source"):
228             Subst["__MAINTAINER_TO__"] += "\nBcc: %s@%s" % (changes["source"], self.Cnf["Dinstall::TrackingServer"])
229
230         # Apply any global override of the Maintainer field
231         if self.Cnf.get("Dinstall::OverrideMaintainer"):
232             Subst["__MAINTAINER_TO__"] = self.Cnf["Dinstall::OverrideMaintainer"];
233             Subst["__MAINTAINER_FROM__"] = self.Cnf["Dinstall::OverrideMaintainer"];
234
235         Subst["__REJECT_MESSAGE__"] = reject_message;
236         Subst["__SOURCE__"] = changes.get("source", "Unknown");
237         Subst["__VERSION__"] = changes.get("version", "Unknown");
238
239     ###########################################################################
240
241     def build_summaries(self):
242         changes = self.pkg.changes;
243         files = self.pkg.files;
244
245         byhand = summary = new = "";
246
247         # changes["distribution"] may not exist in corner cases
248         # (e.g. unreadable changes files)
249         if not changes.has_key("distribution") or not isinstance(changes["distribution"], DictType):
250             changes["distribution"] = {};
251
252         file_keys = files.keys();
253         file_keys.sort();
254         for file in file_keys:
255             if files[file].has_key("byhand"):
256                 byhand = 1
257                 summary += file + " byhand\n"
258             elif files[file].has_key("new"):
259                 new = 1
260                 summary += "(new) %s %s %s\n" % (file, files[file]["priority"], files[file]["section"])
261                 if files[file].has_key("othercomponents"):
262                     summary += "WARNING: Already present in %s distribution.\n" % (files[file]["othercomponents"])
263                 if files[file]["type"] == "deb":
264                     summary += apt_pkg.ParseSection(apt_inst.debExtractControl(utils.open_file(file)))["Description"] + '\n';
265             else:
266                 files[file]["pool name"] = utils.poolify (changes["source"], files[file]["component"])
267                 destination = self.Cnf["Dir::PoolRoot"] + files[file]["pool name"] + file
268                 summary += file + "\n  to " + destination + "\n"
269
270         short_summary = summary;
271
272         # This is for direport's benefit...
273         f = re_fdnic.sub("\n .\n", changes.get("changes",""));
274
275         if byhand or new:
276             summary += "Changes: " + f;
277
278         summary += self.announce(short_summary, 0)
279
280         return (summary, short_summary);
281
282     ###########################################################################
283
284     def close_bugs (self, summary, action):
285         changes = self.pkg.changes;
286         Subst = self.Subst;
287         Cnf = self.Cnf;
288
289         bugs = changes["closes"].keys();
290
291         if not bugs:
292             return summary;
293
294         bugs.sort();
295         if not self.nmu.is_an_nmu(self.pkg):
296             summary += "Closing bugs: ";
297             for bug in bugs:
298                 summary += "%s " % (bug);
299                 if action:
300                     Subst["__BUG_NUMBER__"] = bug;
301                     if changes["distribution"].has_key("stable"):
302                         Subst["__STABLE_WARNING__"] = """
303 Note that this package is not part of the released stable Debian
304 distribution.  It may have dependencies on other unreleased software,
305 or other instabilities.  Please take care if you wish to install it.
306 The update will eventually make its way into the next released Debian
307 distribution.""";
308                     else:
309                         Subst["__STABLE_WARNING__"] = "";
310                     mail_message = utils.TemplateSubst(Subst,Cnf["Dir::Templates"]+"/jennifer.bug-close");
311                     utils.send_mail (mail_message, "");
312             if action:
313                 self.Logger.log(["closing bugs"]+bugs);
314         else:                     # NMU
315             summary += "Setting bugs to severity fixed: ";
316             control_message = "";
317             for bug in bugs:
318                 summary += "%s " % (bug);
319                 control_message += "tag %s + fixed\n" % (bug);
320             if action and control_message != "":
321                 Subst["__CONTROL_MESSAGE__"] = control_message;
322                 mail_message = utils.TemplateSubst(Subst,Cnf["Dir::Templates"]+"/jennifer.bug-nmu-fixed");
323                 utils.send_mail (mail_message, "");
324             if action:
325                 self.Logger.log(["setting bugs to fixed"]+bugs);
326         summary += "\n";
327         return summary;
328
329     ###########################################################################
330
331     def announce (self, short_summary, action):
332         Subst = self.Subst;
333         Cnf = self.Cnf;
334         changes = self.pkg.changes;
335
336         # Only do announcements for source uploads with a recent dpkg-dev installed
337         if float(changes.get("format", 0)) < 1.6 or not changes["architecture"].has_key("source"):
338             return "";
339
340         lists_done = {};
341         summary = "";
342         Subst["__SHORT_SUMMARY__"] = short_summary;
343
344         for dist in changes["distribution"].keys():
345             list = Cnf.Find("Suite::%s::Announce" % (dist));
346             if list == "" or lists_done.has_key(list):
347                 continue;
348             lists_done[list] = 1;
349             summary += "Announcing to %s\n" % (list);
350
351             if action:
352                 Subst["__ANNOUNCE_LIST_ADDRESS__"] = list;
353                 if Cnf.get("Dinstall::TrackingServer") and changes["architecture"].has_key("source"):
354                     Subst["__ANNOUNCE_LIST_ADDRESS__"] = Subst["__ANNOUNCE_LIST_ADDRESS__"] + "\nBcc: %s@%s" % (changes["source"], Cnf["Dinstall::TrackingServer"]);
355                 mail_message = utils.TemplateSubst(Subst,Cnf["Dir::Templates"]+"/jennifer.announce");
356                 utils.send_mail (mail_message, "");
357
358         if Cnf.FindB("Dinstall::CloseBugs"):
359             summary = self.close_bugs(summary, action);
360
361         return summary;
362
363     ###########################################################################
364
365     def accept (self, summary, short_summary):
366         Cnf = self.Cnf;
367         Subst = self.Subst;
368         files = self.pkg.files;
369
370         print "Accepting."
371         self.Logger.log(["Accepting changes",self.pkg.changes_file]);
372
373         self.dump_vars(Cnf["Dir::Queue::Accepted"]);
374
375         # Move all the files into the accepted directory
376         utils.move(self.pkg.changes_file, Cnf["Dir::Queue::Accepted"]);
377         file_keys = files.keys();
378         for file in file_keys:
379             utils.move(file, Cnf["Dir::Queue::Accepted"]);
380             self.accept_bytes += float(files[file]["size"])
381         self.accept_count += 1;
382
383         # Send accept mail, announce to lists, close bugs and check for
384         # override disparities
385         if not Cnf["Dinstall::Options::No-Mail"]:
386             Subst["__SUITE__"] = "";
387             Subst["__SUMMARY__"] = summary;
388             mail_message = utils.TemplateSubst(Subst,Cnf["Dir::Templates"]+"/jennifer.accepted");
389             utils.send_mail(mail_message, "")
390             self.announce(short_summary, 1)
391
392         # Special support to enable clean auto-building of accepted packages
393         self.projectB.query("BEGIN WORK");
394         for suite in self.pkg.changes["distribution"].keys():
395             if suite not in Cnf.ValueList("Dinstall::AcceptedAutoBuildSuites"):
396                 continue;
397             suite_id = db_access.get_suite_id(suite);
398             dest_dir = Cnf["Dir::AcceptedAutoBuild"];
399             if Cnf.FindB("Dinstall::SecurityAcceptedAutoBuild"):
400                 dest_dir = os.path.join(dest_dir, suite);
401             for file in file_keys:
402                 src = os.path.join(Cnf["Dir::Queue::Accepted"], file);
403                 dest = os.path.join(dest_dir, file);
404                 if Cnf.FindB("Dinstall::SecurityAcceptedAutoBuild"):
405                     # Copy it since the original won't be readable by www-data
406                     utils.copy(src, dest);
407                 else:
408                     # Create a symlink to it
409                     os.symlink(src, dest);
410                 # Add it to the list of packages for later processing by apt-ftparchive
411                 self.projectB.query("INSERT INTO accepted_autobuild (suite, filename, in_accepted) VALUES (%s, '%s', 't')" % (suite_id, dest));
412             # If the .orig.tar.gz is in the pool, create a symlink to
413             # it (if one doesn't already exist)
414             if self.pkg.orig_tar_id:
415                 # Determine the .orig.tar.gz file name
416                 for dsc_file in self.pkg.dsc_files.keys():
417                     if dsc_file.endswith(".orig.tar.gz"):
418                         filename = dsc_file;
419                 dest = os.path.join(dest_dir, filename);
420                 # If it doesn't exist, create a symlink
421                 if not os.path.exists(dest):
422                     # Find the .orig.tar.gz in the pool
423                     q = self.projectB.query("SELECT l.path, f.filename from location l, files f WHERE f.id = %s and f.location = l.id" % (self.pkg.orig_tar_id));
424                     ql = q.getresult();
425                     if not ql:
426                         utils.fubar("[INTERNAL ERROR] Couldn't find id %s in files table." % (self.pkg.orig_tar_id));
427                     src = os.path.join(ql[0][0], ql[0][1]);
428                     os.symlink(src, dest);
429                     # Add it to the list of packages for later processing by apt-ftparchive
430                     self.projectB.query("INSERT INTO accepted_autobuild (suite, filename, in_accepted) VALUES (%s, '%s', 't')" % (suite_id, dest));
431                 # if it does, update things to ensure it's not removed prematurely
432                 else:
433                     self.projectB.query("UPDATE accepted_autobuild SET in_accepted = 't', last_used = NULL WHERE filename = '%s' AND suite = %s" % (dest, suite_id));
434
435         self.projectB.query("COMMIT WORK");
436
437     ###########################################################################
438
439     def check_override (self):
440         Subst = self.Subst;
441         changes = self.pkg.changes;
442         files = self.pkg.files;
443         Cnf = self.Cnf;
444
445         # Abandon the check if:
446         #  a) it's a non-sourceful upload
447         #  b) override disparity checks have been disabled
448         #  c) we're not sending mail
449         if not changes["architecture"].has_key("source") or \
450            not Cnf.FindB("Dinstall::OverrideDisparityCheck") or \
451            Cnf["Dinstall::Options::No-Mail"]:
452             return;
453
454         summary = "";
455         for file in files.keys():
456             if not files[file].has_key("new") and files[file]["type"] == "deb":
457                 section = files[file]["section"];
458                 override_section = files[file]["override section"];
459                 if section.lower() != override_section.lower() and section != "-":
460                     # Ignore this; it's a common mistake and not worth whining about
461                     if section.lower() == "non-us/main" and override_section.lower() == "non-us":
462                         continue;
463                     summary += "%s: section is overridden from %s to %s.\n" % (file, section, override_section);
464                 priority = files[file]["priority"];
465                 override_priority = files[file]["override priority"];
466                 if priority != override_priority and priority != "-":
467                     summary += "%s: priority is overridden from %s to %s.\n" % (file, priority, override_priority);
468
469         if summary == "":
470             return;
471
472         Subst["__SUMMARY__"] = summary;
473         mail_message = utils.TemplateSubst(Subst,self.Cnf["Dir::Templates"]+"/jennifer.override-disparity");
474         utils.send_mail (mail_message, "");
475
476     ###########################################################################
477
478     def force_move (self, files):
479         """Forcefully move files from the current directory to the reject
480            directory.  If any file already exists it will be moved to the
481            morgue to make way for the new file."""
482
483         Cnf = self.Cnf
484
485         for file in files:
486             # Skip any files which don't exist or which we don't have permission to copy.
487             if os.access(file,os.R_OK) == 0:
488                 continue;
489             dest_file = os.path.join(Cnf["Dir::Queue::Reject"], file);
490             try:
491                 os.open(dest_file, os.O_RDWR|os.O_CREAT|os.O_EXCL, 0644);
492             except OSError, e:
493                 # File exists?  Let's try and move it to the morgue
494                 if errno.errorcode[e.errno] == 'EEXIST':
495                     morgue_file = os.path.join(Cnf["Dir::Morgue"],Cnf["Dir::MorgueReject"],file);
496                     try:
497                         morgue_file = utils.find_next_free(morgue_file);
498                     except utils.tried_too_hard_exc:
499                         # Something's either gone badly Pete Tong, or
500                         # someone is trying to exploit us.
501                         utils.warn("**WARNING** failed to move %s from the reject directory to the morgue." % (file));
502                         return;
503                     utils.move(dest_file, morgue_file, perms=0660);
504                     try:
505                         os.open(dest_file, os.O_RDWR|os.O_CREAT|os.O_EXCL, 0644);
506                     except OSError, e:
507                         # Likewise
508                         utils.warn("**WARNING** failed to claim %s in the reject directory." % (file));
509                         return;
510                 else:
511                     raise;
512             # If we got here, we own the destination file, so we can
513             # safely overwrite it.
514             utils.move(file, dest_file, 1, perms=0660);
515
516     ###########################################################################
517
518     def do_reject (self, manual = 0, reject_message = ""):
519         # If we weren't given a manual rejection message, spawn an
520         # editor so the user can add one in...
521         if manual and not reject_message:
522             temp_filename = tempfile.mktemp();
523             fd = os.open(temp_filename, os.O_RDWR|os.O_CREAT|os.O_EXCL, 0700);
524             os.close(fd);
525             editor = os.environ.get("EDITOR","vi")
526             answer = 'E';
527             while answer == 'E':
528                 os.system("%s %s" % (editor, temp_filename))
529                 file = utils.open_file(temp_filename);
530                 reject_message = " ".join(file.readlines());
531                 file.close();
532                 print "Reject message:";
533                 print utils.prefix_multi_line_string(reject_message,"  ");
534                 prompt = "[R]eject, Edit, Abandon, Quit ?"
535                 answer = "XXX";
536                 while prompt.find(answer) == -1:
537                     answer = utils.our_raw_input(prompt);
538                     m = re_default_answer.search(prompt);
539                     if answer == "":
540                         answer = m.group(1);
541                     answer = answer[:1].upper();
542             os.unlink(temp_filename);
543             if answer == 'A':
544                 return 1;
545             elif answer == 'Q':
546                 sys.exit(0);
547
548         print "Rejecting.\n"
549
550         Cnf = self.Cnf;
551         Subst = self.Subst;
552         pkg = self.pkg;
553
554         reason_filename = pkg.changes_file[:-8] + ".reason";
555         reject_filename = Cnf["Dir::Queue::Reject"] + '/' + reason_filename;
556
557         # Move all the files into the reject directory
558         reject_files = pkg.files.keys() + [pkg.changes_file];
559         self.force_move(reject_files);
560
561         # If we fail here someone is probably trying to exploit the race
562         # so let's just raise an exception ...
563         if os.path.exists(reject_filename):
564             os.unlink(reject_filename);
565         fd = os.open(reject_filename, os.O_RDWR|os.O_CREAT|os.O_EXCL, 0644);
566
567         if not manual:
568             Subst["__REJECTOR_ADDRESS__"] = Cnf["Dinstall::MyEmailAddress"];
569             Subst["__MANUAL_REJECT_MESSAGE__"] = "";
570             Subst["__CC__"] = "X-Katie-Rejection: automatic (moo)";
571             os.write(fd, reject_message);
572             os.close(fd);
573             reject_mail_message = utils.TemplateSubst(Subst,Cnf["Dir::Templates"]+"/katie.rejected");
574         else:
575             # Build up the rejection email
576             user_email_address = utils.whoami() + " <%s>" % (Cnf["Dinstall::MyAdminAddress"]);
577
578             Subst["__REJECTOR_ADDRESS__"] = user_email_address;
579             Subst["__MANUAL_REJECT_MESSAGE__"] = reject_message;
580             Subst["__CC__"] = "Cc: " + Cnf["Dinstall::MyEmailAddress"];
581             reject_mail_message = utils.TemplateSubst(Subst,Cnf["Dir::Templates"]+"/katie.rejected");
582
583             # Write the rejection email out as the <foo>.reason file
584             os.write(fd, reject_mail_message);
585             os.close(fd);
586
587         # Send the rejection mail if appropriate
588         if not Cnf["Dinstall::Options::No-Mail"]:
589             utils.send_mail (reject_mail_message, "");
590
591         self.Logger.log(["rejected", pkg.changes_file]);
592         return 0;
593
594     ################################################################################
595
596     # Ensure that source exists somewhere in the archive for the binary
597     # upload being processed.
598     #
599     # (1) exact match                      => 1.0-3
600     # (2) Bin-only NMU of an MU            => 1.0-3.0.1
601     # (3) Bin-only NMU of a sourceful-NMU  => 1.0-3.1.1
602
603     def source_exists (self, package, source_version):
604         q = self.projectB.query("SELECT s.version FROM source s WHERE s.source = '%s'" % (package));
605
606         # Reduce the query results to a list of version numbers
607         ql = map(lambda x: x[0], q.getresult());
608
609         # Try (1)
610         if ql.count(source_version):
611             return 1;
612
613         # Try (2)
614         orig_source_version = re_bin_only_nmu_of_mu.sub('', source_version);
615         if ql.count(orig_source_version):
616             return 1;
617
618         # Try (3)
619         orig_source_version = re_bin_only_nmu_of_nmu.sub('', source_version);
620         if ql.count(orig_source_version):
621             return 1;
622
623         # No source found...
624         return 0;
625
626     ################################################################################
627
628     def in_override_p (self, package, component, suite, binary_type, file):
629         files = self.pkg.files;
630
631         if binary_type == "": # must be source
632             type = "dsc";
633         else:
634             type = binary_type;
635
636         # Override suite name; used for example with proposed-updates
637         if self.Cnf.Find("Suite::%s::OverrideSuite" % (suite)) != "":
638             suite = self.Cnf["Suite::%s::OverrideSuite" % (suite)];
639
640         # Avoid <undef> on unknown distributions
641         suite_id = db_access.get_suite_id(suite);
642         if suite_id == -1:
643             return None;
644         component_id = db_access.get_component_id(component);
645         type_id = db_access.get_override_type_id(type);
646
647         # FIXME: nasty non-US speficic hack
648         if component[:7].lower() == "non-us/":
649             component = component[7:];
650
651         q = self.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"
652                            % (package, suite_id, component_id, type_id));
653         result = q.getresult();
654         # If checking for a source package fall back on the binary override type
655         if type == "dsc" and not result:
656             deb_type_id = db_access.get_override_type_id("deb");
657             udeb_type_id = db_access.get_override_type_id("udeb");
658             q = self.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 OR type = %s) AND o.section = s.id AND o.priority = p.id"
659                                % (package, suite_id, component_id, deb_type_id, udeb_type_id));
660             result = q.getresult();
661
662         # Remember the section and priority so we can check them later if appropriate
663         if result:
664             files[file]["override section"] = result[0][0];
665             files[file]["override priority"] = result[0][1];
666
667         return result;
668
669     ################################################################################
670
671     def reject (self, str, prefix="Rejected: "):
672         if str:
673             # Unlike other rejects we add new lines first to avoid trailing
674             # new lines when this message is passed back up to a caller.
675             if self.reject_message:
676                 self.reject_message += "\n";
677             self.reject_message += prefix + str;
678
679     ################################################################################
680
681     def cross_suite_version_check(self, query_result, file, new_version):
682         """Ensure versions are newer than existing packages in target
683         suites and that cross-suite version checking rules as
684         set out in the conf file are satisfied."""
685
686         # Check versions for each target suite
687         for target_suite in self.pkg.changes["distribution"].keys():
688             must_be_newer_than = map(string.lower, self.Cnf.ValueList("Suite::%s::VersionChecks::MustBeNewerThan" % (target_suite)));
689             must_be_older_than = map(string.lower, self.Cnf.ValueList("Suite::%s::VersionChecks::MustBeOlderThan" % (target_suite)));
690             # Enforce "must be newer than target suite" even if conffile omits it
691             if target_suite not in must_be_newer_than:
692                 must_be_newer_than.append(target_suite);
693             for entry in query_result:
694                 existent_version = entry[0];
695                 suite = entry[1];
696                 if suite in must_be_newer_than and \
697                    apt_pkg.VersionCompare(new_version, existent_version) != 1:
698                     self.reject("%s: old version (%s) in %s >= new version (%s) targeted at %s." % (file, existent_version, suite, new_version, target_suite));
699                 if suite in must_be_older_than and \
700                    apt_pkg.VersionCompare(new_version, existent_version) != -1:
701                     self.reject("%s: old version (%s) in %s <= new version (%s) targeted at %s." % (file, existent_version, suite, new_version, target_suite));
702
703     ################################################################################
704
705     def check_binary_against_db(self, file):
706         self.reject_message = "";
707         files = self.pkg.files;
708
709         # Ensure version is sane
710         q = self.projectB.query("""
711 SELECT b.version, su.suite_name FROM binaries b, bin_associations ba, suite su,
712                                      architecture a
713  WHERE b.package = '%s' AND (a.arch_string = '%s' OR a.arch_string = 'all')
714    AND ba.bin = b.id AND ba.suite = su.id AND b.architecture = a.id"""
715                                 % (files[file]["package"],
716                                    files[file]["architecture"]));
717         self.cross_suite_version_check(q.getresult(), file, files[file]["version"]);
718
719         # Check for any existing copies of the file
720         q = self.projectB.query("""
721 SELECT b.id FROM binaries b, architecture a
722  WHERE b.package = '%s' AND b.version = '%s' AND a.arch_string = '%s'
723    AND a.id = b.architecture"""
724                                 % (files[file]["package"],
725                                    files[file]["version"],
726                                    files[file]["architecture"]))
727         if q.getresult():
728             self.reject("can not overwrite existing copy of '%s' already in the archive." % (file));
729
730         return self.reject_message;
731
732     ################################################################################
733
734     def check_source_against_db(self, file):
735         self.reject_message = "";
736         dsc = self.pkg.dsc;
737
738         # Ensure version is sane
739         q = self.projectB.query("""
740 SELECT s.version, su.suite_name FROM source s, src_associations sa, suite su
741  WHERE s.source = '%s' AND sa.source = s.id AND sa.suite = su.id""" % (dsc.get("source")));
742         self.cross_suite_version_check(q.getresult(), file, dsc.get("version"));
743
744         return self.reject_message;
745
746     ################################################################################
747
748     def check_dsc_against_db(self, file):
749         self.reject_message = "";
750         files = self.pkg.files;
751         dsc_files = self.pkg.dsc_files;
752         legacy_source_untouchable = self.pkg.legacy_source_untouchable;
753         orig_tar_gz = None;
754
755         # Try and find all files mentioned in the .dsc.  This has
756         # to work harder to cope with the multiple possible
757         # locations of an .orig.tar.gz.
758         for dsc_file in dsc_files.keys():
759             found = None;
760             if files.has_key(dsc_file):
761                 actual_md5 = files[dsc_file]["md5sum"];
762                 actual_size = int(files[dsc_file]["size"]);
763                 found = "%s in incoming" % (dsc_file)
764                 # Check the file does not already exist in the archive
765                 q = self.projectB.query("SELECT f.size, f.md5sum 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));
766
767                 # "It has not broken them.  It has fixed a
768                 # brokenness.  Your crappy hack exploited a bug in
769                 # the old dinstall.
770                 #
771                 # "(Come on!  I thought it was always obvious that
772                 # one just doesn't release different files with
773                 # the same name and version.)"
774                 #                        -- ajk@ on d-devel@l.d.o
775
776                 ql = q.getresult();
777                 if ql:
778                     # Ignore exact matches for .orig.tar.gz
779                     match = 0;
780                     if dsc_file.endswith(".orig.tar.gz"):
781                         for i in ql:
782                             if files.has_key(dsc_file) and \
783                                int(files[dsc_file]["size"]) == int(i[0]) and \
784                                files[dsc_file]["md5sum"] == i[1]:
785                                 self.reject("ignoring %s, since it's already in the archive." % (dsc_file), "Warning: ");
786                                 del files[dsc_file];
787                                 match = 1;
788
789                     if not match:
790                         self.reject("can not overwrite existing copy of '%s' already in the archive." % (dsc_file));
791             elif dsc_file.endswith(".orig.tar.gz"):
792                 # Check in the pool
793                 q = self.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));
794                 ql = q.getresult();
795
796                 if ql:
797                     # Unfortunately, we make get more than one
798                     # match here if, for example, the package was
799                     # in potato but had a -sa upload in woody.  So
800                     # we need to choose the right one.
801
802                     x = ql[0]; # default to something sane in case we don't match any or have only one
803
804                     if len(ql) > 1:
805                         for i in ql:
806                             old_file = i[0] + i[1];
807                             actual_md5 = apt_pkg.md5sum(utils.open_file(old_file));
808                             actual_size = os.stat(old_file)[stat.ST_SIZE];
809                             if actual_md5 == dsc_files[dsc_file]["md5sum"] and actual_size == int(dsc_files[dsc_file]["size"]):
810                                 x = i;
811                             else:
812                                 legacy_source_untouchable[i[3]] = "";
813
814                     old_file = x[0] + x[1];
815                     actual_md5 = apt_pkg.md5sum(utils.open_file(old_file));
816                     actual_size = os.stat(old_file)[stat.ST_SIZE];
817                     found = old_file;
818                     suite_type = x[2];
819                     dsc_files[dsc_file]["files id"] = x[3]; # need this for updating dsc_files in install()
820                     # See install() in katie...
821                     self.pkg.orig_tar_id = x[3];
822                     if suite_type == "legacy" or suite_type == "legacy-mixed":
823                         self.pkg.orig_tar_location = "legacy";
824                     else:
825                         self.pkg.orig_tar_location = x[4];
826                 else:
827                     # Not there? Check the queue directories...
828
829                     in_unchecked = os.path.join(self.Cnf["Dir::Queue::Unchecked"],dsc_file);
830                     # See process_it() in jennifer for explanation of this
831                     if os.path.exists(in_unchecked):
832                         return (self.reject_message, in_unchecked);
833                     else:
834                         for dir in [ "Accepted", "New", "Byhand" ]:
835                             in_otherdir = os.path.join(self.Cnf["Dir::Queue::%s" % (dir)],dsc_file);
836                             if os.path.exists(in_otherdir):
837                                 actual_md5 = apt_pkg.md5sum(utils.open_file(in_otherdir));
838                                 actual_size = os.stat(in_otherdir)[stat.ST_SIZE];
839                                 found = in_otherdir;
840
841                     if not found:
842                         self.reject("%s refers to %s, but I can't find it in the queue or in the pool." % (file, dsc_file));
843                         continue;
844             else:
845                 self.reject("%s refers to %s, but I can't find it in the queue." % (file, dsc_file));
846                 continue;
847             if actual_md5 != dsc_files[dsc_file]["md5sum"]:
848                 self.reject("md5sum for %s doesn't match %s." % (found, file));
849             if actual_size != int(dsc_files[dsc_file]["size"]):
850                 self.reject("size for %s doesn't match %s." % (found, file));
851
852         return (self.reject_message, orig_tar_gz);
853
854     def do_query(self, q):
855         sys.stderr.write("query: \"%s\" ... " % (q));
856         before = time.time();
857         r = self.projectB.query(q);
858         time_diff = time.time()-before;
859         sys.stderr.write("took %.3f seconds.\n" % (time_diff));
860         return r;