1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17 import re
18 import os
19 import signal
20 import textwrap
21 import socket
22
23 from zope.interface import implements
24 from twisted.python import log, components
25 from twisted.internet import defer, reactor
26 from twisted.application import service
27 from twisted.application.internet import TimerService
28
29 import buildbot
30 import buildbot.pbmanager
31 from buildbot.util import safeTranslate, subscription, epoch2datetime
32 from buildbot.process.builder import Builder
33 from buildbot.status.master import Status
34 from buildbot.changes import changes
35 from buildbot.changes.manager import ChangeManager
36 from buildbot import interfaces, locks
37 from buildbot.process.properties import Properties
38 from buildbot.config import BuilderConfig, MasterConfig
39 from buildbot.process.builder import BuilderControl
40 from buildbot.db import connector, exceptions
41 from buildbot.schedulers.manager import SchedulerManager
42 from buildbot.schedulers.base import isScheduler
43 from buildbot.process.botmaster import BotMaster
44 from buildbot.process import debug
45 from buildbot.process import metrics
46 from buildbot.process import cache
47 from buildbot.status.results import SUCCESS, WARNINGS, FAILURE
48 from buildbot import monkeypatches
53
55 '''holds log rotation parameters (for WebStatus)'''
57 self.rotateLength = 1 * 1000 * 1000
58 self.maxRotatedFiles = 10
59
61 debug = 0
62 manhole = None
63 debugPassword = None
64 title = "(unspecified)"
65 titleURL = None
66 buildbotURL = None
67 change_svc = None
68 properties = Properties()
69
70
71
72 RECLAIM_BUILD_INTERVAL = 10*60
73
74
75
76 UNCLAIMED_BUILD_FACTOR = 6
77
78
79
80
81 WARNING_UNCLAIMED_COUNT = 10000
82
83 - def __init__(self, basedir, configFileName="master.cfg"):
146
164
167
169 """
170 @rtype: L{buildbot.status.builder.Status}
171 """
172 return self.status
173
175 if not configFile:
176 configFile = os.path.join(self.basedir, self.configFileName)
177
178 log.msg("Creating BuildMaster -- buildbot.version: %s" % buildbot.version)
179 log.msg("loading configuration from %s" % configFile)
180 configFile = os.path.expanduser(configFile)
181
182 try:
183 f = open(configFile, "r")
184 except IOError, e:
185 log.msg("unable to open config file '%s'" % configFile)
186 log.msg("leaving old configuration in place")
187 log.err(e)
188 return
189
190 try:
191 d = self.loadConfig(f)
192 except:
193 log.msg("error during loadConfig")
194 log.err()
195 log.msg("The new config file is unusable, so I'll ignore it.")
196 log.msg("I will keep using the previous config file instead.")
197 return
198 f.close()
199 return d
200
202 """Internal function to load a specific configuration file. Any
203 errors in the file will be signalled by raising a failure. Returns
204 a deferred.
205 """
206
207
208
209 d = defer.succeed(None)
210
211 def do_load(_):
212 log.msg("configuration update started")
213
214
215
216 localDict = {'basedir': os.path.expanduser(self.basedir),
217 '__file__': os.path.abspath(self.configFileName)}
218
219 try:
220 exec f in localDict
221 except:
222 log.msg("error while parsing config file")
223 raise
224
225 try:
226 config = localDict['BuildmasterConfig']
227 except KeyError:
228 log.err("missing config dictionary")
229 log.err("config file must define BuildmasterConfig")
230 raise
231
232
233
234 known_keys = ("slaves", "change_source",
235 "schedulers", "builders", "mergeRequests",
236 "slavePortnum", "debugPassword", "logCompressionLimit",
237 "manhole", "status", "projectName", "projectURL",
238 "title", "titleURL",
239 "buildbotURL", "properties", "prioritizeBuilders",
240 "eventHorizon", "buildCacheSize", "changeCacheSize",
241 "logHorizon", "buildHorizon", "changeHorizon",
242 "logMaxSize", "logMaxTailSize", "logCompressionMethod",
243 "db_url", "multiMaster", "db_poll_interval",
244 "metrics", "caches"
245 )
246 for k in config.keys():
247 if k not in known_keys:
248 log.msg("unknown key '%s' defined in config dictionary" % k)
249
250
251
252 try:
253
254 schedulers = config['schedulers']
255 builders = config['builders']
256 slavePortnum = config['slavePortnum']
257
258
259
260
261 db_url = config.get("db_url", "sqlite:///state.sqlite")
262 db_poll_interval = config.get("db_poll_interval", None)
263 debugPassword = config.get('debugPassword')
264 manhole = config.get('manhole')
265 status = config.get('status', [])
266
267
268 title = config.get('title', config.get('projectName'))
269 titleURL = config.get('titleURL', config.get('projectURL'))
270 buildbotURL = config.get('buildbotURL')
271 properties = config.get('properties', {})
272 buildCacheSize = config.get('buildCacheSize', None)
273 changeCacheSize = config.get('changeCacheSize', None)
274 eventHorizon = config.get('eventHorizon', 50)
275 logHorizon = config.get('logHorizon', None)
276 buildHorizon = config.get('buildHorizon', None)
277 logCompressionLimit = config.get('logCompressionLimit', 4*1024)
278 if logCompressionLimit is not None and not \
279 isinstance(logCompressionLimit, int):
280 raise ValueError("logCompressionLimit needs to be bool or int")
281 logCompressionMethod = config.get('logCompressionMethod', "bz2")
282 if logCompressionMethod not in ('bz2', 'gz'):
283 raise ValueError("logCompressionMethod needs to be 'bz2', or 'gz'")
284 logMaxSize = config.get('logMaxSize')
285 if logMaxSize is not None and not \
286 isinstance(logMaxSize, int):
287 raise ValueError("logMaxSize needs to be None or int")
288 logMaxTailSize = config.get('logMaxTailSize')
289 if logMaxTailSize is not None and not \
290 isinstance(logMaxTailSize, int):
291 raise ValueError("logMaxTailSize needs to be None or int")
292 mergeRequests = config.get('mergeRequests')
293 if (mergeRequests not in (None, True, False)
294 and not callable(mergeRequests)):
295 raise ValueError("mergeRequests must be a callable or False")
296 prioritizeBuilders = config.get('prioritizeBuilders')
297 if prioritizeBuilders is not None and not callable(prioritizeBuilders):
298 raise ValueError("prioritizeBuilders must be callable")
299 changeHorizon = config.get("changeHorizon")
300 if changeHorizon is not None and not isinstance(changeHorizon, int):
301 raise ValueError("changeHorizon needs to be an int")
302
303 multiMaster = config.get("multiMaster", False)
304
305 metrics_config = config.get("metrics")
306 caches_config = config.get("caches", {})
307
308
309
310 validation_defaults = {
311 'branch' : re.compile(r'^[\w.+/~-]*$'),
312 'revision' : re.compile(r'^[ \w\.\-\/]*$'),
313 'property_name' : re.compile(r'^[\w\.\-\/\~:]*$'),
314 'property_value' : re.compile(r'^[\w\.\-\/\~:]*$'),
315 }
316 validation_config = validation_defaults.copy()
317 validation_config.update(config.get("validation", {}))
318 v_config_keys = set(validation_config.keys())
319 v_default_keys = set(validation_defaults.keys())
320 if v_config_keys > v_default_keys:
321 raise ValueError("unrecognized validation key(s): %s" %
322 (", ".join(v_config_keys - v_default_keys,)))
323
324 except KeyError:
325 log.msg("config dictionary is missing a required parameter")
326 log.msg("leaving old configuration in place")
327 raise
328
329 if "sources" in config:
330 m = ("c['sources'] is deprecated as of 0.7.6 and is no longer "
331 "accepted in >= 0.8.0 . Please use c['change_source'] instead.")
332 raise KeyError(m)
333
334 if "bots" in config:
335 m = ("c['bots'] is deprecated as of 0.7.6 and is no longer "
336 "accepted in >= 0.8.0 . Please use c['slaves'] instead.")
337 raise KeyError(m)
338
339
340 self.loadConfig_Metrics(metrics_config)
341 self.loadConfig_Caches(caches_config, buildCacheSize,
342 changeCacheSize)
343
344 slaves = config.get('slaves', [])
345 if "slaves" not in config:
346 log.msg("config dictionary must have a 'slaves' key")
347 log.msg("leaving old configuration in place")
348 raise KeyError("must have a 'slaves' key")
349
350 self.config.changeHorizon = changeHorizon
351 self.config.validation = validation_config
352
353 change_source = config.get('change_source', [])
354 if isinstance(change_source, (list, tuple)):
355 change_sources = change_source
356 else:
357 change_sources = [change_source]
358
359
360 for s in slaves:
361 assert interfaces.IBuildSlave.providedBy(s)
362 if s.slavename in ("debug", "change", "status"):
363 raise KeyError(
364 "reserved name '%s' used for a bot" % s.slavename)
365 if config.has_key('interlocks'):
366 raise KeyError("c['interlocks'] is no longer accepted")
367 assert self.db_url is None or db_url == self.db_url, \
368 "Cannot change db_url after master has started"
369 assert db_poll_interval is None or isinstance(db_poll_interval, int), \
370 "db_poll_interval must be an integer: seconds between polls"
371 assert self.db_poll_interval is _Unset or db_poll_interval == self.db_poll_interval, \
372 "Cannot change db_poll_interval after master has started"
373
374 assert isinstance(change_sources, (list, tuple))
375 for s in change_sources:
376 assert interfaces.IChangeSource(s, None)
377 self.checkConfig_Schedulers(schedulers)
378 assert isinstance(status, (list, tuple))
379 for s in status:
380 assert interfaces.IStatusReceiver(s, None)
381
382 slavenames = [s.slavename for s in slaves]
383 buildernames = []
384 dirnames = []
385
386
387 builders_dicts = []
388 for b in builders:
389 if isinstance(b, BuilderConfig):
390 builders_dicts.append(b.getConfigDict())
391 elif type(b) is dict:
392 builders_dicts.append(b)
393 else:
394 raise ValueError("builder %s is not a BuilderConfig object (or a dict)" % b)
395 builders = builders_dicts
396
397 for b in builders:
398 if b.has_key('slavename') and b['slavename'] not in slavenames:
399 raise ValueError("builder %s uses undefined slave %s" \
400 % (b['name'], b['slavename']))
401 for n in b.get('slavenames', []):
402 if n not in slavenames:
403 raise ValueError("builder %s uses undefined slave %s" \
404 % (b['name'], n))
405 if b['name'] in buildernames:
406 raise ValueError("duplicate builder name %s"
407 % b['name'])
408 buildernames.append(b['name'])
409
410
411 if b['name'].startswith("_"):
412 errmsg = ("builder names must not start with an "
413 "underscore: " + b['name'])
414 log.err(errmsg)
415 raise ValueError(errmsg)
416
417
418
419 b.setdefault('builddir', safeTranslate(b['name']))
420 b.setdefault('slavebuilddir', b['builddir'])
421 b.setdefault('buildHorizon', buildHorizon)
422 b.setdefault('logHorizon', logHorizon)
423 b.setdefault('eventHorizon', eventHorizon)
424 if b['builddir'] in dirnames:
425 raise ValueError("builder %s reuses builddir %s"
426 % (b['name'], b['builddir']))
427 dirnames.append(b['builddir'])
428
429 unscheduled_buildernames = buildernames[:]
430 schedulernames = []
431 for s in schedulers:
432 for b in s.listBuilderNames():
433
434 if not multiMaster:
435 assert b in buildernames, \
436 "%s uses unknown builder %s" % (s, b)
437 if b in unscheduled_buildernames:
438 unscheduled_buildernames.remove(b)
439
440 if s.name in schedulernames:
441 msg = ("Schedulers must have unique names, but "
442 "'%s' was a duplicate" % (s.name,))
443 raise ValueError(msg)
444 schedulernames.append(s.name)
445
446
447 if not multiMaster and unscheduled_buildernames:
448 log.msg("Warning: some Builders have no Schedulers to drive them:"
449 " %s" % (unscheduled_buildernames,))
450
451
452
453 lock_dict = {}
454 for b in builders:
455 for l in b.get('locks', []):
456 if isinstance(l, locks.LockAccess):
457 l = l.lockid
458 if lock_dict.has_key(l.name):
459 if lock_dict[l.name] is not l:
460 raise ValueError("Two different locks (%s and %s) "
461 "share the name %s"
462 % (l, lock_dict[l.name], l.name))
463 else:
464 lock_dict[l.name] = l
465
466
467
468 for s in b['factory'].steps:
469 for l in s[1].get('locks', []):
470 if isinstance(l, locks.LockAccess):
471 l = l.lockid
472 if lock_dict.has_key(l.name):
473 if lock_dict[l.name] is not l:
474 raise ValueError("Two different locks (%s and %s)"
475 " share the name %s"
476 % (l, lock_dict[l.name], l.name))
477 else:
478 lock_dict[l.name] = l
479
480 if not isinstance(properties, dict):
481 raise ValueError("c['properties'] must be a dictionary")
482
483
484 if type(slavePortnum) is int:
485 slavePortnum = "tcp:%d" % slavePortnum
486
487
488 if checkOnly:
489 return config
490
491 self.title = title
492 self.titleURL = titleURL
493 self.buildbotURL = buildbotURL
494
495 self.properties = Properties()
496 self.properties.update(properties, self.configFileName)
497
498 self.status.logCompressionLimit = logCompressionLimit
499 self.status.logCompressionMethod = logCompressionMethod
500 self.status.logMaxSize = logMaxSize
501 self.status.logMaxTailSize = logMaxTailSize
502
503
504
505 for builder in self.botmaster.builders.values():
506 builder.builder_status.setLogCompressionLimit(logCompressionLimit)
507 builder.builder_status.setLogCompressionMethod(logCompressionMethod)
508 builder.builder_status.setLogMaxSize(logMaxSize)
509 builder.builder_status.setLogMaxTailSize(logMaxTailSize)
510
511 if mergeRequests is not None:
512 self.botmaster.mergeRequests = mergeRequests
513 if prioritizeBuilders is not None:
514 self.botmaster.prioritizeBuilders = prioritizeBuilders
515
516 self.buildCacheSize = buildCacheSize
517 self.changeCacheSize = changeCacheSize
518 self.eventHorizon = eventHorizon
519 self.logHorizon = logHorizon
520 self.buildHorizon = buildHorizon
521 self.slavePortnum = slavePortnum
522
523
524 d.addCallback(lambda res:
525 self.loadConfig_Database(db_url, db_poll_interval))
526
527
528 d.addCallback(lambda res: self.loadConfig_Slaves(slaves))
529
530
531 if manhole != self.manhole:
532
533 if self.manhole:
534
535 d.addCallback(lambda res: self.manhole.disownServiceParent())
536 def _remove(res):
537 self.manhole = None
538 return res
539 d.addCallback(_remove)
540 if manhole:
541 def _add(res):
542 self.manhole = manhole
543 manhole.setServiceParent(self)
544 d.addCallback(_add)
545
546
547
548 d.addCallback(lambda res: self.loadConfig_Builders(builders))
549
550 d.addCallback(lambda res: self.loadConfig_Status(status))
551
552
553 d.addCallback(lambda _: self.loadConfig_Schedulers(schedulers))
554
555
556 d.addCallback(lambda res: self.loadConfig_Sources(change_sources))
557
558
559 d.addCallback(lambda res: self.loadConfig_DebugClient(debugPassword))
560
561 d.addCallback(do_load)
562
563 def _done(res):
564 self.readConfig = True
565 log.msg("configuration update complete")
566
567 if not checkOnly:
568 d.addCallback(_done)
569 d.addErrback(log.err)
570 return d
571
585
588 if buildCacheSize is not None:
589 caches_config['builds'] = buildCacheSize
590 if changeCacheSize is not None:
591 caches_config['changes'] = changeCacheSize
592 self.caches.load_config(caches_config)
593
595 if self.db:
596 return
597
598 self.db = connector.DBConnector(self, db_url, self.basedir)
599 self.db.setServiceParent(self)
600
601
602 d = self.db.model.is_current()
603 def check_current(res):
604 if res:
605 return
606 raise exceptions.DatabaseNotReadyError, textwrap.dedent("""
607 The Buildmaster database needs to be upgraded before this version of buildbot
608 can run. Use the following command-line
609 buildbot upgrade-master path/to/master
610 to upgrade the database, and try starting the buildmaster again. You may want
611 to make a backup of your buildmaster before doing so. If you are using MySQL,
612 you must specify the connector string on the upgrade-master command line:
613 buildbot upgrade-master --db=<db-url> path/to/master
614 """)
615 d.addCallback(check_current)
616
617
618 def set_up_db_dependents(r):
619
620 self._change_subs.subscribe(self.status.changeAdded)
621
622
623
624
625
626 if db_poll_interval:
627 t1 = TimerService(db_poll_interval, self.pollDatabase)
628 t1.setServiceParent(self)
629
630
631
632 d.addCallback(set_up_db_dependents)
633 return d
634
636 self.db_url = db_url
637 self.db_poll_interval = db_poll_interval
638 return self.loadDatabase(db_url, db_poll_interval)
639
642
644 timer = metrics.Timer("BuildMaster.loadConfig_Sources()")
645 timer.start()
646 if not sources:
647 log.msg("warning: no ChangeSources specified in c['change_source']")
648
649 deleted_sources = [s for s in self.change_svc if s not in sources]
650 added_sources = [s for s in sources if s not in self.change_svc]
651 log.msg("adding %d new changesources, removing %d" %
652 (len(added_sources), len(deleted_sources)))
653 dl = [self.change_svc.removeSource(s) for s in deleted_sources]
654 def addNewOnes(res):
655 [self.change_svc.addSource(s) for s in added_sources]
656 d = defer.DeferredList(dl, fireOnOneErrback=1, consumeErrors=0)
657 d.addCallback(addNewOnes)
658
659 def logCount(_):
660 timer.stop()
661 metrics.MetricCountEvent.log("num_sources",
662 len(list(self.change_svc)), absolute=True)
663 return _
664 d.addBoth(logCount)
665 return d
666
680 d.addCallback(reg)
681 return d
682
684 return list(self.scheduler_manager)
685
687 timer = metrics.Timer("BuildMaster.loadConfig_Builders()")
688 timer.start()
689 somethingChanged = False
690 newList = {}
691 newBuilderNames = []
692 allBuilders = self.botmaster.builders.copy()
693 for data in newBuilderData:
694 name = data['name']
695 newList[name] = data
696 newBuilderNames.append(name)
697
698
699 for oldname in self.botmaster.getBuildernames():
700 if oldname not in newList:
701 log.msg("removing old builder %s" % oldname)
702 del allBuilders[oldname]
703 somethingChanged = True
704
705 self.status.builderRemoved(oldname)
706
707
708 for name, data in newList.items():
709 old = self.botmaster.builders.get(name)
710 basedir = data['builddir']
711
712 if not old:
713
714 category = data.get('category', None)
715 log.msg("adding new builder %s for category %s" %
716 (name, category))
717 statusbag = self.status.builderAdded(name, basedir, category)
718 builder = Builder(data, statusbag)
719 allBuilders[name] = builder
720 somethingChanged = True
721 elif old.compareToSetup(data):
722
723
724 diffs = old.compareToSetup(data)
725 log.msg("updating builder %s: %s" % (name, "\n".join(diffs)))
726
727 statusbag = old.builder_status
728 statusbag.saveYourself()
729
730
731 new_builder = Builder(data, statusbag)
732 new_builder.consumeTheSoulOfYourPredecessor(old)
733
734
735
736
737 statusbag.addPointEvent(["config", "updated"])
738
739 allBuilders[name] = new_builder
740 somethingChanged = True
741 else:
742
743 log.msg("builder %s is unchanged" % name)
744 pass
745
746
747
748 for builder in allBuilders.values():
749 builder.builder_status.reconfigFromBuildmaster(self)
750
751 metrics.MetricCountEvent.log("num_builders",
752 len(allBuilders), absolute=True)
753
754
755 if somethingChanged:
756 sortedAllBuilders = [allBuilders[name] for name in newBuilderNames]
757 d = self.botmaster.setBuilders(sortedAllBuilders)
758 def stop_timer(_):
759 timer.stop()
760 return _
761 d.addBoth(stop_timer)
762 return d
763
764 return None
765
785 d = defer.DeferredList(dl, fireOnOneErrback=1)
786 d.addCallback(addNewOnes)
787
788 def logCount(_):
789 timer.stop()
790 metrics.MetricCountEvent.log("num_status",
791 len(self.statusTargets), absolute=True)
792 return _
793 d.addBoth(logCount)
794
795 return d
796
804
814 d.addBoth(logCount)
815 return d
816
817
818
820 """
821 Return the obejct id for this master, for associating state with the master.
822
823 @returns: ID, via Deferred
824 """
825
826 if self._object_id is not None:
827 return defer.succeed(self._object_id)
828
829
830
831 try:
832 hostname = os.uname()[1]
833 except AttributeError:
834 hostname = socket.getfqdn()
835 master_name = "%s:%s" % (hostname, os.path.abspath(self.basedir))
836
837 d = self.db.state.getObjectId(master_name, "BuildMaster")
838 def keep(id):
839 self._object_id = id
840 d.addCallback(keep)
841 return d
842
843
844
845
846 - def addChange(self, who=None, files=None, comments=None, author=None,
847 isdir=None, is_dir=None, links=None, revision=None, when=None,
848 when_timestamp=None, branch=None, category=None, revlink='',
849 properties={}, repository='', project=''):
850 """
851 Add a change to the buildmaster and act on it.
852
853 This is a wrapper around L{ChangesConnectorComponent.addChange} which
854 also acts on the resulting change and returns a L{Change} instance.
855
856 Note that all parameters are keyword arguments, although C{who},
857 C{files}, and C{comments} can be specified positionally for
858 backward-compatibility.
859
860 @param author: the author of this change
861 @type author: unicode string
862
863 @param who: deprecated name for C{author}
864
865 @param files: a list of filenames that were changed
866 @type branch: list of unicode strings
867
868 @param comments: user comments on the change
869 @type branch: unicode string
870
871 @param is_dir: deprecated
872
873 @param isdir: deprecated name for C{is_dir}
874
875 @param links: a list of links related to this change, e.g., to web
876 viewers or review pages
877 @type links: list of unicode strings
878
879 @param revision: the revision identifier for this change
880 @type revision: unicode string
881
882 @param when_timestamp: when this change occurred, or the current time
883 if None
884 @type when_timestamp: datetime instance or None
885
886 @param when: deprecated name and type for C{when_timestamp}
887 @type when: integer (UNIX epoch time) or None
888
889 @param branch: the branch on which this change took place
890 @type branch: unicode string
891
892 @param category: category for this change (arbitrary use by Buildbot
893 users)
894 @type category: unicode string
895
896 @param revlink: link to a web view of this revision
897 @type revlink: unicode string
898
899 @param properties: properties to set on this change
900 @type properties: dictionary with string keys and simple values
901 (JSON-able). Note that the property source is I{not} included
902 in this dictionary.
903
904 @param repository: the repository in which this change took place
905 @type repository: unicode string
906
907 @param project: the project this change is a part of
908 @type project: unicode string
909
910 @returns: L{Change} instance via Deferred
911 """
912 metrics.MetricCountEvent.log("added_changes", 1)
913
914
915 def handle_deprec(oldname, old, newname, new, default=None,
916 converter = lambda x:x):
917 if old is not None:
918 if new is None:
919 log.msg("WARNING: change source is using deprecated "
920 "addChange parameter '%s'" % oldname)
921 return converter(old)
922 raise TypeError("Cannot provide '%s' and '%s' to addChange"
923 % (oldname, newname))
924 if new is None:
925 new = default
926 return new
927
928 author = handle_deprec("who", who, "author", author)
929 is_dir = handle_deprec("isdir", isdir, "is_dir", is_dir,
930 default=0)
931 when_timestamp = handle_deprec("when", when,
932 "when_timestamp", when_timestamp,
933 converter=epoch2datetime)
934
935
936 for n in properties:
937 properties[n] = (properties[n], 'Change')
938
939 d = self.db.changes.addChange(author=author, files=files,
940 comments=comments, is_dir=is_dir, links=links,
941 revision=revision, when_timestamp=when_timestamp,
942 branch=branch, category=category, revlink=revlink,
943 properties=properties, repository=repository, project=project)
944
945
946 d.addCallback(lambda changeid :
947 self.db.changes.getChange(changeid))
948 d.addCallback(lambda chdict :
949 changes.Change.fromChdict(self, chdict))
950
951 def notify(change):
952 msg = u"added change %s to database" % change
953 log.msg(msg.encode('utf-8', 'replace'))
954
955 if not self.db_poll_interval:
956 self._change_subs.deliver(change)
957 return change
958 d.addCallback(notify)
959 return d
960
962 """
963 Request that C{callback} be called with each Change object added to the
964 cluster.
965
966 Note: this method will go away in 0.9.x
967 """
968 return self._change_subs.subscribe(callback)
969
971 """
972 Add a buildset to the buildmaster and act on it. Interface is
973 identical to
974 L{buildbot.db.buildsets.BuildsetConnectorComponent.addBuildset},
975 including returning a Deferred, but also potentially triggers the
976 resulting builds.
977 """
978 d = self.db.buildsets.addBuildset(**kwargs)
979 def notify((bsid,brids)):
980 log.msg("added buildset %d to database" % bsid)
981
982 self._new_buildset_subs.deliver(bsid=bsid, **kwargs)
983
984 if not self.db_poll_interval:
985 for bn, brid in brids.iteritems():
986 self.buildRequestAdded(bsid=bsid, brid=brid,
987 buildername=bn)
988 return (bsid,brids)
989 d.addCallback(notify)
990 return d
991
993 """
994 Request that C{callback(bsid=bsid, ssid=ssid, reason=reason,
995 properties=properties, builderNames=builderNames,
996 external_idstring=external_idstring)} be called whenever a buildset is
997 added. Properties is a dictionary as expected for
998 L{BuildsetsConnectorComponent.addBuildset}.
999
1000 Note that this only works for buildsets added on this master.
1001
1002 Note: this method will go away in 0.9.x
1003 """
1004 return self._new_buildset_subs.subscribe(callback)
1005
1006 @defer.deferredGenerator
1008 """
1009 Instructs the master to check whether the buildset is complete,
1010 and notify appropriately if it is.
1011
1012 Note that buildset completions are only reported on the master
1013 on which the last build request completes.
1014 """
1015 wfd = defer.waitForDeferred(
1016 self.db.buildrequests.getBuildRequests(bsid=bsid, complete=False))
1017 yield wfd
1018 brdicts = wfd.getResult()
1019
1020
1021 if brdicts:
1022 return
1023
1024 wfd = defer.waitForDeferred(
1025 self.db.buildrequests.getBuildRequests(bsid=bsid))
1026 yield wfd
1027 brdicts = wfd.getResult()
1028
1029
1030 cumulative_results = SUCCESS
1031 for brdict in brdicts:
1032 if brdict['results'] not in (SUCCESS, WARNINGS):
1033 cumulative_results = FAILURE
1034
1035
1036 wfd = defer.waitForDeferred(
1037 self.db.buildsets.completeBuildset(bsid, cumulative_results))
1038 yield wfd
1039 wfd.getResult()
1040
1041
1042 self._buildsetComplete(bsid, cumulative_results)
1043
1046
1048 """
1049 Request that C{callback(bsid, result)} be called whenever a
1050 buildset is complete.
1051
1052 Note: this method will go away in 0.9.x
1053 """
1054 return self._complete_buildset_subs.subscribe(callback)
1055
1057 """
1058 Notifies the master that a build request is available to be claimed;
1059 this may be a brand new build request, or a build request that was
1060 previously claimed and unclaimed through a timeout or other calamity.
1061
1062 @param bsid: containing buildset id
1063 @param brid: buildrequest ID
1064 @param buildername: builder named by the build request
1065 """
1066 self._new_buildrequest_subs.deliver(
1067 dict(bsid=bsid, brid=brid, buildername=buildername))
1068
1070 """
1071 Request that C{callback} be invoked with a dictionary with keys C{brid}
1072 (the build request id), C{bsid} (buildset id) and C{buildername}
1073 whenever a new build request is added to the database. Note that, due
1074 to the delayed nature of subscriptions, the build request may already
1075 be claimed by the time C{callback} is invoked.
1076
1077 Note: this method will go away in 0.9.x
1078 """
1079 return self._new_buildrequest_subs.subscribe(callback)
1080
1081
1082
1083
1097
1098 _last_processed_change = None
1099 @defer.deferredGenerator
1170
1171 _last_unclaimed_brids_set = None
1172 _last_claim_cleanup = 0
1173 @defer.deferredGenerator
1223
1224
1225
1226 _master_objectid = None
1227
1235 d.addCallback(keep)
1236 return d
1237 return defer.succeed(self._master_objectid)
1238
1244 d.addCallback(get)
1245 return d
1246
1252 d.addCallback(set)
1253 return d
1254
1270
1271 components.registerAdapter(Control, BuildMaster, interfaces.IControl)
1272