queue.py 41.4 KB
Newer Older
J
New.  
James Troup 已提交
1
#!/usr/bin/env python
2
# vim:set et sw=4:
J
New.  
James Troup 已提交
3

J
Joerg Jaspert 已提交
4 5 6 7 8 9 10 11
"""
Queue utility functions for dak

@contact: Debian FTP Master <ftpmaster@debian.org>
@copyright: 2001 - 2006 James Troup <james@nocrew.org>
@copyright: 2009  Joerg Jaspert <joerg@debian.org>
@license: GNU General Public License version 2 or later
"""
J
New.  
James Troup 已提交
12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28

# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.

# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.

# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA

###############################################################################

J
Joerg Jaspert 已提交
29 30 31 32 33 34 35 36 37 38
import cPickle
import errno
import os
import pg
import stat
import sys
import time
import apt_inst
import apt_pkg
import utils
39
from types import *
40

J
Joerg Jaspert 已提交
41
from dak_exceptions import *
42
from changes import *
M
Mark Hymers 已提交
43
from regexes import re_default_answer, re_fdnic, re_bin_only_nmu
44
from config import Config
45
from dbconn import *
46
from summarystats import SummaryStats
J
New.  
James Troup 已提交
47 48

###############################################################################
49

50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80
def get_type(f, session=None):
    """
    Get the file type of C{f}

    @type f: dict
    @param f: file entry from Changes object

    @rtype: string
    @return: filetype

    """
    if session is None:
        session = DBConn().session()

    # Determine the type
    if f.has_key("dbtype"):
        file_type = file["dbtype"]
    elif f["type"] in [ "orig.tar.gz", "orig.tar.bz2", "tar.gz", "tar.bz2", "diff.gz", "diff.bz2", "dsc" ]:
        file_type = "dsc"
    else:
        utils.fubar("invalid type (%s) for new.  Dazed, confused and sure as heck not continuing." % (file_type))

    # Validate the override type
    type_id = get_override_type(file_type, session)
    if type_id is None:
        utils.fubar("invalid type (%s) for new.  Say wha?" % (file_type))

    return file_type

################################################################################

81 82
# Determine what parts in a .changes are NEW

83
def determine_new(changes, files, warn=1):
J
Joerg Jaspert 已提交
84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99
    """
    Determine what parts in a C{changes} file are NEW.

    @type changes: Upload.Pkg.changes dict
    @param changes: Changes dictionary

    @type files: Upload.Pkg.files dict
    @param files: Files dictionary

    @type warn: bool
    @param warn: Warn if overrides are added for (old)stable

    @rtype: dict
    @return: dictionary of NEW components.

    """
100 101
    new = {}

102 103
    session = DBConn().session()

104
    # Build up a list of potentially new things
105
    for name, f in files.items():
106 107 108 109 110 111
        # Skip byhand elements
        if f["type"] == "byhand":
            continue
        pkg = f["package"]
        priority = f["priority"]
        section = f["section"]
J
Joerg Jaspert 已提交
112
        file_type = get_type(f)
113 114
        component = f["component"]

J
Joerg Jaspert 已提交
115
        if file_type == "dsc":
116
            priority = "source"
117

118 119 120 121
        if not new.has_key(pkg):
            new[pkg] = {}
            new[pkg]["priority"] = priority
            new[pkg]["section"] = section
J
Joerg Jaspert 已提交
122
            new[pkg]["type"] = file_type
123 124 125 126
            new[pkg]["component"] = component
            new[pkg]["files"] = []
        else:
            old_type = new[pkg]["type"]
J
Joerg Jaspert 已提交
127
            if old_type != file_type:
128 129 130 131
                # source gets trumped by deb or udeb
                if old_type == "dsc":
                    new[pkg]["priority"] = priority
                    new[pkg]["section"] = section
J
Joerg Jaspert 已提交
132
                    new[pkg]["type"] = file_type
133
                    new[pkg]["component"] = component
134 135 136

        new[pkg]["files"].append(name)

137 138 139 140 141
        if f.has_key("othercomponents"):
            new[pkg]["othercomponents"] = f["othercomponents"]

    for suite in changes["suite"].keys():
        for pkg in new.keys():
142 143
            ql = get_override(pkg, suite, new[pkg]["component"], new[pkg]["type"], session)
            if len(ql) > 0:
J
Joerg Jaspert 已提交
144 145 146
                for file_entry in new[pkg]["files"]:
                    if files[file_entry].has_key("new"):
                        del files[file_entry]["new"]
147 148 149
                del new[pkg]

    if warn:
150 151 152
        for s in ['stable', 'oldstable']:
            if changes["suite"].has_key(s):
                print "WARNING: overrides will be added for %s!" % s
153 154 155 156 157 158 159 160 161
        for pkg in new.keys():
            if new[pkg].has_key("othercomponents"):
                print "WARNING: %s already present in %s distribution." % (pkg, new[pkg]["othercomponents"])

    return new

################################################################################

def check_valid(new):
J
Joerg Jaspert 已提交
162 163 164 165 166 167 168 169 170 171 172
    """
    Check if section and priority for NEW packages exist in database.
    Additionally does sanity checks:
      - debian-installer packages have to be udeb (or source)
      - non debian-installer packages can not be udeb
      - source priority can only be assigned to dsc file types

    @type new: dict
    @param new: Dict of new packages with their section, priority and type.

    """
173
    for pkg in new.keys():
174 175
        section_name = new[pkg]["section"]
        priority_name = new[pkg]["priority"]
J
Joerg Jaspert 已提交
176
        file_type = new[pkg]["type"]
177 178 179 180 181 182 183 184 185 186 187 188 189

        section = get_section(section_name)
        if section is None:
            new[pkg]["section id"] = -1
        else:
            new[pkg]["section id"] = section.section_id

        priority = get_priority(priority_name)
        if priority is None:
            new[pkg]["priority id"] = -1
        else:
            new[pkg]["priority id"] = priority.priority_id

