← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] lp:~wgrant/launchpad/builderinteractor-cookies into lp:launchpad

 

William Grant has proposed merging lp:~wgrant/launchpad/builderinteractor-cookies into lp:launchpad.

Commit message:
Move slave cookie verification from BuildFarmJobBehavior to BuilderInteractor, and only call updateSlaveStatus in updateBuild_WAITING.

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)

For more details, see:
https://code.launchpad.net/~wgrant/launchpad/builderinteractor-cookies/+merge/182813

Move slave cookie verification from BuildFarmJobBehavior to BuilderInteractor. All behaviours verify the cookie the same way, so all they need to know is how to calculate it.

I also adjusted BuilderInteractor.slaveStatus to not call BuildFarmJobBehavior.updateSlaveStatus. Only BuildFarmJobBehavior.handleStatus needs the augmented status dict, so updateBuild_WAITING calls updateSlaveStatus directly. This means that the regular builder scan path only touched the BFJB to generate the expected slave cookie.
-- 
https://code.launchpad.net/~wgrant/launchpad/builderinteractor-cookies/+merge/182813
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~wgrant/launchpad/builderinteractor-cookies into lp:launchpad.
=== modified file 'lib/lp/buildmaster/doc/buildfarmjobbehavior.txt'
--- lib/lp/buildmaster/doc/buildfarmjobbehavior.txt	2013-08-27 10:23:43 +0000
+++ lib/lp/buildmaster/doc/buildfarmjobbehavior.txt	2013-08-29 04:17:48 +0000
@@ -103,11 +103,3 @@
      ...
     BuildBehaviorMismatch: Builder was idle when asked to log the start of a
     build.
-
-If a slave is working on a job while we think it is idle, it will always be
-aborted.
-
-    >>> interactor._current_build_behavior.verifySlaveBuildCookie('foo')
-    Traceback (most recent call last):
-    ...
-    CorruptBuildCookie: No job assigned to builder

=== modified file 'lib/lp/buildmaster/interactor.py'
--- lib/lp/buildmaster/interactor.py	2013-08-28 08:03:32 +0000
+++ lib/lp/buildmaster/interactor.py	2013-08-29 04:17:48 +0000
@@ -303,10 +303,7 @@
             else:
                 if status['builder_status'] == 'BuilderStatus.BUILDING':
                     status['logtail'] = status_sentence[2]
-
-            self._current_build_behavior.updateSlaveStatus(
-                status_sentence, status)
-            return status
+            return (status_sentence, status)
 
         return d.addCallback(got_status)
 
@@ -328,6 +325,14 @@
             return status[0] == 'BuilderStatus.IDLE'
         return d.addCallbacks(check_available, catch_fault)
 
+    def verifySlaveBuildCookie(self, slave_build_cookie):
+        """See `IBuildFarmJobBehavior`."""
+        if isinstance(self._current_build_behavior, IdleBuildBehavior):
+            raise CorruptBuildCookie('No job assigned to builder')
+        good_cookie = self._current_build_behavior.generateSlaveBuildCookie()
+        if slave_build_cookie != good_cookie:
+            raise CorruptBuildCookie("Invalid slave build cookie.")
+
     def rescueIfLost(self, logger=None):
         """Reset the slave if its job information doesn't match the DB.
 
@@ -396,8 +401,7 @@
                 return
             slave_build_id = status_sentence[ident_position[status]]
             try:
-                self._current_build_behavior.verifySlaveBuildCookie(
-                    slave_build_id)
+                self.verifySlaveBuildCookie(slave_build_id)
             except CorruptBuildCookie as reason:
                 if status == 'BuilderStatus.WAITING':
                     d = self.cleanSlave()
@@ -625,7 +629,7 @@
                     % (queueItem.builder.url, info))
             raise BuildSlaveFailure(info)
 
-        def got_status(slave_status):
+        def got_status(statuses):
             builder_status_handlers = {
                 'BuilderStatus.IDLE': self.updateBuild_IDLE,
                 'BuilderStatus.BUILDING': self.updateBuild_BUILDING,
@@ -633,8 +637,8 @@
                 'BuilderStatus.ABORTED': self.updateBuild_ABORTED,
                 'BuilderStatus.WAITING': self.updateBuild_WAITING,
                 }
-
-            builder_status = slave_status['builder_status']
+            status_sentence, status_dict = statuses
+            builder_status = status_dict['builder_status']
             if builder_status not in builder_status_handlers:
                 logger.critical(
                     "Builder on %s returned unknown status %s, failing it"
@@ -650,22 +654,16 @@
                 transaction.commit()
                 return
 
-            # Since logtail is a xmlrpclib.Binary container and it is
-            # returned from the IBuilder content class, it arrives
-            # protected by a Zope Security Proxy, which is not declared,
-            # thus empty. Before passing it to the status handlers we
-            # will simply remove the proxy.
-            logtail = removeSecurityProxy(slave_status.get('logtail'))
-
             method = builder_status_handlers[builder_status]
             return defer.maybeDeferred(
-                method, queueItem, slave_status, logtail, logger)
+                method, queueItem, status_sentence, status_dict, logger)
 
         d.addErrback(got_failure)
         d.addCallback(got_status)
         return d
 
-    def updateBuild_IDLE(self, queueItem, slave_status, logtail, logger):
+    def updateBuild_IDLE(self, queueItem, status_sentence, status_dict,
+                         logger):
         """Somehow the builder forgot about the build job.
 
         Log this and reset the record.
