From 77b386426c2ee498e43185553b756224db74fea8 Mon Sep 17 00:00:00 2001 From: "Dustin J. Mitchell" Date: Sun, 15 Feb 2009 20:05:32 -0500 Subject: [PATCH] Add MasterShellCommand --- buildbot/steps/master.py | 76 +++++++++++++++++++++++++++++++++++++++++++++ buildbot/test/test_steps.py | 43 ++++++++++++++++++++++++- docs/buildbot.texinfo | 33 ++++++++++++++++++-- 3 files changed, 148 insertions(+), 4 deletions(-) create mode 100644 buildbot/steps/master.py diff --git a/buildbot/steps/master.py b/buildbot/steps/master.py new file mode 100644 index 0000000..da8a664 --- /dev/null +++ b/buildbot/steps/master.py @@ -0,0 +1,76 @@ +import os, types +from twisted.python import log, failure, runtime +from twisted.internet import reactor, defer, task +from buildbot.process.buildstep import RemoteCommand, BuildStep +from buildbot.process.buildstep import SUCCESS, FAILURE +from twisted.internet.protocol import ProcessProtocol + +class MasterShellCommand(BuildStep): + """ + Run a shell command locally - on the buildmaster. The shell command + COMMAND is specified just as for a RemoteShellCommand. Note that extra + logfiles are not sopported. + """ + name='MasterShellCommand' + description='Running' + descriptionDone='Ran' + + def __init__(self, command, **kwargs): + BuildStep.__init__(self, **kwargs) + self.addFactoryArguments(command=command) + self.command=command + + class LocalPP(ProcessProtocol): + def __init__(self, step): + self.step = step + + def outReceived(self, data): + self.step.stdio_log.addStdout(data) + + def errReceived(self, data): + self.step.stdio_log.addStderr(data) + + def processEnded(self, status_object): + self.step.stdio_log.addHeader("exit status %d\n" % status_object.value.exitCode) + self.step.processEnded(status_object) + + def start(self): + # set up argv + if type(self.command) in types.StringTypes: + if runtime.platformType == 'win32': + argv = os.environ['COMSPEC'].split() # allow %COMSPEC% to have args + if '/c' not in argv: argv += ['/c'] + argv += [self.command] + else: + # for posix, use /bin/sh. for other non-posix, well, doesn't + # hurt to try + argv = ['/bin/sh', '-c', self.command] + else: + if runtime.platformType == 'win32': + argv = os.environ['COMSPEC'].split() # allow %COMSPEC% to have args + if '/c' not in argv: argv += ['/c'] + argv += list(self.command) + else: + argv = self.command + + self.stdio_log = stdio_log = self.addLog("stdio") + + if type(self.command) in types.StringTypes: + stdio_log.addHeader(self.command.strip() + "\n\n") + else: + stdio_log.addHeader(" ".join(self.command) + "\n\n") + stdio_log.addHeader("** RUNNING ON BUILDMASTER **\n") + stdio_log.addHeader(" in dir %s\n" % os.getcwd()) + stdio_log.addHeader(" argv: %s\n" % (argv,)) + + # TODO add a timeout? + proc = reactor.spawnProcess(self.LocalPP(self), argv[0], argv) + # (the LocalPP object will call processEnded for us) + + def processEnded(self, status_object): + if status_object.value.exitCode != 0: + self.step_status.setText(["failed (%d)" % status_object.value.exitCode]) + self.finished(FAILURE) + else: + self.step_status.setText(["succeeded"]) + self.finished(SUCCESS) diff --git a/buildbot/test/test_steps.py b/buildbot/test/test_steps.py index dc6f940..3c7587d 100644 --- a/buildbot/test/test_steps.py +++ b/buildbot/test/test_steps.py @@ -22,7 +22,7 @@ from twisted.internet import reactor, defer from buildbot.sourcestamp import SourceStamp from buildbot.process import buildstep, base, factory from buildbot.buildslave import BuildSlave -from buildbot.steps import shell, source, python +from buildbot.steps import shell, source, python, master from buildbot.status import builder from buildbot.status.builder import SUCCESS, WARNINGS, FAILURE from buildbot.test.runutils import RunMixin, rmtree @@ -744,3 +744,44 @@ Result: FAIL self.failUnlessEqual(ss.getStatistic('tests-failed'), 287) self.failUnlessEqual(ss.getStatistic('tests-total'), 264809) self.failUnlessEqual(ss.getStatistic('tests-passed'), 264522) + +class MasterShellCommand(StepTester, unittest.TestCase): + def testMasterShellCommand(self): + self.slavebase = "testMasterShellCommand.slave" + self.masterbase = "testMasterShellCommand.master" + sb = self.makeSlaveBuilder() + step = self.makeStep(master.MasterShellCommand, command=['echo', 'hi']) + + # we can't invoke runStep until the reactor is started .. hence this + # little dance + d = defer.Deferred() + def _dotest(_): + return self.runStep(step) + d.addCallback(_dotest) + + def _check(results): + self.failUnlessEqual(results, SUCCESS) + logtxt = step.getLog("stdio").getText() + self.failUnlessEqual(logtxt.strip(), "hi") + d.addCallback(_check) + reactor.callLater(0, d.callback, None) + return d + + def testMasterShellCommand_badexit(self): + self.slavebase = "testMasterShellCommand_badexit.slave" + self.masterbase = "testMasterShellCommand_badexit.master" + sb = self.makeSlaveBuilder() + step = self.makeStep(master.MasterShellCommand, command="exit 1") + + # we can't invoke runStep until the reactor is started .. hence this + # little dance + d = defer.Deferred() + def _dotest(_): + return self.runStep(step) + d.addCallback(_dotest) + + def _check(results): + self.failUnlessEqual(results, FAILURE) + d.addCallback(_check) + reactor.callLater(0, d.callback, None) + return d diff --git a/docs/buildbot.texinfo b/docs/buildbot.texinfo index 29ddbaf..5ba5320 100644 --- a/docs/buildbot.texinfo +++ b/docs/buildbot.texinfo @@ -200,6 +200,7 @@ Build Steps * Simple ShellCommand Subclasses:: * Python BuildSteps:: * Transferring Files:: +* Steps That Run on the Master:: * Triggering Schedulers:: * Writing New BuildSteps:: @@ -4579,6 +4580,7 @@ control each. * Simple ShellCommand Subclasses:: * Python BuildSteps:: * Transferring Files:: +* Steps That Run on the Master:: * Triggering Schedulers:: * Writing New BuildSteps:: @end menu @@ -5674,7 +5676,7 @@ f.addStep(PyFlakes(command=["pyflakes", "src"])) @end example -@node Transferring Files, Triggering Schedulers, Python BuildSteps, Build Steps +@node Transferring Files @subsection Transferring Files @cindex File Transfer @@ -5783,8 +5785,33 @@ f.addStep(DirectoryUpload(slavesrc="docs", The DirectoryUpload step will create all necessary directories and transfers empty directories, too. +@node Steps That Run on the Master +@subsection Steps That Run on the Master -@node Triggering Schedulers, Writing New BuildSteps, Transferring Files, Build Steps +Occasionally, it is useful to execute some task on the master, for example to +create a directory, deploy a build result, or trigger some other centralized +processing. This is possible, in a limited fashion, with the +@code{MasterShellCommand} step. + +This step operates similarly to a regular @code{ShellCommand}, but executes on +the master, instead of the slave. To be clear, the enclosing @code{Build} +object must still have a slave object, just as for any other step -- only, in +this step, the slave does not do anything. + +In this example, the step renames a tarball based on the day of the week. + +@example +from buildbot.steps.transfer import FileUpload +from buildbot.steps.master import MasterShellCommand + +f.addStep(FileUpload(slavesrc="widgetsoft.tar.gz", + masterdest="/var/buildoutputs/widgetsoft-new.tar.gz")) +f.addStep(MasterShellCommand(command=""" + cd /var/buildoutputs; + mv widgetsoft-new.tar.gz widgetsoft-`date +%a`.tar.gz""")) +@end example + +@node Triggering Schedulers @subsection Triggering Schedulers The counterpart to the Triggerable described in section @@ -5816,7 +5843,7 @@ useful to ensure that all of the builds use exactly the same SourceStamp, even if other Changes have occurred while the build was running. -@node Writing New BuildSteps, , Triggering Schedulers, Build Steps +@node Writing New BuildSteps @subsection Writing New BuildSteps While it is a good idea to keep your build process self-contained in -- 2.11.4.GIT