queue.py 44.9 KB
Newer Older
J
New.  
James Troup 已提交
1 2
#!/usr/bin/env python

3 4
# Queue utility functions for dak
# Copyright (C) 2001, 2002, 2003, 2004, 2005, 2006  James Troup <james@nocrew.org>
J
New.  
James Troup 已提交
5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21

# 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

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

22
import cPickle, errno, os, pg, re, stat, sys, time
23
import apt_inst, apt_pkg
24
import utils, database
J
New.  
James Troup 已提交
25

26
from types import *
J
New.  
James Troup 已提交
27 28 29

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

30 31 32 33
re_isanum = re.compile (r"^\d+$")
re_default_answer = re.compile(r"\[(.*)\]")
re_fdnic = re.compile(r"\n\n")
re_bin_only_nmu = re.compile(r"\+b\d+$")
J
James Troup 已提交
34

J
New.  
James Troup 已提交
35 36 37 38 39 40
###############################################################################

# Convenience wrapper to carry around all the package information in

class Pkg:
    def __init__(self, **kwds):
41
        self.__dict__.update(kwds)
J
New.  
James Troup 已提交
42 43

    def update(self, **kwds):
44
        self.__dict__.update(kwds)
J
New.  
James Troup 已提交
45 46 47

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

48
class Upload:
J
New.  
James Troup 已提交
49 50

    def __init__(self, Cnf):
51 52 53
        self.Cnf = Cnf
        self.accept_count = 0
        self.accept_bytes = 0L
J
New.  
James Troup 已提交
54
        self.pkg = Pkg(changes = {}, dsc = {}, dsc_files = {}, files = {},
55
                       legacy_source_untouchable = {})
J
New.  
James Troup 已提交
56 57

        # Initialize the substitution template mapping global
58 59 60 61
        Subst = self.Subst = {}
        Subst["__ADMIN_ADDRESS__"] = Cnf["Dinstall::MyAdminAddress"]
        Subst["__BUG_SERVER__"] = Cnf["Dinstall::BugServer"]
        Subst["__DISTRO__"] = Cnf["Dinstall::MyDistribution"]
62
        Subst["__DAK_ADDRESS__"] = Cnf["Dinstall::MyEmailAddress"]
J
New.  
James Troup 已提交
63

64
        self.projectB = pg.connect(Cnf["DB::Name"], Cnf["DB::Host"], int(Cnf["DB::Port"]))
65
        database.init(Cnf, self.projectB)
J
New.  
James Troup 已提交
66 67 68 69 70

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

    def init_vars (self):
        for i in [ "changes", "dsc", "files", "dsc_files", "legacy_source_untouchable" ]:
71 72 73 74
            exec "self.pkg.%s.clear();" % (i)
        self.pkg.orig_tar_id = None
        self.pkg.orig_tar_location = ""
        self.pkg.orig_tar_gz = None
J
New.  
James Troup 已提交
75 76 77 78

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

    def update_vars (self):
79
        dump_filename = self.pkg.changes_file[:-8]+".dak"
80 81
        dump_file = utils.open_file(dump_filename)
        p = cPickle.Unpickler(dump_file)
J
New.  
James Troup 已提交
82
        for i in [ "changes", "dsc", "files", "dsc_files", "legacy_source_untouchable" ]:
83
            exec "self.pkg.%s.update(p.load());" % (i)
J
New.  
James Troup 已提交
84
        for i in [ "orig_tar_id", "orig_tar_location" ]:
85 86
            exec "self.pkg.%s = p.load();" % (i)
        dump_file.close()
J
New.  
James Troup 已提交
87 88 89

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

90 91 92
    # This could just dump the dictionaries as is, but I'd like to
    # avoid this so there's some idea of what process-accepted &
    # process-new use from process-unchecked
J
New.  
James Troup 已提交
93 94 95 96

    def dump_vars(self, dest_dir):
        for i in [ "changes", "dsc", "files", "dsc_files",
                   "legacy_source_untouchable", "orig_tar_id", "orig_tar_location" ]:
97
            exec "%s = self.pkg.%s;" % (i,i)
98
        dump_filename = os.path.join(dest_dir,self.pkg.changes_file[:-8] + ".dak")
99
        dump_file = utils.open_file(dump_filename, 'w')
J
James Troup 已提交
100
        try:
101
            os.chmod(dump_filename, 0660)
J
James Troup 已提交
102 103
        except OSError, e:
            if errno.errorcode[e.errno] == 'EPERM':
104
                perms = stat.S_IMODE(os.stat(dump_filename)[stat.ST_MODE])
J
James Troup 已提交
105
                if perms & stat.S_IROTH:
106
                    utils.fubar("%s is world readable and chmod failed." % (dump_filename))
J
James Troup 已提交
107
            else:
108
                raise
J
James Troup 已提交
109

110
        p = cPickle.Pickler(dump_file, 1)
J
New.  
James Troup 已提交
111
        for i in [ "d_changes", "d_dsc", "d_files", "d_dsc_files" ]:
112
            exec "%s = {}" % i
J
New.  
James Troup 已提交
113 114
        ## files
        for file in files.keys():
115
            d_files[file] = {}
J
New.  
James Troup 已提交
116 117 118
            for i in [ "package", "version", "architecture", "type", "size",
                       "md5sum", "component", "location id", "source package",
                       "source version", "maintainer", "dbtype", "files id",
119
                       "new", "section", "priority", "othercomponents",
120
                       "pool name", "original component" ]:
J
New.  
James Troup 已提交
121
                if files[file].has_key(i):
122
                    d_files[file][i] = files[file][i]
J
New.  
James Troup 已提交
123 124
        ## changes
        # Mandatory changes fields
125 126 127 128 129
        for i in [ "distribution", "source", "architecture", "version",
                   "maintainer", "urgency", "fingerprint", "changedby822",
                   "changedby2047", "changedbyname", "maintainer822",
                   "maintainer2047", "maintainername", "maintaineremail",
                   "closes", "changes" ]:
