~launchpad-pqm/launchpad/devel

« back to all changes in this revision

Viewing changes to lib/lp/soyuz/scripts/processaccepted.py

  • Committer: Launchpad Patch Queue Manager
  • Date: 2011-06-25 08:55:37 UTC
  • mfrom: (13287.1.8 bug-800652)
  • Revision ID: launchpad@pqm.canonical.com-20110625085537-moikyoo2pe98zs7r
[r=jcsackett, julian-edwards][bug=800634,
        800652] Enable and display overrides on sync package uploads.

Show diffs side-by-side

added added

removed removed

Lines of Context:
5
5
 
6
6
__metaclass__ = type
7
7
__all__ = [
 
8
    'close_bugs',
8
9
    'close_bugs_for_queue_item',
9
10
    'close_bugs_for_sourcepackagerelease',
10
11
    'close_bugs_for_sourcepublication',
12
13
    'ProcessAccepted',
13
14
    ]
14
15
 
15
 
from optparse import OptionValueError
16
16
import sys
17
17
 
18
18
from debian.deb822 import Deb822Dict
19
19
from zope.component import getUtility
20
20
from zope.security.proxy import removeSecurityProxy
21
21
 
 
22
from canonical.launchpad.webapp.errorlog import (
 
23
    ErrorReportingUtility,
 
24
    ScriptRequest,
 
25
    )
22
26
from lp.app.errors import NotFoundError
23
27
from lp.app.interfaces.launchpad import ILaunchpadCelebrities
24
28
from lp.archiveuploader.tagfiles import parse_tagfile_content
27
31
from lp.registry.interfaces.distribution import IDistributionSet
28
32
from lp.registry.interfaces.pocket import PackagePublishingPocket
29
33
from lp.services.scripts.base import (
30
 
    LaunchpadCronScript,
 
34
    LaunchpadScript,
31
35
    LaunchpadScriptFailure,
32
36
    )
33
 
from lp.services.webapp.errorlog import (
34
 
    ErrorReportingUtility,
35
 
    ScriptRequest,
36
 
    )
37
37
from lp.soyuz.enums import (
38
38
    ArchivePurpose,
39
39
    PackageUploadStatus,
40
 
    re_bug_numbers,
41
 
    re_closes,
42
 
    re_lp_closes,
43
40
    )
44
41
from lp.soyuz.interfaces.archive import IArchiveSet
45
42
from lp.soyuz.interfaces.queue import IPackageUploadSet
67
64
    return bugs
68
65
 
69
66
 
70
 
def get_bugs_from_changelog_entry(sourcepackagerelease, since_version):
71
 
    """Parse the changelog_entry in the sourcepackagerelease and return a
72
 
    list of `IBug`s referenced by it.
 
67
def close_bugs(queue_ids):
 
68
    """Close any bugs referenced by the queue items.
 
69
 
 
70
    Retrieve PackageUpload objects for the given ID list and perform
 
71
    close_bugs_for_queue_item on each of them.
73
72
    """
74
 
    changelog = sourcepackagerelease.aggregate_changelog(since_version)
75
 
    closes = []
76
 
    # There are 2 main regexes to match.  Each match from those can then
77
 
    # have further multiple matches from the 3rd regex:
78
 
    # closes: NNN, NNN
79
 
    # lp: #NNN, #NNN
80
 
    regexes = (
81
 
        re_closes.finditer(changelog), re_lp_closes.finditer(changelog))
82
 
    for regex in regexes:
83
 
        for match in regex:
84
 
            bug_match = re_bug_numbers.findall(match.group(0))
85
 
            closes += map(int, bug_match)
86
 
 
87
 
    bugs = []
88
 
    for bug_id in closes:
89
 
        try:
90
 
            bug = getUtility(IBugSet).get(bug_id)
91
 
        except NotFoundError:
92
 
            continue
93
 
        else:
94
 
            bugs.append(bug)
95
 
 
96
 
    return bugs
 
73
    for queue_id in queue_ids:
 
74
        queue_item = getUtility(IPackageUploadSet).get(queue_id)
 
75
        close_bugs_for_queue_item(queue_item)
97
76
 
98
77
 
99
78
def can_close_bugs(target):
127
106
    the upload is processed and committed.
128
107
 
129
108
    In practice, 'changesfile_object' is only set when we are closing bugs
130
 
    in upload-time (see nascentupload-closing-bugs.txt).
 
109
    in upload-time (see
 
110
    archiveuploader/ftests/nascentupload-closing-bugs.txt).
131
111
 
132
112
    Skip bug-closing if the upload is target to pocket PROPOSED or if
133
113
    the upload is for a PPA.
150
130
            source_queue_item.sourcepackagerelease, changesfile_object)
151
131
 
152
132
 
153
 
def close_bugs_for_sourcepublication(source_publication, since_version=None):
 
133
def close_bugs_for_sourcepublication(source_publication):
154
134
    """Close bugs for a given sourcepublication.
