]> git.decadent.org.uk Git - dak.git/blob - katie.py
* crypto-in-main changes.* utils.py (move, copy): add an optional perms= parameter...
[dak.git] / katie.py
1 #!/usr/bin/env python
2
3 # Utility functions for katie
4 # Copyright (C) 2001  James Troup <james@nocrew.org>
5 # $Id: katie.py,v 1.11 2002-03-14 14:12:04 ajt 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, time;
24 import utils, db_access;
25 import apt_inst, apt_pkg;
26
27 from types import *;
28 from string import lower;
29
30 ###############################################################################
31
32 re_isanum = re.compile (r"^\d+$");
33 re_default_answer = re.compile(r"\[(.*)\]");
34 re_fdnic = re.compile("\n\n");
35 re_bin_only_nmu_of_mu = re.compile("\.\d+\.\d+$");
36 re_bin_only_nmu_of_nmu = re.compile("\.\d+$");
37
38 ###############################################################################
39
40 # Convenience wrapper to carry around all the package information in
41
42 class Pkg:
43     def __init__(self, **kwds):
44         self.__dict__.update(kwds);
45
46     def update(self, **kwds):
47         self.__dict__.update(kwds);
48
49 ###############################################################################
50
51 class nmu_p:
52     # Read in the group maintainer override file
53     def __init__ (self, Cnf):
54         self.group_maint = {};
55         self.Cnf = Cnf;
56         if Cnf.get("Dinstall::GroupOverrideFilename"):
57             filename = Cnf["Dir::OverrideDir"] + Cnf["Dinstall::GroupOverrideFilename"];
58             file = utils.open_file(filename);
59             for line in file.readlines():
60                 line = lower(string.strip(utils.re_comments.sub('', line)));
61                 if line != "":
62                     self.group_maint[line] = 1;
63             file.close();
64
65     def is_an_nmu (self, pkg):
66         Cnf = self.Cnf;
67         changes = pkg.changes;
68         dsc = pkg.dsc;
69
70         (dsc_rfc822, dsc_name, dsc_email) = utils.fix_maintainer (lower(dsc.get("maintainer",Cnf["Dinstall::MyEmailAddress"])));
71         # changes["changedbyname"] == dsc_name is probably never true, but better safe than sorry
72         if dsc_name == lower(changes["maintainername"]) and \
73            (changes["changedby822"] == "" or lower(changes["changedbyname"]) == dsc_name):
74             return 0;
75
76         if dsc.has_key("uploaders"):
77             uploaders = string.split(lower(dsc["uploaders"]), ",");
78             uploadernames = {};
79             for i in uploaders:
80                 (rfc822, name, email) = utils.fix_maintainer (string.strip(i));
81                 uploadernames[name] = "";
82             if uploadernames.has_key(lower(changes["changedbyname"])):
83                 return 0;
84
85         # Some group maintained packages (e.g. Debian QA) are never NMU's
86         if self.group_maint.has_key(lower(changes["maintaineremail"])):
87             return 0;
88
89         return 1;
90
91 ###############################################################################
92
93 class Katie:
94
95     def __init__(self, Cnf):
96         self.Cnf = Cnf;
97         self.values = {};
98         # Read in the group-maint override file
99         self.nmu = nmu_p(Cnf);
100         self.accept_count = 0;
101         self.accept_bytes = 0L;
102         self.pkg = Pkg(changes = {}, dsc = {}, dsc_files = {}, files = {},
103                        legacy_source_untouchable = {});
104
105         # Initialize the substitution template mapping global
106         Subst = self.Subst = {};
107         Subst["__ADMIN_ADDRESS__"] = Cnf["Dinstall::MyAdminAddress"];
108         Subst["__BUG_SERVER__"] = Cnf["Dinstall::BugServer"];
109         Subst["__DISTRO__"] = Cnf["Dinstall::MyDistribution"];
110         Subst["__KATIE_ADDRESS__"] = Cnf["Dinstall::MyEmailAddress"];
111
112         self.projectB = pg.connect(Cnf["DB::Name"], Cnf["DB::Host"], int(Cnf["DB::Port"]));
113         db_access.init(Cnf, self.projectB);
114
115     ###########################################################################
116
117     def init_vars (self):
118         for i in [ "changes", "dsc", "files", "dsc_files", "legacy_source_untouchable" ]:
119             exec "self.pkg.%s.clear();" % (i);
120         self.pkg.orig_tar_id = None;
121         self.pkg.orig_tar_location = "";
122
123     ###########################################################################
124
125     def update_vars (self):
126         dump_filename = self.pkg.changes_file[:-8]+".katie";
127         dump_file = utils.open_file(dump_filename);
128         p = cPickle.Unpickler(dump_file);
129         for i in [ "changes", "dsc", "files", "dsc_files", "legacy_source_untouchable" ]:
130             exec "self.pkg.%s.update(p.load());" % (i);
131         for i in [ "orig_tar_id", "orig_tar_location" ]:
132             exec "self.pkg.%s = p.load();" % (i);
133         dump_file.close();
134
135     ###########################################################################
136
137     # This could just dump the dictionaries as is, but I'd like to avoid
138     # this so there's some idea of what katie & lisa use from jennifer
139
140     def dump_vars(self, dest_dir):
141         for i in [ "changes", "dsc", "files", "dsc_files",
142                    "legacy_source_untouchable", "orig_tar_id", "orig_tar_location" ]:
143             exec "%s = self.pkg.%s;" % (i,i);
144         dump_filename = os.path.join(dest_dir,self.pkg.changes_file[:-8] + ".katie");
145         dump_file = utils.open_file(dump_filename, 'w');
146         p = cPickle.Pickler(dump_file, 1);
147         for i in [ "d_changes", "d_dsc", "d_files", "d_dsc_files" ]:
148             exec "%s = {}" % i;
149         ## files
150         for file in files.keys():
151             d_files[file] = {};
152             for i in [ "package", "version", "architecture", "type", "size",
153                        "md5sum", "component", "location id", "source package",
154                        "source version", "maintainer", "dbtype", "files id",
155                        "new", "section", "priority", "oldfiles", "othercomponents" ]:
156                 if files[file].has_key(i):
157                     d_files[file][i] = files[file][i];
158         ## changes
159         # Mandatory changes fields
160         for i in [ "distribution", "source", "architecture", "version", "maintainer",
161                    "urgency", "fingerprint", "changedby822", "changedbyname",
162                    "maintainername", "maintaineremail", "closes" ]:
163             d_changes[i] = changes[i];
164         # Optional changes fields
165         for i in [ "changed-by", "maintainer822", "filecontents", "format" ]:
166             if changes.has_key(i):
167                 d_changes[i] = changes[i];
168         ## dsc
169         for i in [ "source", "version", "maintainer", "fingerprint", "uploaders" ]:
170             if dsc.has_key(i):
171                 d_dsc[i] = dsc[i];
172         ## dsc_files
173         for file in dsc_files.keys():
174             d_dsc_files[file] = {};
175             # Mandatory dsc_files fields
176             for i in [ "size", "md5sum" ]:
177                 d_dsc_files[file][i] = dsc_files[file][i];
178             # Optional dsc_files fields
179             for i in [ "files id" ]:
180                 if dsc_files[file].has_key(i):
181                     d_dsc_files[file][i] = dsc_files[file][i];
182
183         for i in [ d_changes, d_dsc, d_files, d_dsc_files,
184                    legacy_source_untouchable, orig_tar_id, orig_tar_location ]:
185             p.dump(i);
186         dump_file.close();
187
188     ###########################################################################
189
190     # Set up the per-package template substitution mappings
191
192     def update_subst (self, reject_message = ""):
193         Subst = self.Subst;
194         changes = self.pkg.changes;
195         # If jennifer crashed out in the right place, architecture may still be a string.
196         if not changes.has_key("architecture") or not isinstance(changes["architecture"], DictType):
197             changes["architecture"] = { "Unknown" : "" };
198         # and maintainer822 may not exist.
199         if not changes.has_key("maintainer822"):
200             changes["maintainer822"] = self.Cnf["Dinstall::MyEmailAddress"];
201
202         Subst["__ARCHITECTURE__"] = string.join(changes["architecture"].keys(), ' ' );
203         Subst["__CHANGES_FILENAME__"] = os.path.basename(self.pkg.changes_file);
204         Subst["__FILE_CONTENTS__"] = changes.get("filecontents", "");
205
206         # For source uploads the Changed-By field wins; otherwise Maintainer wins.
207         if changes["architecture"].has_key("source") and changes["changedby822"] != "" and (changes["changedby822"] != changes["maintainer822"]):
208             Subst["__MAINTAINER_FROM__"] = changes["changedby822"];
209             Subst["__MAINTAINER_TO__"] = changes["changedby822"] + ", " + changes["maintainer822"];
210             Subst["__MAINTAINER__"] = changes.get("changed-by", "Unknown");
211         else:
212             Subst["__MAINTAINER_FROM__"] = changes["maintainer822"];
213             Subst["__MAINTAINER_TO__"] = changes["maintainer822"];
214             Subst["__MAINTAINER__"] = changes.get("maintainer", "Unknown");
215         if self.Cnf.has_key("Dinstall::TrackingServer") and changes.has_key("source"):
216             Subst["__MAINTAINER_TO__"] = Subst["__MAINTAINER_TO__"] + "\nBcc: %s@%s" % (changes["source"], self.Cnf["Dinstall::TrackingServer"])
217
218         Subst["__REJECT_MESSAGE__"] = reject_message;
219         Subst["__SOURCE__"] = changes.get("source", "Unknown");
220         Subst["__VERSION__"] = changes.get("version", "Unknown");
221
222     ###########################################################################
223
224     def build_summaries(self):
225         changes = self.pkg.changes;
226         files = self.pkg.files;
227
228         byhand = summary = new = "";
229
230         # changes["distribution"] may not exist in corner cases
231         # (e.g. unreadable changes files)
232         if not changes.has_key("distribution") or not isinstance(changes["distribution"], DictType):
233             changes["distribution"] = {};
234
235         file_keys = files.keys();
236         file_keys.sort();
237         for file in file_keys:
238             if files[file].has_key("byhand"):
239                 byhand = 1
240                 summary = summary + file + " byhand\n"
241             elif files[file].has_key("new"):
242                 new = 1
243                 summary = summary + "(new) %s %s %s\n" % (file, files[file]["priority"], files[file]["section"])
244                 if files[file].has_key("othercomponents"):
245                     summary = summary + "WARNING: Already present in %s distribution.\n" % (files[file]["othercomponents"])
246                 if files[file]["type"] == "deb":
247                     summary = summary + apt_pkg.ParseSection(apt_inst.debExtractControl(utils.open_file(file)))["Description"] + '\n';
248             else:
249                 files[file]["pool name"] = utils.poolify (changes["source"], files[file]["component"])
250                 destination = self.Cnf["Dir::PoolRoot"] + files[file]["pool name"] + file
251                 summary = summary + file + "\n  to " + destination + "\n"
252
253         short_summary = summary;
254
255         # This is for direport's benefit...
256         f = re_fdnic.sub("\n .\n", changes.get("changes",""));
257
258         if byhand or new:
259             summary = summary + "Changes: " + f;
260
261         summary = summary + self.announce(short_summary, 0)
262
263         return (summary, short_summary);
264
265     ###########################################################################
266
267     def announce (self, short_summary, action):
268         Subst = self.Subst;
269         Cnf = self.Cnf;
270         changes = self.pkg.changes;
271         dsc = self.pkg.dsc;
272
273         # Only do announcements for source uploads with a recent dpkg-dev installed
274         if float(changes.get("format", 0)) < 1.6 or not changes["architecture"].has_key("source"):
275             return ""
276
277         lists_done = {}
278         summary = ""
279         Subst["__SHORT_SUMMARY__"] = short_summary;
280
281         for dist in changes["distribution"].keys():
282             list = Cnf.Find("Suite::%s::Announce" % (dist))
283             if list == "" or lists_done.has_key(list):
284                 continue
285             lists_done[list] = 1
286             summary = summary + "Announcing to %s\n" % (list)
287
288             if action:
289                 Subst["__ANNOUNCE_LIST_ADDRESS__"] = list;
290                 mail_message = utils.TemplateSubst(Subst,open(Cnf["Dir::TemplatesDir"]+"/jennifer.announce","r").read());
291                 utils.send_mail (mail_message, "")
292
293         bugs = changes["closes"].keys()
294         bugs.sort()
295         if not self.nmu.is_an_nmu(self.pkg):
296             summary = summary + "Closing bugs: "
297             for bug in bugs:
298                 summary = 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,open(Cnf["Dir::TemplatesDir"]+"/jennifer.bug-close","r").read());
311                     utils.send_mail (mail_message, "")
312             if action:
313                 self.Logger.log(["closing bugs"]+bugs);
314         else:                     # NMU
315             summary = summary + "Setting bugs to severity fixed: "
316             control_message = ""
317             for bug in bugs:
318                 summary = summary + "%s " % (bug)
319                 control_message = 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,open(Cnf["Dir::TemplatesDir"]+"/jennifer.bug-nmu-fixed","r").read());
323                 utils.send_mail (mail_message, "")
324             if action:
325                 self.Logger.log(["setting bugs to fixed"]+bugs);
326         summary = summary + "\n"
327
328         return summary
329
330     ###########################################################################
331
332     def accept (self, summary, short_summary):
333         Cnf = self.Cnf;
334         Subst = self.Subst;
335         files = self.pkg.files;
336
337         print "Accepting."
338         self.Logger.log(["Accepting changes",self.pkg.changes_file]);
339
340         self.dump_vars(Cnf["Dir::QueueAcceptedDir"]);
341
342         # Move all the files into the accepted directory
343         utils.move(self.pkg.changes_file, Cnf["Dir::QueueAcceptedDir"]);
344         file_keys = files.keys();
345         for file in file_keys:
346             utils.move(file, Cnf["Dir::QueueAcceptedDir"]);
347             self.accept_bytes = self.accept_bytes + float(files[file]["size"])
348         self.accept_count = self.accept_count + 1;
349
350         # Send accept mail, announce to lists, close bugs and check for
351         # override disparities
352         if not Cnf["Dinstall::Options::No-Mail"]:
353             Subst["__SUITE__"] = "";
354             Subst["__SUMMARY__"] = summary;
355             mail_message = utils.TemplateSubst(Subst,open(Cnf["Dir::TemplatesDir"]+"/jennifer.accepted","r").read());
356             utils.send_mail(mail_message, "")
357             self.announce(short_summary, 1)
358
359     ###########################################################################
360
361     def check_override (self):
362         Subst = self.Subst;
363         changes = self.pkg.changes;
364         files = self.pkg.files;
365
366         # Only check section & priority on sourceful uploads
367         if not changes["architecture"].has_key("source"):
368             return;
369
370         summary = "";
371         for file in files.keys():
372             if not files[file].has_key("new") and files[file]["type"] == "deb":
373                 section = files[file]["section"];
374                 override_section = files[file]["override section"];
375                 if lower(section) != lower(override_section) and section != "-":
376                     # Ignore this; it's a common mistake and not worth whining about
377                     if lower(section) == "non-us/main" and lower(override_section) == "non-us":
378                         continue;
379                     summary = summary + "%s: section is overridden from %s to %s.\n" % (file, section, override_section);
380                 priority = files[file]["priority"];
381                 override_priority = files[file]["override priority"];
382                 if priority != override_priority and priority != "-":
383                     summary = summary + "%s: priority is overridden from %s to %s.\n" % (file, priority, override_priority);
384
385         if summary == "":
386             return;
387
388         Subst["__SUMMARY__"] = summary;
389         mail_message = utils.TemplateSubst(Subst,utils.open_file(self.Cnf["Dir::TemplatesDir"]+"/jennifer.override-disparity").read());
390         utils.send_mail (mail_message, "");
391
392     ###########################################################################
393
394     def force_move (self, files):
395         """Forcefully move files from the current directory to the reject
396            directory.  If any file already exists it will be moved to the
397            morgue to make way for the new file."""
398
399         Cnf = self.Cnf
400
401         for file in files:
402             # Skip any files which don't exist or which we don't have permission to copy.
403             if os.access(file,os.R_OK) == 0:
404                 continue;
405             dest_file = os.path.join(Cnf["Dir::QueueRejectDir"], file);
406             try:
407                 os.open(dest_file, os.O_RDWR|os.O_CREAT|os.O_EXCL, 0644);
408             except OSError, e:
409                 # File exists?  Let's try and move it to the morgue
410                 if errno.errorcode[e.errno] == 'EEXIST':
411                     morgue_file = os.path.join(Cnf["Dir::Morgue"],Cnf["Dir::MorgueRejectDir"],file);
412                     try:
413                         morgue_file = utils.find_next_free(morgue_file);
414                     except utils.tried_too_hard_exc:
415                         # Something's either gone badly Pete Tong, or
416                         # someone is trying to exploit us.
417                         utils.warn("**WARNING** failed to move %s from the reject directory to the morgue." % (file));
418                         return;
419                     utils.move(dest_file, morgue_file, perms=0660);
420                     try:
421                         os.open(dest_file, os.O_RDWR|os.O_CREAT|os.O_EXCL, 0644);
422                     except OSError, e:
423                         # Likewise
424                         utils.warn("**WARNING** failed to claim %s in the reject directory." % (file));
425                         return;
426                 else:
427                     raise;
428             # If we got here, we own the destination file, so we can
429             # safely overwrite it.
430             utils.move(file, dest_file, 1, perms=0660);
431
432
433     ###########################################################################
434
435     def do_reject (self, manual = 0, reject_message = ""):
436         print "Rejecting.\n"
437
438         Cnf = self.Cnf;
439         Subst = self.Subst;
440         pkg = self.pkg;
441
442         reason_filename = pkg.changes_file[:-8] + ".reason";
443         reject_filename = Cnf["Dir::QueueRejectDir"] + '/' + reason_filename;
444
445         # Move all the files into the reject directory
446         reject_files = pkg.files.keys() + [pkg.changes_file];
447         self.force_move(reject_files);
448
449         # If we fail here someone is probably trying to exploit the race
450         # so let's just raise an exception ...
451         if os.path.exists(reject_filename):
452             os.unlink(reject_filename);
453         fd = os.open(reject_filename, os.O_RDWR|os.O_CREAT|os.O_EXCL, 0644);
454
455         if not manual:
456             Subst["__REJECTOR_ADDRESS__"] = Cnf["Dinstall::MyEmailAddress"];
457             Subst["__MANUAL_REJECT_MESSAGE__"] = "";
458             Subst["__CC__"] = "X-Katie-Rejection: automatic (moo)";
459             os.write(fd, reject_message);
460             os.close(fd);
461             reject_mail_message = utils.TemplateSubst(Subst,utils.open_file(Cnf["Dir::TemplatesDir"]+"/katie.rejected").read());
462         else:
463             # Build up the rejection email
464             user_email_address = utils.whoami() + " <%s>" % (Cnf["Dinstall::MyAdminAddress"]);
465
466             Subst["__REJECTOR_ADDRESS__"] = user_email_address;
467             Subst["__MANUAL_REJECT_MESSAGE__"] = reject_message;
468             Subst["__CC__"] = "Cc: " + Cnf["Dinstall::MyEmailAddress"];
469             reject_mail_message = utils.TemplateSubst(Subst,utils.open_file(Cnf["Dir::TemplatesDir"]+"/katie.rejected").read());
470
471             # Write the rejection email out as the <foo>.reason file
472             os.write(fd, reject_mail_message);
473             os.close(fd);
474
475             # If we weren't given a manual rejection message, spawn an
476             # editor so the user can add one in...
477             if reject_message == "":
478                 editor = os.environ.get("EDITOR","vi")
479                 result = os.system("%s +6 %s" % (editor, reject_filename))
480                 if result != 0:
481                     utils.fubar("editor invocation failed for '%s'!" % (reject_filename), result);
482                 reject_mail_message = utils.open_file(reject_filename).read();
483
484         # Send the rejection mail if appropriate
485         if not Cnf["Dinstall::Options::No-Mail"]:
486             utils.send_mail (reject_mail_message, "");
487
488         self.Logger.log(["rejected", pkg.changes_file]);
489
490     ################################################################################
491
492     # Ensure that source exists somewhere in the archive for the binary
493     # upload being processed.
494     #
495     # (1) exact match                      => 1.0-3
496     # (2) Bin-only NMU of an MU            => 1.0-3.0.1
497     # (3) Bin-only NMU of a sourceful-NMU  => 1.0-3.1.1
498
499     def source_exists (self, package, source_version):
500         q = self.projectB.query("SELECT s.version FROM source s WHERE s.source = '%s'" % (package));
501
502         # Reduce the query results to a list of version numbers
503         ql = map(lambda x: x[0], q.getresult());
504
505         # Try (1)
506         if ql.count(source_version):
507             return 1;
508
509         # Try (2)
510         orig_source_version = re_bin_only_nmu_of_mu.sub('', source_version);
511         if ql.count(orig_source_version):
512             return 1;
513
514         # Try (3)
515         orig_source_version = re_bin_only_nmu_of_nmu.sub('', source_version);
516         if ql.count(orig_source_version):
517             return 1;
518
519         # No source found...
520         return 0;
521
522     ################################################################################
523
524     def in_override_p (self, package, component, suite, binary_type, file):
525         files = self.pkg.files;
526
527         if binary_type == "": # must be source
528             type = "dsc";
529         else:
530             type = binary_type;
531
532         # Override suite name; used for example with proposed-updates
533         if self.Cnf.Find("Suite::%s::OverrideSuite" % (suite)) != "":
534             suite = self.Cnf["Suite::%s::OverrideSuite" % (suite)];
535
536         # Avoid <undef> on unknown distributions
537         suite_id = db_access.get_suite_id(suite);
538         if suite_id == -1:
539             return None;
540         component_id = db_access.get_component_id(component);
541         type_id = db_access.get_override_type_id(type);
542
543         # FIXME: nasty non-US speficic hack
544         if lower(component[:7]) == "non-us/":
545             component = component[7:];
546
547         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"
548                            % (package, suite_id, component_id, type_id));
549         result = q.getresult();
550         # If checking for a source package fall back on the binary override type
551         if type == "dsc" and not result:
552             type_id = db_access.get_override_type_id("deb");
553             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"
554                                % (package, suite_id, component_id, type_id));
555             result = q.getresult();
556
557         # Remember the section and priority so we can check them later if appropriate
558         if result != []:
559             files[file]["override section"] = result[0][0];
560             files[file]["override priority"] = result[0][1];
561
562         return result;
563
564     ################################################################################
565
566     def reject (self, str, prefix="Rejected: "):
567         if str:
568             # Unlike other rejects we add new lines first to avoid trailing
569             # new lines when this message is passed back up to a caller.
570             if self.reject_message:
571                 self.reject_message = self.reject_message + "\n";
572             self.reject_message = self.reject_message + prefix + str;
573
574     def check_binaries_against_db(self, file, suite):
575         self.reject_message = "";
576         files = self.pkg.files;
577
578         # Find any old binary packages
579         q = self.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"
580                            % (files[file]["package"], suite, files[file]["architecture"]))
581         for oldfile in q.dictresult():
582             files[file]["oldfiles"][suite] = oldfile;
583             # Check versions [NB: per-suite only; no cross-suite checking done (yet)]
584             if apt_pkg.VersionCompare(files[file]["version"], oldfile["version"]) != 1:
585                 self.reject("%s: old version (%s) >= new version (%s)." % (file, oldfile["version"], files[file]["version"]));
586         # Check for any existing copies of the file
587         q = self.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"]))
588         if q.getresult() != []:
589             self.reject("can not overwrite existing copy of '%s' already in the archive." % (file));
590
591         return self.reject_message;
592
593     ################################################################################
594
595     def check_source_against_db(self, file):
596         """Ensure source is newer than existing source in target suites."""
597         self.reject_message = "";
598         changes = self.pkg.changes;
599         dsc = self.pkg.dsc;
600
601         package = dsc.get("source");
602         new_version = dsc.get("version");
603         for suite in changes["distribution"].keys():
604             q = self.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"
605                                % (package, suite));
606             ql = map(lambda x: x[0], q.getresult());
607             for old_version in ql:
608                 if apt_pkg.VersionCompare(new_version, old_version) != 1:
609                     self.reject("%s: Old version `%s' >= new version `%s'." % (file, old_version, new_version));
610         return self.reject_message;
611
612     ################################################################################
613
614     def check_dsc_against_db(self, file):
615         self.reject_message = "";
616         files = self.pkg.files;
617         dsc_files = self.pkg.dsc_files;
618         legacy_source_untouchable = self.pkg.legacy_source_untouchable;
619         orig_tar_gz = None;
620
621         # Try and find all files mentioned in the .dsc.  This has
622         # to work harder to cope with the multiple possible
623         # locations of an .orig.tar.gz.
624         for dsc_file in dsc_files.keys():
625             found = None;
626             if files.has_key(dsc_file):
627                 actual_md5 = files[dsc_file]["md5sum"];
628                 actual_size = int(files[dsc_file]["size"]);
629                 found = "%s in incoming" % (dsc_file)
630                 # Check the file does not already exist in the archive
631                 q = self.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));
632
633                 # "It has not broken them.  It has fixed a
634                 # brokenness.  Your crappy hack exploited a bug in
635                 # the old dinstall.
636                 #
637                 # "(Come on!  I thought it was always obvious that
638                 # one just doesn't release different files with
639                 # the same name and version.)"
640                 #                        -- ajk@ on d-devel@l.d.o
641
642                 if q.getresult() != []:
643                     self.reject("can not overwrite existing copy of '%s' already in the archive." % (dsc_file));
644             elif dsc_file[-12:] == ".orig.tar.gz":
645                 # Check in the pool
646                 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));
647                 ql = q.getresult();
648
649                 if ql != []:
650                     # Unfortunately, we make get more than one
651                     # match here if, for example, the package was
652                     # in potato but had a -sa upload in woody.  So
653                     # we need to choose the right one.
654
655                     x = ql[0]; # default to something sane in case we don't match any or have only one
656
657                     if len(ql) > 1:
658                         for i in ql:
659                             old_file = i[0] + i[1];
660                             actual_md5 = apt_pkg.md5sum(utils.open_file(old_file));
661                             actual_size = os.stat(old_file)[stat.ST_SIZE];
662                             if actual_md5 == dsc_files[dsc_file]["md5sum"] and actual_size == int(dsc_files[dsc_file]["size"]):
663                                 x = i;
664                             else:
665                                 legacy_source_untouchable[i[3]] = "";
666
667                     old_file = x[0] + x[1];
668                     actual_md5 = apt_pkg.md5sum(utils.open_file(old_file));
669                     actual_size = os.stat(old_file)[stat.ST_SIZE];
670                     found = old_file;
671                     suite_type = x[2];
672                     dsc_files[dsc_file]["files id"] = x[3]; # need this for updating dsc_files in install()
673                     # See install() in katie...
674                     self.pkg.orig_tar_id = x[3];
675                     if suite_type == "legacy" or suite_type == "legacy-mixed":
676                         self.pkg.orig_tar_location = "legacy";
677                     else:
678                         self.pkg.orig_tar_location = x[4];
679                 else:
680                     # Not there? Check the queue directories...
681
682                     in_unchecked = os.path.join(self.Cnf["Dir::QueueUncheckedDir"],dsc_file);
683                     # See process_it() in jennifer for explanation of this
684                     if os.path.exists(in_unchecked):
685                         return (self.reject_message, in_unchecked);
686                     else:
687                         for dir in [ "Accepted", "New", "Byhand" ]:
688                             in_otherdir = os.path.join(self.Cnf["Dir::Queue%sDir" % (dir)],dsc_file);
689                             if os.path.exists(in_otherdir):
690                                 actual_md5 = apt_pkg.md5sum(utils.open_file(in_otherdir));
691                                 actual_size = os.stat(in_otherdir)[stat.ST_SIZE];
692                                 found = in_otherdir;
693
694                     if not found:
695                         self.reject("%s refers to %s, but I can't find it in the queue or in the pool." % (file, dsc_file));
696                         continue;
697             else:
698                 self.reject("%s refers to %s, but I can't find it in the queue." % (file, dsc_file));
699                 continue;
700             if actual_md5 != dsc_files[dsc_file]["md5sum"]:
701                 self.reject("md5sum for %s doesn't match %s." % (found, file));
702             if actual_size != int(dsc_files[dsc_file]["size"]):
703                 self.reject("size for %s doesn't match %s." % (found, file));
704
705         return (self.reject_message, orig_tar_gz);
706
707     def do_query(self, q):
708         sys.stderr.write("query: \"%s\" ... " % (q));
709         before = time.time();
710         r = self.projectB.query(q);
711         time_diff = time.time()-before;
712         sys.stderr.write("took %.3f seconds.\n" % (time_diff));
713         return r;