190
        # Sanity checks
191 192 193 194 195
        di = section_name.find("debian-installer") != -1

        # If d-i, we must be udeb and vice-versa
        if     (di and file_type not in ("udeb", "dsc")) or \
           (not di and file_type == "udeb"):
196
            new[pkg]["section id"] = -1
197 198

        # If dsc we need to be source and vice-versa
J
Joerg Jaspert 已提交
199 200
        if (priority == "source" and file_type != "dsc") or \
           (priority != "source" and file_type == "dsc"):
201 202
            new[pkg]["priority id"] = -1

J
New.  
James Troup 已提交
203 204
###############################################################################

205
class Upload(object):
J
Joerg Jaspert 已提交
206 207
    """
    Everything that has to do with an upload processed.
J
New.  
James Troup 已提交
208

J
Joerg Jaspert 已提交
209
    """
210 211 212
    def __init__(self):
        self.pkg = Changes()
        self.reset()
J
New.  
James Troup 已提交
213 214 215

    ###########################################################################

216 217
    def reset (self):
        """ Reset a number of internal variables."""
218

219 220 221 222 223 224 225
       # Initialize the substitution template map
        cnf = Config()
        self.Subst = {}
        self.Subst["__ADMIN_ADDRESS__"] = cnf["Dinstall::MyAdminAddress"]
        self.Subst["__BUG_SERVER__"] = cnf["Dinstall::BugServer"]
        self.Subst["__DISTRO__"] = cnf["Dinstall::MyDistribution"]
        self.Subst["__DAK_ADDRESS__"] = cnf["Dinstall::MyEmailAddress"]
226

227
        self.reject_message = ""
228
        self.pkg.reset()
J
New.  
James Troup 已提交
229 230

    ###########################################################################
231
    def update_subst(self, reject_message = ""):
J
Joerg Jaspert 已提交
232 233
        """ Set up the per-package template substitution mappings """

234 235
        cnf = Config()

236
        # If 'dak process-unchecked' crashed out in the right place, architecture may still be a string.
237 238 239 240
        if not self.pkg.changes.has_key("architecture") or not \
           isinstance(changes["architecture"], DictType):
            self.pkg.changes["architecture"] = { "Unknown" : "" }

241
        # and maintainer2047 may not exist.
242 243
        if not self.pkg.changes.has_key("maintainer2047"):
            self.pkg.changes["maintainer2047"] = cnf["Dinstall::MyEmailAddress"]
J
New.  
James Troup 已提交
244

245 246 247
        self.Subst["__ARCHITECTURE__"] = " ".join(self.pkg.changes["architecture"].keys())
        self.Subst["__CHANGES_FILENAME__"] = os.path.basename(self.pkg.changes_file)
        self.Subst["__FILE_CONTENTS__"] = self.pkg.changes.get("filecontents", "")
J
New.  
James Troup 已提交
248 249

        # For source uploads the Changed-By field wins; otherwise Maintainer wins.
250 251 252 253 254 255 256
        if self.pkg.changes["architecture"].has_key("source") and \
           self.pkg.changes["changedby822"] != "" and \
           (self.pkg.changes["changedby822"] != self.pkg.changes["maintainer822"]):

            self.Subst["__MAINTAINER_FROM__"] = self.pkg.changes["changedby2047"]
            self.Subst["__MAINTAINER_TO__"] = "%s, %s" % (self.pkg.changes["changedby2047"], changes["maintainer2047"])
            self.Subst["__MAINTAINER__"] = self.pkg.changes.get("changed-by", "Unknown")
J
New.  
James Troup 已提交
257
        else:
258 259 260
            self.Subst["__MAINTAINER_FROM__"] = self.pkg.changes["maintainer2047"]
            self.Subst["__MAINTAINER_TO__"] = self.pkg.changes["maintainer2047"]
            self.Subst["__MAINTAINER__"] = self.pkg.changes.get("maintainer", "Unknown")
J
Joerg Jaspert 已提交
261

262 263
        if "sponsoremail" in self.pkg.changes:
            self.Subst["__MAINTAINER_TO__"] += ", %s" % self.pkg.changes["sponsoremail"]
J
Joerg Jaspert 已提交
264

265 266
        if cnf.has_key("Dinstall::TrackingServer") and self.pkg.changes.has_key("source"):
            self.Subst["__MAINTAINER_TO__"] += "\nBcc: %s@%s" % (self.pkg.changes["source"], cnf["Dinstall::TrackingServer"])
J
New.  
James Troup 已提交
267

268
        # Apply any global override of the Maintainer field
269 270 271
        if cnf.get("Dinstall::OverrideMaintainer"):
            self.Subst["__MAINTAINER_TO__"] = cnf["Dinstall::OverrideMaintainer"]
            self.Subst["__MAINTAINER_FROM__"] = cnf["Dinstall::OverrideMaintainer"]
272

273 274 275
        self.Subst["__REJECT_MESSAGE__"] = self.reject_message
        self.Subst["__SOURCE__"] = self.pkg.changes.get("source", "Unknown")
        self.Subst["__VERSION__"] = self.pkg.changes.get("version", "Unknown")
J
New.  
James Troup 已提交
276 277 278 279

    ###########################################################################

    def build_summaries(self):
J
Joerg Jaspert 已提交
280
        """ Build a summary of changes the upload introduces. """
281 282

        (byhand, new, summary, override_summary) = self.pkg.file_summary()
J
New.  
James Troup 已提交
283

284
        short_summary = summary
J
New.  
James Troup 已提交
285 286

        # This is for direport's benefit...
287
        f = re_fdnic.sub("\n .\n", self.pkg.changes.get("changes", ""))
J
New.  
James Troup 已提交
288 289

        if byhand or new:
290
            summary += "Changes: " + f