155
135
 
156
136
    Given a `ISourcePackagePublishingHistory` close bugs mentioned in
162
142
    sourcepackagerelease = source_publication.sourcepackagerelease
163
143
    changesfile_object = sourcepackagerelease.upload_changesfile
164
144
 
 
145
    # No changesfile available, cannot close bugs.
 
146
    if changesfile_object is None:
 
147
        return
 
148
 
165
149
    close_bugs_for_sourcepackagerelease(
166
 
        sourcepackagerelease, changesfile_object, since_version,
167
 
        upload_distroseries=source_publication.distroseries)
168
 
 
169
 
 
170
 
def close_bugs_for_sourcepackagerelease(source_release, changesfile_object,
171
 
                                        since_version=None,
172
 
                                        upload_distroseries=None):
 
150
        sourcepackagerelease, changesfile_object)
 
151
 
 
152
 
 
153
def close_bugs_for_sourcepackagerelease(source_release, changesfile_object):
173
154
    """Close bugs for a given source.
174
155
 
175
156
    Given a `ISourcePackageRelease` and a corresponding changesfile object,
176
157
    close bugs mentioned in the changesfile in the context of the source.
177
 
 
178
 
    If changesfile_object is None and since_version is supplied,
179
 
    close all the bugs in changelog entries made after that version and up
180
 
    to and including the source_release's version.  It does this by parsing
181
 
    the changelog on the sourcepackagerelease.  This could be extended in
182
 
    the future to deal with the changes file as well but there is no
183
 
    requirement to do so right now.
184
158
    """
185
 
    if since_version and source_release.changelog:
186
 
        bugs_to_close = get_bugs_from_changelog_entry(
187
 
            source_release, since_version=since_version)
188
 
    elif changesfile_object:
189
 
        bugs_to_close = get_bugs_from_changes_file(changesfile_object)
190
 
    else:
191
 
        return
 
159
    bugs_to_close = get_bugs_from_changes_file(changesfile_object)
192
160
 
193
161
    # No bugs to be closed by this upload, move on.
194
162
    if not bugs_to_close:
204
172
        # here, BE CAREFUL with the unproxied bug object and look at
205
173
        # what you're doing with it that might violate security.
206
174
        bug = removeSecurityProxy(bug)
207
 
        if upload_distroseries is not None:
208
 
            target = upload_distroseries.getSourcePackage(
209
 
                source_release.sourcepackagename)
210
 
        else:
211
 
            target = source_release.sourcepackage
212
175
        edited_task = bug.setStatus(
213
 
            target=target, status=BugTaskStatus.FIXRELEASED, user=janitor)
 
176
            target=source_release.sourcepackage,
 
177
            status=BugTaskStatus.FIXRELEASED,
 
178
            user=janitor)
214
179
        if edited_task is not None:
215
180
            assert source_release.changelog_entry is not None, (
216
181
                "New source uploads should have a changelog.")
224
189
                content=content)
225
190
 
226
191
 
227
 
class TargetPolicy:
228
 
    """Policy describing what kinds of archives to operate on."""
229
 
 
230
 
    def __init__(self, logger):
231
 
        self.logger = logger
232
 
 
233
 
    def getTargetArchives(self, distribution):
234
 
        """Get target archives of the right sort for `distribution`."""
235
 
        raise NotImplemented("getTargetArchives")
236
 
 
237
 
    def describeArchive(self, archive):
