~launchpad-pqm/launchpad/devel

« back to all changes in this revision

Viewing changes to lib/lp/buildmaster/model/packagebuild.py

Merge with trunk.

Show diffs side-by-side

added added

removed removed

Lines of Context:
9
9
    ]
10
10
 
11
11
 
 
12
import datetime
 
13
import logging
 
14
import os.path
 
15
import subprocess
 
16
 
 
17
from cStringIO import StringIO
12
18
from lazr.delegates import delegates
 
19
import pytz
13
20
from storm.expr import Desc
14
21
from storm.locals import (
15
22
    Int,
16
23
    Reference,
 
24
    Store,
17
25
    Storm,
18
26
    Unicode,
19
27
    )
22
30
    classProvides,
23
31
    implements,
24
32
    )
 
33
from zope.security.proxy import removeSecurityProxy
25
34
 
 
35
from canonical.config import config
26
36
from canonical.database.enumcol import DBEnum
 
37
from canonical.database.sqlbase import (
 
38
    clear_current_connection_cache,
 
39
    cursor,
 
40
    flush_database_updates,
 
41
    )
27
42
from canonical.launchpad.browser.librarian import ProxiedLibraryFileAlias
 
43
from canonical.launchpad.helpers import filenameToContentType
 
44
from canonical.launchpad.interfaces.librarian import ILibraryFileAliasSet
28
45
from canonical.launchpad.interfaces.lpstorm import IMasterStore
29
46
from canonical.launchpad.webapp.interfaces import (
30
47
    DEFAULT_FLAVOR,
31
48
    IStoreSelector,
32
49
    MAIN_STORE,
33
50
    )
34
 
from lp.buildmaster.interfaces.buildbase import BuildStatus
 
51
from canonical.librarian.utils import copy_and_close
 
52
from lp.buildmaster.interfaces.buildbase import (
 
53
    BUILDD_MANAGER_LOG_NAME,
 
54
    BuildStatus,
 
55
    )
35
56
from lp.buildmaster.interfaces.buildfarmjob import IBuildFarmJobSource
36
57
from lp.buildmaster.interfaces.packagebuild import (
37
58
    IPackageBuild,
38
59
    IPackageBuildSet,
39
60
    IPackageBuildSource,
40
61
    )
41
 
from lp.buildmaster.model.buildbase import (
42
 
    BuildBase,
43
 
    handle_status_for_build,
44
 
    )
45
62
from lp.buildmaster.model.buildfarmjob import (
46
63
    BuildFarmJob,
47
64
    BuildFarmJobDerived,
48
65
    )
49
 
from lp.registry.interfaces.pocket import PackagePublishingPocket
 
66
from lp.buildmaster.model.buildqueue import BuildQueue
 
67
from lp.registry.interfaces.pocket import (
 
68
    PackagePublishingPocket,
 
69
    pocketsuffix,
 
70
    )
50
71
from lp.soyuz.adapters.archivedependencies import (
51
72
    default_component_dependency_name,
52
73
    )
53
74
from lp.soyuz.interfaces.component import IComponentSet
54
75
 
55
76
 
 
77
SLAVE_LOG_FILENAME = 'buildlog'
 
78
UPLOAD_LOG_FILENAME = 'uploader.log'
 
79
 
 
80
 
56
81
class PackageBuild(BuildFarmJobDerived, Storm):
57
82
    """An implementation of `IBuildFarmJob` for package builds."""
58
83
 
117
142
 
118
143
    @property
119
144
    def upload_log_url(self):
120
 
        """See `IBuildBase`."""
 
145
        """See `IPackageBuild`."""
121
146
        if self.upload_log is None:
122
147
            return None
123
148
        return ProxiedLibraryFileAlias(self.upload_log, self).http_url
131
156
 
132
157
    @property
133
158
    def is_private(self):
134
 
        """See `IBuildBase`"""
 
159
        """See `IBuildFarmJob`"""
135
160
        return self.archive.private
136
161
 
137
162
    def getUploadDirLeaf(self, build_cookie, now=None):
138
163
        """See `IPackageBuild`."""
139
 
        return BuildBase.getUploadDirLeaf(build_cookie, now)
 
