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, slavename):
317 return self.slaves[slavename]
318
319 - def shutdownSlaves(self):
320 # TODO: make this into a bot method rather than a builder method 321 for b in self.slaves.values(): 322 b.shutdownSlave()
323
324 - def stopService(self):
325 for b in self.builders.values(): 326 b.builder_status.addPointEvent(["master", "shutdown"]) 327 b.builder_status.saveYourself() 328 return service.MultiService.stopService(self)
329
330 - def getLockByID(self, lockid):
331 """Convert a Lock identifier into an actual Lock instance. 332 @param lockid: a locks.MasterLock or locks.SlaveLock instance 333 @return: a locks.RealMasterLock or locks.RealSlaveLock instance 334 """ 335 assert isinstance(lockid, (locks.MasterLock, locks.SlaveLock)) 336 if not lockid in self.locks: 337 self.locks[lockid] = lockid.lockClass(lockid) 338 # if the master.cfg file has changed maxCount= on the lock, the next 339 # time a build is started, they'll get a new RealLock instance. Note 340 # that this requires that MasterLock and SlaveLock (marker) instances 341 # be hashable and that they should compare properly. 342 return self.locks[lockid]
343 344 ######################################## 345 346 347
348 -class DebugPerspective(NewCredPerspective):
349 - def attached(self, mind):
350 return self
351 - def detached(self, mind):
352 pass
353
354 - def perspective_pingBuilder(self, buildername):
355 c = interfaces.IControl(self.master) 356 bc = c.getBuilder(buildername) 357 bc.ping()
358
359 - def perspective_fakeChange(self, file, revision=None, who="fakeUser", 360 branch=None, repository="", 361 project=""):
362 change = Change(who, [file], "some fake comments\n", 363 branch=branch, revision=revision, 364 repository=repository, project=project) 365 c = interfaces.IControl(self.master) 366 c.addChange(change)
367
368 - def perspective_setCurrentState(self, buildername, state):
369 builder = self.botmaster.builders.get(buildername) 370 if not builder: return 371 if state == "offline": 372 builder.statusbag.currentlyOffline() 373 if state == "idle": 374 builder.statusbag.currentlyIdle() 375 if state == "waiting": 376 builder.statusbag.currentlyWaiting(now()+10) 377 if state == "building": 378 builder.statusbag.currentlyBuilding(None)
379 - def perspective_reload(self):
380 print "doing reload of the config file" 381 self.master.loadTheConfigFile()
382 - def perspective_pokeIRC(self):
383 print "saying something on IRC" 384 from buildbot.status import words 385 for s in self.master: 386 if isinstance(s, words.IRC): 387 bot = s.f 388 for channel in bot.channels: 389 print " channel", channel 390 bot.p.msg(channel, "Ow, quit it")
391
392 - def perspective_print(self, msg):
393 print "debug", msg
394
395 -class Dispatcher:
396 implements(portal.IRealm) 397
398 - def __init__(self):
399 self.names = {}
400
401 - def register(self, name, afactory):
402 self.names[name] = afactory
403 - def unregister(self, name):
404 del self.names[name]
405
406 - def requestAvatar(self, avatarID, mind, interface):
407 assert interface == pb.IPerspective 408 afactory = self.names.get(avatarID) 409 if afactory: 410 p = afactory.getPerspective() 411 elif avatarID == "change": 412 raise ValueError("no PBChangeSource installed") 413 elif avatarID == "debug": 414 p = DebugPerspective() 415 p.master = self.master 416 p.botmaster = self.botmaster 417 elif avatarID == "statusClient": 418 p = self.statusClientService.getPerspective() 419 else: 420 # it must be one of the buildslaves: no other names will make it 421 # past the checker 422 p = self.botmaster.getPerspective(avatarID) 423 424 if not p: 425 raise ValueError("no perspective for '%s'" % avatarID) 426 427 d = defer.maybeDeferred(p.attached, mind) 428 d.addCallback(self._avatarAttached, mind) 429 return d
430
431 - def _avatarAttached(self, p, mind):
432 return (pb.IPerspective, p, lambda p=p,mind=mind: p.detached(mind))
433 434 ######################################## 435
436 -class _Unset: pass # marker
437
438 -class LogRotation:
439 '''holds log rotation parameters (for WebStatus)'''
440 - def __init__(self):
441 self.rotateLength = 1 * 1000 * 1000 442 self.maxRotatedFiles = 10
443
444 -class BuildMaster(service.MultiService):
445 debug = 0 446 manhole = None 447 debugPassword = None 448 projectName = "(unspecified)" 449 projectURL = None 450 buildbotURL = None 451 change_svc = None 452 properties = Properties() 453
454 - def __init__(self, basedir, configFileName="master.cfg", db_spec=None):
455 service.MultiService.__init__(self) 456 self.setName("buildmaster") 457 self.basedir = basedir 458 self.configFileName = configFileName 459 460 # the dispatcher is the realm in which all inbound connections are 461 # looked up: slave builders, change notifications, status clients, and 462 # the debug port 463 dispatcher = Dispatcher() 464 dispatcher.master = self 465 self.dispatcher = dispatcher 466 self.checker = checkers.InMemoryUsernamePasswordDatabaseDontUse() 467 # the checker starts with no user/passwd pairs: they are added later 468 p = portal.Portal(dispatcher) 469 p.registerChecker(self.checker) 470 self.slaveFactory = pb.PBServerFactory(p) 471 self.slaveFactory.unsafeTracebacks = True # let them see exceptions 472 473 self.slavePortnum = None 474 self.slavePort = None 475 476 self.change_svc = ChangeManager() 477 self.change_svc.setServiceParent(self) 478 self.dispatcher.changemaster = self.change_svc 479 480 try: 481 hostname = os.uname()[1] # only on unix 482 except AttributeError: 483 hostname = "?" 484 self.master_name = "%s:%s" % (hostname, os.path.abspath(self.basedir)) 485 self.master_incarnation = "pid%d-boot%d" % (os.getpid(), time.time()) 486 487 self.botmaster = BotMaster() 488 self.botmaster.setName("botmaster") 489 self.botmaster.setMasterName(self.master_name, self.master_incarnation) 490 self.botmaster.setServiceParent(self) 491 self.dispatcher.botmaster = self.botmaster 492 493 self.status = Status(self.botmaster, self.basedir) 494 self.statusTargets = [] 495 496 self.db = None 497 self.db_url = None 498 self.db_poll_interval = _Unset 499 if db_spec: 500 self.loadDatabase(db_spec) 501 502 self.readConfig = False 503 504 # create log_rotation object and set default parameters (used by WebStatus) 505 self.log_rotation = LogRotation()
506
507 - def startService(self):
508 service.MultiService.startService(self) 509 if not self.readConfig: 510 # TODO: consider catching exceptions during this call to 511 # loadTheConfigFile and bailing (reactor.stop) if it fails, 512 # since without a config file we can't do anything except reload 513 # the config file, and it would be nice for the user to discover 514 # this quickly. 515 self.loadTheConfigFile() 516 if hasattr(signal, "SIGHUP"): 517 signal.signal(signal.SIGHUP, self._handleSIGHUP) 518 for b in self.botmaster.builders.values(): 519 b.builder_status.addPointEvent(["master", "started"]) 520 b.builder_status.saveYourself()
521
522 - def _handleSIGHUP(self, *args):
523 reactor.callLater(0, self.loadTheConfigFile)
524
525 - def getStatus(self):
526 """ 527 @rtype: L{buildbot.status.builder.Status} 528 """ 529 return self.status
530
531 - def loadTheConfigFile(self, configFile=None):
532 if not configFile: 533 configFile = os.path.join(self.basedir, self.configFileName) 534 535 log.msg("Creating BuildMaster -- buildbot.version: %s" % buildbot.version) 536 log.msg("loading configuration from %s" % configFile) 537 configFile = os.path.expanduser(configFile) 538 539 try: 540 f = open(configFile, "r") 541 except IOError, e: 542 log.msg("unable to open config file '%s'" % configFile) 543 log.msg("leaving old configuration in place") 544 log.err(e) 545 return 546 547 try: 548 d = self.loadConfig(f) 549 except: 550 log.msg("error during loadConfig") 551 log.err() 552 log.msg("The new config file is unusable, so I'll ignore it.") 553 log.msg("I will keep using the previous config file instead.") 554 return # sorry unit tests 555 f.close() 556 return d # for unit tests
557
558 - def loadConfig(self, f, check_synchronously_only=False):
559 """Internal function to load a specific configuration file. Any 560 errors in the file will be signalled by raising an exception. 561 562 If check_synchronously_only=True, I will return (with None) 563 synchronously, after checking the config file for sanity, or raise an 564 exception. I may also emit some DeprecationWarnings. 565 566 If check_synchronously_only=False, I will return a Deferred that 567 fires (with None) when the configuration changes have been completed. 568 This may involve a round-trip to each buildslave that was involved.""" 569 570 localDict = {'basedir': os.path.expanduser(self.basedir)} 571 try: 572 exec f in localDict 573 except: 574 log.msg("error while parsing config file") 575 raise 576 577 try: 578 config = localDict['BuildmasterConfig'] 579 except KeyError: 580 log.err("missing config dictionary") 581 log.err("config file must define BuildmasterConfig") 582 raise 583 584 known_keys = ("slaves", "change_source", 585 "schedulers", "builders", "mergeRequests", 586 "slavePortnum", "debugPassword", "logCompressionLimit", 587 "manhole", "status", "projectName", "projectURL", 588 "buildbotURL", "properties", "prioritizeBuilders", 589 "eventHorizon", "buildCacheSize", "logHorizon", "buildHorizon", 590 "changeHorizon", "logMaxSize", "logMaxTailSize", 591 "logCompressionMethod", "db_url", "multiMaster", 592 "db_poll_interval", 593 ) 594 for k in config.keys(): 595 if k not in known_keys: 596 log.msg("unknown key '%s' defined in config dictionary" % k) 597 598 try: 599 # required 600 schedulers = config['schedulers'] 601 builders = config['builders'] 602 slavePortnum = config['slavePortnum'] 603 #slaves = config['slaves'] 604 #change_source = config['change_source'] 605 606 # optional 607 db_url = config.get("db_url", "sqlite:///state.sqlite") 608 db_poll_interval = config.get("db_poll_interval", None) 609 debugPassword = config.get('debugPassword') 610 manhole = config.get('manhole') 611 status = config.get('status', []) 612 projectName = config.get('projectName') 613 projectURL = config.get('projectURL') 614 buildbotURL = config.get('buildbotURL') 615 properties = config.get('properties', {}) 616 buildCacheSize = config.get('buildCacheSize', None) 617 eventHorizon = config.get('eventHorizon', 50) 618 logHorizon = config.get('logHorizon', None) 619 buildHorizon = config.get('buildHorizon', None) 620 logCompressionLimit = config.get('logCompressionLimit', 4*1024) 621 if logCompressionLimit is not None and not \ 622 isinstance(logCompressionLimit, int): 623 raise ValueError("logCompressionLimit needs to be bool or int") 624 logCompressionMethod = config.get('logCompressionMethod', "bz2") 625 if logCompressionMethod not in ('bz2', 'gz'): 626 raise ValueError("logCompressionMethod needs to be 'bz2', or 'gz'") 627 logMaxSize = config.get('logMaxSize') 628 if logMaxSize is not None and not \ 629 isinstance(logMaxSize, int): 630 raise ValueError("logMaxSize needs to be None or int") 631 logMaxTailSize = config.get('logMaxTailSize') 632 if logMaxTailSize is not None and not \ 633 isinstance(logMaxTailSize, int): 634 raise ValueError("logMaxTailSize needs to be None or int") 635 mergeRequests = config.get('mergeRequests') 636 if mergeRequests is not None and not callable(mergeRequests): 637 raise ValueError("mergeRequests must be a callable") 638 prioritizeBuilders = config.get('prioritizeBuilders') 639 if prioritizeBuilders is not None and not callable(prioritizeBuilders): 640 raise ValueError("prioritizeBuilders must be callable") 641 changeHorizon = config.get("changeHorizon") 642 if changeHorizon is not None and not isinstance(changeHorizon, int): 643 raise ValueError("changeHorizon needs to be an int") 644 645 multiMaster = config.get("multiMaster", False) 646 647 except KeyError: 648 log.msg("config dictionary is missing a required parameter") 649 log.msg("leaving old configuration in place") 650 raise 651 652 if "sources" in config: 653 m = ("c['sources'] is deprecated as of 0.7.6 and is no longer " 654 "accepted in >= 0.8.0 . Please use c['change_source'] instead.") 655 raise KeyError(m) 656 657 if "bots" in config: 658 m = ("c['bots'] is deprecated as of 0.7.6 and is no longer " 659 "accepted in >= 0.8.0 . Please use c['slaves'] instead.") 660 raise KeyError(m) 661 662 slaves = config.get('slaves', []) 663 if "slaves" not in config: 664 log.msg("config dictionary must have a 'slaves' key") 665 log.msg("leaving old configuration in place") 666 raise KeyError("must have a 'slaves' key") 667 668 if changeHorizon is not None: 669 self.change_svc.changeHorizon = changeHorizon 670 671 change_source = config.get('change_source', []) 672 if isinstance(change_source, (list, tuple)): 673 change_sources = change_source 674 else: 675 change_sources = [change_source] 676 677 # do some validation first 678 for s in slaves: 679 assert interfaces.IBuildSlave.providedBy(s) 680 if s.slavename in ("debug", "change", "status"): 681 raise KeyError( 682 "reserved name '%s' used for a bot" % s.slavename) 683 if config.has_key('interlocks'): 684 raise KeyError("c['interlocks'] is no longer accepted") 685 assert self.db_url is None or db_url == self.db_url, \ 686 "Cannot change db_url after master has started" 687 assert db_poll_interval is None or isinstance(db_poll_interval, int), \ 688 "db_poll_interval must be an integer: seconds between polls" 689 assert self.db_poll_interval is _Unset or db_poll_interval == self.db_poll_interval, \ 690 "Cannot change db_poll_interval after master has started" 691 692 assert isinstance(change_sources, (list, tuple)) 693 for s in change_sources: 694 assert interfaces.IChangeSource(s, None) 695 # this assertion catches c['schedulers'] = Scheduler(), since 696 # Schedulers are service.MultiServices and thus iterable. 697 errmsg = "c['schedulers'] must be a list of Scheduler instances" 698 assert isinstance(schedulers, (list, tuple)), errmsg 699 for s in schedulers: 700 assert interfaces.IScheduler(s, None), errmsg 701 assert isinstance(status, (list, tuple)) 702 for s in status: 703 assert interfaces.IStatusReceiver(s, None) 704 705 slavenames = [s.slavename for s in slaves] 706 buildernames = [] 707 dirnames = [] 708 709 # convert builders from objects to config dictionaries 710 builders_dicts = [] 711 for b in builders: 712 if isinstance(b, BuilderConfig): 713 builders_dicts.append(b.getConfigDict()) 714 elif type(b) is dict: 715 builders_dicts.append(b) 716 else: 717 raise ValueError("builder %s is not a BuilderConfig object (or a dict)" % b) 718 builders = builders_dicts 719 720 for b in builders: 721 if b.has_key('slavename') and b['slavename'] not in slavenames: 722 raise ValueError("builder %s uses undefined slave %s" \ 723 % (b['name'], b['slavename'])) 724 for n in b.get('slavenames', []): 725 if n not in slavenames: 726 raise ValueError("builder %s uses undefined slave %s" \ 727 % (b['name'], n)) 728 if b['name'] in buildernames: 729 raise ValueError("duplicate builder name %s" 730 % b['name']) 731 buildernames.append(b['name']) 732 733 # sanity check name (BuilderConfig does this too) 734 if b['name'].startswith("_"): 735 errmsg = ("builder names must not start with an " 736 "underscore: " + b['name']) 737 log.err(errmsg) 738 raise ValueError(errmsg) 739 740 # Fix the dictionnary with default values, in case this wasn't 741 # specified with a BuilderConfig object (which sets the same defaults) 742 b.setdefault('builddir', safeTranslate(b['name'])) 743 b.setdefault('slavebuilddir', b['builddir']) 744 b.setdefault('buildHorizon', buildHorizon) 745 b.setdefault('logHorizon', logHorizon) 746 b.setdefault('eventHorizon', eventHorizon) 747 if b['builddir'] in dirnames: 748 raise ValueError("builder %s reuses builddir %s" 749 % (b['name'], b['builddir'])) 750 dirnames.append(b['builddir']) 751 752 unscheduled_buildernames = buildernames[:] 753 schedulernames = [] 754 for s in schedulers: 755 for b in s.listBuilderNames(): 756 # Skip checks for builders in multimaster mode 757 if not multiMaster: 758 assert b in buildernames, \ 759 "%s uses unknown builder %s" % (s, b) 760 if b in unscheduled_buildernames: 761 unscheduled_buildernames.remove(b) 762 763 if s.name in schedulernames: 764 msg = ("Schedulers must have unique names, but " 765 "'%s' was a duplicate" % (s.name,)) 766 raise ValueError(msg) 767 schedulernames.append(s.name) 768 769 # Skip the checks for builders in multimaster mode 770 if not multiMaster and unscheduled_buildernames: 771 log.msg("Warning: some Builders have no Schedulers to drive them:" 772 " %s" % (unscheduled_buildernames,)) 773 774 # assert that all locks used by the Builds and their Steps are 775 # uniquely named. 776 lock_dict = {} 777 for b in builders: 778 for l in b.get('locks', []): 779 if isinstance(l, locks.LockAccess): # User specified access to the lock 780 l = l.lockid 781 if lock_dict.has_key(l.name): 782 if lock_dict[l.name] is not l: 783 raise ValueError("Two different locks (%s and %s) " 784 "share the name %s" 785 % (l, lock_dict[l.name], l.name)) 786 else: 787 lock_dict[l.name] = l 788 # TODO: this will break with any BuildFactory that doesn't use a 789 # .steps list, but I think the verification step is more 790 # important. 791 for s in b['factory'].steps: 792 for l in s[1].get('locks', []): 793 if isinstance(l, locks.LockAccess): # User specified access to the lock 794 l = l.lockid 795 if lock_dict.has_key(l.name): 796 if lock_dict[l.name] is not l: 797 raise ValueError("Two different locks (%s and %s)" 798 " share the name %s" 799 % (l, lock_dict[l.name], l.name)) 800 else: 801 lock_dict[l.name] = l 802 803 if not isinstance(properties, dict): 804 raise ValueError("c['properties'] must be a dictionary") 805 806 # slavePortnum supposed to be a strports specification 807 if type(slavePortnum) is int: 808 slavePortnum = "tcp:%d" % slavePortnum 809 810 if check_synchronously_only: 811 return 812 # now we're committed to implementing the new configuration, so do 813 # it atomically 814 # TODO: actually, this is spread across a couple of Deferreds, so it 815 # really isn't atomic. 816 817 d = defer.succeed(None) 818 819 self.projectName = projectName 820 self.projectURL = projectURL 821 self.buildbotURL = buildbotURL 822 823 self.properties = Properties() 824 self.properties.update(properties, self.configFileName) 825 826 self.status.logCompressionLimit = logCompressionLimit 827 self.status.logCompressionMethod = logCompressionMethod 828 self.status.logMaxSize = logMaxSize 829 self.status.logMaxTailSize = logMaxTailSize 830 # Update any of our existing builders with the current log parameters. 831 # This is required so that the new value is picked up after a 832 # reconfig. 833 for builder in self.botmaster.builders.values(): 834 builder.builder_status.setLogCompressionLimit(logCompressionLimit) 835 builder.builder_status.setLogCompressionMethod(logCompressionMethod) 836 builder.builder_status.setLogMaxSize(logMaxSize) 837 builder.builder_status.setLogMaxTailSize(logMaxTailSize) 838 839 if mergeRequests is not None: 840 self.botmaster.mergeRequests = mergeRequests 841 if prioritizeBuilders is not None: 842 self.botmaster.prioritizeBuilders = prioritizeBuilders 843 844 self.buildCacheSize = buildCacheSize 845 self.eventHorizon = eventHorizon 846 self.logHorizon = logHorizon 847 self.buildHorizon = buildHorizon 848 849 # Set up the database 850 d.addCallback(lambda res: 851 self.loadConfig_Database(db_url, db_poll_interval)) 852 853 # self.slaves: Disconnect any that were attached and removed from the 854 # list. Update self.checker with the new list of passwords, including 855 # debug/change/status. 856 d.addCallback(lambda res: self.loadConfig_Slaves(slaves)) 857 858 # self.debugPassword 859 if debugPassword: 860 self.checker.addUser("debug", debugPassword) 861 self.debugPassword = debugPassword 862 863 # self.manhole 864 if manhole != self.manhole: 865 # changing 866 if self.manhole: 867 # disownServiceParent may return a Deferred 868 d.addCallback(lambda res: self.manhole.disownServiceParent()) 869 def _remove(res): 870 self.manhole = None 871 return res
872 d.addCallback(_remove) 873 if manhole: 874 def _add(res): 875 self.manhole = manhole 876 manhole.setServiceParent(self)
877 d.addCallback(_add) 878 879 # add/remove self.botmaster.builders to match builders. The 880 # botmaster will handle startup/shutdown issues. 881 d.addCallback(lambda res: self.loadConfig_Builders(builders)) 882 883 d.addCallback(lambda res: self.loadConfig_status(status)) 884 885 # Schedulers are added after Builders in case they start right away 886 d.addCallback(lambda res: 887 self.scheduler_manager.updateSchedulers(schedulers)) 888 # and Sources go after Schedulers for the same reason 889 d.addCallback(lambda res: self.loadConfig_Sources(change_sources)) 890 891 # self.slavePort 892 if self.slavePortnum != slavePortnum: 893 if self.slavePort: 894 def closeSlavePort(res): 895 d1 = self.slavePort.disownServiceParent() 896 self.slavePort = None 897 return d1 898 d.addCallback(closeSlavePort) 899 if slavePortnum is not None: 900 def openSlavePort(res): 901 self.slavePort = strports.service(slavePortnum, 902 self.slaveFactory) 903 self.slavePort.setServiceParent(self) 904 d.addCallback(openSlavePort) 905 log.msg("BuildMaster listening on port %s" % slavePortnum) 906 self.slavePortnum = slavePortnum 907 908 log.msg("configuration update started") 909 def _done(res): 910 self.readConfig = True 911 log.msg("configuration update complete") 912 d.addCallback(_done) 913 d.addCallback(lambda res: self.botmaster.triggerNewBuildCheck()) 914 d.addErrback(log.err) 915 return d 916
917 - def loadDatabase(self, db_spec, db_poll_interval=None):
918 if self.db: 919 return 920 921 # make sure it's up to date 922 sm = DBSchemaManager(db_spec, self.basedir) 923 if not sm.is_current(): 924 raise exceptions.DatabaseNotReadyError, textwrap.dedent(""" 925 The Buildmaster database needs to be upgraded before this version of buildbot 926 can run. Use the following command-line 927 buildbot upgrade-master path/to/master 928 to upgrade the database, and try starting the buildmaster again. You may want 929 to make a backup of your buildmaster before doing so.""") 930 931 self.db = connector.DBConnector(db_spec) 932 self.db.start() 933 934 self.botmaster.db = self.db 935 self.status.setDB(self.db) 936 937 self.db.subscribe_to("add-buildrequest", 938 self.botmaster.trigger_add_buildrequest) 939 940 sm = SchedulerManager(self, self.db, self.change_svc) 941 self.db.subscribe_to("add-change", sm.trigger_add_change) 942 self.db.subscribe_to("modify-buildset", sm.trigger_modify_buildset) 943 944 self.scheduler_manager = sm 945 sm.setServiceParent(self) 946 947 # Set db_poll_interval (perhaps to 30 seconds) if you are using 948 # multiple buildmasters that share a common database, such that the 949 # masters need to discover what each other is doing by polling the 950 # database. TODO: this will be replaced by the DBNotificationServer. 951 if db_poll_interval: 952 # it'd be nice if TimerService let us set now=False 953 t1 = TimerService(db_poll_interval, sm.trigger) 954 t1.setServiceParent(self) 955 t2 = TimerService(db_poll_interval, self.botmaster.loop.trigger) 956 t2.setServiceParent(self)
957 # adding schedulers (like when loadConfig happens) will trigger the 958 # scheduler loop at least once, which we need to jump-start things 959 # like Periodic. 960
961 - def loadConfig_Database(self, db_url, db_poll_interval):
962 self.db_url = db_url 963 self.db_poll_interval = db_poll_interval 964 db_spec = DBSpec.from_url(db_url, self.basedir) 965 self.loadDatabase(db_spec, db_poll_interval)
966
967 - def loadConfig_Slaves(self, new_slaves):
968 # set up the Checker with the names and passwords of all valid slaves 969 self.checker.users = {} # violates abstraction, oh well 970 for s in new_slaves: 971 self.checker.addUser(s.slavename, s.password) 972 self.checker.addUser("change", "changepw") 973 # let the BotMaster take care of the rest 974 return self.botmaster.loadConfig_Slaves(new_slaves)
975
976 - def loadConfig_Sources(self, sources):
977 if not sources: 978 log.msg("warning: no ChangeSources specified in c['change_source']") 979 # shut down any that were removed, start any that were added 980 deleted_sources = [s for s in self.change_svc if s not in sources] 981 added_sources = [s for s in sources if s not in self.change_svc] 982 log.msg("adding %d new changesources, removing %d" % 983 (len(added_sources), len(deleted_sources))) 984 dl = [self.change_svc.removeSource(s) for s in deleted_sources] 985 def addNewOnes(res): 986 [self.change_svc.addSource(s) for s in added_sources]
987 d = defer.DeferredList(dl, fireOnOneErrback=1, consumeErrors=0) 988 d.addCallback(addNewOnes) 989 return d 990
991 - def allSchedulers(self):
992 return list(self.scheduler_manager)
993
994 - def loadConfig_Builders(self, newBuilderData):
995 somethingChanged = False 996 newList = {} 997 newBuilderNames = [] 998 allBuilders = self.botmaster.builders.copy() 999 for data in newBuilderData: 1000 name = data['name'] 1001 newList[name] = data 1002 newBuilderNames.append(name) 1003 1004 # identify all that were removed 1005 for oldname in self.botmaster.getBuildernames(): 1006 if oldname not in newList: 1007 log.msg("removing old builder %s" % oldname) 1008 del allBuilders[oldname] 1009 somethingChanged = True 1010 # announce the change 1011 self.status.builderRemoved(oldname) 1012 1013 # everything in newList is either unchanged, changed, or new 1014 for name, data in newList.items(): 1015 old = self.botmaster.builders.get(name) 1016 basedir = data['builddir'] 1017 #name, slave, builddir, factory = data 1018 if not old: # new 1019 # category added after 0.6.2 1020 category = data.get('category', None) 1021 log.msg("adding new builder %s for category %s" % 1022 (name, category)) 1023 statusbag = self.status.builderAdded(name, basedir, category) 1024 builder = Builder(data, statusbag) 1025 allBuilders[name] = builder 1026 somethingChanged = True 1027 elif old.compareToSetup(data): 1028 # changed: try to minimize the disruption and only modify the 1029 # pieces that really changed 1030 diffs = old.compareToSetup(data) 1031 log.msg("updating builder %s: %s" % (name, "\n".join(diffs))) 1032 1033 statusbag = old.builder_status 1034 statusbag.saveYourself() # seems like a good idea 1035 # TODO: if the basedir was changed, we probably need to make 1036 # a new statusbag 1037 new_builder = Builder(data, statusbag) 1038 new_builder.consumeTheSoulOfYourPredecessor(old) 1039 # that migrates any retained slavebuilders too 1040 1041 # point out that the builder was updated. On the Waterfall, 1042 # this will appear just after any currently-running builds. 1043 statusbag.addPointEvent(["config", "updated"]) 1044 1045 allBuilders[name] = new_builder 1046 somethingChanged = True 1047 else: 1048 # unchanged: leave it alone 1049 log.msg("builder %s is unchanged" % name) 1050 pass 1051 1052 # regardless of whether anything changed, get each builder status 1053 # to update its config 1054 for builder in allBuilders.values(): 1055 builder.builder_status.reconfigFromBuildmaster(self) 1056 1057 # and then tell the botmaster if anything's changed 1058 if somethingChanged: 1059 sortedAllBuilders = [allBuilders[name] for name in newBuilderNames] 1060 d = self.botmaster.setBuilders(sortedAllBuilders) 1061 return d 1062 return None
1063
1064 - def loadConfig_status(self, status):
1065 dl = [] 1066 1067 # remove old ones 1068 for s in self.statusTargets[:]: 1069 if not s in status: 1070 log.msg("removing IStatusReceiver", s) 1071 d = defer.maybeDeferred(s.disownServiceParent) 1072 dl.append(d) 1073 self.statusTargets.remove(s) 1074 # after those are finished going away, add new ones 1075 def addNewOnes(res): 1076 for s in status: 1077 if not s in self.statusTargets: 1078 log.msg("adding IStatusReceiver", s) 1079 s.setServiceParent(self) 1080 self.statusTargets.append(s)
1081 d = defer.DeferredList(dl, fireOnOneErrback=1) 1082 d.addCallback(addNewOnes) 1083 return d 1084 1085
1086 - def addChange(self, change):
1087 self.db.addChangeToDatabase(change) 1088 self.status.changeAdded(change)
1089
1090 - def triggerSlaveManager(self):
1091 self.botmaster.triggerNewBuildCheck()
1092
1093 - def submitBuildSet(self, builderNames, ss, reason, props=None, now=False):
1094 # determine the set of Builders to use 1095 for name in builderNames: 1096 b = self.botmaster.builders.get(name) 1097 if not b: 1098 raise KeyError("no such builder named '%s'" % name) 1099 if now and not b.slaves: 1100 raise interfaces.NoSlaveError 1101 if props is None: 1102 props = Properties() 1103 bsid = self.db.runInteractionNow(self._txn_submitBuildSet, 1104 builderNames, ss, reason, props) 1105 return BuildSetStatus(bsid, self.status, self.db)
1106
1107 - def _txn_submitBuildSet(self, t, builderNames, ss, reason, props):
1108 ssid = self.db.get_sourcestampid(ss, t) 1109 bsid = self.db.create_buildset(ssid, reason, props, builderNames, t) 1110 return bsid
1111 1112
1113 -class Control:
1114 implements(interfaces.IControl) 1115
1116 - def __init__(self, master):
1117 self.master = master
1118
1119 - def addChange(self, change):
1120 self.master.addChange(change)
1121
1122 - def submitBuildSet(self, builderNames, ss, reason, props=None, now=False):
1123 return self.master.submitBuildSet(builderNames, ss, reason, props, now)
1124
1125 - def getBuilder(self, name):
1126 b = self.master.botmaster.builders[name] 1127 return BuilderControl(b, self)
1128 1129 components.registerAdapter(Control, BuildMaster, interfaces.IControl) 1130