J
New.  
James Troup 已提交
291

292 293
        summary += "\n\nOverride entries for your package:\n" + override_summary + "\n"

294
        summary += self.announce(short_summary, 0)
J
New.  
James Troup 已提交
295

296
        return (summary, short_summary)
J
New.  
James Troup 已提交
297 298 299

    ###########################################################################

300
    def close_bugs(self, summary, action):
J
Joerg Jaspert 已提交
301 302 303 304 305 306 307 308 309 310 311 312 313 314
        """
        Send mail to close bugs as instructed by the closes field in the changes file.
        Also add a line to summary if any work was done.

        @type summary: string
        @param summary: summary text, as given by L{build_summaries}

        @type action: bool
        @param action: Set to false no real action will be done.

        @rtype: string
        @return: summary. If action was taken, extended by the list of closed bugs.

        """
J
New.  
James Troup 已提交
315

316 317 318
        template = os.path.join(Config()["Dir::Templates"], 'process-unchecked.bug-close')

        bugs = self.pkg.changes["closes"].keys()
J
New.  
James Troup 已提交
319

320
        if not bugs:
321
            return summary
J
New.  
James Troup 已提交
322

323
        bugs.sort()
324 325 326 327
        summary += "Closing bugs: "
        for bug in bugs:
            summary += "%s " % (bug)
            if action:
328 329 330
                self.Subst["__BUG_NUMBER__"] = bug
                if self.pkg.changes["distribution"].has_key("stable"):
                    self.Subst["__STABLE_WARNING__"] = """
331 332 333 334
Note that this package is not part of the released stable Debian
distribution.  It may have dependencies on other unreleased software,
or other instabilities.  Please take care if you wish to install it.
The update will eventually make its way into the next released Debian
335
distribution."""
336
                else:
337 338 339 340 341 342 343 344
                    self.Subst["__STABLE_WARNING__"] = ""
                    mail_message = utils.TemplateSubst(self.Subst, template)
                    utils.send_mail(mail_message)

                # Clear up after ourselves
                del self.Subst["__BUG_NUMBER__"]
                del self.Subst["__STABLE_WARNING__"]

345
        if action:
346 347
            self.Logger.log(["closing bugs"] + bugs)

348
        summary += "\n"
349

350
        return summary
351 352 353

    ###########################################################################

354
    def announce(self, short_summary, action):
J
Joerg Jaspert 已提交
355 356 357 358 359 360 361 362 363 364 365 366 367
        """
        Send an announce mail about a new upload.

        @type short_summary: string
        @param short_summary: Short summary text to include in the mail

        @type action: bool
        @param action: Set to false no real action will be done.

        @rtype: string
        @return: Textstring about action taken.

        """
368 369 370

        cnf = Config()
        announcetemplate = os.path.join(cnf["Dir::Templates"], 'process-unchecked.announce')
371 372

        # Only do announcements for source uploads with a recent dpkg-dev installed
373 374
        if float(self.pkg.changes.get("format", 0)) < 1.6 or not \
           self.pkg.changes["architecture"].has_key("source"):
375
            return ""
J
New.  
James Troup 已提交
376

377 378
        lists_done = {}
        summary = ""
379

380 381 382
        self.Subst["__SHORT_SUMMARY__"] = short_summary

        for dist in self.pkg.changes["distribution"].keys():
J
Joerg Jaspert 已提交
383 384
            announce_list = Cnf.Find("Suite::%s::Announce" % (dist))
            if announce_list == "" or lists_done.has_key(announce_list):
385
                continue
386

J
Joerg Jaspert 已提交
387 388
            lists_done[announce_list] = 1
            summary += "Announcing to %s\n" % (announce_list)
389 390

            if action:
391 392 393 394 395 396 397 398
                self.Subst["__ANNOUNCE_LIST_ADDRESS__"] = announce_list
                if cnf.get("Dinstall::TrackingServer") and \
                   self.pkg.changes["architecture"].has_key("source"):
                    trackingsendto = "Bcc: %s@%s" % (self.pkg.changes["source"], cnf["Dinstall::TrackingServer"])
                    self.Subst["__ANNOUNCE_LIST_ADDRESS__"] += "\n" + trackingsendto

                mail_message = utils.TemplateSubst(self.Subst, announcetemplate)
                utils.send_mail(mail_message)
399

400 401 402
                del self.Subst["__ANNOUNCE_LIST_ADDRESS__"]

        if cnf.FindB("Dinstall::CloseBugs"):
403
            summary = self.close_bugs(summary, action)
404

405 406
        del self.Subst["__SHORT_SUMMARY__"]

407
        return summary
J
New.  
James Troup 已提交
408 409 410

    ###########################################################################

J
NEW  
Joerg Jaspert 已提交
411
    def accept (self, summary, short_summary, targetdir=None):
J
Joerg Jaspert 已提交
412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428
        """
        Accept an upload.

        This moves all files referenced from the .changes into the I{accepted}
        queue, sends the accepted mail, announces to lists, closes bugs and
        also checks for override disparities. If enabled it will write out
        the version history for the BTS Version Tracking and will finally call
        L{queue_build}.

        @type summary: string
        @param summary: Summary text

        @type short_summary: string
        @param short_summary: Short summary

        """

429 430 431 432
        cnf = Config()
        stats = SummaryStats()

        accepttemplate = os.path.join(cnf["Dir::Templates"], 'process-unchecked.accepted')
J
New.  
James Troup 已提交
433

J
NEW  
Joerg Jaspert 已提交
434
        if targetdir is None:
435
            targetdir = cnf["Dir::Queue::Accepted"]
J
NEW  
Joerg Jaspert 已提交
436

J
New.  
James Troup 已提交
437
        print "Accepting."
438
        self.Logger.log(["Accepting changes", self.pkg.changes_file])
J
New.  
James Troup 已提交
439

