launchpad-reviewers team mailing list archive
-
launchpad-reviewers team
-
Mailing list archive
-
Message #15811
[Merge] lp:~wgrant/launchpad/builderinteractor-extract into lp:launchpad
William Grant has proposed merging lp:~wgrant/launchpad/builderinteractor-extract into lp:launchpad with lp:~wgrant/launchpad/builderinteractor-c-b-b as a prerequisite.
Commit message:
Extract BuilderInteractor and BuilderSlave to lp.buildmaster.interactor.
Requested reviews:
Launchpad code reviewers (launchpad-reviewers)
For more details, see:
https://code.launchpad.net/~wgrant/launchpad/builderinteractor-extract/+merge/182317
Extract BuilderInteractor and BuilderSlave to lp.buildmaster.interactor. They're only used by buildd-manager itself.
--
https://code.launchpad.net/~wgrant/launchpad/builderinteractor-extract/+merge/182317
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~wgrant/launchpad/builderinteractor-extract into lp:launchpad.
=== modified file 'lib/lp/buildmaster/doc/buildfarmjobbehavior.txt'
--- lib/lp/buildmaster/doc/buildfarmjobbehavior.txt 2013-08-27 09:39:36 +0000
+++ lib/lp/buildmaster/doc/buildfarmjobbehavior.txt 2013-08-27 09:39:36 +0000
@@ -96,7 +96,7 @@
idle, such as making a call to log the start of a build, will raise an
appropriate exception.
- >>> from lp.buildmaster.model.builder import BuilderInteractor
+ >>> from lp.buildmaster.interactor import BuilderInteractor
>>> interactor = BuilderInteractor(bob)
>>> interactor.current_build_behavior.logStartBuild(None)
Traceback (most recent call last):
=== added file 'lib/lp/buildmaster/interactor.py'
--- lib/lp/buildmaster/interactor.py 1970-01-01 00:00:00 +0000
+++ lib/lp/buildmaster/interactor.py 2013-08-27 09:39:36 +0000
@@ -0,0 +1,719 @@
+# Copyright 2009-2013 Canonical Ltd. This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+__metaclass__ = type
+
+__all__ = [
+ 'BuilderInteractor',
+ 'ProxyWithConnectionTimeout',
+ ]
+
+import gzip
+import logging
+import os
+import socket
+import tempfile
+from urlparse import urlparse
+import xmlrpclib
+
+from twisted.internet import (
+ defer,
+ reactor as default_reactor,
+ )
+from twisted.web import xmlrpc
+from twisted.web.client import downloadPage
+from zope.component import getUtility
+from zope.security.proxy import (
+ isinstance as zope_isinstance,
+ removeSecurityProxy,
+ )
+
+from lp.buildmaster.interfaces.builder import (
+ BuildDaemonError,
+ BuildSlaveFailure,
+ CannotFetchFile,
+ CannotResumeHost,
+ CorruptBuildCookie,
+ )
+from lp.buildmaster.model.buildfarmjobbehavior import IdleBuildBehavior
+from lp.services.config import config
+from lp.services.helpers import filenameToContentType
+from lp.services.librarian.interfaces import ILibraryFileAliasSet
+from lp.services.librarian.utils import copy_and_close
+from lp.services.twistedsupport import cancel_on_timeout
+from lp.services.twistedsupport.processmonitor import ProcessWithTimeout
+from lp.services.webapp import urlappend
+
+
+class QuietQueryFactory(xmlrpc._QueryFactory):
+ """XMLRPC client factory that doesn't splatter the log with junk."""
+ noisy = False
+
+
+class ProxyWithConnectionTimeout(xmlrpc.Proxy):
+ """Extend Twisted's Proxy to provide a configurable connection timeout."""
+
+ def __init__(self, url, user=None, password=None, allowNone=False,
+ useDateTime=False, timeout=None):
+ xmlrpc.Proxy.__init__(
+ self, url, user, password, allowNone, useDateTime)
+ self.timeout = timeout
+
+ def callRemote(self, method, *args):
+ """Basically a carbon copy of the parent but passes the timeout
+ to connectTCP."""
+
+ def cancel(d):
+ factory.deferred = None
+ connector.disconnect()
+ factory = self.queryFactory(
+ self.path, self.host, method, self.user,
+ self.password, self.allowNone, args, cancel, self.useDateTime)
+ if self.secure:
+ from twisted.internet import ssl
+ connector = default_reactor.connectSSL(
+ self.host, self.port or 443, factory,
+ ssl.ClientContextFactory(),
+ timeout=self.timeout)
+ else:
+ connector = default_reactor.connectTCP(
+ self.host, self.port or 80, factory,
+ timeout=self.timeout)
+ return factory.deferred
+
+
+class BuilderSlave(object):
+ """Add in a few useful methods for the XMLRPC slave.
+
+ :ivar url: The URL of the actual builder. The XML-RPC resource and
+ the filecache live beneath this.
+ """
+
+ # WARNING: If you change the API for this, you should also change the APIs
+ # of the mocks in soyuzbuilderhelpers to match. Otherwise, you will have
+ # many false positives in your test run and will most likely break
+ # production.
+
+ def __init__(self, proxy, builder_url, vm_host, timeout, reactor):
+ """Initialize a BuilderSlave.
+
+ :param proxy: An XML-RPC proxy, implementing 'callRemote'. It must
+ support passing and returning None objects.
+ :param builder_url: The URL of the builder.
+ :param vm_host: The VM host to use when resuming.
+ """
+ self.url = builder_url
+ self._vm_host = vm_host
+ self._file_cache_url = urlappend(builder_url, 'filecache')
+ self._server = proxy
+ self.timeout = timeout
+ self.reactor = reactor
+
+ @classmethod
+ def makeBuilderSlave(cls, builder_url, vm_host, timeout, reactor=None,
+ proxy=None):
+ """Create and return a `BuilderSlave`.
+
+ :param builder_url: The URL of the slave buildd machine,
+ e.g. http://localhost:8221
+ :param vm_host: If the slave is virtual, specify its host machine
+ here.
+ :param reactor: Used by tests to override the Twisted reactor.
+ :param proxy: Used By tests to override the xmlrpc.Proxy.
+ """
+ rpc_url = urlappend(builder_url.encode('utf-8'), 'rpc')
+ if proxy is None:
+ server_proxy = ProxyWithConnectionTimeout(
+ rpc_url, allowNone=True, timeout=timeout)
+ server_proxy.queryFactory = QuietQueryFactory
+ else:
+ server_proxy = proxy
+ return cls(server_proxy, builder_url, vm_host, timeout, reactor)
+
+ def _with_timeout(self, d):
+ return cancel_on_timeout(d, self.timeout, self.reactor)
+
+ def abort(self):
+ """Abort the current build."""
+ return self._with_timeout(self._server.callRemote('abort'))
+
+ def clean(self):
+ """Clean up the waiting files and reset the slave's internal state."""
+ return self._with_timeout(self._server.callRemote('clean'))
+
+ def echo(self, *args):
+ """Echo the arguments back."""
+ return self._with_timeout(self._server.callRemote('echo', *args))
+
+ def info(self):
+ """Return the protocol version and the builder methods supported."""
+ return self._with_timeout(self._server.callRemote('info'))
+
+ def status(self):
+ """Return the status of the build daemon."""
+ return self._with_timeout(self._server.callRemote('status'))
+
+ def ensurepresent(self, sha1sum, url, username, password):
+ # XXX: Nothing external calls this. Make it private.
+ """Attempt to ensure the given file is present."""
+ return self._with_timeout(self._server.callRemote(
+ 'ensurepresent', sha1sum, url, username, password))
+
+ def getFile(self, sha_sum, file_to_write):
+ """Fetch a file from the builder.
+
+ :param sha_sum: The sha of the file (which is also its name on the
+ builder)
+ :param file_to_write: A file name or file-like object to write
+ the file to
+ :return: A Deferred that calls back when the download is done, or
+ errback with the error string.
+ """
+ file_url = urlappend(self._file_cache_url, sha_sum).encode('utf8')
+ # If desired we can pass a param "timeout" here but let's leave
+ # it at the default value if it becomes obvious we need to
+ # change it.
+ return downloadPage(file_url, file_to_write, followRedirect=0)
+
+ def getFiles(self, filemap):
+ """Fetch many files from the builder.
+
+ :param filemap: A Dictionary containing key values of the builder
+ file name to retrieve, which maps to a value containing the
+ file name or file object to write the file to.
+
+ :return: A DeferredList that calls back when the download is done.
+ """
+ dl = defer.gatherResults([
+ self.getFile(builder_file, filemap[builder_file])
+ for builder_file in filemap])
+ return dl
+
+ def resume(self, clock=None):
+ """Resume the builder in an asynchronous fashion.
+
+ We use the builddmaster configuration 'socket_timeout' as
+ the process timeout.
+
+ :param clock: An optional twisted.internet.task.Clock to override
+ the default clock. For use in tests.
+
+ :return: a Deferred that returns a
+ (stdout, stderr, subprocess exitcode) triple
+ """
+ url_components = urlparse(self.url)
+ buildd_name = url_components.hostname.split('.')[0]
+ resume_command = config.builddmaster.vm_resume_command % {
+ 'vm_host': self._vm_host,
+ 'buildd_name': buildd_name}
+ # Twisted API requires string but the configuration provides unicode.
+ resume_argv = [
+ term.encode('utf-8') for term in resume_command.split()]
+ d = defer.Deferred()
+ p = ProcessWithTimeout(d, self.timeout, clock=clock)
+ p.spawnProcess(resume_argv[0], tuple(resume_argv))
+ return d
+
+ def cacheFile(self, logger, libraryfilealias):
+ """Make sure that the file at 'libraryfilealias' is on the slave.
+
+ :param logger: A python `Logger` object.
+ :param libraryfilealias: An `ILibraryFileAlias`.
+ """
+ url = libraryfilealias.http_url
+ logger.info(
+ "Asking builder on %s to ensure it has file %s (%s, %s)" % (
+ self._file_cache_url, libraryfilealias.filename, url,
+ libraryfilealias.content.sha1))
+ return self.sendFileToSlave(libraryfilealias.content.sha1, url)
+
+ def sendFileToSlave(self, sha1, url, username="", password=""):
+ """Helper to send the file at 'url' with 'sha1' to this builder."""
+ d = self.ensurepresent(sha1, url, username, password)
+
+ def check_present((present, info)):
+ if not present:
+ raise CannotFetchFile(url, info)
+ return d.addCallback(check_present)
+
+ def build(self, buildid, builder_type, chroot_sha1, filemap, args):
+ """Build a thing on this build slave.
+
+ :param buildid: A string identifying this build.
+ :param builder_type: The type of builder needed.
+ :param chroot_sha1: XXX
+ :param filemap: A dictionary mapping from paths to SHA-1 hashes of
+ the file contents.
+ :param args: A dictionary of extra arguments. The contents depend on
+ the build job type.
+ """
+ d = self._with_timeout(self._server.callRemote(
+ 'build', buildid, builder_type, chroot_sha1, filemap, args))
+
+ def got_fault(failure):
+ failure.trap(xmlrpclib.Fault)
+ raise BuildSlaveFailure(failure.value)
+ return d.addErrback(got_fault)
+
+
+class BuilderInteractor(object):
+
+ _cached_build_behavior = None
+ _cached_currentjob = None
+
+ _cached_slave = None
+ _cached_slave_attrs = None
+
+ # Tests can override current_build_behavior and slave.
+ _override_behavior = None
+ _override_slave = None
+
+ def __init__(self, builder, override_slave=None, override_behavior=None):
+ self.builder = builder
+ self._override_slave = override_slave
+ self._override_behavior = override_behavior
+
+ @property
+ def slave(self):
+ """See IBuilder."""
+ if self._override_slave is not None:
+ return self._override_slave
+ # The slave cache is invalidated when the builder's URL, VM host
+ # or virtualisation change.
+ new_slave_attrs = (
+ self.builder.url, self.builder.vm_host, self.builder.virtualized)
+ if self._cached_slave_attrs != new_slave_attrs:
+ if self.builder.virtualized:
+ timeout = config.builddmaster.virtualized_socket_timeout
+ else:
+ timeout = config.builddmaster.socket_timeout
+ self._cached_slave = BuilderSlave.makeBuilderSlave(
+ self.builder.url, self.builder.vm_host, timeout)
+ self._cached_slave_attrs = new_slave_attrs
+ return self._cached_slave
+
+ @property
+ def current_build_behavior(self):
+ """Return the current build behavior."""
+ if self._override_behavior is not None:
+ return self._override_behavior
+ # The current_build_behavior cache is invalidated when
+ # builder.currentjob changes.
+ currentjob = self.builder.currentjob
+ if currentjob is None:
+ if not isinstance(
+ self._cached_build_behavior, IdleBuildBehavior):
+ self._cached_build_behavior = IdleBuildBehavior()
+ elif currentjob != self._cached_currentjob:
+ self._cached_build_behavior = currentjob.required_build_behavior
+ self._cached_build_behavior.setBuilderInteractor(self)
+ self._cached_currentjob = currentjob
+ return self._cached_build_behavior
+
+ def slaveStatus(self):
+ """Get the slave status for this builder.
+
+ :return: A Deferred which fires when the slave dialog is complete.
+ Its value is a dict containing at least builder_status, but
+ potentially other values included by the current build
+ behavior.
+ """
+ d = self.slave.status()
+
+ def got_status(status_sentence):
+ status = {'builder_status': status_sentence[0]}
+
+ # Extract detailed status and log information if present.
+ # Although build_id is also easily extractable here, there is no
+ # valid reason for anything to use it, so we exclude it.
+ if status['builder_status'] == 'BuilderStatus.WAITING':
+ status['build_status'] = status_sentence[1]
+ else:
+ if status['builder_status'] == 'BuilderStatus.BUILDING':
+ status['logtail'] = status_sentence[2]
+
+ self.current_build_behavior.updateSlaveStatus(
+ status_sentence, status)
+ return status
+
+ return d.addCallback(got_status)
+
+ def slaveStatusSentence(self):
+ """Get the slave status sentence for this builder.
+
+ :return: A Deferred which fires when the slave dialog is complete.
+ Its value is a tuple with the first element containing the
+ slave status, build_id-queue-id and then optionally more
+ elements depending on the status.
+ """
+ return self.slave.status()
+
+ def verifySlaveBuildCookie(self, slave_build_id):
+ """Verify that a slave's build cookie is consistent.
+
+ This should delegate to the current `IBuildFarmJobBehavior`.
+ """
+ return self.current_build_behavior.verifySlaveBuildCookie(
+ slave_build_id)
+
+ def isAvailable(self):
+ """Whether or not a builder is available for building new jobs.
+
+ :return: A Deferred that fires with True or False, depending on
+ whether the builder is available or not.
+ """
+ if not self.builder.builderok:
+ return defer.succeed(False)
+ d = self.slaveStatusSentence()
+
+ def catch_fault(failure):
+ failure.trap(xmlrpclib.Fault, socket.error)
+ return False
+
+ def check_available(status):
+ return status[0] == 'BuilderStatus.IDLE'
+ return d.addCallbacks(check_available, catch_fault)
+
+ def rescueIfLost(self, logger=None):
+ """Reset the slave if its job information doesn't match the DB.
+
+ This checks the build ID reported in the slave status against the
+ database. If it isn't building what we think it should be, the current
+ build will be aborted and the slave cleaned in preparation for a new
+ task. The decision about the slave's correctness is left up to
+ `IBuildFarmJobBehavior.verifySlaveBuildCookie`.
+
+ :return: A Deferred that fires when the dialog with the slave is
+ finished. It does not have a return value.
+ """
+ # 'ident_position' dict relates the position of the job identifier
+ # token in the sentence received from status(), according to the
+ # two statuses we care about. See lp:launchpad-buildd
+ # for further information about sentence format.
+ ident_position = {
+ 'BuilderStatus.BUILDING': 1,
+ 'BuilderStatus.WAITING': 2
+ }
+
+ d = self.slaveStatusSentence()
+
+ def got_status(status_sentence):
+ """After we get the status, clean if we have to.
+
+ Always return status_sentence.
+ """
+ # Isolate the BuilderStatus string, always the first token in
+ # IBuilder.slaveStatusSentence().
+ status = status_sentence[0]
+
+ # If the cookie test below fails, it will request an abort of the
+ # builder. This will leave the builder in the aborted state and
+ # with no assigned job, and we should now "clean" the slave which
+ # will reset its state back to IDLE, ready to accept new builds.
+ # This situation is usually caused by a temporary loss of
+ # communications with the slave and the build manager had to reset
+ # the job.
+ if (status == 'BuilderStatus.ABORTED'
+ and self.builder.currentjob is None):
+ if not self.builder.virtualized:
+ # We can't reset non-virtual builders reliably as the
+ # abort() function doesn't kill the actual build job,
+ # only the sbuild process! All we can do here is fail
+ # the builder with a message indicating the problem and
+ # wait for an admin to reboot it.
+ self.builder.failBuilder(
+ "Non-virtual builder in ABORTED state, requires admin "
+ "to restart")
+ return "dummy status"
+ if logger is not None:
+ logger.info(
+ "Builder '%s' being cleaned up from ABORTED" %
+ (self.builder.name,))
+ d = self.cleanSlave()
+ return d.addCallback(lambda ignored: status_sentence)
+ else:
+ return status_sentence
+
+ def rescue_slave(status_sentence):
+ # If slave is not building nor waiting, it's not in need of
+ # rescuing.
+ status = status_sentence[0]
+ if status not in ident_position.keys():
+ return
+ slave_build_id = status_sentence[ident_position[status]]
+ try:
+ self.verifySlaveBuildCookie(slave_build_id)
+ except CorruptBuildCookie as reason:
+ if status == 'BuilderStatus.WAITING':
+ d = self.cleanSlave()
+ else:
+ d = self.requestAbort()
+
+ def log_rescue(ignored):
+ if logger:
+ logger.info(
+ "Builder '%s' rescued from '%s': '%s'" %
+ (self.builder.name, slave_build_id, reason))
+ return d.addCallback(log_rescue)
+
+ d.addCallback(got_status)
+ d.addCallback(rescue_slave)
+ return d
+
+ def updateStatus(self, logger=None):
+ """Update the builder's status by probing it.
+
+ :return: A Deferred that fires when the dialog with the slave is
+ finished. It does not have a return value.
+ """
+ if logger:
+ logger.debug('Checking %s' % self.builder.name)
+
+ return self.rescueIfLost(logger)
+
+ def cleanSlave(self):
+ """Clean any temporary files from the slave.
+
+ :return: A Deferred that fires when the dialog with the slave is
+ finished. It does not have a return value.
+ """
+ return self.slave.clean()
+
+ def requestAbort(self):
+ """Ask that a build be aborted.
+
+ This takes place asynchronously: Actually killing everything running
+ can take some time so the slave status should be queried again to
+ detect when the abort has taken effect. (Look for status ABORTED).
+
+ :return: A Deferred that fires when the dialog with the slave is
+ finished. It does not have a return value.
+ """
+ return self.slave.abort()
+
+ def resumeSlaveHost(self):
+ """Resume the slave host to a known good condition.
+
+ Issues 'builddmaster.vm_resume_command' specified in the configuration
+ to resume the slave.
+
+ :raises: CannotResumeHost: if builder is not virtual or if the
+ configuration command has failed.
+
+ :return: A Deferred that fires when the resume operation finishes,
+ whose value is a (stdout, stderr) tuple for success, or a Failure
+ whose value is a CannotResumeHost exception.
+ """
+ if not self.builder.virtualized:
+ return defer.fail(CannotResumeHost('Builder is not virtualized.'))
+
+ if not self.builder.vm_host:
+ return defer.fail(CannotResumeHost('Undefined vm_host.'))
+
+ logger = self._getSlaveScannerLogger()
+ logger.info("Resuming %s (%s)" % (self.builder.name, self.builder.url))
+
+ d = self.slave.resume()
+
+ def got_resume_ok((stdout, stderr, returncode)):
+ return stdout, stderr
+
+ def got_resume_bad(failure):
+ stdout, stderr, code = failure.value
+ raise CannotResumeHost(
+ "Resuming failed:\nOUT:\n%s\nERR:\n%s\n" % (stdout, stderr))
+
+ return d.addCallback(got_resume_ok).addErrback(got_resume_bad)
+
+ def _startBuild(self, build_queue_item, logger):
+ """Start a build on this builder.
+
+ :param build_queue_item: A BuildQueueItem to build.
+ :param logger: A logger to be used to log diagnostic information.
+
+ :return: A Deferred that fires after the dispatch has completed whose
+ value is None, or a Failure that contains an exception
+ explaining what went wrong.
+ """
+ needed_bfjb = type(removeSecurityProxy(
+ build_queue_item.required_build_behavior))
+ if not zope_isinstance(self.current_build_behavior, needed_bfjb):
+ raise AssertionError(
+ "Inappropriate IBuildFarmJobBehavior: %r is not a %r" %
+ (self.current_build_behavior, needed_bfjb))
+ self.current_build_behavior.logStartBuild(logger)
+
+ # Make sure the request is valid; an exception is raised if it's not.
+ self.current_build_behavior.verifyBuildRequest(logger)
+
+ # Set the build behavior depending on the provided build queue item.
+ if not self.builder.builderok:
+ raise BuildDaemonError(
+ "Attempted to start a build on a known-bad builder.")
+
+ # If we are building a virtual build, resume the virtual machine.
+ if self.builder.virtualized:
+ d = self.resumeSlaveHost()
+ else:
+ d = defer.succeed(None)
+
+ def ping_done(ignored):
+ return self.current_build_behavior.dispatchBuildToSlave(
+ build_queue_item.id, logger)
+
+ def resume_done(ignored):
+ # Before we try and contact the resumed slave, we're going
+ # to send it a message. This is to ensure it's accepting
+ # packets from the outside world, because testing has shown
+ # that the first packet will randomly fail for no apparent
+ # reason. This could be a quirk of the Xen guest, we're not
+ # sure. We also don't care about the result from this message,
+ # just that it's sent, hence the "addBoth".
+ # See bug 586359.
+ if self.builder.virtualized:
+ d = self.slave.echo("ping")
+ else:
+ d = defer.succeed(None)
+ d.addBoth(ping_done)
+ return d
+
+ d.addCallback(resume_done)
+ return d
+
+ def _dispatchBuildCandidate(self, candidate):
+ """Dispatch the pending job to the associated buildd slave.
+
+ This method can only be executed in the builddmaster machine, since
+ it will actually issues the XMLRPC call to the buildd-slave.
+
+ :param candidate: The job to dispatch.
+ """
+ logger = self._getSlaveScannerLogger()
+ # Using maybeDeferred ensures that any exceptions are also
+ # wrapped up and caught later.
+ d = defer.maybeDeferred(self._startBuild, candidate, logger)
+ return d
+
+ def resetOrFail(self, logger, exception):
+ """Handle "confirmed" build slave failures.
+
+ Call this when there have been multiple failures that are not just
+ the fault of failing jobs, or when the builder has entered an
+ ABORTED state without having been asked to do so.
+
+ In case of a virtualized/PPA buildd slave an attempt will be made
+ to reset it (using `resumeSlaveHost`).
+
+ Conversely, a non-virtualized buildd slave will be (marked as)
+ failed straightaway (using `failBuilder`).
+
+ :param logger: The logger object to be used for logging.
+ :param exception: An exception to be used for logging.
+ :return: A Deferred that fires after the virtual slave was resumed
+ or immediately if it's a non-virtual slave.
+ """
+ error_message = str(exception)
+ if self.builder.virtualized:
+ # Virtualized/PPA builder: attempt a reset, unless the failure
+ # was itself a failure to reset. (In that case, the slave
+ # scanner will try again until we reach the failure threshold.)
+ if not isinstance(exception, CannotResumeHost):
+ logger.warn(
+ "Resetting builder: %s -- %s" % (
+ self.builder.url, error_message),
+ exc_info=True)
+ return self.resumeSlaveHost()
+ else:
+ # XXX: This should really let the failure bubble up to the
+ # scan() method that does the failure counting.
+ # Mark builder as 'failed'.
+ logger.warn(
+ "Disabling builder: %s -- %s" % (
+ self.builder.url, error_message))
+ self.builder.failBuilder(error_message)
+ return defer.succeed(None)
+
+ def findAndStartJob(self):
+ """Find a job to run and send it to the buildd slave.
+
+ :return: A Deferred whose value is the `IBuildQueue` instance
+ found or None if no job was found.
+ """
+ # XXX This method should be removed in favour of two separately
+ # called methods that find and dispatch the job. It will
+ # require a lot of test fixing.
+ logger = self._getSlaveScannerLogger()
+ candidate = self.builder.acquireBuildCandidate()
+
+ if candidate is None:
+ logger.debug("No build candidates available for builder.")
+ return defer.succeed(None)
+
+ d = self._dispatchBuildCandidate(candidate)
+ return d.addCallback(lambda ignored: candidate)
+
+ def updateBuild(self, queueItem):
+ """Verify the current build job status.
+
+ Perform the required actions for each state.
+
+ :return: A Deferred that fires when the slave dialog is finished.
+ """
+ return self.current_build_behavior.updateBuild(queueItem)
+
+ def transferSlaveFileToLibrarian(self, file_sha1, filename, private):
+ """Transfer a file from the slave to the librarian.
+
+ :param file_sha1: The file's sha1, which is how the file is addressed
+ in the slave XMLRPC protocol. Specially, the file_sha1 'buildlog'
+ will cause the build log to be retrieved and gzipped.
+ :param filename: The name of the file to be given to the librarian
+ file alias.
+ :param private: True if the build is for a private archive.
+ :return: A Deferred that calls back with a librarian file alias.
+ """
+ out_file_fd, out_file_name = tempfile.mkstemp(suffix=".buildlog")
+ out_file = os.fdopen(out_file_fd, "r+")
+
+ def got_file(ignored, filename, out_file, out_file_name):
+ try:
+ # If the requested file is the 'buildlog' compress it
+ # using gzip before storing in Librarian.
+ if file_sha1 == 'buildlog':
+ out_file = open(out_file_name)
+ filename += '.gz'
+ out_file_name += '.gz'
+ gz_file = gzip.GzipFile(out_file_name, mode='wb')
+ copy_and_close(out_file, gz_file)
+ os.remove(out_file_name.replace('.gz', ''))
+
+ # Reopen the file, seek to its end position, count and seek
+ # to beginning, ready for adding to the Librarian.
+ out_file = open(out_file_name)
+ out_file.seek(0, 2)
+ bytes_written = out_file.tell()
+ out_file.seek(0)
+
+ library_file = getUtility(ILibraryFileAliasSet).create(
+ filename, bytes_written, out_file,
+ contentType=filenameToContentType(filename),
+ restricted=private)
+ finally:
+ # Remove the temporary file. getFile() closes the file
+ # object.
+ os.remove(out_file_name)
+
+ return library_file.id
+
+ d = self.slave.getFile(file_sha1, out_file)
+ d.addCallback(got_file, filename, out_file, out_file_name)
+ return d
+
+ def _getSlaveScannerLogger(self):
+ """Return the logger instance from buildd-slave-scanner.py."""
+ # XXX cprov 20071120: Ideally the Launchpad logging system
+ # should be able to configure the root-logger instead of creating
+ # a new object, then the logger lookups won't require the specific
+ # name argument anymore. See bug 164203.
+ logger = logging.getLogger('slave-scanner')
+ return logger
=== modified file 'lib/lp/buildmaster/manager.py'
--- lib/lp/buildmaster/manager.py 2013-08-19 23:23:19 +0000
+++ lib/lp/buildmaster/manager.py 2013-08-27 09:39:36 +0000
@@ -23,6 +23,7 @@
from zope.component import getUtility
from lp.buildmaster.enums import BuildStatus
+from lp.buildmaster.interactor import BuilderInteractor
from lp.buildmaster.interfaces.builder import (
BuildDaemonError,
BuildSlaveFailure,
@@ -33,10 +34,7 @@
from lp.buildmaster.interfaces.buildfarmjobbehavior import (
BuildBehaviorMismatch,
)
-from lp.buildmaster.model.builder import (
- Builder,
- BuilderInteractor,
- )
+from lp.buildmaster.model.builder import Builder
from lp.services.propertycache import get_property_cache
=== modified file 'lib/lp/buildmaster/model/builder.py'
--- lib/lp/buildmaster/model/builder.py 2013-08-27 09:39:36 +0000
+++ lib/lp/buildmaster/model/builder.py 2013-08-27 09:39:36 +0000
@@ -5,20 +5,10 @@
__all__ = [
'Builder',
- 'BuilderInteractor',
'BuilderSet',
- 'ProxyWithConnectionTimeout',
- 'rescueBuilderIfLost',
- 'updateBuilderStatus',
]
-import gzip
import logging
-import os
-import socket
-import tempfile
-from urlparse import urlparse
-import xmlrpclib
from sqlobject import (
BoolCol,
@@ -33,38 +23,21 @@
Sum,
)
import transaction
-from twisted.internet import (
- defer,
- reactor as default_reactor,
- )
-from twisted.web import xmlrpc
-from twisted.web.client import downloadPage
from zope.component import getUtility
from zope.interface import implements
-from zope.security.proxy import (
- isinstance as zope_isinstance,
- removeSecurityProxy,
- )
from lp.app.errors import NotFoundError
from lp.buildmaster.interfaces.builder import (
- BuildDaemonError,
- BuildSlaveFailure,
- CannotFetchFile,
- CannotResumeHost,
- CorruptBuildCookie,
IBuilder,
IBuilderSet,
)
from lp.buildmaster.interfaces.buildfarmjob import IBuildFarmJobSet
from lp.buildmaster.interfaces.buildqueue import IBuildQueueSet
-from lp.buildmaster.model.buildfarmjobbehavior import IdleBuildBehavior
from lp.buildmaster.model.buildqueue import (
BuildQueue,
specific_job_classes,
)
from lp.registry.interfaces.person import validate_public_person
-from lp.services.config import config
from lp.services.database.interfaces import (
ISlaveStore,
IStore,
@@ -73,15 +46,9 @@
SQLBase,
sqlvalues,
)
-from lp.services.helpers import filenameToContentType
from lp.services.job.interfaces.job import JobStatus
from lp.services.job.model.job import Job
-from lp.services.librarian.interfaces import ILibraryFileAliasSet
-from lp.services.librarian.utils import copy_and_close
from lp.services.propertycache import cachedproperty
-from lp.services.twistedsupport import cancel_on_timeout
-from lp.services.twistedsupport.processmonitor import ProcessWithTimeout
-from lp.services.webapp import urlappend
# XXX Michael Nelson 2010-01-13 bug=491330
# These dependencies on soyuz will be removed when getBuildRecords()
# is moved.
@@ -93,691 +60,6 @@
from lp.soyuz.model.processor import Processor
-class QuietQueryFactory(xmlrpc._QueryFactory):
- """XMLRPC client factory that doesn't splatter the log with junk."""
- noisy = False
-
-
-class ProxyWithConnectionTimeout(xmlrpc.Proxy):
- """Extend Twisted's Proxy to provide a configurable connection timeout."""
-
- def __init__(self, url, user=None, password=None, allowNone=False,
- useDateTime=False, timeout=None):
- xmlrpc.Proxy.__init__(
- self, url, user, password, allowNone, useDateTime)
- self.timeout = timeout
-
- def callRemote(self, method, *args):
- """Basically a carbon copy of the parent but passes the timeout
- to connectTCP."""
-
- def cancel(d):
- factory.deferred = None
- connector.disconnect()
- factory = self.queryFactory(
- self.path, self.host, method, self.user,
- self.password, self.allowNone, args, cancel, self.useDateTime)
- if self.secure:
- from twisted.internet import ssl
- connector = default_reactor.connectSSL(
- self.host, self.port or 443, factory,
- ssl.ClientContextFactory(),
- timeout=self.timeout)
- else:
- connector = default_reactor.connectTCP(
- self.host, self.port or 80, factory,
- timeout=self.timeout)
- return factory.deferred
-
-
-class BuilderSlave(object):
- """Add in a few useful methods for the XMLRPC slave.
-
- :ivar url: The URL of the actual builder. The XML-RPC resource and
- the filecache live beneath this.
- """
-
- # WARNING: If you change the API for this, you should also change the APIs
- # of the mocks in soyuzbuilderhelpers to match. Otherwise, you will have
- # many false positives in your test run and will most likely break
- # production.
-
- def __init__(self, proxy, builder_url, vm_host, timeout, reactor):
- """Initialize a BuilderSlave.
-
- :param proxy: An XML-RPC proxy, implementing 'callRemote'. It must
- support passing and returning None objects.
- :param builder_url: The URL of the builder.
- :param vm_host: The VM host to use when resuming.
- """
- self.url = builder_url
- self._vm_host = vm_host
- self._file_cache_url = urlappend(builder_url, 'filecache')
- self._server = proxy
- self.timeout = timeout
- self.reactor = reactor
-
- @classmethod
- def makeBuilderSlave(cls, builder_url, vm_host, timeout, reactor=None,
- proxy=None):
- """Create and return a `BuilderSlave`.
-
- :param builder_url: The URL of the slave buildd machine,
- e.g. http://localhost:8221
- :param vm_host: If the slave is virtual, specify its host machine
- here.
- :param reactor: Used by tests to override the Twisted reactor.
- :param proxy: Used By tests to override the xmlrpc.Proxy.
- """
- rpc_url = urlappend(builder_url.encode('utf-8'), 'rpc')
- if proxy is None:
- server_proxy = ProxyWithConnectionTimeout(
- rpc_url, allowNone=True, timeout=timeout)
- server_proxy.queryFactory = QuietQueryFactory
- else:
- server_proxy = proxy
- return cls(server_proxy, builder_url, vm_host, timeout, reactor)
-
- def _with_timeout(self, d):
- return cancel_on_timeout(d, self.timeout, self.reactor)
-
- def abort(self):
- """Abort the current build."""
- return self._with_timeout(self._server.callRemote('abort'))
-
- def clean(self):
- """Clean up the waiting files and reset the slave's internal state."""
- return self._with_timeout(self._server.callRemote('clean'))
-
- def echo(self, *args):
- """Echo the arguments back."""
- return self._with_timeout(self._server.callRemote('echo', *args))
-
- def info(self):
- """Return the protocol version and the builder methods supported."""
- return self._with_timeout(self._server.callRemote('info'))
-
- def status(self):
- """Return the status of the build daemon."""
- return self._with_timeout(self._server.callRemote('status'))
-
- def ensurepresent(self, sha1sum, url, username, password):
- # XXX: Nothing external calls this. Make it private.
- """Attempt to ensure the given file is present."""
- return self._with_timeout(self._server.callRemote(
- 'ensurepresent', sha1sum, url, username, password))
-
- def getFile(self, sha_sum, file_to_write):
- """Fetch a file from the builder.
-
- :param sha_sum: The sha of the file (which is also its name on the
- builder)
- :param file_to_write: A file name or file-like object to write
- the file to
- :return: A Deferred that calls back when the download is done, or
- errback with the error string.
- """
- file_url = urlappend(self._file_cache_url, sha_sum).encode('utf8')
- # If desired we can pass a param "timeout" here but let's leave
- # it at the default value if it becomes obvious we need to
- # change it.
- return downloadPage(file_url, file_to_write, followRedirect=0)
-
- def getFiles(self, filemap):
- """Fetch many files from the builder.
-
- :param filemap: A Dictionary containing key values of the builder
- file name to retrieve, which maps to a value containing the
- file name or file object to write the file to.
-
- :return: A DeferredList that calls back when the download is done.
- """
- dl = defer.gatherResults([
- self.getFile(builder_file, filemap[builder_file])
- for builder_file in filemap])
- return dl
-
- def resume(self, clock=None):
- """Resume the builder in an asynchronous fashion.
-
- We use the builddmaster configuration 'socket_timeout' as
- the process timeout.
-
- :param clock: An optional twisted.internet.task.Clock to override
- the default clock. For use in tests.
-
- :return: a Deferred that returns a
- (stdout, stderr, subprocess exitcode) triple
- """
- url_components = urlparse(self.url)
- buildd_name = url_components.hostname.split('.')[0]
- resume_command = config.builddmaster.vm_resume_command % {
- 'vm_host': self._vm_host,
- 'buildd_name': buildd_name}
- # Twisted API requires string but the configuration provides unicode.
- resume_argv = [
- term.encode('utf-8') for term in resume_command.split()]
- d = defer.Deferred()
- p = ProcessWithTimeout(d, self.timeout, clock=clock)
- p.spawnProcess(resume_argv[0], tuple(resume_argv))
- return d
-
- def cacheFile(self, logger, libraryfilealias):
- """Make sure that the file at 'libraryfilealias' is on the slave.
-
- :param logger: A python `Logger` object.
- :param libraryfilealias: An `ILibraryFileAlias`.
- """
- url = libraryfilealias.http_url
- logger.info(
- "Asking builder on %s to ensure it has file %s (%s, %s)" % (
- self._file_cache_url, libraryfilealias.filename, url,
- libraryfilealias.content.sha1))
- return self.sendFileToSlave(libraryfilealias.content.sha1, url)
-
- def sendFileToSlave(self, sha1, url, username="", password=""):
- """Helper to send the file at 'url' with 'sha1' to this builder."""
- d = self.ensurepresent(sha1, url, username, password)
-
- def check_present((present, info)):
- if not present:
- raise CannotFetchFile(url, info)
- return d.addCallback(check_present)
-
- def build(self, buildid, builder_type, chroot_sha1, filemap, args):
- """Build a thing on this build slave.
-
- :param buildid: A string identifying this build.
- :param builder_type: The type of builder needed.
- :param chroot_sha1: XXX
- :param filemap: A dictionary mapping from paths to SHA-1 hashes of
- the file contents.
- :param args: A dictionary of extra arguments. The contents depend on
- the build job type.
- """
- d = self._with_timeout(self._server.callRemote(
- 'build', buildid, builder_type, chroot_sha1, filemap, args))
-
- def got_fault(failure):
- failure.trap(xmlrpclib.Fault)
- raise BuildSlaveFailure(failure.value)
- return d.addErrback(got_fault)
-
-
-# This is a separate function since MockBuilder needs to use it too.
-# Do not use it -- (Mock)Builder.rescueIfLost should be used instead.
-def rescueBuilderIfLost(interactor, logger=None):
- """See `IBuilder`."""
- # 'ident_position' dict relates the position of the job identifier
- # token in the sentence received from status(), according to the
- # two statuses we care about. See lp:launchpad-buildd
- # for further information about sentence format.
- ident_position = {
- 'BuilderStatus.BUILDING': 1,
- 'BuilderStatus.WAITING': 2
- }
-
- d = interactor.slaveStatusSentence()
-
- def got_status(status_sentence):
- """After we get the status, clean if we have to.
-
- Always return status_sentence.
- """
- # Isolate the BuilderStatus string, always the first token in
- # IBuilder.slaveStatusSentence().
- status = status_sentence[0]
-
- # If the cookie test below fails, it will request an abort of the
- # builder. This will leave the builder in the aborted state and
- # with no assigned job, and we should now "clean" the slave which
- # will reset its state back to IDLE, ready to accept new builds.
- # This situation is usually caused by a temporary loss of
- # communications with the slave and the build manager had to reset
- # the job.
- if (status == 'BuilderStatus.ABORTED'
- and interactor.builder.currentjob is None):
- if not interactor.builder.virtualized:
- # We can't reset non-virtual builders reliably as the
- # abort() function doesn't kill the actual build job,
- # only the sbuild process! All we can do here is fail
- # the builder with a message indicating the problem and
- # wait for an admin to reboot it.
- interactor.builder.failBuilder(
- "Non-virtual builder in ABORTED state, requires admin to "
- "restart")
- return "dummy status"
- if logger is not None:
- logger.info(
- "Builder '%s' being cleaned up from ABORTED" %
- (interactor.builder.name,))
- d = interactor.cleanSlave()
- return d.addCallback(lambda ignored: status_sentence)
- else:
- return status_sentence
-
- def rescue_slave(status_sentence):
- # If slave is not building nor waiting, it's not in need of rescuing.
- status = status_sentence[0]
- if status not in ident_position.keys():
- return
- slave_build_id = status_sentence[ident_position[status]]
- try:
- interactor.verifySlaveBuildCookie(slave_build_id)
- except CorruptBuildCookie as reason:
- if status == 'BuilderStatus.WAITING':
- d = interactor.cleanSlave()
- else:
- d = interactor.requestAbort()
-
- def log_rescue(ignored):
- if logger:
- logger.info(
- "Builder '%s' rescued from '%s': '%s'" %
- (interactor.builder.name, slave_build_id, reason))
- return d.addCallback(log_rescue)
-
- d.addCallback(got_status)
- d.addCallback(rescue_slave)
- return d
-
-
-def updateBuilderStatus(interactor, logger=None):
- """See `IBuilder`."""
- if logger:
- logger.debug('Checking %s' % interactor.builder.name)
-
- return interactor.rescueIfLost(logger)
-
-
-class BuilderInteractor(object):
-
- _cached_build_behavior = None
- _cached_currentjob = None
-
- _cached_slave = None
- _cached_slave_attrs = None
-
- # Tests can override current_build_behavior and slave.
- _override_behavior = None
- _override_slave = None
-
- def __init__(self, builder, override_slave=None, override_behavior=None):
- self.builder = builder
- self._override_slave = override_slave
- self._override_behavior = override_behavior
-
- @property
- def slave(self):
- """See IBuilder."""
- if self._override_slave is not None:
- return self._override_slave
- # The slave cache is invalidated when the builder's URL, VM host
- # or virtualisation change.
- new_slave_attrs = (
- self.builder.url, self.builder.vm_host, self.builder.virtualized)
- if self._cached_slave_attrs != new_slave_attrs:
- if self.builder.virtualized:
- timeout = config.builddmaster.virtualized_socket_timeout
- else:
- timeout = config.builddmaster.socket_timeout
- self._cached_slave = BuilderSlave.makeBuilderSlave(
- self.builder.url, self.builder.vm_host, timeout)
- self._cached_slave_attrs = new_slave_attrs
- return self._cached_slave
-
- @property
- def current_build_behavior(self):
- """Return the current build behavior."""
- if self._override_behavior is not None:
- return self._override_behavior
- # The current_build_behavior cache is invalidated when
- # builder.currentjob changes.
- currentjob = self.builder.currentjob
- if currentjob is None:
- if not isinstance(
- self._cached_build_behavior, IdleBuildBehavior):
- self._cached_build_behavior = IdleBuildBehavior()
- elif currentjob != self._cached_currentjob:
- self._cached_build_behavior = currentjob.required_build_behavior
- self._cached_build_behavior.setBuilderInteractor(self)
- self._cached_currentjob = currentjob
- return self._cached_build_behavior
-
- def slaveStatus(self):
- """Get the slave status for this builder.
-
- :return: A Deferred which fires when the slave dialog is complete.
- Its value is a dict containing at least builder_status, but
- potentially other values included by the current build
- behavior.
- """
- d = self.slave.status()
-
- def got_status(status_sentence):
- status = {'builder_status': status_sentence[0]}
-
- # Extract detailed status and log information if present.
- # Although build_id is also easily extractable here, there is no
- # valid reason for anything to use it, so we exclude it.
- if status['builder_status'] == 'BuilderStatus.WAITING':
- status['build_status'] = status_sentence[1]
- else:
- if status['builder_status'] == 'BuilderStatus.BUILDING':
- status['logtail'] = status_sentence[2]
-
- self.current_build_behavior.updateSlaveStatus(
- status_sentence, status)
- return status
-
- return d.addCallback(got_status)
-
- def slaveStatusSentence(self):
- """Get the slave status sentence for this builder.
-
- :return: A Deferred which fires when the slave dialog is complete.
- Its value is a tuple with the first element containing the
- slave status, build_id-queue-id and then optionally more
- elements depending on the status.
- """
- return self.slave.status()
-
- def verifySlaveBuildCookie(self, slave_build_id):
- """Verify that a slave's build cookie is consistent.
-
- This should delegate to the current `IBuildFarmJobBehavior`.
- """
- return self.current_build_behavior.verifySlaveBuildCookie(
- slave_build_id)
-
- def isAvailable(self):
- """Whether or not a builder is available for building new jobs.
-
- :return: A Deferred that fires with True or False, depending on
- whether the builder is available or not.
- """
- if not self.builder.builderok:
- return defer.succeed(False)
- d = self.slaveStatusSentence()
-
- def catch_fault(failure):
- failure.trap(xmlrpclib.Fault, socket.error)
- return False
-
- def check_available(status):
- return status[0] == 'BuilderStatus.IDLE'
- return d.addCallbacks(check_available, catch_fault)
-
- def rescueIfLost(self, logger=None):
- """Reset the slave if its job information doesn't match the DB.
-
- This checks the build ID reported in the slave status against the
- database. If it isn't building what we think it should be, the current
- build will be aborted and the slave cleaned in preparation for a new
- task. The decision about the slave's correctness is left up to
- `IBuildFarmJobBehavior.verifySlaveBuildCookie`.
-
- :return: A Deferred that fires when the dialog with the slave is
- finished. It does not have a return value.
- """
- return rescueBuilderIfLost(self, logger)
-
- def updateStatus(self, logger=None):
- """Update the builder's status by probing it.
-
- :return: A Deferred that fires when the dialog with the slave is
- finished. It does not have a return value.
- """
- return updateBuilderStatus(self, logger)
-
- def cleanSlave(self):
- """Clean any temporary files from the slave.
-
- :return: A Deferred that fires when the dialog with the slave is
- finished. It does not have a return value.
- """
- return self.slave.clean()
-
- def requestAbort(self):
- """Ask that a build be aborted.
-
- This takes place asynchronously: Actually killing everything running
- can take some time so the slave status should be queried again to
- detect when the abort has taken effect. (Look for status ABORTED).
-
- :return: A Deferred that fires when the dialog with the slave is
- finished. It does not have a return value.
- """
- return self.slave.abort()
-
- def resumeSlaveHost(self):
- """Resume the slave host to a known good condition.
-
- Issues 'builddmaster.vm_resume_command' specified in the configuration
- to resume the slave.
-
- :raises: CannotResumeHost: if builder is not virtual or if the
- configuration command has failed.
-
- :return: A Deferred that fires when the resume operation finishes,
- whose value is a (stdout, stderr) tuple for success, or a Failure
- whose value is a CannotResumeHost exception.
- """
- if not self.builder.virtualized:
- return defer.fail(CannotResumeHost('Builder is not virtualized.'))
-
- if not self.builder.vm_host:
- return defer.fail(CannotResumeHost('Undefined vm_host.'))
-
- logger = self._getSlaveScannerLogger()
- logger.info("Resuming %s (%s)" % (self.builder.name, self.builder.url))
-
- d = self.slave.resume()
-
- def got_resume_ok((stdout, stderr, returncode)):
- return stdout, stderr
-
- def got_resume_bad(failure):
- stdout, stderr, code = failure.value
- raise CannotResumeHost(
- "Resuming failed:\nOUT:\n%s\nERR:\n%s\n" % (stdout, stderr))
-
- return d.addCallback(got_resume_ok).addErrback(got_resume_bad)
-
- def _startBuild(self, build_queue_item, logger):
- """Start a build on this builder.
-
- :param build_queue_item: A BuildQueueItem to build.
- :param logger: A logger to be used to log diagnostic information.
-
- :return: A Deferred that fires after the dispatch has completed whose
- value is None, or a Failure that contains an exception
- explaining what went wrong.
- """
- needed_bfjb = type(removeSecurityProxy(
- build_queue_item.required_build_behavior))
- if not zope_isinstance(self.current_build_behavior, needed_bfjb):
- raise AssertionError(
- "Inappropriate IBuildFarmJobBehavior: %r is not a %r" %
- (self.current_build_behavior, needed_bfjb))
- self.current_build_behavior.logStartBuild(logger)
-
- # Make sure the request is valid; an exception is raised if it's not.
- self.current_build_behavior.verifyBuildRequest(logger)
-
- # Set the build behavior depending on the provided build queue item.
- if not self.builder.builderok:
- raise BuildDaemonError(
- "Attempted to start a build on a known-bad builder.")
-
- # If we are building a virtual build, resume the virtual machine.
- if self.builder.virtualized:
- d = self.resumeSlaveHost()
- else:
- d = defer.succeed(None)
-
- def ping_done(ignored):
- return self.current_build_behavior.dispatchBuildToSlave(
- build_queue_item.id, logger)
-
- def resume_done(ignored):
- # Before we try and contact the resumed slave, we're going
- # to send it a message. This is to ensure it's accepting
- # packets from the outside world, because testing has shown
- # that the first packet will randomly fail for no apparent
- # reason. This could be a quirk of the Xen guest, we're not
- # sure. We also don't care about the result from this message,
- # just that it's sent, hence the "addBoth".
- # See bug 586359.
- if self.builder.virtualized:
- d = self.slave.echo("ping")
- else:
- d = defer.succeed(None)
- d.addBoth(ping_done)
- return d
-
- d.addCallback(resume_done)
- return d
-
- def _dispatchBuildCandidate(self, candidate):
- """Dispatch the pending job to the associated buildd slave.
-
- This method can only be executed in the builddmaster machine, since
- it will actually issues the XMLRPC call to the buildd-slave.
-
- :param candidate: The job to dispatch.
- """
- logger = self._getSlaveScannerLogger()
- # Using maybeDeferred ensures that any exceptions are also
- # wrapped up and caught later.
- d = defer.maybeDeferred(self._startBuild, candidate, logger)
- return d
-
- def resetOrFail(self, logger, exception):
- """Handle "confirmed" build slave failures.
-
- Call this when there have been multiple failures that are not just
- the fault of failing jobs, or when the builder has entered an
- ABORTED state without having been asked to do so.
-
- In case of a virtualized/PPA buildd slave an attempt will be made
- to reset it (using `resumeSlaveHost`).
-
- Conversely, a non-virtualized buildd slave will be (marked as)
- failed straightaway (using `failBuilder`).
-
- :param logger: The logger object to be used for logging.
- :param exception: An exception to be used for logging.
- :return: A Deferred that fires after the virtual slave was resumed
- or immediately if it's a non-virtual slave.
- """
- error_message = str(exception)
- if self.builder.virtualized:
- # Virtualized/PPA builder: attempt a reset, unless the failure
- # was itself a failure to reset. (In that case, the slave
- # scanner will try again until we reach the failure threshold.)
- if not isinstance(exception, CannotResumeHost):
- logger.warn(
- "Resetting builder: %s -- %s" % (
- self.builder.url, error_message),
- exc_info=True)
- return self.resumeSlaveHost()
- else:
- # XXX: This should really let the failure bubble up to the
- # scan() method that does the failure counting.
- # Mark builder as 'failed'.
- logger.warn(
- "Disabling builder: %s -- %s" % (
- self.builder.url, error_message))
- self.builder.failBuilder(error_message)
- return defer.succeed(None)
-
- def findAndStartJob(self):
- """Find a job to run and send it to the buildd slave.
-
- :return: A Deferred whose value is the `IBuildQueue` instance
- found or None if no job was found.
- """
- # XXX This method should be removed in favour of two separately
- # called methods that find and dispatch the job. It will
- # require a lot of test fixing.
- logger = self._getSlaveScannerLogger()
- candidate = self.builder.acquireBuildCandidate()
-
- if candidate is None:
- logger.debug("No build candidates available for builder.")
- return defer.succeed(None)
-
- d = self._dispatchBuildCandidate(candidate)
- return d.addCallback(lambda ignored: candidate)
-
- def updateBuild(self, queueItem):
- """Verify the current build job status.
-
- Perform the required actions for each state.
-
- :return: A Deferred that fires when the slave dialog is finished.
- """
- return self.current_build_behavior.updateBuild(queueItem)
-
- def transferSlaveFileToLibrarian(self, file_sha1, filename, private):
- """Transfer a file from the slave to the librarian.
-
- :param file_sha1: The file's sha1, which is how the file is addressed
- in the slave XMLRPC protocol. Specially, the file_sha1 'buildlog'
- will cause the build log to be retrieved and gzipped.
- :param filename: The name of the file to be given to the librarian
- file alias.
- :param private: True if the build is for a private archive.
- :return: A Deferred that calls back with a librarian file alias.
- """
- out_file_fd, out_file_name = tempfile.mkstemp(suffix=".buildlog")
- out_file = os.fdopen(out_file_fd, "r+")
-
- def got_file(ignored, filename, out_file, out_file_name):
- try:
- # If the requested file is the 'buildlog' compress it
- # using gzip before storing in Librarian.
- if file_sha1 == 'buildlog':
- out_file = open(out_file_name)
- filename += '.gz'
- out_file_name += '.gz'
- gz_file = gzip.GzipFile(out_file_name, mode='wb')
- copy_and_close(out_file, gz_file)
- os.remove(out_file_name.replace('.gz', ''))
-
- # Reopen the file, seek to its end position, count and seek
- # to beginning, ready for adding to the Librarian.
- out_file = open(out_file_name)
- out_file.seek(0, 2)
- bytes_written = out_file.tell()
- out_file.seek(0)
-
- library_file = getUtility(ILibraryFileAliasSet).create(
- filename, bytes_written, out_file,
- contentType=filenameToContentType(filename),
- restricted=private)
- finally:
- # Remove the temporary file. getFile() closes the file
- # object.
- os.remove(out_file_name)
-
- return library_file.id
-
- d = self.slave.getFile(file_sha1, out_file)
- d.addCallback(got_file, filename, out_file, out_file_name)
- return d
-
- def _getSlaveScannerLogger(self):
- """Return the logger instance from buildd-slave-scanner.py."""
- # XXX cprov 20071120: Ideally the Launchpad logging system
- # should be able to configure the root-logger instead of creating
- # a new object, then the logger lookups won't require the specific
- # name argument anymore. See bug 164203.
- logger = logging.getLogger('slave-scanner')
- return logger
-
-
class Builder(SQLBase):
implements(IBuilder, IHasBuildRecords)
=== modified file 'lib/lp/buildmaster/tests/mock_slaves.py'
--- lib/lp/buildmaster/tests/mock_slaves.py 2013-08-27 09:39:36 +0000
+++ lib/lp/buildmaster/tests/mock_slaves.py 2013-08-27 09:39:36 +0000
@@ -32,11 +32,11 @@
from twisted.internet import defer
from twisted.web import xmlrpc
+from lp.buildmaster.interactor import BuilderSlave
from lp.buildmaster.interfaces.builder import (
CannotFetchFile,
CorruptBuildCookie,
)
-from lp.buildmaster.model.builder import BuilderSlave
from lp.services.config import config
from lp.testing.sampledata import I386_ARCHITECTURE_NAME
=== modified file 'lib/lp/buildmaster/tests/test_builder.py'
--- lib/lp/buildmaster/tests/test_builder.py 2013-08-27 09:39:36 +0000
+++ lib/lp/buildmaster/tests/test_builder.py 2013-08-27 09:39:36 +0000
@@ -29,21 +29,18 @@
)
from lp.buildmaster.enums import BuildStatus
+from lp.buildmaster.interactor import (
+ BuilderInteractor,
+ BuilderSlave,
+ ProxyWithConnectionTimeout,
+ )
from lp.buildmaster.interfaces.builder import (
CannotFetchFile,
CannotResumeHost,
IBuilder,
IBuilderSet,
)
-from lp.buildmaster.interfaces.buildfarmjobbehavior import (
- IBuildFarmJobBehavior,
- )
from lp.buildmaster.interfaces.buildqueue import IBuildQueueSet
-from lp.buildmaster.model.builder import (
- BuilderInteractor,
- BuilderSlave,
- ProxyWithConnectionTimeout,
- )
from lp.buildmaster.model.buildfarmjobbehavior import IdleBuildBehavior
from lp.buildmaster.model.buildqueue import BuildQueue
from lp.buildmaster.tests.mock_slaves import (
=== modified file 'lib/lp/buildmaster/tests/test_buildfarmjobbehavior.py'
--- lib/lp/buildmaster/tests/test_buildfarmjobbehavior.py 2013-08-27 09:39:36 +0000
+++ lib/lp/buildmaster/tests/test_buildfarmjobbehavior.py 2013-08-27 09:39:36 +0000
@@ -15,8 +15,8 @@
from lp.archiveuploader.uploadprocessor import parse_build_upload_leaf_name
from lp.buildmaster.enums import BuildStatus
+from lp.buildmaster.interactor import BuilderInteractor
from lp.buildmaster.interfaces.builder import CorruptBuildCookie
-from lp.buildmaster.model.builder import BuilderInteractor
from lp.buildmaster.model.buildfarmjobbehavior import BuildFarmJobBehaviorBase
from lp.buildmaster.tests.mock_slaves import WaitingSlave
from lp.registry.interfaces.pocket import PackagePublishingPocket
=== modified file 'lib/lp/buildmaster/tests/test_manager.py'
--- lib/lp/buildmaster/tests/test_manager.py 2013-08-19 23:23:19 +0000
+++ lib/lp/buildmaster/tests/test_manager.py 2013-08-27 09:39:36 +0000
@@ -25,6 +25,10 @@
from zope.security.proxy import removeSecurityProxy
from lp.buildmaster.enums import BuildStatus
+from lp.buildmaster.interactor import (
+ BuilderInteractor,
+ BuilderSlave,
+ )
from lp.buildmaster.interfaces.builder import IBuilderSet
from lp.buildmaster.interfaces.buildqueue import IBuildQueueSet
from lp.buildmaster.manager import (
@@ -33,11 +37,7 @@
NewBuildersScanner,
SlaveScanner,
)
-from lp.buildmaster.model.builder import (
- Builder,
- BuilderInteractor,
- BuilderSlave,
- )
+from lp.buildmaster.model.builder import Builder
from lp.buildmaster.tests.harness import BuilddManagerTestSetup
from lp.buildmaster.tests.mock_slaves import (
BrokenSlave,
=== modified file 'lib/lp/code/model/tests/test_recipebuilder.py'
--- lib/lp/code/model/tests/test_recipebuilder.py 2013-08-27 09:39:36 +0000
+++ lib/lp/code/model/tests/test_recipebuilder.py 2013-08-27 09:39:36 +0000
@@ -24,14 +24,11 @@
BuildFarmJobType,
BuildStatus,
)
+from lp.buildmaster.interactor import BuilderInteractor
from lp.buildmaster.interfaces.builder import CannotBuild
from lp.buildmaster.interfaces.buildfarmjobbehavior import (
IBuildFarmJobBehavior,
)
-from lp.buildmaster.model.builder import (
- BuilderInteractor,
- BuilderSlave,
- )
from lp.buildmaster.model.buildqueue import BuildQueue
from lp.buildmaster.tests.mock_slaves import (
MockBuilder,
@@ -374,8 +371,7 @@
self.addCleanup(config.pop, 'tmp_builddmaster_root')
queue_record.builder = self.factory.makeBuilder()
slave = WaitingSlave('BuildStatus.OK')
- self.patch(BuilderSlave, 'makeBuilderSlave', FakeMethod(slave))
- interactor = BuilderInteractor(queue_record.builder)
+ interactor = BuilderInteractor(queue_record.builder, slave)
return removeSecurityProxy(interactor.current_build_behavior)
def assertDeferredNotifyCount(self, status, behavior, expected_count):
=== modified file 'lib/lp/soyuz/tests/test_binarypackagebuildbehavior.py'
--- lib/lp/soyuz/tests/test_binarypackagebuildbehavior.py 2013-08-27 09:39:36 +0000
+++ lib/lp/soyuz/tests/test_binarypackagebuildbehavior.py 2013-08-27 09:39:36 +0000
@@ -18,11 +18,11 @@
from zope.security.proxy import removeSecurityProxy
from lp.buildmaster.enums import BuildStatus
-from lp.buildmaster.interfaces.builder import CannotBuild
-from lp.buildmaster.model.builder import (
+from lp.buildmaster.interactor import (
BuilderInteractor,
BuilderSlave,
)
+from lp.buildmaster.interfaces.builder import CannotBuild
from lp.buildmaster.tests.mock_slaves import (
AbortedSlave,
AbortingSlave,
@@ -47,9 +47,6 @@
get_sources_list_for_building,
)
from lp.soyuz.enums import ArchivePurpose
-from lp.soyuz.model.binarypackagebuildbehavior import (
- BinaryPackageBuildBehavior,
- )
from lp.testing import TestCaseWithFactory
from lp.testing.dbuser import switch_dbuser
from lp.testing.fakemethod import FakeMethod
=== modified file 'lib/lp/translations/stories/buildfarm/xx-build-summary.txt'
--- lib/lp/translations/stories/buildfarm/xx-build-summary.txt 2013-01-24 05:50:23 +0000
+++ lib/lp/translations/stories/buildfarm/xx-build-summary.txt 2013-08-27 09:39:36 +0000
@@ -15,8 +15,8 @@
>>> from lp.services.librarian.interfaces import (
... ILibraryFileAliasSet)
>>> from lp.app.enums import ServiceUsage
+ >>> from lp.buildmaster.interactor import BuilderSlave
>>> from lp.buildmaster.interfaces.buildqueue import IBuildQueueSet
- >>> from lp.buildmaster.model.builder import BuilderSlave
>>> from lp.testing.factory import (
... remove_security_proxy_and_shout_at_engineer)
>>> from lp.testing.fakemethod import FakeMethod
=== modified file 'lib/lp/translations/tests/test_translationtemplatesbuildbehavior.py'
--- lib/lp/translations/tests/test_translationtemplatesbuildbehavior.py 2013-08-19 23:23:19 +0000
+++ lib/lp/translations/tests/test_translationtemplatesbuildbehavior.py 2013-08-27 09:39:36 +0000
@@ -15,15 +15,12 @@
from lp.app.interfaces.launchpad import ILaunchpadCelebrities
from lp.buildmaster.enums import BuildStatus
+from lp.buildmaster.interactor import BuilderInteractor
from lp.buildmaster.interfaces.builder import CannotBuild
from lp.buildmaster.interfaces.buildfarmjobbehavior import (
IBuildFarmJobBehavior,
)
from lp.buildmaster.interfaces.buildqueue import IBuildQueueSet
-from lp.buildmaster.model.builder import (
- BuilderInteractor,
- BuilderSlave,
- )
from lp.buildmaster.tests.mock_slaves import (
SlaveTestHelpers,
WaitingSlave,
@@ -86,8 +83,7 @@
behavior = IBuildFarmJobBehavior(specific_job)
slave = WaitingSlave()
behavior.setBuilderInteractor(
- BuilderInteractor(self.factory.makeBuilder()))
- self.patch(BuilderSlave, 'makeBuilderSlave', FakeMethod(slave))
+ BuilderInteractor(self.factory.makeBuilder(), slave))
if use_fake_chroot:
lf = self.factory.makeLibraryFileAlias()
self.layer.txn.commit()
Follow ups