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