440
        self.write_dot_dak(targetdir)
J
New.  
James Troup 已提交
441 442

        # Move all the files into the accepted directory
443 444 445 446 447 448 449
        utils.move(self.pkg.changes_file, targetdir)

        for name, entry in sorted(self.pkg.files.items()):
            utils.move(name, targetdir)
            stats.accept_bytes += float(entry["size"])

        stats.accept_count += 1
J
New.  
James Troup 已提交
450 451 452

        # Send accept mail, announce to lists, close bugs and check for
        # override disparities
453 454 455 456
        if not cnf["Dinstall::Options::No-Mail"]:
            self.Subst["__SUITE__"] = ""
            self.Subst["__SUMMARY__"] = summary
            mail_message = utils.TemplateSubst(self.Subst, accepttemplate)
457
            utils.send_mail(mail_message)
J
New.  
James Troup 已提交
458 459
            self.announce(short_summary, 1)

460
        ## Helper stuff for DebBugs Version Tracking
461
        if cnf.Find("Dir::Queue::BTSVersionTrack"):
462 463 464 465 466
            # ??? once queue/* is cleared on *.d.o and/or reprocessed
            # the conditionalization on dsc["bts changelog"] should be
            # dropped.

            # Write out the version history from the changelog
467 468
            if self.pkg.changes["architecture"].has_key("source") and \
               self.pkg.dsc.has_key("bts changelog"):
469

470
                (fd, temp_filename) = utils.temp_filename(cnf["Dir::Queue::BTSVersionTrack"], prefix=".")
J
fdopen  
Joerg Jaspert 已提交
471
                version_history = os.fdopen(fd, 'w')
472
                version_history.write(self.pkg.dsc["bts changelog"])
473
                version_history.close()
474 475
                filename = "%s/%s" % (cnf["Dir::Queue::BTSVersionTrack"],
                                      self.pkg.changes_file[:-8]+".versions")
476
                os.rename(temp_filename, filename)
J
Joerg Jaspert 已提交
477
                os.chmod(filename, 0644)
478 479

            # Write out the binary -> source mapping.
480
            (fd, temp_filename) = utils.temp_filename(cnf["Dir::Queue::BTSVersionTrack"], prefix=".")
J
fdopen  
Joerg Jaspert 已提交
481
            debinfo = os.fdopen(fd, 'w')
482 483 484 485 486
            for name, entry in sorted(self.pkg.files.items()):
                if entry["type"] == "deb":
                    line = " ".join([entry["package"], entry["version"],
                                     entry["architecture"], entry["source package"],
                                     entry["source version"]])
487 488
                    debinfo.write(line+"\n")
            debinfo.close()
489 490
            filename = "%s/%s" % (cnf["Dir::Queue::BTSVersionTrack"],
                                  self.pkg.changes_file[:-8]+".debinfo")
491
            os.rename(temp_filename, filename)
J
Joerg Jaspert 已提交
492
            os.chmod(filename, 0644)
493

J
NEW  
Joerg Jaspert 已提交
494 495 496 497 498 499 500 501 502 503 504 505
        # Its is Cnf["Dir::Queue::Accepted"] here, not targetdir!
        # <Ganneff> we do call queue_build too
        # <mhy> well yes, we'd have had to if we were inserting into accepted
        # <Ganneff> now. thats database only.
        # <mhy> urgh, that's going to get messy
        # <Ganneff> so i make the p-n call to it *also* using accepted/
        # <mhy> but then the packages will be in the queue_build table without the files being there
        # <Ganneff> as the buildd queue is only regenerated whenever unchecked runs
        # <mhy> ah, good point
        # <Ganneff> so it will work out, as unchecked move it over
        # <mhy> that's all completely sick
        # <Ganneff> yes
506

507 508 509 510
        # This routine returns None on success or an error on failure
        res = get_queue('accepted').autobuild_upload(self.pkg, cnf["Dir::Queue::Accepted"])
        if res:
            utils.fubar(res)
J
Joerg Jaspert 已提交
511

J
New.  
James Troup 已提交
512 513

    def check_override (self):
J
Joerg Jaspert 已提交
514 515 516 517 518 519 520 521
        """
        Checks override entries for validity. Mails "Override disparity" warnings,
        if that feature is enabled.

        Abandons the check if
          - override disparity checks are disabled
          - mail sending is disabled
        """
522 523

        cnf = Config()
J
New.  
James Troup 已提交
524

525
        # Abandon the check if:
526 527 528 529
        #  a) override disparity checks have been disabled
        #  b) we're not sending mail
        if not cnf.FindB("Dinstall::OverrideDisparityCheck") or \
           cnf["Dinstall::Options::No-Mail"]:
530
            return
J
New.  
James Troup 已提交
531

532
        summary = self.pkg.check_override()
J
New.  
James Troup 已提交
533 534

        if summary == "":
535
            return
J
New.  
James Troup 已提交
536

537 538 539 540
        overridetemplate = os.path.join(cnf["Dir::Templates"], 'process-unchecked.override-disparity')

        self.Subst["__SUMMARY__"] = summary
        mail_message = utils.TemplateSubst(self.Subst, overridetemplate)
541
        utils.send_mail(mail_message)
542
        del self.Subst["__SUMMARY__"]
J
New.  
James Troup 已提交
543 544

    ###########################################################################
545
    def force_reject(self, reject_files):
J
Joerg Jaspert 已提交
546 547 548 549 550 551 552 553 554 555
        """
        Forcefully move files from the current directory to the
        reject directory.  If any file already exists in the reject
        directory it will be moved to the morgue to make way for
        the new file.

        @type files: dict
        @param files: file dictionary

        """
J
New.  
James Troup 已提交
556

557
        cnf = Config()
J
New.  
James Troup 已提交
558

559
        for file_entry in reject_files:
J
New.  
James Troup 已提交
560
            # Skip any files which don't exist or which we don't have permission to copy.
