]> git.decadent.org.uk Git - dak.git/blob - daklib/queue.py
convert daklib/queue.py to use new dbconn and changes API
[dak.git] / daklib / queue.py
1 #!/usr/bin/env python
2 # vim:set et sw=4:
3
4 """
5 Queue utility functions for dak
6
7 @contact: Debian FTP Master <ftpmaster@debian.org>
8 @copyright: 2001 - 2006 James Troup <james@nocrew.org>
9 @copyright: 2009  Joerg Jaspert <joerg@debian.org>
10 @license: GNU General Public License version 2 or later
11 """
12
13 # This program is free software; you can redistribute it and/or modify
14 # it under the terms of the GNU General Public License as published by
15 # the Free Software Foundation; either version 2 of the License, or
16 # (at your option) any later version.
17
18 # This program is distributed in the hope that it will be useful,
19 # but WITHOUT ANY WARRANTY; without even the implied warranty of
20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
21 # GNU General Public License for more details.
22
23 # You should have received a copy of the GNU General Public License
24 # along with this program; if not, write to the Free Software
25 # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
26
27 ###############################################################################
28
29 import cPickle
30 import errno
31 import os
32 import pg
33 import stat
34 import sys
35 import time
36 import apt_inst
37 import apt_pkg
38 import utils
39 import database
40
41 from dak_exceptions import *
42 from changes import *
43 from regexes import re_default_answer, re_fdnic, re_bin_only_nmu
44 from config import Config
45 from summarystats import SummaryStats
46
47 from types import *
48
49 ###############################################################################
50
51 # Determine what parts in a .changes are NEW
52
53 def determine_new(changes, files, projectB, warn=1):
54     """
55     Determine what parts in a C{changes} file are NEW.
56
57     @type changes: Upload.Pkg.changes dict
58     @param changes: Changes dictionary
59
60     @type files: Upload.Pkg.files dict
61     @param files: Files dictionary
62
63     @type projectB: pgobject
64     @param projectB: DB handle
65
66     @type warn: bool
67     @param warn: Warn if overrides are added for (old)stable
68
69     @rtype: dict
70     @return: dictionary of NEW components.
71
72     """
73     new = {}
74
75     # Build up a list of potentially new things
76     for file_entry in files.keys():
77         f = files[file_entry]
78         # Skip byhand elements
79         if f["type"] == "byhand":
80             continue
81         pkg = f["package"]
82         priority = f["priority"]
83         section = f["section"]
84         file_type = get_type(f)
85         component = f["component"]
86
87         if file_type == "dsc":
88             priority = "source"
89         if not new.has_key(pkg):
90             new[pkg] = {}
91             new[pkg]["priority"] = priority
92             new[pkg]["section"] = section
93             new[pkg]["type"] = file_type
94             new[pkg]["component"] = component
95             new[pkg]["files"] = []
96         else:
97             old_type = new[pkg]["type"]
98             if old_type != file_type:
99                 # source gets trumped by deb or udeb
100                 if old_type == "dsc":
101                     new[pkg]["priority"] = priority
102                     new[pkg]["section"] = section
103                     new[pkg]["type"] = file_type
104                     new[pkg]["component"] = component
105         new[pkg]["files"].append(file_entry)
106         if f.has_key("othercomponents"):
107             new[pkg]["othercomponents"] = f["othercomponents"]
108
109     for suite in changes["suite"].keys():
110         suite_id = database.get_suite_id(suite)
111         for pkg in new.keys():
112             component_id = database.get_component_id(new[pkg]["component"])
113             type_id = database.get_override_type_id(new[pkg]["type"])
114             q = projectB.query("SELECT package FROM override WHERE package = '%s' AND suite = %s AND component = %s AND type = %s" % (pkg, suite_id, component_id, type_id))
115             ql = q.getresult()
116             if ql:
117                 for file_entry in new[pkg]["files"]:
118                     if files[file_entry].has_key("new"):
119                         del files[file_entry]["new"]
120                 del new[pkg]
121
122     if warn:
123         if changes["suite"].has_key("stable"):
124             print "WARNING: overrides will be added for stable!"
125             if changes["suite"].has_key("oldstable"):
126                 print "WARNING: overrides will be added for OLDstable!"
127         for pkg in new.keys():
128             if new[pkg].has_key("othercomponents"):
129                 print "WARNING: %s already present in %s distribution." % (pkg, new[pkg]["othercomponents"])
130
131     return new
132
133 ################################################################################
134
135 def get_type(file):
136     """
137     Get the file type of C{file}
138
139     @type file: dict
140     @param file: file entry
141
142     @rtype: string
143     @return: filetype
144
145     """
146     # Determine the type
147     if file.has_key("dbtype"):
148         file_type = file["dbtype"]
149     elif file["type"] in [ "orig.tar.gz", "orig.tar.bz2", "tar.gz", "tar.bz2", "diff.gz", "diff.bz2", "dsc" ]:
150         file_type = "dsc"
151     else:
152         utils.fubar("invalid type (%s) for new.  Dazed, confused and sure as heck not continuing." % (file_type))
153
154     # Validate the override type
155     type_id = database.get_override_type_id(file_type)
156     if type_id == -1:
157         utils.fubar("invalid type (%s) for new.  Say wha?" % (file_type))
158
159     return file_type
160
161 ################################################################################
162
163
164
165 def check_valid(new):
166     """
167     Check if section and priority for NEW packages exist in database.
168     Additionally does sanity checks:
169       - debian-installer packages have to be udeb (or source)
170       - non debian-installer packages can not be udeb
171       - source priority can only be assigned to dsc file types
172
173     @type new: dict
174     @param new: Dict of new packages with their section, priority and type.
175
176     """
177     for pkg in new.keys():
178         section = new[pkg]["section"]
179         priority = new[pkg]["priority"]
180         file_type = new[pkg]["type"]
181         new[pkg]["section id"] = database.get_section_id(section)
182         new[pkg]["priority id"] = database.get_priority_id(new[pkg]["priority"])
183         # Sanity checks
184         di = section.find("debian-installer") != -1
185         if (di and file_type not in ("udeb", "dsc")) or (not di and file_type == "udeb"):
186             new[pkg]["section id"] = -1
187         if (priority == "source" and file_type != "dsc") or \
188            (priority != "source" and file_type == "dsc"):
189             new[pkg]["priority id"] = -1
190
191
192 ###############################################################################
193
194 class Upload(object):
195     """
196     Everything that has to do with an upload processed.
197
198     """
199     def __init__(self):
200         """
201         Initialize various variables and the global substitution template mappings.
202         Also connect to the DB and initialize the Database module.
203
204         """
205
206         self.pkg = Changes()
207         self.reset()
208
209     ###########################################################################
210
211     def reset (self):
212         """ Reset a number of internal variables."""
213
214        # Initialize the substitution template map
215         cnf = Config()
216         self.Subst = {}
217         self.Subst["__ADMIN_ADDRESS__"] = cnf["Dinstall::MyAdminAddress"]
218         self.Subst["__BUG_SERVER__"] = cnf["Dinstall::BugServer"]
219         self.Subst["__DISTRO__"] = cnf["Dinstall::MyDistribution"]
220         self.Subst["__DAK_ADDRESS__"] = cnf["Dinstall::MyEmailAddress"]
221
222         self.reject_message = ""
223         self.changes.reset()
224
225     ###########################################################################
226     def update_subst(self, reject_message = ""):
227         """ Set up the per-package template substitution mappings """
228
229         cnf = Config()
230
231         # If 'dak process-unchecked' crashed out in the right place, architecture may still be a string.
232         if not self.pkg.changes.has_key("architecture") or not \
233            isinstance(changes["architecture"], DictType):
234             self.pkg.changes["architecture"] = { "Unknown" : "" }
235
236         # and maintainer2047 may not exist.
237         if not self.pkg.changes.has_key("maintainer2047"):
238             self.pkg.changes["maintainer2047"] = cnf["Dinstall::MyEmailAddress"]
239
240         self.Subst["__ARCHITECTURE__"] = " ".join(self.pkg.changes["architecture"].keys())
241         self.Subst["__CHANGES_FILENAME__"] = os.path.basename(self.pkg.changes_file)
242         self.Subst["__FILE_CONTENTS__"] = self.pkg.changes.get("filecontents", "")
243
244         # For source uploads the Changed-By field wins; otherwise Maintainer wins.
245         if self.pkg.changes["architecture"].has_key("source") and \
246            self.pkg.changes["changedby822"] != "" and \
247            (self.pkg.changes["changedby822"] != self.pkg.changes["maintainer822"]):
248
249             self.Subst["__MAINTAINER_FROM__"] = self.pkg.changes["changedby2047"]
250             self.Subst["__MAINTAINER_TO__"] = "%s, %s" % (self.pkg.changes["changedby2047"], changes["maintainer2047"])
251             self.Subst["__MAINTAINER__"] = self.pkg.changes.get("changed-by", "Unknown")
252         else:
253             self.Subst["__MAINTAINER_FROM__"] = self.pkg.changes["maintainer2047"]
254             self.Subst["__MAINTAINER_TO__"] = self.pkg.changes["maintainer2047"]
255             self.Subst["__MAINTAINER__"] = self.pkg.changes.get("maintainer", "Unknown")
256
257         if "sponsoremail" in self.pkg.changes:
258             self.Subst["__MAINTAINER_TO__"] += ", %s" % self.pkg.changes["sponsoremail"]
259
260         if cnf.has_key("Dinstall::TrackingServer") and self.pkg.changes.has_key("source"):
261             self.Subst["__MAINTAINER_TO__"] += "\nBcc: %s@%s" % (self.pkg.changes["source"], cnf["Dinstall::TrackingServer"])
262
263         # Apply any global override of the Maintainer field
264         if cnf.get("Dinstall::OverrideMaintainer"):
265             self.Subst["__MAINTAINER_TO__"] = cnf["Dinstall::OverrideMaintainer"]
266             self.Subst["__MAINTAINER_FROM__"] = cnf["Dinstall::OverrideMaintainer"]
267
268         self.Subst["__REJECT_MESSAGE__"] = self.reject_message
269         self.Subst["__SOURCE__"] = self.pkg.changes.get("source", "Unknown")
270         self.Subst["__VERSION__"] = self.pkg.changes.get("version", "Unknown")
271
272     ###########################################################################
273
274     def build_summaries(self):
275         """ Build a summary of changes the upload introduces. """
276
277         (byhand, new, summary, override_summary) = self.pkg.file_summary()
278
279         short_summary = summary
280
281         # This is for direport's benefit...
282         f = re_fdnic.sub("\n .\n", self.pkg.changes.get("changes", ""))
283
284         if byhand or new:
285             summary += "Changes: " + f
286
287         summary += "\n\nOverride entries for your package:\n" + override_summary + "\n"
288
289         summary += self.announce(short_summary, 0)
290
291         return (summary, short_summary)
292
293     ###########################################################################
294
295     def close_bugs(self, summary, action):
296         """
297         Send mail to close bugs as instructed by the closes field in the changes file.
298         Also add a line to summary if any work was done.
299
300         @type summary: string
301         @param summary: summary text, as given by L{build_summaries}
302
303         @type action: bool
304         @param action: Set to false no real action will be done.
305
306         @rtype: string
307         @return: summary. If action was taken, extended by the list of closed bugs.
308
309         """
310
311         template = os.path.join(Config()["Dir::Templates"], 'process-unchecked.bug-close')
312
313         bugs = self.pkg.changes["closes"].keys()
314
315         if not bugs:
316             return summary
317
318         bugs.sort()
319         summary += "Closing bugs: "
320         for bug in bugs:
321             summary += "%s " % (bug)
322             if action:
323                 self.Subst["__BUG_NUMBER__"] = bug
324                 if self.pkg.changes["distribution"].has_key("stable"):
325                     self.Subst["__STABLE_WARNING__"] = """
326 Note that this package is not part of the released stable Debian
327 distribution.  It may have dependencies on other unreleased software,
328 or other instabilities.  Please take care if you wish to install it.
329 The update will eventually make its way into the next released Debian
330 distribution."""
331                 else:
332                     self.Subst["__STABLE_WARNING__"] = ""
333                     mail_message = utils.TemplateSubst(self.Subst, template)
334                     utils.send_mail(mail_message)
335
336                 # Clear up after ourselves
337                 del self.Subst["__BUG_NUMBER__"]
338                 del self.Subst["__STABLE_WARNING__"]
339
340         if action:
341             self.Logger.log(["closing bugs"] + bugs)
342
343         summary += "\n"
344
345         return summary
346
347     ###########################################################################
348
349     def announce(self, short_summary, action):
350         """
351         Send an announce mail about a new upload.
352
353         @type short_summary: string
354         @param short_summary: Short summary text to include in the mail
355
356         @type action: bool
357         @param action: Set to false no real action will be done.
358
359         @rtype: string
360         @return: Textstring about action taken.
361
362         """
363
364         cnf = Config()
365         announcetemplate = os.path.join(cnf["Dir::Templates"], 'process-unchecked.announce')
366
367         # Only do announcements for source uploads with a recent dpkg-dev installed
368         if float(self.pkg.changes.get("format", 0)) < 1.6 or not \
369            self.pkg.changes["architecture"].has_key("source"):
370             return ""
371
372         lists_done = {}
373         summary = ""
374
375         self.Subst["__SHORT_SUMMARY__"] = short_summary
376
377         for dist in self.pkg.changes["distribution"].keys():
378             announce_list = Cnf.Find("Suite::%s::Announce" % (dist))
379             if announce_list == "" or lists_done.has_key(announce_list):
380                 continue
381
382             lists_done[announce_list] = 1
383             summary += "Announcing to %s\n" % (announce_list)
384
385             if action:
386                 self.Subst["__ANNOUNCE_LIST_ADDRESS__"] = announce_list
387                 if cnf.get("Dinstall::TrackingServer") and \
388                    self.pkg.changes["architecture"].has_key("source"):
389                     trackingsendto = "Bcc: %s@%s" % (self.pkg.changes["source"], cnf["Dinstall::TrackingServer"])
390                     self.Subst["__ANNOUNCE_LIST_ADDRESS__"] += "\n" + trackingsendto
391
392                 mail_message = utils.TemplateSubst(self.Subst, announcetemplate)
393                 utils.send_mail(mail_message)
394
395                 del self.Subst["__ANNOUNCE_LIST_ADDRESS__"]
396
397         if cnf.FindB("Dinstall::CloseBugs"):
398             summary = self.close_bugs(summary, action)
399
400         del self.Subst["__SHORT_SUMMARY__"]
401
402         return summary
403
404     ###########################################################################
405
406     def accept (self, summary, short_summary, targetdir=None):
407         """
408         Accept an upload.
409
410         This moves all files referenced from the .changes into the I{accepted}
411         queue, sends the accepted mail, announces to lists, closes bugs and
412         also checks for override disparities. If enabled it will write out
413         the version history for the BTS Version Tracking and will finally call
414         L{queue_build}.
415
416         @type summary: string
417         @param summary: Summary text
418
419         @type short_summary: string
420         @param short_summary: Short summary
421
422         """
423
424         cnf = Config()
425         stats = SummaryStats()
426
427         accepttemplate = os.path.join(cnf["Dir::Templates"], 'process-unchecked.accepted')
428
429         if targetdir is None:
430             targetdir = cnf["Dir::Queue::Accepted"]
431
432         print "Accepting."
433         self.Logger.log(["Accepting changes", self.pkg.changes_file])
434
435         self.write_dot_dak(targetdir)
436
437         # Move all the files into the accepted directory
438         utils.move(self.pkg.changes_file, targetdir)
439
440         for name, entry in sorted(self.pkg.files.items()):
441             utils.move(name, targetdir)
442             stats.accept_bytes += float(entry["size"])
443
444         stats.accept_count += 1
445
446         # Send accept mail, announce to lists, close bugs and check for
447         # override disparities
448         if not cnf["Dinstall::Options::No-Mail"]:
449             self.Subst["__SUITE__"] = ""
450             self.Subst["__SUMMARY__"] = summary
451             mail_message = utils.TemplateSubst(self.Subst, accepttemplate)
452             utils.send_mail(mail_message)
453             self.announce(short_summary, 1)
454
455         ## Helper stuff for DebBugs Version Tracking
456         if cnf.Find("Dir::Queue::BTSVersionTrack"):
457             # ??? once queue/* is cleared on *.d.o and/or reprocessed
458             # the conditionalization on dsc["bts changelog"] should be
459             # dropped.
460
461             # Write out the version history from the changelog
462             if self.pkg.changes["architecture"].has_key("source") and \
463                self.pkg.dsc.has_key("bts changelog"):
464
465                 (fd, temp_filename) = utils.temp_filename(cnf["Dir::Queue::BTSVersionTrack"], prefix=".")
466                 version_history = os.fdopen(fd, 'w')
467                 version_history.write(self.pkg.dsc["bts changelog"])
468                 version_history.close()
469                 filename = "%s/%s" % (cnf["Dir::Queue::BTSVersionTrack"],
470                                       self.pkg.changes_file[:-8]+".versions")
471                 os.rename(temp_filename, filename)
472                 os.chmod(filename, 0644)
473
474             # Write out the binary -> source mapping.
475             (fd, temp_filename) = utils.temp_filename(cnf["Dir::Queue::BTSVersionTrack"], prefix=".")
476             debinfo = os.fdopen(fd, 'w')
477             for name, entry in sorted(self.pkg.files.items()):
478                 if entry["type"] == "deb":
479                     line = " ".join([entry["package"], entry["version"],
480                                      entry["architecture"], entry["source package"],
481                                      entry["source version"]])
482                     debinfo.write(line+"\n")
483             debinfo.close()
484             filename = "%s/%s" % (cnf["Dir::Queue::BTSVersionTrack"],
485                                   self.pkg.changes_file[:-8]+".debinfo")
486             os.rename(temp_filename, filename)
487             os.chmod(filename, 0644)
488
489         # Its is Cnf["Dir::Queue::Accepted"] here, not targetdir!
490         # <Ganneff> we do call queue_build too
491         # <mhy> well yes, we'd have had to if we were inserting into accepted
492         # <Ganneff> now. thats database only.
493         # <mhy> urgh, that's going to get messy
494         # <Ganneff> so i make the p-n call to it *also* using accepted/
495         # <mhy> but then the packages will be in the queue_build table without the files being there
496         # <Ganneff> as the buildd queue is only regenerated whenever unchecked runs
497         # <mhy> ah, good point
498         # <Ganneff> so it will work out, as unchecked move it over
499         # <mhy> that's all completely sick
500         # <Ganneff> yes
501
502         # This routine returns None on success or an error on failure
503         res = get_queue('accepted').autobuild_upload(self.pkg, cnf["Dir::Queue::Accepted"])
504         if res:
505             utils.fubar(res)
506
507
508     def check_override (self):
509         """
510         Checks override entries for validity. Mails "Override disparity" warnings,
511         if that feature is enabled.
512
513         Abandons the check if
514           - override disparity checks are disabled
515           - mail sending is disabled
516         """
517
518         cnf = Config()
519
520         # Abandon the check if:
521         #  a) override disparity checks have been disabled
522         #  b) we're not sending mail
523         if not cnf.FindB("Dinstall::OverrideDisparityCheck") or \
524            cnf["Dinstall::Options::No-Mail"]:
525             return
526
527         summary = self.pkg.check_override()
528
529         if summary == "":
530             return
531
532         overridetemplate = os.path.join(cnf["Dir::Templates"], 'process-unchecked.override-disparity')
533
534         self.Subst["__SUMMARY__"] = summary
535         mail_message = utils.TemplateSubst(self.Subst, overridetemplate)
536         utils.send_mail(mail_message)
537         del self.Subst["__SUMMARY__"]
538
539     ###########################################################################
540     def force_reject(self, reject_files):
541         """
542         Forcefully move files from the current directory to the
543         reject directory.  If any file already exists in the reject
544         directory it will be moved to the morgue to make way for
545         the new file.
546
547         @type files: dict
548         @param files: file dictionary
549
550         """
551
552         cnf = Config()
553
554         for file_entry in reject_files:
555             # Skip any files which don't exist or which we don't have permission to copy.
556             if os.access(file_entry, os.R_OK) == 0:
557                 continue
558
559             dest_file = os.path.join(cnf["Dir::Queue::Reject"], file_entry)
560
561             try:
562                 dest_fd = os.open(dest_file, os.O_RDWR | os.O_CREAT | os.O_EXCL, 0644)
563             except OSError, e:
564                 # File exists?  Let's try and move it to the morgue
565                 if e.errno == errno.EEXIST:
566                     morgue_file = os.path.join(cnf["Dir::Morgue"], cnf["Dir::MorgueReject"], file_entry)
567                     try:
568                         morgue_file = utils.find_next_free(morgue_file)
569                     except NoFreeFilenameError:
570                         # Something's either gone badly Pete Tong, or
571                         # someone is trying to exploit us.
572                         utils.warn("**WARNING** failed to move %s from the reject directory to the morgue." % (file_entry))
573                         return
574                     utils.move(dest_file, morgue_file, perms=0660)
575                     try:
576                         dest_fd = os.open(dest_file, os.O_RDWR|os.O_CREAT|os.O_EXCL, 0644)
577                     except OSError, e:
578                         # Likewise
579                         utils.warn("**WARNING** failed to claim %s in the reject directory." % (file_entry))
580                         return
581                 else:
582                     raise
583             # If we got here, we own the destination file, so we can
584             # safely overwrite it.
585             utils.move(file_entry, dest_file, 1, perms=0660)
586             os.close(dest_fd)
587
588     ###########################################################################
589     def do_reject (self, manual=0, reject_message="", note=""):
590         """
591         Reject an upload. If called without a reject message or C{manual} is
592         true, spawn an editor so the user can write one.
593
594         @type manual: bool
595         @param manual: manual or automated rejection
596
597         @type reject_message: string
598         @param reject_message: A reject message
599
600         @return: 0
601
602         """
603         # If we weren't given a manual rejection message, spawn an
604         # editor so the user can add one in...
605         if manual and not reject_message:
606             (fd, temp_filename) = utils.temp_filename()
607             temp_file = os.fdopen(fd, 'w')
608             if len(note) > 0:
609                 for line in note:
610                     temp_file.write(line)
611             temp_file.close()
612             editor = os.environ.get("EDITOR","vi")
613             answer = 'E'
614             while answer == 'E':
615                 os.system("%s %s" % (editor, temp_filename))
616                 temp_fh = utils.open_file(temp_filename)
617                 reject_message = "".join(temp_fh.readlines())
618                 temp_fh.close()
619                 print "Reject message:"
620                 print utils.prefix_multi_line_string(reject_message,"  ",include_blank_lines=1)
621                 prompt = "[R]eject, Edit, Abandon, Quit ?"
622                 answer = "XXX"
623                 while prompt.find(answer) == -1:
624                     answer = utils.our_raw_input(prompt)
625                     m = re_default_answer.search(prompt)
626                     if answer == "":
627                         answer = m.group(1)
628                     answer = answer[:1].upper()
629             os.unlink(temp_filename)
630             if answer == 'A':
631                 return 1
632             elif answer == 'Q':
633                 sys.exit(0)
634
635         print "Rejecting.\n"
636
637         cnf = Config()
638
639         reason_filename = self.pkg.changes_file[:-8] + ".reason"
640         reason_filename = os.path.join(cnf["Dir::Queue::Reject"], reason_filename)
641
642         # Move all the files into the reject directory
643         reject_files = self.pkg.files.keys() + [self.pkg.changes_file]
644         self.force_reject(reject_files)
645
646         # If we fail here someone is probably trying to exploit the race
647         # so let's just raise an exception ...
648         if os.path.exists(reason_filename):
649             os.unlink(reason_filename)
650         reason_fd = os.open(reason_filename, os.O_RDWR|os.O_CREAT|os.O_EXCL, 0644)
651
652         rej_template = os.path.join(cnf["Dir::Templates"], "queue.rejected")
653
654         if not manual:
655             self.Subst["__REJECTOR_ADDRESS__"] = cnf["Dinstall::MyEmailAddress"]
656             self.Subst["__MANUAL_REJECT_MESSAGE__"] = ""
657             self.Subst["__CC__"] = "X-DAK-Rejection: automatic (moo)\nX-Katie-Rejection: automatic (moo)"
658             os.write(reason_fd, reject_message)
659             reject_mail_message = utils.TemplateSubst(self.Subst, rej_template)
660         else:
661             # Build up the rejection email
662             user_email_address = utils.whoami() + " <%s>" % (cnf["Dinstall::MyAdminAddress"])
663             self.Subst["__REJECTOR_ADDRESS__"] = user_email_address
664             self.Subst["__MANUAL_REJECT_MESSAGE__"] = reject_message
665             self.Subst["__CC__"] = "Cc: " + Cnf["Dinstall::MyEmailAddress"]
666             reject_mail_message = utils.TemplateSubst(self.Subst, rej_template)
667             # Write the rejection email out as the <foo>.reason file
668             os.write(reason_fd, reject_mail_message)
669
670         del self.Subst["__REJECTOR_ADDRESS__"]
671         del self.Subst["__MANUAL_REJECT_MESSAGE__"]
672         del self.Subst["__CC__"]
673
674         os.close(reason_fd)
675
676         # Send the rejection mail if appropriate
677         if not cnf["Dinstall::Options::No-Mail"]:
678             utils.send_mail(reject_mail_message)
679
680         self.Logger.log(["rejected", pkg.changes_file])
681
682         return 0
683
684     ################################################################################
685     def in_override_p(self, package, component, suite, binary_type, file, session=None):
686         """
687         Check if a package already has override entries in the DB
688
689         @type package: string
690         @param package: package name
691
692         @type component: string
693         @param component: database id of the component, as returned by L{database.get_component_id}
694
695         @type suite: int
696         @param suite: database id of the suite, as returned by L{database.get_suite_id}
697
698         @type binary_type: string
699         @param binary_type: type of the package
700
701         @type file: string
702         @param file: filename we check
703
704         @return: the database result. But noone cares anyway.
705
706         """
707
708         cnf = Config()
709
710         if session is None:
711             session = DBConn().session()
712
713         if binary_type == "": # must be source
714             file_type = "dsc"
715         else:
716             file_type = binary_type
717
718         # Override suite name; used for example with proposed-updates
719         if cnf.Find("Suite::%s::OverrideSuite" % (suite)) != "":
720             suite = cnf["Suite::%s::OverrideSuite" % (suite)]
721
722         result = get_override(package, suite, component, file_type, session)
723
724         # If checking for a source package fall back on the binary override type
725         if file_type == "dsc" and len(result) < 1:
726             result = get_override(package, suite, component, ['deb', 'udeb'], session)
727
728         # Remember the section and priority so we can check them later if appropriate
729         if len(result) > 0:
730             result = result[0]
731             self.pkg.files[file]["override section"] = result.section.section
732             self.pkg.files[file]["override priority"] = result.priority.priority
733             return result
734
735         return None
736
737     ################################################################################
738     def reject (self, str, prefix="Rejected: "):
739         """
740         Add C{str} to reject_message. Adds C{prefix}, by default "Rejected: "
741
742         @type str: string
743         @param str: Reject text
744
745         @type prefix: string
746         @param prefix: Prefix text, default Rejected:
747
748         """
749         if str:
750             # Unlike other rejects we add new lines first to avoid trailing
751             # new lines when this message is passed back up to a caller.
752             if self.reject_message:
753                 self.reject_message += "\n"
754             self.reject_message += prefix + str
755
756     ################################################################################
757     def get_anyversion(self, sv_list, suite):
758         """
759         @type sv_list: list
760         @param sv_list: list of (suite, version) tuples to check
761
762         @type suite: string
763         @param suite: suite name
764
765         Description: TODO
766         """
767         anyversion = None
768         anysuite = [suite] + self.Cnf.ValueList("Suite::%s::VersionChecks::Enhances" % (suite))
769         for (s, v) in sv_list:
770             if s in [ x.lower() for x in anysuite ]:
771                 if not anyversion or apt_pkg.VersionCompare(anyversion, v) <= 0:
772                     anyversion = v
773
774         return anyversion
775
776     ################################################################################
777
778     def cross_suite_version_check(self, sv_list, file, new_version, sourceful=False):
779         """
780         @type sv_list: list
781         @param sv_list: list of (suite, version) tuples to check
782
783         @type file: string
784         @param file: XXX
785
786         @type new_version: string
787         @param new_version: XXX
788
789         Ensure versions are newer than existing packages in target
790         suites and that cross-suite version checking rules as
791         set out in the conf file are satisfied.
792         """
793
794         cnf = Config()
795
796         # Check versions for each target suite
797         for target_suite in self.pkg.changes["distribution"].keys():
798             must_be_newer_than = [ i.lower() for i in cnf.ValueList("Suite::%s::VersionChecks::MustBeNewerThan" % (target_suite)) ]
799             must_be_older_than = [ i.lower() for i in cnf.ValueList("Suite::%s::VersionChecks::MustBeOlderThan" % (target_suite)) ]
800
801             # Enforce "must be newer than target suite" even if conffile omits it
802             if target_suite not in must_be_newer_than:
803                 must_be_newer_than.append(target_suite)
804
805             for (suite, existent_version) in sv_list:
806                 vercmp = apt_pkg.VersionCompare(new_version, existent_version)
807
808                 if suite in must_be_newer_than and sourceful and vercmp < 1:
809                     self.reject("%s: old version (%s) in %s >= new version (%s) targeted at %s." % (file, existent_version, suite, new_version, target_suite))
810
811                 if suite in must_be_older_than and vercmp > -1:
812                     cansave = 0
813
814                     if self.pkg.changes.get('distribution-version', {}).has_key(suite):
815                         # we really use the other suite, ignoring the conflicting one ...
816                         addsuite = self.pkg.changes["distribution-version"][suite]
817
818                         add_version = self.get_anyversion(sv_list, addsuite)
819                         target_version = self.get_anyversion(sv_list, target_suite)
820
821                         if not add_version:
822                             # not add_version can only happen if we map to a suite
823                             # that doesn't enhance the suite we're propup'ing from.
824                             # so "propup-ver x a b c; map a d" is a problem only if
825                             # d doesn't enhance a.
826                             #
827                             # i think we could always propagate in this case, rather
828                             # than complaining. either way, this isn't a REJECT issue
829                             #
830                             # And - we really should complain to the dorks who configured dak
831                             self.reject("%s is mapped to, but not enhanced by %s - adding anyways" % (suite, addsuite), "Warning: ")
832                             self.pkg.changes.setdefault("propdistribution", {})
833                             self.pkg.changes["propdistribution"][addsuite] = 1
834                             cansave = 1
835                         elif not target_version:
836                             # not targets_version is true when the package is NEW
837                             # we could just stick with the "...old version..." REJECT
838                             # for this, I think.
839                             self.reject("Won't propogate NEW packages.")
840                         elif apt_pkg.VersionCompare(new_version, add_version) < 0:
841                             # propogation would be redundant. no need to reject though.
842                             self.reject("ignoring versionconflict: %s: old version (%s) in %s <= new version (%s) targeted at %s." % (file, existent_version, suite, new_version, target_suite), "Warning: ")
843                             cansave = 1
844                         elif apt_pkg.VersionCompare(new_version, add_version) > 0 and \
845                              apt_pkg.VersionCompare(add_version, target_version) >= 0:
846                             # propogate!!
847                             self.reject("Propogating upload to %s" % (addsuite), "Warning: ")
848                             self.pkg.changes.setdefault("propdistribution", {})
849                             self.pkg.changes["propdistribution"][addsuite] = 1
850                             cansave = 1
851
852                     if not cansave:
853                         self.reject("%s: old version (%s) in %s <= new version (%s) targeted at %s." % (file, existent_version, suite, new_version, target_suite))
854
855     ################################################################################
856
857     def check_binary_against_db(self, file, session=None):
858         """
859
860         """
861
862         if session is None:
863             session = DBConn().session()
864
865         self.reject_message = ""
866
867         # Ensure version is sane
868         q = session.query(BinAssociation)
869         q = q.join(DBBinary).filter(DBBinary.package==self.pkg.files[file]["package"])
870         q = q.join(Architecture).filter(Architecture.arch_string.in_([self.pkg.files[file]["architecture"], 'all']))
871
872         self.cross_suite_version_check([ (x.suite.suite_name, x.binary.version) for x in q.all() ],
873                                        file, files[file]["version"], sourceful=False)
874
875         # Check for any existing copies of the file
876         q = session.query(DBBinary).filter_by(files[file]["package"])
877         q = q.filter_by(version=files[file]["version"])
878         q = q.join(Architecture).filter_by(arch_string=files[file]["architecture"])
879
880         if q.count() > 0:
881             self.reject("%s: can not overwrite existing copy already in the archive." % (file))
882
883         return self.reject_message
884
885     ################################################################################
886
887     def check_source_against_db(self, file, session=None):
888         """
889         """
890         if session is None:
891             session = DBConn().session()
892
893         self.reject_message = ""
894         source = self.pkg.dsc.get("source")
895         version = self.pkg.dsc.get("version")
896
897         # Ensure version is sane
898         q = session.query(SrcAssociation)
899         q = q.join(DBSource).filter(DBSource.source==source)
900
901         self.cross_suite_version_check([ (x.suite.suite_name, x.source.version) for x in q.all() ],
902                                        file, version, sourceful=True)
903
904         return self.reject_message
905
906     ################################################################################
907     def check_dsc_against_db(self, file):
908         """
909
910         @warning: NB: this function can remove entries from the 'files' index [if
911          the .orig.tar.gz is a duplicate of the one in the archive]; if
912          you're iterating over 'files' and call this function as part of
913          the loop, be sure to add a check to the top of the loop to
914          ensure you haven't just tried to dereference the deleted entry.
915
916         """
917         self.reject_message = ""
918         self.pkg.orig_tar_gz = None
919
920         # Try and find all files mentioned in the .dsc.  This has
921         # to work harder to cope with the multiple possible
922         # locations of an .orig.tar.gz.
923         # The ordering on the select is needed to pick the newest orig
924         # when it exists in multiple places.
925         for dsc_name, dsc_entry in self.pkg.dsc_files.items():
926             found = None
927             if self.pkg.files.has_key(dsc_name):
928                 actual_md5 = self.pkg.files[dsc_name]["md5sum"]
929                 actual_size = int(self.pkg.files[dsc_name]["size"])
930                 found = "%s in incoming" % (dsc_name)
931
932                 # Check the file does not already exist in the archive
933                 ql = get_poolfile_like_name(dsc_name)
934
935                 # Strip out anything that isn't '%s' or '/%s$'
936                 for i in ql:
937                     if not i.filename.endswith(dsc_name):
938                         ql.remove(i)
939
940                 # "[dak] has not broken them.  [dak] has fixed a
941                 # brokenness.  Your crappy hack exploited a bug in
942                 # the old dinstall.
943                 #
944                 # "(Come on!  I thought it was always obvious that
945                 # one just doesn't release different files with
946                 # the same name and version.)"
947                 #                        -- ajk@ on d-devel@l.d.o
948
949                 if len(ql) > 0:
950                     # Ignore exact matches for .orig.tar.gz
951                     match = 0
952                     if dsc_name.endswith(".orig.tar.gz"):
953                         for i in ql:
954                             if self.pkg.files.has_key(dsc_name) and \
955                                int(self.pkg.files[dsc_name]["size"]) == int(i.filesize) and \
956                                self.pkg.files[dsc_name]["md5sum"] == i.md5sum:
957                                 self.reject("ignoring %s, since it's already in the archive." % (dsc_name), "Warning: ")
958                                 # TODO: Don't delete the entry, just mark it as not needed
959                                 # This would fix the stupidity of changing something we often iterate over
960                                 # whilst we're doing it
961                                 del files[dsc_name]
962                                 self.pkg.orig_tar_gz = os.path.join(i.location.path, i.filename)
963                                 match = 1
964
965                     if not match:
966                         self.reject("can not overwrite existing copy of '%s' already in the archive." % (dsc_name))
967
968             elif dsc_name.endswith(".orig.tar.gz"):
969                 # Check in the pool
970                 ql = get_poolfile_like_name(dsc_name)
971
972                 # Strip out anything that isn't '%s' or '/%s$'
973                 # TODO: Shouldn't we just search for things which end with our string explicitly in the SQL?
974                 for i in ql:
975                     if not i.filename.endswith(dsc_name):
976                         ql.remove(i)
977
978                 if len(ql) > 0:
979                     # Unfortunately, we may get more than one match here if,
980                     # for example, the package was in potato but had an -sa
981                     # upload in woody.  So we need to choose the right one.
982
983                     # default to something sane in case we don't match any or have only one
984                     x = ql[0]
985
986                     if len(ql) > 1:
987                         for i in ql:
988                             old_file = os.path.join(i.location.path, i.filename)
989                             old_file_fh = utils.open_file(old_file)
990                             actual_md5 = apt_pkg.md5sum(old_file_fh)
991                             old_file_fh.close()
992                             actual_size = os.stat(old_file)[stat.ST_SIZE]
993                             if actual_md5 == dsc_entry["md5sum"] and actual_size == int(dsc_entry["size"]):
994                                 x = i
995
996                     old_file = os.path.join(i.location.path, i.filename)
997                     old_file_fh = utils.open_file(old_file)
998                     actual_md5 = apt_pkg.md5sum(old_file_fh)
999                     old_file_fh.close()
1000                     actual_size = os.stat(old_file)[stat.ST_SIZE]
1001                     found = old_file
1002                     suite_type = f.location.archive_type
1003                     # need this for updating dsc_files in install()
1004                     dsc_entry["files id"] = f.file_id
1005                     # See install() in process-accepted...
1006                     self.pkg.orig_tar_id = f.file_id
1007                     self.pkg.orig_tar_gz = old_file
1008                     self.pkg.orig_tar_location = f.location.location_id
1009                 else:
1010                     # TODO: Record the queues and info in the DB so we don't hardcode all this crap
1011                     # Not there? Check the queue directories...
1012                     for directory in [ "Accepted", "New", "Byhand", "ProposedUpdates", "OldProposedUpdates", "Embargoed", "Unembargoed" ]:
1013                         in_otherdir = os.path.join(self.Cnf["Dir::Queue::%s" % (directory)], dsc_name)
1014                         if os.path.exists(in_otherdir):
1015                             in_otherdir_fh = utils.open_file(in_otherdir)
1016                             actual_md5 = apt_pkg.md5sum(in_otherdir_fh)
1017                             in_otherdir_fh.close()
1018                             actual_size = os.stat(in_otherdir)[stat.ST_SIZE]
1019                             found = in_otherdir
1020                             self.pkg.orig_tar_gz = in_otherdir
1021
1022                     if not found:
1023                         self.reject("%s refers to %s, but I can't find it in the queue or in the pool." % (file, dsc_name))
1024                         self.pkg.orig_tar_gz = -1
1025                         continue
1026             else:
1027                 self.reject("%s refers to %s, but I can't find it in the queue." % (file, dsc_name))
1028                 continue
1029             if actual_md5 != dsc_entry["md5sum"]:
1030                 self.reject("md5sum for %s doesn't match %s." % (found, file))
1031             if actual_size != int(dsc_entry["size"]):
1032                 self.reject("size for %s doesn't match %s." % (found, file))
1033
1034         return (self.reject_message, None)