130
            d_changes[i] = changes[i]
J
New.  
James Troup 已提交
131
        # Optional changes fields
J
James Troup 已提交
132
        for i in [ "changed-by", "filecontents", "format", "process-new note", "adv id", "distribution-version" ]:
J
James Troup 已提交
133
            if changes.has_key(i):
134
                d_changes[i] = changes[i]
J
New.  
James Troup 已提交
135
        ## dsc
136 137
        for i in [ "source", "version", "maintainer", "fingerprint",
                   "uploaders", "bts changelog" ]:
J
New.  
James Troup 已提交
138
            if dsc.has_key(i):
139
                d_dsc[i] = dsc[i]
J
New.  
James Troup 已提交
140 141
        ## dsc_files
        for file in dsc_files.keys():
142
            d_dsc_files[file] = {}
J
New.  
James Troup 已提交
143 144
            # Mandatory dsc_files fields
            for i in [ "size", "md5sum" ]:
145
                d_dsc_files[file][i] = dsc_files[file][i]
J
New.  
James Troup 已提交
146 147 148
            # Optional dsc_files fields
            for i in [ "files id" ]:
                if dsc_files[file].has_key(i):
149
                    d_dsc_files[file][i] = dsc_files[file][i]
J
New.  
James Troup 已提交
150 151 152

        for i in [ d_changes, d_dsc, d_files, d_dsc_files,
                   legacy_source_untouchable, orig_tar_id, orig_tar_location ]:
153 154
            p.dump(i)
        dump_file.close()
J
New.  
James Troup 已提交
155 156 157 158 159 160

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

    # Set up the per-package template substitution mappings

    def update_subst (self, reject_message = ""):
161 162
        Subst = self.Subst
        changes = self.pkg.changes
163
        # If 'dak process-unchecked' crashed out in the right place, architecture may still be a string.
J
New.  
James Troup 已提交
164
        if not changes.has_key("architecture") or not isinstance(changes["architecture"], DictType):
165
            changes["architecture"] = { "Unknown" : "" }
166 167
        # and maintainer2047 may not exist.
        if not changes.has_key("maintainer2047"):
168
            changes["maintainer2047"] = self.Cnf["Dinstall::MyEmailAddress"]
J
New.  
James Troup 已提交
169

170 171 172
        Subst["__ARCHITECTURE__"] = " ".join(changes["architecture"].keys())
        Subst["__CHANGES_FILENAME__"] = os.path.basename(self.pkg.changes_file)
        Subst["__FILE_CONTENTS__"] = changes.get("filecontents", "")
J
New.  
James Troup 已提交
173 174 175

        # For source uploads the Changed-By field wins; otherwise Maintainer wins.
        if changes["architecture"].has_key("source") and changes["changedby822"] != "" and (changes["changedby822"] != changes["maintainer822"]):
176
            Subst["__MAINTAINER_FROM__"] = changes["changedby2047"]
177
            Subst["__MAINTAINER_TO__"] = "%s, %s" % (changes["changedby2047"],
178 179
                                                     changes["maintainer2047"])
            Subst["__MAINTAINER__"] = changes.get("changed-by", "Unknown")
J
New.  
James Troup 已提交
180
        else:
181 182 183
            Subst["__MAINTAINER_FROM__"] = changes["maintainer2047"]
            Subst["__MAINTAINER_TO__"] = changes["maintainer2047"]
            Subst["__MAINTAINER__"] = changes.get("maintainer", "Unknown")
J
New.  
James Troup 已提交
184
        if self.Cnf.has_key("Dinstall::TrackingServer") and changes.has_key("source"):
185
            Subst["__MAINTAINER_TO__"] += "\nBcc: %s@%s" % (changes["source"], self.Cnf["Dinstall::TrackingServer"])
J
New.  
James Troup 已提交
186

187 188
        # Apply any global override of the Maintainer field
        if self.Cnf.get("Dinstall::OverrideMaintainer"):
189 190
            Subst["__MAINTAINER_TO__"] = self.Cnf["Dinstall::OverrideMaintainer"]
            Subst["__MAINTAINER_FROM__"] = self.Cnf["Dinstall::OverrideMaintainer"]
191

192 193 194
        Subst["__REJECT_MESSAGE__"] = reject_message
        Subst["__SOURCE__"] = changes.get("source", "Unknown")
        Subst["__VERSION__"] = changes.get("version", "Unknown")
J
New.  
James Troup 已提交
195 196 197 198

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

    def build_summaries(self):
199 200
        changes = self.pkg.changes
        files = self.pkg.files
J
New.  
James Troup 已提交
201

202
        byhand = summary = new = ""
J
New.  
James Troup 已提交
203 204 205 206

        # changes["distribution"] may not exist in corner cases
        # (e.g. unreadable changes files)
        if not changes.has_key("distribution") or not isinstance(changes["distribution"], DictType):
207
            changes["distribution"] = {}
J
New.  
James Troup 已提交
208

209
        override_summary ="";
210 211
        file_keys = files.keys()
        file_keys.sort()
J
New.  
James Troup 已提交
212 213 214
        for file in file_keys:
            if files[file].has_key("byhand"):
                byhand = 1
215
                summary += file + " byhand\n"
J
New.  
James Troup 已提交
216 217
            elif files[file].has_key("new"):
                new = 1
218
                summary += "(new) %s %s %s\n" % (file, files[file]["priority"], files[file]["section"])
J
New.  
James Troup 已提交
219
                if files[file].has_key("othercomponents"):
220
                    summary += "WARNING: Already present in %s distribution.\n" % (files[file]["othercomponents"])
J
New.  
James Troup 已提交
221
                if files[file]["type"] == "deb":
