Package buildbot :: Module master
[frames] | no frames]

Source Code for Module buildbot.master

  1  # This file is part of Buildbot.  Buildbot is free software: you can 
  2  # redistribute it and/or modify it under the terms of the GNU General Public 
  3  # License as published by the Free Software Foundation, version 2. 
  4  # 
  5  # This program is distributed in the hope that it will be useful, but WITHOUT 
  6  # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 
  7  # FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more 
  8  # details. 
  9  # 
 10  # You should have received a copy of the GNU General Public License along with 
 11  # this program; if not, write to the Free Software Foundation, Inc., 51 
 12  # Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 
 13  # 
 14  # Copyright Buildbot Team Members 
 15   
 16   
 17  import os 
 18  import signal 
 19  import socket 
 20   
 21  from zope.interface import implements 
 22  from twisted.python import log, components, failure 
 23  from twisted.internet import defer, reactor, task 
 24  from twisted.application import service 
 25   
 26  import buildbot 
 27  import buildbot.pbmanager 
 28  from buildbot.util import subscription, epoch2datetime 
 29  from buildbot.status.master import Status 
 30  from buildbot.changes import changes 
 31  from buildbot.changes.manager import ChangeManager 
 32  from buildbot import interfaces 
 33  from buildbot.process.builder import BuilderControl 
 34  from buildbot.db import connector 
 35  from buildbot.schedulers.manager import SchedulerManager 
 36  from buildbot.process.botmaster import BotMaster 
 37  from buildbot.process import debug 
 38  from buildbot.process import metrics 
 39  from buildbot.process import cache 
 40  from buildbot.process.users import users 
 41  from buildbot.process.users.manager import UserManagerManager 
 42  from buildbot.status.results import SUCCESS, WARNINGS, FAILURE 
 43  from buildbot import monkeypatches 
 44  from buildbot import config 