561
            if os.access(file_entry, os.R_OK) == 0:
562
                continue
563 564 565

            dest_file = os.path.join(cnf["Dir::Queue::Reject"], file_entry)

J
New.  
James Troup 已提交
566
            try:
567
                dest_fd = os.open(dest_file, os.O_RDWR | os.O_CREAT | os.O_EXCL, 0644)
J
New.  
James Troup 已提交
568 569
            except OSError, e:
                # File exists?  Let's try and move it to the morgue
570 571
                if e.errno == errno.EEXIST:
                    morgue_file = os.path.join(cnf["Dir::Morgue"], cnf["Dir::MorgueReject"], file_entry)
J
New.  
James Troup 已提交
572
                    try:
573
                        morgue_file = utils.find_next_free(morgue_file)
J
Joerg Jaspert 已提交
574
                    except NoFreeFilenameError:
J
New.  
James Troup 已提交
575 576
                        # Something's either gone badly Pete Tong, or
                        # someone is trying to exploit us.
J
Joerg Jaspert 已提交
577
                        utils.warn("**WARNING** failed to move %s from the reject directory to the morgue." % (file_entry))
578 579
                        return
                    utils.move(dest_file, morgue_file, perms=0660)
J
New.  
James Troup 已提交
580
                    try:
581
                        dest_fd = os.open(dest_file, os.O_RDWR|os.O_CREAT|os.O_EXCL, 0644)
J
New.  
James Troup 已提交
582 583
                    except OSError, e:
                        # Likewise
J
Joerg Jaspert 已提交
584
                        utils.warn("**WARNING** failed to claim %s in the reject directory." % (file_entry))
585
                        return
J
New.  
James Troup 已提交
586
                else:
587
                    raise
J
New.  
James Troup 已提交
588 589
            # If we got here, we own the destination file, so we can
            # safely overwrite it.
J
Joerg Jaspert 已提交
590
            utils.move(file_entry, dest_file, 1, perms=0660)
591
            os.close(dest_fd)
592

J
New.  
James Troup 已提交
593
    ###########################################################################
594
    def do_reject (self, manual=0, reject_message="", note=""):
J
Joerg Jaspert 已提交
595 596 597 598 599 600 601 602 603 604 605 606 607
        """
        Reject an upload. If called without a reject message or C{manual} is
        true, spawn an editor so the user can write one.

        @type manual: bool
        @param manual: manual or automated rejection

        @type reject_message: string
        @param reject_message: A reject message

        @return: 0

        """
J
James Troup 已提交
608 609 610
        # If we weren't given a manual rejection message, spawn an
        # editor so the user can add one in...
        if manual and not reject_message:
J
various  
Joerg Jaspert 已提交
611
            (fd, temp_filename) = utils.temp_filename()
J
Joerg Jaspert 已提交
612 613 614 615 616
            temp_file = os.fdopen(fd, 'w')
            if len(note) > 0:
                for line in note:
                    temp_file.write(line)
            temp_file.close()
J
James Troup 已提交
617
            editor = os.environ.get("EDITOR","vi")
618
            answer = 'E'
J
James Troup 已提交
619 620
            while answer == 'E':
                os.system("%s %s" % (editor, temp_filename))
621 622 623 624 625
                temp_fh = utils.open_file(temp_filename)
                reject_message = "".join(temp_fh.readlines())
                temp_fh.close()
                print "Reject message:"
                print utils.prefix_multi_line_string(reject_message,"  ",include_blank_lines=1)
J
James Troup 已提交
626
                prompt = "[R]eject, Edit, Abandon, Quit ?"
627
                answer = "XXX"
628
                while prompt.find(answer) == -1:
629 630
                    answer = utils.our_raw_input(prompt)
                    m = re_default_answer.search(prompt)
J
James Troup 已提交
631
                    if answer == "":
632 633 634
                        answer = m.group(1)
                    answer = answer[:1].upper()
            os.unlink(temp_filename)
J
James Troup 已提交
635
            if answer == 'A':
636
                return 1
J
James Troup 已提交
637
            elif answer == 'Q':
638
                sys.exit(0)
J
James Troup 已提交
639

J
New.  
James Troup 已提交
640 641
        print "Rejecting.\n"

642
        cnf = Config()
J
New.  
James Troup 已提交
643

644 645
        reason_filename = self.pkg.changes_file[:-8] + ".reason"
        reason_filename = os.path.join(cnf["Dir::Queue::Reject"], reason_filename)
J
New.  
James Troup 已提交
646 647

        # Move all the files into the reject directory
648
        reject_files = self.pkg.files.keys() + [self.pkg.changes_file]
649
        self.force_reject(reject_files)
J
New.  
James Troup 已提交
650 651 652

        # If we fail here someone is probably trying to exploit the race
        # so let's just raise an exception ...
J
James Troup 已提交
653
        if os.path.exists(reason_filename):
654 655
            os.unlink(reason_filename)
        reason_fd = os.open(reason_filename, os.O_RDWR|os.O_CREAT|os.O_EXCL, 0644)
J
New.  
James Troup 已提交
656

657 658
        rej_template = os.path.join(cnf["Dir::Templates"], "queue.rejected")

J
New.  
James Troup 已提交
659
        if not manual:
660 661 662
            self.Subst["__REJECTOR_ADDRESS__"] = cnf["Dinstall::MyEmailAddress"]
            self.Subst["__MANUAL_REJECT_MESSAGE__"] = ""
            self.Subst["__CC__"] = "X-DAK-Rejection: automatic (moo)\nX-Katie-Rejection: automatic (moo)"
663
            os.write(reason_fd, reject_message)
664
            reject_mail_message = utils.TemplateSubst(self.Subst, rej_template)
J
New.  
James Troup 已提交
665 666
        else:
            # Build up the rejection email