164
        if now is None:
 
165
            now = datetime.datetime.now()
 
166
        timestamp = now.strftime("%Y%m%d-%H%M%S")
 
167
        return '%s-%s' % (timestamp, build_cookie)
140
168
 
141
169
    def getUploadDir(self, upload_leaf):
142
170
        """See `IPackageBuild`."""
143
 
        return BuildBase.getUploadDir(upload_leaf)
 
171
        return os.path.join(config.builddmaster.root, 'incoming', upload_leaf)
144
172
 
145
173
    @staticmethod
146
174
    def getUploaderCommand(package_build, upload_leaf, upload_logfilename):
147
175
        """See `IPackageBuild`."""
148
 
        return BuildBase.getUploaderCommand(
149
 
            package_build, upload_leaf, upload_logfilename)
 
176
        root = os.path.abspath(config.builddmaster.root)
 
177
        uploader_command = list(config.builddmaster.uploader.split())
 
178
 
 
179
        # Add extra arguments for processing a package upload.
 
180
        extra_args = [
 
181
            "--log-file", "%s" % upload_logfilename,
 
182
            "-d", "%s" % package_build.distribution.name,
 
183
            "-s", "%s" % (
 
184
                package_build.distro_series.getSuite(package_build.pocket)),
 
185
            "-b", "%s" % package_build.id,
 
186
            "-J", "%s" % upload_leaf,
 
187
            '--context=%s' % package_build.policy_name,
 
188
            "%s" % root,
 
189
            ]
 
190
 
 
191
        uploader_command.extend(extra_args)
 
192
        return uploader_command
150
193
 
151
194
    @staticmethod
152
195
    def getLogFromSlave(package_build):
153
196
        """See `IPackageBuild`."""
154
 
        return BuildBase.getLogFromSlave(package_build)
 
197
        builder = package_build.buildqueue_record.builder
 
198
        return builder.transferSlaveFileToLibrarian(
 
199
            SLAVE_LOG_FILENAME,
 
200
            package_build.buildqueue_record.getLogFileName(),
 
201
            package_build.is_private)
155
202
 
156
203
    @staticmethod
157
204
    def getUploadLogContent(root, leaf):
158
 
        """See `IPackageBuild`."""
159
 
        return BuildBase.getUploadLogContent(root, leaf)
 
205
        """Retrieve the upload log contents.
 
206
 
 
207
        :param root: Root directory for the uploads
 
208
        :param leaf: Leaf for this particular upload
 
209
        :return: Contents of log file or message saying no log file was found.
 
210
        """
 
211
        # Retrieve log file content.
 
212
        possible_locations = (
 
213
            'failed', 'failed-to-move', 'rejected', 'accepted')
 
214
        for location_dir in possible_locations:
 
215
            log_filepath = os.path.join(root, location_dir, leaf,
 
216
                UPLOAD_LOG_FILENAME)
 
217
            if os.path.exists(log_filepath):
 
218
                with open(log_filepath, 'r') as uploader_log_file:
 
219
                    return uploader_log_file.read()
 
220
        else:
 
221
            return 'Could not find upload log file'
160
222
 
161
223
    def estimateDuration(self):
162
224
        """See `IPackageBuild`."""
163
225
        raise NotImplementedError
164
226
 
165
227
    @staticmethod
166
 
    def storeBuildInfo(package_build, librarian, slave_status):
 
228
    def storeBuildInfo(build, librarian, slave_status):
167
229
        """See `IPackageBuild`."""
168
 
        return BuildBase.storeBuildInfo(package_build, librarian,
169
 
                                        slave_status)
 
230
        # log, builder and date_finished are read-only, so we must
 
231
        # currently remove the security proxy to set them.
 
232
        naked_build = removeSecurityProxy(build)
 
233
        naked_build.log = build.getLogFromSlave(build)
 
234
        naked_build.builder = build.buildqueue_record.builder
 
235
        # XXX cprov 20060615 bug=120584: Currently buildduration includes
 
236
        # the scanner latency, it should really be asking the slave for
 
237
        # the duration spent building locally.
 
238
        naked_build.date_finished = datetime.datetime.now(pytz.UTC)
 
