1 # -*- test-case-name: buildbot.test.test_run -*-
9 from cPickle
import load
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
23 from buildbot
.util
import now
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
34 ########################################
36 class BotMaster(service
.MultiService
):
38 """This is the master-side service which manages remote buildbot slaves.
39 It provides them with BuildSlaves, and distributes file change
40 notification messages to them.
46 service
.MultiService
.__init
__(self
)
48 self
.builderNames
= []
49 # builders maps Builder names to instances of bb.p.builder.Builder,
50 # which is the master-side object that defines and controls a build.
51 # They are added by calling botmaster.addBuilder() from the startup
54 # self.slaves contains a ready BuildSlave instance for each
55 # potential buildslave, i.e. all the ones listed in the config file.
56 # If the slave is connected, self.slaves[slavename].slave will
57 # contain a RemoteReference to their Bot instance. If it is not
58 # connected, that attribute will hold None.
59 self
.slaves
= {} # maps slavename to BuildSlave
60 self
.statusClientService
= None
63 # self.locks holds the real Lock instances
66 # self.mergeRequests is the callable override for merging build
68 self
.mergeRequests
= None
70 # self.prioritizeBuilders is the callable override for builder order
72 self
.prioritizeBuilders
= None
74 # these four are convenience functions for testing
76 def waitUntilBuilderAttached(self
, name
):
77 b
= self
.builders
[name
]
79 # return defer.succeed(None)
81 b
.watchers
['attach'].append(d
)
84 def waitUntilBuilderDetached(self
, name
):
85 b
= self
.builders
.get(name
)
86 if not b
or not b
.slaves
:
87 return defer
.succeed(None)
89 b
.watchers
['detach'].append(d
)
92 def waitUntilBuilderFullyDetached(self
, name
):
93 b
= self
.builders
.get(name
)
94 # TODO: this looks too deeply inside the Builder object
95 if not b
or not b
.slaves
:
96 return defer
.succeed(None)
98 b
.watchers
['detach_all'].append(d
)
101 def waitUntilBuilderIdle(self
, name
):
102 b
= self
.builders
[name
]
103 # TODO: this looks way too deeply inside the Builder object
107 b
.watchers
['idle'].append(d
)
109 return defer
.succeed(None)
111 def loadConfig_Slaves(self
, new_slaves
):
112 old_slaves
= [c
for c
in list(self
)
113 if interfaces
.IBuildSlave
.providedBy(c
)]
115 # identify added/removed slaves. For each slave we construct a tuple
116 # of (name, password, class), and we consider the slave to be already
117 # present if the tuples match. (we include the class to make sure
118 # that BuildSlave(name,pw) is different than
119 # SubclassOfBuildSlave(name,pw) ). If the password or class has
120 # changed, we will remove the old version of the slave and replace it
121 # with a new one. If anything else has changed, we just update the
122 # old BuildSlave instance in place. If the name has changed, of
123 # course, it looks exactly the same as deleting one slave and adding
127 old_t
[(s
.slavename
, s
.password
, s
.__class
__)] = s
130 new_t
[(s
.slavename
, s
.password
, s
.__class
__)] = s
140 # removeSlave will hang up on the old bot
143 dl
.append(self
.removeSlave(s
))
144 d
= defer
.DeferredList(dl
, fireOnOneErrback
=True)
148 for t
in remaining_t
:
149 old_t
[t
].update(new_t
[t
])
153 def addSlave(self
, s
):
154 s
.setServiceParent(self
)
156 self
.slaves
[s
.slavename
] = s
158 def removeSlave(self
, s
):
159 # TODO: technically, disownServiceParent could return a Deferred
160 s
.disownServiceParent()
161 d
= self
.slaves
[s
.slavename
].disconnect()
162 del self
.slaves
[s
.slavename
]
165 def slaveLost(self
, bot
):
166 for name
, b
in self
.builders
.items():
167 if bot
.slavename
in b
.slavenames
:
170 def getBuildersForSlave(self
, slavename
):
172 for b
in self
.builders
.values()
173 if slavename
in b
.slavenames
]
175 def getBuildernames(self
):
176 return self
.builderNames
178 def getBuilders(self
):
179 allBuilders
= [self
.builders
[name
] for name
in self
.builderNames
]
182 def setBuilders(self
, builders
):
184 self
.builderNames
= []
186 for slavename
in b
.slavenames
:
187 # this is actually validated earlier
188 assert slavename
in self
.slaves
189 self
.builders
[b
.name
] = b
190 self
.builderNames
.append(b
.name
)
192 d
= self
._updateAllSlaves
()
195 def _updateAllSlaves(self
):
196 """Notify all buildslaves about changes in their Builders."""
197 dl
= [s
.updateSlave() for s
in self
.slaves
.values()]
198 return defer
.DeferredList(dl
)
200 def maybeStartAllBuilds(self
):
201 builders
= self
.builders
.values()
202 if self
.prioritizeBuilders
is not None:
204 builders
= self
.prioritizeBuilders(self
.parent
, builders
)
206 log
.msg("Exception prioritizing builders")
210 def _sortfunc(b1
, b2
):
211 t1
= b1
.getOldestRequestTime()
212 t2
= b2
.getOldestRequestTime()
213 # If t1 or t2 is None, then there are no build requests,
214 # so sort it at the end
220 builders
.sort(cmp=_sortfunc
)
225 log
.msg("Exception starting builds")
228 def shouldMergeRequests(self
, builder
, req1
, req2
):
229 """Determine whether two BuildRequests should be merged for
233 if self
.mergeRequests
is not None:
234 return self
.mergeRequests(builder
, req1
, req2
)
235 return req1
.canBeMergedWith(req2
)
237 def getPerspective(self
, slavename
):
238 return self
.slaves
[slavename
]
240 def shutdownSlaves(self
):
241 # TODO: make this into a bot method rather than a builder method
242 for b
in self
.slaves
.values():
245 def stopService(self
):
246 for b
in self
.builders
.values():
247 b
.builder_status
.addPointEvent(["master", "shutdown"])
248 b
.builder_status
.saveYourself()
249 return service
.Service
.stopService(self
)
251 def getLockByID(self
, lockid
):
252 """Convert a Lock identifier into an actual Lock instance.
253 @param lockid: a locks.MasterLock or locks.SlaveLock instance
254 @return: a locks.RealMasterLock or locks.RealSlaveLock instance
256 assert isinstance(lockid
, (locks
.MasterLock
, locks
.SlaveLock
))
257 if not lockid
in self
.locks
:
258 self
.locks
[lockid
] = lockid
.lockClass(lockid
)
259 # if the master.cfg file has changed maxCount= on the lock, the next
260 # time a build is started, they'll get a new RealLock instance. Note
261 # that this requires that MasterLock and SlaveLock (marker) instances
262 # be hashable and that they should compare properly.
263 return self
.locks
[lockid
]
265 ########################################
269 class DebugPerspective(NewCredPerspective
):
270 def attached(self
, mind
):
272 def detached(self
, mind
):
275 def perspective_requestBuild(self
, buildername
, reason
, branch
, revision
, properties
={}):
276 c
= interfaces
.IControl(self
.master
)
277 bc
= c
.getBuilder(buildername
)
278 ss
= SourceStamp(branch
, revision
)
280 bpr
.update(properties
, "remote requestBuild")
281 br
= BuildRequest(reason
, ss
, builderName
=buildername
, properties
=bpr
)
284 def perspective_pingBuilder(self
, buildername
):
285 c
= interfaces
.IControl(self
.master
)
286 bc
= c
.getBuilder(buildername
)
289 def perspective_fakeChange(self
, file, revision
=None, who
="fakeUser",
291 change
= Change(who
, [file], "some fake comments\n",
292 branch
=branch
, revision
=revision
)
293 c
= interfaces
.IControl(self
.master
)
296 def perspective_setCurrentState(self
, buildername
, state
):
297 builder
= self
.botmaster
.builders
.get(buildername
)
298 if not builder
: return
299 if state
== "offline":
300 builder
.statusbag
.currentlyOffline()
302 builder
.statusbag
.currentlyIdle()
303 if state
== "waiting":
304 builder
.statusbag
.currentlyWaiting(now()+10)
305 if state
== "building":
306 builder
.statusbag
.currentlyBuilding(None)
307 def perspective_reload(self
):
308 print "doing reload of the config file"
309 self
.master
.loadTheConfigFile()
310 def perspective_pokeIRC(self
):
311 print "saying something on IRC"
312 from buildbot
.status
import words
313 for s
in self
.master
:
314 if isinstance(s
, words
.IRC
):
316 for channel
in bot
.channels
:
317 print " channel", channel
318 bot
.p
.msg(channel
, "Ow, quit it")
320 def perspective_print(self
, msg
):
323 class Dispatcher(styles
.Versioned
):
324 implements(portal
.IRealm
)
325 persistenceVersion
= 2
330 def upgradeToVersion1(self
):
331 self
.master
= self
.botmaster
.parent
332 def upgradeToVersion2(self
):
335 def register(self
, name
, afactory
):
336 self
.names
[name
] = afactory
337 def unregister(self
, name
):
340 def requestAvatar(self
, avatarID
, mind
, interface
):
341 assert interface
== pb
.IPerspective
342 afactory
= self
.names
.get(avatarID
)
344 p
= afactory
.getPerspective()
345 elif avatarID
== "debug":
346 p
= DebugPerspective()
347 p
.master
= self
.master
348 p
.botmaster
= self
.botmaster
349 elif avatarID
== "statusClient":
350 p
= self
.statusClientService
.getPerspective()
352 # it must be one of the buildslaves: no other names will make it
354 p
= self
.botmaster
.getPerspective(avatarID
)
357 raise ValueError("no perspective for '%s'" % avatarID
)
359 d
= defer
.maybeDeferred(p
.attached
, mind
)
360 d
.addCallback(self
._avatarAttached
, mind
)
363 def _avatarAttached(self
, p
, mind
):
364 return (pb
.IPerspective
, p
, lambda p
=p
,mind
=mind
: p
.detached(mind
))
366 ########################################
372 # all IChangeSource objects
373 # StatusClientService
374 # TCPClient(self.ircFactory)
375 # TCPServer(self.slaveFactory) -> dispatcher.requestAvatar
376 # TCPServer(self.site)
377 # UNIXServer(ResourcePublisher(self.site))
380 class BuildMaster(service
.MultiService
, styles
.Versioned
):
382 persistenceVersion
= 3
385 projectName
= "(unspecified)"
389 properties
= Properties()
391 def __init__(self
, basedir
, configFileName
="master.cfg"):
392 service
.MultiService
.__init
__(self
)
393 self
.setName("buildmaster")
394 self
.basedir
= basedir
395 self
.configFileName
= configFileName
397 # the dispatcher is the realm in which all inbound connections are
398 # looked up: slave builders, change notifications, status clients, and
400 dispatcher
= Dispatcher()
401 dispatcher
.master
= self
402 self
.dispatcher
= dispatcher
403 self
.checker
= checkers
.InMemoryUsernamePasswordDatabaseDontUse()
404 # the checker starts with no user/passwd pairs: they are added later
405 p
= portal
.Portal(dispatcher
)
406 p
.registerChecker(self
.checker
)
407 self
.slaveFactory
= pb
.PBServerFactory(p
)
408 self
.slaveFactory
.unsafeTracebacks
= True # let them see exceptions
410 self
.slavePortnum
= None
411 self
.slavePort
= None
413 self
.botmaster
= BotMaster()
414 self
.botmaster
.setName("botmaster")
415 self
.botmaster
.setServiceParent(self
)
416 dispatcher
.botmaster
= self
.botmaster
418 self
.status
= Status(self
.botmaster
, self
.basedir
)
420 self
.statusTargets
= []
422 # this ChangeMaster is a dummy, only used by tests. In the real
423 # buildmaster, where the BuildMaster instance is activated
424 # (startService is called) by twistd, this attribute is overwritten.
425 self
.useChanges(TestChangeMaster())
427 self
.readConfig
= False
429 def upgradeToVersion1(self
):
430 self
.dispatcher
= self
.slaveFactory
.root
.portal
.realm
432 def upgradeToVersion2(self
): # post-0.4.3
433 self
.webServer
= self
.webTCPPort
435 self
.webDistribServer
= self
.webUNIXPort
437 self
.configFileName
= "master.cfg"
439 def upgradeToVersion3(self
):
440 # post 0.6.3, solely to deal with the 0.6.3 breakage. Starting with
441 # 0.6.5 I intend to do away with .tap files altogether
443 self
.namedServices
= {}
446 def startService(self
):
447 service
.MultiService
.startService(self
)
448 self
.loadChanges() # must be done before loading the config file
449 if not self
.readConfig
:
450 # TODO: consider catching exceptions during this call to
451 # loadTheConfigFile and bailing (reactor.stop) if it fails,
452 # since without a config file we can't do anything except reload
453 # the config file, and it would be nice for the user to discover
455 self
.loadTheConfigFile()
456 if signal
and hasattr(signal
, "SIGHUP"):
457 signal
.signal(signal
.SIGHUP
, self
._handleSIGHUP
)
458 for b
in self
.botmaster
.builders
.values():
459 b
.builder_status
.addPointEvent(["master", "started"])
460 b
.builder_status
.saveYourself()
462 def useChanges(self
, changes
):
464 # TODO: can return a Deferred
465 self
.change_svc
.disownServiceParent()
466 self
.change_svc
= changes
467 self
.change_svc
.basedir
= self
.basedir
468 self
.change_svc
.setName("changemaster")
469 self
.dispatcher
.changemaster
= self
.change_svc
470 self
.change_svc
.setServiceParent(self
)
472 def loadChanges(self
):
473 filename
= os
.path
.join(self
.basedir
, "changes.pck")
475 changes
= load(open(filename
, "rb"))
478 log
.msg("changes.pck missing, using new one")
479 changes
= ChangeMaster()
481 log
.msg("corrupted changes.pck, using new one")
482 changes
= ChangeMaster()
483 self
.useChanges(changes
)
485 def _handleSIGHUP(self
, *args
):
486 reactor
.callLater(0, self
.loadTheConfigFile
)
490 @rtype: L{buildbot.status.builder.Status}
494 def loadTheConfigFile(self
, configFile
=None):
496 configFile
= os
.path
.join(self
.basedir
, self
.configFileName
)
498 log
.msg("Creating BuildMaster -- buildbot.version: %s" % buildbot
.version
)
499 log
.msg("loading configuration from %s" % configFile
)
500 configFile
= os
.path
.expanduser(configFile
)
503 f
= open(configFile
, "r")
505 log
.msg("unable to open config file '%s'" % configFile
)
506 log
.msg("leaving old configuration in place")
513 log
.msg("error during loadConfig")
515 log
.msg("The new config file is unusable, so I'll ignore it.")
516 log
.msg("I will keep using the previous config file instead.")
519 def loadConfig(self
, f
):
520 """Internal function to load a specific configuration file. Any
521 errors in the file will be signalled by raising an exception.
523 @return: a Deferred that will fire (with None) when the configuration
524 changes have been completed. This may involve a round-trip to each
525 buildslave that was involved."""
527 localDict
= {'basedir': os
.path
.expanduser(self
.basedir
)}
531 log
.msg("error while parsing config file")
535 config
= localDict
['BuildmasterConfig']
537 log
.err("missing config dictionary")
538 log
.err("config file must define BuildmasterConfig")
541 known_keys
= ("bots", "slaves",
542 "sources", "change_source",
543 "schedulers", "builders", "mergeRequests",
544 "slavePortnum", "debugPassword", "logCompressionLimit",
545 "manhole", "status", "projectName", "projectURL",
546 "buildbotURL", "properties", "prioritizeBuilders",
547 "eventHorizon", "buildCacheSize", "logHorizon", "buildHorizon",
550 for k
in config
.keys():
551 if k
not in known_keys
:
552 log
.msg("unknown key '%s' defined in config dictionary" % k
)
556 schedulers
= config
['schedulers']
557 builders
= config
['builders']
559 if k
['name'].startswith("_"):
560 errmsg
= ("builder names must not start with an "
561 "underscore: " + k
['name'])
563 raise ValueError(errmsg
)
565 slavePortnum
= config
['slavePortnum']
566 #slaves = config['slaves']
567 #change_source = config['change_source']
570 debugPassword
= config
.get('debugPassword')
571 manhole
= config
.get('manhole')
572 status
= config
.get('status', [])
573 projectName
= config
.get('projectName')
574 projectURL
= config
.get('projectURL')
575 buildbotURL
= config
.get('buildbotURL')
576 properties
= config
.get('properties', {})
577 buildCacheSize
= config
.get('buildCacheSize', None)
578 eventHorizon
= config
.get('eventHorizon', None)
579 logHorizon
= config
.get('logHorizon', None)
580 buildHorizon
= config
.get('buildHorizon', None)
581 logCompressionLimit
= config
.get('logCompressionLimit')
582 if logCompressionLimit
is not None and not \
583 isinstance(logCompressionLimit
, int):
584 raise ValueError("logCompressionLimit needs to be bool or int")
585 mergeRequests
= config
.get('mergeRequests')
586 if mergeRequests
is not None and not callable(mergeRequests
):
587 raise ValueError("mergeRequests must be a callable")
588 prioritizeBuilders
= config
.get('prioritizeBuilders')
589 if prioritizeBuilders
is not None and not callable(prioritizeBuilders
):
590 raise ValueError("prioritizeBuilders must be callable")
591 changeHorizon
= config
.get("changeHorizon")
592 if changeHorizon
is not None and not isinstance(changeHorizon
, int):
593 raise ValueError("changeHorizon needs to be an int")
596 log
.msg("config dictionary is missing a required parameter")
597 log
.msg("leaving old configuration in place")
600 #if "bots" in config:
601 # raise KeyError("c['bots'] is no longer accepted")
603 slaves
= config
.get('slaves', [])
605 m
= ("c['bots'] is deprecated as of 0.7.6 and will be "
606 "removed by 0.8.0 . Please use c['slaves'] instead.")
608 warnings
.warn(m
, DeprecationWarning)
609 for name
, passwd
in config
['bots']:
610 slaves
.append(BuildSlave(name
, passwd
))
612 if "bots" not in config
and "slaves" not in config
:
613 log
.msg("config dictionary must have either 'bots' or 'slaves'")
614 log
.msg("leaving old configuration in place")
615 raise KeyError("must have either 'bots' or 'slaves'")
617 #if "sources" in config:
618 # raise KeyError("c['sources'] is no longer accepted")
620 if changeHorizon
is not None:
621 self
.change_svc
.changeHorizon
= changeHorizon
623 change_source
= config
.get('change_source', [])
624 if isinstance(change_source
, (list, tuple)):
625 change_sources
= change_source
627 change_sources
= [change_source
]
628 if "sources" in config
:
629 m
= ("c['sources'] is deprecated as of 0.7.6 and will be "
630 "removed by 0.8.0 . Please use c['change_source'] instead.")
632 warnings
.warn(m
, DeprecationWarning)
633 for s
in config
['sources']:
634 change_sources
.append(s
)
636 # do some validation first
638 assert interfaces
.IBuildSlave
.providedBy(s
)
639 if s
.slavename
in ("debug", "change", "status"):
641 "reserved name '%s' used for a bot" % s
.slavename
)
642 if config
.has_key('interlocks'):
643 raise KeyError("c['interlocks'] is no longer accepted")
645 assert isinstance(change_sources
, (list, tuple))
646 for s
in change_sources
:
647 assert interfaces
.IChangeSource(s
, None)
648 # this assertion catches c['schedulers'] = Scheduler(), since
649 # Schedulers are service.MultiServices and thus iterable.
650 errmsg
= "c['schedulers'] must be a list of Scheduler instances"
651 assert isinstance(schedulers
, (list, tuple)), errmsg
653 assert interfaces
.IScheduler(s
, None), errmsg
654 assert isinstance(status
, (list, tuple))
656 assert interfaces
.IStatusReceiver(s
, None)
658 slavenames
= [s
.slavename
for s
in slaves
]
663 raise ValueError("builder %s must be defined with a dict, "
664 "not a tuple" % b
[0])
665 if b
.has_key('slavename') and b
['slavename'] not in slavenames
:
666 raise ValueError("builder %s uses undefined slave %s" \
667 % (b
['name'], b
['slavename']))
668 for n
in b
.get('slavenames', []):
669 if n
not in slavenames
:
670 raise ValueError("builder %s uses undefined slave %s" \
672 if b
['name'] in buildernames
:
673 raise ValueError("duplicate builder name %s"
675 buildernames
.append(b
['name'])
676 if b
['builddir'] in dirnames
:
677 raise ValueError("builder %s reuses builddir %s"
678 % (b
['name'], b
['builddir']))
679 dirnames
.append(b
['builddir'])
681 unscheduled_buildernames
= buildernames
[:]
684 for b
in s
.listBuilderNames():
685 assert b
in buildernames
, \
686 "%s uses unknown builder %s" % (s
, b
)
687 if b
in unscheduled_buildernames
:
688 unscheduled_buildernames
.remove(b
)
690 if s
.name
in schedulernames
:
691 # TODO: schedulers share a namespace with other Service
692 # children of the BuildMaster node, like status plugins, the
693 # Manhole, the ChangeMaster, and the BotMaster (although most
694 # of these don't have names)
695 msg
= ("Schedulers must have unique names, but "
696 "'%s' was a duplicate" % (s
.name
,))
697 raise ValueError(msg
)
698 schedulernames
.append(s
.name
)
700 if unscheduled_buildernames
:
701 log
.msg("Warning: some Builders have no Schedulers to drive them:"
702 " %s" % (unscheduled_buildernames
,))
704 # assert that all locks used by the Builds and their Steps are
708 for l
in b
.get('locks', []):
709 if isinstance(l
, locks
.LockAccess
): # User specified access to the lock
711 if lock_dict
.has_key(l
.name
):
712 if lock_dict
[l
.name
] is not l
:
713 raise ValueError("Two different locks (%s and %s) "
715 % (l
, lock_dict
[l
.name
], l
.name
))
717 lock_dict
[l
.name
] = l
718 # TODO: this will break with any BuildFactory that doesn't use a
719 # .steps list, but I think the verification step is more
721 for s
in b
['factory'].steps
:
722 for l
in s
[1].get('locks', []):
723 if isinstance(l
, locks
.LockAccess
): # User specified access to the lock
725 if lock_dict
.has_key(l
.name
):
726 if lock_dict
[l
.name
] is not l
:
727 raise ValueError("Two different locks (%s and %s)"
729 % (l
, lock_dict
[l
.name
], l
.name
))
731 lock_dict
[l
.name
] = l
733 if not isinstance(properties
, dict):
734 raise ValueError("c['properties'] must be a dictionary")
736 # slavePortnum supposed to be a strports specification
737 if type(slavePortnum
) is int:
738 slavePortnum
= "tcp:%d" % slavePortnum
740 # now we're committed to implementing the new configuration, so do
742 # TODO: actually, this is spread across a couple of Deferreds, so it
743 # really isn't atomic.
745 d
= defer
.succeed(None)
747 self
.projectName
= projectName
748 self
.projectURL
= projectURL
749 self
.buildbotURL
= buildbotURL
751 self
.properties
= Properties()
752 self
.properties
.update(properties
, self
.configFileName
)
753 if logCompressionLimit
is not None:
754 self
.status
.logCompressionLimit
= logCompressionLimit
755 if mergeRequests
is not None:
756 self
.botmaster
.mergeRequests
= mergeRequests
757 if prioritizeBuilders
is not None:
758 self
.botmaster
.prioritizeBuilders
= prioritizeBuilders
760 self
.buildCacheSize
= buildCacheSize
761 self
.eventHorizon
= eventHorizon
762 self
.logHorizon
= logHorizon
763 self
.buildHorizon
= buildHorizon
765 # self.slaves: Disconnect any that were attached and removed from the
766 # list. Update self.checker with the new list of passwords, including
767 # debug/change/status.
768 d
.addCallback(lambda res
: self
.loadConfig_Slaves(slaves
))
772 self
.checker
.addUser("debug", debugPassword
)
773 self
.debugPassword
= debugPassword
776 if manhole
!= self
.manhole
:
779 # disownServiceParent may return a Deferred
780 d
.addCallback(lambda res
: self
.manhole
.disownServiceParent())
784 d
.addCallback(_remove
)
787 self
.manhole
= manhole
788 manhole
.setServiceParent(self
)
791 # add/remove self.botmaster.builders to match builders. The
792 # botmaster will handle startup/shutdown issues.
793 d
.addCallback(lambda res
: self
.loadConfig_Builders(builders
))
795 d
.addCallback(lambda res
: self
.loadConfig_status(status
))
797 # Schedulers are added after Builders in case they start right away
798 d
.addCallback(lambda res
: self
.loadConfig_Schedulers(schedulers
))
799 # and Sources go after Schedulers for the same reason
800 d
.addCallback(lambda res
: self
.loadConfig_Sources(change_sources
))
803 if self
.slavePortnum
!= slavePortnum
:
805 def closeSlavePort(res
):
806 d1
= self
.slavePort
.disownServiceParent()
807 self
.slavePort
= None
809 d
.addCallback(closeSlavePort
)
810 if slavePortnum
is not None:
811 def openSlavePort(res
):
812 self
.slavePort
= strports
.service(slavePortnum
,
814 self
.slavePort
.setServiceParent(self
)
815 d
.addCallback(openSlavePort
)
816 log
.msg("BuildMaster listening on port %s" % slavePortnum
)
817 self
.slavePortnum
= slavePortnum
819 log
.msg("configuration update started")
821 self
.readConfig
= True
822 log
.msg("configuration update complete")
824 d
.addCallback(lambda res
: self
.botmaster
.maybeStartAllBuilds())
827 def loadConfig_Slaves(self
, new_slaves
):
828 # set up the Checker with the names and passwords of all valid bots
829 self
.checker
.users
= {} # violates abstraction, oh well
831 self
.checker
.addUser(s
.slavename
, s
.password
)
832 self
.checker
.addUser("change", "changepw")
833 # let the BotMaster take care of the rest
834 return self
.botmaster
.loadConfig_Slaves(new_slaves
)
836 def loadConfig_Sources(self
, sources
):
838 log
.msg("warning: no ChangeSources specified in c['change_source']")
839 # shut down any that were removed, start any that were added
840 deleted_sources
= [s
for s
in self
.change_svc
if s
not in sources
]
841 added_sources
= [s
for s
in sources
if s
not in self
.change_svc
]
842 log
.msg("adding %d new changesources, removing %d" %
843 (len(added_sources
), len(deleted_sources
)))
844 dl
= [self
.change_svc
.removeSource(s
) for s
in deleted_sources
]
846 [self
.change_svc
.addSource(s
) for s
in added_sources
]
847 d
= defer
.DeferredList(dl
, fireOnOneErrback
=1, consumeErrors
=0)
848 d
.addCallback(addNewOnes
)
851 def allSchedulers(self
):
852 return [child
for child
in self
853 if interfaces
.IScheduler
.providedBy(child
)]
856 def loadConfig_Schedulers(self
, newschedulers
):
857 oldschedulers
= self
.allSchedulers()
858 removed
= [s
for s
in oldschedulers
if s
not in newschedulers
]
859 added
= [s
for s
in newschedulers
if s
not in oldschedulers
]
860 dl
= [defer
.maybeDeferred(s
.disownServiceParent
) for s
in removed
]
862 log
.msg("adding %d new schedulers, removed %d" %
863 (len(added
), len(dl
)))
865 s
.setServiceParent(self
)
866 d
= defer
.DeferredList(dl
, fireOnOneErrback
=1)
867 d
.addCallback(addNewOnes
)
869 # notify Downstream schedulers to potentially pick up
870 # new schedulers now that we have removed and added some
871 def updateDownstreams(res
):
872 log
.msg("notifying downstream schedulers of changes")
873 for s
in newschedulers
:
874 if interfaces
.IDownstreamScheduler
.providedBy(s
):
875 s
.checkUpstreamScheduler()
876 d
.addCallback(updateDownstreams
)
879 def loadConfig_Builders(self
, newBuilderData
):
880 somethingChanged
= False
883 allBuilders
= self
.botmaster
.builders
.copy()
884 for data
in newBuilderData
:
887 newBuilderNames
.append(name
)
889 # identify all that were removed
890 for oldname
in self
.botmaster
.getBuildernames():
891 if oldname
not in newList
:
892 log
.msg("removing old builder %s" % oldname
)
893 del allBuilders
[oldname
]
894 somethingChanged
= True
895 # announce the change
896 self
.status
.builderRemoved(oldname
)
898 # everything in newList is either unchanged, changed, or new
899 for name
, data
in newList
.items():
900 old
= self
.botmaster
.builders
.get(name
)
901 basedir
= data
['builddir'] # used on both master and slave
902 #name, slave, builddir, factory = data
904 # category added after 0.6.2
905 category
= data
.get('category', None)
906 log
.msg("adding new builder %s for category %s" %
908 statusbag
= self
.status
.builderAdded(name
, basedir
, category
)
909 builder
= Builder(data
, statusbag
)
910 allBuilders
[name
] = builder
911 somethingChanged
= True
912 elif old
.compareToSetup(data
):
913 # changed: try to minimize the disruption and only modify the
914 # pieces that really changed
915 diffs
= old
.compareToSetup(data
)
916 log
.msg("updating builder %s: %s" % (name
, "\n".join(diffs
)))
918 statusbag
= old
.builder_status
919 statusbag
.saveYourself() # seems like a good idea
920 # TODO: if the basedir was changed, we probably need to make
922 new_builder
= Builder(data
, statusbag
)
923 new_builder
.consumeTheSoulOfYourPredecessor(old
)
924 # that migrates any retained slavebuilders too
926 # point out that the builder was updated. On the Waterfall,
927 # this will appear just after any currently-running builds.
928 statusbag
.addPointEvent(["config", "updated"])
930 allBuilders
[name
] = new_builder
931 somethingChanged
= True
933 # unchanged: leave it alone
934 log
.msg("builder %s is unchanged" % name
)
937 # regardless of whether anything changed, get each builder status
938 # to update its config
939 for builder
in allBuilders
.values():
940 builder
.builder_status
.reconfigFromBuildmaster(self
)
942 # and then tell the botmaster if anything's changed
944 sortedAllBuilders
= [allBuilders
[name
] for name
in newBuilderNames
]
945 d
= self
.botmaster
.setBuilders(sortedAllBuilders
)
949 def loadConfig_status(self
, status
):
953 for s
in self
.statusTargets
[:]:
955 log
.msg("removing IStatusReceiver", s
)
956 d
= defer
.maybeDeferred(s
.disownServiceParent
)
958 self
.statusTargets
.remove(s
)
959 # after those are finished going away, add new ones
962 if not s
in self
.statusTargets
:
963 log
.msg("adding IStatusReceiver", s
)
964 s
.setServiceParent(self
)
965 self
.statusTargets
.append(s
)
966 d
= defer
.DeferredList(dl
, fireOnOneErrback
=1)
967 d
.addCallback(addNewOnes
)
971 def addChange(self
, change
):
972 for s
in self
.allSchedulers():
975 def submitBuildSet(self
, bs
):
976 # determine the set of Builders to use
978 for name
in bs
.builderNames
:
979 b
= self
.botmaster
.builders
.get(name
)
981 if b
not in builders
:
984 # TODO: add aliases like 'all'
985 raise KeyError("no such builder named '%s'" % name
)
987 # now tell the BuildSet to create BuildRequests for all those
988 # Builders and submit them
990 self
.status
.buildsetSubmitted(bs
.status
)
994 implements(interfaces
.IControl
)
996 def __init__(self
, master
):
999 def addChange(self
, change
):
1000 self
.master
.change_svc
.addChange(change
)
1002 def submitBuildSet(self
, bs
):
1003 self
.master
.submitBuildSet(bs
)
1005 def getBuilder(self
, name
):
1006 b
= self
.master
.botmaster
.builders
[name
]
1007 return interfaces
.IBuilderControl(b
)
1009 components
.registerAdapter(Control
, BuildMaster
, interfaces
.IControl
)
1011 # so anybody who can get a handle on the BuildMaster can cause a build with:
1012 # IControl(master).getBuilder("full-2.3").requestBuild(buildrequest)