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