~launchpad-pqm/launchpad/devel

« back to all changes in this revision

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

  • Committer: Launchpad Patch Queue Manager
  • Date: 2011-09-26 16:35:37 UTC
  • mfrom: (13995.1.6 add-longpoll)
  • Revision ID: launchpad@pqm.canonical.com-20110926163537-o17y6ic6g9i3g943
[r=julian-edwards][no-qa] Upgrade txlongpoll version.

Show diffs side-by-side

added added

removed removed

Lines of Context:
6
6
__metaclass__ = type
7
7
 
8
8
__all__ = [
 
9
    'ArchiveCruftChecker',
 
10
    'ArchiveCruftCheckerError',
9
11
    'ChrootManager',
10
12
    'ChrootManagerError',
11
13
    'LpQueryDistro',
67
69
    )
68
70
 
69
71
 
 
72
class ArchiveCruftCheckerError(Exception):
 
73
    """ArchiveCruftChecker specific exception.
 
74
 
 
75
    Mostly used to describe errors in the initialization of this object.
 
76
    """
 
77
 
 
78
 
 
79
class TagFileNotFound(Exception):
 
80
    """Raised when an archive tag file could not be found."""
 
81
 
 
82
 
 
83
class ArchiveCruftChecker:
 
84
    """Perform overall checks to identify and remove obsolete records.
 
85
 
 
86
    Use initialize() method to validate passed parameters and build the
 
87
    infrastructure variables. It will raise ArchiveCruftCheckerError if
 
88
    something goes wrong.
 
89
    """
 
90
 
 
91
    # XXX cprov 2006-05-15: the default archive path should come
 
92
    # from the config.
 
93
    def __init__(self, logger, distribution_name='ubuntu', suite=None,
 
94
                 archive_path='/srv/launchpad.net/ubuntu-archive'):
 
95
        """Store passed arguments.
 
96
 
 
97
        Also Initialize empty variables for storing preliminar results.
 
98
        """
 
99
        self.distribution_name = distribution_name
 
100
        self.suite = suite
 
101
        self.archive_path = archive_path
 
102
        self.logger = logger
 
103
        # initialize a group of variables to store temporary results
 
104
        # available versions of published sources
 
105
        self.source_versions = {}
 
106
        # available binaries produced by published sources
 
107
        self.source_binaries = {}
 
108
        # 'Not Build From Source' binaries
 
109
        self.nbs = {}
 
110
        # 'All superseded by Any' binaries
 
111
        self.asba = {}
 
112
        # published binary package names
 
113
        self.bin_pkgs = {}
 
114
        # Architecture specific binary packages
 
115
        self.arch_any = {}
 
116
        # proposed NBS (before clean up)
 
117
        self.dubious_nbs = {}
 
118
        # NBS after clean up
 
119
        self.real_nbs = {}
 
120
        # definitive NBS organized for clean up
 
121
        self.nbs_to_remove = []
 
122
 
 
123
    @property
 
124
    def architectures(self):
 
125
        return dict([(a.architecturetag, a)
 
126
                     for a in self.distroseries.architectures])
 
127
 
 
128
    @property
 
129
    def components(self):
 
130
        return dict([(c.name, c) for c in self.distroseries.components])
 
131
 
 
132
    @property
 
133
    def components_and_di(self):
 
134
        components_and_di = []
 
135
        for component in self.components:
 
136
            components_and_di.append(component)
 
137
            components_and_di.append('%s/debian-installer' % (component))
 
138
        return components_and_di
 
139
 
 
140
    @property
 
141
    def dist_archive(self):
 
142
        return os.path.join(
 
143
            self.archive_path, self.distro.name, 'dists',
 
144
            self.distroseries.name + pocketsuffix[self.pocket])
 
145
 
 
146
    def gunzipTagFileContent(self, filename):
 
147
        """Gunzip the contents of passed filename.
 
148
 
 
149
        Check filename presence, if not present in the filesystem,
 
150
        raises ArchiveCruftCheckerError. Use an tempfile.mkstemp()
 
151
        to store the uncompressed content. Invoke system available
 
152
        gunzip`, raises ArchiveCruftCheckError if it fails.
 
153
 
 
154
        This method doesn't close the file descriptor used and does not
 
155
        remove the temporary file from the filesystem, those actions
 
156
        are required in the callsite. (apt_pkg.ParseTagFile is lazy)
 
157
 
 
158
        Return a tuple containing:
 
159
         * temp file descriptor
 
160
         * temp filename
 
161
         * the contents parsed by apt_pkg.ParseTagFile()
 
162
        """
 