667 668 669 670 671
            user_email_address = utils.whoami() + " <%s>" % (cnf["Dinstall::MyAdminAddress"])
            self.Subst["__REJECTOR_ADDRESS__"] = user_email_address
            self.Subst["__MANUAL_REJECT_MESSAGE__"] = reject_message
            self.Subst["__CC__"] = "Cc: " + Cnf["Dinstall::MyEmailAddress"]
            reject_mail_message = utils.TemplateSubst(self.Subst, rej_template)
J
New.  
James Troup 已提交
672
            # Write the rejection email out as the <foo>.reason file
673
            os.write(reason_fd, reject_mail_message)
J
James Troup 已提交
674

675 676 677 678
        del self.Subst["__REJECTOR_ADDRESS__"]
        del self.Subst["__MANUAL_REJECT_MESSAGE__"]
        del self.Subst["__CC__"]

679
        os.close(reason_fd)
J
New.  
James Troup 已提交
680 681

        # Send the rejection mail if appropriate
682
        if not cnf["Dinstall::Options::No-Mail"]:
683
            utils.send_mail(reject_mail_message)
J
New.  
James Troup 已提交
684

685
        self.Logger.log(["rejected", pkg.changes_file])
J
Joerg Jaspert 已提交
686

687
        return 0
J
New.  
James Troup 已提交
688 689

    ################################################################################
690
    def in_override_p(self, package, component, suite, binary_type, file, session=None):
J
Joerg Jaspert 已提交
691 692 693 694 695 696 697
        """
        Check if a package already has override entries in the DB

        @type package: string
        @param package: package name

        @type component: string
698
        @param component: database id of the component
J
Joerg Jaspert 已提交
699 700

        @type suite: int
701
        @param suite: database id of the suite
J
Joerg Jaspert 已提交
702 703 704 705 706 707 708 709 710 711

        @type binary_type: string
        @param binary_type: type of the package

        @type file: string
        @param file: filename we check

        @return: the database result. But noone cares anyway.

        """
712 713 714 715 716

        cnf = Config()

        if session is None:
            session = DBConn().session()
J
New.  
James Troup 已提交
717 718

        if binary_type == "": # must be source
J
Joerg Jaspert 已提交
719
            file_type = "dsc"
J
New.  
James Troup 已提交
720
        else:
J
Joerg Jaspert 已提交
721
            file_type = binary_type
J
New.  
James Troup 已提交
722 723

        # Override suite name; used for example with proposed-updates
724 725 726 727
        if cnf.Find("Suite::%s::OverrideSuite" % (suite)) != "":
            suite = cnf["Suite::%s::OverrideSuite" % (suite)]

        result = get_override(package, suite, component, file_type, session)
J
New.  
James Troup 已提交
728 729

        # If checking for a source package fall back on the binary override type
730 731
        if file_type == "dsc" and len(result) < 1:
            result = get_override(package, suite, component, ['deb', 'udeb'], session)
J
New.  
James Troup 已提交
732 733

        # Remember the section and priority so we can check them later if appropriate
734 735 736 737 738
        if len(result) > 0:
            result = result[0]
            self.pkg.files[file]["override section"] = result.section.section
            self.pkg.files[file]["override priority"] = result.priority.priority
            return result
J
New.  
James Troup 已提交
739

740
        return None
J
New.  
James Troup 已提交
741 742 743

    ################################################################################
    def reject (self, str, prefix="Rejected: "):
J
Joerg Jaspert 已提交
744 745 746 747 748 749 750 751 752 753
        """
        Add C{str} to reject_message. Adds C{prefix}, by default "Rejected: "

        @type str: string
        @param str: Reject text

        @type prefix: string
        @param prefix: Prefix text, default Rejected:

        """
J
New.  
James Troup 已提交
754
        if str:
755 756 757
            # Unlike other rejects we add new lines first to avoid trailing
            # new lines when this message is passed back up to a caller.
            if self.reject_message:
758 759
                self.reject_message += "\n"
            self.reject_message += prefix + str
J
New.  
James Troup 已提交
760

761
    ################################################################################
762 763 764 765 766 767 768
    def get_anyversion(self, sv_list, suite):
        """
        @type sv_list: list
        @param sv_list: list of (suite, version) tuples to check

        @type suite: string
        @param suite: suite name
769

770 771 772
        Description: TODO
        """
        anyversion = None
773
        anysuite = [suite] + self.Cnf.ValueList("Suite::%s::VersionChecks::Enhances" % (suite))
774
        for (s, v) in sv_list:
775
            if s in [ x.lower() for x in anysuite ]:
776
                if not anyversion or apt_pkg.VersionCompare(anyversion, v) <= 0:
777 778
                    anyversion = v

779 780 781 782
        return anyversion

    ################################################################################

783
    def cross_suite_version_check(self, sv_list, file, new_version, sourceful=False):
J
Joerg Jaspert 已提交
784
        """
785 786 787 788 789 790 791 792 793
        @type sv_list: list
        @param sv_list: list of (suite, version) tuples to check

        @type file: string
        @param file: XXX

        @type new_version: string
        @param new_version: XXX

J
Joerg Jaspert 已提交
794
        Ensure versions are newer than existing packages in target
795
        suites and that cross-suite version checking rules as
J
Joerg Jaspert 已提交
796 797
        set out in the conf file are satisfied.
        """
798

799 800
        cnf = Config()

801 802
        # Check versions for each target suite
        for target_suite in self.pkg.changes["distribution"].keys():
803 804 805
            must_be_newer_than = [ i.lower() for i in cnf.ValueList("Suite::%s::VersionChecks::MustBeNewerThan" % (target_suite)) ]
            must_be_older_than = [ i.lower() for i in cnf.ValueList("Suite::%s::VersionChecks::MustBeOlderThan" % (target_suite)) ]