238
 
        """Return textual description for `archive` in this script run."""
239
 
        raise NotImplemented("describeArchive")
240
 
 
241
 
    def postprocessSuccesses(self, queue_ids):
242
 
        """Optionally, post-process successfully processed queue items.
243
 
 
244
 
        :param queue_ids: An iterable of `PackageUpload` ids that were
245
 
            successfully processed.
246
 
        """
247
 
 
248
 
 
249
 
class PPATargetPolicy(TargetPolicy):
250
 
    """Target policy for PPA archives."""
251
 
 
252
 
    def getTargetArchives(self, distribution):
253
 
        """See `TargetPolicy`."""
254
 
        return distribution.getPendingAcceptancePPAs()
255
 
 
256
 
    def describeArchive(self, archive):
257
 
        """See `TargetPolicy`."""
258
 
        return archive.archive_url
259
 
 
260
 
 
261
 
class CopyArchiveTargetPolicy(TargetPolicy):
262
 
    """Target policy for copy archives."""
263
 
 
264
 
    def getTargetArchives(self, distribution):
265
 
        """See `TargetPolicy`."""
266
 
        return getUtility(IArchiveSet).getArchivesForDistribution(
267
 
            distribution, purposes=[ArchivePurpose.COPY])
268
 
 
269
 
    def describeArchive(self, archive):
270
 
        """See `TargetPolicy`."""
271
 
        return archive.displayname
272
 
 
273
 
 
274
 
class DistroTargetPolicy(TargetPolicy):
275
 
    """Target policy for distro archives."""
276
 
 
277
 
    def getTargetArchives(self, distribution):
278
 
        """See `TargetPolicy`."""
279
 
        return distribution.all_distro_archives
280
 
 
281
 
    def describeArchive(self, archive):
282
 
        """See `TargetPolicy`."""
283
 
        return archive.purpose.title
284
 
 
285
 
    def postprocessSuccesses(self, queue_ids):
286
 
        """See `TargetPolicy`."""
287
 
        self.logger.debug("Closing bugs.")
288
 
        for queue_id in queue_ids:
289
 
            queue_item = getUtility(IPackageUploadSet).get(queue_id)
290
 
            close_bugs_for_queue_item(queue_item)
291
 
 
292
 
 
293
 
class ProcessAccepted(LaunchpadCronScript):
 
192
class ProcessAccepted(LaunchpadScript):
294
193
    """Queue/Accepted processor.
295
194
 
296
195
    Given a distribution to run on, obtains all the queue items for the
298
197
    them for publishing as appropriate.
299
198
    """
300
199
 
301
 
    @property
302
 
    def lockfilename(self):
303
 
        """See `LaunchpadScript`."""
304
 
        # Avoid circular imports.
305
 
        from lp.archivepublisher.publishing import GLOBAL_PUBLISHER_LOCK
306
 
        return GLOBAL_PUBLISHER_LOCK
307
 
 
308
200
    def add_my_options(self):
309
201
        """Command line options for this script."""
310
202
        self.parser.add_option(
313
205
            help="Whether to treat this as a dry-run or not.")
314
206
 
315
207
        self.parser.add_option(
316
 
            '-D', '--derived', action="store_true", dest="derived",
317
 
            default=False, help="Process all Ubuntu-derived distributions.")
318
 
 
319
 
        self.parser.add_option(
320
 
            "--ppa", action="store_true", dest="ppa", default=False,
 
208
            "--ppa", action="store_true",
 
209
            dest="ppa", metavar="PPA", default=False,
321
210
            help="Run only over PPA archives.")
322
211
 
323
212
        self.parser.add_option(
324
 
            "--copy-archives", action="store_true", dest="copy_archives",
 
213
            "--copy-archives", action="store_true",
 
214
            dest="copy_archives", metavar="COPY_ARCHIVES",
325
215
            default=False, help="Run only over COPY archives.")
326
216
 
327
 
    def _commit(self):
328
 
        """Commit transaction (unless in dry-run mode)."""
329
 
        if self.options.dryrun:
330
 
            self.logger.debug("Skipping commit: dry-run mode.")
331
 
        else:
332
 
            self.txn.commit()