163
        if not os.path.exists(filename):
 
164
            raise TagFileNotFound("File does not exist: %s" % filename)
 
165
 
 
166
        temp_fd, temp_filename = tempfile.mkstemp()
 
167
        (result, output) = commands.getstatusoutput(
 
168
            "gunzip -c %s > %s" % (filename, temp_filename))
 
169
        if result != 0:
 
170
            raise ArchiveCruftCheckerError(
 
171
                "Gunzip invocation failed!\n%s" % output)
 
172
 
 
173
        temp_file = os.fdopen(temp_fd)
 
174
        # XXX cprov 2006-05-15: maybe we need some sort of data integrity
 
175
        # check at this point, and maybe keep the uncrompressed file
 
176
        # for debug purposes, let's see how it behaves in real conditions.
 
177
        parsed_contents = apt_pkg.ParseTagFile(temp_file)
 
178
 
 
179
        return temp_file, temp_filename, parsed_contents
 
180
 
 
181
    def processSources(self):
 
182
        """Process archive sources index.
 
183
 
 
184
        Build source_binaries, source_versions and bin_pkgs lists.
 
185
        """
 
186
        self.logger.debug("Considering Sources:")
 
187
        for component in self.components:
 
188
            filename = os.path.join(
 
189
                self.dist_archive, "%s/source/Sources.gz" % component)
 
190
 
 
191
            self.logger.debug("Processing %s" % filename)
 
192
            try:
 
193
                temp_fd, temp_filename, parsed_sources = (
 
194
                    self.gunzipTagFileContent(filename))
 
195
            except TagFileNotFound, warning:
 
196
                self.logger.warn(warning)
 
197
                return
 
198
            try:
 
199
                while parsed_sources.Step():
 
200
                    source = parsed_sources.Section.Find("Package")
 
201
                    source_version = parsed_sources.Section.Find("Version")
 
202
                    binaries = parsed_sources.Section.Find("Binary")
 
203
                    for binary in [
 
204
                        item.strip() for item in binaries.split(',')]:
 
205
                        self.bin_pkgs.setdefault(binary, [])
 
206
                        self.bin_pkgs[binary].append(source)
 
207
 
 
208
                    self.source_binaries[source] = binaries
 
209
                    self.source_versions[source] = source_version
 
210
            finally:
 
211
                # close fd and remove temporary file used to store
 
212
                # uncompressed tag file content from the filesystem.
 
213
                temp_fd.close()
 
214
                os.unlink(temp_filename)
 
215
 
 
216
    def buildNBS(self):
 
217
        """Build the group of 'not build from source' binaries"""
 
218
        # Checks based on the Packages files
 
219
        self.logger.debug("Building not build from source list (NBS):")
 
220
        for component in self.components_and_di:
 
221
            for architecture in self.architectures:
 
222
                self.buildArchNBS(component, architecture)
 
223
 
 
224
    def buildArchNBS(self, component, architecture):
 
225
        """Build NBS per architecture.
 
226
 
 
227
        Store results in self.nbs, also build architecture specific
 
228
        binaries group (stored in self.arch_any)
 
229
        """
 
230
        filename = os.path.join(
 
231
            self.dist_archive,
 
232
            "%s/binary-%s/Packages.gz" % (component, architecture))
 
233
 
 
234
        self.logger.debug("Processing %s" % filename)
 
235
        try:
 
236
            temp_fd, temp_filename, parsed_packages = (
 
237
                self.gunzipTagFileContent(filename))
 
238
        except TagFileNotFound, warning:
 
239
            self.logger.warn(warning)
 
240
            return
 
241
 
 
242
        try:
 
243
            while parsed_packages.Step():
 
244
                package = parsed_packages.Section.Find('Package')
 
245
                source = parsed_packages.Section.Find('Source', "")
 
246
                version = parsed_packages.Section.Find('Version')
 
247
                architecture = parsed_packages.Section.Find('Architecture')
 
248
 
 
249
                if source == "":
 
250
                    source = package
 
251
 
 
252
                if source.find("(") != -1:
 
253
                    m = re_extract_src_version.match(source)
 
254
                    source = m.group(1)
 
255
                    version = m.group(2)
 
256
 
 
257
                if package not in self.bin_pkgs:
 
258
                    self.nbs.setdefault(source, {})
 
259
                    self.nbs[source].setdefault(package, {})
 
260
                    self.nbs[source][package][version] = ""
 
261
 
 
262
                if architecture != "all":
 