806 807
            # Enforce "must be newer than target suite" even if conffile omits it
            if target_suite not in must_be_newer_than:
808
                must_be_newer_than.append(target_suite)
809 810 811 812 813

            for (suite, existent_version) in sv_list:
                vercmp = apt_pkg.VersionCompare(new_version, existent_version)

                if suite in must_be_newer_than and sourceful and vercmp < 1:
814
                    self.reject("%s: old version (%s) in %s >= new version (%s) targeted at %s." % (file, existent_version, suite, new_version, target_suite))
815 816

                if suite in must_be_older_than and vercmp > -1:
817
                    cansave = 0
818

819 820 821 822 823 824
                    if self.pkg.changes.get('distribution-version', {}).has_key(suite):
                        # we really use the other suite, ignoring the conflicting one ...
                        addsuite = self.pkg.changes["distribution-version"][suite]

                        add_version = self.get_anyversion(sv_list, addsuite)
                        target_version = self.get_anyversion(sv_list, target_suite)
825

826 827 828 829 830 831 832 833 834 835 836
                        if not add_version:
                            # not add_version can only happen if we map to a suite
                            # that doesn't enhance the suite we're propup'ing from.
                            # so "propup-ver x a b c; map a d" is a problem only if
                            # d doesn't enhance a.
                            #
                            # i think we could always propagate in this case, rather
                            # than complaining. either way, this isn't a REJECT issue
                            #
                            # And - we really should complain to the dorks who configured dak
                            self.reject("%s is mapped to, but not enhanced by %s - adding anyways" % (suite, addsuite), "Warning: ")
837 838
                            self.pkg.changes.setdefault("propdistribution", {})
                            self.pkg.changes["propdistribution"][addsuite] = 1
839 840 841 842 843 844 845 846
                            cansave = 1
                        elif not target_version:
                            # not targets_version is true when the package is NEW
                            # we could just stick with the "...old version..." REJECT
                            # for this, I think.
                            self.reject("Won't propogate NEW packages.")
                        elif apt_pkg.VersionCompare(new_version, add_version) < 0:
                            # propogation would be redundant. no need to reject though.
847
                            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: ")
848 849
                            cansave = 1
                        elif apt_pkg.VersionCompare(new_version, add_version) > 0 and \
850
                             apt_pkg.VersionCompare(add_version, target_version) >= 0:
851
                            # propogate!!
852 853 854
                            self.reject("Propogating upload to %s" % (addsuite), "Warning: ")
                            self.pkg.changes.setdefault("propdistribution", {})
                            self.pkg.changes["propdistribution"][addsuite] = 1
855
                            cansave = 1
856

857
                    if not cansave:
858
                        self.reject("%s: old version (%s) in %s <= new version (%s) targeted at %s." % (file, existent_version, suite, new_version, target_suite))
859 860 861

    ################################################################################

862
    def check_binary_against_db(self, file, session=None):
J
Joerg Jaspert 已提交
863 864 865
        """

        """
866 867 868 869

        if session is None:
            session = DBConn().session()

870
        self.reject_message = ""
J
New.  
James Troup 已提交
871

872
        # Ensure version is sane
873 874 875 876 877 878
        q = session.query(BinAssociation)
        q = q.join(DBBinary).filter(DBBinary.package==self.pkg.files[file]["package"])
        q = q.join(Architecture).filter(Architecture.arch_string.in_([self.pkg.files[file]["architecture"], 'all']))

        self.cross_suite_version_check([ (x.suite.suite_name, x.binary.version) for x in q.all() ],
                                       file, files[file]["version"], sourceful=False)
879

J
New.  
James Troup 已提交
880
        # Check for any existing copies of the file
881 882 883 884 885
        q = session.query(DBBinary).filter_by(files[file]["package"])
        q = q.filter_by(version=files[file]["version"])
        q = q.join(Architecture).filter_by(arch_string=files[file]["architecture"])

        if q.count() > 0:
886
            self.reject("%s: can not overwrite existing copy already in the archive." % (file))
J
New.  
James Troup 已提交
887

888
        return self.reject_message
J
New.  
James Troup 已提交
889 890 891

    ################################################################################

892
    def check_source_against_db(self, file, session=None):
J
Joerg Jaspert 已提交
893 894
        """
        """
895 896 897
        if session is None:
            session = DBConn().session()

898
        self.reject_message = ""
899 900
        source = self.pkg.dsc.get("source")
        version = self.pkg.dsc.get("version")
J
New.  
James Troup 已提交
901

902
        # Ensure version is sane
903 904 905 906 907
        q = session.query(SrcAssociation)
        q = q.join(DBSource).filter(DBSource.source==source)

        self.cross_suite_version_check([ (x.suite.suite_name, x.source.version) for x in q.all() ],
                                       file, version, sourceful=True)
908

909
        return self.reject_message
J
New.  
James Troup 已提交
910 911 912

    ################################################################################
    def check_dsc_against_db(self, file):
J
Joerg Jaspert 已提交
913 914 915 916 917 918 919 920 921
        """

        @warning: NB: this function can remove entries from the 'files' index [if
         the .orig.tar.gz is a duplicate of the one in the archive]; if
         you're iterating over 'files' and call this function as part of
         the loop, be sure to add a check to the top of the loop to
         ensure you haven't just tried to dereference the deleted entry.

        """
922 923
        self.reject_message = ""
        self.pkg.orig_tar_gz = None
J
New.  
James Troup 已提交
924 925 926 927

        # Try and find all files mentioned in the .dsc.  This has
        # to work harder to cope with the multiple possible
        # locations of an .orig.tar.gz.
928 929
        # The ordering on the select is needed to pick the newest orig
        # when it exists in multiple places.
930
        for dsc_name, dsc_entry in self.pkg.dsc_files.items():
931
            found = None