222
                    deb_fh = utils.open_file(file)
223
                    summary += apt_pkg.ParseSection(apt_inst.debExtractControl(deb_fh))["Description"] + '\n'
224
                    deb_fh.close()
J
New.  
James Troup 已提交
225
            else:
226
                files[file]["pool name"] = utils.poolify (changes.get("source",""), files[file]["component"])
J
New.  
James Troup 已提交
227
                destination = self.Cnf["Dir::PoolRoot"] + files[file]["pool name"] + file
228
                summary += file + "\n  to " + destination + "\n"
229 230
		if not files[file].has_key("type"):
		    files[file]["type"] = "unknown"
231
                if files[file]["type"] in ["deb", "udeb", "dsc"]:
232 233 234 235 236
                    # (queue/unchecked), there we have override entries already, use them
                    # (process-new), there we dont have override entries, use the newly generated ones.
                    override_prio = files[file].get("override priority", files[file]["priority"])
                    override_sect = files[file].get("override section", files[file]["section"])
                    override_summary += "%s - %s %s\n" % (file, override_prio, override_sect)
J
New.  
James Troup 已提交
237

238
        short_summary = summary
J
New.  
James Troup 已提交
239 240

        # This is for direport's benefit...
241
        f = re_fdnic.sub("\n .\n", changes.get("changes",""))
J
New.  
James Troup 已提交
242 243

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

246 247
        summary += "\n\nOverride entries for your package:\n" + override_summary + "\n"

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

250
        return (summary, short_summary)
J
New.  
James Troup 已提交
251 252 253

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

254
    def close_bugs (self, summary, action):
255 256 257
        changes = self.pkg.changes
        Subst = self.Subst
        Cnf = self.Cnf
J
New.  
James Troup 已提交
258

259
        bugs = changes["closes"].keys()
J
New.  
James Troup 已提交
260

261
        if not bugs:
262
            return summary
J
New.  
James Troup 已提交
263

264
        bugs.sort()
265 266 267 268 269 270 271
        summary += "Closing bugs: "
        for bug in bugs:
            summary += "%s " % (bug)
            if action:
                Subst["__BUG_NUMBER__"] = bug
                if changes["distribution"].has_key("stable"):
                    Subst["__STABLE_WARNING__"] = """
272 273 274 275
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
276
distribution."""
277 278 279 280 281 282
                else:
                    Subst["__STABLE_WARNING__"] = ""
                    mail_message = utils.TemplateSubst(Subst,Cnf["Dir::Templates"]+"/process-unchecked.bug-close")
                    utils.send_mail (mail_message)
        if action:
            self.Logger.log(["closing bugs"]+bugs)
283
        summary += "\n"
284

285
        return summary
286 287 288 289

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

    def announce (self, short_summary, action):
290 291 292
        Subst = self.Subst
        Cnf = self.Cnf
        changes = self.pkg.changes
293 294 295

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

298 299 300
        lists_done = {}
        summary = ""
        Subst["__SHORT_SUMMARY__"] = short_summary
301 302

        for dist in changes["distribution"].keys():
303
            list = Cnf.Find("Suite::%s::Announce" % (dist))
304
            if list == "" or lists_done.has_key(list):
305 306 307
                continue
            lists_done[list] = 1
            summary += "Announcing to %s\n" % (list)
308 309

            if action:
310
                Subst["__ANNOUNCE_LIST_ADDRESS__"] = list
311
                if Cnf.get("Dinstall::TrackingServer") and changes["architecture"].has_key("source"):
312
                    Subst["__ANNOUNCE_LIST_ADDRESS__"] = Subst["__ANNOUNCE_LIST_ADDRESS__"] + "\nBcc: %s@%s" % (changes["source"], Cnf["Dinstall::TrackingServer"])
313
                mail_message = utils.TemplateSubst(Subst,Cnf["Dir::Templates"]+"/process-unchecked.announce")
314
                utils.send_mail (mail_message)
315

J
James Troup 已提交
316
        if Cnf.FindB("Dinstall::CloseBugs"):
317
            summary = self.close_bugs(summary, action)
318

319
        return summary
J
New.  
James Troup 已提交
320 321 322 323

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

    def accept (self, summary, short_summary):
324 325 326 327 328 329
        Cnf = self.Cnf
        Subst = self.Subst
        files = self.pkg.files
        changes = self.pkg.changes
        changes_file = self.pkg.changes_file
        dsc = self.pkg.dsc
J
New.  
James Troup 已提交
330 331

        print "Accepting."
332
        self.Logger.log(["Accepting changes",changes_file])
J
New.  
James Troup 已提交
333

334
        self.dump_vars(Cnf["Dir::Queue::Accepted"])
J
New.  
James Troup 已提交
335 336

        # Move all the files into the accepted directory
337 338
        utils.move(changes_file, Cnf["Dir::Queue::Accepted"])
        file_keys = files.keys()
J
New.  
James Troup 已提交
339
        for file in file_keys:
340
            utils.move(file, Cnf["Dir::Queue::Accepted"])
341
            self.accept_bytes += float(files[file]["size"])
342
        self.accept_count += 1
J
New.  
James Troup 已提交
343 344 345 346

        # Send accept mail, announce to lists, close bugs and check for
        # override disparities
        if not Cnf["Dinstall::Options::No-Mail"]:
347 348
            Subst["__SUITE__"] = ""
            Subst["__SUMMARY__"] = summary
349
            mail_message = utils.TemplateSubst(Subst,Cnf["Dir::Templates"]+"/process-unchecked.accepted")
350
            utils.send_mail(mail_message)
J
New.  
James Troup 已提交
351 352
            self.announce(short_summary, 1)

