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
22 from buildbot
.util
import now
23 from buildbot
.pbutil
import NewCredPerspective
24 from buildbot
.process
.builder
import Builder
, IDLE
25 from buildbot
.process
.base
import BuildRequest
26 from buildbot
.status
.builder
import Status
27 from buildbot
.changes
.changes
import Change
, ChangeMaster
28 from buildbot
.sourcestamp
import SourceStamp
29 from buildbot
.buildslave
import BuildSlave
30 from buildbot
import interfaces
31 from buildbot
.process
.properties
import Properties
33 ########################################
35 class BotMaster(service
.MultiService
):
37 """This is the master-side service which manages remote buildbot slaves.
38 It provides them with BuildSlaves, and distributes file change
39 notification messages to them.
45 service
.MultiService
.__init
__(self
)
47 self
.builderNames
= []
48 # builders maps Builder names to instances of bb.p.builder.Builder,
49 # which is the master-side object that defines and controls a build.
50 # They are added by calling botmaster.addBuilder() from the startup
53 # self.slaves contains a ready BuildSlave instance for each
54 # potential buildslave, i.e. all the ones listed in the config file.
55 # If the slave is connected, self.slaves[slavename].slave will
56 # contain a RemoteReference to their Bot instance. If it is not
57 # connected, that attribute will hold None.
58 self
.slaves
= {} # maps slavename to BuildSlave
59 self
.statusClientService
= None
62 # self.locks holds the real Lock instances
65 # these four are convenience functions for testing
67 def waitUntilBuilderAttached(self
, name
):
68 b
= self
.builders
[name
]
70 # return defer.succeed(None)
72 b
.watchers
['attach'].append(d
)
75 def waitUntilBuilderDetached(self
, name
):
76 b
= self
.builders
.get(name
)
77 if not b
or not b
.slaves
:
78 return defer
.succeed(None)
80 b
.watchers
['detach'].append(d
)
83 def waitUntilBuilderFullyDetached(self
, name
):
84 b
= self
.builders
.get(name
)
85 # TODO: this looks too deeply inside the Builder object
86 if not b
or not b
.slaves
:
87 return defer
.succeed(None)
89 b
.watchers
['detach_all'].append(d
)
92 def waitUntilBuilderIdle(self
, name
):
93 b
= self
.builders
[name
]
94 # TODO: this looks way too deeply inside the Builder object
98 b
.watchers
['idle'].append(d
)
100 return defer
.succeed(None)
102 def loadConfig_Slaves(self
, new_slaves
):
103 old_slaves
= [c
for c
in list(self
)
104 if interfaces
.IBuildSlave
.providedBy(c
)]
106 # identify added/removed slaves. For each slave we construct a tuple
107 # of (name, password, class), and we consider the slave to be already
108 # present if the tuples match. (we include the class to make sure
109 # that BuildSlave(name,pw) is different than
110 # SubclassOfBuildSlave(name,pw) ). If the password or class has
111 # changed, we will remove the old version of the slave and replace it
112 # with a new one. If anything else has changed, we just update the
113 # old BuildSlave instance in place. If the name has changed, of
114 # course, it looks exactly the same as deleting one slave and adding
118 old_t
[(s
.slavename
, s
.password
, s
.__class
__)] = s
121 new_t
[(s
.slavename
, s
.password
, s
.__class
__)] = s
131 # removeSlave will hang up on the old bot
134 dl
.append(self
.removeSlave(s
))
135 d
= defer
.DeferredList(dl
, fireOnOneErrback
=True)
139 for t
in remaining_t
:
140 old_t
[t
].update(new_t
[t
])
144 def addSlave(self
, s
):
145 s
.setServiceParent(self
)
147 self
.slaves
[s
.slavename
] = s
149 def removeSlave(self
, s
):
150 # TODO: technically, disownServiceParent could return a Deferred
151 s
.disownServiceParent()
152 d
= self
.slaves
[s
.slavename
].disconnect()
153 del self
.slaves
[s
.slavename
]
156 def slaveLost(self
, bot
):
157 for name
, b
in self
.builders
.items():
158 if bot
.slavename
in b
.slavenames
:
161 def getBuildersForSlave(self
, slavename
):
163 for b
in self
.builders
.values()
164 if slavename
in b
.slavenames
]
166 def getBuildernames(self
):
167 return self
.builderNames
169 def getBuilders(self
):
170 allBuilders
= [self
.builders
[name
] for name
in self
.builderNames
]
173 def setBuilders(self
, builders
):
175 self
.builderNames
= []
177 for slavename
in b
.slavenames
:
178 # this is actually validated earlier
179 assert slavename
in self
.slaves
180 self
.builders
[b
.name
] = b
181 self
.builderNames
.append(b
.name
)
183 d
= self
._updateAllSlaves
()
186 def _updateAllSlaves(self
):
187 """Notify all buildslaves about changes in their Builders."""
188 dl
= [s
.updateSlave() for s
in self
.slaves
.values()]
189 return defer
.DeferredList(dl
)
191 def maybeStartAllBuilds(self
):
192 for b
in self
.builders
.values():
195 def getPerspective(self
, slavename
):
196 return self
.slaves
[slavename
]
198 def shutdownSlaves(self
):
199 # TODO: make this into a bot method rather than a builder method
200 for b
in self
.slaves
.values():
203 def stopService(self
):
204 for b
in self
.builders
.values():
205 b
.builder_status
.addPointEvent(["master", "shutdown"])
206 b
.builder_status
.saveYourself()
207 return service
.Service
.stopService(self
)
209 def getLockByID(self
, lockid
):
210 """Convert a Lock identifier into an actual Lock instance.
211 @param lockid: a locks.MasterLock or locks.SlaveLock instance
212 @return: a locks.RealMasterLock or locks.RealSlaveLock instance
214 if not lockid
in self
.locks
:
215 self
.locks
[lockid
] = lockid
.lockClass(lockid
)
216 # if the master.cfg file has changed maxCount= on the lock, the next
217 # time a build is started, they'll get a new RealLock instance. Note
218 # that this requires that MasterLock and SlaveLock (marker) instances
219 # be hashable and that they should compare properly.
220 return self
.locks
[lockid
]
222 ########################################
226 class DebugPerspective(NewCredPerspective
):
227 def attached(self
, mind
):
229 def detached(self
, mind
):
232 def perspective_requestBuild(self
, buildername
, reason
, branch
, revision
, properties
={}):
233 c
= interfaces
.IControl(self
.master
)
234 bc
= c
.getBuilder(buildername
)
235 ss
= SourceStamp(branch
, revision
)
237 bpr
.update(properties
, "remote requestBuild")
238 br
= BuildRequest(reason
, ss
, builderName
=buildername
, properties
=bpr
)
241 def perspective_pingBuilder(self
, buildername
):
242 c
= interfaces
.IControl(self
.master
)
243 bc
= c
.getBuilder(buildername
)
246 def perspective_fakeChange(self
, file, revision
=None, who
="fakeUser",
248 change
= Change(who
, [file], "some fake comments\n",
249 branch
=branch
, revision
=revision
)
250 c
= interfaces
.IControl(self
.master
)
253 def perspective_setCurrentState(self
, buildername
, state
):
254 builder
= self
.botmaster
.builders
.get(buildername
)
255 if not builder
: return
256 if state
== "offline":
257 builder
.statusbag
.currentlyOffline()
259 builder
.statusbag
.currentlyIdle()
260 if state
== "waiting":
261 builder
.statusbag
.currentlyWaiting(now()+10)
262 if state
== "building":
263 builder
.statusbag
.currentlyBuilding(None)
264 def perspective_reload(self
):
265 print "doing reload of the config file"
266 self
.master
.loadTheConfigFile()
267 def perspective_pokeIRC(self
):
268 print "saying something on IRC"
269 from buildbot
.status
import words
270 for s
in self
.master
:
271 if isinstance(s
, words
.IRC
):
273 for channel
in bot
.channels
:
274 print " channel", channel
275 bot
.p
.msg(channel
, "Ow, quit it")
277 def perspective_print(self
, msg
):
280 class Dispatcher(styles
.Versioned
):
281 implements(portal
.IRealm
)
282 persistenceVersion
= 2
287 def upgradeToVersion1(self
):
288 self
.master
= self
.botmaster
.parent
289 def upgradeToVersion2(self
):
292 def register(self
, name
, afactory
):
293 self
.names
[name
] = afactory
294 def unregister(self
, name
):
297 def requestAvatar(self
, avatarID
, mind
, interface
):
298 assert interface
== pb
.IPerspective
299 afactory
= self
.names
.get(avatarID
)
301 p
= afactory
.getPerspective()
302 elif avatarID
== "debug":
303 p
= DebugPerspective()
304 p
.master
= self
.master
305 p
.botmaster
= self
.botmaster
306 elif avatarID
== "statusClient":
307 p
= self
.statusClientService
.getPerspective()
309 # it must be one of the buildslaves: no other names will make it
311 p
= self
.botmaster
.getPerspective(avatarID
)
314 raise ValueError("no perspective for '%s'" % avatarID
)
316 d
= defer
.maybeDeferred(p
.attached
, mind
)
317 d
.addCallback(self
._avatarAttached
, mind
)
320 def _avatarAttached(self
, p
, mind
):
321 return (pb
.IPerspective
, p
, lambda p
=p
,mind
=mind
: p
.detached(mind
))
323 ########################################
329 # all IChangeSource objects
330 # StatusClientService
331 # TCPClient(self.ircFactory)
332 # TCPServer(self.slaveFactory) -> dispatcher.requestAvatar
333 # TCPServer(self.site)
334 # UNIXServer(ResourcePublisher(self.site))
337 class BuildMaster(service
.MultiService
, styles
.Versioned
):
339 persistenceVersion
= 3
342 projectName
= "(unspecified)"
346 properties
= Properties()
348 def __init__(self
, basedir
, configFileName
="master.cfg"):
349 service
.MultiService
.__init
__(self
)
350 self
.setName("buildmaster")
351 self
.basedir
= basedir
352 self
.configFileName
= configFileName
354 # the dispatcher is the realm in which all inbound connections are
355 # looked up: slave builders, change notifications, status clients, and
357 dispatcher
= Dispatcher()
358 dispatcher
.master
= self
359 self
.dispatcher
= dispatcher
360 self
.checker
= checkers
.InMemoryUsernamePasswordDatabaseDontUse()
361 # the checker starts with no user/passwd pairs: they are added later
362 p
= portal
.Portal(dispatcher
)
363 p
.registerChecker(self
.checker
)
364 self
.slaveFactory
= pb
.PBServerFactory(p
)
365 self
.slaveFactory
.unsafeTracebacks
= True # let them see exceptions
367 self
.slavePortnum
= None
368 self
.slavePort
= None
370 self
.botmaster
= BotMaster()
371 self
.botmaster
.setName("botmaster")
372 self
.botmaster
.setServiceParent(self
)
373 dispatcher
.botmaster
= self
.botmaster
375 self
.status
= Status(self
.botmaster
, self
.basedir
)
377 self
.statusTargets
= []
379 # this ChangeMaster is a dummy, only used by tests. In the real
380 # buildmaster, where the BuildMaster instance is activated
381 # (startService is called) by twistd, this attribute is overwritten.
382 self
.useChanges(ChangeMaster())
384 self
.readConfig
= False
386 def upgradeToVersion1(self
):
387 self
.dispatcher
= self
.slaveFactory
.root
.portal
.realm
389 def upgradeToVersion2(self
): # post-0.4.3
390 self
.webServer
= self
.webTCPPort
392 self
.webDistribServer
= self
.webUNIXPort
394 self
.configFileName
= "master.cfg"
396 def upgradeToVersion3(self
):
397 # post 0.6.3, solely to deal with the 0.6.3 breakage. Starting with
398 # 0.6.5 I intend to do away with .tap files altogether
400 self
.namedServices
= {}
403 def startService(self
):
404 service
.MultiService
.startService(self
)
405 self
.loadChanges() # must be done before loading the config file
406 if not self
.readConfig
:
407 # TODO: consider catching exceptions during this call to
408 # loadTheConfigFile and bailing (reactor.stop) if it fails,
409 # since without a config file we can't do anything except reload
410 # the config file, and it would be nice for the user to discover
412 self
.loadTheConfigFile()
413 if signal
and hasattr(signal
, "SIGHUP"):
414 signal
.signal(signal
.SIGHUP
, self
._handleSIGHUP
)
415 for b
in self
.botmaster
.builders
.values():
416 b
.builder_status
.addPointEvent(["master", "started"])
417 b
.builder_status
.saveYourself()
419 def useChanges(self
, changes
):
421 # TODO: can return a Deferred
422 self
.change_svc
.disownServiceParent()
423 self
.change_svc
= changes
424 self
.change_svc
.basedir
= self
.basedir
425 self
.change_svc
.setName("changemaster")
426 self
.dispatcher
.changemaster
= self
.change_svc
427 self
.change_svc
.setServiceParent(self
)
429 def loadChanges(self
):
430 filename
= os
.path
.join(self
.basedir
, "changes.pck")
432 changes
= load(open(filename
, "rb"))
435 log
.msg("changes.pck missing, using new one")
436 changes
= ChangeMaster()
438 log
.msg("corrupted changes.pck, using new one")
439 changes
= ChangeMaster()
440 self
.useChanges(changes
)
442 def _handleSIGHUP(self
, *args
):
443 reactor
.callLater(0, self
.loadTheConfigFile
)
447 @rtype: L{buildbot.status.builder.Status}
451 def loadTheConfigFile(self
, configFile
=None):
453 configFile
= os
.path
.join(self
.basedir
, self
.configFileName
)
455 log
.msg("Creating BuildMaster -- buildbot.version: %s" % buildbot
.version
)
456 log
.msg("loading configuration from %s" % configFile
)
457 configFile
= os
.path
.expanduser(configFile
)
460 f
= open(configFile
, "r")
462 log
.msg("unable to open config file '%s'" % configFile
)
463 log
.msg("leaving old configuration in place")
470 log
.msg("error during loadConfig")
472 log
.msg("The new config file is unusable, so I'll ignore it.")
473 log
.msg("I will keep using the previous config file instead.")
476 def loadConfig(self
, f
):
477 """Internal function to load a specific configuration file. Any
478 errors in the file will be signalled by raising an exception.
480 @return: a Deferred that will fire (with None) when the configuration
481 changes have been completed. This may involve a round-trip to each
482 buildslave that was involved."""
484 localDict
= {'basedir': os
.path
.expanduser(self
.basedir
)}
488 log
.msg("error while parsing config file")
492 config
= localDict
['BuildmasterConfig']
494 log
.err("missing config dictionary")
495 log
.err("config file must define BuildmasterConfig")
498 known_keys
= ("bots", "slaves",
499 "sources", "change_source",
500 "schedulers", "builders",
501 "slavePortnum", "debugPassword", "manhole",
502 "status", "projectName", "projectURL", "buildbotURL",
505 for k
in config
.keys():
506 if k
not in known_keys
:
507 log
.msg("unknown key '%s' defined in config dictionary" % k
)
511 schedulers
= config
['schedulers']
512 builders
= config
['builders']
514 if k
['name'].startswith("_"):
515 errmsg
= ("builder names must not start with an "
516 "underscore: " + k
['name'])
518 raise ValueError(errmsg
)
520 slavePortnum
= config
['slavePortnum']
521 #slaves = config['slaves']
522 #change_source = config['change_source']
525 debugPassword
= config
.get('debugPassword')
526 manhole
= config
.get('manhole')
527 status
= config
.get('status', [])
528 projectName
= config
.get('projectName')
529 projectURL
= config
.get('projectURL')
530 buildbotURL
= config
.get('buildbotURL')
531 properties
= config
.get('properties', {})
534 log
.msg("config dictionary is missing a required parameter")
535 log
.msg("leaving old configuration in place")
538 #if "bots" in config:
539 # raise KeyError("c['bots'] is no longer accepted")
541 slaves
= config
.get('slaves', [])
543 m
= ("c['bots'] is deprecated as of 0.7.6 and will be "
544 "removed by 0.8.0 . Please use c['slaves'] instead.")
546 warnings
.warn(m
, DeprecationWarning)
547 for name
, passwd
in config
['bots']:
548 slaves
.append(BuildSlave(name
, passwd
))
550 if "bots" not in config
and "slaves" not in config
:
551 log
.msg("config dictionary must have either 'bots' or 'slaves'")
552 log
.msg("leaving old configuration in place")
553 raise KeyError("must have either 'bots' or 'slaves'")
555 #if "sources" in config:
556 # raise KeyError("c['sources'] is no longer accepted")
558 change_source
= config
.get('change_source', [])
559 if isinstance(change_source
, (list, tuple)):
560 change_sources
= change_source
562 change_sources
= [change_source
]
563 if "sources" in config
:
564 m
= ("c['sources'] is deprecated as of 0.7.6 and will be "
565 "removed by 0.8.0 . Please use c['change_source'] instead.")
567 warnings
.warn(m
, DeprecationWarning)
568 for s
in config
['sources']:
569 change_sources
.append(s
)
571 # do some validation first
573 assert isinstance(s
, BuildSlave
)
574 if s
.slavename
in ("debug", "change", "status"):
575 raise KeyError, "reserved name '%s' used for a bot" % s
.slavename
576 if config
.has_key('interlocks'):
577 raise KeyError("c['interlocks'] is no longer accepted")
579 assert isinstance(change_sources
, (list, tuple))
580 for s
in change_sources
:
581 assert interfaces
.IChangeSource(s
, None)
582 # this assertion catches c['schedulers'] = Scheduler(), since
583 # Schedulers are service.MultiServices and thus iterable.
584 errmsg
= "c['schedulers'] must be a list of Scheduler instances"
585 assert isinstance(schedulers
, (list, tuple)), errmsg
587 assert interfaces
.IScheduler(s
, None), errmsg
588 assert isinstance(status
, (list, tuple))
590 assert interfaces
.IStatusReceiver(s
, None)
592 slavenames
= [s
.slavename
for s
in slaves
]
597 raise ValueError("builder %s must be defined with a dict, "
598 "not a tuple" % b
[0])
599 if b
.has_key('slavename') and b
['slavename'] not in slavenames
:
600 raise ValueError("builder %s uses undefined slave %s" \
601 % (b
['name'], b
['slavename']))
602 for n
in b
.get('slavenames', []):
603 if n
not in slavenames
:
604 raise ValueError("builder %s uses undefined slave %s" \
606 if b
['name'] in buildernames
:
607 raise ValueError("duplicate builder name %s"
609 buildernames
.append(b
['name'])
610 if b
['builddir'] in dirnames
:
611 raise ValueError("builder %s reuses builddir %s"
612 % (b
['name'], b
['builddir']))
613 dirnames
.append(b
['builddir'])
615 unscheduled_buildernames
= buildernames
[:]
618 for b
in s
.listBuilderNames():
619 assert b
in buildernames
, \
620 "%s uses unknown builder %s" % (s
, b
)
621 if b
in unscheduled_buildernames
:
622 unscheduled_buildernames
.remove(b
)
624 if s
.name
in schedulernames
:
625 # TODO: schedulers share a namespace with other Service
626 # children of the BuildMaster node, like status plugins, the
627 # Manhole, the ChangeMaster, and the BotMaster (although most
628 # of these don't have names)
629 msg
= ("Schedulers must have unique names, but "
630 "'%s' was a duplicate" % (s
.name
,))
631 raise ValueError(msg
)
632 schedulernames
.append(s
.name
)
634 if unscheduled_buildernames
:
635 log
.msg("Warning: some Builders have no Schedulers to drive them:"
636 " %s" % (unscheduled_buildernames
,))
638 # assert that all locks used by the Builds and their Steps are
642 for l
in b
.get('locks', []):
643 if locks
.has_key(l
.name
):
644 if locks
[l
.name
] is not l
:
645 raise ValueError("Two different locks (%s and %s) "
647 % (l
, locks
[l
.name
], l
.name
))
650 # TODO: this will break with any BuildFactory that doesn't use a
651 # .steps list, but I think the verification step is more
653 for s
in b
['factory'].steps
:
654 for l
in s
[1].get('locks', []):
655 if locks
.has_key(l
.name
):
656 if locks
[l
.name
] is not l
:
657 raise ValueError("Two different locks (%s and %s)"
659 % (l
, locks
[l
.name
], l
.name
))
663 if not isinstance(properties
, dict):
664 raise ValueError("c['properties'] must be a dictionary")
666 # slavePortnum supposed to be a strports specification
667 if type(slavePortnum
) is int:
668 slavePortnum
= "tcp:%d" % slavePortnum
670 # now we're committed to implementing the new configuration, so do
672 # TODO: actually, this is spread across a couple of Deferreds, so it
673 # really isn't atomic.
675 d
= defer
.succeed(None)
677 self
.projectName
= projectName
678 self
.projectURL
= projectURL
679 self
.buildbotURL
= buildbotURL
681 self
.properties
= Properties()
682 self
.properties
.update(properties
, self
.configFileName
)
684 # self.slaves: Disconnect any that were attached and removed from the
685 # list. Update self.checker with the new list of passwords, including
686 # debug/change/status.
687 d
.addCallback(lambda res
: self
.loadConfig_Slaves(slaves
))
691 self
.checker
.addUser("debug", debugPassword
)
692 self
.debugPassword
= debugPassword
695 if manhole
!= self
.manhole
:
698 # disownServiceParent may return a Deferred
699 d
.addCallback(lambda res
: self
.manhole
.disownServiceParent())
703 d
.addCallback(_remove
)
706 self
.manhole
= manhole
707 manhole
.setServiceParent(self
)
710 # add/remove self.botmaster.builders to match builders. The
711 # botmaster will handle startup/shutdown issues.
712 d
.addCallback(lambda res
: self
.loadConfig_Builders(builders
))
714 d
.addCallback(lambda res
: self
.loadConfig_status(status
))
716 # Schedulers are added after Builders in case they start right away
717 d
.addCallback(lambda res
: self
.loadConfig_Schedulers(schedulers
))
718 # and Sources go after Schedulers for the same reason
719 d
.addCallback(lambda res
: self
.loadConfig_Sources(change_sources
))
722 if self
.slavePortnum
!= slavePortnum
:
724 def closeSlavePort(res
):
725 d1
= self
.slavePort
.disownServiceParent()
726 self
.slavePort
= None
728 d
.addCallback(closeSlavePort
)
729 if slavePortnum
is not None:
730 def openSlavePort(res
):
731 self
.slavePort
= strports
.service(slavePortnum
,
733 self
.slavePort
.setServiceParent(self
)
734 d
.addCallback(openSlavePort
)
735 log
.msg("BuildMaster listening on port %s" % slavePortnum
)
736 self
.slavePortnum
= slavePortnum
738 log
.msg("configuration update started")
740 self
.readConfig
= True
741 log
.msg("configuration update complete")
743 d
.addCallback(lambda res
: self
.botmaster
.maybeStartAllBuilds())
746 def loadConfig_Slaves(self
, new_slaves
):
747 # set up the Checker with the names and passwords of all valid bots
748 self
.checker
.users
= {} # violates abstraction, oh well
750 self
.checker
.addUser(s
.slavename
, s
.password
)
751 self
.checker
.addUser("change", "changepw")
752 # let the BotMaster take care of the rest
753 return self
.botmaster
.loadConfig_Slaves(new_slaves
)
755 def loadConfig_Sources(self
, sources
):
757 log
.msg("warning: no ChangeSources specified in c['change_source']")
758 # shut down any that were removed, start any that were added
759 deleted_sources
= [s
for s
in self
.change_svc
if s
not in sources
]
760 added_sources
= [s
for s
in sources
if s
not in self
.change_svc
]
761 dl
= [self
.change_svc
.removeSource(s
) for s
in deleted_sources
]
763 [self
.change_svc
.addSource(s
) for s
in added_sources
]
764 d
= defer
.DeferredList(dl
, fireOnOneErrback
=1, consumeErrors
=0)
765 d
.addCallback(addNewOnes
)
768 def allSchedulers(self
):
769 return [child
for child
in self
770 if interfaces
.IScheduler
.providedBy(child
)]
773 def loadConfig_Schedulers(self
, newschedulers
):
774 oldschedulers
= self
.allSchedulers()
775 removed
= [s
for s
in oldschedulers
if s
not in newschedulers
]
776 added
= [s
for s
in newschedulers
if s
not in oldschedulers
]
777 dl
= [defer
.maybeDeferred(s
.disownServiceParent
) for s
in removed
]
780 s
.setServiceParent(self
)
781 d
= defer
.DeferredList(dl
, fireOnOneErrback
=1)
782 d
.addCallback(addNewOnes
)
785 def loadConfig_Builders(self
, newBuilderData
):
786 somethingChanged
= False
789 allBuilders
= self
.botmaster
.builders
.copy()
790 for data
in newBuilderData
:
793 newBuilderNames
.append(name
)
795 # identify all that were removed
796 for oldname
in self
.botmaster
.getBuildernames():
797 if oldname
not in newList
:
798 log
.msg("removing old builder %s" % oldname
)
799 del allBuilders
[oldname
]
800 somethingChanged
= True
801 # announce the change
802 self
.status
.builderRemoved(oldname
)
804 # everything in newList is either unchanged, changed, or new
805 for name
, data
in newList
.items():
806 old
= self
.botmaster
.builders
.get(name
)
807 basedir
= data
['builddir'] # used on both master and slave
808 #name, slave, builddir, factory = data
810 # category added after 0.6.2
811 category
= data
.get('category', None)
812 log
.msg("adding new builder %s for category %s" %
814 statusbag
= self
.status
.builderAdded(name
, basedir
, category
)
815 builder
= Builder(data
, statusbag
)
816 allBuilders
[name
] = builder
817 somethingChanged
= True
818 elif old
.compareToSetup(data
):
819 # changed: try to minimize the disruption and only modify the
820 # pieces that really changed
821 diffs
= old
.compareToSetup(data
)
822 log
.msg("updating builder %s: %s" % (name
, "\n".join(diffs
)))
824 statusbag
= old
.builder_status
825 statusbag
.saveYourself() # seems like a good idea
826 # TODO: if the basedir was changed, we probably need to make
828 new_builder
= Builder(data
, statusbag
)
829 new_builder
.consumeTheSoulOfYourPredecessor(old
)
830 # that migrates any retained slavebuilders too
832 # point out that the builder was updated. On the Waterfall,
833 # this will appear just after any currently-running builds.
834 statusbag
.addPointEvent(["config", "updated"])
836 allBuilders
[name
] = new_builder
837 somethingChanged
= True
839 # unchanged: leave it alone
840 log
.msg("builder %s is unchanged" % name
)
844 sortedAllBuilders
= [allBuilders
[name
] for name
in newBuilderNames
]
845 d
= self
.botmaster
.setBuilders(sortedAllBuilders
)
849 def loadConfig_status(self
, status
):
853 for s
in self
.statusTargets
[:]:
855 log
.msg("removing IStatusReceiver", s
)
856 d
= defer
.maybeDeferred(s
.disownServiceParent
)
858 self
.statusTargets
.remove(s
)
859 # after those are finished going away, add new ones
862 if not s
in self
.statusTargets
:
863 log
.msg("adding IStatusReceiver", s
)
864 s
.setServiceParent(self
)
865 self
.statusTargets
.append(s
)
866 d
= defer
.DeferredList(dl
, fireOnOneErrback
=1)
867 d
.addCallback(addNewOnes
)
871 def addChange(self
, change
):
872 for s
in self
.allSchedulers():
875 def submitBuildSet(self
, bs
):
876 # determine the set of Builders to use
878 for name
in bs
.builderNames
:
879 b
= self
.botmaster
.builders
.get(name
)
881 if b
not in builders
:
884 # TODO: add aliases like 'all'
885 raise KeyError("no such builder named '%s'" % name
)
887 # now tell the BuildSet to create BuildRequests for all those
888 # Builders and submit them
890 self
.status
.buildsetSubmitted(bs
.status
)
894 implements(interfaces
.IControl
)
896 def __init__(self
, master
):
899 def addChange(self
, change
):
900 self
.master
.change_svc
.addChange(change
)
902 def submitBuildSet(self
, bs
):
903 self
.master
.submitBuildSet(bs
)
905 def getBuilder(self
, name
):
906 b
= self
.master
.botmaster
.builders
[name
]
907 return interfaces
.IBuilderControl(b
)
909 components
.registerAdapter(Control
, BuildMaster
, interfaces
.IControl
)
911 # so anybody who can get a handle on the BuildMaster can cause a build with:
912 # IControl(master).getBuilder("full-2.3").requestBuild(buildrequest)