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
14 (ATTACHING,
15 IDLE,
16 PINGING,
17 BUILDING,
18 LATENT,
19 SUBSTANTIATING,
20 ) = range(6)
21
22
24 """I am the master-side representative for one of the
25 L{buildbot.slave.bot.SlaveBuilder} objects that lives in a remote
26 buildbot. When a remote builder connects, I query it for command versions
27 and then make it available to any Builds that are ready to run. """
28
30 self.ping_watchers = []
31 self.state = None
32 self.remote = None
33 self.slave = None
34 self.builder_name = None
35
37 r = ["<", self.__class__.__name__]
38 if self.builder_name:
39 r.extend([" builder=", self.builder_name])
40 if self.slave:
41 r.extend([" slave=", self.slave.slavename])
42 r.append(">")
43 return ''.join(r)
44
48
50 if self.remoteCommands is None:
51
52 return oldversion
53 return self.remoteCommands.get(command)
54
66
69
72
76
77 - def attached(self, slave, remote, commands):
78 """
79 @type slave: L{buildbot.buildslave.BuildSlave}
80 @param slave: the BuildSlave that represents the buildslave as a
81 whole
82 @type remote: L{twisted.spread.pb.RemoteReference}
83 @param remote: a reference to the L{buildbot.slave.bot.SlaveBuilder}
84 @type commands: dict: string -> string, or None
85 @param commands: provides the slave's version of each RemoteCommand
86 """
87 self.state = ATTACHING
88 self.remote = remote
89 self.remoteCommands = commands
90 if self.slave is None:
91 self.slave = slave
92 self.slave.addSlaveBuilder(self)
93 else:
94 assert self.slave == slave
95 log.msg("Buildslave %s attached to %s" % (slave.slavename,
96 self.builder_name))
97 d = self.remote.callRemote("setMaster", self)
98 d.addErrback(self._attachFailure, "Builder.setMaster")
99 d.addCallback(self._attached2)
100 return d
101
107
109
110 self.state = IDLE
111 return self
112
114 assert isinstance(where, str)
115 log.msg(where)
116 log.err(why)
117 return why
118
119 - def ping(self, timeout, status=None):
120 """Ping the slave to make sure it is still there. Returns a Deferred
121 that fires with True if it is.
122
123 @param status: if you point this at a BuilderStatus, a 'pinging'
124 event will be pushed.
125 """
126 oldstate = self.state
127 self.state = PINGING
128 newping = not self.ping_watchers
129 d = defer.Deferred()
130 self.ping_watchers.append(d)
131 if newping:
132 if status:
133 event = status.addEvent(["pinging"])
134 d2 = defer.Deferred()
135 d2.addCallback(self._pong_status, event)
136 self.ping_watchers.insert(0, d2)
137
138
139 Ping().ping(self.remote, timeout).addCallback(self._pong)
140
141 def reset_state(res):
142 if self.state == PINGING:
143 self.state = oldstate
144 return res
145 d.addCallback(reset_state)
146 return d
147
149 watchers, self.ping_watchers = self.ping_watchers, []
150 for d in watchers:
151 d.callback(res)
152
154 if res:
155 event.text = ["ping", "success"]
156 else:
157 event.text = ["ping", "failed"]
158 event.finish()
159
168
169
171 running = False
172 timer = None
173
174 - def ping(self, remote, timeout):
192
199
200
209
211 log.msg("ping finished: success")
212 self._stopTimer()
213 self.d.callback(True)
214
226
227
251
252
262
264 self.state = SUBSTANTIATING
265 d = self.slave.substantiate(self)
266 if not self.slave.substantiated:
267 event = self.builder.builder_status.addEvent(
268 ["substantiating"])
269 def substantiated(res):
270 msg = ["substantiate", "success"]
271 if isinstance(res, basestring):
272 msg.append(res)
273 elif isinstance(res, (tuple, list)):
274 msg.extend(res)
275 event.text = msg
276 event.finish()
277 return res
278 def substantiation_failed(res):
279 event.text = ["substantiate", "failed"]
280
281 event.finish()
282 return res
283 d.addCallbacks(substantiated, substantiation_failed)
284 return d
285
289
293
297
301
302 - def ping(self, timeout, status=None):
308
309
311 """I manage all Builds of a given type.
312
313 Each Builder is created by an entry in the config file (the c['builders']
314 list), with a number of parameters.
315
316 One of these parameters is the L{buildbot.process.factory.BuildFactory}
317 object that is associated with this Builder. The factory is responsible
318 for creating new L{Build<buildbot.process.base.Build>} objects. Each
319 Build object defines when and how the build is performed, so a new
320 Factory or Builder should be defined to control this behavior.
321
322 The Builder holds on to a number of L{base.BuildRequest} objects in a
323 list named C{.buildable}. Incoming BuildRequest objects will be added to
324 this list, or (if possible) merged into an existing request. When a slave
325 becomes available, I will use my C{BuildFactory} to turn the request into
326 a new C{Build} object. The C{BuildRequest} is forgotten, the C{Build}
327 goes into C{.building} while it runs. Once the build finishes, I will
328 discard it.
329
330 I maintain a list of available SlaveBuilders, one for each connected
331 slave that the C{slavenames} parameter says we can use. Some of these
332 will be idle, some of them will be busy running builds for me. If there
333 are multiple slaves, I can run multiple builds at once.
334
335 I also manage forced builds, progress expectation (ETA) management, and
336 some status delivery chores.
337
338 I am persisted in C{BASEDIR/BUILDERNAME/builder}, so I can remember how
339 long a build usually takes to run (in my C{expectations} attribute). This
340 pickle also includes the L{buildbot.status.builder.BuilderStatus} object,
341 which remembers the set of historic builds.
342
343 @type buildable: list of L{buildbot.process.base.BuildRequest}
344 @ivar buildable: BuildRequests that are ready to build, but which are
345 waiting for a buildslave to be available.
346
347 @type building: list of L{buildbot.process.base.Build}
348 @ivar building: Builds that are actively running
349
350 @type slaves: list of L{buildbot.buildslave.BuildSlave} objects
351 @ivar slaves: the slaves currently available for building
352 """
353
354 expectations = None
355 START_BUILD_TIMEOUT = 10
356 CHOOSE_SLAVES_RANDOMLY = True
357
358 - def __init__(self, setup, builder_status):
359 """
360 @type setup: dict
361 @param setup: builder setup data, as stored in
362 BuildmasterConfig['builders']. Contains name,
363 slavename(s), builddir, factory, locks.
364 @type builder_status: L{buildbot.status.builder.BuilderStatus}
365 """
366 self.name = setup['name']
367 self.slavenames = []
368 if setup.has_key('slavename'):
369 self.slavenames.append(setup['slavename'])
370 if setup.has_key('slavenames'):
371 self.slavenames.extend(setup['slavenames'])
372 self.builddir = setup['builddir']
373 self.buildFactory = setup['factory']
374 self.nextSlave = setup.get('nextSlave')
375 if self.nextSlave is not None and not callable(self.nextSlave):
376 raise ValueError("nextSlave must be callable")
377 self.locks = setup.get("locks", [])
378 self.env = setup.get('env', {})
379 assert isinstance(self.env, dict)
380 if setup.has_key('periodicBuildTime'):
381 raise ValueError("periodicBuildTime can no longer be defined as"
382 " part of the Builder: use scheduler.Periodic"
383 " instead")
384 self.nextBuild = setup.get('nextBuild')
385 if self.nextBuild is not None and not callable(self.nextBuild):
386 raise ValueError("nextBuild must be callable")
387
388
389 self.buildable = []
390 self.building = []
391
392 self.old_building = weakref.WeakKeyDictionary()
393
394
395
396 self.attaching_slaves = []
397
398
399
400
401 self.slaves = []
402
403 self.builder_status = builder_status
404 self.builder_status.setSlavenames(self.slavenames)
405
406
407 self.watchers = {'attach': [], 'detach': [], 'detach_all': [],
408 'idle': []}
409
411 self.botmaster = botmaster
412
414 diffs = []
415 setup_slavenames = []
416 if setup.has_key('slavename'):
417 setup_slavenames.append(setup['slavename'])
418 setup_slavenames.extend(setup.get('slavenames', []))
419 if setup_slavenames != self.slavenames:
420 diffs.append('slavenames changed from %s to %s' \
421 % (self.slavenames, setup_slavenames))
422 if setup['builddir'] != self.builddir:
423 diffs.append('builddir changed from %s to %s' \
424 % (self.builddir, setup['builddir']))
425 if setup['factory'] != self.buildFactory:
426 diffs.append('factory changed')
427 oldlocks = [(lock.__class__, lock.name)
428 for lock in self.locks]
429 newlocks = [(lock.__class__, lock.name)
430 for lock in setup.get('locks',[])]
431 if oldlocks != newlocks:
432 diffs.append('locks changed from %s to %s' % (oldlocks, newlocks))
433 if setup.get('nextSlave') != self.nextSlave:
434 diffs.append('nextSlave changed from %s to %s' % (self.nextSlave, setup['nextSlave']))
435 if setup.get('nextBuild') != self.nextBuild:
436 diffs.append('nextBuild changed from %s to %s' % (self.nextBuild, setup['nextBuild']))
437 return diffs
438
440 return "<Builder '%s' at %d>" % (self.name, id(self))
441
443 """Returns the timestamp of the oldest build request for this builder.
444
445 If there are no build requests, None is returned."""
446 if self.buildable:
447 return self.buildable[0].getSubmitTime()
448 else:
449 return None
450
457
459 if req in self.buildable:
460 self.buildable.remove(req)
461 self.builder_status.removeBuildRequest(req.status, cancelled=True)
462 return True
463 return False
464
466 d = self.__dict__.copy()
467
468 del d['building']
469 del d['slaves']
470 return d
471
473 self.__dict__ = d
474 self.building = []
475 self.slaves = []
476
478 """Suck the brain out of an old Builder.
479
480 This takes all the runtime state from an existing Builder and moves
481 it into ourselves. This is used when a Builder is changed in the
482 master.cfg file: the new Builder has a different factory, but we want
483 all the builds that were queued for the old one to get processed by
484 the new one. Any builds which are already running will keep running.
485 The new Builder will get as many of the old SlaveBuilder objects as
486 it wants."""
487
488 log.msg("consumeTheSoulOfYourPredecessor: %s feeding upon %s" %
489 (self, old))
490
491
492
493 log.msg(" stealing %s buildrequests" % len(old.buildable))
494 self.buildable.extend(old.buildable)
495 old.buildable = []
496
497
498
499
500
501
502 if old.building:
503 self.builder_status.setBigState("building")
504
505
506
507
508 for b in old.building:
509 self.old_building[b] = None
510 for b in old.old_building:
511 self.old_building[b] = None
512
513
514
515 for sb in old.slaves[:]:
516 if sb.slave.slavename in self.slavenames:
517 log.msg(" stealing buildslave %s" % sb)
518 self.slaves.append(sb)
519 old.slaves.remove(sb)
520 sb.setBuilder(self)
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541 return
542
551
559
571
572 - def attached(self, slave, remote, commands):
573 """This is invoked by the BuildSlave when the self.slavename bot
574 registers their builder.
575
576 @type slave: L{buildbot.buildslave.BuildSlave}
577 @param slave: the BuildSlave that represents the buildslave as a whole
578 @type remote: L{twisted.spread.pb.RemoteReference}
579 @param remote: a reference to the L{buildbot.slave.bot.SlaveBuilder}
580 @type commands: dict: string -> string, or None
581 @param commands: provides the slave's version of each RemoteCommand
582
583 @rtype: L{twisted.internet.defer.Deferred}
584 @return: a Deferred that fires (with 'self') when the slave-side
585 builder is fully attached and ready to accept commands.
586 """
587 for s in self.attaching_slaves + self.slaves:
588 if s.slave == slave:
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605 return defer.succeed(self)
606
607 sb = SlaveBuilder()
608 sb.setBuilder(self)
609 self.attaching_slaves.append(sb)
610 d = sb.attached(slave, remote, commands)
611 d.addCallback(self._attached)
612 d.addErrback(self._not_attached, slave)
613 return d
614
623
633
668
677
679 log.msg("maybeStartBuild %s: %s %s" %
680 (self, self.buildable, self.slaves))
681 if not self.buildable:
682 self.updateBigStatus()
683 return
684
685
686 available_slaves = [sb for sb in self.slaves if sb.isAvailable()]
687 if not available_slaves:
688 log.msg("%s: want to start build, but we don't have a remote"
689 % self)
690 self.updateBigStatus()
691 return
692 if self.nextSlave:
693 sb = None
694 try:
695 sb = self.nextSlave(self, available_slaves)
696 except:
697 log.msg("Exception choosing next slave")
698 log.err(Failure())
699
700 if not sb:
701 log.msg("%s: want to start build, but we don't have a remote"
702 % self)
703 self.updateBigStatus()
704 return
705 elif self.CHOOSE_SLAVES_RANDOMLY:
706 sb = random.choice(available_slaves)
707 else:
708 sb = available_slaves[0]
709
710
711
712
713 if not self.nextBuild:
714 req = self.buildable.pop(0)
715 else:
716 try:
717 req = self.nextBuild(self, self.buildable)
718 if not req:
719
720 self.updateBigStatus()
721 return
722 self.buildable.remove(req)
723 except:
724 log.msg("Exception choosing next build")
725 log.err(Failure())
726 self.updateBigStatus()
727 return
728 self.builder_status.removeBuildRequest(req.status)
729 mergers = []
730 botmaster = self.botmaster
731 for br in self.buildable[:]:
732 if botmaster.shouldMergeRequests(self, req, br):
733 self.buildable.remove(br)
734 self.builder_status.removeBuildRequest(br.status)
735 mergers.append(br)
736 requests = [req] + mergers
737
738
739
740 build = self.buildFactory.newBuild(requests)
741 build.setBuilder(self)
742 build.setLocks(self.locks)
743 if len(self.env) > 0:
744 build.setSlaveEnvironment(self.env)
745
746
747 self.startBuild(build, sb)
748
750 """Start a build on the given slave.
751 @param build: the L{base.Build} to start
752 @param sb: the L{SlaveBuilder} which will host this build
753
754 @return: a Deferred which fires with a
755 L{buildbot.interfaces.IBuildControl} that can be used to stop the
756 Build, or to access a L{buildbot.interfaces.IBuildStatus} which will
757 watch the Build as it runs. """
758
759 self.building.append(build)
760 self.updateBigStatus()
761 if isinstance(sb, LatentSlaveBuilder):
762 log.msg("starting build %s.. substantiating the slave %s" %
763 (build, sb))
764 d = sb.substantiate(build)
765 def substantiated(res):
766 return sb.ping(self.START_BUILD_TIMEOUT)
767 def substantiation_failed(res):
768 self.builder_status.addPointEvent(
769 ['removing', 'latent', sb.slave.slavename])
770 sb.slave.disconnect()
771
772
773 d.addCallbacks(substantiated, substantiation_failed)
774 else:
775 log.msg("starting build %s.. pinging the slave %s" % (build, sb))
776 d = sb.ping(self.START_BUILD_TIMEOUT)
777
778
779
780
781
782 d.addCallback(self._startBuild_1, build, sb)
783 return d
784
796
812
814
815 log.msg("I tried to tell the slave that the build %s started, but "
816 "remote_startBuild failed: %s" % (build, why))
817
818
819
820 sb.buildFinished()
821
822 log.msg("re-queueing the BuildRequest")
823 self.building.remove(build)
824 for req in build.requests:
825 self.buildable.insert(0, req)
826
827 self.builder_status.addBuildRequest(req.status)
828
829
831 """This is called when the Build has finished (either success or
832 failure). Any exceptions during the build are reported with
833 results=FAILURE, not with an errback."""
834
835
836
837
838 self.building.remove(build)
839 for req in build.requests:
840 req.finished(build.build_status)
841
843 """Mark the build as successful and update expectations for the next
844 build. Only call this when the build did not fail in any way that
845 would invalidate the time expectations generated by it. (if the
846 compile failed and thus terminated early, we can't use the last
847 build to predict how long the next one will take).
848 """
849 if self.expectations:
850 self.expectations.update(progress)
851 else:
852
853
854 self.expectations = Expectations(progress)
855 log.msg("new expectations: %s seconds" % \
856 self.expectations.expectedBuildTime())
857
861
862
864 implements(interfaces.IBuilderControl)
865
867 """Submit a BuildRequest to this Builder."""
868 self.original.submitBuildRequest(req)
869
871 """Submit a BuildRequest like requestBuild, but raise a
872 L{buildbot.interfaces.NoSlaveError} if no slaves are currently
873 available, so it cannot be used to queue a BuildRequest in the hopes
874 that a slave will eventually connect. This method is appropriate for
875 use by things like the web-page 'Force Build' button."""
876 if not self.original.slaves:
877 raise interfaces.NoSlaveError
878 self.requestBuild(req)
879
880 - def resubmitBuild(self, bs, reason="<rebuild, no reason given>"):
887
889
890 retval = []
891 for r in self.original.buildable:
892 retval.append(BuildRequestControl(self.original, r))
893
894 return retval
895
898
899 - def ping(self, timeout=30):
900 if not self.original.slaves:
901 self.original.builder_status.addPointEvent(["ping", "no slave"])
902 return defer.succeed(False)
903 dl = []
904 for s in self.original.slaves:
905 dl.append(s.ping(timeout, self.original.builder_status))
906 d = defer.DeferredList(dl)
907 d.addCallback(self._gatherPingResults)
908 return d
909
911 for ignored,success in res:
912 if not success:
913 return False
914 return True
915
916 components.registerAdapter(BuilderControl, Builder, interfaces.IBuilderControl)
917
919 implements(interfaces.IBuildRequestControl)
920
922 self.original_builder = builder
923 self.original_request = request
924
926 raise NotImplementedError
927
929 raise NotImplementedError
930
933