353 354 355 356 357 358 359 360 361 362 363 364

        ## Helper stuff for DebBugs Version Tracking
        if Cnf.Find("Dir::Queue::BTSVersionTrack"):
            # ??? 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
            if changes["architecture"].has_key("source") and \
               dsc.has_key("bts changelog"):

                temp_filename = utils.temp_filename(Cnf["Dir::Queue::BTSVersionTrack"],
365 366 367 368
                                                    dotprefix=1, perms=0644)
                version_history = utils.open_file(temp_filename, 'w')
                version_history.write(dsc["bts changelog"])
                version_history.close()
369
                filename = "%s/%s" % (Cnf["Dir::Queue::BTSVersionTrack"],
370 371
                                      changes_file[:-8]+".versions")
                os.rename(temp_filename, filename)
372 373 374

            # Write out the binary -> source mapping.
            temp_filename = utils.temp_filename(Cnf["Dir::Queue::BTSVersionTrack"],
375 376
                                                dotprefix=1, perms=0644)
            debinfo = utils.open_file(temp_filename, 'w')
377
            for file in file_keys:
378
                f = files[file]
379 380 381
                if f["type"] == "deb":
                    line = " ".join([f["package"], f["version"],
                                     f["architecture"], f["source package"],
382 383 384
                                     f["source version"]])
                    debinfo.write(line+"\n")
            debinfo.close()
385
            filename = "%s/%s" % (Cnf["Dir::Queue::BTSVersionTrack"],
386 387
                                  changes_file[:-8]+".debinfo")
            os.rename(temp_filename, filename)
388

389 390 391 392 393
        self.queue_build("accepted", Cnf["Dir::Queue::Accepted"])

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

    def queue_build (self, queue, path):
394 395 396 397 398 399 400 401
        Cnf = self.Cnf
        Subst = self.Subst
        files = self.pkg.files
        changes = self.pkg.changes
        changes_file = self.pkg.changes_file
        dsc = self.pkg.dsc
        file_keys = files.keys()

402
        ## Special support to enable clean auto-building of queued packages
403
        queue_id = database.get_or_set_queue_id(queue)
404

405
        self.projectB.query("BEGIN WORK")
406
        for suite in changes["distribution"].keys():
407
            if suite not in Cnf.ValueList("Dinstall::QueueBuildSuites"):
408
                continue
409
            suite_id = database.get_suite_id(suite)
410
            dest_dir = Cnf["Dir::QueueBuild"]
411
            if Cnf.FindB("Dinstall::SecurityQueueBuild"):
412
                dest_dir = os.path.join(dest_dir, suite)
413
            for file in file_keys:
414 415
                src = os.path.join(path, file)
                dest = os.path.join(dest_dir, file)
416
                if Cnf.FindB("Dinstall::SecurityQueueBuild"):
417
                    # Copy it since the original won't be readable by www-data
418
                    utils.copy(src, dest)
419 420
                else:
                    # Create a symlink to it
421
                    os.symlink(src, dest)
J
James Troup 已提交
422
                # Add it to the list of packages for later processing by apt-ftparchive
423
                self.projectB.query("INSERT INTO queue_build (suite, queue, filename, in_queue) VALUES (%s, %s, '%s', 't')" % (suite_id, queue_id, dest))
J
James Troup 已提交
424 425
            # If the .orig.tar.gz is in the pool, create a symlink to
            # it (if one doesn't already exist)
426 427 428
            if self.pkg.orig_tar_id:
                # Determine the .orig.tar.gz file name
                for dsc_file in self.pkg.dsc_files.keys():
429
                    if dsc_file.endswith(".orig.tar.gz"):
430 431
                        filename = dsc_file
                dest = os.path.join(dest_dir, filename)
432 433 434
                # If it doesn't exist, create a symlink
                if not os.path.exists(dest):
                    # Find the .orig.tar.gz in the pool
435 436
                    q = self.projectB.query("SELECT l.path, f.filename from location l, files f WHERE f.id = %s and f.location = l.id" % (self.pkg.orig_tar_id))
                    ql = q.getresult()
437
                    if not ql:
438 439 440
                        utils.fubar("[INTERNAL ERROR] Couldn't find id %s in files table." % (self.pkg.orig_tar_id))
                    src = os.path.join(ql[0][0], ql[0][1])
                    os.symlink(src, dest)
J
James Troup 已提交
441
                    # Add it to the list of packages for later processing by apt-ftparchive
442
                    self.projectB.query("INSERT INTO queue_build (suite, queue, filename, in_queue) VALUES (%s, %s, '%s', 't')" % (suite_id, queue_id, dest))
443 444
                # if it does, update things to ensure it's not removed prematurely
                else:
445
                    self.projectB.query("UPDATE queue_build SET in_queue = 't', last_used = NULL WHERE filename = '%s' AND suite = %s" % (dest, suite_id))
J
James Troup 已提交
446

447
        self.projectB.query("COMMIT WORK")
448

J
New.  
James Troup 已提交
449 450 451
    ###########################################################################

    def check_override (self):
452 453 454 455
        Subst = self.Subst
        changes = self.pkg.changes
        files = self.pkg.files
        Cnf = self.Cnf
J
New.  
James Troup 已提交
456

457 458 459 460 461 462 463
        # Abandon the check if:
        #  a) it's a non-sourceful upload
        #  b) override disparity checks have been disabled
        #  c) we're not sending mail
        if not changes["architecture"].has_key("source") or \
           not Cnf.FindB("Dinstall::OverrideDisparityCheck") or \
           Cnf["Dinstall::Options::No-Mail"]:
464
            return
J
New.  
James Troup 已提交
465

466 467 468
        summary = ""
        file_keys = files.keys()
        file_keys.sort()
J
James Troup 已提交
469
        for file in file_keys:
J
New.  
James Troup 已提交
470
            if not files[file].has_key("new") and files[file]["type"] == "deb":
471 472
                section = files[file]["section"]
                override_section = files[file]["override section"]
473
                if section.lower() != override_section.lower() and section != "-":
