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 builders
= self
.builders
.values()
193 def _sortfunc(b1
, b2
):
194 t1
= b1
.getOldestRequestTime()
195 t2
= b2
.getOldestRequestTime()
196 # If t1 or t2 is None, then there are no build requests,
197 # so sort it at the end
203 builders
.sort(cmp=_sortfunc
)
207 def getPerspective(self
, slavename
):
208 return self
.slaves
[slavename
]
210 def shutdownSlaves(self
):
211 # TODO: make this into a bot method rather than a builder method
212 for b
in self
.slaves
.values():
215 def stopService(self
):
216 for b
in self
.builders
.values():
217 b
.builder_status
.addPointEvent(["master", "shutdown"])
218 b
.builder_status
.saveYourself()
219 return service
.Service
.stopService(self
)
221 def getLockByID(self
, lockid
):
222 """Convert a Lock identifier into an actual Lock instance.
223 @param lockid: a locks.MasterLock or locks.SlaveLock instance
224 @return: a locks.RealMasterLock or locks.RealSlaveLock instance
226 assert isinstance(lockid
, (locks
.MasterLock
, locks
.SlaveLock
))
227 if not lockid
in self
.locks
:
228 self
.locks
[lockid
] = lockid
.lockClass(lockid
)
229 # if the master.cfg file has changed maxCount= on the lock, the next
230 # time a build is started, they'll get a new RealLock instance. Note
231 # that this requires that MasterLock and SlaveLock (marker) instances
232 # be hashable and that they should compare properly.
233 return self
.locks
[lockid
]
235 ########################################
239 class DebugPerspective(NewCredPerspective
):
240 def attached(self
, mind
):
242 def detached(self
, mind
):
245 def perspective_requestBuild(self
, buildername
, reason
, branch
, revision
, properties
={}):
246 c
= interfaces
.IControl(self
.master
)
247 bc
= c
.getBuilder(buildername
)
248 ss
= SourceStamp(branch
, revision
)
250 bpr
.update(properties
, "remote requestBuild")
251 br
= BuildRequest(reason
, ss
, builderName
=buildername
, properties
=bpr
)
254 def perspective_pingBuilder(self
, buildername
):
255 c
= interfaces
.IControl(self
.master
)
256 bc
= c
.getBuilder(buildername
)
259 def perspective_fakeChange(self
, file, revision
=None, who
="fakeUser",
261 change
= Change(who
, [file], "some fake comments\n",
262 branch
=branch
, revision
=revision
)
263 c
= interfaces
.IControl(self
.master
)
266 def perspective_setCurrentState(self
, buildername
, state
):
267 builder
= self
.botmaster
.builders
.get(buildername
)
268 if not builder
: return
269 if state
== "offline":
270 builder
.statusbag
.currentlyOffline()
272 builder
.statusbag
.currentlyIdle()
273 if state
== "waiting":
274 builder
.statusbag
.currentlyWaiting(now()+10)
275 if state
== "building":
276 builder
.statusbag
.currentlyBuilding(None)
277 def perspective_reload(self
):
278 print "doing reload of the config file"
279 self
.master
.loadTheConfigFile()
280 def perspective_pokeIRC(self
):
281 print "saying something on IRC"
282 from buildbot
.status
import words
283 for s
in self
.master
:
284 if isinstance(s
, words
.IRC
):
286 for channel
in bot
.channels
:
287 print " channel", channel
288 bot
.p
.msg(channel
, "Ow, quit it")
290 def perspective_print(self
, msg
):
293 class Dispatcher(styles
.Versioned
):
294 implements(portal
.IRealm
)
295 persistenceVersion
= 2
300 def upgradeToVersion1(self
):
301 self
.master
= self
.botmaster
.parent
302 def upgradeToVersion2(self
):
305 def register(self
, name
, afactory
):
306 self
.names
[name
] = afactory
307 def unregister(self
, name
):
310 def requestAvatar(self
, avatarID
, mind
, interface
):
311 assert interface
== pb
.IPerspective
312 afactory
= self
.names
.get(avatarID
)
314 p
= afactory
.getPerspective()
315 elif avatarID
== "debug":
316 p
= DebugPerspective()
317 p
.master
= self
.master
318 p
.botmaster
= self
.botmaster
319 elif avatarID
== "statusClient":
320 p
= self
.statusClientService
.getPerspective()
322 # it must be one of the buildslaves: no other names will make it
324 p
= self
.botmaster
.getPerspective(avatarID
)
327 raise ValueError("no perspective for '%s'" % avatarID
)
329 d
= defer
.maybeDeferred(p
.attached
, mind
)
330 d
.addCallback(self
._avatarAttached
, mind
)
333 def _avatarAttached(self
, p
, mind
):
334 return (pb
.IPerspective
, p
, lambda p
=p
,mind
=mind
: p
.detached(mind
))
336 ########################################
342 # all IChangeSource objects
343 # StatusClientService
344 # TCPClient(self.ircFactory)
345 # TCPServer(self.slaveFactory) -> dispatcher.requestAvatar
346 # TCPServer(self.site)
347 # UNIXServer(ResourcePublisher(self.site))
350 class BuildMaster(service
.MultiService
, styles
.Versioned
):
352 persistenceVersion
= 3
355 projectName
= "(unspecified)"
359 properties
= Properties()
361 def __init__(self
, basedir
, configFileName
="master.cfg"):
362 service
.MultiService
.__init
__(self
)
363 self
.setName("buildmaster")
364 self
.basedir
= basedir
365 self
.configFileName
= configFileName
367 # the dispatcher is the realm in which all inbound connections are
368 # looked up: slave builders, change notifications, status clients, and
370 dispatcher
= Dispatcher()
371 dispatcher
.master
= self
372 self
.dispatcher
= dispatcher
373 self
.checker
= checkers
.InMemoryUsernamePasswordDatabaseDontUse()
374 # the checker starts with no user/passwd pairs: they are added later
375 p
= portal
.Portal(dispatcher
)
376 p
.registerChecker(self
.checker
)
377 self
.slaveFactory
= pb
.PBServerFactory(p
)
378 self
.slaveFactory
.unsafeTracebacks
= True # let them see exceptions
380 self
.slavePortnum
= None
381 self
.slavePort
= None
383 self
.botmaster
= BotMaster()
384 self
.botmaster
.setName("botmaster")
385 self
.botmaster
.setServiceParent(self
)
386 dispatcher
.botmaster
= self
.botmaster
388 self
.status
= Status(self
.botmaster
, self
.basedir
)
390 self
.statusTargets
= []
392 # this ChangeMaster is a dummy, only used by tests. In the real
393 # buildmaster, where the BuildMaster instance is activated
394 # (startService is called) by twistd, this attribute is overwritten.
395 self
.useChanges(ChangeMaster())
397 self
.readConfig
= False
399 def upgradeToVersion1(self
):
400 self
.dispatcher
= self
.slaveFactory
.root
.portal
.realm
402 def upgradeToVersion2(self
): # post-0.4.3
403 self
.webServer
= self
.webTCPPort
405 self
.webDistribServer
= self
.webUNIXPort
407 self
.configFileName
= "master.cfg"
409 def upgradeToVersion3(self
):
410 # post 0.6.3, solely to deal with the 0.6.3 breakage. Starting with
411 # 0.6.5 I intend to do away with .tap files altogether
413 self
.namedServices
= {}
416 def startService(self
):
417 service
.MultiService
.startService(self
)
418 self
.loadChanges() # must be done before loading the config file
419 if not self
.readConfig
:
420 # TODO: consider catching exceptions during this call to
421 # loadTheConfigFile and bailing (reactor.stop) if it fails,
422 # since without a config file we can't do anything except reload
423 # the config file, and it would be nice for the user to discover
425 self
.loadTheConfigFile()
426 if signal
and hasattr(signal
, "SIGHUP"):
427 signal
.signal(signal
.SIGHUP
, self
._handleSIGHUP
)
428 for b
in self
.botmaster
.builders
.values():
429 b
.builder_status
.addPointEvent(["master", "started"])
430 b
.builder_status
.saveYourself()
432 def useChanges(self
, changes
):
434 # TODO: can return a Deferred
435 self
.change_svc
.disownServiceParent()
436 self
.change_svc
= changes
437 self
.change_svc
.basedir
= self
.basedir
438 self
.change_svc
.setName("changemaster")
439 self
.dispatcher
.changemaster
= self
.change_svc
440 self
.change_svc
.setServiceParent(self
)
442 def loadChanges(self
):
443 filename
= os
.path
.join(self
.basedir
, "changes.pck")
445 changes
= load(open(filename
, "rb"))
448 log
.msg("changes.pck missing, using new one")
449 changes
= ChangeMaster()
451 log
.msg("corrupted changes.pck, using new one")
452 changes
= ChangeMaster()
453 self
.useChanges(changes
)
455 def _handleSIGHUP(self
, *args
):
456 reactor
.callLater(0, self
.loadTheConfigFile
)
460 @rtype: L{buildbot.status.builder.Status}
464 def loadTheConfigFile(self
, configFile
=None):
466 configFile
= os
.path
.join(self
.basedir
, self
.configFileName
)
468 log
.msg("Creating BuildMaster -- buildbot.version: %s" % buildbot
.version
)
469 log
.msg("loading configuration from %s" % configFile
)
470 configFile
= os
.path
.expanduser(configFile
)
473 f
= open(configFile
, "r")
475 log
.msg("unable to open config file '%s'" % configFile
)
476 log
.msg("leaving old configuration in place")
483 log
.msg("error during loadConfig")
485 log
.msg("The new config file is unusable, so I'll ignore it.")
486 log
.msg("I will keep using the previous config file instead.")
489 def loadConfig(self
, f
):
490 """Internal function to load a specific configuration file. Any
491 errors in the file will be signalled by raising an exception.
493 @return: a Deferred that will fire (with None) when the configuration
494 changes have been completed. This may involve a round-trip to each
495 buildslave that was involved."""
497 localDict
= {'basedir': os
.path
.expanduser(self
.basedir
)}
501 log
.msg("error while parsing config file")
505 config
= localDict
['BuildmasterConfig']
507 log
.err("missing config dictionary")
508 log
.err("config file must define BuildmasterConfig")
511 known_keys
= ("bots", "slaves",
512 "sources", "change_source",
513 "schedulers", "builders",
514 "slavePortnum", "debugPassword", "logCompressionLimit",
515 "manhole", "status", "projectName", "projectURL",
516 "buildbotURL", "properties"
518 for k
in config
.keys():
519 if k
not in known_keys
:
520 log
.msg("unknown key '%s' defined in config dictionary" % k
)
524 schedulers
= config
['schedulers']
525 builders
= config
['builders']
527 if k
['name'].startswith("_"):
528 errmsg
= ("builder names must not start with an "
529 "underscore: " + k
['name'])
531 raise ValueError(errmsg
)
533 slavePortnum
= config
['slavePortnum']
534 #slaves = config['slaves']
535 #change_source = config['change_source']
538 debugPassword
= config
.get('debugPassword')
539 manhole
= config
.get('manhole')
540 status
= config
.get('status', [])
541 projectName
= config
.get('projectName')
542 projectURL
= config
.get('projectURL')
543 buildbotURL
= config
.get('buildbotURL')
544 properties
= config
.get('properties', {})
545 logCompressionLimit
= config
.get('logCompressionLimit')
546 if logCompressionLimit
is not None and not \
547 isinstance(logCompressionLimit
, int):
548 raise ValueError("logCompressionLimit needs to be bool or int")
551 log
.msg("config dictionary is missing a required parameter")
552 log
.msg("leaving old configuration in place")
555 #if "bots" in config:
556 # raise KeyError("c['bots'] is no longer accepted")
558 slaves
= config
.get('slaves', [])
560 m
= ("c['bots'] is deprecated as of 0.7.6 and will be "
561 "removed by 0.8.0 . Please use c['slaves'] instead.")
563 warnings
.warn(m
, DeprecationWarning)
564 for name
, passwd
in config
['bots']:
565 slaves
.append(BuildSlave(name
, passwd
))
567 if "bots" not in config
and "slaves" not in config
:
568 log
.msg("config dictionary must have either 'bots' or 'slaves'")
569 log
.msg("leaving old configuration in place")
570 raise KeyError("must have either 'bots' or 'slaves'")
572 #if "sources" in config:
573 # raise KeyError("c['sources'] is no longer accepted")
575 change_source
= config
.get('change_source', [])
576 if isinstance(change_source
, (list, tuple)):
577 change_sources
= change_source
579 change_sources
= [change_source
]
580 if "sources" in config
:
581 m
= ("c['sources'] is deprecated as of 0.7.6 and will be "
582 "removed by 0.8.0 . Please use c['change_source'] instead.")
584 warnings
.warn(m
, DeprecationWarning)
585 for s
in config
['sources']:
586 change_sources
.append(s
)
588 # do some validation first
590 assert interfaces
.IBuildSlave
.providedBy(s
)
591 if s
.slavename
in ("debug", "change", "status"):
593 "reserved name '%s' used for a bot" % s
.slavename
)
594 if config
.has_key('interlocks'):
595 raise KeyError("c['interlocks'] is no longer accepted")
597 assert isinstance(change_sources
, (list, tuple))
598 for s
in change_sources
:
599 assert interfaces
.IChangeSource(s
, None)
600 # this assertion catches c['schedulers'] = Scheduler(), since
601 # Schedulers are service.MultiServices and thus iterable.
602 errmsg
= "c['schedulers'] must be a list of Scheduler instances"
603 assert isinstance(schedulers
, (list, tuple)), errmsg
605 assert interfaces
.IScheduler(s
, None), errmsg
606 assert isinstance(status
, (list, tuple))
608 assert interfaces
.IStatusReceiver(s
, None)
610 slavenames
= [s
.slavename
for s
in slaves
]
615 raise ValueError("builder %s must be defined with a dict, "
616 "not a tuple" % b
[0])
617 if b
.has_key('slavename') and b
['slavename'] not in slavenames
:
618 raise ValueError("builder %s uses undefined slave %s" \
619 % (b
['name'], b
['slavename']))
620 for n
in b
.get('slavenames', []):
621 if n
not in slavenames
:
622 raise ValueError("builder %s uses undefined slave %s" \
624 if b
['name'] in buildernames
:
625 raise ValueError("duplicate builder name %s"
627 buildernames
.append(b
['name'])
628 if b
['builddir'] in dirnames
:
629 raise ValueError("builder %s reuses builddir %s"
630 % (b
['name'], b
['builddir']))
631 dirnames
.append(b
['builddir'])
633 unscheduled_buildernames
= buildernames
[:]
636 for b
in s
.listBuilderNames():
637 assert b
in buildernames
, \
638 "%s uses unknown builder %s" % (s
, b
)
639 if b
in unscheduled_buildernames
:
640 unscheduled_buildernames
.remove(b
)
642 if s
.name
in schedulernames
:
643 # TODO: schedulers share a namespace with other Service
644 # children of the BuildMaster node, like status plugins, the
645 # Manhole, the ChangeMaster, and the BotMaster (although most
646 # of these don't have names)
647 msg
= ("Schedulers must have unique names, but "
648 "'%s' was a duplicate" % (s
.name
,))
649 raise ValueError(msg
)
650 schedulernames
.append(s
.name
)
652 if unscheduled_buildernames
:
653 log
.msg("Warning: some Builders have no Schedulers to drive them:"
654 " %s" % (unscheduled_buildernames
,))
656 # assert that all locks used by the Builds and their Steps are
660 for l
in b
.get('locks', []):
661 if isinstance(l
, locks
.LockAccess
): # User specified access to the lock
663 if lock_dict
.has_key(l
.name
):
664 if lock_dict
[l
.name
] is not l
:
665 raise ValueError("Two different locks (%s and %s) "
667 % (l
, lock_dict
[l
.name
], l
.name
))
669 lock_dict
[l
.name
] = l
670 # TODO: this will break with any BuildFactory that doesn't use a
671 # .steps list, but I think the verification step is more
673 for s
in b
['factory'].steps
:
674 for l
in s
[1].get('locks', []):
675 if isinstance(l
, locks
.LockAccess
): # User specified access to the lock
677 if lock_dict
.has_key(l
.name
):
678 if lock_dict
[l
.name
] is not l
:
679 raise ValueError("Two different locks (%s and %s)"
681 % (l
, lock_dict
[l
.name
], l
.name
))
683 lock_dict
[l
.name
] = l
685 if not isinstance(properties
, dict):
686 raise ValueError("c['properties'] must be a dictionary")
688 # slavePortnum supposed to be a strports specification
689 if type(slavePortnum
) is int:
690 slavePortnum
= "tcp:%d" % slavePortnum
692 # now we're committed to implementing the new configuration, so do
694 # TODO: actually, this is spread across a couple of Deferreds, so it
695 # really isn't atomic.
697 d
= defer
.succeed(None)
699 self
.projectName
= projectName
700 self
.projectURL
= projectURL
701 self
.buildbotURL
= buildbotURL
703 self
.properties
= Properties()
704 self
.properties
.update(properties
, self
.configFileName
)
705 if logCompressionLimit
is not None:
706 self
.status
.logCompressionLimit
= logCompressionLimit
708 # self.slaves: Disconnect any that were attached and removed from the
709 # list. Update self.checker with the new list of passwords, including
710 # debug/change/status.
711 d
.addCallback(lambda res
: self
.loadConfig_Slaves(slaves
))
715 self
.checker
.addUser("debug", debugPassword
)
716 self
.debugPassword
= debugPassword
719 if manhole
!= self
.manhole
:
722 # disownServiceParent may return a Deferred
723 d
.addCallback(lambda res
: self
.manhole
.disownServiceParent())
727 d
.addCallback(_remove
)
730 self
.manhole
= manhole
731 manhole
.setServiceParent(self
)
734 # add/remove self.botmaster.builders to match builders. The
735 # botmaster will handle startup/shutdown issues.
736 d
.addCallback(lambda res
: self
.loadConfig_Builders(builders
))
738 d
.addCallback(lambda res
: self
.loadConfig_status(status
))
740 # Schedulers are added after Builders in case they start right away
741 d
.addCallback(lambda res
: self
.loadConfig_Schedulers(schedulers
))
742 # and Sources go after Schedulers for the same reason
743 d
.addCallback(lambda res
: self
.loadConfig_Sources(change_sources
))
746 if self
.slavePortnum
!= slavePortnum
:
748 def closeSlavePort(res
):
749 d1
= self
.slavePort
.disownServiceParent()
750 self
.slavePort
= None
752 d
.addCallback(closeSlavePort
)
753 if slavePortnum
is not None:
754 def openSlavePort(res
):
755 self
.slavePort
= strports
.service(slavePortnum
,
757 self
.slavePort
.setServiceParent(self
)
758 d
.addCallback(openSlavePort
)
759 log
.msg("BuildMaster listening on port %s" % slavePortnum
)
760 self
.slavePortnum
= slavePortnum
762 log
.msg("configuration update started")
764 self
.readConfig
= True
765 log
.msg("configuration update complete")
767 d
.addCallback(lambda res
: self
.botmaster
.maybeStartAllBuilds())
770 def loadConfig_Slaves(self
, new_slaves
):
771 # set up the Checker with the names and passwords of all valid bots
772 self
.checker
.users
= {} # violates abstraction, oh well
774 self
.checker
.addUser(s
.slavename
, s
.password
)
775 self
.checker
.addUser("change", "changepw")
776 # let the BotMaster take care of the rest
777 return self
.botmaster
.loadConfig_Slaves(new_slaves
)
779 def loadConfig_Sources(self
, sources
):
781 log
.msg("warning: no ChangeSources specified in c['change_source']")
782 # shut down any that were removed, start any that were added
783 deleted_sources
= [s
for s
in self
.change_svc
if s
not in sources
]
784 added_sources
= [s
for s
in sources
if s
not in self
.change_svc
]
785 dl
= [self
.change_svc
.removeSource(s
) for s
in deleted_sources
]
787 [self
.change_svc
.addSource(s
) for s
in added_sources
]
788 d
= defer
.DeferredList(dl
, fireOnOneErrback
=1, consumeErrors
=0)
789 d
.addCallback(addNewOnes
)
792 def allSchedulers(self
):
793 return [child
for child
in self
794 if interfaces
.IScheduler
.providedBy(child
)]
797 def loadConfig_Schedulers(self
, newschedulers
):
798 oldschedulers
= self
.allSchedulers()
799 removed
= [s
for s
in oldschedulers
if s
not in newschedulers
]
800 added
= [s
for s
in newschedulers
if s
not in oldschedulers
]
801 dl
= [defer
.maybeDeferred(s
.disownServiceParent
) for s
in removed
]
803 log
.msg("adding %d new schedulers, removed %d" %
804 (len(added
), len(dl
)))
806 s
.setServiceParent(self
)
807 d
= defer
.DeferredList(dl
, fireOnOneErrback
=1)
808 d
.addCallback(addNewOnes
)
810 # notify Downstream schedulers to potentially pick up
811 # new schedulers now that we have removed and added some
812 def updateDownstreams(res
):
813 log
.msg("notifying downstream schedulers of changes")
814 for s
in newschedulers
:
815 if interfaces
.IDownstreamScheduler
.providedBy(s
):
816 s
.checkUpstreamScheduler()
817 d
.addCallback(updateDownstreams
)
820 def loadConfig_Builders(self
, newBuilderData
):
821 somethingChanged
= False
824 allBuilders
= self
.botmaster
.builders
.copy()
825 for data
in newBuilderData
:
828 newBuilderNames
.append(name
)
830 # identify all that were removed
831 for oldname
in self
.botmaster
.getBuildernames():
832 if oldname
not in newList
:
833 log
.msg("removing old builder %s" % oldname
)
834 del allBuilders
[oldname
]
835 somethingChanged
= True
836 # announce the change
837 self
.status
.builderRemoved(oldname
)
839 # everything in newList is either unchanged, changed, or new
840 for name
, data
in newList
.items():
841 old
= self
.botmaster
.builders
.get(name
)
842 basedir
= data
['builddir'] # used on both master and slave
843 #name, slave, builddir, factory = data
845 # category added after 0.6.2
846 category
= data
.get('category', None)
847 log
.msg("adding new builder %s for category %s" %
849 statusbag
= self
.status
.builderAdded(name
, basedir
, category
)
850 builder
= Builder(data
, statusbag
)
851 allBuilders
[name
] = builder
852 somethingChanged
= True
853 elif old
.compareToSetup(data
):
854 # changed: try to minimize the disruption and only modify the
855 # pieces that really changed
856 diffs
= old
.compareToSetup(data
)
857 log
.msg("updating builder %s: %s" % (name
, "\n".join(diffs
)))
859 statusbag
= old
.builder_status
860 statusbag
.saveYourself() # seems like a good idea
861 # TODO: if the basedir was changed, we probably need to make
863 new_builder
= Builder(data
, statusbag
)
864 new_builder
.consumeTheSoulOfYourPredecessor(old
)
865 # that migrates any retained slavebuilders too
867 # point out that the builder was updated. On the Waterfall,
868 # this will appear just after any currently-running builds.
869 statusbag
.addPointEvent(["config", "updated"])
871 allBuilders
[name
] = new_builder
872 somethingChanged
= True
874 # unchanged: leave it alone
875 log
.msg("builder %s is unchanged" % name
)
879 sortedAllBuilders
= [allBuilders
[name
] for name
in newBuilderNames
]
880 d
= self
.botmaster
.setBuilders(sortedAllBuilders
)
884 def loadConfig_status(self
, status
):
888 for s
in self
.statusTargets
[:]:
890 log
.msg("removing IStatusReceiver", s
)
891 d
= defer
.maybeDeferred(s
.disownServiceParent
)
893 self
.statusTargets
.remove(s
)
894 # after those are finished going away, add new ones
897 if not s
in self
.statusTargets
:
898 log
.msg("adding IStatusReceiver", s
)
899 s
.setServiceParent(self
)
900 self
.statusTargets
.append(s
)
901 d
= defer
.DeferredList(dl
, fireOnOneErrback
=1)
902 d
.addCallback(addNewOnes
)
906 def addChange(self
, change
):
907 for s
in self
.allSchedulers():
910 def submitBuildSet(self
, bs
):
911 # determine the set of Builders to use
913 for name
in bs
.builderNames
:
914 b
= self
.botmaster
.builders
.get(name
)
916 if b
not in builders
:
919 # TODO: add aliases like 'all'
920 raise KeyError("no such builder named '%s'" % name
)
922 # now tell the BuildSet to create BuildRequests for all those
923 # Builders and submit them
925 self
.status
.buildsetSubmitted(bs
.status
)
929 implements(interfaces
.IControl
)
931 def __init__(self
, master
):
934 def addChange(self
, change
):
935 self
.master
.change_svc
.addChange(change
)
937 def submitBuildSet(self
, bs
):
938 self
.master
.submitBuildSet(bs
)
940 def getBuilder(self
, name
):
941 b
= self
.master
.botmaster
.builders
[name
]
942 return interfaces
.IBuilderControl(b
)
944 components
.registerAdapter(Control
, BuildMaster
, interfaces
.IControl
)
946 # so anybody who can get a handle on the BuildMaster can cause a build with:
947 # IControl(master).getBuilder("full-2.3").requestBuild(buildrequest)