Package buildbot :: Module master
[frames] | no frames]

Source Code for Module buildbot.master

   1  # -*- test-case-name: buildbot.test.test_run -*- 
   2   
   3  import os 
   4  import signal 
   5  import time 
   6  import textwrap 
   7   
   8  from zope.interface import implements 
   9  from twisted.python import log, components 
  10  from twisted.python.failure import Failure 
  11  from twisted.internet import defer, reactor 
  12  from twisted.spread import pb 
  13  from twisted.cred import portal, checkers 
  14  from twisted.application import service, strports 
  15  from twisted.application.internet import TimerService 
  16   
  17  import buildbot 
  18  # sibling imports 
  19  from buildbot.util import now, safeTranslate, eventual 
  20  from buildbot.pbutil import NewCredPerspective 
  21  from buildbot.process.builder import Builder, IDLE 
  22  from buildbot.status.builder import Status, BuildSetStatus 
  23  from buildbot.changes.changes import Change 
  24  from buildbot.changes.manager import ChangeManager 
  25  from buildbot import interfaces, locks 
  26  from buildbot.process.properties import Properties 
  27  from buildbot.config import BuilderConfig 
  28  from buildbot.process.builder import BuilderControl 
  29  from buildbot.db.dbspec import DBSpec 
  30  from buildbot.db import connector, exceptions 
  31  from buildbot.db.schema.manager import DBSchemaManager 
  32  from buildbot.schedulers.manager import SchedulerManager 
  33  from buildbot.util.loop import DelegateLoop 
  34   
  35  ######################################## 
  36   
37 -class BotMaster(service.MultiService):
38 39 """This is the master-side service which manages remote buildbot slaves. 40 It provides them with BuildSlaves, and distributes file change 41 notification messages to them. 42 """ 43 44 debug = 0 45 reactor = reactor 46
47 - def __init__(self):
48 service.MultiService.__init__(self) 49 self.builders = {} 50 self.builderNames = [] 51 # builders maps Builder names to instances of bb.p.builder.Builder, 52 # which is the master-side object that defines and controls a build. 53 # They are added by calling botmaster.addBuilder() from the startup 54 # code. 55 56 # self.slaves contains a ready BuildSlave instance for each 57 # potential buildslave, i.e. all the ones listed in the config file. 58 # If the slave is connected, self.slaves[slavename].slave will 59 # contain a RemoteReference to their Bot instance. If it is not 60 # connected, that attribute will hold None. 61 self.slaves = {} # maps slavename to BuildSlave 62 self.statusClientService = None 63 self.watchers = {} 64 65 # self.locks holds the real Lock instances 66 self.locks = {} 67 68 # self.mergeRequests is the callable override for merging build 69 # requests 70 self.mergeRequests = None 71 72 # self.prioritizeBuilders is the callable override for builder order 73 # traversal 74 self.prioritizeBuilders = None 75 76 self.loop = DelegateLoop(self._get_processors) 77 self.loop.setServiceParent(self) 78 79 self.shuttingDown = False
80
81 - def setMasterName(self, name, incarnation):
82 self.master_name = name 83 self.master_incarnation = incarnation
84
85 - def cleanShutdown(self):
86 if self.shuttingDown: 87 return 88 log.msg("Initiating clean shutdown") 89 self.shuttingDown = True 90 91 # Wait for all builds to finish 92 l = [] 93 for builder in self.builders.values(): 94 for build in builder.builder_status.getCurrentBuilds(): 95 l.append(build.waitUntilFinished()) 96 if len(l) == 0: 97 log.msg("No running jobs, starting shutdown immediately") 98 self.loop.trigger() 99 d = self.loop.when_quiet() 100 else: 101 log.msg("Waiting for %i build(s) to finish" % len(l)) 102 d = defer.DeferredList(l) 103 d.addCallback(lambda ign: self.loop.when_quiet()) 104 105 # Flush the eventual queue 106 d.addCallback(eventual.flushEventualQueue) 107 108 # Finally, shut the whole process down 109 def shutdown(ign): 110 # Double check that we're still supposed to be shutting down 111 # The shutdown may have been cancelled! 112 if self.shuttingDown: 113 # Check that there really aren't any running builds 114 for builder in self.builders.values(): 115 n = len(builder.builder_status.getCurrentBuilds()) 116 if n > 0: 117 log.msg("Not shutting down, builder %s has %i builds running" % (builder, n)) 118 log.msg("Trying shutdown sequence again") 119 self.shuttingDown = False 120 self.cleanShutdown() 121 return 122 log.msg("Stopping reactor") 123 self.reactor.stop()
124 d.addCallback(shutdown) 125 return d
126
127 - def cancelCleanShutdown(self):
128 if not self.shuttingDown: 129 return 130 log.msg("Cancelling clean shutdown") 131 self.shuttingDown = False
132
133 - def _sortfunc(self, b1, b2):
134 t1 = b1.getOldestRequestTime() 135 t2 = b2.getOldestRequestTime() 136 # If t1 or t2 is None, then there are no build requests, 137 # so sort it at the end 138 if t1 is None: 139 return 1 140 if t2 is None: 141 return -1 142 return cmp(t1, t2)
143
144 - def _sort_builders(self, parent, builders):
145 return sorted(builders, self._sortfunc)
146
147 - def _get_processors(self):
148 if self.shuttingDown: 149 return [] 150 builders = self.builders.values() 151 sorter = self.prioritizeBuilders or self._sort_builders 152 try: 153 builders = sorter(self.parent, builders) 154 except: 155 log.msg("Exception prioritizing builders") 156 log.err(Failure()) 157 # leave them in the original order 158 return [b.run for b in builders]
159
160 - def trigger_add_buildrequest(self, category, *brids):
161 # a buildrequest has been added or resubmitted 162 self.loop.trigger()
163 - def triggerNewBuildCheck(self):
164 # called when a build finishes, or a slave attaches 165 self.loop.trigger()
166 167 # these four are convenience functions for testing 168
169 - def waitUntilBuilderAttached(self, name):
170 b = self.builders[name] 171 #if b.slaves: 172 # return defer.succeed(None) 173 d = defer.Deferred() 174 b.watchers['attach'].append(d) 175 return d
176
177 - def waitUntilBuilderDetached(self, name):
178 b = self.builders.get(name) 179 if not b or not b.slaves: 180 return defer.succeed(None) 181 d = defer.Deferred() 182 b.watchers['detach'].append(d) 183 return d
184
185 - def waitUntilBuilderFullyDetached(self, name):
186 b = self.builders.get(name) 187 # TODO: this looks too deeply inside the Builder object 188 if not b or not b.slaves: 189 return defer.succeed(None) 190 d = defer.Deferred() 191 b.watchers['detach_all'].append(d) 192 return d
193
194 - def waitUntilBuilderIdle(self, name):
195 b = self.builders[name] 196 # TODO: this looks way too deeply inside the Builder object 197 for sb in b.slaves: 198 if sb.state != IDLE: 199 d = defer.Deferred() 200 b.watchers['idle'].append(d) 201 return d 202 return defer.succeed(None)
203
204 - def loadConfig_Slaves(self, new_slaves):
205 old_slaves = [c for c in list(self) 206 if interfaces.IBuildSlave.providedBy(c)] 207 208 # identify added/removed slaves. For each slave we construct a tuple 209 # of (name, password, class), and we consider the slave to be already 210 # present if the tuples match. (we include the class to make sure 211 # that BuildSlave(name,pw) is different than 212 # SubclassOfBuildSlave(name,pw) ). If the password or class has 213 # changed, we will remove the old version of the slave and replace it 214 # with a new one. If anything else has changed, we just update the 215 # old BuildSlave instance in place. If the name has changed, of 216 # course, it looks exactly the same as deleting one slave and adding 217 # an unrelated one. 218 old_t = {} 219 for s in old_slaves: 220 old_t[(s.slavename, s.password, s.__class__)] = s 221 new_t = {} 222 for s in new_slaves: 223 new_t[(s.slavename, s.password, s.__class__)] = s 224 removed = [old_t[t] 225 for t in old_t 226 if t not in new_t] 227 added = [new_t[t] 228 for t in new_t 229 if t not in old_t] 230 remaining_t = [t 231 for t in new_t 232 if t in old_t] 233 # removeSlave will hang up on the old bot 234 dl = [] 235 for s in removed: 236 dl.append(self.removeSlave(s)) 237 d = defer.DeferredList(dl, fireOnOneErrback=True) 238 def _add(res): 239 for s in added: 240 self.addSlave(s) 241 for t in remaining_t: 242 old_t[t].update(new_t[t])
243 d.addCallback(_add) 244 return d 245
246 - def addSlave(self, s):
247 s.setServiceParent(self) 248 s.setBotmaster(self) 249 self.slaves[s.slavename] = s
250
251 - def removeSlave(self, s):
252 # TODO: technically, disownServiceParent could return a Deferred 253 s.disownServiceParent() 254 d = self.slaves[s.slavename].disconnect() 255 del self.slaves[s.slavename] 256 return d
257
258 - def slaveLost(self, bot):
259 for name, b in self.builders.items(): 260 if bot.slavename in b.slavenames: 261 b.detached(bot)
262
263 - def getBuildersForSlave(self, slavename):
264 return [b 265 for b in self.builders.values() 266 if slavename in b.slavenames]
267
268 - def getBuildernames(self):
269 return self.builderNames
270
271 - def getBuilders(self):
272 allBuilders = [self.builders[name] for name in self.builderNames] 273 return allBuilders
274
275 - def setBuilders(self, builders):
276 # TODO: remove self.builders and just use the Service hierarchy to 277 # keep track of active builders. We could keep self.builderNames to 278 # retain ordering, if it seems important. 279 self.builders = {} 280 self.builderNames = [] 281 d = defer.DeferredList([b.disownServiceParent() for b in list(self) 282 if isinstance(b, Builder)], 283 fireOnOneErrback=True) 284 def _add(ign): 285 log.msg("setBuilders._add: %s %s" % (list(self), builders)) 286 for b in builders: 287 for slavename in b.slavenames: 288 # this is actually validated earlier 289 assert slavename in self.slaves 290 self.builders[b.name] = b 291 self.builderNames.append(b.name) 292 b.setBotmaster(self) 293 b.setServiceParent(self)
294 d.addCallback(_add) 295 d.addCallback(lambda ign: self._updateAllSlaves()) 296 return d 297
298 - def _updateAllSlaves(self):
299 """Notify all buildslaves about changes in their Builders.""" 300 dl = [] 301 for s in self.slaves.values(): 302 d = s.updateSlave() 303 d.addErrback(log.err) 304 dl.append(d) 305 return defer.DeferredList(dl)
306
307 - def shouldMergeRequests(self, builder, req1, req2):
308 """Determine whether two BuildRequests should be merged for 309 the given builder. 310 311 """ 312 if self.mergeRequests is not None: 313 return self.mergeRequests(builder, req1, req2) 314 return req1.canBeMergedWith(req2)
315
316 - def getPerspective(self, mind, slavename):
317 sl = self.slaves[slavename] 318 if not sl: 319 return None 320 321 # record when this connection attempt occurred 322 sl.recordConnectTime() 323 324 if sl.isConnected(): 325 # uh-oh, we've got a duplicate slave. The most likely 326 # explanation is that the slave is behind a slow link, thinks we 327 # went away, and has attempted to reconnect, so we've got two 328 # "connections" from the same slave. The old may not be stale at this 329 # point, if there are two slave proceses out there with the same name, 330 # so instead of booting the old (which may be in the middle of a build), 331 # we reject the new connection and ping the old slave. 332 log.msg("duplicate slave %s; rejecting new slave and pinging old" % sl.slavename) 333 334 # just in case we've got two identically-configured slaves, 335 # report the IP addresses of both so someone can resolve the 336 # squabble 337 old_tport = sl.slave.broker.transport 338 new_tport = mind.broker.transport 339 log.msg("old slave was connected from", old_tport.getPeer()) 340 log.msg("new slave is from", new_tport.getPeer()) 341 342 # ping the old slave. If this kills it, then the new slave will connect 343 # again and everyone will be happy. 344 d = sl.slave.callRemote("print", "master got a duplicate connection; keeping this one") 345 346 # now return a dummy avatar and kill the new connection in 5 347 # seconds, thereby giving the ping a bit of time to kill the old 348 # connection, if necessary 349 def kill(): 350 log.msg("killing new slave on", new_tport.getPeer()) 351 new_tport.loseConnection()
352 reactor.callLater(5, kill) 353 class DummyAvatar(pb.Avatar): 354 def attached(self, *args): 355 pass 356 def detached(self, *args): 357 pass 358 return DummyAvatar() 359 360 return sl 361
362 - def shutdownSlaves(self):
363 # TODO: make this into a bot method rather than a builder method 364 for b in self.slaves.values(): 365 b.shutdownSlave()
366
367 - def stopService(self):
368 for b in self.builders.values(): 369 b.builder_status.addPointEvent(["master", "shutdown"]) 370 b.builder_status.saveYourself() 371 return service.MultiService.stopService(self)
372
373 - def getLockByID(self, lockid):
374 """Convert a Lock identifier into an actual Lock instance. 375 @param lockid: a locks.MasterLock or locks.SlaveLock instance 376 @return: a locks.RealMasterLock or locks.RealSlaveLock instance 377 """ 378 assert isinstance(lockid, (locks.MasterLock, locks.SlaveLock)) 379 if not lockid in self.locks: 380 self.locks[lockid] = lockid.lockClass(lockid) 381 # if the master.cfg file has changed maxCount= on the lock, the next 382 # time a build is started, they'll get a new RealLock instance. Note 383 # that this requires that MasterLock and SlaveLock (marker) instances 384 # be hashable and that they should compare properly. 385 return self.locks[lockid]
386 387 ######################################## 388 389 390
391 -class DebugPerspective(NewCredPerspective):
392 - def attached(self, mind):
393 return self
394 - def detached(self, mind):
395 pass
396
397 - def perspective_requestBuild(self, buildername, reason, branch, revision, properties={}):
398 from buildbot.sourcestamp import SourceStamp 399 c = interfaces.IControl(self.master) 400 bc = c.getBuilder(buildername) 401 ss = SourceStamp(branch, revision) 402 bpr = Properties() 403 bpr.update(properties, "remote requestBuild") 404 bc.submitBuildRequest(ss, reason, bpr)
405
406 - def perspective_pingBuilder(self, buildername):
407 c = interfaces.IControl(self.master) 408 bc = c.getBuilder(buildername) 409 bc.ping()
410
411 - def perspective_fakeChange(self, file, revision=None, who="fakeUser", 412 branch=None, repository="", 413 project=""):
414 change = Change(who, [file], "some fake comments\n", 415 branch=branch, revision=revision, 416 repository=repository, project=project) 417 c = interfaces.IControl(self.master) 418 c.addChange(change)
419
420 - def perspective_setCurrentState(self, buildername, state):
421 builder = self.botmaster.builders.get(buildername) 422 if not builder: return 423 if state == "offline": 424 builder.statusbag.currentlyOffline() 425 if state == "idle": 426 builder.statusbag.currentlyIdle() 427 if state == "waiting": 428 builder.statusbag.currentlyWaiting(now()+10) 429 if state == "building": 430 builder.statusbag.currentlyBuilding(None)
431 - def perspective_reload(self):
432 print "doing reload of the config file" 433 self.master.loadTheConfigFile()
434 - def perspective_pokeIRC(self):
435 print "saying something on IRC" 436 from buildbot.status import words 437 for s in self.master: 438 if isinstance(s, words.IRC): 439 bot = s.f 440 for channel in bot.channels: 441 print " channel", channel 442 bot.p.msg(channel, "Ow, quit it")
443
444 - def perspective_print(self, msg):
445 print "debug", msg
446
447 -class Dispatcher:
448 implements(portal.IRealm) 449
450 - def __init__(self):
451 self.names = {}
452
453 - def register(self, name, afactory):
454 self.names[name] = afactory
455 - def unregister(self, name):
456 del self.names[name]
457
458 - def requestAvatar(self, avatarID, mind, interface):
459 assert interface == pb.IPerspective 460 afactory = self.names.get(avatarID) 461 if afactory: 462 p = afactory.getPerspective() 463 elif avatarID == "change": 464 raise ValueError("no PBChangeSource installed") 465 elif avatarID == "debug": 466 p = DebugPerspective() 467 p.master = self.master 468 p.botmaster = self.botmaster 469 elif avatarID == "statusClient": 470 p = self.statusClientService.getPerspective() 471 else: 472 # it must be one of the buildslaves: no other names will make it 473 # past the checker 474 p = self.botmaster.getPerspective(mind, avatarID) 475 476 if not p: 477 raise ValueError("no perspective for '%s'" % avatarID) 478 479 d = defer.maybeDeferred(p.attached, mind) 480 def _avatarAttached(_, mind): 481 return (pb.IPerspective, p, lambda: p.detached(mind))
482 d.addCallback(_avatarAttached, mind) 483 return d
484 485 486 ######################################## 487
488 -class _Unset: pass # marker
489
490 -class LogRotation:
491 '''holds log rotation parameters (for WebStatus)'''
492 - def __init__(self):
493 self.rotateLength = 1 * 1000 * 1000 494 self.maxRotatedFiles = 10
495
496 -class BuildMaster(service.MultiService):
497 debug = 0 498 manhole = None 499 debugPassword = None 500 projectName = "(unspecified)" 501 projectURL = None 502 buildbotURL = None 503 change_svc = None 504 properties = Properties() 505
506 - def __init__(self, basedir, configFileName="master.cfg", db_spec=None):
507 service.MultiService.__init__(self) 508 self.setName("buildmaster") 509 self.basedir = basedir 510 self.configFileName = configFileName 511 512 # the dispatcher is the realm in which all inbound connections are 513 # looked up: slave builders, change notifications, status clients, and 514 # the debug port 515 dispatcher = Dispatcher() 516 dispatcher.master = self 517 self.dispatcher = dispatcher 518 self.checker = checkers.InMemoryUsernamePasswordDatabaseDontUse() 519 # the checker starts with no user/passwd pairs: they are added later 520 p = portal.Portal(dispatcher) 521 p.registerChecker(self.checker) 522 self.slaveFactory = pb.PBServerFactory(p) 523 self.slaveFactory.unsafeTracebacks = True # let them see exceptions 524 525 self.slavePortnum = None 526 self.slavePort = None 527 528 self.change_svc = ChangeManager() 529 self.change_svc.setServiceParent(self) 530 self.dispatcher.changemaster = self.change_svc 531 532 try: 533 hostname = os.uname()[1] # only on unix 534 except AttributeError: 535 hostname = "?" 536 self.master_name = "%s:%s" % (hostname, os.path.abspath(self.basedir)) 537 self.master_incarnation = "pid%d-boot%d" % (os.getpid(), time.time()) 538 539 self.botmaster = BotMaster() 540 self.botmaster.setName("botmaster") 541 self.botmaster.setMasterName(self.master_name, self.master_incarnation) 542 self.botmaster.setServiceParent(self) 543 self.dispatcher.botmaster = self.botmaster 544 545 self.status = Status(self.botmaster, self.basedir) 546 self.statusTargets = [] 547 548 self.db = None 549 self.db_url = None 550 self.db_poll_interval = _Unset 551 if db_spec: 552 self.loadDatabase(db_spec) 553 554 self.readConfig = False 555 556 # create log_rotation object and set default parameters (used by WebStatus) 557 self.log_rotation = LogRotation()
558
559 - def startService(self):
560 service.MultiService.startService(self) 561 if not self.readConfig: 562 # TODO: consider catching exceptions during this call to 563 # loadTheConfigFile and bailing (reactor.stop) if it fails, 564 # since without a config file we can't do anything except reload 565 # the config file, and it would be nice for the user to discover 566 # this quickly. 567 self.loadTheConfigFile() 568 if hasattr(signal, "SIGHUP"): 569 signal.signal(signal.SIGHUP, self._handleSIGHUP) 570 for b in self.botmaster.builders.values(): 571 b.builder_status.addPointEvent(["master", "started"]) 572 b.builder_status.saveYourself()
573
574 - def _handleSIGHUP(self, *args):
575 reactor.callLater(0, self.loadTheConfigFile)
576
577 - def getStatus(self):
578 """ 579 @rtype: L{buildbot.status.builder.Status} 580 """ 581 return self.status
582
583 - def loadTheConfigFile(self, configFile=None):
584 if not configFile: 585 configFile = os.path.join(self.basedir, self.configFileName) 586 587 log.msg("Creating BuildMaster -- buildbot.version: %s" % buildbot.version) 588 log.msg("loading configuration from %s" % configFile) 589 configFile = os.path.expanduser(configFile) 590 591 try: 592 f = open(configFile, "r") 593 except IOError, e: 594 log.msg("unable to open config file '%s'" % configFile) 595 log.msg("leaving old configuration in place") 596 log.err(e) 597 return 598 599 try: 600 d = self.loadConfig(f) 601 except: 602 log.msg("error during loadConfig") 603 log.err() 604 log.msg("The new config file is unusable, so I'll ignore it.") 605 log.msg("I will keep using the previous config file instead.") 606 return # sorry unit tests 607 f.close() 608 return d # for unit tests
609
610 - def loadConfig(self, f, check_synchronously_only=False):
611 """Internal function to load a specific configuration file. Any 612 errors in the file will be signalled by raising an exception. 613 614 If check_synchronously_only=True, I will return (with None) 615 synchronously, after checking the config file for sanity, or raise an 616 exception. I may also emit some DeprecationWarnings. 617 618 If check_synchronously_only=False, I will return a Deferred that 619 fires (with None) when the configuration changes have been completed. 620 This may involve a round-trip to each buildslave that was involved.""" 621 622 localDict = {'basedir': os.path.expanduser(self.basedir)} 623 try: 624 exec f in localDict 625 except: 626 log.msg("error while parsing config file") 627 raise 628 629 try: 630 config = localDict['BuildmasterConfig'] 631 except KeyError: 632 log.err("missing config dictionary") 633 log.err("config file must define BuildmasterConfig") 634 raise 635 636 known_keys = ("slaves", "change_source", 637 "schedulers", "builders", "mergeRequests", 638 "slavePortnum", "debugPassword", "logCompressionLimit", 639 "manhole", "status", "projectName", "projectURL", 640 "buildbotURL", "properties", "prioritizeBuilders", 641 "eventHorizon", "buildCacheSize", "changeCacheSize", 642 "logHorizon", "buildHorizon", "changeHorizon", 643 "logMaxSize", "logMaxTailSize", "logCompressionMethod", 644 "db_url", "multiMaster", "db_poll_interval", 645 ) 646 for k in config.keys(): 647 if k not in known_keys: 648 log.msg("unknown key '%s' defined in config dictionary" % k) 649 650 try: 651 # required 652 schedulers = config['schedulers'] 653 builders = config['builders'] 654 slavePortnum = config['slavePortnum'] 655 #slaves = config['slaves'] 656 #change_source = config['change_source'] 657 658 # optional 659 db_url = config.get("db_url", "sqlite:///state.sqlite") 660 db_poll_interval = config.get("db_poll_interval", None) 661 debugPassword = config.get('debugPassword') 662 manhole = config.get('manhole') 663 status = config.get('status', []) 664 projectName = config.get('projectName') 665 projectURL = config.get('projectURL') 666 buildbotURL = config.get('buildbotURL') 667 properties = config.get('properties', {}) 668 buildCacheSize = config.get('buildCacheSize', None) 669 changeCacheSize = config.get('changeCacheSize', None) 670 eventHorizon = config.get('eventHorizon', 50) 671 logHorizon = config.get('logHorizon', None) 672 buildHorizon = config.get('buildHorizon', None) 673 logCompressionLimit = config.get('logCompressionLimit', 4*1024) 674 if logCompressionLimit is not None and not \ 675 isinstance(logCompressionLimit, int): 676 raise ValueError("logCompressionLimit needs to be bool or int") 677 logCompressionMethod = config.get('logCompressionMethod', "bz2") 678 if logCompressionMethod not in ('bz2', 'gz'): 679 raise ValueError("logCompressionMethod needs to be 'bz2', or 'gz'") 680 logMaxSize = config.get('logMaxSize') 681 if logMaxSize is not None and not \ 682 isinstance(logMaxSize, int): 683 raise ValueError("logMaxSize needs to be None or int") 684 logMaxTailSize = config.get('logMaxTailSize') 685 if logMaxTailSize is not None and not \ 686 isinstance(logMaxTailSize, int): 687 raise ValueError("logMaxTailSize needs to be None or int") 688 mergeRequests = config.get('mergeRequests') 689 if mergeRequests is not None and not callable(mergeRequests): 690 raise ValueError("mergeRequests must be a callable") 691 prioritizeBuilders = config.get('prioritizeBuilders') 692 if prioritizeBuilders is not None and not callable(prioritizeBuilders): 693 raise ValueError("prioritizeBuilders must be callable") 694 changeHorizon = config.get("changeHorizon") 695 if changeHorizon is not None and not isinstance(changeHorizon, int): 696 raise ValueError("changeHorizon needs to be an int") 697 698 multiMaster = config.get("multiMaster", False) 699 700 except KeyError: 701 log.msg("config dictionary is missing a required parameter") 702 log.msg("leaving old configuration in place") 703 raise 704 705 if "sources" in config: 706 m = ("c['sources'] is deprecated as of 0.7.6 and is no longer " 707 "accepted in >= 0.8.0 . Please use c['change_source'] instead.") 708 raise KeyError(m) 709 710 if "bots" in config: 711 m = ("c['bots'] is deprecated as of 0.7.6 and is no longer " 712 "accepted in >= 0.8.0 . Please use c['slaves'] instead.") 713 raise KeyError(m) 714 715 slaves = config.get('slaves', []) 716 if "slaves" not in config: 717 log.msg("config dictionary must have a 'slaves' key") 718 log.msg("leaving old configuration in place") 719 raise KeyError("must have a 'slaves' key") 720 721 if changeHorizon is not None: 722 self.change_svc.changeHorizon = changeHorizon 723 724 change_source = config.get('change_source', []) 725 if isinstance(change_source, (list, tuple)): 726 change_sources = change_source 727 else: 728 change_sources = [change_source] 729 730 # do some validation first 731 for s in slaves: 732 assert interfaces.IBuildSlave.providedBy(s) 733 if s.slavename in ("debug", "change", "status"): 734 raise KeyError( 735 "reserved name '%s' used for a bot" % s.slavename) 736 if config.has_key('interlocks'): 737 raise KeyError("c['interlocks'] is no longer accepted") 738 assert self.db_url is None or db_url == self.db_url, \ 739 "Cannot change db_url after master has started" 740 assert db_poll_interval is None or isinstance(db_poll_interval, int), \ 741 "db_poll_interval must be an integer: seconds between polls" 742 assert self.db_poll_interval is _Unset or db_poll_interval == self.db_poll_interval, \ 743 "Cannot change db_poll_interval after master has started" 744 745 assert isinstance(change_sources, (list, tuple)) 746 for s in change_sources: 747 assert interfaces.IChangeSource(s, None) 748 # this assertion catches c['schedulers'] = Scheduler(), since 749 # Schedulers are service.MultiServices and thus iterable. 750 errmsg = "c['schedulers'] must be a list of Scheduler instances" 751 assert isinstance(schedulers, (list, tuple)), errmsg 752 for s in schedulers: 753 assert interfaces.IScheduler(s, None), errmsg 754 assert isinstance(status, (list, tuple)) 755 for s in status: 756 assert interfaces.IStatusReceiver(s, None) 757 758 slavenames = [s.slavename for s in slaves] 759 buildernames = [] 760 dirnames = [] 761 762 # convert builders from objects to config dictionaries 763 builders_dicts = [] 764 for b in builders: 765 if isinstance(b, BuilderConfig): 766 builders_dicts.append(b.getConfigDict()) 767 elif type(b) is dict: 768 builders_dicts.append(b) 769 else: 770 raise ValueError("builder %s is not a BuilderConfig object (or a dict)" % b) 771 builders = builders_dicts 772 773 for b in builders: 774 if b.has_key('slavename') and b['slavename'] not in slavenames: 775 raise ValueError("builder %s uses undefined slave %s" \ 776 % (b['name'], b['slavename'])) 777 for n in b.get('slavenames', []): 778 if n not in slavenames: 779 raise ValueError("builder %s uses undefined slave %s" \ 780 % (b['name'], n)) 781 if b['name'] in buildernames: 782 raise ValueError("duplicate builder name %s" 783 % b['name']) 784 buildernames.append(b['name']) 785 786 # sanity check name (BuilderConfig does this too) 787 if b['name'].startswith("_"): 788 errmsg = ("builder names must not start with an " 789 "underscore: " + b['name']) 790 log.err(errmsg) 791 raise ValueError(errmsg) 792 793 # Fix the dictionary with default values, in case this wasn't 794 # specified with a BuilderConfig object (which sets the same defaults) 795 b.setdefault('builddir', safeTranslate(b['name'])) 796 b.setdefault('slavebuilddir', b['builddir']) 797 b.setdefault('buildHorizon', buildHorizon) 798 b.setdefault('logHorizon', logHorizon) 799 b.setdefault('eventHorizon', eventHorizon) 800 if b['builddir'] in dirnames: 801 raise ValueError("builder %s reuses builddir %s" 802 % (b['name'], b['builddir'])) 803 dirnames.append(b['builddir']) 804 805 unscheduled_buildernames = buildernames[:] 806 schedulernames = [] 807 for s in schedulers: 808 for b in s.listBuilderNames(): 809 # Skip checks for builders in multimaster mode 810 if not multiMaster: 811 assert b in buildernames, \ 812 "%s uses unknown builder %s" % (s, b) 813 if b in unscheduled_buildernames: 814 unscheduled_buildernames.remove(b) 815 816 if s.name in schedulernames: 817 msg = ("Schedulers must have unique names, but " 818 "'%s' was a duplicate" % (s.name,)) 819 raise ValueError(msg) 820 schedulernames.append(s.name) 821 822 # Skip the checks for builders in multimaster mode 823 if not multiMaster and unscheduled_buildernames: 824 log.msg("Warning: some Builders have no Schedulers to drive them:" 825 " %s" % (unscheduled_buildernames,)) 826 827 # assert that all locks used by the Builds and their Steps are 828 # uniquely named. 829 lock_dict = {} 830 for b in builders: 831 for l in b.get('locks', []): 832 if isinstance(l, locks.LockAccess): # User specified access to the lock 833 l = l.lockid 834 if lock_dict.has_key(l.name): 835 if lock_dict[l.name] is not l: 836 raise ValueError("Two different locks (%s and %s) " 837 "share the name %s" 838 % (l, lock_dict[l.name], l.name)) 839 else: 840 lock_dict[l.name] = l 841 # TODO: this will break with any BuildFactory that doesn't use a 842 # .steps list, but I think the verification step is more 843 # important. 844 for s in b['factory'].steps: 845 for l in s[1].get('locks', []): 846 if isinstance(l, locks.LockAccess): # User specified access to the lock 847 l = l.lockid 848 if lock_dict.has_key(l.name): 849 if lock_dict[l.name] is not l: 850 raise ValueError("Two different locks (%s and %s)" 851 " share the name %s" 852 % (l, lock_dict[l.name], l.name)) 853 else: 854 lock_dict[l.name] = l 855 856 if not isinstance(properties, dict): 857 raise ValueError("c['properties'] must be a dictionary") 858 859 # slavePortnum supposed to be a strports specification 860 if type(slavePortnum) is int: 861 slavePortnum = "tcp:%d" % slavePortnum 862 863 if check_synchronously_only: 864 return 865 # now we're committed to implementing the new configuration, so do 866 # it atomically 867 # TODO: actually, this is spread across a couple of Deferreds, so it 868 # really isn't atomic. 869 870 d = defer.succeed(None) 871 872 self.projectName = projectName 873 self.projectURL = projectURL 874 self.buildbotURL = buildbotURL 875 876 self.properties = Properties() 877 self.properties.update(properties, self.configFileName) 878 879 self.status.logCompressionLimit = logCompressionLimit 880 self.status.logCompressionMethod = logCompressionMethod 881 self.status.logMaxSize = logMaxSize 882 self.status.logMaxTailSize = logMaxTailSize 883 # Update any of our existing builders with the current log parameters. 884 # This is required so that the new value is picked up after a 885 # reconfig. 886 for builder in self.botmaster.builders.values(): 887 builder.builder_status.setLogCompressionLimit(logCompressionLimit) 888 builder.builder_status.setLogCompressionMethod(logCompressionMethod) 889 builder.builder_status.setLogMaxSize(logMaxSize) 890 builder.builder_status.setLogMaxTailSize(logMaxTailSize) 891 892 if mergeRequests is not None: 893 self.botmaster.mergeRequests = mergeRequests 894 if prioritizeBuilders is not None: 895 self.botmaster.prioritizeBuilders = prioritizeBuilders 896 897 self.buildCacheSize = buildCacheSize 898 self.changeCacheSize = changeCacheSize 899 self.eventHorizon = eventHorizon 900 self.logHorizon = logHorizon 901 self.buildHorizon = buildHorizon 902 903 # Set up the database 904 d.addCallback(lambda res: 905 self.loadConfig_Database(db_url, db_poll_interval)) 906 907 # self.slaves: Disconnect any that were attached and removed from the 908 # list. Update self.checker with the new list of passwords, including 909 # debug/change/status. 910 d.addCallback(lambda res: self.loadConfig_Slaves(slaves)) 911 912 # self.debugPassword 913 if debugPassword: 914 self.checker.addUser("debug", debugPassword) 915 self.debugPassword = debugPassword 916 917 # self.manhole 918 if manhole != self.manhole: 919 # changing 920 if self.manhole: 921 # disownServiceParent may return a Deferred 922 d.addCallback(lambda res: self.manhole.disownServiceParent()) 923 def _remove(res): 924 self.manhole = None 925 return res
926 d.addCallback(_remove) 927 if manhole: 928 def _add(res): 929 self.manhole = manhole 930 manhole.setServiceParent(self)
931 d.addCallback(_add) 932 933 # add/remove self.botmaster.builders to match builders. The 934 # botmaster will handle startup/shutdown issues. 935 d.addCallback(lambda res: self.loadConfig_Builders(builders)) 936 937 d.addCallback(lambda res: self.loadConfig_status(status)) 938 939 # Schedulers are added after Builders in case they start right away 940 d.addCallback(lambda res: 941 self.scheduler_manager.updateSchedulers(schedulers)) 942 # and Sources go after Schedulers for the same reason 943 d.addCallback(lambda res: self.loadConfig_Sources(change_sources)) 944 945 # self.slavePort 946 if self.slavePortnum != slavePortnum: 947 if self.slavePort: 948 def closeSlavePort(res): 949 d1 = self.slavePort.disownServiceParent() 950 self.slavePort = None 951 return d1 952 d.addCallback(closeSlavePort) 953 if slavePortnum is not None: 954 def openSlavePort(res): 955 self.slavePort = strports.service(slavePortnum, 956 self.slaveFactory) 957 self.slavePort.setServiceParent(self) 958 d.addCallback(openSlavePort) 959 log.msg("BuildMaster listening on port %s" % slavePortnum) 960 self.slavePortnum = slavePortnum 961 962 log.msg("configuration update started") 963 def _done(res): 964 self.readConfig = True 965 log.msg("configuration update complete") 966 d.addCallback(_done) 967 d.addCallback(lambda res: self.botmaster.triggerNewBuildCheck()) 968 d.addErrback(log.err) 969 return d 970
971 - def loadDatabase(self, db_spec, db_poll_interval=None):
972 if self.db: 973 return 974 975 # make sure it's up to date 976 sm = DBSchemaManager(db_spec, self.basedir) 977 if not sm.is_current(): 978 raise exceptions.DatabaseNotReadyError, textwrap.dedent(""" 979 The Buildmaster database needs to be upgraded before this version of buildbot 980 can run. Use the following command-line 981 buildbot upgrade-master path/to/master 982 to upgrade the database, and try starting the buildmaster again. You may want 983 to make a backup of your buildmaster before doing so. If you are using MySQL, 984 you must specify the connector string on the upgrade-master command line: 985 buildbot upgrade-master --db=<db-connector-string> path/to/master 986 """) 987 988 self.db = connector.DBConnector(db_spec) 989 if self.changeCacheSize: 990 self.db.setChangeCacheSize(self.changeCacheSize) 991 self.db.start() 992 993 self.botmaster.db = self.db 994 self.status.setDB(self.db) 995 996 self.db.subscribe_to("add-buildrequest", 997 self.botmaster.trigger_add_buildrequest) 998 999 sm = SchedulerManager(self, self.db, self.change_svc) 1000 self.db.subscribe_to("add-change", sm.trigger_add_change) 1001 self.db.subscribe_to("modify-buildset", sm.trigger_modify_buildset) 1002 1003 self.scheduler_manager = sm 1004 sm.setServiceParent(self) 1005 1006 # Set db_poll_interval (perhaps to 30 seconds) if you are using 1007 # multiple buildmasters that share a common database, such that the 1008 # masters need to discover what each other is doing by polling the 1009 # database. TODO: this will be replaced by the DBNotificationServer. 1010 if db_poll_interval: 1011 # it'd be nice if TimerService let us set now=False 1012 t1 = TimerService(db_poll_interval, sm.trigger) 1013 t1.setServiceParent(self) 1014 t2 = TimerService(db_poll_interval, self.botmaster.loop.trigger) 1015 t2.setServiceParent(self)
1016 # adding schedulers (like when loadConfig happens) will trigger the 1017 # scheduler loop at least once, which we need to jump-start things 1018 # like Periodic. 1019
1020 - def loadConfig_Database(self, db_url, db_poll_interval):
1021 self.db_url = db_url 1022 self.db_poll_interval = db_poll_interval 1023 db_spec = DBSpec.from_url(db_url, self.basedir) 1024 self.loadDatabase(db_spec, db_poll_interval)
1025
1026 - def loadConfig_Slaves(self, new_slaves):
1027 # set up the Checker with the names and passwords of all valid slaves 1028 self.checker.users = {} # violates abstraction, oh well 1029 for s in new_slaves: 1030 self.checker.addUser(s.slavename, s.password) 1031 self.checker.addUser("change", "changepw") 1032 # let the BotMaster take care of the rest 1033 return self.botmaster.loadConfig_Slaves(new_slaves)
1034
1035 - def loadConfig_Sources(self, sources):
1036 if not sources: 1037 log.msg("warning: no ChangeSources specified in c['change_source']") 1038 # shut down any that were removed, start any that were added 1039 deleted_sources = [s for s in self.change_svc if s not in sources] 1040 added_sources = [s for s in sources if s not in self.change_svc] 1041 log.msg("adding %d new changesources, removing %d" % 1042 (len(added_sources), len(deleted_sources))) 1043 dl = [self.change_svc.removeSource(s) for s in deleted_sources] 1044 def addNewOnes(res): 1045 [self.change_svc.addSource(s) for s in added_sources]
1046 d = defer.DeferredList(dl, fireOnOneErrback=1, consumeErrors=0) 1047 d.addCallback(addNewOnes) 1048 return d 1049
1050 - def allSchedulers(self):
1051 return list(self.scheduler_manager)
1052
1053 - def loadConfig_Builders(self, newBuilderData):
1054 somethingChanged = False 1055 newList = {} 1056 newBuilderNames = [] 1057 allBuilders = self.botmaster.builders.copy() 1058 for data in newBuilderData: 1059 name = data['name'] 1060 newList[name] = data 1061 newBuilderNames.append(name) 1062 1063 # identify all that were removed 1064 for oldname in self.botmaster.getBuildernames(): 1065 if oldname not in newList: 1066 log.msg("removing old builder %s" % oldname) 1067 del allBuilders[oldname] 1068 somethingChanged = True 1069 # announce the change 1070 self.status.builderRemoved(oldname) 1071 1072 # everything in newList is either unchanged, changed, or new 1073 for name, data in newList.items(): 1074 old = self.botmaster.builders.get(name) 1075 basedir = data['builddir'] 1076 #name, slave, builddir, factory = data 1077 if not old: # new 1078 # category added after 0.6.2 1079 category = data.get('category', None) 1080 log.msg("adding new builder %s for category %s" % 1081 (name, category)) 1082 statusbag = self.status.builderAdded(name, basedir, category) 1083 builder = Builder(data, statusbag) 1084 allBuilders[name] = builder 1085 somethingChanged = True 1086 elif old.compareToSetup(data): 1087 # changed: try to minimize the disruption and only modify the 1088 # pieces that really changed 1089 diffs = old.compareToSetup(data) 1090 log.msg("updating builder %s: %s" % (name, "\n".join(diffs))) 1091 1092 statusbag = old.builder_status 1093 statusbag.saveYourself() # seems like a good idea 1094 # TODO: if the basedir was changed, we probably need to make 1095 # a new statusbag 1096 new_builder = Builder(data, statusbag) 1097 new_builder.consumeTheSoulOfYourPredecessor(old) 1098 # that migrates any retained slavebuilders too 1099 1100 # point out that the builder was updated. On the Waterfall, 1101 # this will appear just after any currently-running builds. 1102 statusbag.addPointEvent(["config", "updated"]) 1103 1104 allBuilders[name] = new_builder 1105 somethingChanged = True 1106 else: 1107 # unchanged: leave it alone 1108 log.msg("builder %s is unchanged" % name) 1109 pass 1110 1111 # regardless of whether anything changed, get each builder status 1112 # to update its config 1113 for builder in allBuilders.values(): 1114 builder.builder_status.reconfigFromBuildmaster(self) 1115 1116 # and then tell the botmaster if anything's changed 1117 if somethingChanged: 1118 sortedAllBuilders = [allBuilders[name] for name in newBuilderNames] 1119 d = self.botmaster.setBuilders(sortedAllBuilders) 1120 return d 1121 return None
1122
1123 - def loadConfig_status(self, status):
1124 dl = [] 1125 1126 # remove old ones 1127 for s in self.statusTargets[:]: 1128 if not s in status: 1129 log.msg("removing IStatusReceiver", s) 1130 d = defer.maybeDeferred(s.disownServiceParent) 1131 dl.append(d) 1132 self.statusTargets.remove(s) 1133 # after those are finished going away, add new ones 1134 def addNewOnes(res): 1135 for s in status: 1136 if not s in self.statusTargets: 1137 log.msg("adding IStatusReceiver", s) 1138 s.setServiceParent(self) 1139 self.statusTargets.append(s)
1140 d = defer.DeferredList(dl, fireOnOneErrback=1) 1141 d.addCallback(addNewOnes) 1142 return d 1143 1144
1145 - def addChange(self, change):
1146 self.db.addChangeToDatabase(change) 1147 self.status.changeAdded(change)
1148
1149 - def triggerSlaveManager(self):
1150 self.botmaster.triggerNewBuildCheck()
1151
1152 - def submitBuildSet(self, builderNames, ss, reason, props=None, now=False):
1153 # determine the set of Builders to use 1154 for name in builderNames: 1155 b = self.botmaster.builders.get(name) 1156 if not b: 1157 raise KeyError("no such builder named '%s'" % name) 1158 if now and not b.slaves: 1159 raise interfaces.NoSlaveError 1160 if props is None: 1161 props = Properties() 1162 bsid = self.db.runInteractionNow(self._txn_submitBuildSet, 1163 builderNames, ss, reason, props) 1164 return BuildSetStatus(bsid, self.status, self.db)
1165
1166 - def _txn_submitBuildSet(self, t, builderNames, ss, reason, props):
1167 ssid = self.db.get_sourcestampid(ss, t) 1168 bsid = self.db.create_buildset(ssid, reason, props, builderNames, t) 1169 return bsid
1170 1171
1172 -class Control:
1173 implements(interfaces.IControl) 1174
1175 - def __init__(self, master):
1176 self.master = master
1177
1178 - def addChange(self, change):
1179 self.master.addChange(change)
1180
1181 - def submitBuildSet(self, builderNames, ss, reason, props=None, now=False):
1182 return self.master.submitBuildSet(builderNames, ss, reason, props, now)
1183
1184 - def getBuilder(self, name):
1185 b = self.master.botmaster.builders[name] 1186 return BuilderControl(b, self)
1187 1188 components.registerAdapter(Control, BuildMaster, interfaces.IControl) 1189