J
New.  
James Troup 已提交
474
                    # Ignore this; it's a common mistake and not worth whining about
475
                    if section.lower() == "non-us/main" and override_section.lower() == "non-us":
476 477 478 479
                        continue
                    summary += "%s: package says section is %s, override says %s.\n" % (file, section, override_section)
                priority = files[file]["priority"]
                override_priority = files[file]["override priority"]
J
New.  
James Troup 已提交
480
                if priority != override_priority and priority != "-":
481
                    summary += "%s: package says priority is %s, override says %s.\n" % (file, priority, override_priority)
J
New.  
James Troup 已提交
482 483

        if summary == "":
484
            return
J
New.  
James Troup 已提交
485

486
        Subst["__SUMMARY__"] = summary
487
        mail_message = utils.TemplateSubst(Subst,self.Cnf["Dir::Templates"]+"/process-unchecked.override-disparity")
488
        utils.send_mail(mail_message)
J
New.  
James Troup 已提交
489 490 491

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

492 493 494 495 496
    def force_reject (self, files):
        """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."""
J
New.  
James Troup 已提交
497 498 499 500 501 502

        Cnf = self.Cnf

        for file in files:
            # Skip any files which don't exist or which we don't have permission to copy.
            if os.access(file,os.R_OK) == 0:
503 504
                continue
            dest_file = os.path.join(Cnf["Dir::Queue::Reject"], file)
J
New.  
James Troup 已提交
505
            try:
506
                dest_fd = os.open(dest_file, os.O_RDWR|os.O_CREAT|os.O_EXCL, 0644)
J
New.  
James Troup 已提交
507 508 509
            except OSError, e:
                # File exists?  Let's try and move it to the morgue
                if errno.errorcode[e.errno] == 'EEXIST':
510
                    morgue_file = os.path.join(Cnf["Dir::Morgue"],Cnf["Dir::MorgueReject"],file)
J
New.  
James Troup 已提交
511
                    try:
512
                        morgue_file = utils.find_next_free(morgue_file)
J
New.  
James Troup 已提交
513 514 515
                    except utils.tried_too_hard_exc:
                        # Something's either gone badly Pete Tong, or
                        # someone is trying to exploit us.
516 517 518
                        utils.warn("**WARNING** failed to move %s from the reject directory to the morgue." % (file))
                        return
                    utils.move(dest_file, morgue_file, perms=0660)
J
New.  
James Troup 已提交
519
                    try:
520
                        dest_fd = os.open(dest_file, os.O_RDWR|os.O_CREAT|os.O_EXCL, 0644)
J
New.  
James Troup 已提交
521 522
                    except OSError, e:
                        # Likewise
523 524
                        utils.warn("**WARNING** failed to claim %s in the reject directory." % (file))
                        return
J
New.  
James Troup 已提交
525
                else:
526
                    raise
J
New.  
James Troup 已提交
527 528
            # If we got here, we own the destination file, so we can
            # safely overwrite it.
529
            utils.move(file, dest_file, 1, perms=0660)
530
            os.close(dest_fd)
531

J
New.  
James Troup 已提交
532 533 534
    ###########################################################################

    def do_reject (self, manual = 0, reject_message = ""):
J
James Troup 已提交
535 536 537
        # 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:
538
            temp_filename = utils.temp_filename()
J
James Troup 已提交
539
            editor = os.environ.get("EDITOR","vi")
540
            answer = 'E'
J
James Troup 已提交
541 542
            while answer == 'E':
                os.system("%s %s" % (editor, temp_filename))
543 544 545 546 547
                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 已提交
548
                prompt = "[R]eject, Edit, Abandon, Quit ?"
549
                answer = "XXX"
550
                while prompt.find(answer) == -1:
551 552
                    answer = utils.our_raw_input(prompt)
                    m = re_default_answer.search(prompt)
J
James Troup 已提交
553
                    if answer == "":
554 555 556
                        answer = m.group(1)
                    answer = answer[:1].upper()
            os.unlink(temp_filename)
J
James Troup 已提交
557
            if answer == 'A':
558
                return 1
J
James Troup 已提交
559
            elif answer == 'Q':
560
                sys.exit(0)
J
James Troup 已提交
561

J
New.  
James Troup 已提交
562 563
        print "Rejecting.\n"

564 565 566
        Cnf = self.Cnf
        Subst = self.Subst
        pkg = self.pkg
J
New.  
James Troup 已提交
567

568 569
        reason_filename = pkg.changes_file[:-8] + ".reason"
        reason_filename = Cnf["Dir::Queue::Reject"] + '/' + reason_filename
J
New.  
James Troup 已提交
570 571

        # Move all the files into the reject directory
572 573
        reject_files = pkg.files.keys() + [pkg.changes_file]
        self.force_reject(reject_files)
J
New.  
James Troup 已提交
574 575 576

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

        if not manual:
582 583
            Subst["__REJECTOR_ADDRESS__"] = Cnf["Dinstall::MyEmailAddress"]
            Subst["__MANUAL_REJECT_MESSAGE__"] = ""
584
            Subst["__CC__"] = "X-DAK-Rejection: automatic (moo)\nX-Katie-Rejection: automatic (moo)"
585
            os.write(reason_fd, reject_message)
586
            reject_mail_message = utils.TemplateSubst(Subst,Cnf["Dir::Templates"]+"/queue.rejected")
J
New.  
James Troup 已提交
587 588
        else:
            # Build up the rejection email
589
            user_email_address = utils.whoami() + " <%s>" % (Cnf["Dinstall::MyAdminAddress"])
J
New.  
James Troup 已提交
590

591 592 593
            Subst["__REJECTOR_ADDRESS__"] = user_email_address
            Subst["__MANUAL_REJECT_MESSAGE__"] = reject_message
            Subst["__CC__"] = "Cc: " + Cnf["Dinstall::MyEmailAddress"]