45 46 ######################################## 47 48 -class LogRotation(object):
49 - def __init__(self):
50 self.rotateLength = 1 * 1000 * 1000 51 self.maxRotatedFiles = 10
52
53 -class BuildMaster(config.ReconfigurableServiceMixin, service.MultiService):
54 55 # frequency with which to reclaim running builds; this should be set to 56 # something fairly long, to avoid undue database load 57 RECLAIM_BUILD_INTERVAL = 10*60 58 59 # multiplier on RECLAIM_BUILD_INTERVAL at which a build is considered 60 # unclaimed; this should be at least 2 to avoid false positives 61 UNCLAIMED_BUILD_FACTOR = 6 62 63 # if this quantity of unclaimed build requests are present in the table, 64 # then something is probably wrong! The master will log a WARNING on every 65 # database poll operation. 66 WARNING_UNCLAIMED_COUNT = 10000 67
68 - def __init__(self, basedir, configFileName="master.cfg"):
69 service.MultiService.__init__(self) 70 self.setName("buildmaster") 71 72 self.basedir = basedir 73 assert os.path.isdir(self.basedir) 74 self.configFileName = configFileName 75 76 # set up child services 77 self.create_child_services() 78 79 # loop for polling the db 80 self.db_loop = None 81 82 # configuration / reconfiguration handling 83 self.config = config.MasterConfig() 84 self.reconfig_active = False 85 self.reconfig_requested = False 86 self.reconfig_notifier = None 87 88 # this stores parameters used in the tac file, and is accessed by the 89 # WebStatus to duplicate those values. 90 self.log_rotation = LogRotation() 91 92 # subscription points 93 self._change_subs = \ 94 subscription.SubscriptionPoint("changes") 95 self._new_buildrequest_subs = \ 96 subscription.SubscriptionPoint("buildrequest_additions") 97 self._new_buildset_subs = \ 98 subscription.SubscriptionPoint("buildset_additions") 99 self._complete_buildset_subs = \ 100 subscription.SubscriptionPoint("buildset_completion") 101 102 # local cache for this master's object ID 103 self._object_id = None
104 105
106 - def create_child_services(self):
107 # note that these are order-dependent. If you get the order wrong, 108 # you'll know it, as the master will fail to start. 109 110 self.metrics = metrics.MetricLogObserver() 111 self.metrics.setServiceParent(self) 112 113 self.caches = cache.CacheManager() 114 self.caches.setServiceParent(self) 115 116 self.pbmanager = buildbot.pbmanager.PBManager() 117 self.pbmanager.setServiceParent(self) 118 119 self.change_svc = ChangeManager(self) 120 self.change_svc.setServiceParent(self) 121 122 self.botmaster = BotMaster(self) 123 self.botmaster.setServiceParent(self) 124 125 self.scheduler_manager = SchedulerManager(self) 126 self.scheduler_manager.setServiceParent(self) 127 128 self.user_manager = UserManagerManager(self) 129 self.user_manager.setServiceParent(self) 130 131 self.db = connector.DBConnector(self, self.basedir) 132 self.db.setServiceParent(self) 133 134 self.debug = debug.DebugServices(self) 135 self.debug.setServiceParent(self) 136 137 self.status = Status(self) 138 self.status.setServiceParent(self)
139 140 # setup and reconfig handling 141 142 _already_started = False 143 @defer.deferredGenerator
144 - def startService(self, _reactor=reactor):
145 assert not self._already_started, "can only start the master once" 146 self._already_started = True 147 148 log.msg("Starting BuildMaster -- buildbot.version: %s" % 149 buildbot.version) 150 151 # first, apply all monkeypatches 152 monkeypatches.patch_all() 153 154 # we want to wait until the reactor is running, so we can call 155 # reactor.stop() for fatal errors 156 d = defer.Deferred() 157 _reactor.callWhenRunning(d.callback, None) 158 wfd = defer.waitForDeferred(d) 159 yield wfd 160 wfd.getResult() 161 162 try: 163 # load the configuration file, treating errors as fatal 164 try: 165 self.config = config.MasterConfig.loadConfig(self.basedir, 166 self.configFileName) 167 except config.ConfigErrors, e: 168 log.msg("Configuration Errors:") 169 for msg in e.errors: 170 log.msg(" " + msg) 171 log.msg("Halting master.") 172 _reactor.stop() 173 return 174 except: 175 log.err(failure.Failure(), 'while starting BuildMaster') 176 _reactor.stop() 177 return 178 179 # set up services that need access to the config before everything else 180 # gets told to reconfig 181 try: 182 wfd = defer.waitForDeferred( 183 self.db.setup()) 184 yield wfd 185 wfd.getResult() 186 except connector.DatabaseNotReadyError: 187 # (message was already logged) 188 _reactor.stop() 189 return 190 191 if hasattr(signal, "SIGHUP"): 192 def sighup(*args): 193 _reactor.callLater(0, self.reconfig)
194 signal.signal(signal.SIGHUP, sighup) 195 196 # call the parent method 197 wfd = defer.waitForDeferred( 198 defer.maybeDeferred(lambda : 199 service.MultiService.startService(self))) 200 yield wfd 201 wfd.getResult() 202 203 # give all services a chance to load the new configuration, rather than 204 # the base configuration 205 wfd = defer.waitForDeferred( 206 self.reconfigService(self.config)) 207 yield wfd 208 wfd.getResult() 209 except: 210 f = failure.Failure() 211 log.err(f, 'while starting BuildMaster') 212 _reactor.stop() 213 214 log.msg("BuildMaster is running")
215 216
217 - def stopService(self):
218 if self.db_loop: 219 self.db_loop.stop() 220 self.db_loop = None
221 222
223 - def reconfig(self):
224 # this method wraps doConfig, ensuring it is only ever called once at 225 # a time, and alerting the user if the reconfig takes too long 226 if self.reconfig_active: 227 log.msg("reconfig already active; will reconfig again after") 228 self.reconfig_requested = True 229 return 230 231 self.reconfig_active = reactor.seconds() 232 metrics.MetricCountEvent.log("loaded_config", 1) 233 234 # notify every 10 seconds that the reconfig is still going on, although 235 # reconfigs should not take that long! 236 self.reconfig_notifier = task.LoopingCall(lambda : 237 log.msg("reconfig is ongoing for %d s" % 238 (reactor.seconds() - self.reconfig_active))) 239 self.reconfig_notifier.start(10, now=False) 240 241 timer = metrics.Timer("BuildMaster.reconfig") 242 timer.start() 243 244 d = self.doReconfig() 245 246 @d.addBoth 247 def cleanup(res): 248 timer.stop() 249 self.reconfig_notifier.stop() 250 self.reconfig_notifier = None 251 self.reconfig_active = False 252 if self.reconfig_requested: 253 self.reconfig_requested = False 254 self.reconfig() 255 return res
256 257 d.addErrback(log.err, 'while reconfiguring') 258 259 return d # for tests 260 261 262 @defer.deferredGenerator
263 - def doReconfig(self):
264 log.msg("beginning configuration update") 265 changes_made = False 266 failed = False 267 try: 268 new_config = config.MasterConfig.loadConfig(self.basedir, 269 self.configFileName) 270 changes_made = True 271 self.config = new_config 272 wfd = defer.waitForDeferred( 273 self.reconfigService(new_config)) 274 yield wfd 275 wfd.getResult() 276 277 except config.ConfigErrors, e: 278 for msg in e.errors: 279 log.msg(msg) 280 failed = True 281 282 except: 283 log.err(failure.Failure(), 'during reconfig:') 284 failed = True 285 286 if failed: 287 if changes_made: 288 log.msg("WARNING: reconfig partially applied; master " 289 "may malfunction") 290 else: 291 log.msg("reconfig aborted without making any changes") 292 else: 293 log.msg("configuration update complete")
294 295
296 - def reconfigService(self, new_config):
297 if self.config.db['db_url'] != new_config.db['db_url']: 298 config.error( 299 "Cannot change c['db']['db_url'] after the master has started", 300 ) 301 302 # adjust the db poller 303 if (self.config.db['db_poll_interval'] 304 != new_config.db['db_poll_interval']): 305 if self.db_loop: 306 self.db_loop.stop() 307 self.db_loop = None 308 poll_interval = new_config.db['db_poll_interval'] 309 if poll_interval: 310 self.db_loop = task.LoopingCall(self.pollDatabase) 311 self.db_loop.start(poll_interval, now=False) 312 313 return config.ReconfigurableServiceMixin.reconfigService(self, 314 new_config)
315 316 317 ## informational methods 318
319 - def allSchedulers(self):
320 return list(self.scheduler_manager)
321
322 - def getStatus(self):
323 """ 324 @rtype: L{buildbot.status.builder.Status} 325 """ 326 return self.status
327
328 - def getObjectId(self):
329 """ 330 Return the obejct id for this master, for associating state with the 331 master. 332 333 @returns: ID, via Deferred 334 """ 335 # try to get the cached value 336 if self._object_id is not None: 337 return defer.succeed(self._object_id) 338 339 # failing that, get it from the DB; multiple calls to this function 340 # at the same time will not hurt 341 try: 342 hostname = os.uname()[1] # only on unix 343 except AttributeError: 344 hostname = socket.getfqdn() 345 master_name = "%s:%s" % (hostname, os.path.abspath(self.basedir)) 346 347 d = self.db.state.getObjectId(master_name, 348 "buildbot.master.BuildMaster") 349 def keep(id): 350 self._object_id = id 351 return id
352 d.addCallback(keep) 353 return d 354 355 356 ## triggering methods and subscriptions 357
358 - def addChange(self, who=None, files=None, comments=None, author=None, 359 isdir=None, is_dir=None, revision=None, when=None, 360 when_timestamp=None, branch=None, category=None, revlink='', 361 properties={}, repository='', project='', src=None):
362 """ 363 Add a change to the buildmaster and act on it. 364 365 This is a wrapper around L{ChangesConnectorComponent.addChange} which 366 also acts on the resulting change and returns a L{Change} instance. 367 368 Note that all parameters are keyword arguments, although C{who}, 369 C{files}, and C{comments} can be specified positionally for 370 backward-compatibility. 371 372 @param author: the author of this change 373 @type author: unicode string 374 375 @param who: deprecated name for C{author} 376 377 @param files: a list of filenames that were changed 378 @type branch: list of unicode strings 379 380 @param comments: user comments on the change 381 @type branch: unicode string 382 383 @param is_dir: deprecated 384 385 @param isdir: deprecated name for C{is_dir} 386 387 @param revision: the revision identifier for this change 388 @type revision: unicode string 389 390 @param when_timestamp: when this change occurred, or the current time 391 if None 392 @type when_timestamp: datetime instance or None 393 394 @param when: deprecated name and type for C{when_timestamp} 395 @type when: integer (UNIX epoch time) or None 396 397 @param branch: the branch on which this change took place 398 @type branch: unicode string 399 400 @param category: category for this change (arbitrary use by Buildbot 401 users) 402 @type category: unicode string 403 404 @param revlink: link to a web view of this revision 405 @type revlink: unicode string 406 407 @param properties: properties to set on this change 408 @type properties: dictionary with string keys and simple values 409 (JSON-able). Note that the property source is I{not} included 410 in this dictionary. 411 412 @param repository: the repository in which this change took place 413 @type repository: unicode string 414 415 @param project: the project this change is a part of 416 @type project: unicode string 417 418 @param src: source of the change (vcs or other) 419 @type src: string 420 421 @returns: L{Change} instance via Deferred 422 """ 423 metrics.MetricCountEvent.log("added_changes", 1) 424 425 # handle translating deprecated names into new names for db.changes 426 def handle_deprec(oldname, old, newname, new, default=None, 427 converter = lambda x:x): 428 if old is not None: 429 if new is None: 430 log.msg("WARNING: change source is using deprecated " 431 "addChange parameter '%s'" % oldname) 432 return converter(old) 433 raise TypeError("Cannot provide '%s' and '%s' to addChange" 434 % (oldname, newname)) 435 if new is None: 436 new = default 437 return new
438 439 author = handle_deprec("who", who, "author", author) 440 is_dir = handle_deprec("isdir", isdir, "is_dir", is_dir, 441 default=0) 442 when_timestamp = handle_deprec("when", when, 443 "when_timestamp", when_timestamp, 444 converter=epoch2datetime) 445 446 # add a source to each property 447 for n in properties: 448 properties[n] = (properties[n], 'Change') 449 450 d = defer.succeed(None) 451 if src: 452 # create user object, returning a corresponding uid 453 d.addCallback(lambda _ : users.createUserObject(self, author, src)) 454 455 # add the Change to the database 456 d.addCallback(lambda uid : 457 self.db.changes.addChange(author=author, files=files, 458 comments=comments, is_dir=is_dir, 459 revision=revision, 460 when_timestamp=when_timestamp, 461 branch=branch, category=category, 462 revlink=revlink, properties=properties, 463 repository=repository, project=project, 464 uid=uid)) 465 466 # convert the changeid to a Change instance 467 d.addCallback(lambda changeid : 468 self.db.changes.getChange(changeid)) 469 d.addCallback(lambda chdict : 470 changes.Change.fromChdict(self, chdict)) 471 472 def notify(change): 473 msg = u"added change %s to database" % change 474 log.msg(msg.encode('utf-8', 'replace')) 475 # only deliver messages immediately if we're not polling 476 if not self.config.db['db_poll_interval']: 477 self._change_subs.deliver(change) 478 return change 479 d.addCallback(notify) 480 return d 481
482 - def subscribeToChanges(self, callback):
483 """ 484 Request that C{callback} be called with each Change object added to the 485 cluster. 486 487 Note: this method will go away in 0.9.x 488 """ 489 return self._change_subs.subscribe(callback)
490
491 - def addBuildset(self, **kwargs):
492 """ 493 Add a buildset to the buildmaster and act on it. Interface is 494 identical to 495 L{buildbot.db.buildsets.BuildsetConnectorComponent.addBuildset}, 496 including returning a Deferred, but also potentially triggers the 497 resulting builds. 498 """ 499 d = self.db.buildsets.addBuildset(**kwargs) 500 def notify((bsid,brids)): 501 log.msg("added buildset %d to database" % bsid) 502 # note that buildset additions are only reported on this master 503 self._new_buildset_subs.deliver(bsid=bsid, **kwargs) 504 # only deliver messages immediately if we're not polling 505 if not self.config.db['db_poll_interval']: 506 for bn, brid in brids.iteritems(): 507 self.buildRequestAdded(bsid=bsid, brid=brid, 508 buildername=bn) 509 return (bsid,brids)
510 d.addCallback(notify) 511 return d 512
513 - def subscribeToBuildsets(self, callback):
514 """ 515 Request that C{callback(bsid=bsid, ssid=ssid, reason=reason, 516 properties=properties, builderNames=builderNames, 517 external_idstring=external_idstring)} be called whenever a buildset is 518 added. Properties is a dictionary as expected for 519 L{BuildsetsConnectorComponent.addBuildset}. 520 521 Note that this only works for buildsets added on this master. 522 523 Note: this method will go away in 0.9.x 524 """ 525 return self._new_buildset_subs.subscribe(callback)
526 527 @defer.deferredGenerator
528 - def maybeBuildsetComplete(self, bsid):
529 """ 530 Instructs the master to check whether the buildset is complete, 531 and notify appropriately if it is. 532 533 Note that buildset completions are only reported on the master 534 on which the last build request completes. 535 """ 536 wfd = defer.waitForDeferred( 537 self.db.buildrequests.getBuildRequests(bsid=bsid, complete=False)) 538 yield wfd 539 brdicts = wfd.getResult() 540 541 # if there are incomplete buildrequests, bail out 542 if brdicts: 543 return 544 545 wfd = defer.waitForDeferred( 546 self.db.buildrequests.getBuildRequests(bsid=bsid)) 547 yield wfd 548 brdicts = wfd.getResult() 549 550 # figure out the overall results of the buildset 551 cumulative_results = SUCCESS 552 for brdict in brdicts: 553 if brdict['results'] not in (SUCCESS, WARNINGS): 554 cumulative_results = FAILURE 555 556 # mark it as completed in the database 557 wfd = defer.waitForDeferred( 558 self.db.buildsets.completeBuildset(bsid, cumulative_results)) 559 yield wfd 560 wfd.getResult() 561 562 # and deliver to any listeners 563 self._buildsetComplete(bsid, cumulative_results)
564
565 - def _buildsetComplete(self, bsid, results):
566 self._complete_buildset_subs.deliver(bsid, results)
567
568 - def subscribeToBuildsetCompletions(self, callback):
569 """ 570 Request that C{callback(bsid, result)} be called whenever a 571 buildset is complete. 572 573 Note: this method will go away in 0.9.x 574 """ 575 return self._complete_buildset_subs.subscribe(callback)
576
577 - def buildRequestAdded(self, bsid, brid, buildername):
578 """ 579 Notifies the master that a build request is available to be claimed; 580 this may be a brand new build request, or a build request that was 581 previously claimed and unclaimed through a timeout or other calamity. 582 583 @param bsid: containing buildset id 584 @param brid: buildrequest ID 585 @param buildername: builder named by the build request 586 """ 587 self._new_buildrequest_subs.deliver( 588 dict(bsid=bsid, brid=brid, buildername=buildername))
589
590 - def subscribeToBuildRequests(self, callback):
591 """ 592 Request that C{callback} be invoked with a dictionary with keys C{brid} 593 (the build request id), C{bsid} (buildset id) and C{buildername} 594 whenever a new build request is added to the database. Note that, due 595 to the delayed nature of subscriptions, the build request may already 596 be claimed by the time C{callback} is invoked. 597 598 Note: this method will go away in 0.9.x 599 """ 600 return self._new_buildrequest_subs.subscribe(callback)
601 602 603 ## database polling 604
605 - def pollDatabase(self):
606 # poll each of the tables that can indicate new, actionable stuff for 607 # this buildmaster to do. This is used in a TimerService, so returning 608 # a Deferred means that we won't run two polling operations 609 # simultaneously. Each particular poll method handles errors itself, 610 # although catastrophic errors are handled here 611 d = defer.gatherResults([ 612 self.pollDatabaseChanges(), 613 self.pollDatabaseBuildRequests(), 614 # also unclaim 615 ]) 616 d.addErrback(log.err, 'while polling database') 617 return d
618 619 _last_processed_change = None 620 @defer.deferredGenerator
621 - def pollDatabaseChanges(self):
622 # Older versions of Buildbot had each scheduler polling the database 623 # independently, and storing a "last_processed" state indicating the 624 # last change it had processed. This had the advantage of allowing 625 # schedulers to pick up changes that arrived in the database while 626 # the scheduler was not running, but was horribly inefficient. 627 628 # This version polls the database on behalf of the schedulers, using a 629 # similar state at the master level. 630 631 timer = metrics.Timer("BuildMaster.pollDatabaseChanges()") 632 timer.start() 633 634 need_setState = False 635 636 # get the last processed change id 637 if self._last_processed_change is None: 638 wfd = defer.waitForDeferred( 639 self._getState('last_processed_change')) 640 yield wfd 641 self._last_processed_change = wfd.getResult() 642 643 # if it's still None, assume we've processed up to the latest changeid 644 if self._last_processed_change is None: 645 wfd = defer.waitForDeferred( 646 self.db.changes.getLatestChangeid()) 647 yield wfd 648 lpc = wfd.getResult() 649 # if there *are* no changes, count the last as '0' so that we don't 650 # skip the first change 651 if lpc is None: 652 lpc = 0 653 self._last_processed_change = lpc 654 655 need_setState = True 656 657 if self._last_processed_change is None: 658 timer.stop() 659 return 660 661 while True: 662 changeid = self._last_processed_change + 1 663 wfd = defer.waitForDeferred( 664 self.db.changes.getChange(changeid)) 665 yield wfd 666 chdict = wfd.getResult() 667 668 # if there's no such change, we've reached the end and can 669 # stop polling 670 if not chdict: 671 break 672 673 wfd = defer.waitForDeferred( 674 changes.Change.fromChdict(self, chdict)) 675 yield wfd 676 change = wfd.getResult() 677 678 self._change_subs.deliver(change) 679 680 self._last_processed_change = changeid 681 need_setState = True 682 683 # write back the updated state, if it's changed 684 if need_setState: 685 wfd = defer.waitForDeferred( 686 self._setState('last_processed_change', 687 self._last_processed_change)) 688 yield wfd 689 wfd.getResult() 690 timer.stop()
691 692 _last_unclaimed_brids_set = None 693 _last_claim_cleanup = 0 694 @defer.deferredGenerator
695 - def pollDatabaseBuildRequests(self):
696 # deal with cleaning up unclaimed requests, and (if necessary) 697 # requests from a previous instance of this master 698 timer = metrics.Timer("BuildMaster.pollDatabaseBuildRequests()") 699 timer.start() 700 701 # cleanup unclaimed builds 702 since_last_cleanup = reactor.seconds() - self._last_claim_cleanup 703 if since_last_cleanup < self.RECLAIM_BUILD_INTERVAL: 704 unclaimed_age = (self.RECLAIM_BUILD_INTERVAL 705 * self.UNCLAIMED_BUILD_FACTOR) 706 wfd = defer.waitForDeferred( 707 self.db.buildrequests.unclaimExpiredRequests(unclaimed_age)) 708 yield wfd 709 wfd.getResult() 710 711 self._last_claim_cleanup = reactor.seconds() 712 713 # _last_unclaimed_brids_set tracks the state of unclaimed build 714 # requests; whenever it sees a build request which was not claimed on 715 # the last poll, it notifies the subscribers. It only tracks that 716 # state within the master instance, though; on startup, it notifies for 717 # all unclaimed requests in the database. 718 719 last_unclaimed = self._last_unclaimed_brids_set or set() 720 if len(last_unclaimed) > self.WARNING_UNCLAIMED_COUNT: 721 log.msg("WARNING: %d unclaimed buildrequests - is a scheduler " 722 "producing builds for which no builder is running?" 723 % len(last_unclaimed)) 724 725 # get the current set of unclaimed buildrequests 726 wfd = defer.waitForDeferred( 727 self.db.buildrequests.getBuildRequests(claimed=False)) 728 yield wfd 729 now_unclaimed_brdicts = wfd.getResult() 730 now_unclaimed = set([ brd['brid'] for brd in now_unclaimed_brdicts ]) 731 732 # and store that for next time 733 self._last_unclaimed_brids_set = now_unclaimed 734 735 # see what's new, and notify if anything is 736 new_unclaimed = now_unclaimed - last_unclaimed 737 if new_unclaimed: 738 brdicts = dict((brd['brid'], brd) for brd in now_unclaimed_brdicts) 739 for brid in new_unclaimed: 740 brd = brdicts[brid] 741 self.buildRequestAdded(brd['buildsetid'], brd['brid'], 742 brd['buildername']) 743 timer.stop()
744 745 ## state maintenance (private) 746
747 - def _getState(self, name, default=None):
748 "private wrapper around C{self.db.state.getState}" 749 d = self.getObjectId() 750 def get(objectid): 751 return self.db.state.getState(objectid, name, default)
752 d.addCallback(get) 753 return d 754
755 - def _setState(self, name, value):
756 "private wrapper around C{self.db.state.setState}" 757 d = self.getObjectId() 758 def set(objectid): 759 return self.db.state.setState(objectid, name, value)
760 d.addCallback(set) 761 return d 762
763 -class Control:
764 implements(interfaces.IControl) 765
766 - def __init__(self, master):
767 self.master = master
768
769 - def addChange(self, change):
770 self.master.addChange(change)
771
772 - def addBuildset(self, **kwargs):
773 return self.master.addBuildset(**kwargs)
774
775 - def getBuilder(self, name):
776 b = self.master.botmaster.builders[name] 777 return BuilderControl(b, self)
778 779 components.registerAdapter(Control, BuildMaster, interfaces.IControl) 780