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
, locks
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 assert isinstance(lockid
, (locks
.MasterLock
, locks
.SlaveLock
))
215 if not lockid
in self
.locks
:
216 self
.locks
[lockid
] = lockid
.lockClass(lockid
)
217 # if the master.cfg file has changed maxCount= on the lock, the next
218 # time a build is started, they'll get a new RealLock instance. Note
219 # that this requires that MasterLock and SlaveLock (marker) instances
220 # be hashable and that they should compare properly.
221 return self
.locks
[lockid
]
223 ########################################
227 class DebugPerspective(NewCredPerspective
):
228 def attached(self
, mind
):
230 def detached(self
, mind
):
233 def perspective_requestBuild(self
, buildername
, reason
, branch
, revision
, properties
={}):
234 c
= interfaces
.IControl(self
.master
)
235 bc
= c
.getBuilder(buildername
)
236 ss
= SourceStamp(branch
, revision
)
238 bpr
.update(properties
, "remote requestBuild")
239 br
= BuildRequest(reason
, ss
, builderName
=buildername
, properties
=bpr
)
242 def perspective_pingBuilder(self
, buildername
):
243 c
= interfaces
.IControl(self
.master
)
244 bc
= c
.getBuilder(buildername
)
247 def perspective_fakeChange(self
, file, revision
=None, who
="fakeUser",
249 change
= Change(who
, [file], "some fake comments\n",
250 branch
=branch
, revision
=revision
)
251 c
= interfaces
.IControl(self
.master
)
254 def perspective_setCurrentState(self
, buildername
, state
):
255 builder
= self
.botmaster
.builders
.get(buildername
)
256 if not builder
: return
257 if state
== "offline":
258 builder
.statusbag
.currentlyOffline()
260 builder
.statusbag
.currentlyIdle()
261 if state
== "waiting":
262 builder
.statusbag
.currentlyWaiting(now()+10)
263 if state
== "building":
264 builder
.statusbag
.currentlyBuilding(None)
265 def perspective_reload(self
):
266 print "doing reload of the config file"
267 self
.master
.loadTheConfigFile()
268 def perspective_pokeIRC(self
):
269 print "saying something on IRC"
270 from buildbot
.status
import words
271 for s
in self
.master
:
272 if isinstance(s
, words
.IRC
):
274 for channel
in bot
.channels
:
275 print " channel", channel
276 bot
.p
.msg(channel
, "Ow, quit it")
278 def perspective_print(self
, msg
):
281 class Dispatcher(styles
.Versioned
):
282 implements(portal
.IRealm
)
283 persistenceVersion
= 2
288 def upgradeToVersion1(self
):
289 self
.master
= self
.botmaster
.parent
290 def upgradeToVersion2(self
):
293 def register(self
, name
, afactory
):
294 self
.names
[name
] = afactory
295 def unregister(self
, name
):
298 def requestAvatar(self
, avatarID
, mind
, interface
):
299 assert interface
== pb
.IPerspective
300 afactory
= self
.names
.get(avatarID
)
302 p
= afactory
.getPerspective()
303 elif avatarID
== "debug":
304 p
= DebugPerspective()
305 p
.master
= self
.master
306 p
.botmaster
= self
.botmaster
307 elif avatarID
== "statusClient":
308 p
= self
.statusClientService
.getPerspective()
310 # it must be one of the buildslaves: no other names will make it
312 p
= self
.botmaster
.getPerspective(avatarID
)
315 raise ValueError("no perspective for '%s'" % avatarID
)
317 d
= defer
.maybeDeferred(p
.attached
, mind
)
318 d
.addCallback(self
._avatarAttached
, mind
)
321 def _avatarAttached(self
, p
, mind
):
322 return (pb
.IPerspective
, p
, lambda p
=p
,mind
=mind
: p
.detached(mind
))
324 ########################################
330 # all IChangeSource objects
331 # StatusClientService
332 # TCPClient(self.ircFactory)
333 # TCPServer(self.slaveFactory) -> dispatcher.requestAvatar
334 # TCPServer(self.site)
335 # UNIXServer(ResourcePublisher(self.site))
338 class BuildMaster(service
.MultiService
, styles
.Versioned
):
340 persistenceVersion
= 3
343 projectName
= "(unspecified)"
347 properties
= Properties()
349 def __init__(self
, basedir
, configFileName
="master.cfg"):
350 service
.MultiService
.__init
__(self
)
351 self
.setName("buildmaster")
352 self
.basedir
= basedir
353 self
.configFileName
= configFileName
355 # the dispatcher is the realm in which all inbound connections are
356 # looked up: slave builders, change notifications, status clients, and
358 dispatcher
= Dispatcher()
359 dispatcher
.master
= self
360 self
.dispatcher
= dispatcher
361 self
.checker
= checkers
.InMemoryUsernamePasswordDatabaseDontUse()
362 # the checker starts with no user/passwd pairs: they are added later
363 p
= portal
.Portal(dispatcher
)
364 p
.registerChecker(self
.checker
)
365 self
.slaveFactory
= pb
.PBServerFactory(p
)
366 self
.slaveFactory
.unsafeTracebacks
= True # let them see exceptions
368 self
.slavePortnum
= None
369 self
.slavePort
= None
371 self
.botmaster
= BotMaster()
372 self
.botmaster
.setName("botmaster")
373 self
.botmaster
.setServiceParent(self
)
374 dispatcher
.botmaster
= self
.botmaster
376 self
.status
= Status(self
.botmaster
, self
.basedir
)
378 self
.statusTargets
= []
380 # this ChangeMaster is a dummy, only used by tests. In the real
381 # buildmaster, where the BuildMaster instance is activated
382 # (startService is called) by twistd, this attribute is overwritten.
383 self
.useChanges(ChangeMaster())
385 self
.readConfig
= False
387 def upgradeToVersion1(self
):
388 self
.dispatcher
= self
.slaveFactory
.root
.portal
.realm
390 def upgradeToVersion2(self
): # post-0.4.3
391 self
.webServer
= self
.webTCPPort
393 self
.webDistribServer
= self
.webUNIXPort
395 self
.configFileName
= "master.cfg"
397 def upgradeToVersion3(self
):
398 # post 0.6.3, solely to deal with the 0.6.3 breakage. Starting with
399 # 0.6.5 I intend to do away with .tap files altogether
401 self
.namedServices
= {}
404 def startService(self
):
405 service
.MultiService
.startService(self
)
406 self
.loadChanges() # must be done before loading the config file
407 if not self
.readConfig
:
408 # TODO: consider catching exceptions during this call to
409 # loadTheConfigFile and bailing (reactor.stop) if it fails,
410 # since without a config file we can't do anything except reload
411 # the config file, and it would be nice for the user to discover
413 self
.loadTheConfigFile()
414 if signal
and hasattr(signal
, "SIGHUP"):
415 signal
.signal(signal
.SIGHUP
, self
._handleSIGHUP
)
416 for b
in self
.botmaster
.builders
.values():
417 b
.builder_status
.addPointEvent(["master", "started"])
418 b
.builder_status
.saveYourself()
420 def useChanges(self
, changes
):
422 # TODO: can return a Deferred
423 self
.change_svc
.disownServiceParent()
424 self
.change_svc
= changes
425 self
.change_svc
.basedir
= self
.basedir
426 self
.change_svc
.setName("changemaster")
427 self
.dispatcher
.changemaster
= self
.change_svc
428 self
.change_svc
.setServiceParent(self
)
430 def loadChanges(self
):
431 filename
= os
.path
.join(self
.basedir
, "changes.pck")
433 changes
= load(open(filename
, "rb"))
436 log
.msg("changes.pck missing, using new one")
437 changes
= ChangeMaster()
439 log
.msg("corrupted changes.pck, using new one")
440 changes
= ChangeMaster()
441 self
.useChanges(changes
)
443 def _handleSIGHUP(self
, *args
):
444 reactor
.callLater(0, self
.loadTheConfigFile
)
448 @rtype: L{buildbot.status.builder.Status}
452 def loadTheConfigFile(self
, configFile
=None):
454 configFile
= os
.path
.join(self
.basedir
, self
.configFileName
)
456 log
.msg("Creating BuildMaster -- buildbot.version: %s" % buildbot
.version
)
457 log
.msg("loading configuration from %s" % configFile
)
458 configFile
= os
.path
.expanduser(configFile
)
461 f
= open(configFile
, "r")
463 log
.msg("unable to open config file '%s'" % configFile
)
464 log
.msg("leaving old configuration in place")
471 log
.msg("error during loadConfig")
473 log
.msg("The new config file is unusable, so I'll ignore it.")
474 log
.msg("I will keep using the previous config file instead.")
477 def loadConfig(self
, f
):
478 """Internal function to load a specific configuration file. Any
479 errors in the file will be signalled by raising an exception.
481 @return: a Deferred that will fire (with None) when the configuration
482 changes have been completed. This may involve a round-trip to each
483 buildslave that was involved."""
485 localDict
= {'basedir': os
.path
.expanduser(self
.basedir
)}
489 log
.msg("error while parsing config file")
493 config
= localDict
['BuildmasterConfig']
495 log
.err("missing config dictionary")
496 log
.err("config file must define BuildmasterConfig")
499 known_keys
= ("bots", "slaves",
500 "sources", "change_source",
501 "schedulers", "builders",
502 "slavePortnum", "debugPassword", "manhole",
503 "status", "projectName", "projectURL", "buildbotURL",
506 for k
in config
.keys():
507 if k
not in known_keys
:
508 log
.msg("unknown key '%s' defined in config dictionary" % k
)
512 schedulers
= config
['schedulers']
513 builders
= config
['builders']
515 if k
['name'].startswith("_"):
516 errmsg
= ("builder names must not start with an "
517 "underscore: " + k
['name'])
519 raise ValueError(errmsg
)
521 slavePortnum
= config
['slavePortnum']
522 #slaves = config['slaves']
523 #change_source = config['change_source']
526 debugPassword
= config
.get('debugPassword')
527 manhole
= config
.get('manhole')
528 status
= config
.get('status', [])
529 projectName
= config
.get('projectName')
530 projectURL
= config
.get('projectURL')
531 buildbotURL
= config
.get('buildbotURL')
532 properties
= config
.get('properties', {})
535 log
.msg("config dictionary is missing a required parameter")
536 log
.msg("leaving old configuration in place")
539 #if "bots" in config:
540 # raise KeyError("c['bots'] is no longer accepted")
542 slaves
= config
.get('slaves', [])
544 m
= ("c['bots'] is deprecated as of 0.7.6 and will be "
545 "removed by 0.8.0 . Please use c['slaves'] instead.")
547 warnings
.warn(m
, DeprecationWarning)
548 for name
, passwd
in config
['bots']:
549 slaves
.append(BuildSlave(name
, passwd
))
551 if "bots" not in config
and "slaves" not in config
:
552 log
.msg("config dictionary must have either 'bots' or 'slaves'")
553 log
.msg("leaving old configuration in place")
554 raise KeyError("must have either 'bots' or 'slaves'")
556 #if "sources" in config:
557 # raise KeyError("c['sources'] is no longer accepted")
559 change_source
= config
.get('change_source', [])
560 if isinstance(change_source
, (list, tuple)):
561 change_sources
= change_source
563 change_sources
= [change_source
]
564 if "sources" in config
:
565 m
= ("c['sources'] is deprecated as of 0.7.6 and will be "
566 "removed by 0.8.0 . Please use c['change_source'] instead.")
568 warnings
.warn(m
, DeprecationWarning)
569 for s
in config
['sources']:
570 change_sources
.append(s
)
572 # do some validation first
574 assert isinstance(s
, BuildSlave
)
575 if s
.slavename
in ("debug", "change", "status"):
576 raise KeyError, "reserved name '%s' used for a bot" % s
.slavename
577 if config
.has_key('interlocks'):
578 raise KeyError("c['interlocks'] is no longer accepted")
580 assert isinstance(change_sources
, (list, tuple))
581 for s
in change_sources
:
582 assert interfaces
.IChangeSource(s
, None)
583 # this assertion catches c['schedulers'] = Scheduler(), since
584 # Schedulers are service.MultiServices and thus iterable.
585 errmsg
= "c['schedulers'] must be a list of Scheduler instances"
586 assert isinstance(schedulers
, (list, tuple)), errmsg
588 assert interfaces
.IScheduler(s
, None), errmsg
589 assert isinstance(status
, (list, tuple))
591 assert interfaces
.IStatusReceiver(s
, None)
593 slavenames
= [s
.slavename
for s
in slaves
]
598 raise ValueError("builder %s must be defined with a dict, "
599 "not a tuple" % b
[0])
600 if b
.has_key('slavename') and b
['slavename'] not in slavenames
:
601 raise ValueError("builder %s uses undefined slave %s" \
602 % (b
['name'], b
['slavename']))
603 for n
in b
.get('slavenames', []):
604 if n
not in slavenames
:
605 raise ValueError("builder %s uses undefined slave %s" \
607 if b
['name'] in buildernames
:
608 raise ValueError("duplicate builder name %s"
610 buildernames
.append(b
['name'])
611 if b
['builddir'] in dirnames
:
612 raise ValueError("builder %s reuses builddir %s"
613 % (b
['name'], b
['builddir']))
614 dirnames
.append(b
['builddir'])
616 unscheduled_buildernames
= buildernames
[:]
619 for b
in s
.listBuilderNames():
620 assert b
in buildernames
, \
621 "%s uses unknown builder %s" % (s
, b
)
622 if b
in unscheduled_buildernames
:
623 unscheduled_buildernames
.remove(b
)
625 if s
.name
in schedulernames
:
626 # TODO: schedulers share a namespace with other Service
627 # children of the BuildMaster node, like status plugins, the
628 # Manhole, the ChangeMaster, and the BotMaster (although most
629 # of these don't have names)
630 msg
= ("Schedulers must have unique names, but "
631 "'%s' was a duplicate" % (s
.name
,))
632 raise ValueError(msg
)
633 schedulernames
.append(s
.name
)
635 if unscheduled_buildernames
:
636 log
.msg("Warning: some Builders have no Schedulers to drive them:"
637 " %s" % (unscheduled_buildernames
,))
639 # assert that all locks used by the Builds and their Steps are
643 for l
in b
.get('locks', []):
644 if isinstance(l
, locks
.LockAccess
): # User specified access to the lock
646 if lock_dict
.has_key(l
.name
):
647 if lock_dict
[l
.name
] is not l
:
648 raise ValueError("Two different locks (%s and %s) "
650 % (l
, lock_dict
[l
.name
], l
.name
))
652 lock_dict
[l
.name
] = l
653 # TODO: this will break with any BuildFactory that doesn't use a
654 # .steps list, but I think the verification step is more
656 for s
in b
['factory'].steps
:
657 for l
in s
[1].get('locks', []):
658 if isinstance(l
, locks
.LockAccess
): # User specified access to the lock
660 if lock_dict
.has_key(l
.name
):
661 if lock_dict
[l
.name
] is not l
:
662 raise ValueError("Two different locks (%s and %s)"
664 % (l
, lock_dict
[l
.name
], l
.name
))
666 lock_dict
[l
.name
] = l
668 if not isinstance(properties
, dict):
669 raise ValueError("c['properties'] must be a dictionary")
671 # slavePortnum supposed to be a strports specification
672 if type(slavePortnum
) is int:
673 slavePortnum
= "tcp:%d" % slavePortnum
675 # now we're committed to implementing the new configuration, so do
677 # TODO: actually, this is spread across a couple of Deferreds, so it
678 # really isn't atomic.
680 d
= defer
.succeed(None)
682 self
.projectName
= projectName
683 self
.projectURL
= projectURL
684 self
.buildbotURL
= buildbotURL
686 self
.properties
= Properties()
687 self
.properties
.update(properties
, self
.configFileName
)
689 # self.slaves: Disconnect any that were attached and removed from the
690 # list. Update self.checker with the new list of passwords, including
691 # debug/change/status.
692 d
.addCallback(lambda res
: self
.loadConfig_Slaves(slaves
))
696 self
.checker
.addUser("debug", debugPassword
)
697 self
.debugPassword
= debugPassword
700 if manhole
!= self
.manhole
:
703 # disownServiceParent may return a Deferred
704 d
.addCallback(lambda res
: self
.manhole
.disownServiceParent())
708 d
.addCallback(_remove
)
711 self
.manhole
= manhole
712 manhole
.setServiceParent(self
)
715 # add/remove self.botmaster.builders to match builders. The
716 # botmaster will handle startup/shutdown issues.
717 d
.addCallback(lambda res
: self
.loadConfig_Builders(builders
))
719 d
.addCallback(lambda res
: self
.loadConfig_status(status
))
721 # Schedulers are added after Builders in case they start right away
722 d
.addCallback(lambda res
: self
.loadConfig_Schedulers(schedulers
))
723 # and Sources go after Schedulers for the same reason
724 d
.addCallback(lambda res
: self
.loadConfig_Sources(change_sources
))
727 if self
.slavePortnum
!= slavePortnum
:
729 def closeSlavePort(res
):
730 d1
= self
.slavePort
.disownServiceParent()
731 self
.slavePort
= None
733 d
.addCallback(closeSlavePort
)
734 if slavePortnum
is not None:
735 def openSlavePort(res
):
736 self
.slavePort
= strports
.service(slavePortnum
,
738 self
.slavePort
.setServiceParent(self
)
739 d
.addCallback(openSlavePort
)
740 log
.msg("BuildMaster listening on port %s" % slavePortnum
)
741 self
.slavePortnum
= slavePortnum
743 log
.msg("configuration update started")
745 self
.readConfig
= True
746 log
.msg("configuration update complete")
748 d
.addCallback(lambda res
: self
.botmaster
.maybeStartAllBuilds())
751 def loadConfig_Slaves(self
, new_slaves
):
752 # set up the Checker with the names and passwords of all valid bots
753 self
.checker
.users
= {} # violates abstraction, oh well
755 self
.checker
.addUser(s
.slavename
, s
.password
)
756 self
.checker
.addUser("change", "changepw")
757 # let the BotMaster take care of the rest
758 return self
.botmaster
.loadConfig_Slaves(new_slaves
)
760 def loadConfig_Sources(self
, sources
):
762 log
.msg("warning: no ChangeSources specified in c['change_source']")
763 # shut down any that were removed, start any that were added
764 deleted_sources
= [s
for s
in self
.change_svc
if s
not in sources
]
765 added_sources
= [s
for s
in sources
if s
not in self
.change_svc
]
766 dl
= [self
.change_svc
.removeSource(s
) for s
in deleted_sources
]
768 [self
.change_svc
.addSource(s
) for s
in added_sources
]
769 d
= defer
.DeferredList(dl
, fireOnOneErrback
=1, consumeErrors
=0)
770 d
.addCallback(addNewOnes
)
773 def allSchedulers(self
):
774 return [child
for child
in self
775 if interfaces
.IScheduler
.providedBy(child
)]
778 def loadConfig_Schedulers(self
, newschedulers
):
779 oldschedulers
= self
.allSchedulers()
780 removed
= [s
for s
in oldschedulers
if s
not in newschedulers
]
781 added
= [s
for s
in newschedulers
if s
not in oldschedulers
]
782 dl
= [defer
.maybeDeferred(s
.disownServiceParent
) for s
in removed
]
785 s
.setServiceParent(self
)
786 d
= defer
.DeferredList(dl
, fireOnOneErrback
=1)
787 d
.addCallback(addNewOnes
)
790 def loadConfig_Builders(self
, newBuilderData
):
791 somethingChanged
= False
794 allBuilders
= self
.botmaster
.builders
.copy()
795 for data
in newBuilderData
:
798 newBuilderNames
.append(name
)
800 # identify all that were removed
801 for oldname
in self
.botmaster
.getBuildernames():
802 if oldname
not in newList
:
803 log
.msg("removing old builder %s" % oldname
)
804 del allBuilders
[oldname
]
805 somethingChanged
= True
806 # announce the change
807 self
.status
.builderRemoved(oldname
)
809 # everything in newList is either unchanged, changed, or new
810 for name
, data
in newList
.items():
811 old
= self
.botmaster
.builders
.get(name
)
812 basedir
= data
['builddir'] # used on both master and slave
813 #name, slave, builddir, factory = data
815 # category added after 0.6.2
816 category
= data
.get('category', None)
817 log
.msg("adding new builder %s for category %s" %
819 statusbag
= self
.status
.builderAdded(name
, basedir
, category
)
820 builder
= Builder(data
, statusbag
)
821 allBuilders
[name
] = builder
822 somethingChanged
= True
823 elif old
.compareToSetup(data
):
824 # changed: try to minimize the disruption and only modify the
825 # pieces that really changed
826 diffs
= old
.compareToSetup(data
)
827 log
.msg("updating builder %s: %s" % (name
, "\n".join(diffs
)))
829 statusbag
= old
.builder_status
830 statusbag
.saveYourself() # seems like a good idea
831 # TODO: if the basedir was changed, we probably need to make
833 new_builder
= Builder(data
, statusbag
)
834 new_builder
.consumeTheSoulOfYourPredecessor(old
)
835 # that migrates any retained slavebuilders too
837 # point out that the builder was updated. On the Waterfall,
838 # this will appear just after any currently-running builds.
839 statusbag
.addPointEvent(["config", "updated"])
841 allBuilders
[name
] = new_builder
842 somethingChanged
= True
844 # unchanged: leave it alone
845 log
.msg("builder %s is unchanged" % name
)
849 sortedAllBuilders
= [allBuilders
[name
] for name
in newBuilderNames
]
850 d
= self
.botmaster
.setBuilders(sortedAllBuilders
)
854 def loadConfig_status(self
, status
):
858 for s
in self
.statusTargets
[:]:
860 log
.msg("removing IStatusReceiver", s
)
861 d
= defer
.maybeDeferred(s
.disownServiceParent
)
863 self
.statusTargets
.remove(s
)
864 # after those are finished going away, add new ones
867 if not s
in self
.statusTargets
:
868 log
.msg("adding IStatusReceiver", s
)
869 s
.setServiceParent(self
)
870 self
.statusTargets
.append(s
)
871 d
= defer
.DeferredList(dl
, fireOnOneErrback
=1)
872 d
.addCallback(addNewOnes
)
876 def addChange(self
, change
):
877 for s
in self
.allSchedulers():
880 def submitBuildSet(self
, bs
):
881 # determine the set of Builders to use
883 for name
in bs
.builderNames
:
884 b
= self
.botmaster
.builders
.get(name
)
886 if b
not in builders
:
889 # TODO: add aliases like 'all'
890 raise KeyError("no such builder named '%s'" % name
)
892 # now tell the BuildSet to create BuildRequests for all those
893 # Builders and submit them
895 self
.status
.buildsetSubmitted(bs
.status
)
899 implements(interfaces
.IControl
)
901 def __init__(self
, master
):
904 def addChange(self
, change
):
905 self
.master
.change_svc
.addChange(change
)
907 def submitBuildSet(self
, bs
):
908 self
.master
.submitBuildSet(bs
)
910 def getBuilder(self
, name
):
911 b
= self
.master
.botmaster
.builders
[name
]
912 return interfaces
.IBuilderControl(b
)
914 components
.registerAdapter(Control
, BuildMaster
, interfaces
.IControl
)
916 # so anybody who can get a handle on the BuildMaster can cause a build with:
917 # IControl(master).getBuilder("full-2.3").requestBuild(buildrequest)