1
2
3 import os
4 signal = None
5 try:
6 import signal
7 except ImportError:
8 pass
9 from cPickle import load
10 import warnings
11
12 from zope.interface import implements
13 from twisted.python import log, components
14 from twisted.python.failure import Failure
15 from twisted.internet import defer, reactor
16 from twisted.spread import pb
17 from twisted.cred import portal, checkers
18 from twisted.application import service, strports
19 from twisted.persisted import styles
20
21 import buildbot
22
23 from buildbot.util import now
24 from buildbot.pbutil import NewCredPerspective
25 from buildbot.process.builder import Builder, IDLE
26 from buildbot.process.base import BuildRequest
27 from buildbot.status.builder import Status
28 from buildbot.changes.changes import Change, ChangeMaster, TestChangeMaster
29 from buildbot.sourcestamp import SourceStamp
30 from buildbot.buildslave import BuildSlave
31 from buildbot import interfaces, locks
32 from buildbot.process.properties import Properties
33
34
35
37
38 """This is the master-side service which manages remote buildbot slaves.
39 It provides them with BuildSlaves, and distributes file change
40 notification messages to them.
41 """
42
43 debug = 0
44
46 service.MultiService.__init__(self)
47 self.builders = {}
48 self.builderNames = []
49
50
51
52
53
54
55
56
57
58
59 self.slaves = {}
60 self.statusClientService = None
61 self.watchers = {}
62
63
64 self.locks = {}
65
66
67
68 self.mergeRequests = None
69
70
71
72 self.prioritizeBuilders = None
73
74
75
77 b = self.builders[name]
78
79
80 d = defer.Deferred()
81 b.watchers['attach'].append(d)
82 return d
83
85 b = self.builders.get(name)
86 if not b or not b.slaves:
87 return defer.succeed(None)
88 d = defer.Deferred()
89 b.watchers['detach'].append(d)
90 return d
91
93 b = self.builders.get(name)
94
95 if not b or not b.slaves:
96 return defer.succeed(None)
97 d = defer.Deferred()
98 b.watchers['detach_all'].append(d)
99 return d
100
102 b = self.builders[name]
103
104 for sb in b.slaves:
105 if sb.state != IDLE:
106 d = defer.Deferred()
107 b.watchers['idle'].append(d)
108 return d
109 return defer.succeed(None)
110
112 old_slaves = [c for c in list(self)
113 if interfaces.IBuildSlave.providedBy(c)]
114
115
116
117
118
119
120
121
122
123
124
125 old_t = {}
126 for s in old_slaves:
127 old_t[(s.slavename, s.password, s.__class__)] = s
128 new_t = {}
129 for s in new_slaves:
130 new_t[(s.slavename, s.password, s.__class__)] = s
131 removed = [old_t[t]
132 for t in old_t
133 if t not in new_t]
134 added = [new_t[t]
135 for t in new_t
136 if t not in old_t]
137 remaining_t = [t
138 for t in new_t
139 if t in old_t]
140
141 dl = []
142 for s in removed:
143 dl.append(self.removeSlave(s))
144 d = defer.DeferredList(dl, fireOnOneErrback=True)
145 def _add(res):
146 for s in added:
147 self.addSlave(s)
148 for t in remaining_t:
149 old_t[t].update(new_t[t])
150 d.addCallback(_add)
151 return d
152
157
164
169
171 return [b
172 for b in self.builders.values()
173 if slavename in b.slavenames]
174
176 return self.builderNames
177
179 allBuilders = [self.builders[name] for name in self.builderNames]
180 return allBuilders
181
194
196 """Notify all buildslaves about changes in their Builders."""
197 dl = [s.updateSlave() for s in self.slaves.values()]
198 return defer.DeferredList(dl)
199
201 builders = self.builders.values()
202 if self.prioritizeBuilders is not None:
203 try:
204 builders = self.prioritizeBuilders(self.parent, builders)
205 except:
206 log.msg("Exception prioritizing builders")
207 log.err(Failure())
208 return
209 else:
210 def _sortfunc(b1, b2):
211 t1 = b1.getOldestRequestTime()
212 t2 = b2.getOldestRequestTime()
213
214
215 if t1 is None:
216 return 1
217 if t2 is None:
218 return -1
219 return cmp(t1, t2)
220 builders.sort(_sortfunc)
221 try:
222 for b in builders:
223 b.maybeStartBuild()
224 except:
225 log.msg("Exception starting builds")
226 log.err(Failure())
227
229 """Determine whether two BuildRequests should be merged for
230 the given builder.
231
232 """
233 if self.mergeRequests is not None:
234 return self.mergeRequests(builder, req1, req2)
235 return req1.canBeMergedWith(req2)
236
239
244
250
252 """Convert a Lock identifier into an actual Lock instance.
253 @param lockid: a locks.MasterLock or locks.SlaveLock instance
254 @return: a locks.RealMasterLock or locks.RealSlaveLock instance
255 """
256 assert isinstance(lockid, (locks.MasterLock, locks.SlaveLock))
257 if not lockid in self.locks:
258 self.locks[lockid] = lockid.lockClass(lockid)
259
260
261
262
263 return self.locks[lockid]
264
265
266
267
268
274
283
288
295
297 builder = self.botmaster.builders.get(buildername)
298 if not builder: return
299 if state == "offline":
300 builder.statusbag.currentlyOffline()
301 if state == "idle":
302 builder.statusbag.currentlyIdle()
303 if state == "waiting":
304 builder.statusbag.currentlyWaiting(now()+10)
305 if state == "building":
306 builder.statusbag.currentlyBuilding(None)
311 print "saying something on IRC"
312 from buildbot.status import words
313 for s in self.master:
314 if isinstance(s, words.IRC):
315 bot = s.f
316 for channel in bot.channels:
317 print " channel", channel
318 bot.p.msg(channel, "Ow, quit it")
319
322
324 implements(portal.IRealm)
325 persistenceVersion = 2
326
329
334
336 self.names[name] = afactory
339
341 assert interface == pb.IPerspective
342 afactory = self.names.get(avatarID)
343 if afactory:
344 p = afactory.getPerspective()
345 elif avatarID == "debug":
346 p = DebugPerspective()
347 p.master = self.master
348 p.botmaster = self.botmaster
349 elif avatarID == "statusClient":
350 p = self.statusClientService.getPerspective()
351 else:
352
353
354 p = self.botmaster.getPerspective(avatarID)
355
356 if not p:
357 raise ValueError("no perspective for '%s'" % avatarID)
358
359 d = defer.maybeDeferred(p.attached, mind)
360 d.addCallback(self._avatarAttached, mind)
361 return d
362
364 return (pb.IPerspective, p, lambda p=p,mind=mind: p.detached(mind))
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380 -class BuildMaster(service.MultiService, styles.Versioned):
381 debug = 0
382 persistenceVersion = 3
383 manhole = None
384 debugPassword = None
385 projectName = "(unspecified)"
386 projectURL = None
387 buildbotURL = None
388 change_svc = None
389 properties = Properties()
390
391 - def __init__(self, basedir, configFileName="master.cfg"):
392 service.MultiService.__init__(self)
393 self.setName("buildmaster")
394 self.basedir = basedir
395 self.configFileName = configFileName
396
397
398
399
400 dispatcher = Dispatcher()
401 dispatcher.master = self
402 self.dispatcher = dispatcher
403 self.checker = checkers.InMemoryUsernamePasswordDatabaseDontUse()
404
405 p = portal.Portal(dispatcher)
406 p.registerChecker(self.checker)
407 self.slaveFactory = pb.PBServerFactory(p)
408 self.slaveFactory.unsafeTracebacks = True
409
410 self.slavePortnum = None
411 self.slavePort = None
412
413 self.botmaster = BotMaster()
414 self.botmaster.setName("botmaster")
415 self.botmaster.setServiceParent(self)
416 dispatcher.botmaster = self.botmaster
417
418 self.status = Status(self.botmaster, self.basedir)
419
420 self.statusTargets = []
421
422
423
424
425 self.useChanges(TestChangeMaster())
426
427 self.readConfig = False
428
430 self.dispatcher = self.slaveFactory.root.portal.realm
431
433 self.webServer = self.webTCPPort
434 del self.webTCPPort
435 self.webDistribServer = self.webUNIXPort
436 del self.webUNIXPort
437 self.configFileName = "master.cfg"
438
440
441
442 self.services = []
443 self.namedServices = {}
444 del self.change_svc
445
461
471
484
487
489 """
490 @rtype: L{buildbot.status.builder.Status}
491 """
492 return self.status
493
495 if not configFile:
496 configFile = os.path.join(self.basedir, self.configFileName)
497
498 log.msg("Creating BuildMaster -- buildbot.version: %s" % buildbot.version)
499 log.msg("loading configuration from %s" % configFile)
500 configFile = os.path.expanduser(configFile)
501
502 try:
503 f = open(configFile, "r")
504 except IOError, e:
505 log.msg("unable to open config file '%s'" % configFile)
506 log.msg("leaving old configuration in place")
507 log.err(e)
508 return
509
510 try:
511 self.loadConfig(f)
512 except:
513 log.msg("error during loadConfig")
514 log.err()
515 log.msg("The new config file is unusable, so I'll ignore it.")
516 log.msg("I will keep using the previous config file instead.")
517 f.close()
518
520 """Internal function to load a specific configuration file. Any
521 errors in the file will be signalled by raising an exception.
522
523 @return: a Deferred that will fire (with None) when the configuration
524 changes have been completed. This may involve a round-trip to each
525 buildslave that was involved."""
526
527 localDict = {'basedir': os.path.expanduser(self.basedir)}
528 try:
529 exec f in localDict
530 except:
531 log.msg("error while parsing config file")
532 raise
533
534 try:
535 config = localDict['BuildmasterConfig']
536 except KeyError:
537 log.err("missing config dictionary")
538 log.err("config file must define BuildmasterConfig")
539 raise
540
541 known_keys = ("bots", "slaves",
542 "sources", "change_source",
543 "schedulers", "builders", "mergeRequests",
544 "slavePortnum", "debugPassword", "logCompressionLimit",
545 "manhole", "status", "projectName", "projectURL",
546 "buildbotURL", "properties", "prioritizeBuilders",
547 "eventHorizon", "buildCacheSize", "logHorizon", "buildHorizon",
548 "changeHorizon",
549 )
550 for k in config.keys():
551 if k not in known_keys:
552 log.msg("unknown key '%s' defined in config dictionary" % k)
553
554 try:
555
556 schedulers = config['schedulers']
557 builders = config['builders']
558 for k in builders:
559 if k['name'].startswith("_"):
560 errmsg = ("builder names must not start with an "
561 "underscore: " + k['name'])
562 log.err(errmsg)
563 raise ValueError(errmsg)
564
565 slavePortnum = config['slavePortnum']
566
567
568
569
570 debugPassword = config.get('debugPassword')
571 manhole = config.get('manhole')
572 status = config.get('status', [])
573 projectName = config.get('projectName')
574 projectURL = config.get('projectURL')
575 buildbotURL = config.get('buildbotURL')
576 properties = config.get('properties', {})
577 buildCacheSize = config.get('buildCacheSize', None)
578 eventHorizon = config.get('eventHorizon', None)
579 logHorizon = config.get('logHorizon', None)
580 buildHorizon = config.get('buildHorizon', None)
581 logCompressionLimit = config.get('logCompressionLimit')
582 if logCompressionLimit is not None and not \
583 isinstance(logCompressionLimit, int):
584 raise ValueError("logCompressionLimit needs to be bool or int")
585 mergeRequests = config.get('mergeRequests')
586 if mergeRequests is not None and not callable(mergeRequests):
587 raise ValueError("mergeRequests must be a callable")
588 prioritizeBuilders = config.get('prioritizeBuilders')
589 if prioritizeBuilders is not None and not callable(prioritizeBuilders):
590 raise ValueError("prioritizeBuilders must be callable")
591 changeHorizon = config.get("changeHorizon")
592 if changeHorizon is not None and not isinstance(changeHorizon, int):
593 raise ValueError("changeHorizon needs to be an int")
594
595 except KeyError, e:
596 log.msg("config dictionary is missing a required parameter")
597 log.msg("leaving old configuration in place")
598 raise
599
600
601
602
603 slaves = config.get('slaves', [])
604 if "bots" in config:
605 m = ("c['bots'] is deprecated as of 0.7.6 and will be "
606 "removed by 0.8.0 . Please use c['slaves'] instead.")
607 log.msg(m)
608 warnings.warn(m, DeprecationWarning)
609 for name, passwd in config['bots']:
610 slaves.append(BuildSlave(name, passwd))
611
612 if "bots" not in config and "slaves" not in config:
613 log.msg("config dictionary must have either 'bots' or 'slaves'")
614 log.msg("leaving old configuration in place")
615 raise KeyError("must have either 'bots' or 'slaves'")
616
617
618
619
620 if changeHorizon is not None:
621 self.change_svc.changeHorizon = changeHorizon
622
623 change_source = config.get('change_source', [])
624 if isinstance(change_source, (list, tuple)):
625 change_sources = change_source
626 else:
627 change_sources = [change_source]
628 if "sources" in config:
629 m = ("c['sources'] is deprecated as of 0.7.6 and will be "
630 "removed by 0.8.0 . Please use c['change_source'] instead.")
631 log.msg(m)
632 warnings.warn(m, DeprecationWarning)
633 for s in config['sources']:
634 change_sources.append(s)
635
636
637 for s in slaves:
638 assert interfaces.IBuildSlave.providedBy(s)
639 if s.slavename in ("debug", "change", "status"):
640 raise KeyError(
641 "reserved name '%s' used for a bot" % s.slavename)
642 if config.has_key('interlocks'):
643 raise KeyError("c['interlocks'] is no longer accepted")
644
645 assert isinstance(change_sources, (list, tuple))
646 for s in change_sources:
647 assert interfaces.IChangeSource(s, None)
648
649
650 errmsg = "c['schedulers'] must be a list of Scheduler instances"
651 assert isinstance(schedulers, (list, tuple)), errmsg
652 for s in schedulers:
653 assert interfaces.IScheduler(s, None), errmsg
654 assert isinstance(status, (list, tuple))
655 for s in status:
656 assert interfaces.IStatusReceiver(s, None)
657
658 slavenames = [s.slavename for s in slaves]
659 buildernames = []
660 dirnames = []
661 for b in builders:
662 if type(b) is tuple:
663 raise ValueError("builder %s must be defined with a dict, "
664 "not a tuple" % b[0])
665 if b.has_key('slavename') and b['slavename'] not in slavenames:
666 raise ValueError("builder %s uses undefined slave %s" \
667 % (b['name'], b['slavename']))
668 for n in b.get('slavenames', []):
669 if n not in slavenames:
670 raise ValueError("builder %s uses undefined slave %s" \
671 % (b['name'], n))
672 if b['name'] in buildernames:
673 raise ValueError("duplicate builder name %s"
674 % b['name'])
675 buildernames.append(b['name'])
676 if b['builddir'] in dirnames:
677 raise ValueError("builder %s reuses builddir %s"
678 % (b['name'], b['builddir']))
679 dirnames.append(b['builddir'])
680
681 unscheduled_buildernames = buildernames[:]
682 schedulernames = []
683 for s in schedulers:
684 for b in s.listBuilderNames():
685 assert b in buildernames, \
686 "%s uses unknown builder %s" % (s, b)
687 if b in unscheduled_buildernames:
688 unscheduled_buildernames.remove(b)
689
690 if s.name in schedulernames:
691
692
693
694
695 msg = ("Schedulers must have unique names, but "
696 "'%s' was a duplicate" % (s.name,))
697 raise ValueError(msg)
698 schedulernames.append(s.name)
699
700 if unscheduled_buildernames:
701 log.msg("Warning: some Builders have no Schedulers to drive them:"
702 " %s" % (unscheduled_buildernames,))
703
704
705
706 lock_dict = {}
707 for b in builders:
708 for l in b.get('locks', []):
709 if isinstance(l, locks.LockAccess):
710 l = l.lockid
711 if lock_dict.has_key(l.name):
712 if lock_dict[l.name] is not l:
713 raise ValueError("Two different locks (%s and %s) "
714 "share the name %s"
715 % (l, lock_dict[l.name], l.name))
716 else:
717 lock_dict[l.name] = l
718
719
720
721 for s in b['factory'].steps:
722 for l in s[1].get('locks', []):
723 if isinstance(l, locks.LockAccess):
724 l = l.lockid
725 if lock_dict.has_key(l.name):
726 if lock_dict[l.name] is not l:
727 raise ValueError("Two different locks (%s and %s)"
728 " share the name %s"
729 % (l, lock_dict[l.name], l.name))
730 else:
731 lock_dict[l.name] = l
732
733 if not isinstance(properties, dict):
734 raise ValueError("c['properties'] must be a dictionary")
735
736
737 if type(slavePortnum) is int:
738 slavePortnum = "tcp:%d" % slavePortnum
739
740
741
742
743
744
745 d = defer.succeed(None)
746
747 self.projectName = projectName
748 self.projectURL = projectURL
749 self.buildbotURL = buildbotURL
750
751 self.properties = Properties()
752 self.properties.update(properties, self.configFileName)
753 if logCompressionLimit is not None:
754 self.status.logCompressionLimit = logCompressionLimit
755 if mergeRequests is not None:
756 self.botmaster.mergeRequests = mergeRequests
757 if prioritizeBuilders is not None:
758 self.botmaster.prioritizeBuilders = prioritizeBuilders
759
760 self.buildCacheSize = buildCacheSize
761 self.eventHorizon = eventHorizon
762 self.logHorizon = logHorizon
763 self.buildHorizon = buildHorizon
764
765
766
767
768 d.addCallback(lambda res: self.loadConfig_Slaves(slaves))
769
770
771 if debugPassword:
772 self.checker.addUser("debug", debugPassword)
773 self.debugPassword = debugPassword
774
775
776 if manhole != self.manhole:
777
778 if self.manhole:
779
780 d.addCallback(lambda res: self.manhole.disownServiceParent())
781 def _remove(res):
782 self.manhole = None
783 return res
784 d.addCallback(_remove)
785 if manhole:
786 def _add(res):
787 self.manhole = manhole
788 manhole.setServiceParent(self)
789 d.addCallback(_add)
790
791
792
793 d.addCallback(lambda res: self.loadConfig_Builders(builders))
794
795 d.addCallback(lambda res: self.loadConfig_status(status))
796
797
798 d.addCallback(lambda res: self.loadConfig_Schedulers(schedulers))
799
800 d.addCallback(lambda res: self.loadConfig_Sources(change_sources))
801
802
803 if self.slavePortnum != slavePortnum:
804 if self.slavePort:
805 def closeSlavePort(res):
806 d1 = self.slavePort.disownServiceParent()
807 self.slavePort = None
808 return d1
809 d.addCallback(closeSlavePort)
810 if slavePortnum is not None:
811 def openSlavePort(res):
812 self.slavePort = strports.service(slavePortnum,
813 self.slaveFactory)
814 self.slavePort.setServiceParent(self)
815 d.addCallback(openSlavePort)
816 log.msg("BuildMaster listening on port %s" % slavePortnum)
817 self.slavePortnum = slavePortnum
818
819 log.msg("configuration update started")
820 def _done(res):
821 self.readConfig = True
822 log.msg("configuration update complete")
823 d.addCallback(_done)
824 d.addCallback(lambda res: self.botmaster.maybeStartAllBuilds())
825 return d
826
828
829 self.checker.users = {}
830 for s in new_slaves:
831 self.checker.addUser(s.slavename, s.password)
832 self.checker.addUser("change", "changepw")
833
834 return self.botmaster.loadConfig_Slaves(new_slaves)
835
837 if not sources:
838 log.msg("warning: no ChangeSources specified in c['change_source']")
839
840 deleted_sources = [s for s in self.change_svc if s not in sources]
841 added_sources = [s for s in sources if s not in self.change_svc]
842 log.msg("adding %d new changesources, removing %d" %
843 (len(added_sources), len(deleted_sources)))
844 dl = [self.change_svc.removeSource(s) for s in deleted_sources]
845 def addNewOnes(res):
846 [self.change_svc.addSource(s) for s in added_sources]
847 d = defer.DeferredList(dl, fireOnOneErrback=1, consumeErrors=0)
848 d.addCallback(addNewOnes)
849 return d
850
854
855
857 oldschedulers = self.allSchedulers()
858 removed = [s for s in oldschedulers if s not in newschedulers]
859 added = [s for s in newschedulers if s not in oldschedulers]
860 dl = [defer.maybeDeferred(s.disownServiceParent) for s in removed]
861 def addNewOnes(res):
862 log.msg("adding %d new schedulers, removed %d" %
863 (len(added), len(dl)))
864 for s in added:
865 s.setServiceParent(self)
866 d = defer.DeferredList(dl, fireOnOneErrback=1)
867 d.addCallback(addNewOnes)
868 if removed or added:
869
870
871 def updateDownstreams(res):
872 log.msg("notifying downstream schedulers of changes")
873 for s in newschedulers:
874 if interfaces.IDownstreamScheduler.providedBy(s):
875 s.checkUpstreamScheduler()
876 d.addCallback(updateDownstreams)
877 return d
878
880 somethingChanged = False
881 newList = {}
882 newBuilderNames = []
883 allBuilders = self.botmaster.builders.copy()
884 for data in newBuilderData:
885 name = data['name']
886 newList[name] = data
887 newBuilderNames.append(name)
888
889
890 for oldname in self.botmaster.getBuildernames():
891 if oldname not in newList:
892 log.msg("removing old builder %s" % oldname)
893 del allBuilders[oldname]
894 somethingChanged = True
895
896 self.status.builderRemoved(oldname)
897
898
899 for name, data in newList.items():
900 old = self.botmaster.builders.get(name)
901 basedir = data['builddir']
902
903 if not old:
904
905 category = data.get('category', None)
906 log.msg("adding new builder %s for category %s" %
907 (name, category))
908 statusbag = self.status.builderAdded(name, basedir, category)
909 builder = Builder(data, statusbag)
910 allBuilders[name] = builder
911 somethingChanged = True
912 elif old.compareToSetup(data):
913
914
915 diffs = old.compareToSetup(data)
916 log.msg("updating builder %s: %s" % (name, "\n".join(diffs)))
917
918 statusbag = old.builder_status
919 statusbag.saveYourself()
920
921
922 new_builder = Builder(data, statusbag)
923 new_builder.consumeTheSoulOfYourPredecessor(old)
924
925
926
927
928 statusbag.addPointEvent(["config", "updated"])
929
930 allBuilders[name] = new_builder
931 somethingChanged = True
932 else:
933
934 log.msg("builder %s is unchanged" % name)
935 pass
936
937
938
939 for builder in allBuilders.values():
940 builder.builder_status.reconfigFromBuildmaster(self)
941
942
943 if somethingChanged:
944 sortedAllBuilders = [allBuilders[name] for name in newBuilderNames]
945 d = self.botmaster.setBuilders(sortedAllBuilders)
946 return d
947 return None
948
950 dl = []
951
952
953 for s in self.statusTargets[:]:
954 if not s in status:
955 log.msg("removing IStatusReceiver", s)
956 d = defer.maybeDeferred(s.disownServiceParent)
957 dl.append(d)
958 self.statusTargets.remove(s)
959
960 def addNewOnes(res):
961 for s in status:
962 if not s in self.statusTargets:
963 log.msg("adding IStatusReceiver", s)
964 s.setServiceParent(self)
965 self.statusTargets.append(s)
966 d = defer.DeferredList(dl, fireOnOneErrback=1)
967 d.addCallback(addNewOnes)
968 return d
969
970
974
976
977 builders = []
978 for name in bs.builderNames:
979 b = self.botmaster.builders.get(name)
980 if b:
981 if b not in builders:
982 builders.append(b)
983 continue
984
985 raise KeyError("no such builder named '%s'" % name)
986
987
988
989 bs.start(builders)
990 self.status.buildsetSubmitted(bs.status)
991
992
1008
1009 components.registerAdapter(Control, BuildMaster, interfaces.IControl)
1010
1011
1012
1013