239
        if slave_status.get('dependencies') is not None:
 
240
            build.dependencies = unicode(slave_status.get('dependencies'))
 
241
        else:
 
242
            build.dependencies = None
170
243
 
171
244
    def verifySuccessfulUpload(self):
172
245
        """See `IPackageBuild`."""
173
246
        raise NotImplementedError
174
247
 
 
248
    def createUploadLog(self, content, filename=None):
 
249
        """Creates a file on the librarian for the upload log.
 
250
 
 
251
        :return: ILibraryFileAlias for the upload log file.
 
252
        """
 
253
        # The given content is stored in the librarian, restricted as
 
254
        # necessary according to the targeted archive's privacy.  The content
 
255
        # object's 'upload_log' attribute will point to the
 
256
        # `LibrarianFileAlias`.
 
257
 
 
258
        assert self.upload_log is None, (
 
259
            "Upload log information already exists and cannot be overridden.")
 
260
 
 
261
        if filename is None:
 
262
            filename = 'upload_%s_log.txt' % self.id
 
263
        contentType = filenameToContentType(filename)
 
264
        file_size = len(content)
 
265
        file_content = StringIO(content)
 
266
        restricted = self.is_private
 
267
 
 
268
        return getUtility(ILibraryFileAliasSet).create(
 
269
            filename, file_size, file_content, contentType=contentType,
 
270
            restricted=restricted)
 
271
 
175
272
    def storeUploadLog(self, content):
176
273
        """See `IPackageBuild`."""
177
274
        filename = "upload_%s_log.txt" % self.build_farm_job.id
178
 
        library_file = BuildBase.createUploadLog(
179
 
            self, content, filename=filename)
 
275
        library_file = self.createUploadLog(content, filename=filename)
180
276
        self.upload_log = library_file
181
277
 
182
278
    def notify(self, extra_info=None):
202
298
 
203
299
    def queueBuild(self, suspended=False):
204
300
        """See `IPackageBuild`."""
205
 
        return BuildBase.queueBuild(self, suspended=suspended)
 
301
        specific_job = self.makeJob()
 
302
 
 
303
        # This build queue job is to be created in a suspended state.
 
304
        if suspended:
 
305
            specific_job.job.suspend()
 
306
 
 
307
        duration_estimate = self.estimateDuration()
 
308
        queue_entry = BuildQueue(
 
309
            estimated_duration=duration_estimate,
 
310
            job_type=self.build_farm_job_type,
 
311
            job=specific_job.job, processor=specific_job.processor,
 
312
            virtualized=specific_job.virtualized)
 
313
        Store.of(self).add(queue_entry)
 
314
        return queue_entry
206
315
 
207
316
    def handleStatus(self, status, librarian, slave_status):
208
317
        """See `IPackageBuild`."""
209
 
        return handle_status_for_build(self, status, librarian, slave_status)
 
318
        logger = logging.getLogger(BUILDD_MANAGER_LOG_NAME)
 
319
        method = getattr(self, '_handleStatus_' + status, None)
 
320
        if method is None:
 
321
            logger.critical("Unknown BuildStatus '%s' for builder '%s'"
 
322
                            % (status, self.buildqueue_record.builder.url))
 
323
            return
 
324
        method(librarian, slave_status, logger)
210
325
 
211
 
    # The following private handlers currently re-use the BuildBase
212
 
    # implementation until it is no longer in use. If we find in the
213
 
    # future that it would be useful to delegate these also, they can be
214
 
    # added to IBuildFarmJob or IPackageBuild as necessary.
215
326
    def _handleStatus_OK(self, librarian, slave_status, logger):
216
 
        return BuildBase._handleStatus_OK(
217
 
            self, librarian, slave_status, logger)
 
327
        """Handle a package that built successfully.
 
328
 
 
329
        Once built successfully, we pull the files, store them in a
 
330
        directory, store build information and push them through the
 
331
        uploader.
 
332
        """
 
333
        filemap = slave_status['filemap']
 
334
 
 
335
        logger.info("Processing successful build %s from builder %s" % (
 
336
            self.buildqueue_record.specific_job.build.title,
 
337
            self.buildqueue_record.builder.name))
 
