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