@@ -676,14 +674,16 @@
         queueItem.reset()
         transaction.commit()
 
-    def updateBuild_BUILDING(self, queueItem, slave_status, logtail, logger):
+    def updateBuild_BUILDING(self, queueItem, status_sentence, status_dict,
+                             logger):
         """Build still building, collect the logtail"""
         if queueItem.job.status != JobStatus.RUNNING:
             queueItem.job.start()
-        queueItem.logtail = encoding.guess(str(logtail))
+        queueItem.logtail = encoding.guess(str(status_dict.get('logtail')))
         transaction.commit()
 
-    def updateBuild_ABORTING(self, queueItem, slave_status, logtail, logger):
+    def updateBuild_ABORTING(self, queueItem, status_sentence, status_dict,
+                             logger):
         """Build was ABORTED.
 
         Master-side should wait until the slave finish the process correctly.
@@ -691,7 +691,8 @@
         queueItem.logtail = "Waiting for slave process to be terminated"
         transaction.commit()
 
-    def updateBuild_ABORTED(self, queueItem, slave_status, logtail, logger):
+    def updateBuild_ABORTED(self, queueItem, status_sentence, status_dict,
+                            logger):
         """ABORTING process has successfully terminated.
 
         Clean the builder for another jobs.
@@ -706,21 +707,22 @@
             transaction.commit()
         return d.addCallback(got_cleaned)
 
-    def extractBuildStatus(self, slave_status):
+    def extractBuildStatus(self, status_dict):
         """Read build status name.
 
-        :param slave_status: build status dict as passed to the
+        :param status_dict: build status dict as passed to the
             updateBuild_* methods.
         :return: the unqualified status name, e.g. "OK".
         """
-        status_string = slave_status['build_status']
+        status_string = status_dict['build_status']
         lead_string = 'BuildStatus.'
         assert status_string.startswith(lead_string), (
             "Malformed status string: '%s'" % status_string)
 
         return status_string[len(lead_string):]
 
-    def updateBuild_WAITING(self, queueItem, slave_status, logtail, logger):
+    def updateBuild_WAITING(self, queueItem, status_sentence, status_dict,
+                            logger):
         """Perform the actions needed for a slave in a WAITING state
 
         Buildslave can be WAITING in five situations:
@@ -734,12 +736,11 @@
           Librarian with getFileFromSlave() and then pass the binaries to
           the uploader for processing.
         """
-        librarian = getUtility(ILibrarianClient)
-        build_status = self.extractBuildStatus(slave_status)
-
-        # XXX: dsilvers 2005-03-02: Confirm the builder has the right build?
+        self._current_build_behavior.updateSlaveStatus(
+            status_sentence, status_dict)
         d = self._current_build_behavior.handleStatus(
-            build_status, librarian, slave_status)
+            self.extractBuildStatus(status_dict),
+            getUtility(ILibrarianClient), status_dict)
         return d
 
     def transferSlaveFileToLibrarian(self, file_sha1, filename, private):