338
        # Explode before collect a binary that is denied in this
 
339
        # distroseries/pocket
 
340
        if not self.archive.allowUpdatesToReleasePocket():
 
341
            assert self.distro_series.canUploadToPocket(self.pocket), (
 
342
                "%s (%s) can not be built for pocket %s: illegal status"
 
343
                % (self.title, self.id, self.pocket.name))
 
344
 
 
345
        # Ensure we have the correct build root as:
 
346
        # <BUILDMASTER_ROOT>/incoming/<UPLOAD_LEAF>/<TARGET_PATH>/[FILES]
 
347
        root = os.path.abspath(config.builddmaster.root)
 
348
 
 
349
        # Create a single directory to store build result files.
 
350
        upload_leaf = self.getUploadDirLeaf(
 
351
            '%s-%s' % (self.id, self.buildqueue_record.id))
 
352
        upload_dir = self.getUploadDir(upload_leaf)
 
353
        logger.debug("Storing build result at '%s'" % upload_dir)
 
354
 
 
355
        # Build the right UPLOAD_PATH so the distribution and archive
 
356
        # can be correctly found during the upload:
 
357
        #       <archive_id>/distribution_name
 
358
        # for all destination archive types.
 
359
        archive = self.archive
 
360
        distribution_name = self.distribution.name
 
361
        target_path = '%s/%s' % (archive.id, distribution_name)
 
362
        upload_path = os.path.join(upload_dir, target_path)
 
363
        os.makedirs(upload_path)
 
364
 
 
365
        slave = removeSecurityProxy(self.buildqueue_record.builder.slave)
 
366
        successful_copy_from_slave = True
 
367
        for filename in filemap:
 
368
            logger.info("Grabbing file: %s" % filename)
 
369
            out_file_name = os.path.join(upload_path, filename)
 
370
            # If the evaluated output file name is not within our
 
371
            # upload path, then we don't try to copy this or any
 
372
            # subsequent files.
 
373
            if not os.path.realpath(out_file_name).startswith(upload_path):
 
374
                successful_copy_from_slave = False
 
375
                logger.warning(
 
376
                    "A slave tried to upload the file '%s' "
 
377
                    "for the build %d." % (filename, self.id))
 
378
                break
 
379
            out_file = open(out_file_name, "wb")
 
380
            slave_file = slave.getFile(filemap[filename])
 
381
            copy_and_close(slave_file, out_file)
 
382
 
 
383
        # We only attempt the upload if we successfully copied all the
 
384
        # files from the slave.
 
385
        if successful_copy_from_slave:
 
386
            uploader_logfilename = os.path.join(
 
387
                upload_dir, UPLOAD_LOG_FILENAME)
 
388
            uploader_command = self.getUploaderCommand(
 
389
                self, upload_leaf, uploader_logfilename)
 
390
            logger.debug("Saving uploader log at '%s'" % uploader_logfilename)
 
391
 
 
392
            logger.info("Invoking uploader on %s" % root)
 
393
            logger.info("%s" % uploader_command)
 
394
 
 
395
            uploader_process = subprocess.Popen(
 
396
                uploader_command, stdout=subprocess.PIPE,
 
397
                stderr=subprocess.PIPE)
 
398
 
 
399
            # Nothing should be written to the stdout/stderr.
 
400
            upload_stdout, upload_stderr = uploader_process.communicate()
 
401
 
 
402
            # XXX cprov 2007-04-17: we do not check uploader_result_code
 
403
            # anywhere. We need to find out what will be best strategy
 
404
            # when it failed HARD (there is a huge effort in process-upload
 
405
            # to not return error, it only happen when the code is broken).
 
406
            uploader_result_code = uploader_process.returncode
 
407
            logger.info("Uploader returned %d" % uploader_result_code)
 
408
 
 
409
        # Quick and dirty hack to carry on on process-upload failures
 
410
        if os.path.exists(upload_dir):
 
411
            logger.warning("The upload directory did not get moved.")
 
412
            failed_dir = os.path.join(root, "failed-to-move")
 
413
            if not os.path.exists(failed_dir):
 
414
                os.mkdir(failed_dir)
 