594
            reject_mail_message = utils.TemplateSubst(Subst,Cnf["Dir::Templates"]+"/queue.rejected")
J
New.  
James Troup 已提交
595
            # Write the rejection email out as the <foo>.reason file
596
            os.write(reason_fd, reject_mail_message)
J
James Troup 已提交
597

598
        os.close(reason_fd)
J
New.  
James Troup 已提交
599 600 601

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

604 605
        self.Logger.log(["rejected", pkg.changes_file])
        return 0
J
New.  
James Troup 已提交
606 607 608 609 610 611 612

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

    # Ensure that source exists somewhere in the archive for the binary
    # upload being processed.
    #
    # (1) exact match                      => 1.0-3
R
Ryan Murray 已提交
613
    # (2) Bin-only NMU                     => 1.0-3+b1 , 1.0-3.1+b1
J
New.  
James Troup 已提交
614

615 616 617
    def source_exists (self, package, source_version, suites = ["any"]):
	okay = 1
	for suite in suites:
618
	    if suite == "any":
619 620 621
	    	que = "SELECT s.version FROM source s WHERE s.source = '%s'" % \
		    (package)
	    else:
622 623 624 625 626 627
		# source must exist in suite X, or in some other suite that's
		# mapped to X, recursively... silent-maps are counted too,
		# unreleased-maps aren't.
		maps = self.Cnf.ValueList("SuiteMappings")[:]
		maps.reverse()
		maps = [ m.split() for m in maps ]
628
		maps = [ (x[1], x[2]) for x in maps
629 630 631 632 633
				if x[0] == "map" or x[0] == "silent-map" ]
		s = [suite]
		for x in maps:
			if x[1] in s and x[0] not in s:
				s.append(x[0])
634

635
		que = "SELECT s.version FROM source s JOIN src_associations sa ON (s.id = sa.source) JOIN suite su ON (sa.suite = su.id) WHERE s.source = '%s' AND (%s)" % (package, " OR ".join(["su.suite_name = '%s'" % a for a in s]))
636 637 638
            q = self.projectB.query(que)

            # Reduce the query results to a list of version numbers
639
            ql = [ i[0] for i in q.getresult() ]
640 641

            # Try (1)
642
            if source_version in ql:
643 644 645
                continue

            # Try (2)
646
            orig_source_version = re_bin_only_nmu.sub('', source_version)
647
            if orig_source_version in ql:
648 649 650 651
                continue

            # No source found...
            okay = 0
R
Ryan Murray 已提交
652
	    break
653
	return okay
J
New.  
James Troup 已提交
654 655

    ################################################################################
656
    
J
New.  
James Troup 已提交
657
    def in_override_p (self, package, component, suite, binary_type, file):
658
        files = self.pkg.files
J
New.  
James Troup 已提交
659 660

        if binary_type == "": # must be source
661
            type = "dsc"
J
New.  
James Troup 已提交
662
        else:
663
            type = binary_type
J
New.  
James Troup 已提交
664 665 666

        # Override suite name; used for example with proposed-updates
        if self.Cnf.Find("Suite::%s::OverrideSuite" % (suite)) != "":
667
            suite = self.Cnf["Suite::%s::OverrideSuite" % (suite)]
J
New.  
James Troup 已提交
668 669

        # Avoid <undef> on unknown distributions
670
        suite_id = database.get_suite_id(suite)
J
New.  
James Troup 已提交
671
        if suite_id == -1:
672
            return None
673 674
        component_id = database.get_component_id(component)
        type_id = database.get_override_type_id(type)
J
New.  
James Troup 已提交
675 676

        # FIXME: nasty non-US speficic hack
677
        if component.lower().startswith("non-us/"):
678
            component = component[7:]
J
New.  
James Troup 已提交
679 680

        q = self.projectB.query("SELECT s.section, p.priority FROM override o, section s, priority p WHERE package = '%s' AND suite = %s AND component = %s AND type = %s AND o.section = s.id AND o.priority = p.id"
681 682
                           % (package, suite_id, component_id, type_id))
        result = q.getresult()
J
New.  
James Troup 已提交
683 684
        # If checking for a source package fall back on the binary override type
        if type == "dsc" and not result:
685 686
            deb_type_id = database.get_override_type_id("deb")
            udeb_type_id = database.get_override_type_id("udeb")
687
            q = self.projectB.query("SELECT s.section, p.priority FROM override o, section s, priority p WHERE package = '%s' AND suite = %s AND component = %s AND (type = %s OR type = %s) AND o.section = s.id AND o.priority = p.id"
688 689
                               % (package, suite_id, component_id, deb_type_id, udeb_type_id))
            result = q.getresult()
J
New.  
James Troup 已提交
690 691

        # Remember the section and priority so we can check them later if appropriate
692
        if result:
693 694
            files[file]["override section"] = result[0][0]
            files[file]["override priority"] = result[0][1]
J
New.  
James Troup 已提交
695

696
        return result
J
New.  
James Troup 已提交
697 698 699 700 701

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

    def reject (self, str, prefix="Rejected: "):
        if str:
702 703 704
            # 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:
705 706
                self.reject_message += "\n"
            self.reject_message += prefix + str
J
New.  
James Troup 已提交
707

708 709
    ################################################################################

710 711 712 713
    def get_anyversion(self, query_result, suite):
        anyversion=None
        anysuite = [suite] + self.Cnf.ValueList("Suite::%s::VersionChecks::Enhances" % (suite))
        for (v, s) in query_result:
714
            if s in [ x.lower() for x in anysuite ]:
715 716 717 718 719 720
                if not anyversion or apt_pkg.VersionCompare(anyversion, v) <= 0:
                    anyversion=v
        return anyversion

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