333
 
 
334
 
    def findNamedDistro(self, distro_name):
335
 
        """Find the `Distribution` called `distro_name`."""
336
 
        self.logger.debug("Finding distribution %s.", distro_name)
337
 
        distro = getUtility(IDistributionSet).getByName(distro_name)
338
 
        if distro is None:
339
 
            raise LaunchpadScriptFailure(
340
 
                "Distribution '%s' not found." % distro_name)
341
 
        return distro
342
 
 
343
 
    def findTargetDistros(self):
344
 
        """Find the distribution(s) to process, based on arguments."""
345
 
        if self.options.derived:
346
 
            return getUtility(IDistributionSet).getDerivedDistributions()
347
 
        else:
348
 
            return [self.findNamedDistro(self.args[0])]
349
 
 
350
 
    def validateArguments(self):
351
 
        """Validate command-line arguments."""
 
217
    @property
 
218
    def lockfilename(self):
 
219
        """Override LaunchpadScript's lock file name."""
 
220
        return "/var/lock/launchpad-upload-queue.lock"
 
221
 
 
222
    def main(self):
 
223
        """Entry point for a LaunchpadScript."""
 
224
        if len(self.args) != 1:
 
225
            self.logger.error(
 
226
                "Need to be given exactly one non-option argument. "
 
227
                "Namely the distribution to process.")
 
228
            return 1
 
229
 
352
230
        if self.options.ppa and self.options.copy_archives:
353
 
            raise OptionValueError(
 
231
            self.logger.error(
354
232
                "Specify only one of copy archives or ppa archives.")
355
 
        if self.options.derived:
356
 
            if len(self.args) != 0:
357
 
                raise OptionValueError(
358
 
                    "Can't combine --derived with a distribution name.")
359
 
        else:
360
 
            if len(self.args) != 1:
361
 
                raise OptionValueError(
362
 
                    "Need to be given exactly one non-option argument. "
363
 
                    "Namely the distribution to process.")
364
 
 
365
 
    def makeTargetPolicy(self):
366
 
        """Pick and instantiate a `TargetPolicy` based on given options."""
367
 
        if self.options.ppa:
368
 
            policy_class = PPATargetPolicy
369
 
        elif self.options.copy_archives:
370
 
            policy_class = CopyArchiveTargetPolicy
371
 
        else:
372
 
            policy_class = DistroTargetPolicy
373
 
        return policy_class(self.logger)
374
 
 
375
 
    def processQueueItem(self, queue_item):
376
 
        """Attempt to process `queue_item`.
377
 
 
378
 
        This method swallows exceptions that occur while processing the
379
 
        item.
380
 
 
381
 
        :param queue_item: A `PackageUpload` to process.
382
 
        :return: True on success, or False on failure.
383
 
        """
384
 
        self.logger.debug("Processing queue item %d" % queue_item.id)
385
 
        try:
386
 
            queue_item.realiseUpload(self.logger)
387
 
        except Exception:
388
 
            message = "Failure processing queue_item %d" % queue_item.id
389
 
            properties = [('error-explanation', message)]
390
 
            request = ScriptRequest(properties)
391
 
            ErrorReportingUtility().raising(sys.exc_info(), request)
392
 
            self.logger.error('%s (%s)', message, request.oopsid)
393
 
            return False
394
 
        else:
395
 
            self.logger.debug(
396
 
                "Successfully processed queue item %d", queue_item.id)
397
 
            return True
398
 
 
399
 
    def processForDistro(self, distribution, target_policy):
400
 
        """Process all queue items for a distribution.
401
 
 
402
 
        Commits between items, except in dry-run mode.
403
 
 
404
 
        :param distribution: The `Distribution` to process queue items for.
405
 
        :param target_policy: The applicable `TargetPolicy`.
406
 
        :return: A list of all successfully processed items' ids.
407
 
        """
 
233
            return 1
 
234
 
 
235
        distro_name = self.args[0]
 
236
 
408
237
        processed_queue_ids = []
409
 
        for archive in target_policy.getTargetArchives(distribution):
410
 
            description = target_policy.describeArchive(archive)
411
 
            for distroseries in distribution.series:
412
 
 
413
 
                self.logger.debug("Processing queue for %s %s" % (
414
 
                    distroseries.name, description))
415
 
 
416
 
                queue_items = distroseries.getPackageUploads(
417
 
                    status=PackageUploadStatus.ACCEPTED, archive=archive)
418
 
                for queue_item in queue_items:
419
 
                    if self.processQueueItem(queue_item):
420
 
                        processed_queue_ids.append(queue_item.id)
421
 
                    # Commit even on error; we may have altered the
422
 
                    # on-disk archive, so the partial state must
423
 
                    # make it to the DB.
424
 
                    self._commit()
425
 
        return processed_queue_ids
426
 
 
427
 
    def main(self):
428
 
        """Entry point for a LaunchpadScript."""
429
 
        self.validateArguments()
430
 
        target_policy = self.makeTargetPolicy()
431
238
        try:
432
 
            for distro in self.findTargetDistros():
433
 
                queue_ids = self.processForDistro(distro, target_policy)
434
 
                self._commit()
435
 
                target_policy.postprocessSuccesses(queue_ids)
436
 
                self._commit()
 
239
            self.logger.debug("Finding distribution %s." % distro_name)
 
240
            distribution = getUtility(IDistributionSet).getByName(distro_name)
 
241
            if distribution is None:
 
242
                raise LaunchpadScriptFailure(
 
243
                    "Distribution '%s' not found." % distro_name)
 
244
 
 
245
            # target_archives is a tuple of (archive, description).
 
246
            if self.options.ppa:
 
247
                target_archives = [
 
248
                    (archive, archive.archive_url)
 
249
                    for archive in distribution.getPendingAcceptancePPAs()]
 
250
            elif self.options.copy_archives:
 
251
                copy_archives = getUtility(
 
252
                    IArchiveSet).getArchivesForDistribution(
 
253
                        distribution, purposes=[ArchivePurpose.COPY])
 
254
                target_archives = [
 
255
                    (archive, archive.displayname)
 
256
                    for archive in copy_archives]
 
257
            else:
 
258
                target_archives = [
 
259
                    (archive, archive.purpose.title)
 
260
                    for archive in distribution.all_distro_archives]
 
261
 
 
262
            for archive, description in target_archives:
 
263
                for distroseries in distribution.series:
 
264
 
 
265
                    self.logger.debug("Processing queue for %s %s" % (
 
266
                        distroseries.name, description))
 
267
 
 
268
                    queue_items = distroseries.getPackageUploads(
 
269
                        status=PackageUploadStatus.ACCEPTED, archive=archive)
 
270
                    for queue_item in queue_items:
 
271
                        self.logger.debug(
 
272
                            "Processing queue item %d" % queue_item.id)
 
273
                        try:
 
274
                            queue_item.realiseUpload(self.logger)
 
275
                        except Exception:
 
276
                            message = "Failure processing queue_item %d" % (
 
277
                                queue_item.id)
 
278
                            properties = [('error-explanation', message)]
 
279
                            request = ScriptRequest(properties)
 
280
                            error_utility = ErrorReportingUtility()
 
281
                            error_utility.raising(sys.exc_info(), request)
 
282
                            self.logger.error('%s (%s)' % (message,
 
283
                                request.oopsid))
 
284
                        else:
 
285
                            self.logger.debug(
 
286
                                "Successfully processed queue item %d" %
 
287
                                queue_item.id)
 
288
                            processed_queue_ids.append(queue_item.id)
 
289
                        # Commit even on error; we may have altered the
 
290
                        # on-disk archive, so the partial state must
 
291
                        # make it to the DB.
 
292
                        self.txn.commit()
 
293
 
 
294
            if not self.options.dryrun:
 
295
                self.txn.commit()
 
296
            else:
 
297
                self.logger.debug("Dry Run mode.")
 
298
 
 
299
            if not self.options.ppa and not self.options.copy_archives:
 
300
                self.logger.debug("Closing bugs.")
 
301
                close_bugs(processed_queue_ids)
 
302
 
 
303
            if not self.options.dryrun:
 
304
                self.txn.commit()
437
305
 
438
306
        finally:
439
307
            self.logger.debug("Rolling back any remaining transactions.")