=== modified file 'lib/lp/buildmaster/interfaces/buildfarmjob.py'
--- lib/lp/buildmaster/interfaces/buildfarmjob.py	2013-05-01 22:53:22 +0000
+++ lib/lp/buildmaster/interfaces/buildfarmjob.py	2013-08-29 04:17:48 +0000
@@ -149,11 +149,8 @@
     def generateSlaveBuildCookie():
         """Produce a cookie for the slave as a token of the job it's doing.
 
-        The cookie need not be unique, but should be hard for a
-        compromised slave to guess.
-
-        :return: a hard-to-guess ASCII string that can be reproduced
-            accurately based on this job's properties.
+        The cookie should uniquely represent the current dispatch of this
+        build.
         """
 
     def cleanUp():

=== modified file 'lib/lp/buildmaster/interfaces/buildfarmjobbehavior.py'
--- lib/lp/buildmaster/interfaces/buildfarmjobbehavior.py	2013-08-28 10:13:18 +0000
+++ lib/lp/buildmaster/interfaces/buildfarmjobbehavior.py	2013-08-29 04:17:48 +0000
@@ -46,30 +46,29 @@
         :param logger: A logger to be used to log diagnostic information.
         """
 
-    def updateSlaveStatus(raw_slave_status, status):
+    def generateSlaveBuildCookie():
+        """Produce a cookie for the slave as a token of the job it's doing.
+
+        The cookie should uniquely represent the current dispatch of the
+        current build.
+        """
+
+    def updateSlaveStatus(status_sentence, status_dict):
         """Update the slave status dict with custom values for this behavior.
 
-        :param raw_slave_status: The value returned by the build slave's
+        :param status_sentence: The value returned by the build slave's
            status() method.
-        :param status: A dict of the processed slave status values provided
-           by all types: builder_status, build_id, and optionally build_status
-           or logtail. This should have any behaviour-specific values
-           added to it.
-        """
-
-    def verifySlaveBuildCookie(slave_build_cookie):
-        """Verify that a slave's build cookie shows no signs of corruption.
-
-        :param slave_build_cookie: The slave's build cookie, as specified in
-           `dispatchBuildToSlave`.
-        :raises CorruptBuildCookie: if the build cookie isn't what it's
-            supposed to be.
-        """
-
-    def handleStatus(status, librarian, slave_status):
+        :param status_dict: A dict of the processed slave status values
+           provided by all types: builder_status, build_id, and optionally
+           build_status or logtail. This should have any behaviour-specific
+           values added to it.
+        """
+
+    def handleStatus(status, librarian, status_dict):
         """Update the build from a WAITING slave result.
 
         :param status: The tail of the BuildStatus (eg. OK or PACKAGEFAIL).
         :param librarian: An `ILibrarianClient` to use for file uploads.
-        :param slave_status: Slave status from `BuilderInteractor.slaveStatus`.
+        :param status_dict: Slave status dict from
+           `BuilderInteractor.slaveStatus` and `updateSlaveStatus`.
         """

=== modified file 'lib/lp/buildmaster/model/buildfarmjobbehavior.py'
--- lib/lp/buildmaster/model/buildfarmjobbehavior.py	2013-08-28 10:13:18 +0000
+++ lib/lp/buildmaster/model/buildfarmjobbehavior.py	2013-08-29 04:17:48 +0000
@@ -22,10 +22,7 @@
     BuildFarmJobType,
     BuildStatus,
     )