721 722 723 724 725 726 727
    def cross_suite_version_check(self, query_result, file, new_version):
        """Ensure versions are newer than existing packages in target
        suites and that cross-suite version checking rules as
        set out in the conf file are satisfied."""

        # Check versions for each target suite
        for target_suite in self.pkg.changes["distribution"].keys():
728 729
            must_be_newer_than = [ i.lower for i in self.Cnf.ValueList("Suite::%s::VersionChecks::MustBeNewerThan" % (target_suite)) ]
            must_be_older_than = [ i.lower for i in self.Cnf.ValueList("Suite::%s::VersionChecks::MustBeOlderThan" % (target_suite)) ]
730 731
            # Enforce "must be newer than target suite" even if conffile omits it
            if target_suite not in must_be_newer_than:
732
                must_be_newer_than.append(target_suite)
733
            for entry in query_result:
734 735
                existent_version = entry[0]
                suite = entry[1]
736
                if suite in must_be_newer_than and \
737
                   apt_pkg.VersionCompare(new_version, existent_version) < 1:
738
                    self.reject("%s: old version (%s) in %s >= new version (%s) targeted at %s." % (file, existent_version, suite, new_version, target_suite))
739
                if suite in must_be_older_than and \
740
                   apt_pkg.VersionCompare(new_version, existent_version) > -1:
741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760
                    ch = self.pkg.changes
                    cansave = 0
                    if ch.get('distribution-version', {}).has_key(suite):
                        # we really use the other suite, ignoring the conflicting one ...
                        addsuite = ch["distribution-version"][suite]
                    
                        add_version = self.get_anyversion(query_result, addsuite)
                        target_version = self.get_anyversion(query_result, target_suite)
                    
                        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: ")
761 762
                            self.pkg.changes.setdefault("propdistribution", {})
                            self.pkg.changes["propdistribution"][addsuite] = 1
763 764 765 766 767 768 769 770
                            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.
771
                            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: ")
772 773
                            cansave = 1
                        elif apt_pkg.VersionCompare(new_version, add_version) > 0 and \
774
                             apt_pkg.VersionCompare(add_version, target_version) >= 0:
775
                            # propogate!!
776 777 778
                            self.reject("Propogating upload to %s" % (addsuite), "Warning: ")
                            self.pkg.changes.setdefault("propdistribution", {})
                            self.pkg.changes["propdistribution"][addsuite] = 1
779 780 781
                            cansave = 1
                
                    if not cansave:
782
                        self.reject("%s: old version (%s) in %s <= new version (%s) targeted at %s." % (file, existent_version, suite, new_version, target_suite))
783 784 785 786

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

    def check_binary_against_db(self, file):
787 788
        self.reject_message = ""
        files = self.pkg.files
J
New.  
James Troup 已提交
789

790 791 792 793 794 795 796
        # Ensure version is sane
        q = self.projectB.query("""
SELECT b.version, su.suite_name FROM binaries b, bin_associations ba, suite su,
                                     architecture a
 WHERE b.package = '%s' AND (a.arch_string = '%s' OR a.arch_string = 'all')
   AND ba.bin = b.id AND ba.suite = su.id AND b.architecture = a.id"""
                                % (files[file]["package"],
797 798
                                   files[file]["architecture"]))
        self.cross_suite_version_check(q.getresult(), file, files[file]["version"])
799

J
New.  
James Troup 已提交
800
        # Check for any existing copies of the file
801 802 803 804 805 806 807 808
        q = self.projectB.query("""
SELECT b.id FROM binaries b, architecture a
 WHERE b.package = '%s' AND b.version = '%s' AND a.arch_string = '%s'
   AND a.id = b.architecture"""
                                % (files[file]["package"],
                                   files[file]["version"],
                                   files[file]["architecture"]))
        if q.getresult():
809
            self.reject("%s: can not overwrite existing copy already in the archive." % (file))
J
New.  
James Troup 已提交
810

811
        return self.reject_message
J
New.  
James Troup 已提交
812 813 814 815

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

    def check_source_against_db(self, file):
816 817
        self.reject_message = ""
        dsc = self.pkg.dsc
J
New.  
James Troup 已提交
818

819 820 821
        # Ensure version is sane
        q = self.projectB.query("""
SELECT s.version, su.suite_name FROM source s, src_associations sa, suite su
822 823
 WHERE s.source = '%s' AND sa.source = s.id AND sa.suite = su.id""" % (dsc.get("source")))
        self.cross_suite_version_check(q.getresult(), file, dsc.get("version"))
824

825
        return self.reject_message
J
New.  
James Troup 已提交
826 827 828

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

829 830 831 832 833 834 835 836
    # **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 derefernece the deleted entry.
    # **WARNING**

J
New.  
James Troup 已提交
837
    def check_dsc_against_db(self, file):
838 839 840 841 842
        self.reject_message = ""
        files = self.pkg.files
        dsc_files = self.pkg.dsc_files
        legacy_source_untouchable = self.pkg.legacy_source_untouchable
        self.pkg.orig_tar_gz = None
J
New.  
James Troup 已提交
843 844 845 846

        # 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.
847 848
        # The ordering on the select is needed to pick the newest orig
        # when it exists in multiple places.
J
New.  
James Troup 已提交
849
        for dsc_file in dsc_files.keys():
850
            found = None
J
New.  
James Troup 已提交
851
            if files.has_key(dsc_file):
852 853
                actual_md5 = files[dsc_file]["md5sum"]
                actual_size = int(files[dsc_file]["size"])
J
New.  
James Troup 已提交
854 855
                found = "%s in incoming" % (dsc_file)
                # Check the file does not already exist in the archive
856
                q = self.projectB.query("SELECT f.size, f.md5sum, l.path, f.filename FROM files f, location l WHERE f.filename LIKE '%%%s%%' AND l.id = f.location ORDER BY f.id DESC" % (dsc_file))
857
                ql = q.getresult()
