Package buildslave :: Package commands :: Module base
[frames] | no frames]

Source Code for Module buildslave.commands.base

  1  # This file is part of Buildbot.  Buildbot is free software: you can 
  2  # redistribute it and/or modify it under the terms of the GNU General Public 
  3  # License as published by the Free Software Foundation, version 2. 
  4  # 
  5  # This program is distributed in the hope that it will be useful, but WITHOUT 
  6  # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 
  7  # FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more 
  8  # details. 
  9  # 
 10  # You should have received a copy of the GNU General Public License along with 
 11  # this program; if not, write to the Free Software Foundation, Inc., 51 
 12  # Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 
 13  # 
 14  # Copyright Buildbot Team Members 
 15   
 16   
 17  import os 
 18  from base64 import b64encode 
 19  import sys 
 20  import shutil 
 21   
 22  from zope.interface import implements 
 23  from twisted.internet import reactor, threads, defer 
 24  from twisted.python import log, failure, runtime 
 25   
 26  from buildslave.interfaces import ISlaveCommand 
 27  from buildslave import runprocess 
 28  from buildslave.exceptions import AbandonChain 
 29  from buildslave.commands import utils 
 30  from buildslave import util 
 31   
 32  # this used to be a CVS $-style "Revision" auto-updated keyword, but since I 
 33  # moved to Darcs as the primary repository, this is updated manually each 
 34  # time this file is changed. The last cvs_ver that was here was 1.51 . 
 35  command_version = "2.15" 
 36   
 37  # version history: 
 38  #  >=1.17: commands are interruptable 
 39  #  >=1.28: Arch understands 'revision', added Bazaar 
 40  #  >=1.33: Source classes understand 'retry' 
 41  #  >=1.39: Source classes correctly handle changes in branch (except Git) 
 42  #          Darcs accepts 'revision' (now all do but Git) (well, and P4Sync) 
 43  #          Arch/Baz should accept 'build-config' 
 44  #  >=1.51: (release 0.7.3) 
 45  #  >= 2.1: SlaveShellCommand now accepts 'initial_stdin', 'keep_stdin_open', 
 46  #          and 'logfiles'. It now sends 'log' messages in addition to 
 47  #          stdout/stdin/header/rc. It acquired writeStdin/closeStdin methods, 
 48  #          but these are not remotely callable yet. 
 49  #          (not externally visible: ShellCommandPP has writeStdin/closeStdin. 
 50  #          ShellCommand accepts new arguments (logfiles=, initialStdin=, 
 51  #          keepStdinOpen=) and no longer accepts stdin=) 
 52  #          (release 0.7.4) 
 53  #  >= 2.2: added monotone, uploadFile, and downloadFile (release 0.7.5) 
 54  #  >= 2.3: added bzr (release 0.7.6) 
 55  #  >= 2.4: Git understands 'revision' and branches 
 56  #  >= 2.5: workaround added for remote 'hg clone --rev REV' when hg<0.9.2 
 57  #  >= 2.6: added uploadDirectory 
 58  #  >= 2.7: added usePTY option to SlaveShellCommand 
 59  #  >= 2.8: added username and password args to SVN class 
 60  #  >= 2.9: add depth arg to SVN class 
 61  #  >= 2.10: CVS can handle 'extra_options' and 'export_options' 
 62  #  >= 2.11: Arch, Bazaar, and Monotone removed 
 63  #  >= 2.12: SlaveShellCommand no longer accepts 'keep_stdin_open' 
 64  #  >= 2.13: SlaveFileUploadCommand supports option 'keepstamp' 
 65  #  >= 2.14: RemoveDirectory can delete multiple directories 
 66  #  >= 2.15: 'interruptSignal' option is added to SlaveShellCommand 
 67   