-from lp.buildmaster.interfaces.builder import (
-    BuildSlaveFailure,
-    CorruptBuildCookie,
-    )
+from lp.buildmaster.interfaces.builder import BuildSlaveFailure
 from lp.buildmaster.interfaces.buildfarmjobbehavior import (
     BuildBehaviorMismatch,
     IBuildFarmJobBehavior,
@@ -66,11 +63,8 @@
         The default behavior is that we don't add any extra values."""
         pass
 
-    def verifySlaveBuildCookie(self, slave_build_cookie):
-        """See `IBuildFarmJobBehavior`."""
-        expected_cookie = self.buildfarmjob.generateSlaveBuildCookie()
-        if slave_build_cookie != expected_cookie:
-            raise CorruptBuildCookie("Invalid slave build cookie.")
+    def generateSlaveBuildCookie(self):
+        return self.buildfarmjob.generateSlaveBuildCookie()
 
     def getBuildCookie(self):
         """See `IPackageBuild`."""
@@ -336,12 +330,3 @@
         """See `IBuildFarmJobBehavior`."""
         raise BuildBehaviorMismatch(
             "Builder was idle when asked to dispatch a build to the slave.")
-
-    @property
-    def status(self):
-        """See `IBuildFarmJobBehavior`."""
-        return "Idle"
-
-    def verifySlaveBuildCookie(self, slave_build_id):
-        """See `IBuildFarmJobBehavior`."""
-        raise CorruptBuildCookie('No job assigned to builder')

=== modified file 'lib/lp/buildmaster/tests/mock_slaves.py'
--- lib/lp/buildmaster/tests/mock_slaves.py	2013-08-27 09:17:07 +0000
+++ lib/lp/buildmaster/tests/mock_slaves.py	2013-08-29 04:17:48 +0000
@@ -33,10 +33,7 @@
 from twisted.web import xmlrpc
 
 from lp.buildmaster.interactor import BuilderSlave
-from lp.buildmaster.interfaces.builder import (
-    CannotFetchFile,
-    CorruptBuildCookie,
-    )
+from lp.buildmaster.interfaces.builder import CannotFetchFile
 from lp.services.config import config
 from lp.testing.sampledata import I386_ARCHITECTURE_NAME
 
@@ -238,16 +235,10 @@
         return defer.fail(xmlrpclib.Fault(8001, "Broken slave"))
 
 
-class CorruptBehavior:
-
-    def verifySlaveBuildCookie(self, cookie):
-        raise CorruptBuildCookie("Bad value: %r" % (cookie,))
-
-
 class TrivialBehavior:
 
-    def verifySlaveBuildCookie(self, cookie):
-        pass
+    def generateSlaveBuildCookie(self):
+        return 'trivial'
 
 
 class DeadProxy(xmlrpc.Proxy):

=== modified file 'lib/lp/buildmaster/tests/test_builder.py'
--- lib/lp/buildmaster/tests/test_builder.py	2013-08-28 09:26:17 +0000
+++ lib/lp/buildmaster/tests/test_builder.py	2013-08-29 04:17:48 +0000
@@ -18,6 +18,7 @@
 from twisted.internet.defer import (
     CancelledError,
     DeferredList,
+    inlineCallbacks,
     )
 from twisted.internet.task import Clock
 from twisted.python.failure import Failure
@@ -36,6 +37,7 @@
 from lp.buildmaster.interfaces.builder import (
     CannotFetchFile,
     CannotResumeHost,
+    CorruptBuildCookie,
     IBuilder,
     IBuilderSet,
     )
@@ -47,7 +49,6 @@
     AbortingSlave,
     BrokenSlave,
     BuildingSlave,
-    CorruptBehavior,
     DeadProxy,
     LostBuildingBrokenSlave,
     make_publisher,
@@ -268,11 +269,28 @@
         self.assertRaises(
             AssertionError, interactor.extractBuildStatus, slave_status)
 
+    def test_verifySlaveBuildCookie_good(self):
+        interactor = BuilderInteractor(MockBuilder(), None, TrivialBehavior())
+        interactor.verifySlaveBuildCookie('trivial')
+
+    def test_verifySlaveBuildCookie_bad(self):
+        interactor = BuilderInteractor(MockBuilder(), None, TrivialBehavior())
+        self.assertRaises(
+            CorruptBuildCookie,
+            interactor.verifySlaveBuildCookie, 'difficult')
+
+    def test_verifySlaveBuildCookie_idle(self):
+        interactor = BuilderInteractor(MockBuilder())
+        self.assertTrue(
+            isinstance(interactor._current_build_behavior, IdleBuildBehavior))
+        self.assertRaises(
+            CorruptBuildCookie, interactor.verifySlaveBuildCookie, 'foo')
+
     def test_updateStatus_aborts_lost_and_broken_slave(self):
         # A slave that's 'lost' should be aborted; when the slave is
         # broken then abort() should also throw a fault.
         slave = LostBuildingBrokenSlave()
-        interactor = BuilderInteractor(MockBuilder(), slave, CorruptBehavior())
+        interactor = BuilderInteractor(MockBuilder(), slave, TrivialBehavior())
         d = interactor.updateStatus(BufferLogger())
 
         def check_slave_status(failure):
@@ -383,7 +401,7 @@
     def test_recover_waiting_slave_with_good_id(self):
         # rescueIfLost does not attempt to abort or clean a builder that is
         # WAITING.
-        waiting_slave = WaitingSlave()
+        waiting_slave = WaitingSlave(build_id='trivial')
         d = BuilderInteractor(
             MockBuilder(), waiting_slave, TrivialBehavior()).rescueIfLost()
 
@@ -399,9 +417,9 @@
         # then rescueBuilderIfLost should attempt to abort it, so that the
         # builder is reset for a new build, and the corrupt build is
         # discarded.
-        waiting_slave = WaitingSlave()
+        waiting_slave = WaitingSlave(build_id='non-trivial')
         d = BuilderInteractor(
-            MockBuilder(), waiting_slave, CorruptBehavior()).rescueIfLost()
+            MockBuilder(), waiting_slave, TrivialBehavior()).rescueIfLost()
 
         def check_slave_calls(ignored):
             self.assertNotIn('abort', waiting_slave.call_log)