858 859
                # Strip out anything that isn't '%s' or '/%s$'
                for i in ql:
860
                    if i[3] != dsc_file and i[3][-(len(dsc_file)+1):] != '/'+dsc_file:
861
                        ql.remove(i)
862

863
                # "[dak] has not broken them.  [dak] has fixed a
J
New.  
James Troup 已提交
864 865 866 867 868 869 870 871
                # 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

872
                if ql:
873
                    # Ignore exact matches for .orig.tar.gz
874
                    match = 0
875
                    if dsc_file.endswith(".orig.tar.gz"):
876
                        for i in ql:
877 878
                            if files.has_key(dsc_file) and \
                               int(files[dsc_file]["size"]) == int(i[0]) and \
879
                               files[dsc_file]["md5sum"] == i[1]:
880 881 882 883
                                self.reject("ignoring %s, since it's already in the archive." % (dsc_file), "Warning: ")
                                del files[dsc_file]
                                self.pkg.orig_tar_gz = i[2] + i[3]
                                match = 1
884 885

                    if not match:
886
                        self.reject("can not overwrite existing copy of '%s' already in the archive." % (dsc_file))
887
            elif dsc_file.endswith(".orig.tar.gz"):
J
New.  
James Troup 已提交
888
                # Check in the pool
889 890
                q = self.projectB.query("SELECT l.path, f.filename, l.type, f.id, l.id FROM files f, location l WHERE f.filename LIKE '%%%s%%' AND l.id = f.location" % (dsc_file))
                ql = q.getresult()
891 892 893
                # Strip out anything that isn't '%s' or '/%s$'
                for i in ql:
                    if i[1] != dsc_file and i[1][-(len(dsc_file)+1):] != '/'+dsc_file:
894
                        ql.remove(i)
J
New.  
James Troup 已提交
895

896
                if ql:
897 898 899
                    # 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 已提交
900 901 902 903 904

                    x = ql[0]; # default to something sane in case we don't match any or have only one

                    if len(ql) > 1:
                        for i in ql:
905
                            old_file = i[0] + i[1]
906
                            old_file_fh = utils.open_file(old_file)
907
                            actual_md5 = apt_pkg.md5sum(old_file_fh)
908
                            old_file_fh.close()
909
                            actual_size = os.stat(old_file)[stat.ST_SIZE]
J
New.  
James Troup 已提交
910
                            if actual_md5 == dsc_files[dsc_file]["md5sum"] and actual_size == int(dsc_files[dsc_file]["size"]):
911
                                x = i
J
New.  
James Troup 已提交
912
                            else:
913
                                legacy_source_untouchable[i[3]] = ""
J
New.  
James Troup 已提交
914

915
                    old_file = x[0] + x[1]
916
                    old_file_fh = utils.open_file(old_file)
917
                    actual_md5 = apt_pkg.md5sum(old_file_fh)
918
                    old_file_fh.close()
919 920 921
                    actual_size = os.stat(old_file)[stat.ST_SIZE]
                    found = old_file
                    suite_type = x[2]
J
New.  
James Troup 已提交
922
                    dsc_files[dsc_file]["files id"] = x[3]; # need this for updating dsc_files in install()
923
                    # See install() in process-accepted...
924 925
                    self.pkg.orig_tar_id = x[3]
                    self.pkg.orig_tar_gz = old_file
J
New.  
James Troup 已提交
926
                    if suite_type == "legacy" or suite_type == "legacy-mixed":
927
                        self.pkg.orig_tar_location = "legacy"
J
New.  
James Troup 已提交
928
                    else:
929
                        self.pkg.orig_tar_location = x[4]
J
New.  
James Troup 已提交
930
                else:
931
                    # Not there? Check the queue directories...
J
New.  
James Troup 已提交
932

933
                    in_unchecked = os.path.join(self.Cnf["Dir::Queue::Unchecked"],dsc_file)
934
                    # See process_it() in 'dak process-unchecked' for explanation of this
935
                    if os.path.exists(in_unchecked):
936
                        return (self.reject_message, in_unchecked)
J
New.  
James Troup 已提交
937
                    else:
938
                        for dir in [ "Accepted", "New", "Byhand" ]:
939
                            in_otherdir = os.path.join(self.Cnf["Dir::Queue::%s" % (dir)],dsc_file)
940
                            if os.path.exists(in_otherdir):
941
                                in_otherdir_fh = utils.open_file(in_otherdir)
942
                                actual_md5 = apt_pkg.md5sum(in_otherdir_fh)
943
                                in_otherdir_fh.close()
944 945 946
                                actual_size = os.stat(in_otherdir)[stat.ST_SIZE]
                                found = in_otherdir
                                self.pkg.orig_tar_gz = in_otherdir
947 948

                    if not found:
949 950 951
                        self.reject("%s refers to %s, but I can't find it in the queue or in the pool." % (file, dsc_file))
                        self.pkg.orig_tar_gz = -1
                        continue
J
New.  
James Troup 已提交
952
            else:
953 954
                self.reject("%s refers to %s, but I can't find it in the queue." % (file, dsc_file))
                continue
J
New.  
James Troup 已提交
955
            if actual_md5 != dsc_files[dsc_file]["md5sum"]:
956
                self.reject("md5sum for %s doesn't match %s." % (found, file))
J
New.  
James Troup 已提交
957
            if actual_size != int(dsc_files[dsc_file]["size"]):
958
                self.reject("size for %s doesn't match %s." % (found, file))
J
New.  
James Troup 已提交
959

960
        return (self.reject_message, None)
J
James Troup 已提交
961 962

    def do_query(self, q):
963 964 965 966 967 968
        sys.stderr.write("query: \"%s\" ... " % (q))
        before = time.time()
        r = self.projectB.query(q)
        time_diff = time.time()-before
        sys.stderr.write("took %.3f seconds.\n" % (time_diff))
        return r