263
                    self.arch_any.setdefault(package, "0")
 
264
                    if apt_pkg.VersionCompare(
 
265
                        version, self.arch_any[package]) < 1:
 
266
                        self.arch_any[package] = version
 
267
        finally:
 
268
            # close fd and remove temporary file used to store uncompressed
 
269
            # tag file content from the filesystem.
 
270
            temp_fd.close()
 
271
            os.unlink(temp_filename)
 
272
 
 
273
    def buildASBA(self):
 
274
        """Build the group of 'all superseded by any' binaries."""
 
275
        self.logger.debug("Building all superseded by any list (ASBA):")
 
276
        for component in self.components_and_di:
 
277
            for architecture in self.architectures:
 
278
                self.buildArchASBA(component, architecture)
 
279
 
 
280
    def buildArchASBA(self, component, architecture):
 
281
        """Build ASBA per architecture.
 
282
 
 
283
        Store the result in self.asba, require self.arch_any to be built
 
284
        previously.
 
285
        """
 
286
        filename = os.path.join(
 
287
            self.dist_archive,
 
288
            "%s/binary-%s/Packages.gz" % (component, architecture))
 
289
 
 
290
        try:
 
291
            temp_fd, temp_filename, parsed_packages = (
 
292
                self.gunzipTagFileContent(filename))
 
293
        except TagFileNotFound, warning:
 
294
            self.logger.warn(warning)
 
295
            return
 
296
 
 
297
        try:
 
298
            while parsed_packages.Step():
 
299
                package = parsed_packages.Section.Find('Package')
 
300
                source = parsed_packages.Section.Find('Source', "")
 
301
                version = parsed_packages.Section.Find('Version')
 
302
                architecture = parsed_packages.Section.Find('Architecture')
 
303
 
 
304
                if source == "":
 
305
                    source = package
 
306
 
 
307
                if source.find("(") != -1:
 
308
                    m = re_extract_src_version.match(source)
 
309
                    source = m.group(1)
 
310
                    version = m.group(2)
 
311
 
 
312
                if architecture == "all":
 
313
                    if (package in self.arch_any and
 
314
                        apt_pkg.VersionCompare(
 
315
                        version, self.arch_any[package]) > -1):
 
316
                        self.asba.setdefault(source, {})
 
317
                        self.asba[source].setdefault(package, {})
 
318
                        self.asba[source][package].setdefault(version, {})
 
319
                        self.asba[source][package][version][architecture] = ""
 
320
        finally:
 
321
            # close fd and remove temporary file used to store uncompressed
 
322
            # tag file content from the filesystem.
 
323
            temp_fd.close()
 
324
            os.unlink(temp_filename)
 
325
 
 
326
    def addNBS(self, nbs_d, source, version, package):
 
327
        """Add a new entry in given organized nbs_d list
 
328
 
 
329
        Ensure the package is still published in the suite before add.
 
330
        """
 
331
        result = self.distroseries.getBinaryPackagePublishing(name=package)
 
332
 
 
333
        if len(list(result)) == 0:
 
334
            return
 
335
 
 
336
        nbs_d.setdefault(source, {})
 
337
        nbs_d[source].setdefault(version, {})
 
338
        nbs_d[source][version][package] = ""
 
339
 
 
340
    def refineNBS(self):
 
341
        """ Distinguish dubious from real NBS.
 
342
 
 
343
        They are 'dubious' if the version numbers match and 'real'
 
344
        if the versions don't match.
 
345
        It stores results in self.dubious_nbs and self.real_nbs.
 
346
        """
 
347
        for source in self.nbs.keys():
 
348
            for package in self.nbs[source].keys():
 
349
                versions = self.nbs[source][package].keys()
 
350
                versions.sort(apt_pkg.VersionCompare)
 
351
                latest_version = versions.pop()
 
352
 
 
353
                source_version = self.source_versions.get(source, "0")
 
354
 
 
355
                if apt_pkg.VersionCompare(latest_version,
 
356
                                          source_version) == 0:
 
357
                    self.addNBS(self.dubious_nbs, source, latest_version,
 
358
                                package)
 
359
                else:
 
360
                    self.addNBS(self.real_nbs, source, latest_version,
 
361
                                package)
 
362
 
 
363
    def outputNBS(self):
 
364
        """Properly display built NBS entries.
 
365
 
 
366
        Also organize the 'real' NBSs for removal in self.nbs_to_remove
 
367
        attribute.
 
368
        """
 
369
        output = "Not Built from Source\n"
 
370
        output += "---------------------\n\n"
 