@@ -412,7 +430,7 @@
     def test_recover_building_slave_with_good_id(self):
         # rescueIfLost does not attempt to abort or clean a builder that is
         # BUILDING.
-        building_slave = BuildingSlave()
+        building_slave = BuildingSlave(build_id='trivial')
         d = BuilderInteractor(
             MockBuilder(), building_slave, TrivialBehavior()).rescueIfLost()
 
@@ -425,9 +443,9 @@
     def test_recover_building_slave_with_bad_id(self):
         # If a slave is BUILDING with a build id we don't recognize, then we
         # abort the build, thus stopping it in its tracks.
-        building_slave = BuildingSlave()
+        building_slave = BuildingSlave(build_id='non-trivial')
         d = BuilderInteractor(
-            MockBuilder(), building_slave, CorruptBehavior()).rescueIfLost()
+            MockBuilder(), building_slave, TrivialBehavior()).rescueIfLost()
 
         def check_slave_calls(ignored):
             self.assertIn('abort', building_slave.call_log)
@@ -442,29 +460,28 @@
 
     run_tests_with = AsynchronousDeferredRunTest
 
+    @inlineCallbacks
     def assertStatus(self, slave, builder_status=None,
                      build_status=None, logtail=False, filemap=None,
                      dependencies=None):
