1
2 import random, weakref
3 from zope.interface import implements
4 from twisted.python import log, components
5 from twisted.python.failure import Failure
6 from twisted.spread import pb
7 from twisted.internet import reactor, defer
8
9 from buildbot import interfaces
10 from buildbot.status.progress import Expectations
11 from buildbot.util import now
12 from buildbot.process import base
13 from buildbot.process.properties import Properties
14
15 (ATTACHING,
16 IDLE,
17 PINGING,
18 BUILDING,
19 LATENT,
20 SUBSTANTIATING,
21 ) = range(6)
22
23
25 """I am the master-side representative for one of the
26 L{buildbot.slave.bot.SlaveBuilder} objects that lives in a remote
27 buildbot. When a remote builder connects, I query it for command versions
28 and then make it available to any Builds that are ready to run. """
29
31 self.ping_watchers = []
32 self.state = None
33 self.remote = None
34 self.slave = None
35 self.builder_name = None
36
38 r = ["<", self.__class__.__name__]
39 if self.builder_name:
40 r.extend([" builder=", self.builder_name])
41 if self.slave:
42 r.extend([" slave=", self.slave.slavename])
43 r.append(">")
44 return ''.join(r)
45
49
51 if self.remoteCommands is None:
52
53 return oldversion
54 return self.remoteCommands.get(command)
55
67
70
73
77
78 - def attached(self, slave, remote, commands):
79 """
80 @type slave: L{buildbot.buildslave.BuildSlave}
81 @param slave: the BuildSlave that represents the buildslave as a
82 whole
83 @type remote: L{twisted.spread.pb.RemoteReference}
84 @param remote: a reference to the L{buildbot.slave.bot.SlaveBuilder}
85 @type commands: dict: string -> string, or None
86 @param commands: provides the slave's version of each RemoteCommand
87 """
88 self.state = ATTACHING
89 self.remote = remote
90 self.remoteCommands = commands
91 if self.slave is None:
92 self.slave = slave
93 self.slave.addSlaveBuilder(self)
94 else:
95 assert self.slave == slave
96 log.msg("Buildslave %s attached to %s" % (slave.slavename,
97 self.builder_name))
98 d = self.remote.callRemote("setMaster", self)
99 d.addErrback(self._attachFailure, "Builder.setMaster")
100 d.addCallback(self._attached2)
101 return d
102
104 d = self.remote.callRemote("print", "attached")
105 d.addErrback(self._attachFailure, "Builder.print 'attached'")
106 d.addCallback(self._attached3)
107 return d
108
110
111 self.state = IDLE
112 return self
113
115 assert isinstance(where, str)
116 log.msg(where)
117 log.err(why)
118 return why
119
120 - def prepare(self, builder_status):
121 return defer.succeed(None)
122
123 - def ping(self, status=None):
124 """Ping the slave to make sure it is still there. Returns a Deferred
125 that fires with True if it is.
126
127 @param status: if you point this at a BuilderStatus, a 'pinging'
128 event will be pushed.
129 """
130 oldstate = self.state
131 self.state = PINGING
132 newping = not self.ping_watchers
133 d = defer.Deferred()
134 self.ping_watchers.append(d)
135 if newping:
136 if status:
137 event = status.addEvent(["pinging"])
138 d2 = defer.Deferred()
139 d2.addCallback(self._pong_status, event)
140 self.ping_watchers.insert(0, d2)
141
142
143 Ping().ping(self.remote).addCallback(self._pong)
144
145 def reset_state(res):
146 if self.state == PINGING:
147 self.state = oldstate
148 return res
149 d.addCallback(reset_state)
150 return d
151
153 watchers, self.ping_watchers = self.ping_watchers, []
154 for d in watchers:
155 d.callback(res)
156
158 if res:
159 event.text = ["ping", "success"]
160 else:
161 event.text = ["ping", "failed"]
162 event.finish()
163
172
173
175 running = False
176
177 - def ping(self, remote):
178 assert not self.running
179 self.running = True
180 log.msg("sending ping")
181 self.d = defer.Deferred()
182
183
184 remote.callRemote("print", "ping").addCallbacks(self._pong,
185 self._ping_failed,
186 errbackArgs=(remote,))
187 return self.d
188
190 log.msg("ping finished: success")
191 self.d.callback(True)
192
194 log.msg("ping finished: failure")
195
196
197
198 remote.broker.transport.loseConnection()
199
200
201
202 self.d.callback(False)
203
204
228
229
239
240 - def prepare(self, builder_status):
249 d.addErrback(substantiation_failed)
250 return d
251
253 self.state = SUBSTANTIATING
254 d = self.slave.substantiate(self)
255 if not self.slave.substantiated:
256 event = self.builder.builder_status.addEvent(
257 ["substantiating"])
258 def substantiated(res):
259 msg = ["substantiate", "success"]
260 if isinstance(res, basestring):
261 msg.append(res)
262 elif isinstance(res, (tuple, list)):
263 msg.extend(res)
264 event.text = msg
265 event.finish()
266 return res
267 def substantiation_failed(res):
268 event.text = ["substantiate", "failed"]
269
270 event.finish()
271 return res
272 d.addCallbacks(substantiated, substantiation_failed)
273 return d
274
278
282
286
290
291 - def ping(self, status=None):
297
298
300 """I manage all Builds of a given type.
301
302 Each Builder is created by an entry in the config file (the c['builders']
303 list), with a number of parameters.
304
305 One of these parameters is the L{buildbot.process.factory.BuildFactory}
306 object that is associated with this Builder. The factory is responsible
307 for creating new L{Build<buildbot.process.base.Build>} objects. Each
308 Build object defines when and how the build is performed, so a new
309 Factory or Builder should be defined to control this behavior.
310
311 The Builder holds on to a number of L{base.BuildRequest} objects in a
312 list named C{.buildable}. Incoming BuildRequest objects will be added to
313 this list, or (if possible) merged into an existing request. When a slave
314 becomes available, I will use my C{BuildFactory} to turn the request into
315 a new C{Build} object. The C{BuildRequest} is forgotten, the C{Build}
316 goes into C{.building} while it runs. Once the build finishes, I will
317 discard it.
318
319 I maintain a list of available SlaveBuilders, one for each connected
320 slave that the C{slavenames} parameter says we can use. Some of these
321 will be idle, some of them will be busy running builds for me. If there
322 are multiple slaves, I can run multiple builds at once.
323
324 I also manage forced builds, progress expectation (ETA) management, and
325 some status delivery chores.
326
327 @type buildable: list of L{buildbot.process.base.BuildRequest}
328 @ivar buildable: BuildRequests that are ready to build, but which are
329 waiting for a buildslave to be available.
330
331 @type building: list of L{buildbot.process.base.Build}
332 @ivar building: Builds that are actively running
333
334 @type slaves: list of L{buildbot.buildslave.BuildSlave} objects
335 @ivar slaves: the slaves currently available for building
336 """
337
338 expectations = None
339 CHOOSE_SLAVES_RANDOMLY = True
340
341 - def __init__(self, setup, builder_status):
342 """
343 @type setup: dict
344 @param setup: builder setup data, as stored in
345 BuildmasterConfig['builders']. Contains name,
346 slavename(s), builddir, slavebuilddir, factory, locks.
347 @type builder_status: L{buildbot.status.builder.BuilderStatus}
348 """
349 self.name = setup['name']
350 self.slavenames = []
351 if setup.has_key('slavename'):
352 self.slavenames.append(setup['slavename'])
353 if setup.has_key('slavenames'):
354 self.slavenames.extend(setup['slavenames'])
355 self.builddir = setup['builddir']
356 self.slavebuilddir = setup['slavebuilddir']
357 self.buildFactory = setup['factory']
358 self.nextSlave = setup.get('nextSlave')
359 if self.nextSlave is not None and not callable(self.nextSlave):
360 raise ValueError("nextSlave must be callable")
361 self.locks = setup.get("locks", [])
362 self.env = setup.get('env', {})
363 assert isinstance(self.env, dict)
364 if setup.has_key('periodicBuildTime'):
365 raise ValueError("periodicBuildTime can no longer be defined as"
366 " part of the Builder: use scheduler.Periodic"
367 " instead")
368 self.nextBuild = setup.get('nextBuild')
369 if self.nextBuild is not None and not callable(self.nextBuild):
370 raise ValueError("nextBuild must be callable")
371
372
373 self.buildable = []
374 self.building = []
375
376 self.old_building = weakref.WeakKeyDictionary()
377
378
379
380 self.attaching_slaves = []
381
382
383
384
385 self.slaves = []
386
387 self.builder_status = builder_status
388 self.builder_status.setSlavenames(self.slavenames)
389
390
391 self.watchers = {'attach': [], 'detach': [], 'detach_all': [],
392 'idle': []}
393
395 self.botmaster = botmaster
396
398 diffs = []
399 setup_slavenames = []
400 if setup.has_key('slavename'):
401 setup_slavenames.append(setup['slavename'])
402 setup_slavenames.extend(setup.get('slavenames', []))
403 if setup_slavenames != self.slavenames:
404 diffs.append('slavenames changed from %s to %s' \
405 % (self.slavenames, setup_slavenames))
406 if setup['builddir'] != self.builddir:
407 diffs.append('builddir changed from %s to %s' \
408 % (self.builddir, setup['builddir']))
409 if setup['slavebuilddir'] != self.slavebuilddir:
410 diffs.append('slavebuilddir changed from %s to %s' \
411 % (self.slavebuilddir, setup['slavebuilddir']))
412 if setup['factory'] != self.buildFactory:
413 diffs.append('factory changed')
414 if setup.get('locks', []) != self.locks:
415 diffs.append('locks changed from %s to %s' % (self.locks, setup.get('locks')))
416 if setup.get('nextSlave') != self.nextSlave:
417 diffs.append('nextSlave changed from %s to %s' % (self.nextSlave, setup['nextSlave']))
418 if setup.get('nextBuild') != self.nextBuild:
419 diffs.append('nextBuild changed from %s to %s' % (self.nextBuild, setup['nextBuild']))
420 return diffs
421
423 return "<Builder '%s' at %d>" % (self.name, id(self))
424
426 """Returns the timestamp of the oldest build request for this builder.
427
428 If there are no build requests, None is returned."""
429 if self.buildable:
430 return self.buildable[0].getSubmitTime()
431 else:
432 return None
433
440
442 if req in self.buildable:
443 self.buildable.remove(req)
444 self.builder_status.removeBuildRequest(req.status, cancelled=True)
445 return True
446 return False
447
449 """Suck the brain out of an old Builder.
450
451 This takes all the runtime state from an existing Builder and moves
452 it into ourselves. This is used when a Builder is changed in the
453 master.cfg file: the new Builder has a different factory, but we want
454 all the builds that were queued for the old one to get processed by
455 the new one. Any builds which are already running will keep running.
456 The new Builder will get as many of the old SlaveBuilder objects as
457 it wants."""
458
459 log.msg("consumeTheSoulOfYourPredecessor: %s feeding upon %s" %
460 (self, old))
461
462
463
464 log.msg(" stealing %s buildrequests" % len(old.buildable))
465 self.buildable.extend(old.buildable)
466 old.buildable = []
467
468
469
470
471
472
473 if old.building:
474 self.builder_status.setBigState("building")
475
476
477
478
479 for b in old.building:
480 self.old_building[b] = None
481 for b in old.old_building:
482 self.old_building[b] = None
483
484
485
486 for sb in old.slaves[:]:
487 if sb.slave.slavename in self.slavenames:
488 log.msg(" stealing buildslave %s" % sb)
489 self.slaves.append(sb)
490 old.slaves.remove(sb)
491 sb.setBuilder(self)
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512 return
513
522
530
542
543 - def attached(self, slave, remote, commands):
544 """This is invoked by the BuildSlave when the self.slavename bot
545 registers their builder.
546
547 @type slave: L{buildbot.buildslave.BuildSlave}
548 @param slave: the BuildSlave that represents the buildslave as a whole
549 @type remote: L{twisted.spread.pb.RemoteReference}
550 @param remote: a reference to the L{buildbot.slave.bot.SlaveBuilder}
551 @type commands: dict: string -> string, or None
552 @param commands: provides the slave's version of each RemoteCommand
553
554 @rtype: L{twisted.internet.defer.Deferred}
555 @return: a Deferred that fires (with 'self') when the slave-side
556 builder is fully attached and ready to accept commands.
557 """
558 for s in self.attaching_slaves + self.slaves:
559 if s.slave == slave:
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576 return defer.succeed(self)
577
578 sb = SlaveBuilder()
579 sb.setBuilder(self)
580 self.attaching_slaves.append(sb)
581 d = sb.attached(slave, remote, commands)
582 d.addCallback(self._attached)
583 d.addErrback(self._not_attached, slave)
584 return d
585
594
604
638
647
714
716 """Start a build on the given slave.
717 @param build: the L{base.Build} to start
718 @param sb: the L{SlaveBuilder} which will host this build
719
720 @return: a Deferred which fires with a
721 L{buildbot.interfaces.IBuildControl} that can be used to stop the
722 Build, or to access a L{buildbot.interfaces.IBuildStatus} which will
723 watch the Build as it runs. """
724
725 self.building.append(build)
726 self.updateBigStatus()
727 log.msg("starting build %s using slave %s" % (build, sb))
728 d = sb.prepare(self.builder_status)
729 def _ping(ign):
730
731
732
733
734
735
736
737
738 log.msg("starting build %s.. pinging the slave %s" % (build, sb))
739 return sb.ping()
740 d.addCallback(_ping)
741 d.addCallback(self._startBuild_1, build, sb)
742 return d
743
745 if not res:
746 return self._startBuildFailed("slave ping failed", build, sb)
747
748
749
750 sb.buildStarted()
751 d = sb.remote.callRemote("startBuild")
752 d.addCallbacks(self._startBuild_2, self._startBuildFailed,
753 callbackArgs=(build,sb), errbackArgs=(build,sb))
754 return d
755
771
773
774 log.msg("I tried to tell the slave that the build %s started, but "
775 "remote_startBuild failed: %s" % (build, why))
776
777
778
779 sb.buildFinished()
780
781 log.msg("re-queueing the BuildRequest")
782 self.building.remove(build)
783 for req in build.requests:
784 self.buildable.insert(0, req)
785
786 self.builder_status.addBuildRequest(req.status)
787
788
790 """This is called when the Build has finished (either success or
791 failure). Any exceptions during the build are reported with
792 results=FAILURE, not with an errback."""
793
794
795
796
797 self.building.remove(build)
798 for req in build.requests:
799 req.finished(build.build_status)
800
802 """Mark the build as successful and update expectations for the next
803 build. Only call this when the build did not fail in any way that
804 would invalidate the time expectations generated by it. (if the
805 compile failed and thus terminated early, we can't use the last
806 build to predict how long the next one will take).
807 """
808 if self.expectations:
809 self.expectations.update(progress)
810 else:
811
812
813 self.expectations = Expectations(progress)
814 log.msg("new expectations: %s seconds" % \
815 self.expectations.expectedBuildTime())
816
820
821
823 implements(interfaces.IBuilderControl)
824
826 """Submit a BuildRequest to this Builder."""
827 self.original.submitBuildRequest(req)
828
830 """Submit a BuildRequest like requestBuild, but raise a
831 L{buildbot.interfaces.NoSlaveError} if no slaves are currently
832 available, so it cannot be used to queue a BuildRequest in the hopes
833 that a slave will eventually connect. This method is appropriate for
834 use by things like the web-page 'Force Build' button."""
835 if not self.original.slaves:
836 raise interfaces.NoSlaveError
837 self.requestBuild(req)
838
839 - def resubmitBuild(self, bs, reason="<rebuild, no reason given>", extraProperties=None):
854
856
857 retval = []
858 for r in self.original.buildable:
859 retval.append(BuildRequestControl(self.original, r))
860
861 return retval
862
865
867 if not self.original.slaves:
868 self.original.builder_status.addPointEvent(["ping", "no slave"])
869 return defer.succeed(False)
870 dl = []
871 for s in self.original.slaves:
872 dl.append(s.ping(self.original.builder_status))
873 d = defer.DeferredList(dl)
874 d.addCallback(self._gatherPingResults)
875 return d
876
878 for ignored,success in res:
879 if not success:
880 return False
881 return True
882
883 components.registerAdapter(BuilderControl, Builder, interfaces.IBuilderControl)
884
886 implements(interfaces.IBuildRequestControl)
887
889 self.original_builder = builder
890 self.original_request = request
891
893 raise NotImplementedError
894
896 raise NotImplementedError
897
900