~launchpad-pqm/launchpad/devel

« back to all changes in this revision

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

  • Committer: Canonical.com Patch Queue Manager
  • Date: 2004-08-03 09:17:25 UTC
  • mfrom: (unknown (missing))
  • Revision ID: Arch-1:rocketfuel@canonical.com%launchpad--devel--0--patch-19
Removed defaultSkin directive to make launchpad work with the latest zope.
Patches applied:

 * steve.alexander@canonical.com/launchpad--devel--0--patch-16
   merge from rocketfuel

 * steve.alexander@canonical.com/launchpad--devel--0--patch-17
   removed use of defaultSkin directive, which has been removed from zope3

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# Copyright 2009-2011 Canonical Ltd.  This software is licensed under the
2
 
# GNU Affero General Public License version 3 (see the file LICENSE).
3
 
 
4
 
"""FTPMaster utilities."""
5
 
 
6
 
__metaclass__ = type
7
 
 
8
 
__all__ = [
9
 
    'ChrootManager',
10
 
    'ChrootManagerError',
11
 
    'LpQueryDistro',
12
 
    'ManageChrootScript',
13
 
    'ObsoleteDistroseries',
14
 
    'PackageRemover',
15
 
    'PubSourceChecker',
16
 
    'SyncSource',
17
 
    'SyncSourceError',
18
 
    ]
19
 
 
20
 
import hashlib
21
 
from itertools import chain
22
 
import os
23
 
import stat
24
 
import sys
25
 
import time
26
 
 
27
 
from debian.deb822 import Changes
28
 
from zope.component import getUtility
29
 
 
30
 
from canonical.database.constants import UTC_NOW
31
 
from canonical.launchpad.helpers import filenameToContentType
32
 
from canonical.launchpad.interfaces.librarian import ILibraryFileAliasSet
33
 
from canonical.librarian.interfaces import (
34
 
    ILibrarianClient,
35
 
    UploadFailed,
36
 
    )
37
 
from canonical.librarian.utils import copy_and_close
38
 
from lp.app.errors import NotFoundError
39
 
from lp.archiveuploader.utils import determine_source_file_type
40
 
from lp.registry.interfaces.person import IPersonSet
41
 
from lp.registry.interfaces.pocket import pocketsuffix
42
 
from lp.registry.interfaces.series import SeriesStatus
43
 
from lp.registry.interfaces.sourcepackage import SourcePackageFileType
44
 
from lp.services.browser_helpers import get_plural_text
45
 
from lp.services.scripts.base import (
46
 
    LaunchpadScript,
47
 
    LaunchpadScriptFailure,
48
 
    )
49
 
from lp.soyuz.adapters.packagelocation import (
50
 
    build_package_location,
51
 
    PackageLocationError,
52
 
    )
53
 
from lp.soyuz.enums import PackagePublishingStatus
54
 
from lp.soyuz.scripts.ftpmasterbase import (
55
 
    SoyuzScript,
56
 
    SoyuzScriptError,
57
 
    )
58
 
 
59
 
 
60
 
class PubBinaryContent:
61
 
    """Binary publication container.
62
 
 
63
 
    Currently used for auxiliary storage in PubSourceChecker.
64
 
    """
65
 
 
66
 
    def __init__(self, name, version, arch, component, section, priority):
67
 
        self.name = name
68
 
        self.version = version
69
 
        self.arch = arch
70
 
        self.component = component
71
 
        self.section = section
72
 
        self.priority = priority
73
 
        self.messages = []
74
 
 
75
 
    def warn(self, message):
76
 
        """Append a warning in the message list."""
77
 
        self.messages.append('W: %s' % message)
78
 
 
79
 
    def error(self, message):
80
 
        """Append a error in the message list."""
81
 
        self.messages.append('E: %s' % message)
82
 
 
83
 
    def renderReport(self):
84
 
        """Render a report with the appended messages (self.messages).
85
 
 
86
 
        Return None if no message was found, otherwise return
87
 
        a properly formatted string, including
88
 
 
89
 
        <TAB>BinaryName_Version Arch Component/Section/Priority
90
 
        <TAB><TAB>MESSAGE
91
 
        """
92
 
        if not len(self.messages):
93
 
            return
94
 
 
95
 
        report = [('\t%s_%s %s %s/%s/%s'
96
 
                   % (self.name, self.version, self.arch,
97
 
                      self.component, self.section, self.priority))]
98
 
 
99
 
        for message in self.messages:
100
 
            report.append('\t\t%s' % message)
101
 
 
102
 
        return "\n".join(report)
103
 
 
104
 
 
105
 
class PubBinaryDetails:
106
 
    """Store the component, section and priority of binary packages and, for
107
 
    each binary package the most frequent component, section and priority.
108
 
 
109
 
    These are stored in the following attributes:
110
 
 
111
 
    - components: A dictionary mapping binary package names to other
112
 
      dictionaries mapping component names to binary packages published
113
 
      in this component.
114
 
    - sections: The same as components, but for sections.
115
 
    - priorities: The same as components, but for priorities.
116
 
    - correct_components: a dictionary mapping binary package name
117
 
      to the most frequent (considered the correct) component name.
118
 
    - correct_sections: same as correct_components, but for sections
119
 
    - correct_priorities: same as correct_components, but for priorities
120
 
    """
121
 
 
122
 
    def __init__(self):
123
 
        self.components = {}
124
 
        self.sections = {}
125
 
        self.priorities = {}
126
 
        self.correct_components = {}
127
 
        self.correct_sections = {}
128
 
        self.correct_priorities = {}
129
 
 
130
 
    def addBinaryDetails(self, bin):