371
 
 
372
        nbs_keys = self.real_nbs.keys()
 
373
        nbs_keys.sort()
 
374
 
 
375
        for source in nbs_keys:
 
376
            proposed_bin = self.source_binaries.get(
 
377
                source, "(source does not exist)")
 
378
            porposed_version = self.source_versions.get(source, "??")
 
379
            output += (" * %s_%s builds: %s\n"
 
380
                       % (source, porposed_version, proposed_bin))
 
381
            output += "\tbut no longer builds:\n"
 
382
            versions = self.real_nbs[source].keys()
 
383
            versions.sort(apt_pkg.VersionCompare)
 
384
 
 
385
            for version in versions:
 
386
                packages = self.real_nbs[source][version].keys()
 
387
                packages.sort()
 
388
 
 
389
                for pkg in packages:
 
390
                    self.nbs_to_remove.append(pkg)
 
391
 
 
392
                output += "        o %s: %s\n" % (
 
393
                    version, ", ".join(packages))
 
394
 
 
395
            output += "\n"
 
396
 
 
397
        if self.nbs_to_remove:
 
398
            self.logger.info(output)
 
399
        else:
 
400
            self.logger.debug("No NBS found")
 
401
 
 
402
    def initialize(self):
 
403
        """Initialize and build required lists of obsolete entries in archive.
 
404
 
 
405
        Check integrity of passed parameters and store organised data.
 
406
        The result list is the self.nbs_to_remove which should contain
 
407
        obsolete packages not currently able to be built from again.
 
408
        Another preliminary lists can be inspected in order to have better
 
409
        idea of what was computed.
 
410
        If anything goes wrong mid-process, it raises ArchiveCruftCheckError,
 
411
        otherwise a list of packages to be removes is printed.
 
412
        """
 
413
        if self.distribution_name is None:
 
414
            self.distro = getUtility(ILaunchpadCelebrities).ubuntu
 
415
        else:
 
416
            try:
 
417
                self.distro = getUtility(IDistributionSet)[
 
418
                    self.distribution_name]
 
419
            except NotFoundError:
 
420
                raise ArchiveCruftCheckerError(
 
421
                    "Invalid distribution: '%s'" % self.distribution_name)
 
422
 
 
423
        if not self.suite:
 
424
            self.distroseries = self.distro.currentseries
 
425
            self.pocket = PackagePublishingPocket.RELEASE
 
426
        else:
 
427
            try:
 
428
                self.distroseries, self.pocket = (
 
429
                    self.distro.getDistroSeriesAndPocket(self.suite))
 
430
            except NotFoundError:
 
431
                raise ArchiveCruftCheckerError(
 
432
                    "Invalid suite: '%s'" % self.suite)
 
433
 
 
434
        if not os.path.exists(self.dist_archive):
 
435
            raise ArchiveCruftCheckerError(
 
436
                "Invalid archive path: '%s'" % self.dist_archive)
 
437
 
 
438
        apt_pkg.init()
 
439
        self.processSources()
 
440
        self.buildNBS()
 
441
        self.buildASBA()
 
442
        self.refineNBS()
 
443
        self.outputNBS()
 
444
 
 
445
    def doRemovals(self):
 
446
        """Perform the removal of the obsolete packages found.
 
447
 
 
448
        It iterates over the previously build list (self.nbs_to_remove)
 
449
        and mark them as 'superseded' in the archive DB model. They will
 
450
        get removed later by the archive sanity check run each cycle
 
451
        of the cron.daily.
 
452
        """
 
453
        for package in self.nbs_to_remove:
 
454
 
 
455
            for distroarchseries in self.distroseries.architectures:
 
456
                binarypackagename = getUtility(IBinaryPackageNameSet)[package]
 
457
                dasbp = distroarchseries.getBinaryPackage(binarypackagename)
 
458
                dasbpr = dasbp.currentrelease
 
459
                try:
 
460
                    bpph = dasbpr.current_publishing_record
 
461
                    bpph.supersede()
 
462
                    # We're blindly removing for all arches, if it's not there
 
463
                    # for some, that's fine ...
 
464
                except NotFoundError:
 
465
                    pass
 
466
                else:
 
467
                    version = bpph.binarypackagerelease.version
 
468
                    self.logger.info("Removed %s_%s from %s/%s ... "
 
469
                                      % (package, version,
 
470
                                         self.distroseries.name,
 
471
                                         distroarchseries.architecturetag))
 
472
 
 
473
 
70
474
class PubBinaryContent:
71
475
    """Binary publication container.
72
476