932 933 934 935 936
            if self.pkg.files.has_key(dsc_name):
                actual_md5 = self.pkg.files[dsc_name]["md5sum"]
                actual_size = int(self.pkg.files[dsc_name]["size"])
                found = "%s in incoming" % (dsc_name)

J
New.  
James Troup 已提交
937
                # Check the file does not already exist in the archive
938 939
                ql = get_poolfile_like_name(dsc_name)

940 941
                # Strip out anything that isn't '%s' or '/%s$'
                for i in ql:
942
                    if not i.filename.endswith(dsc_name):
943
                        ql.remove(i)
944

945
                # "[dak] has not broken them.  [dak] has fixed a
J
New.  
James Troup 已提交
946 947 948 949 950 951 952 953
                # brokenness.  Your crappy hack exploited a bug in
                # the old dinstall.
                #
                # "(Come on!  I thought it was always obvious that
                # one just doesn't release different files with
                # the same name and version.)"
                #                        -- ajk@ on d-devel@l.d.o

954
                if len(ql) > 0:
955
                    # Ignore exact matches for .orig.tar.gz
956
                    match = 0
957
                    if dsc_name.endswith(".orig.tar.gz"):
958
                        for i in ql:
959 960 961 962 963 964 965 966 967
                            if self.pkg.files.has_key(dsc_name) and \
                               int(self.pkg.files[dsc_name]["size"]) == int(i.filesize) and \
                               self.pkg.files[dsc_name]["md5sum"] == i.md5sum:
                                self.reject("ignoring %s, since it's already in the archive." % (dsc_name), "Warning: ")
                                # TODO: Don't delete the entry, just mark it as not needed
                                # This would fix the stupidity of changing something we often iterate over
                                # whilst we're doing it
                                del files[dsc_name]
                                self.pkg.orig_tar_gz = os.path.join(i.location.path, i.filename)
968
                                match = 1
969 970

                    if not match:
971 972 973
                        self.reject("can not overwrite existing copy of '%s' already in the archive." % (dsc_name))

            elif dsc_name.endswith(".orig.tar.gz"):
J
New.  
James Troup 已提交
974
                # Check in the pool
975 976
                ql = get_poolfile_like_name(dsc_name)

977
                # Strip out anything that isn't '%s' or '/%s$'
978
                # TODO: Shouldn't we just search for things which end with our string explicitly in the SQL?
979
                for i in ql:
980
                    if not i.filename.endswith(dsc_name):
981
                        ql.remove(i)
J
New.  
James Troup 已提交
982

983
                if len(ql) > 0:
984 985 986
                    # Unfortunately, we may get more than one match here if,
                    # for example, the package was in potato but had an -sa
                    # upload in woody.  So we need to choose the right one.
J
New.  
James Troup 已提交
987

J
Various  
Joerg Jaspert 已提交
988 989
                    # default to something sane in case we don't match any or have only one
                    x = ql[0]
J
New.  
James Troup 已提交
990 991 992

                    if len(ql) > 1:
                        for i in ql:
993
                            old_file = os.path.join(i.location.path, i.filename)
994
                            old_file_fh = utils.open_file(old_file)
995
                            actual_md5 = apt_pkg.md5sum(old_file_fh)
996
                            old_file_fh.close()
997
                            actual_size = os.stat(old_file)[stat.ST_SIZE]
998
                            if actual_md5 == dsc_entry["md5sum"] and actual_size == int(dsc_entry["size"]):
999
                                x = i
J
New.  
James Troup 已提交
1000

1001
                    old_file = os.path.join(i.location.path, i.filename)
1002
                    old_file_fh = utils.open_file(old_file)
1003
                    actual_md5 = apt_pkg.md5sum(old_file_fh)
1004
                    old_file_fh.close()
1005 1006
                    actual_size = os.stat(old_file)[stat.ST_SIZE]
                    found = old_file
1007
                    suite_type = f.location.archive_type
J
Various  
Joerg Jaspert 已提交
1008
                    # need this for updating dsc_files in install()
1009
                    dsc_entry["files id"] = f.file_id
1010
                    # See install() in process-accepted...
1011
                    self.pkg.orig_tar_id = f.file_id
1012
                    self.pkg.orig_tar_gz = old_file
1013
                    self.pkg.orig_tar_location = f.location.location_id
J
New.  
James Troup 已提交
1014
                else:
1015
                    # TODO: Record the queues and info in the DB so we don't hardcode all this crap
1016
                    # Not there? Check the queue directories...
1017 1018 1019 1020 1021 1022 1023 1024 1025
                    for directory in [ "Accepted", "New", "Byhand", "ProposedUpdates", "OldProposedUpdates", "Embargoed", "Unembargoed" ]:
                        in_otherdir = os.path.join(self.Cnf["Dir::Queue::%s" % (directory)], dsc_name)
                        if os.path.exists(in_otherdir):
                            in_otherdir_fh = utils.open_file(in_otherdir)
                            actual_md5 = apt_pkg.md5sum(in_otherdir_fh)
                            in_otherdir_fh.close()
                            actual_size = os.stat(in_otherdir)[stat.ST_SIZE]
                            found = in_otherdir
                            self.pkg.orig_tar_gz = in_otherdir
1026 1027

                    if not found:
1028
                        self.reject("%s refers to %s, but I can't find it in the queue or in the pool." % (file, dsc_name))
1029 1030
                        self.pkg.orig_tar_gz = -1
                        continue
J
New.  
James Troup 已提交
1031
            else:
1032
                self.reject("%s refers to %s, but I can't find it in the queue." % (file, dsc_name))
1033
                continue
1034
            if actual_md5 != dsc_entry["md5sum"]:
1035
                self.reject("md5sum for %s doesn't match %s." % (found, file))
1036
            if actual_size != int(dsc_entry["size"]):
1037
                self.reject("size for %s doesn't match %s." % (found, file))
J
New.  
James Troup 已提交
1038

1039
        return (self.reject_message, None)