-        d = BuilderInteractor(MockBuilder(), slave).slaveStatus()
-
-        def got_status(status_dict):
-            expected = {}
-            if builder_status is not None:
-                expected["builder_status"] = builder_status
-            if build_status is not None:
-                expected["build_status"] = build_status
-            if dependencies is not None:
-                expected["dependencies"] = dependencies
-
-            # We don't care so much about the content of the logtail,
-            # just that it's there.
-            if logtail:
-                tail = status_dict.pop("logtail")
-                self.assertIsInstance(tail, xmlrpclib.Binary)
-
-            self.assertEqual(expected, status_dict)
-
-        return d.addCallback(got_status)
+        statuses = yield BuilderInteractor(MockBuilder(), slave).slaveStatus()
+        status_dict = statuses[1]
+
+        expected = {}
+        if builder_status is not None:
+            expected["builder_status"] = builder_status
+        if build_status is not None:
+            expected["build_status"] = build_status
+        if dependencies is not None:
+            expected["dependencies"] = dependencies
+
+        # We don't care so much about the content of the logtail,
+        # just that it's there.
+        if logtail:
+            tail = status_dict.pop("logtail")
+            self.assertIsInstance(tail, xmlrpclib.Binary)
+
+        self.assertEqual(expected, status_dict)
 
     def test_slaveStatus_idle_slave(self):
         self.assertStatus(

=== modified file 'lib/lp/buildmaster/tests/test_buildfarmjobbehavior.py'
--- lib/lp/buildmaster/tests/test_buildfarmjobbehavior.py	2013-08-28 09:26:17 +0000
+++ lib/lp/buildmaster/tests/test_buildfarmjobbehavior.py	2013-08-29 04:17:48 +0000
@@ -16,7 +16,6 @@
 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.interfaces.buildfarmjobbehavior import (
     IBuildFarmJobBehavior,
     )
@@ -54,11 +53,6 @@
             buildfarmjob = removeSecurityProxy(buildfarmjob)
         return BuildFarmJobBehaviorBase(buildfarmjob)
 
-    def _changeBuildFarmJobName(self, buildfarmjob):
-        """Manipulate `buildfarmjob` so that its `getName` changes."""
-        name = buildfarmjob.getName() + 'x'
-        removeSecurityProxy(buildfarmjob).getName = FakeMethod(result=name)
-
     def _makeBuild(self):
         """Create a `Build` object."""
         x86 = getUtility(IProcessorFamilySet).getByName('x86')
@@ -78,6 +72,11 @@
         """Create a `BuildQueue` object."""
         return self.factory.makeSourcePackageRecipeBuildJob()
 
+    def _changeBuildFarmJobName(self, buildfarmjob):
+        """Manipulate `buildfarmjob` so that its `getName` changes."""
+        name = buildfarmjob.getName() + 'x'
+        removeSecurityProxy(buildfarmjob).getName = FakeMethod(result=name)
+
     def test_cookie_baseline(self):
         buildfarmjob = self.factory.makeTranslationTemplatesBuildJob()
 
@@ -89,43 +88,13 @@
 
         self.assertEqual(cookie, buildfarmjob.generateSlaveBuildCookie())
 
-    def test_verifySlaveBuildCookie_good(self):
-        buildfarmjob = self.factory.makeTranslationTemplatesBuildJob()
-        behavior = self._makeBehavior(buildfarmjob)
-
-        cookie = buildfarmjob.generateSlaveBuildCookie()
-
-        # The correct cookie validates successfully.
-        behavior.verifySlaveBuildCookie(cookie)
-
-    def test_verifySlaveBuildCookie_bad(self):
-        buildfarmjob = self.factory.makeTranslationTemplatesBuildJob()
-        behavior = self._makeBehavior(buildfarmjob)
-
-        cookie = buildfarmjob.generateSlaveBuildCookie()
-
-        self.assertRaises(
-            CorruptBuildCookie,
-            behavior.verifySlaveBuildCookie,
-            cookie + 'x')
-
     def test_cookie_includes_job_name(self):
         # The cookie is a hash that includes the job's name.
         buildfarmjob = self.factory.makeTranslationTemplatesBuildJob()
-        buildfarmjob = removeSecurityProxy(buildfarmjob)
-        behavior = self._makeBehavior(buildfarmjob)
         cookie = buildfarmjob.generateSlaveBuildCookie()
-
-        self._changeBuildFarmJobName(buildfarmjob)
-
-        self.assertRaises(
-            CorruptBuildCookie,
-            behavior.verifySlaveBuildCookie,
-            cookie)
-
-        # However, the name is not included in plaintext so as not to
-        # provide a compromised slave a starting point for guessing
-        # another slave's cookie.
+        self._changeBuildFarmJobName(removeSecurityProxy(buildfarmjob))
+
+        self.assertNotEqual(cookie, buildfarmjob.generateSlaveBuildCookie())
         self.assertNotIn(buildfarmjob.getName(), cookie)
 
     def test_cookie_includes_more_than_name(self):

=== modified file 'lib/lp/translations/tests/test_translationtemplatesbuildbehavior.py'
--- lib/lp/translations/tests/test_translationtemplatesbuildbehavior.py	2013-08-28 09:21:19 +0000
+++ lib/lp/translations/tests/test_translationtemplatesbuildbehavior.py	2013-08-29 04:17:48 +0000
@@ -41,17 +41,6 @@
     )
 
 
-class FakeBuilder:
-    """Pretend `Builder`."""
-
-    def __init__(self, slave):
-        self.slave = slave
-        self.cleanSlave = FakeMethod()
-
-    def slaveStatus(self):
-        return self.slave._status
-
-
 class FakeBuildQueue:
     """Pretend `BuildQueue`."""
 


Follow ups