415
            os.rename(upload_dir, os.path.join(failed_dir, upload_leaf))
 
416
 
 
417
        # The famous 'flush_updates + clear_cache' will make visible
 
418
        # the DB changes done in process-upload, considering that the
 
419
        # transaction was set with ISOLATION_LEVEL_READ_COMMITED
 
420
        # isolation level.
 
421
        cur = cursor()
 
422
        cur.execute('SHOW transaction_isolation')
 
423
        isolation_str = cur.fetchone()[0]
 
424
        assert isolation_str == 'read committed', (
 
425
            'BuildMaster/BuilderGroup transaction isolation should be '
 
426
            'ISOLATION_LEVEL_READ_COMMITTED (not "%s")' % isolation_str)
 
427
 
 
428
        original_slave = self.buildqueue_record.builder.slave
 
429
 
 
430
        # XXX Robert Collins, Celso Providelo 2007-05-26 bug=506256:
 
431
        # 'Refreshing' objects  procedure  is forced on us by using a
 
432
        # different process to do the upload, but as that process runs
 
433
        # in the same unix account, it is simply double handling and we
 
434
        # would be better off to do it within this process.
 
435
        flush_database_updates()
 
436
        clear_current_connection_cache()
 
437
 
 
438
        # XXX cprov 2007-06-15: Re-issuing removeSecurityProxy is forced on
 
439
        # us by sqlobject refreshing the builder object during the
 
440
        # transaction cache clearing. Once we sort the previous problem
 
441
        # this step should probably not be required anymore.
 
442
        self.buildqueue_record.builder.setSlaveForTesting(
 
443
            removeSecurityProxy(original_slave))
 
444
 
 
445
        # Store build information, build record was already updated during
 
446
        # the binary upload.
 
447
        self.storeBuildInfo(self, librarian, slave_status)
 
448
 
 
449
        # Retrive the up-to-date build record and perform consistency
 
450
        # checks. The build record should be updated during the binary
 
451
        # upload processing, if it wasn't something is broken and needs
 
452
        # admins attention. Even when we have a FULLYBUILT build record,
 
453
        # if it is not related with at least one binary, there is also
 
454
        # a problem.
 
455
        # For both situations we will mark the builder as FAILEDTOUPLOAD
 
456
        # and the and update the build details (datebuilt, duration,
 
457
        # buildlog, builder) in LP. A build-failure-notification will be
 
458
        # sent to the lp-build-admin celebrity and to the sourcepackagerelease
 
459
        # uploader about this occurrence. The failure notification will
 
460
        # also contain the information required to manually reprocess the
 
461
        # binary upload when it was the case.
 
462
        if (self.status != BuildStatus.FULLYBUILT or
 
463
            not successful_copy_from_slave or
 
464
            not self.verifySuccessfulUpload()):
 
465
            logger.warning("Build %s upload failed." % self.id)
 
466
            self.status = BuildStatus.FAILEDTOUPLOAD
 
467
            uploader_log_content = self.getUploadLogContent(root,
 
468
                upload_leaf)
 
469
            # Store the upload_log_contents in librarian so it can be
 
470
            # accessed by anyone with permission to see the build.
 
471
            self.storeUploadLog(uploader_log_content)
 
472
            # Notify the build failure.
 
473
            self.notify(extra_info=uploader_log_content)
 
474
        else:
 
475
            logger.info(
 
476
                "Gathered %s %d completely" % (
 
477
                self.__class__.__name__, self.id))
 
478
 
 
479
        # Release the builder for another job.
 
480
        self.buildqueue_record.builder.cleanSlave()
 
481
        # Remove BuildQueue record.
 
482
        self.buildqueue_record.destroySelf()
218
483
 
219
484
    def _handleStatus_PACKAGEFAIL(self, librarian, slave_status, logger):
220
 
        return BuildBase._handleStatus_PACKAGEFAIL(
221
 
            self, librarian, slave_status, logger)
 
485
        """Handle a package that had failed to build.
 
486
 
 
487
        Build has failed when trying the work with the target package,
 
488
        set the job status as FAILEDTOBUILD, store available info and
 
489
        remove Buildqueue entry.
 
490
        """
 