131
 
        """Include a binary publication and update internal registers."""
132
 
        name_components = self.components.setdefault(bin.name, {})
133
 
        bin_component = name_components.setdefault(bin.component, [])
134
 
        bin_component.append(bin)
135
 
 
136
 
        name_sections = self.sections.setdefault(bin.name, {})
137
 
        bin_section = name_sections.setdefault(bin.section, [])
138
 
        bin_section.append(bin)
139
 
 
140
 
        name_priorities = self.priorities.setdefault(bin.name, {})
141
 
        bin_priority = name_priorities.setdefault(bin.priority, [])
142
 
        bin_priority.append(bin)
143
 
 
144
 
    def _getMostFrequentValue(self, data):
145
 
        """Return a dict of name and the most frequent value.
146
 
 
147
 
        Used for self.{components, sections, priorities}
148
 
        """
149
 
        results = {}
150
 
 
151
 
        for name, items in data.iteritems():
152
 
            highest = 0
153
 
            for item, occurrences in items.iteritems():
154
 
                if len(occurrences) > highest:
155
 
                    highest = len(occurrences)
156
 
                    results[name] = item
157
 
 
158
 
        return results
159
 
 
160
 
    def setCorrectValues(self):
161
 
        """Find out the correct values for the same binary name
162
 
 
163
 
        Consider correct the most frequent.
164
 
        """
165
 
        self.correct_components = self._getMostFrequentValue(self.components)
166
 
        self.correct_sections = self._getMostFrequentValue(self.sections)
167
 
        self.correct_priorities = self._getMostFrequentValue(self.priorities)
168
 
 
169
 
 
170
 
class PubSourceChecker:
171
 
    """Map and probe a Source/Binaries publication couple.
172
 
 
173
 
    Receive the source publication data and its binaries and perform
174
 
    a group of heuristic consistency checks.
175
 
    """
176
 
 
177
 
    def __init__(self, name, version, component, section, urgency):
178
 
        self.name = name
179
 
        self.version = version
180
 
        self.component = component
181
 
        self.section = section
182
 
        self.urgency = urgency
183
 
        self.binaries = []
184
 
        self.binaries_details = PubBinaryDetails()
185
 
 
186
 
    def addBinary(self, name, version, architecture, component, section,
187
 
                  priority):
188
 
        """Append the binary data to the current publication list."""
189
 
        bin = PubBinaryContent(
190
 
            name, version, architecture, component, section, priority)
191
 
 
192
 
        self.binaries.append(bin)
193
 
 
194
 
        self.binaries_details.addBinaryDetails(bin)
195
 
 
196
 
    def check(self):
197
 
        """Setup check environment and perform the required checks."""
198
 
        self.binaries_details.setCorrectValues()
199
 
 
200
 
        for bin in self.binaries:
201
 
            self._checkComponent(bin)
202
 
            self._checkSection(bin)
203
 
            self._checkPriority(bin)
204
 
 
205
 
    def _checkComponent(self, bin):
206
 
        """Check if the binary component matches the correct component.
207
 
 
208
 
        'correct' is the most frequent component in this binary package
209
 
        group
210
 
        """
211
 
        correct_component = self.binaries_details.correct_components[bin.name]
212
 
        if bin.component != correct_component:
213
 
            bin.warn('Component mismatch: %s != %s'
214
 
                     % (bin.component, correct_component))
215
 
 
216
 
    def _checkSection(self, bin):
217
 
        """Check if the binary section matches the correct section.
218
 
 
219
 
        'correct' is the most frequent section in this binary package
220
 
        group
221
 
        """
222
 
        correct_section = self.binaries_details.correct_sections[bin.name]
223
 
        if bin.section != correct_section:
224
 
            bin.warn('Section mismatch: %s != %s'
225
 
                     % (bin.section, correct_section))
226
 
 
227
 
    def _checkPriority(self, bin):
228
 
        """Check if the binary priority matches the correct priority.
229
 
 
230
 
        'correct' is the most frequent priority in this binary package
231
 
        group
232
 
        """
233
 
        correct_priority = self.binaries_details.correct_priorities[bin.name]
234
 
        if bin.priority != correct_priority:
235
 
            bin.warn('Priority mismatch: %s != %s'
236
 
                     % (bin.priority, correct_priority))
237
 
 
238
 
    def renderReport(self):
239
 
        """Render a formatted report for the publication group.
240
 
 
241
 
        Return None if no issue was annotated or an formatted string
242
 
        including:
243
 
 
244
 
          SourceName_Version Component/Section/Urgency | # bin
245
 
          <BINREPORTS>
246
 
        """
247
 
        report = []
248
 
 
249
 
        for bin in self.binaries:
250
 
            bin_report = bin.renderReport()
251
 
            if bin_report:
252
 
                report.append(bin_report)
253
 
 
254
 
        if not len(report):
255
 
            return
256
 
 
257
 
        result = [('%s_%s %s/%s/%s | %s bin'
258
 
                   % (self.name, self.version, self.component,
259
 
                      self.section, self.urgency, len(self.binaries)))]
260
 
 
261
 
        result.extend(report)
262
 
 
263
 
        return "\n".join(result)
264
 
 
265
 
 
266
 
class ChrootManagerError(Exception):
267
 
    """Any error generated during the ChrootManager procedures."""
268
 
 
269
 
 
270
 
class ChrootManager:
271
 
    """Chroot actions wrapper.
272
 
 
273
 
    The 'distroarchseries' argument is mandatory and 'filepath' is
274
 
    optional.
275
 
 
276
 
    'filepath' is required by some allowed actions as source or destination,
277
 
 
278
 
    ChrootManagerError will be raised if anything wrong occurred in this
279
 
    class, things like missing parameter or infrastructure pieces not in
280
 
    place.
281
 
    """
282
 
 
283
 
    allowed_actions = ['add', 'update', 'remove', 'get']
284
 
 
285
 
    def __init__(self, distroarchseries, filepath=None):
286
 
        self.distroarchseries = distroarchseries
287
 
        self.filepath = filepath
288
 
        self._messages = []
289
 
 
290
 
    def _upload(self):
291
 
        """Upload the self.filepath contents to Librarian.
292
 
 
293
 
        Return the respective ILibraryFileAlias instance.
294
 
        Raises ChrootManagerError if it could not be found.
295
 
        """
296
 
        try:
297
 
            fd = open(self.filepath)
298
 
        except IOError:
299
 
            raise ChrootManagerError('Could not open: %s' % self.filepath)
300
 
 
301
 
        flen = os.stat(self.filepath).st_size
302
 
        filename = os.path.basename(self.filepath)
303
 
        ftype = filenameToContentType(filename)
304
 
 
305
 
        try:
306
 
            alias_id = getUtility(ILibrarianClient).addFile(
307
 
                filename, flen, fd, contentType=ftype)
308
 
        except UploadFailed, info:
309
 
            raise ChrootManagerError("Librarian upload failed: %s" % info)
310
 
 
311
 
        lfa = getUtility(ILibraryFileAliasSet)[alias_id]
312
 
 
313
 
        self._messages.append(
314
 
            "LibraryFileAlias: %d, %s bytes, %s"
315
 
            % (lfa.id, lfa.content.filesize, lfa.content.md5))
316
 
 
317
 
        return lfa
318
 
 
319
 
    def _getPocketChroot(self):
320
 
        """Retrive PocketChroot record.
321
 
 
322
 
        Return the respective IPocketChroot instance.
323
 
        Raises ChrootManagerError if it could not be found.
324
 
        """
325
 
        pocket_chroot = self.distroarchseries.getPocketChroot()
326
 
        if pocket_chroot is None:
327
 
            raise ChrootManagerError(
328
 
                'Could not find chroot for %s'
329
 
                % (self.distroarchseries.title))
330
 
 
331
 
        self._messages.append(
332
 
            "PocketChroot for '%s' (%d) retrieved."
333
 
            % (pocket_chroot.distroarchseries.title, pocket_chroot.id))
334
 
 
335
 
        return pocket_chroot
336
 
 
337
 
    def _update(self):
338
 
        """Base method for add and update action."""
339
 
        if self.filepath is None:
340
 
            raise ChrootManagerError('Missing local chroot file path.')
341
 
        alias = self._upload()
342
 
        return self.distroarchseries.addOrUpdateChroot(alias)
343
 
 
344
 
    def add(self):
345
 
        """Create a new PocketChroot record.
346
 
 
347
 
        Raises ChrootManagerError if self.filepath isn't set.
348
 
        Update of pre-existing PocketChroot record will be automatically
349
 
        handled.
350
 
        It's a bind to the self.update method.
351
 
        """
352
 
        pocket_chroot = self._update()
353
 
        self._messages.append(
354
 
            "PocketChroot for '%s' (%d) added."
355
 
            % (pocket_chroot.distroarchseries.title, pocket_chroot.id))
356
 
 
357
 
    def update(self):
358
 
        """Update a PocketChroot record.
359
 
 
360
 
        Raises ChrootManagerError if filepath isn't set
361
 
        Creation of non-existing PocketChroot records will be automatically
362
 
        handled.
363
 
        """
364
 
        pocket_chroot = self._update()
365
 
        self._messages.append(
366
 
            "PocketChroot for '%s' (%d) updated."
367
 
            % (pocket_chroot.distroarchseries.title, pocket_chroot.id))
368
 
 
369
 
    def remove(self):
370
 
        """Overwrite existing PocketChroot file to none.
371
 
 
372
 
        Raises ChrootManagerError if the chroot record isn't found.
373
 
        """
374
 
        pocket_chroot = self._getPocketChroot()
375
 
        self.distroarchseries.addOrUpdateChroot(None)
376
 
        self._messages.append(
377
 
            "PocketChroot for '%s' (%d) removed."
378
 
            % (pocket_chroot.distroarchseries.title, pocket_chroot.id))
379
 
 
380
 
    def get(self):
381
 
        """Download chroot file from Librarian and store."""
382
 
        pocket_chroot = self._getPocketChroot()
383
 
 
384
 
        if self.filepath is None:
385
 
            abs_filepath = os.path.abspath(pocket_chroot.chroot.filename)
386
 
            if os.path.exists(abs_filepath):
387
 
                raise ChrootManagerError(
388
 
                    'cannot overwrite %s' % abs_filepath)
389
 
            self._messages.append(
390
 
                "Writing to '%s'." % abs_filepath)
391
 
            local_file = open(pocket_chroot.chroot.filename, "w")
392
 
        else:
393
 
            abs_filepath = os.path.abspath(self.filepath)
394
 
            if os.path.exists(abs_filepath):
395
 
                raise ChrootManagerError(
396
 
                    'cannot overwrite %s' % abs_filepath)
397
 
            self._messages.append(
398
 
                "Writing to '%s'." % abs_filepath)
399
 
            local_file = open(abs_filepath, "w")
400
 
 
401
 
        if pocket_chroot.chroot is None:
402
 
            raise ChrootManagerError('Chroot was deleted.')
403
 
 
404
 
        pocket_chroot.chroot.open()
405
 
        copy_and_close(pocket_chroot.chroot, local_file)
406
 
 
407
 
 
408
 
class SyncSourceError(Exception):
409
 
    """Raised when an critical error occurs inside SyncSource.
410
 
 
411
 
    The entire procedure should be aborted in order to avoid unknown problems.
412
 
    """
413
 
 
414
 
 
415
 
class SyncSource:
416
 
    """Sync Source procedure helper class.
417
 
 
418
 
    It provides the backend for retrieving files from Librarian or the
419
 
    'sync source' location. Also provides a method to check the downloaded
420
 
    files integrity.
421
 
    'aptMD5Sum' is provided as a classmethod during the integration time.
422
 
    """
423
 
 
424
 
    def __init__(self, files, origin, logger, downloader, todistro):
425
 
        """Store local context.
426
 
 
427
 
        files: a dictionary where the keys are the filename and the
428
 
               value another dictionary with the file informations.
429
 
        origin: a dictionary similar to 'files' but where the values
430
 
                contain information for download files to be synchronized
431
 
        logger: a logger
432
 
        downloader: a callable that fetchs URLs,
433
 
                    'downloader(url, destination)'
434
 
        todistro: target distribution object
435
 
        """
436
 
        self.files = files
437
 
        self.origin = origin
438
 
        self.logger = logger
439
 
        self.downloader = downloader
440
 
        self.todistro = todistro
441
 
 
442
 
    @classmethod
443
 
    def generateMD5Sum(self, filename):
444
 
        file_handle = open(filename)
445
 
        md5sum = hashlib.md5(file_handle.read()).hexdigest()
446
 
        file_handle.close()
447
 
        return md5sum
448
 
 
449
 
    def fetchFileFromLibrarian(self, filename):
450
 
        """Fetch file from librarian.
451
 
 
452
 
        Store the contents in local path with the original filename.
453
 
        Return the fetched filename if it was present in Librarian or None
454
 
        if it wasn't.
455
 
        """
456
 
        try:
457
 
            libraryfilealias = self.todistro.main_archive.getFileByName(
458
 
                filename)
459
 
        except NotFoundError:
460
 
            return None
461
 
 
462
 
        self.logger.info(
463
 
            "%s: already in distro - downloading from librarian" %
464
 
            filename)
465
 
 
466
 
        output_file = open(filename, 'w')
467
 
        libraryfilealias.open()
468
 
        copy_and_close(libraryfilealias, output_file)
469
 
        return filename
470
 
 
471
 
    def fetchLibrarianFiles(self):
472
 
        """Try to fetch files from Librarian.
473
 
 
474
 
        It raises SyncSourceError if anything else then an
475
 
        orig tarball was found in Librarian.
476
 
        Return the names of the files retrieved from the librarian.
477
 
        """
478
 
        retrieved = []
479
 
        for filename in self.files.keys():
480
 
            if not self.fetchFileFromLibrarian(filename):
481
 
                continue
482
 
            file_type = determine_source_file_type(filename)
483
 
            # set the return code if an orig was, in fact,
484
 
            # fetched from Librarian
485
 
            orig_types = (
486
 
                SourcePackageFileType.ORIG_TARBALL,
487
 
                SourcePackageFileType.COMPONENT_ORIG_TARBALL)
488
 
            if file_type not in orig_types:
489
 
                raise SyncSourceError(
490
 
                    'Oops, only orig tarball can be retrieved from '
491
 
                    'librarian.')
492
 
            retrieved.append(filename)
493
 
 
494
 
        return retrieved
495
 
 
496
 
    def fetchSyncFiles(self):
497
 
        """Fetch files from the original sync source.
498
 
 
499
 
        Return DSC filename, which should always come via this path.
500
 
        """
501
 
        dsc_filename = None
502
 
        for filename in self.files.keys():
503
 
            file_type = determine_source_file_type(filename)
504
 
            if file_type == SourcePackageFileType.DSC:
505
 
                dsc_filename = filename
506
 
            if os.path.exists(filename):
507
 
                self.logger.info("  - <%s: cached>" % (filename))
508
 
                continue
509
 
            self.logger.info(
510
 
                "  - <%s: downloading from %s>" %
511
 
                (filename, self.origin["url"]))
512
 
            download_f = ("%s%s" % (self.origin["url"],
513
 
                                    self.files[filename]["remote filename"]))
514
 
            sys.stdout.flush()
515
 
            self.downloader(download_f, filename)
516
 
        return dsc_filename
517
 
 
518
 
    def checkDownloadedFiles(self):
519
 
        """Check md5sum and size match Source.
520
 
 
521
 
        If anything fails SyncSourceError will be raised.
522
 
        """
523
 
        for filename in self.files.keys():
524
 
            actual_md5sum = self.generateMD5Sum(filename)
525
 
            expected_md5sum = self.files[filename]["md5sum"]
526
 
            if actual_md5sum != expected_md5sum:
527
 
                raise SyncSourceError(
528
 
                    "%s: md5sum check failed (%s [actual] "
529
 
                    "vs. %s [expected])."
530
 
                    % (filename, actual_md5sum, expected_md5sum))
531
 
 
532
 
            actual_size = os.stat(filename)[stat.ST_SIZE]
533
 
            expected_size = int(self.files[filename]["size"])
534
 
            if actual_size != expected_size:
535
 
                raise SyncSourceError(
536
 
                    "%s: size mismatch (%s [actual] vs. %s [expected])."
537
 
                    % (filename, actual_size, expected_size))
538
 
 
539
 
 
540
 
class LpQueryDistro(LaunchpadScript):
541
 
    """Main class for scripts/ftpmaster-tools/lp-query-distro.py."""
542
 
 
543
 
    def __init__(self, *args, **kwargs):
544
 
        """Initialize dynamic 'usage' message and LaunchpadScript parent.
545
 
 
546
 
        Also initialize the list 'allowed_arguments'.
547
 
        """
548
 
        self.allowed_actions = [
549
 
            'current', 'development', 'supported', 'pending_suites', 'archs',
550
 
            'official_archs', 'nominated_arch_indep', 'pocket_suffixes']
551
 
        self.usage = '%%prog <%s>' % ' | '.join(self.allowed_actions)
552
 
        LaunchpadScript.__init__(self, *args, **kwargs)
553
 
 
554
 
    def add_my_options(self):
555
 
        """Add 'distribution' and 'suite' context options."""
556
 
        self.parser.add_option(
557
 
            '-d', '--distribution', dest='distribution_name',
558
 
            default='ubuntu', help='Context distribution name.')
559
 
        self.parser.add_option(
560
 
            '-s', '--suite', dest='suite', default=None,
561
 
            help='Context suite name.')
562
 
 
563
 
    def main(self):
564
 
        """Main procedure, basically a runAction wrapper.
565
 
 
566
 
        Execute the given and allowed action using the default presenter
567
 
        (see self.runAction for further information).
568
 
        """
569
 
        self.runAction()
570
 
 
571
 
    def _buildLocation(self):
572
 
        """Build a PackageLocation object
573
 
 
574
 
        The location will correspond to the given 'distribution' and 'suite',
575
 
        Any PackageLocationError occurring at this point will be masked into
576
 
        LaunchpadScriptFailure.
577
 
        """
578
 
        try:
579
 
            self.location = build_package_location(
580
 
                distribution_name=self.options.distribution_name,
581
 
                suite=self.options.suite)
582
 
        except PackageLocationError, err:
583
 
            raise LaunchpadScriptFailure(err)
584
 
 
585
 
    def defaultPresenter(self, result):
586
 
        """Default result presenter.
587
 
 
588
 
        Directly prints result in the standard output (print).
589
 
        """
590
 
        print result
591
 
 
592
 
    def runAction(self, presenter=None):
593
 
        """Run a given initialized action (self.action_name).
594
 
 
595
 
        It accepts an optional 'presenter' which will be used to
596
 
        store/present the action result.
597
 
 
598
 
        Ensure at least one argument was passed, known as 'action'.
599
 
        Verify if the given 'action' is listed as an 'allowed_action'.
600
 
        Raise LaunchpadScriptFailure if those requirements were not
601
 
        accomplished.
602
 
 
603
 
        It builds context 'location' object (see self._buildLocation).
604
 
 
605
 
        It may raise LaunchpadScriptFailure is the 'action' is not properly
606
 
        supported by the current code (missing corresponding property).
607
 
        """
608
 
        if presenter is None:
609
 
            presenter = self.defaultPresenter
610
 
 
611
 
        if len(self.args) != 1:
612
 
            raise LaunchpadScriptFailure('<action> is required')
613
 
 
614
 
        [self.action_name] = self.args
615
 
 
616
 
        if self.action_name not in self.allowed_actions:
617
 
            raise LaunchpadScriptFailure(
618
 
                'Action "%s" is not supported' % self.action_name)
619
 
 
620
 
        self._buildLocation()
621
 
 
622
 
        try:
623
 
            action_result = getattr(self, self.action_name)
624
 
        except AttributeError:
625
 
            raise AssertionError(
626
 
                "No handler found for action '%s'" % self.action_name)
627
 
 
628
 
        presenter(action_result)
629
 
 
630
 
    def checkNoSuiteDefined(self):
631
 
        """Raises LaunchpadScriptError if a suite location was passed.
632
 
 
633
 
        It is re-used in action properties to avoid conflicting contexts,
634
 
        i.e, passing an arbitrary 'suite' and asking for the CURRENT suite
635
 
        in the context distribution.
636
 
        """
637
 
        if self.options.suite is not None:
638
 
            raise LaunchpadScriptFailure(
639
 
                "Action does not accept defined suite.")
640
 
 
641
 
    @property
642
 
    def current(self):
643
 
        """Return the name of the CURRENT distroseries.
644
 
 
645
 
        It is restricted for the context distribution.
646
 
 
647
 
        It may raise LaunchpadScriptFailure if a suite was passed on the
648
 
        command-line or if not CURRENT distroseries was found.
649
 
        """
650
 
        self.checkNoSuiteDefined()
651
 
        series = self.location.distribution.getSeriesByStatus(
652
 
            SeriesStatus.CURRENT)
653
 
        if not series:
654
 
            raise LaunchpadScriptFailure("No CURRENT series.")
655
 
 
656
 
        return series[0].name
657
 
 
658
 
    @property
659
 
    def development(self):
660
 
        """Return the name of the DEVELOPMENT distroseries.
661
 
 
662
 
        It is restricted for the context distribution.
663
 
 
664
 
        It may raise `LaunchpadScriptFailure` if a suite was passed on the
665
 
        command-line.
666
 
 
667
 
        Return the first FROZEN distroseries found if there is no
668
 
        DEVELOPMENT one available.
669
 
 
670
 
        Raises `NotFoundError` if neither a CURRENT nor a FROZEN
671
 
        candidate could be found.
672
 
        """
673
 
        self.checkNoSuiteDefined()
674
 
        series = None
675
 
        wanted_status = (SeriesStatus.DEVELOPMENT,
676
 
                         SeriesStatus.FROZEN)
677
 
        for status in wanted_status:
678
 
            series = self.location.distribution.getSeriesByStatus(status)
679
 
            if series.count() > 0:
680
 
                break
681
 
        else:
682
 
            raise LaunchpadScriptFailure(
683
 
                'There is no DEVELOPMENT distroseries for %s' %
684
 
                self.location.distribution.name)
685
 
        return series[0].name
686
 
 
687
 
    @property
688
 
    def supported(self):
689
 
        """Return the names of the distroseries currently supported.
690
 
 
691
 
        'supported' means not EXPERIMENTAL or OBSOLETE.
692
 
 
693
 
        It is restricted for the context distribution.
694
 
 
695
 
        It may raise `LaunchpadScriptFailure` if a suite was passed on the
696
 
        command-line or if there is not supported distroseries for the
697
 
        distribution given.
698
 
 
699
 
        Return a space-separated list of distroseries names.
700
 
        """
701
 
        self.checkNoSuiteDefined()
702
 
        supported_series = []
703
 
        unsupported_status = (SeriesStatus.EXPERIMENTAL,
704
 
                              SeriesStatus.OBSOLETE)
705
 
        for distroseries in self.location.distribution:
706
 
            if distroseries.status not in unsupported_status:
707
 
                supported_series.append(distroseries.name)
708
 
 
709
 
        if not supported_series:
710
 
            raise LaunchpadScriptFailure(
711
 
                'There is no supported distroseries for %s' %
712
 
                self.location.distribution.name)
713
 
 
714
 
        return " ".join(supported_series)
715
 
 
716
 
    @property
717
 
    def pending_suites(self):
718
 
        """Return the suite names containing PENDING publication.
719
 
 
720
 
        It check for sources and/or binary publications.
721
 
        """
722
 
        self.checkNoSuiteDefined()
723
 
        pending_suites = set()
724
 
        pending_sources = self.location.archive.getPublishedSources(
725
 
            status=PackagePublishingStatus.PENDING)
726
 
        for pub in pending_sources:
727
 
            pending_suites.add((pub.distroseries, pub.pocket))
728
 
 
729
 
        pending_binaries = self.location.archive.getAllPublishedBinaries(
730
 
            status=PackagePublishingStatus.PENDING)
731
 
        for pub in pending_binaries:
732
 
            pending_suites.add(
733
 
                (pub.distroarchseries.distroseries, pub.pocket))
734
 
 
735
 
        return " ".join([distroseries.name + pocketsuffix[pocket]
736
 
                         for distroseries, pocket in pending_suites])
737
 
 
738
 
    @property
739
 
    def archs(self):
740
 
        """Return a space-separated list of architecture tags.
741
 
 
742
 
        It is restricted for the context distribution and suite.
743
 
        """
744
 
        architectures = self.location.distroseries.architectures
745
 
        return " ".join(arch.architecturetag for arch in architectures)
746
 
 
747
 
    @property
748
 
    def official_archs(self):
749
 
        """Return a space-separated list of official architecture tags.
750
 
 
751
 
        It is restricted to the context distribution and suite.
752
 
        """
753
 
        architectures = self.location.distroseries.architectures
754
 
        return " ".join(arch.architecturetag
755
 
                        for arch in architectures
756
 
                        if arch.official)
757
 
 
758
 
    @property
759
 
    def nominated_arch_indep(self):
760
 
        """Return the nominated arch indep architecture tag.
761
 
 
762
 
        It is restricted to the context distribution and suite.
763
 
        """
764
 
        series = self.location.distroseries
765
 
        return series.nominatedarchindep.architecturetag
766
 
 
767
 
    @property
768
 
    def pocket_suffixes(self):
769
 
        """Return a space-separated list of non-empty pocket suffixes.
770
 
 
771
 
        The RELEASE pocket (whose suffix is the empty string) is omitted.
772
 
 
773
 
        The returned space-separated string is ordered alphabetically.
774
 
        """
775
 
        sorted_non_empty_suffixes = sorted(
776
 
            suffix for suffix in pocketsuffix.values() if suffix != '')
777
 
        return " ".join(sorted_non_empty_suffixes)
778
 
 
779
 
 
780
 
class PackageRemover(SoyuzScript):
781
 
    """SoyuzScript implementation for published package removal.."""
782
 
 
783
 
    usage = '%prog -s warty mozilla-firefox'
784
 
    description = 'REMOVE a published package.'
785
 
    success_message = (
786
 
        "The archive will be updated in the next publishing cycle.")
787
 
 
788
 
    def add_my_options(self):
789
 
        """Adding local options."""
790
 
        # XXX cprov 20071025: we need a hook for loading SoyuzScript default
791
 
        # options automatically. This is ugly.
792
 
        SoyuzScript.add_my_options(self)
793
 
 
794
 
        # Mode options.
795
 
        self.parser.add_option("-b", "--binary", dest="binaryonly",
796
 
                               default=False, action="store_true",
797
 
                               help="Remove binaries only.")
798
 
        self.parser.add_option("-S", "--source-only", dest="sourceonly",
799
 
                               default=False, action="store_true",
800
 
                               help="Remove source only.")
801
 
 
802
 
        # Removal information options.
803
 
        self.parser.add_option("-u", "--user", dest="user",
804
 
                               help="Launchpad user name.")
805
 
        self.parser.add_option("-m", "--removal_comment",
806
 
                               dest="removal_comment",
807
 
                               help="Removal comment")
808
 
 
809
 
    def mainTask(self):
810
 
        """Execute the package removal task.
811
 
 
812
 
        Build location and target objects.
813
 
 
814
 
        Can raise SoyuzScriptError.
815
 
        """
816
 
        if len(self.args) == 0:
817
 
            raise SoyuzScriptError(
818
 
                "At least one non-option argument must be given, "
819
 
                "a package name to be removed.")
820
 
 
821
 
        if self.options.user is None:
822
 
            raise SoyuzScriptError("Launchpad username must be given.")
823
 
 
824
 
        if self.options.removal_comment is None:
825
 
            raise SoyuzScriptError("Removal comment must be given.")
826
 
 
827
 
        removed_by = getUtility(IPersonSet).getByName(self.options.user)
828
 
        if removed_by is None:
829
 
            raise SoyuzScriptError(
830
 
                "Invalid launchpad usename: %s" % self.options.user)
831
 
 
832
 
        removables = []
833
 
        for packagename in self.args:
834
 
            if self.options.binaryonly:
835
 
                removables.extend(
836
 
                    self.findLatestPublishedBinaries(packagename))
837
 
            elif self.options.sourceonly:
838
 
                removables.append(self.findLatestPublishedSource(packagename))
839
 
            else:
840
 
                source_pub = self.findLatestPublishedSource(packagename)
841
 
                removables.append(source_pub)
842
 
                removables.extend(source_pub.getPublishedBinaries())
843
 
 
844
 
        self.logger.info("Removing candidates:")
845
 
        for removable in removables:
846
 
            self.logger.info('\t%s', removable.displayname)
847
 
 
848
 
        self.logger.info("Removed-by: %s", removed_by.displayname)
849
 
        self.logger.info("Comment: %s", self.options.removal_comment)
850
 
 
851
 
        removals = []
852
 
        for removable in removables:
853
 
            removable.requestDeletion(
854
 
                removed_by=removed_by,
855
 
                removal_comment=self.options.removal_comment)
856
 
            removals.append(removable)
857
 
 
858
 
        if len(removals) == 0:
859
 
            self.logger.info("No package removed (bug ?!?).")
860
 
        else:
861
 
            self.logger.info(
862
 
                "%d %s successfully removed.", len(removals),
863
 
                get_plural_text(len(removals), "package", "packages"))
864
 
 
865
 
        # Information returned mainly for the benefit of the test harness.
866
 
        return removals
867
 
 
868
 
 
869
 
class ObsoleteDistroseries(SoyuzScript):
870
 
    """`SoyuzScript` that obsoletes a distroseries."""
871
 
 
872
 
    usage = "%prog -d <distribution> -s <suite>"
873
 
    description = ("Make obsolete (schedule for removal) packages in an "
874
 
                  "obsolete distroseries.")
875
 
 
876
 
    def add_my_options(self):
877
 
        """Add -d, -s, dry-run and confirmation options."""
878
 
        SoyuzScript.add_distro_options(self)
879
 
        SoyuzScript.add_transaction_options(self)
880
 
 
881
 
    def mainTask(self):
882
 
        """Execute package obsolescence procedure.
883
 
 
884
 
        Modules using this class outside of its normal usage in the
885
 
        main script can call this method to start the copy.
886
 
 
887
 
        In this case the caller can override test_args on __init__
888
 
        to set the command line arguments.
889
 
 
890
 
        :raise SoyuzScriptError: If the distroseries is not provided or
891
 
            it is already obsolete.
892
 
        """
893
 
        assert self.location, (
894
 
            "location is not available, call SoyuzScript.setupLocation() "
895
 
            "before calling mainTask().")
896
 
 
897
 
        # Shortcut variable name to reduce long lines.
898
 
        distroseries = self.location.distroseries
899
 
 
900
 
        self._checkParameters(distroseries)
901
 
 
902
 
        self.logger.info("Obsoleting all packages for distroseries %s in "
903
 
                         "the %s distribution." % (
904
 
                            distroseries.name,
905
 
                            distroseries.distribution.name))
906
 
 
907
 
        # First, mark all Published sources as Obsolete.
908
 
        sources = distroseries.getAllPublishedSources()
909
 
        binaries = distroseries.getAllPublishedBinaries()
910
 
        self.logger.info(
911
 
            "Obsoleting published packages (%d sources, %d binaries)."
912
 
            % (sources.count(), binaries.count()))
913
 
        for package in chain(sources, binaries):
914
 
            self.logger.debug("Obsoleting %s" % package.displayname)
915
 
            package.requestObsolescence()
916
 
 
917
 
        # Next, ensure that everything is scheduled for deletion.  The
918
 
        # dominator will normally leave some superseded publications
919
 
        # uncondemned, for example sources that built NBSed binaries.
920
 
        sources = distroseries.getAllUncondemnedSources()
921
 
        binaries = distroseries.getAllUncondemnedBinaries()
922
 
        self.logger.info(
923
 
            "Scheduling deletion of other packages (%d sources, %d binaries)."
924
 
            % (sources.count(), binaries.count()))
925
 
        for package in chain(sources, binaries):
926
 
            self.logger.debug(
927
 
                "Scheduling deletion of %s" % package.displayname)
928
 
            package.scheduleddeletiondate = UTC_NOW
929
 
 
930
 
        # The packages from both phases will be caught by death row
931
 
        # processing the next time it runs.  We skip the domination
932
 
        # phase in the publisher because it won't consider stable
933
 
        # distroseries.
934
 
 
935
 
    def _checkParameters(self, distroseries):
936
 
        """Sanity check the supplied script parameters."""
937
 
        # Did the user provide a suite name? (distribution defaults
938
 
        # to 'ubuntu' which is fine.)
939
 
        if distroseries == distroseries.distribution.currentseries:
940
 
            # SoyuzScript defaults to the latest series.  Since this
941
 
            # will never get obsoleted it's safe to assume that the
942
 
            # user let this option default, so complain and exit.
943
 
            raise SoyuzScriptError(
944
 
                "Please specify a valid distroseries name with -s/--suite "
945
 
                "and which is not the most recent distroseries.")
946
 
 
947
 
        # Is the distroseries in an obsolete state?  Bail out now if not.
948
 
        if distroseries.status != SeriesStatus.OBSOLETE:
949
 
            raise SoyuzScriptError(
950
 
                "%s is not at status OBSOLETE." % distroseries.name)
951
 
 
952
 
 
953
 
class ManageChrootScript(SoyuzScript):
954
 
    """`SoyuzScript` that manages chroot files."""
955
 
 
956
 
    usage = "%prog -d <distribution> -s <suite> -a <architecture> -f file"
957
 
    description = "Manage the chroot files used by the builders."
958
 
    success_message = "Success."
959
 
 
960
 
    def add_my_options(self):
961
 
        """Add script options."""
962
 
        SoyuzScript.add_distro_options(self)
963
 
        SoyuzScript.add_transaction_options(self)
964
 
        self.parser.add_option(
965
 
            '-a', '--architecture', dest='architecture', default=None,
966
 
            help='Architecture tag')
967
 
        self.parser.add_option(
968
 
            '-f', '--filepath', dest='filepath', default=None,
969
 
            help='Chroot file path')
970
 
 
971
 
    def mainTask(self):
972
 
        """Set up a ChrootManager object and invoke it."""
973
 
        if len(self.args) != 1:
974
 
            raise SoyuzScriptError(
975
 
                "manage-chroot.py <add|update|remove|get>")
976
 
 
977
 
        [action] = self.args
978
 
 
979
 
        series = self.location.distroseries
980
 
 
981
 
        try:
982
 
            distroarchseries = series[self.options.architecture]
983
 
        except NotFoundError, info:
984
 
            raise SoyuzScriptError("Architecture not found: %s" % info)
985
 
 
986
 
        # We don't want to have to force the user to confirm transactions
987
 
        # for manage-chroot.py, so disable that feature of SoyuzScript.
988
 
        self.options.confirm_all = True
989
 
 
990
 
        self.logger.debug(
991
 
            "Initializing ChrootManager for '%s'" % (distroarchseries.title))
992
 
        chroot_manager = ChrootManager(
993
 
            distroarchseries, filepath=self.options.filepath)
994
 
 
995
 
        if action in chroot_manager.allowed_actions:
996
 
            chroot_action = getattr(chroot_manager, action)
997
 
        else:
998
 
            self.logger.error(
999
 
                "Allowed actions: %s" % chroot_manager.allowed_actions)
1000
 
            raise SoyuzScriptError("Unknown action: %s" % action)
1001
 
 
1002
 
        try:
1003
 
            chroot_action()
1004
 
        except ChrootManagerError, info:
1005
 
            raise SoyuzScriptError(info)
1006
 
        else:
1007
 
            # Collect extra debug messages from chroot_manager.
1008
 
            for debug_message in chroot_manager._messages:
1009
 
                self.logger.debug(debug_message)
1010
 
 
1011
 
 
1012
 
def generate_changes(dsc, dsc_files, suite, changelog, urgency, closes,
1013
 
                     lp_closes, section, priority, description,
1014
 
                     files_from_librarian, requested_by, origin):
1015
 
    """Generate a Changes object.
1016
 
 
1017
 
    :param dsc: A `Dsc` instance for the related source package.
1018
 
    :param suite: Distribution name
1019
 
    :param changelog: Relevant changelog data
1020
 
    :param urgency: Urgency string (low, medium, high, etc)
1021
 
    :param closes: Sequence of Debian bug numbers (as strings) fixed by
1022
 
        this upload.
1023
 
    :param section: Debian section
1024
 
    :param priority: Package priority
1025
 
    """
1026
 
 
1027
 
    # XXX cprov 2007-07-03:
1028
 
    # Changed-By can be extracted from most-recent changelog footer,
1029
 
    # but do we care?
1030
 
 
1031
 
    changes = Changes()
1032
 
    changes["Origin"] = "%s/%s" % (origin["name"], origin["suite"])
1033
 
    changes["Format"] = "1.7"
1034
 
    changes["Date"] = time.strftime("%a,  %d %b %Y %H:%M:%S %z")
1035
 
    changes["Source"] = dsc["source"]
1036
 
    changes["Binary"] = dsc["binary"]
1037
 
    changes["Architecture"] = "source"
1038
 
    changes["Version"] = dsc["version"]
1039
 
    changes["Distribution"] = suite
1040
 
    changes["Urgency"] = urgency
1041
 
    changes["Maintainer"] = dsc["maintainer"]
1042
 
    changes["Changed-By"] = requested_by
1043
 
    if description:
1044
 
        changes["Description"] = "\n %s" % description
1045
 
    if closes:
1046
 
        changes["Closes"] = " ".join(closes)
1047
 
    if lp_closes:
1048
 
        changes["Launchpad-bugs-fixed"] = " ".join(lp_closes)
1049
 
    files = []
1050
 
    for filename in dsc_files:
1051
 
        if filename in files_from_librarian:
1052
 
            continue
1053
 
        files.append({"md5sum": dsc_files[filename]["md5sum"],
1054
 
                      "size": dsc_files[filename]["size"],
1055
 
                      "section": section,
1056
 
                      "priority": priority,
1057
 
                      "name": filename,
1058
 
                     })
1059
 
 
1060
 
    changes["Files"] = files
1061
 
    changes["Changes"] = "\n%s" % changelog
1062
 
    return changes