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