491
        self.status = BuildStatus.FAILEDTOBUILD
 
492
        self.storeBuildInfo(self, librarian, slave_status)
 
493
        self.buildqueue_record.builder.cleanSlave()
 
494
        self.notify()
 
495
        self.buildqueue_record.destroySelf()
222
496
 
223
497
    def _handleStatus_DEPFAIL(self, librarian, slave_status, logger):
224
 
        return BuildBase._handleStatus_DEPFAIL(
225
 
            self, librarian, slave_status, logger)
 
498
        """Handle a package that had missing dependencies.
 
499
 
 
500
        Build has failed by missing dependencies, set the job status as
 
501
        MANUALDEPWAIT, store available information, remove BuildQueue
 
502
        entry and release builder slave for another job.
 
503
        """
 
504
        self.status = BuildStatus.MANUALDEPWAIT
 
505
        self.storeBuildInfo(self, librarian, slave_status)
 
506
        logger.critical("***** %s is MANUALDEPWAIT *****"
 
507
                        % self.buildqueue_record.builder.name)
 
508
        self.buildqueue_record.builder.cleanSlave()
 
509
        self.buildqueue_record.destroySelf()
226
510
 
227
511
    def _handleStatus_CHROOTFAIL(self, librarian, slave_status, logger):
228
 
        return BuildBase._handleStatus_CHROOTFAIL(
229
 
            self, librarian, slave_status, logger)
 
512
        """Handle a package that had failed when unpacking the CHROOT.
 
513
 
 
514
        Build has failed when installing the current CHROOT, mark the
 
515
        job as CHROOTFAIL, store available information, remove BuildQueue
 
516
        and release the builder.
 
517
        """
 
518
        self.status = BuildStatus.CHROOTWAIT
 
519
        self.storeBuildInfo(self, librarian, slave_status)
 
520
        logger.critical("***** %s is CHROOTWAIT *****" %
 
521
                        self.buildqueue_record.builder.name)
 
522
        self.buildqueue_record.builder.cleanSlave()
 
523
        self.notify()
 
524
        self.buildqueue_record.destroySelf()
230
525
 
231
526
    def _handleStatus_BUILDERFAIL(self, librarian, slave_status, logger):
232
 
        return BuildBase._handleStatus_BUILDERFAIL(
233
 
            self, librarian, slave_status, logger)
 
527
        """Handle builder failures.
 
528
 
 
529
        Build has been failed when trying to build the target package,
 
530
        The environment is working well, so mark the job as NEEDSBUILD again
 
531
        and 'clean' the builder to do another jobs.
 
532
        """
 
533
        logger.warning("***** %s has failed *****"
 
534
                       % self.buildqueue_record.builder.name)
 
535
        self.buildqueue_record.builder.failBuilder(
 
536
            "Builder returned BUILDERFAIL when asked for its status")
 
537
        # simply reset job
 
538
        self.storeBuildInfo(self, librarian, slave_status)
 
539
        self.buildqueue_record.reset()
234
540
 
235
541
    def _handleStatus_GIVENBACK(self, librarian, slave_status, logger):
236
 
        return BuildBase._handleStatus_GIVENBACK(
237
 
            self, librarian, slave_status, logger)
 
542
        """Handle automatic retry requested by builder.
 
543
 
 
544
        GIVENBACK pseudo-state represents a request for automatic retry
 
545
        later, the build records is delayed by reducing the lastscore to
 
546
        ZERO.
 
547
        """
 
548
        logger.warning("***** %s is GIVENBACK by %s *****"
 
549
                       % (self.buildqueue_record.specific_job.build.title,
 
550
                          self.buildqueue_record.builder.name))
 
551
        self.storeBuildInfo(self, librarian, slave_status)
 
552
        # XXX cprov 2006-05-30: Currently this information is not
 
553
        # properly presented in the Web UI. We will discuss it in
 
554
        # the next Paris Summit, infinity has some ideas about how
 
555
        # to use this content. For now we just ensure it's stored.
 
556
        self.buildqueue_record.builder.cleanSlave()
 
557
        self.buildqueue_record.reset()
238
558
 
239
559
 
240
560
class PackageBuildSet: