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
.internet
import defer
, reactor
15 from twisted
.spread
import pb
16 from twisted
.cred
import portal
, checkers
17 from twisted
.application
import service
, strports
18 from twisted
.persisted
import styles
21 from buildbot
.util
import now
22 from buildbot
.pbutil
import NewCredPerspective
23 from buildbot
.process
.builder
import Builder
, IDLE
24 from buildbot
.process
.base
import BuildRequest
25 from buildbot
.status
.builder
import Status
26 from buildbot
.changes
.changes
import Change
, ChangeMaster
27 from buildbot
.sourcestamp
import SourceStamp
28 from buildbot
.buildslave
import BuildSlave
29 from buildbot
import interfaces
31 ########################################
33 class BotMaster(service
.MultiService
):
35 """This is the master-side service which manages remote buildbot slaves.
36 It provides them with BuildSlaves, and distributes file change
37 notification messages to them.
43 service
.MultiService
.__init
__(self
)
45 self
.builderNames
= []
46 # builders maps Builder names to instances of bb.p.builder.Builder,
47 # which is the master-side object that defines and controls a build.
48 # They are added by calling botmaster.addBuilder() from the startup
51 # self.slaves contains a ready BuildSlave instance for each
52 # potential buildslave, i.e. all the ones listed in the config file.
53 # If the slave is connected, self.slaves[slavename].slave will
54 # contain a RemoteReference to their Bot instance. If it is not
55 # connected, that attribute will hold None.
56 self
.slaves
= {} # maps slavename to BuildSlave
57 self
.statusClientService
= None
60 # self.locks holds the real Lock instances
63 # these four are convenience functions for testing
65 def waitUntilBuilderAttached(self
, name
):
66 b
= self
.builders
[name
]
68 # return defer.succeed(None)
70 b
.watchers
['attach'].append(d
)
73 def waitUntilBuilderDetached(self
, name
):
74 b
= self
.builders
.get(name
)
75 if not b
or not b
.slaves
:
76 return defer
.succeed(None)
78 b
.watchers
['detach'].append(d
)
81 def waitUntilBuilderFullyDetached(self
, name
):
82 b
= self
.builders
.get(name
)
83 # TODO: this looks too deeply inside the Builder object
84 if not b
or not b
.slaves
:
85 return defer
.succeed(None)
87 b
.watchers
['detach_all'].append(d
)
90 def waitUntilBuilderIdle(self
, name
):
91 b
= self
.builders
[name
]
92 # TODO: this looks way too deeply inside the Builder object
96 b
.watchers
['idle'].append(d
)
98 return defer
.succeed(None)
100 def loadConfig_Slaves(self
, new_slaves
):
101 old_slaves
= [c
for c
in list(self
)
102 if interfaces
.IBuildSlave
.providedBy(c
)]
104 # identify added/removed slaves. For each slave we construct a tuple
105 # of (name, password, class), and we consider the slave to be already
106 # present if the tuples match. (we include the class to make sure
107 # that BuildSlave(name,pw) is different than
108 # SubclassOfBuildSlave(name,pw) ). If the password or class has
109 # changed, we will remove the old version of the slave and replace it
110 # with a new one. If anything else has changed, we just update the
111 # old BuildSlave instance in place. If the name has changed, of
112 # course, it looks exactly the same as deleting one slave and adding
116 old_t
[(s
.slavename
, s
.password
, s
.__class
__)] = s
119 new_t
[(s
.slavename
, s
.password
, s
.__class
__)] = s
129 # removeSlave will hang up on the old bot
132 dl
.append(self
.removeSlave(s
))
133 d
= defer
.DeferredList(dl
, fireOnOneErrback
=True)
137 for t
in remaining_t
:
138 old_t
[t
].update(new_t
[t
])
142 def addSlave(self
, s
):
143 s
.setServiceParent(self
)
145 self
.slaves
[s
.slavename
] = s
147 def removeSlave(self
, s
):
148 # TODO: technically, disownServiceParent could return a Deferred
149 s
.disownServiceParent()
150 d
= self
.slaves
[s
.slavename
].disconnect()
151 del self
.slaves
[s
.slavename
]
154 def slaveLost(self
, bot
):
155 for name
, b
in self
.builders
.items():
156 if bot
.slavename
in b
.slavenames
:
159 def getBuildersForSlave(self
, slavename
):
161 for b
in self
.builders
.values()
162 if slavename
in b
.slavenames
]
164 def getBuildernames(self
):
165 return self
.builderNames
167 def getBuilders(self
):
168 allBuilders
= [self
.builders
[name
] for name
in self
.builderNames
]
171 def setBuilders(self
, builders
):
173 self
.builderNames
= []
175 for slavename
in b
.slavenames
:
176 # this is actually validated earlier
177 assert slavename
in self
.slaves
178 self
.builders
[b
.name
] = b
179 self
.builderNames
.append(b
.name
)
181 d
= self
._updateAllSlaves
()
184 def _updateAllSlaves(self
):
185 """Notify all buildslaves about changes in their Builders."""
186 dl
= [s
.updateSlave() for s
in self
.slaves
.values()]
187 return defer
.DeferredList(dl
)
189 def maybeStartAllBuilds(self
):
190 for b
in self
.builders
.values():
193 def getPerspective(self
, slavename
):
194 return self
.slaves
[slavename
]
196 def shutdownSlaves(self
):
197 # TODO: make this into a bot method rather than a builder method
198 for b
in self
.slaves
.values():
201 def stopService(self
):
202 for b
in self
.builders
.values():
203 b
.builder_status
.addPointEvent(["master", "shutdown"])
204 b
.builder_status
.saveYourself()
205 return service
.Service
.stopService(self
)
207 def getLockByID(self
, lockid
):
208 """Convert a Lock identifier into an actual Lock instance.
209 @param lockid: a locks.MasterLock or locks.SlaveLock instance
210 @return: a locks.RealMasterLock or locks.RealSlaveLock instance
212 if not lockid
in self
.locks
:
213 self
.locks
[lockid
] = lockid
.lockClass(lockid
)
214 # if the master.cfg file has changed maxCount= on the lock, the next
215 # time a build is started, they'll get a new RealLock instance. Note
216 # that this requires that MasterLock and SlaveLock (marker) instances
217 # be hashable and that they should compare properly.
218 return self
.locks
[lockid
]
220 ########################################
224 class DebugPerspective(NewCredPerspective
):
225 def attached(self
, mind
):
227 def detached(self
, mind
):
230 def perspective_requestBuild(self
, buildername
, reason
, branch
, revision
):
231 c
= interfaces
.IControl(self
.master
)
232 bc
= c
.getBuilder(buildername
)
233 ss
= SourceStamp(branch
, revision
)
234 br
= BuildRequest(reason
, ss
, buildername
)
237 def perspective_pingBuilder(self
, buildername
):
238 c
= interfaces
.IControl(self
.master
)
239 bc
= c
.getBuilder(buildername
)
242 def perspective_fakeChange(self
, file, revision
=None, who
="fakeUser",
244 change
= Change(who
, [file], "some fake comments\n",
245 branch
=branch
, revision
=revision
)
246 c
= interfaces
.IControl(self
.master
)
249 def perspective_setCurrentState(self
, buildername
, state
):
250 builder
= self
.botmaster
.builders
.get(buildername
)
251 if not builder
: return
252 if state
== "offline":
253 builder
.statusbag
.currentlyOffline()
255 builder
.statusbag
.currentlyIdle()
256 if state
== "waiting":
257 builder
.statusbag
.currentlyWaiting(now()+10)
258 if state
== "building":
259 builder
.statusbag
.currentlyBuilding(None)
260 def perspective_reload(self
):
261 print "doing reload of the config file"
262 self
.master
.loadTheConfigFile()
263 def perspective_pokeIRC(self
):
264 print "saying something on IRC"
265 from buildbot
.status
import words
266 for s
in self
.master
:
267 if isinstance(s
, words
.IRC
):
269 for channel
in bot
.channels
:
270 print " channel", channel
271 bot
.p
.msg(channel
, "Ow, quit it")
273 def perspective_print(self
, msg
):
276 class Dispatcher(styles
.Versioned
):
277 implements(portal
.IRealm
)
278 persistenceVersion
= 2
283 def upgradeToVersion1(self
):
284 self
.master
= self
.botmaster
.parent
285 def upgradeToVersion2(self
):
288 def register(self
, name
, afactory
):
289 self
.names
[name
] = afactory
290 def unregister(self
, name
):
293 def requestAvatar(self
, avatarID
, mind
, interface
):
294 assert interface
== pb
.IPerspective
295 afactory
= self
.names
.get(avatarID
)
297 p
= afactory
.getPerspective()
298 elif avatarID
== "debug":
299 p
= DebugPerspective()
300 p
.master
= self
.master
301 p
.botmaster
= self
.botmaster
302 elif avatarID
== "statusClient":
303 p
= self
.statusClientService
.getPerspective()
305 # it must be one of the buildslaves: no other names will make it
307 p
= self
.botmaster
.getPerspective(avatarID
)
310 raise ValueError("no perspective for '%s'" % avatarID
)
312 d
= defer
.maybeDeferred(p
.attached
, mind
)
313 d
.addCallback(self
._avatarAttached
, mind
)
316 def _avatarAttached(self
, p
, mind
):
317 return (pb
.IPerspective
, p
, lambda p
=p
,mind
=mind
: p
.detached(mind
))
319 ########################################
325 # all IChangeSource objects
326 # StatusClientService
327 # TCPClient(self.ircFactory)
328 # TCPServer(self.slaveFactory) -> dispatcher.requestAvatar
329 # TCPServer(self.site)
330 # UNIXServer(ResourcePublisher(self.site))
333 class BuildMaster(service
.MultiService
, styles
.Versioned
):
335 persistenceVersion
= 3
338 projectName
= "(unspecified)"
343 def __init__(self
, basedir
, configFileName
="master.cfg"):
344 service
.MultiService
.__init
__(self
)
345 self
.setName("buildmaster")
346 self
.basedir
= basedir
347 self
.configFileName
= configFileName
349 # the dispatcher is the realm in which all inbound connections are
350 # looked up: slave builders, change notifications, status clients, and
352 dispatcher
= Dispatcher()
353 dispatcher
.master
= self
354 self
.dispatcher
= dispatcher
355 self
.checker
= checkers
.InMemoryUsernamePasswordDatabaseDontUse()
356 # the checker starts with no user/passwd pairs: they are added later
357 p
= portal
.Portal(dispatcher
)
358 p
.registerChecker(self
.checker
)
359 self
.slaveFactory
= pb
.PBServerFactory(p
)
360 self
.slaveFactory
.unsafeTracebacks
= True # let them see exceptions
362 self
.slavePortnum
= None
363 self
.slavePort
= None
365 self
.botmaster
= BotMaster()
366 self
.botmaster
.setName("botmaster")
367 self
.botmaster
.setServiceParent(self
)
368 dispatcher
.botmaster
= self
.botmaster
370 self
.status
= Status(self
.botmaster
, self
.basedir
)
372 self
.statusTargets
= []
374 # this ChangeMaster is a dummy, only used by tests. In the real
375 # buildmaster, where the BuildMaster instance is activated
376 # (startService is called) by twistd, this attribute is overwritten.
377 self
.useChanges(ChangeMaster())
379 self
.readConfig
= False
381 def upgradeToVersion1(self
):
382 self
.dispatcher
= self
.slaveFactory
.root
.portal
.realm
384 def upgradeToVersion2(self
): # post-0.4.3
385 self
.webServer
= self
.webTCPPort
387 self
.webDistribServer
= self
.webUNIXPort
389 self
.configFileName
= "master.cfg"
391 def upgradeToVersion3(self
):
392 # post 0.6.3, solely to deal with the 0.6.3 breakage. Starting with
393 # 0.6.5 I intend to do away with .tap files altogether
395 self
.namedServices
= {}
398 def startService(self
):
399 service
.MultiService
.startService(self
)
400 self
.loadChanges() # must be done before loading the config file
401 if not self
.readConfig
:
402 # TODO: consider catching exceptions during this call to
403 # loadTheConfigFile and bailing (reactor.stop) if it fails,
404 # since without a config file we can't do anything except reload
405 # the config file, and it would be nice for the user to discover
407 self
.loadTheConfigFile()
408 if signal
and hasattr(signal
, "SIGHUP"):
409 signal
.signal(signal
.SIGHUP
, self
._handleSIGHUP
)
410 for b
in self
.botmaster
.builders
.values():
411 b
.builder_status
.addPointEvent(["master", "started"])
412 b
.builder_status
.saveYourself()
414 def useChanges(self
, changes
):
416 # TODO: can return a Deferred
417 self
.change_svc
.disownServiceParent()
418 self
.change_svc
= changes
419 self
.change_svc
.basedir
= self
.basedir
420 self
.change_svc
.setName("changemaster")
421 self
.dispatcher
.changemaster
= self
.change_svc
422 self
.change_svc
.setServiceParent(self
)
424 def loadChanges(self
):
425 filename
= os
.path
.join(self
.basedir
, "changes.pck")
427 changes
= load(open(filename
, "rb"))
430 log
.msg("changes.pck missing, using new one")
431 changes
= ChangeMaster()
433 log
.msg("corrupted changes.pck, using new one")
434 changes
= ChangeMaster()
435 self
.useChanges(changes
)
437 def _handleSIGHUP(self
, *args
):
438 reactor
.callLater(0, self
.loadTheConfigFile
)
442 @rtype: L{buildbot.status.builder.Status}
446 def loadTheConfigFile(self
, configFile
=None):
448 configFile
= os
.path
.join(self
.basedir
, self
.configFileName
)
450 log
.msg("loading configuration from %s" % configFile
)
451 configFile
= os
.path
.expanduser(configFile
)
454 f
= open(configFile
, "r")
456 log
.msg("unable to open config file '%s'" % configFile
)
457 log
.msg("leaving old configuration in place")
464 log
.msg("error during loadConfig")
466 log
.msg("The new config file is unusable, so I'll ignore it.")
467 log
.msg("I will keep using the previous config file instead.")
470 def loadConfig(self
, f
):
471 """Internal function to load a specific configuration file. Any
472 errors in the file will be signalled by raising an exception.
474 @return: a Deferred that will fire (with None) when the configuration
475 changes have been completed. This may involve a round-trip to each
476 buildslave that was involved."""
478 localDict
= {'basedir': os
.path
.expanduser(self
.basedir
)}
482 log
.msg("error while parsing config file")
486 config
= localDict
['BuildmasterConfig']
488 log
.err("missing config dictionary")
489 log
.err("config file must define BuildmasterConfig")
492 known_keys
= ("bots", "slaves",
493 "sources", "change_source",
494 "schedulers", "builders",
495 "slavePortnum", "debugPassword", "manhole",
496 "status", "projectName", "projectURL", "buildbotURL",
498 for k
in config
.keys():
499 if k
not in known_keys
:
500 log
.msg("unknown key '%s' defined in config dictionary" % k
)
504 schedulers
= config
['schedulers']
505 builders
= config
['builders']
507 if k
['name'].startswith("_"):
508 errmsg
= ("builder names must not start with an "
509 "underscore: " + k
['name'])
511 raise ValueError(errmsg
)
513 slavePortnum
= config
['slavePortnum']
514 #slaves = config['slaves']
515 #change_source = config['change_source']
518 debugPassword
= config
.get('debugPassword')
519 manhole
= config
.get('manhole')
520 status
= config
.get('status', [])
521 projectName
= config
.get('projectName')
522 projectURL
= config
.get('projectURL')
523 buildbotURL
= config
.get('buildbotURL')
526 log
.msg("config dictionary is missing a required parameter")
527 log
.msg("leaving old configuration in place")
530 #if "bots" in config:
531 # raise KeyError("c['bots'] is no longer accepted")
533 slaves
= config
.get('slaves', [])
535 m
= ("c['bots'] is deprecated as of 0.7.6 and will be "
536 "removed by 0.8.0 . Please use c['slaves'] instead.")
538 warnings
.warn(m
, DeprecationWarning)
539 for name
, passwd
in config
['bots']:
540 slaves
.append(BuildSlave(name
, passwd
))
542 if "bots" not in config
and "slaves" not in config
:
543 log
.msg("config dictionary must have either 'bots' or 'slaves'")
544 log
.msg("leaving old configuration in place")
545 raise KeyError("must have either 'bots' or 'slaves'")
547 #if "sources" in config:
548 # raise KeyError("c['sources'] is no longer accepted")
550 change_source
= config
.get('change_source', [])
551 if isinstance(change_source
, (list, tuple)):
552 change_sources
= change_source
554 change_sources
= [change_source
]
555 if "sources" in config
:
556 m
= ("c['sources'] is deprecated as of 0.7.6 and will be "
557 "removed by 0.8.0 . Please use c['change_source'] instead.")
559 warnings
.warn(m
, DeprecationWarning)
560 for s
in config
['sources']:
561 change_sources
.append(s
)
563 # do some validation first
565 assert isinstance(s
, BuildSlave
)
566 if s
.slavename
in ("debug", "change", "status"):
567 raise KeyError, "reserved name '%s' used for a bot" % s
.slavename
568 if config
.has_key('interlocks'):
569 raise KeyError("c['interlocks'] is no longer accepted")
571 assert isinstance(change_sources
, (list, tuple))
572 for s
in change_sources
:
573 assert interfaces
.IChangeSource(s
, None)
574 # this assertion catches c['schedulers'] = Scheduler(), since
575 # Schedulers are service.MultiServices and thus iterable.
576 errmsg
= "c['schedulers'] must be a list of Scheduler instances"
577 assert isinstance(schedulers
, (list, tuple)), errmsg
579 assert interfaces
.IScheduler(s
, None), errmsg
580 assert isinstance(status
, (list, tuple))
582 assert interfaces
.IStatusReceiver(s
, None)
584 slavenames
= [s
.slavename
for s
in slaves
]
589 raise ValueError("builder %s must be defined with a dict, "
590 "not a tuple" % b
[0])
591 if b
.has_key('slavename') and b
['slavename'] not in slavenames
:
592 raise ValueError("builder %s uses undefined slave %s" \
593 % (b
['name'], b
['slavename']))
594 for n
in b
.get('slavenames', []):
595 if n
not in slavenames
:
596 raise ValueError("builder %s uses undefined slave %s" \
598 if b
['name'] in buildernames
:
599 raise ValueError("duplicate builder name %s"
601 buildernames
.append(b
['name'])
602 if b
['builddir'] in dirnames
:
603 raise ValueError("builder %s reuses builddir %s"
604 % (b
['name'], b
['builddir']))
605 dirnames
.append(b
['builddir'])
607 unscheduled_buildernames
= buildernames
[:]
610 for b
in s
.listBuilderNames():
611 assert b
in buildernames
, \
612 "%s uses unknown builder %s" % (s
, b
)
613 if b
in unscheduled_buildernames
:
614 unscheduled_buildernames
.remove(b
)
616 if s
.name
in schedulernames
:
617 # TODO: schedulers share a namespace with other Service
618 # children of the BuildMaster node, like status plugins, the
619 # Manhole, the ChangeMaster, and the BotMaster (although most
620 # of these don't have names)
621 msg
= ("Schedulers must have unique names, but "
622 "'%s' was a duplicate" % (s
.name
,))
623 raise ValueError(msg
)
624 schedulernames
.append(s
.name
)
626 if unscheduled_buildernames
:
627 log
.msg("Warning: some Builders have no Schedulers to drive them:"
628 " %s" % (unscheduled_buildernames
,))
630 # assert that all locks used by the Builds and their Steps are
634 for l
in b
.get('locks', []):
635 if locks
.has_key(l
.name
):
636 if locks
[l
.name
] is not l
:
637 raise ValueError("Two different locks (%s and %s) "
639 % (l
, locks
[l
.name
], l
.name
))
642 # TODO: this will break with any BuildFactory that doesn't use a
643 # .steps list, but I think the verification step is more
645 for s
in b
['factory'].steps
:
646 for l
in s
[1].get('locks', []):
647 if locks
.has_key(l
.name
):
648 if locks
[l
.name
] is not l
:
649 raise ValueError("Two different locks (%s and %s)"
651 % (l
, locks
[l
.name
], l
.name
))
655 # slavePortnum supposed to be a strports specification
656 if type(slavePortnum
) is int:
657 slavePortnum
= "tcp:%d" % slavePortnum
659 # now we're committed to implementing the new configuration, so do
661 # TODO: actually, this is spread across a couple of Deferreds, so it
662 # really isn't atomic.
664 d
= defer
.succeed(None)
666 self
.projectName
= projectName
667 self
.projectURL
= projectURL
668 self
.buildbotURL
= buildbotURL
670 # self.slaves: Disconnect any that were attached and removed from the
671 # list. Update self.checker with the new list of passwords, including
672 # debug/change/status.
673 d
.addCallback(lambda res
: self
.loadConfig_Slaves(slaves
))
677 self
.checker
.addUser("debug", debugPassword
)
678 self
.debugPassword
= debugPassword
681 if manhole
!= self
.manhole
:
684 # disownServiceParent may return a Deferred
685 d
.addCallback(lambda res
: self
.manhole
.disownServiceParent())
689 d
.addCallback(_remove
)
692 self
.manhole
= manhole
693 manhole
.setServiceParent(self
)
696 # add/remove self.botmaster.builders to match builders. The
697 # botmaster will handle startup/shutdown issues.
698 d
.addCallback(lambda res
: self
.loadConfig_Builders(builders
))
700 d
.addCallback(lambda res
: self
.loadConfig_status(status
))
702 # Schedulers are added after Builders in case they start right away
703 d
.addCallback(lambda res
: self
.loadConfig_Schedulers(schedulers
))
704 # and Sources go after Schedulers for the same reason
705 d
.addCallback(lambda res
: self
.loadConfig_Sources(change_sources
))
708 if self
.slavePortnum
!= slavePortnum
:
710 def closeSlavePort(res
):
711 d1
= self
.slavePort
.disownServiceParent()
712 self
.slavePort
= None
714 d
.addCallback(closeSlavePort
)
715 if slavePortnum
is not None:
716 def openSlavePort(res
):
717 self
.slavePort
= strports
.service(slavePortnum
,
719 self
.slavePort
.setServiceParent(self
)
720 d
.addCallback(openSlavePort
)
721 log
.msg("BuildMaster listening on port %s" % slavePortnum
)
722 self
.slavePortnum
= slavePortnum
724 log
.msg("configuration update started")
726 self
.readConfig
= True
727 log
.msg("configuration update complete")
729 d
.addCallback(lambda res
: self
.botmaster
.maybeStartAllBuilds())
732 def loadConfig_Slaves(self
, new_slaves
):
733 # set up the Checker with the names and passwords of all valid bots
734 self
.checker
.users
= {} # violates abstraction, oh well
736 self
.checker
.addUser(s
.slavename
, s
.password
)
737 self
.checker
.addUser("change", "changepw")
738 # let the BotMaster take care of the rest
739 return self
.botmaster
.loadConfig_Slaves(new_slaves
)
741 def loadConfig_Sources(self
, sources
):
743 log
.msg("warning: no ChangeSources specified in c['change_source']")
744 # shut down any that were removed, start any that were added
745 deleted_sources
= [s
for s
in self
.change_svc
if s
not in sources
]
746 added_sources
= [s
for s
in sources
if s
not in self
.change_svc
]
747 dl
= [self
.change_svc
.removeSource(s
) for s
in deleted_sources
]
749 [self
.change_svc
.addSource(s
) for s
in added_sources
]
750 d
= defer
.DeferredList(dl
, fireOnOneErrback
=1, consumeErrors
=0)
751 d
.addCallback(addNewOnes
)
754 def allSchedulers(self
):
755 return [child
for child
in self
756 if interfaces
.IScheduler
.providedBy(child
)]
759 def loadConfig_Schedulers(self
, newschedulers
):
760 oldschedulers
= self
.allSchedulers()
761 removed
= [s
for s
in oldschedulers
if s
not in newschedulers
]
762 added
= [s
for s
in newschedulers
if s
not in oldschedulers
]
763 dl
= [defer
.maybeDeferred(s
.disownServiceParent
) for s
in removed
]
766 s
.setServiceParent(self
)
767 d
= defer
.DeferredList(dl
, fireOnOneErrback
=1)
768 d
.addCallback(addNewOnes
)
771 def loadConfig_Builders(self
, newBuilderData
):
772 somethingChanged
= False
775 allBuilders
= self
.botmaster
.builders
.copy()
776 for data
in newBuilderData
:
779 newBuilderNames
.append(name
)
781 # identify all that were removed
782 for oldname
in self
.botmaster
.getBuildernames():
783 if oldname
not in newList
:
784 log
.msg("removing old builder %s" % oldname
)
785 del allBuilders
[oldname
]
786 somethingChanged
= True
787 # announce the change
788 self
.status
.builderRemoved(oldname
)
790 # everything in newList is either unchanged, changed, or new
791 for name
, data
in newList
.items():
792 old
= self
.botmaster
.builders
.get(name
)
793 basedir
= data
['builddir'] # used on both master and slave
794 #name, slave, builddir, factory = data
796 # category added after 0.6.2
797 category
= data
.get('category', None)
798 log
.msg("adding new builder %s for category %s" %
800 statusbag
= self
.status
.builderAdded(name
, basedir
, category
)
801 builder
= Builder(data
, statusbag
)
802 allBuilders
[name
] = builder
803 somethingChanged
= True
804 elif old
.compareToSetup(data
):
805 # changed: try to minimize the disruption and only modify the
806 # pieces that really changed
807 diffs
= old
.compareToSetup(data
)
808 log
.msg("updating builder %s: %s" % (name
, "\n".join(diffs
)))
810 statusbag
= old
.builder_status
811 statusbag
.saveYourself() # seems like a good idea
812 # TODO: if the basedir was changed, we probably need to make
814 new_builder
= Builder(data
, statusbag
)
815 new_builder
.consumeTheSoulOfYourPredecessor(old
)
816 # that migrates any retained slavebuilders too
818 # point out that the builder was updated. On the Waterfall,
819 # this will appear just after any currently-running builds.
820 statusbag
.addPointEvent(["config", "updated"])
822 allBuilders
[name
] = new_builder
823 somethingChanged
= True
825 # unchanged: leave it alone
826 log
.msg("builder %s is unchanged" % name
)
830 sortedAllBuilders
= [allBuilders
[name
] for name
in newBuilderNames
]
831 d
= self
.botmaster
.setBuilders(sortedAllBuilders
)
835 def loadConfig_status(self
, status
):
839 for s
in self
.statusTargets
[:]:
841 log
.msg("removing IStatusReceiver", s
)
842 d
= defer
.maybeDeferred(s
.disownServiceParent
)
844 self
.statusTargets
.remove(s
)
845 # after those are finished going away, add new ones
848 if not s
in self
.statusTargets
:
849 log
.msg("adding IStatusReceiver", s
)
850 s
.setServiceParent(self
)
851 self
.statusTargets
.append(s
)
852 d
= defer
.DeferredList(dl
, fireOnOneErrback
=1)
853 d
.addCallback(addNewOnes
)
857 def addChange(self
, change
):
858 for s
in self
.allSchedulers():
861 def submitBuildSet(self
, bs
):
862 # determine the set of Builders to use
864 for name
in bs
.builderNames
:
865 b
= self
.botmaster
.builders
.get(name
)
867 if b
not in builders
:
870 # TODO: add aliases like 'all'
871 raise KeyError("no such builder named '%s'" % name
)
873 # now tell the BuildSet to create BuildRequests for all those
874 # Builders and submit them
876 self
.status
.buildsetSubmitted(bs
.status
)
880 implements(interfaces
.IControl
)
882 def __init__(self
, master
):
885 def addChange(self
, change
):
886 self
.master
.change_svc
.addChange(change
)
888 def submitBuildSet(self
, bs
):
889 self
.master
.submitBuildSet(bs
)
891 def getBuilder(self
, name
):
892 b
= self
.master
.botmaster
.builders
[name
]
893 return interfaces
.IBuilderControl(b
)
895 components
.registerAdapter(Control
, BuildMaster
, interfaces
.IControl
)
897 # so anybody who can get a handle on the BuildMaster can cause a build with:
898 # IControl(master).getBuilder("full-2.3").requestBuild(buildrequest)