68 -class Command:
69 implements(ISlaveCommand) 70 71 """This class defines one command that can be invoked by the build master. 72 The command is executed on the slave side, and always sends back a 73 completion message when it finishes. It may also send intermediate status 74 as it runs (by calling builder.sendStatus). Some commands can be 75 interrupted (either by the build master or a local timeout), in which 76 case the step is expected to complete normally with a status message that 77 indicates an error occurred. 78 79 These commands are used by BuildSteps on the master side. Each kind of 80 BuildStep uses a single Command. The slave must implement all the 81 Commands required by the set of BuildSteps used for any given build: 82 this is checked at startup time. 83 84 All Commands are constructed with the same signature: 85 c = CommandClass(builder, stepid, args) 86 where 'builder' is the parent SlaveBuilder object, and 'args' is a 87 dict that is interpreted per-command. 88 89 The setup(args) method is available for setup, and is run from __init__. 90 91 The Command is started with start(). This method must be implemented in a 92 subclass, and it should return a Deferred. When your step is done, you 93 should fire the Deferred (the results are not used). If the command is 94 interrupted, it should fire the Deferred anyway. 95 96 While the command runs. it may send status messages back to the 97 buildmaster by calling self.sendStatus(statusdict). The statusdict is 98 interpreted by the master-side BuildStep however it likes. 99 100 A separate completion message is sent when the deferred fires, which 101 indicates that the Command has finished, but does not carry any status 102 data. If the Command needs to return an exit code of some sort, that 103 should be sent as a regular status message before the deferred is fired . 104 Once builder.commandComplete has been run, no more status messages may be 105 sent. 106 107 If interrupt() is called, the Command should attempt to shut down as 108 quickly as possible. Child processes should be killed, new ones should 109 not be started. The Command should send some kind of error status update, 110 then complete as usual by firing the Deferred. 111 112 .interrupted should be set by interrupt(), and can be tested to avoid 113 sending multiple error status messages. 114 115 If .running is False, the bot is shutting down (or has otherwise lost the 116 connection to the master), and should not send any status messages. This 117 is checked in Command.sendStatus . 118 119 """ 120 121 # builder methods: 122 # sendStatus(dict) (zero or more) 123 # commandComplete() or commandInterrupted() (one, at end) 124 125 debug = False 126 interrupted = False 127 running = False # set by Builder, cleared on shutdown or when the 128 # Deferred fires 129 130 _reactor = reactor 131
132 - def __init__(self, builder, stepId, args):
133 self.builder = builder 134 self.stepId = stepId # just for logging 135 self.args = args 136 self.startTime = None 137 self.setup(args)
138
139 - def setup(self, args):
140 """Override this in a subclass to extract items from the args dict.""" 141 pass
142
143 - def doStart(self):
144 self.running = True 145 self.startTime = util.now(self._reactor) 146 d = defer.maybeDeferred(self.start) 147 def commandComplete(res): 148 self.sendStatus({"elapsed": util.now(self._reactor) - self.startTime}) 149 self.running = False 150 return res
151 d.addBoth(commandComplete) 152 return d
153
154 - def start(self):
155 """Start the command. This method should return a Deferred that will 156 fire when the command has completed. The Deferred's argument will be 157 ignored. 158 159 This method should be overridden by subclasses.""" 160 raise NotImplementedError, "You must implement this in a subclass"
161
162 - def sendStatus(self, status):
163 """Send a status update to the master.""" 164 if self.debug: 165 log.msg("sendStatus", status) 166 if not self.running: 167 log.msg("would sendStatus but not .running") 168 return 169 self.builder.sendUpdate(status)
170
171 - def doInterrupt(self):
172 self.running = False 173 self.interrupt()
174
175 - def interrupt(self):
176 """Override this in a subclass to allow commands to be interrupted. 177 May be called multiple times, test and set self.interrupted=True if 178 this matters.""" 179 pass
180 181 # utility methods, mostly used by SlaveShellCommand and the like 182
183 - def _abandonOnFailure(self, rc):
184 if type(rc) is not int: 185 log.msg("weird, _abandonOnFailure was given rc=%s (%s)" % \ 186 (rc, type(rc))) 187 assert isinstance(rc, int) 188 if rc != 0: 189 raise AbandonChain(rc) 190 return rc
191
192 - def _sendRC(self, res):
193 self.sendStatus({'rc': 0})
194
195 - def _checkAbandoned(self, why):
196 log.msg("_checkAbandoned", why) 197 why.trap(AbandonChain) 198 log.msg(" abandoning chain", why.value) 199 self.sendStatus({'rc': why.value.args[0]}) 200 return None
201
202 -class SourceBaseCommand(Command):
203 """Abstract base class for Version Control System operations (checkout 204 and update). This class extracts the following arguments from the 205 dictionary received from the master: 206 207 - ['workdir']: (required) the subdirectory where the buildable sources 208 should be placed 209 210 - ['mode']: one of update/copy/clobber/export, defaults to 'update' 211 212 - ['revision']: (required) If not None, this is an int or string which indicates 213 which sources (along a time-like axis) should be used. 214 It is the thing you provide as the CVS -r or -D 215 argument. 216 217 - ['patch']: If not None, this is a tuple of (striplevel, patch) 218 which contains a patch that should be applied after the 219 checkout has occurred. Once applied, the tree is no 220 longer eligible for use with mode='update', and it only 221 makes sense to use this in conjunction with a 222 ['revision'] argument. striplevel is an int, and patch 223 is a string in standard unified diff format. The patch 224 will be applied with 'patch -p%d <PATCH', with 225 STRIPLEVEL substituted as %d. The command will fail if 226 the patch process fails (rejected hunks). 227 228 - ['timeout']: seconds of silence tolerated before we kill off the 229 command 230 231 - ['maxTime']: seconds before we kill off the command 232 233 - ['retry']: If not None, this is a tuple of (delay, repeats) 234 which means that any failed VC updates should be 235 reattempted, up to REPEATS times, after a delay of 236 DELAY seconds. This is intended to deal with slaves 237 that experience transient network failures. 238 """ 239 240 sourcedata = "" 241
242 - def setup(self, args):
243 # if we need to parse the output, use this environment. Otherwise 244 # command output will be in whatever the buildslave's native language 245 # has been set to. 246 self.env = os.environ.copy() 247 self.env['LC_MESSAGES'] = "C" 248 249 self.workdir = args['workdir'] 250 self.mode = args.get('mode', "update") 251 self.revision = args.get('revision') 252 self.patch = args.get('patch') 253 self.timeout = args.get('timeout', 120) 254 self.maxTime = args.get('maxTime', None) 255 self.retry = args.get('retry') 256 self.logEnviron = args.get('logEnviron',True) 257 self._commandPaths = {}
258 # VC-specific subclasses should override this to extract more args. 259 # Make sure to upcall! 260
261 - def getCommand(self, name):
262 """Wrapper around utils.getCommand that will output a resonable 263 error message and raise AbandonChain if the command cannot be 264 found""" 265 if name not in self._commandPaths: 266 try: 267 self._commandPaths[name] = utils.getCommand(name) 268 except RuntimeError: 269 self.sendStatus({'stderr' : "could not find '%s'\n" % name}) 270 self.sendStatus({'stderr' : "PATH is '%s'\n" % os.environ.get('PATH', '')}) 271 raise AbandonChain(-1) 272 return self._commandPaths[name]
273
274 - def start(self):
275 self.sendStatus({'header': "starting " + self.header + "\n"}) 276 self.command = None 277 278 # self.srcdir is where the VC system should put the sources 279 if self.mode == "copy": 280 self.srcdir = "source" # hardwired directory name, sorry 281 else: 282 self.srcdir = self.workdir 283 284 self.sourcedatafile = os.path.join(self.builder.basedir, 285 ".buildbot-sourcedata-" + b64encode(self.srcdir)) 286 287 # upgrade older versions to the new sourcedata location 288 old_sd_path = os.path.join(self.builder.basedir, self.srcdir, ".buildbot-sourcedata") 289 if os.path.exists(old_sd_path) and not os.path.exists(self.sourcedatafile): 290 os.rename(old_sd_path, self.sourcedatafile) 291 292 # also upgrade versions that didn't include the encoded version of the 293 # source directory 294 old_sd_path = os.path.join(self.builder.basedir, ".buildbot-sourcedata") 295 if os.path.exists(old_sd_path) and not os.path.exists(self.sourcedatafile): 296 os.rename(old_sd_path, self.sourcedatafile) 297 298 d = defer.succeed(None) 299 self.maybeClobber(d) 300 if not (self.sourcedirIsUpdateable() and self.sourcedataMatches()): 301 # the directory cannot be updated, so we have to clobber it. 302 # Perhaps the master just changed modes from 'export' to 303 # 'update'. 304 d.addCallback(self.doClobber, self.srcdir) 305 306 d.addCallback(self.doVC) 307 308 if self.mode == "copy": 309 d.addCallback(self.doCopy) 310 if self.patch: 311 d.addCallback(self.doPatch) 312 d.addCallbacks(self._sendRC, self._checkAbandoned) 313 return d
314
315 - def maybeClobber(self, d):
316 # do we need to clobber anything? 317 if self.mode in ("copy", "clobber", "export"): 318 d.addCallback(self.doClobber, self.workdir)
319
320 - def interrupt(self):
321 self.interrupted = True 322 if self.command: 323 self.command.kill("command interrupted")
324
325 - def doVC(self, res):
326 if self.interrupted: 327 raise AbandonChain(1) 328 if self.sourcedirIsUpdateable() and self.sourcedataMatches(): 329 d = self.doVCUpdate() 330 d.addBoth(self.maybeDoVCFallback) 331 else: 332 d = self.doVCFull() 333 d.addBoth(self.maybeDoVCRetry) 334 d.addCallback(self._abandonOnFailure) 335 d.addCallback(self._handleGotRevision) 336 d.addCallback(self.writeSourcedata) 337 return d
338
339 - def sourcedataMatches(self):
340 try: 341 olddata = self.readSourcedata() 342 if olddata != self.sourcedata: 343 return False 344 except IOError: 345 return False 346 return True
347
348 - def sourcedirIsPatched(self):
349 return os.path.exists(os.path.join(self.builder.basedir, 350 self.workdir, 351 ".buildbot-patched"))
352
353 - def _handleGotRevision(self, res):
354 d = defer.maybeDeferred(self.parseGotRevision) 355 d.addCallback(lambda got_revision: 356 self.sendStatus({'got_revision': got_revision})) 357 return d
358
359 - def parseGotRevision(self):
360 """Override this in a subclass. It should return a string that 361 represents which revision was actually checked out, or a Deferred 362 that will fire with such a string. If, in a future build, you were to 363 pass this 'got_revision' string in as the 'revision' component of a 364 SourceStamp, you should wind up with the same source code as this 365 checkout just obtained. 366 367 It is probably most useful to scan self.command.stdout for a string 368 of some sort. Be sure to set keepStdout=True on the VC command that 369 you run, so that you'll have something available to look at. 370 371 If this information is unavailable, just return None.""" 372 373 return None
374
375 - def readSourcedata(self):
376 """ 377 Read the sourcedata file and return its contents 378 379 @returns: source data 380 @raises: IOError if the file does not exist 381 """ 382 return open(self.sourcedatafile, "r").read()
383
384 - def writeSourcedata(self, res):
385 open(self.sourcedatafile, "w").write(self.sourcedata) 386 return res
387
388 - def sourcedirIsUpdateable(self):
389 """Returns True if the tree can be updated.""" 390 raise NotImplementedError("this must be implemented in a subclass")
391
392 - def doVCUpdate(self):
393 """Returns a deferred with the steps to update a checkout.""" 394 raise NotImplementedError("this must be implemented in a subclass")
395
396 - def doVCFull(self):
397 """Returns a deferred with the steps to do a fresh checkout.""" 398 raise NotImplementedError("this must be implemented in a subclass")
399
400 - def maybeDoVCFallback(self, rc):
401 if type(rc) is int and rc == 0: 402 return rc 403 if self.interrupted: 404 raise AbandonChain(1) 405 406 # allow AssertionErrors to fall through, for benefit of the tests; for 407 # all other errors, carry on to try the fallback 408 if isinstance(rc, failure.Failure) and rc.check(AssertionError): 409 return rc 410 411 # Let VCS subclasses have an opportunity to handle 412 # unrecoverable errors without having to clobber the repo 413 self.maybeNotDoVCFallback(rc) 414 msg = "update failed, clobbering and trying again" 415 self.sendStatus({'header': msg + "\n"}) 416 log.msg(msg) 417 d = self.doClobber(None, self.srcdir) 418 d.addCallback(self.doVCFallback2) 419 return d
420
421 - def doVCFallback2(self, res):
422 msg = "now retrying VC operation" 423 self.sendStatus({'header': msg + "\n"}) 424 log.msg(msg) 425 d = self.doVCFull() 426 d.addBoth(self.maybeDoVCRetry) 427 d.addCallback(self._abandonOnFailure) 428 return d
429
430 - def maybeNotDoVCFallback(self, rc):
431 """Override this in a subclass if you want to detect unrecoverable 432 checkout errors where clobbering the repo wouldn't help, and stop 433 the current VC chain before it clobbers the repo for future builds. 434 435 Use 'raise AbandonChain' to pass up a halt if you do detect such.""" 436 pass
437
438 - def maybeDoVCRetry(self, res):
439 """We get here somewhere after a VC chain has finished. res could 440 be:: 441 442 - 0: the operation was successful 443 - nonzero: the operation failed. retry if possible 444 - AbandonChain: the operation failed, someone else noticed. retry. 445 - Failure: some other exception, re-raise 446 """ 447 448 if isinstance(res, failure.Failure): 449 if self.interrupted: 450 return res # don't re-try interrupted builds 451 res.trap(AbandonChain) 452 else: 453 if type(res) is int and res == 0: 454 return res 455 if self.interrupted: 456 raise AbandonChain(1) 457 # if we get here, we should retry, if possible 458 if self.retry: 459 delay, repeats = self.retry 460 if repeats >= 0: 461 self.retry = (delay, repeats-1) 462 msg = ("update failed, trying %d more times after %d seconds" 463 % (repeats, delay)) 464 self.sendStatus({'header': msg + "\n"}) 465 log.msg(msg) 466 d = defer.Deferred() 467 # we are going to do a full checkout, so a clobber is 468 # required first 469 self.doClobber(d, self.workdir) 470 if self.srcdir: 471 self.doClobber(d, self.srcdir) 472 d.addCallback(lambda res: self.doVCFull()) 473 d.addBoth(self.maybeDoVCRetry) 474 self._reactor.callLater(delay, d.callback, None) 475 return d 476 return res
477
478 - def doClobber(self, dummy, dirname, chmodDone=False):
479 d = os.path.join(self.builder.basedir, dirname) 480 if runtime.platformType != "posix": 481 d = threads.deferToThread(utils.rmdirRecursive, d) 482 def cb(_): 483 return 0 # rc=0
484 def eb(f): 485 self.sendStatus({'header' : 'exception from rmdirRecursive\n' + f.getTraceback()}) 486 return -1 # rc=-1
487 d.addCallbacks(cb, eb) 488 return d 489 command = ["rm", "-rf", d] 490 c = runprocess.RunProcess(self.builder, command, self.builder.basedir, 491 sendRC=0, timeout=self.timeout, maxTime=self.maxTime, 492 logEnviron=self.logEnviron, usePTY=False) 493 494 self.command = c 495 # sendRC=0 means the rm command will send stdout/stderr to the 496 # master, but not the rc=0 when it finishes. That job is left to 497 # _sendRC 498 d = c.start() 499 # The rm -rf may fail if there is a left-over subdir with chmod 000 500 # permissions. So if we get a failure, we attempt to chmod suitable 501 # permissions and re-try the rm -rf. 502 if chmodDone: 503 d.addCallback(self._abandonOnFailure) 504 else: 505 d.addCallback(lambda rc: self.doClobberTryChmodIfFail(rc, dirname)) 506 return d 507
508 - def doClobberTryChmodIfFail(self, rc, dirname):
509 assert isinstance(rc, int) 510 if rc == 0: 511 return defer.succeed(0) 512 # Attempt a recursive chmod and re-try the rm -rf after. 513 514 command = ["chmod", "-Rf", "u+rwx", os.path.join(self.builder.basedir, dirname)] 515 if sys.platform.startswith('freebsd'): 516 # Work around a broken 'chmod -R' on FreeBSD (it tries to recurse into a 517 # directory for which it doesn't have permission, before changing that 518 # permission) by running 'find' instead 519 command = ["find", os.path.join(self.builder.basedir, dirname), 520 '-exec', 'chmod', 'u+rwx', '{}', ';' ] 521 c = runprocess.RunProcess(self.builder, command, self.builder.basedir, 522 sendRC=0, timeout=self.timeout, maxTime=self.maxTime, 523 logEnviron=self.logEnviron, usePTY=False) 524 525 self.command = c 526 d = c.start() 527 d.addCallback(self._abandonOnFailure) 528 d.addCallback(lambda dummy: self.doClobber(dummy, dirname, True)) 529 return d
530
531 - def doCopy(self, res):
532 # now copy tree to workdir 533 fromdir = os.path.join(self.builder.basedir, self.srcdir) 534 todir = os.path.join(self.builder.basedir, self.workdir) 535 if runtime.platformType != "posix": 536 d = threads.deferToThread(shutil.copytree, fromdir, todir) 537 def cb(_): 538 return 0 # rc=0
539 def eb(f): 540 self.sendStatus({'header' : 'exception from copytree\n' + f.getTraceback()}) 541 return -1 # rc=-1 542 d.addCallbacks(cb, eb) 543 return d 544 545 if not os.path.exists(os.path.dirname(todir)): 546 os.makedirs(os.path.dirname(todir)) 547 if os.path.exists(todir): 548 # I don't think this happens, but just in case.. 549 log.msg("cp target '%s' already exists -- cp will not do what you think!" % todir) 550 551 command = ['cp', '-R', '-P', '-p', fromdir, todir] 552 c = runprocess.RunProcess(self.builder, command, self.builder.basedir, 553 sendRC=False, timeout=self.timeout, maxTime=self.maxTime, 554 logEnviron=self.logEnviron, usePTY=False) 555 self.command = c 556 d = c.start() 557 d.addCallback(self._abandonOnFailure) 558 return d 559
560 - def doPatch(self, res):
561 patchlevel = self.patch[0] 562 diff = self.patch[1] 563 root = None 564 if len(self.patch) >= 3: 565 root = self.patch[2] 566 command = [ 567 utils.getCommand("patch"), 568 '-p%d' % patchlevel, 569 '--remove-empty-files', 570 '--force', 571 '--forward', 572 '-i', '.buildbot-diff', 573 ] 574 dir = os.path.join(self.builder.basedir, self.workdir) 575 # Mark the directory so we don't try to update it later, or at least try 576 # to revert first. 577 open(os.path.join(dir, ".buildbot-patched"), "w").write("patched\n") 578 579 # write the diff to a file, for reading later 580 open(os.path.join(dir, ".buildbot-diff"), "w").write(diff) 581 582 # Update 'dir' with the 'root' option. Make sure it is a subdirectory 583 # of dir. 584 if (root and 585 os.path.abspath(os.path.join(dir, root) 586 ).startswith(os.path.abspath(dir))): 587 dir = os.path.join(dir, root) 588 589 # now apply the patch 590 c = runprocess.RunProcess(self.builder, command, dir, 591 sendRC=False, timeout=self.timeout, 592 maxTime=self.maxTime, logEnviron=self.logEnviron, 593 usePTY=False) 594 self.command = c 595 d = c.start() 596 597 # clean up the temp file 598 def cleanup(x): 599 try: 600 os.unlink(os.path.join(dir, ".buildbot-diff")) 601 except: 602 pass 603 return x
604 d.addBoth(cleanup) 605 606 d.addCallback(self._abandonOnFailure) 607 return d 608
609 - def setFileContents(self, filename, contents):
610 """Put the given C{contents} in C{filename}; this is a bit more 611 succinct than opening, writing, and closing, and has the advantage of 612 being patchable in tests. Note that the enclosing directory is 613 not automatically created, nor is this an "atomic" overwrite.""" 614 f = open(filename, 'w') 615 f.write(contents) 616 f.close()
617