1
2
3 import time
4 from email.Message import Message
5 from email.Utils import formatdate
6 from zope.interface import implements
7 from twisted.python import log
8 from twisted.internet import defer, reactor
9 from twisted.application import service
10 import twisted.spread.pb
11
12 from buildbot.pbutil import NewCredPerspective
13 from buildbot.status.builder import SlaveStatus
14 from buildbot.status.mail import MailNotifier
15 from buildbot.interfaces import IBuildSlave, ILatentBuildSlave
16 from buildbot.process.properties import Properties
17
18 import sys
19 if sys.version_info[:3] < (2,4,0):
20 from sets import Set as set
21
23 """This is the master-side representative for a remote buildbot slave.
24 There is exactly one for each slave described in the config file (the
25 c['slaves'] list). When buildbots connect in (.attach), they get a
26 reference to this instance. The BotMaster object is stashed as the
27 .botmaster attribute. The BotMaster is also our '.parent' Service.
28
29 I represent a build slave -- a remote machine capable of
30 running builds. I am instantiated by the configuration file, and can be
31 subclassed to add extra functionality."""
32
33 implements(IBuildSlave)
34
35 - def __init__(self, name, password, max_builds=None,
36 notify_on_missing=[], missing_timeout=3600,
37 properties={}):
38 """
39 @param name: botname this machine will supply when it connects
40 @param password: password this machine will supply when
41 it connects
42 @param max_builds: maximum number of simultaneous builds that will
43 be run concurrently on this buildslave (the
44 default is None for no limit)
45 @param properties: properties that will be applied to builds run on
46 this slave
47 @type properties: dictionary
48 """
49 service.MultiService.__init__(self)
50 self.slavename = name
51 self.password = password
52 self.botmaster = None
53 self.slave_status = SlaveStatus(name)
54 self.slave = None
55 self.slave_commands = None
56 self.slavebuilders = {}
57 self.max_builds = max_builds
58
59 self.properties = Properties()
60 self.properties.update(properties, "BuildSlave")
61 self.properties.setProperty("slavename", name, "BuildSlave")
62
63 self.lastMessageReceived = 0
64 if isinstance(notify_on_missing, str):
65 notify_on_missing = [notify_on_missing]
66 self.notify_on_missing = notify_on_missing
67 for i in notify_on_missing:
68 assert isinstance(i, str)
69 self.missing_timeout = missing_timeout
70 self.missing_timer = None
71
73 """
74 Given a new BuildSlave, configure this one identically. Because
75 BuildSlave objects are remotely referenced, we can't replace them
76 without disconnecting the slave, yet there's no reason to do that.
77 """
78
79 assert self.slavename == new.slavename
80 assert self.password == new.password
81 assert self.__class__ == new.__class__
82 self.max_builds = new.max_builds
83
93
95 assert not self.botmaster, "BuildSlave already has a botmaster"
96 self.botmaster = botmaster
97 self.startMissingTimer()
98
100 if self.missing_timer:
101 self.missing_timer.cancel()
102 self.missing_timer = None
103
105 if self.notify_on_missing and self.missing_timeout and self.parent:
106 self.stopMissingTimer()
107 self.missing_timer = reactor.callLater(self.missing_timeout,
108 self._missing_timer_fired)
109
111 self.missing_timer = None
112
113 if not self.parent:
114 return
115
116 buildmaster = self.botmaster.parent
117 status = buildmaster.getStatus()
118 text = "The Buildbot working for '%s'\n" % status.getProjectName()
119 text += ("has noticed that the buildslave named %s went away\n" %
120 self.slavename)
121 text += "\n"
122 text += ("It last disconnected at %s (buildmaster-local time)\n" %
123 time.ctime(time.time() - self.missing_timeout))
124 text += "\n"
125 text += "The admin on record (as reported by BUILDSLAVE:info/admin)\n"
126 text += "was '%s'.\n" % self.slave_status.getAdmin()
127 text += "\n"
128 text += "Sincerely,\n"
129 text += " The Buildbot\n"
130 text += " %s\n" % status.getProjectURL()
131 subject = "Buildbot: buildslave %s was lost" % self.slavename
132 return self._mail_missing_message(subject, text)
133
134
136 """Called to add or remove builders after the slave has connected.
137
138 @return: a Deferred that indicates when an attached slave has
139 accepted the new builders and/or released the old ones."""
140 if self.slave:
141 return self.sendBuilderList()
142 else:
143 return defer.succeed(None)
144
150
152 """This is called when the slave connects.
153
154 @return: a Deferred that fires with a suitable pb.IPerspective to
155 give to the slave (i.e. 'self')"""
156
157 if self.slave:
158
159
160
161
162
163 log.msg("duplicate slave %s replacing old one" % self.slavename)
164
165
166
167
168 tport = self.slave.broker.transport
169 log.msg("old slave was connected from", tport.getPeer())
170 log.msg("new slave is from", bot.broker.transport.getPeer())
171 d = self.disconnect()
172 else:
173 d = defer.succeed(None)
174
175
176
177
178
179
180 state = {}
181
182
183 self.slave_status.setGraceful(False)
184
185 self.slave_status.addGracefulWatcher(self._gracefulChanged)
186
187 def _log_attachment_on_slave(res):
188 d1 = bot.callRemote("print", "attached")
189 d1.addErrback(lambda why: None)
190 return d1
191 d.addCallback(_log_attachment_on_slave)
192
193 def _get_info(res):
194 d1 = bot.callRemote("getSlaveInfo")
195 def _got_info(info):
196 log.msg("Got slaveinfo from '%s'" % self.slavename)
197
198 state["admin"] = info.get("admin")
199 state["host"] = info.get("host")
200 state["access_uri"] = info.get("access_uri", None)
201 def _info_unavailable(why):
202
203 log.msg("BuildSlave.info_unavailable")
204 log.err(why)
205 d1.addCallbacks(_got_info, _info_unavailable)
206 return d1
207 d.addCallback(_get_info)
208
209 def _get_version(res):
210 d1 = bot.callRemote("getVersion")
211 def _got_version(version):
212 state["version"] = version
213 def _version_unavailable(why):
214
215 log.msg("BuildSlave.version_unavailable")
216 log.err(why)
217 d1.addCallbacks(_got_version, _version_unavailable)
218 d.addCallback(_get_version)
219
220 def _get_commands(res):
221 d1 = bot.callRemote("getCommands")
222 def _got_commands(commands):
223 state["slave_commands"] = commands
224 def _commands_unavailable(why):
225
226 log.msg("BuildSlave._commands_unavailable")
227 if why.check(AttributeError):
228 return
229 log.err(why)
230 d1.addCallbacks(_got_commands, _commands_unavailable)
231 return d1
232 d.addCallback(_get_commands)
233
234 def _accept_slave(res):
235 self.slave_status.setAdmin(state.get("admin"))
236 self.slave_status.setHost(state.get("host"))
237 self.slave_status.setAccessURI(state.get("access_uri"))
238 self.slave_status.setVersion(state.get("version"))
239 self.slave_status.setConnected(True)
240 self.slave_commands = state.get("slave_commands")
241 self.slave = bot
242 log.msg("bot attached")
243 self.messageReceivedFromSlave()
244 self.stopMissingTimer()
245 self.botmaster.parent.status.slaveConnected(self.slavename)
246
247 return self.updateSlave()
248 d.addCallback(_accept_slave)
249 d.addCallback(lambda res: self.botmaster.triggerNewBuildCheck())
250
251
252
253 d.addCallback(lambda res: self)
254 return d
255
260
267
269 """Forcibly disconnect the slave.
270
271 This severs the TCP connection and returns a Deferred that will fire
272 (with None) when the connection is probably gone.
273
274 If the slave is still alive, they will probably try to reconnect
275 again in a moment.
276
277 This is called in two circumstances. The first is when a slave is
278 removed from the config file. In this case, when they try to
279 reconnect, they will be rejected as an unknown slave. The second is
280 when we wind up with two connections for the same slave, in which
281 case we disconnect the older connection.
282 """
283
284 if not self.slave:
285 return defer.succeed(None)
286 log.msg("disconnecting old slave %s now" % self.slavename)
287
288 return self._disconnect(self.slave)
289
291
292
293
294
295
296
297 d = defer.Deferred()
298
299
300
301 def _disconnected(rref):
302 reactor.callLater(0, d.callback, None)
303 slave.notifyOnDisconnect(_disconnected)
304 tport = slave.broker.transport
305
306 tport.loseConnection()
307 try:
308
309
310
311
312
313
314
315 tport.offset = 0
316 tport.dataBuffer = ""
317 except:
318
319
320 log.msg("failed to accelerate the shutdown process")
321 log.msg("waiting for slave to finish disconnecting")
322
323 return d
324
326 our_builders = self.botmaster.getBuildersForSlave(self.slavename)
327 blist = [(b.name, b.slavebuilddir) for b in our_builders]
328 d = self.slave.callRemote("setBuilderList", blist)
329 return d
330
333
335 self.slavebuilders[sb.builder_name] = sb
336
338 try:
339 del self.slavebuilders[sb.builder_name]
340 except KeyError:
341 pass
342
344 """
345 I am called when a build is requested to see if this buildslave
346 can start a build. This function can be used to limit overall
347 concurrency on the buildslave.
348 """
349
350
351 if self.slave_status.getGraceful():
352 return False
353
354 if self.max_builds:
355 active_builders = [sb for sb in self.slavebuilders.values()
356 if sb.isBusy()]
357 if len(active_builders) >= self.max_builds:
358 return False
359 return True
360
362
363
364 buildmaster = self.botmaster.parent
365 for st in buildmaster.statusTargets:
366 if isinstance(st, MailNotifier):
367 break
368 else:
369
370
371 log.msg("buildslave-missing msg using default MailNotifier")
372 st = MailNotifier("buildbot")
373
374
375 m = Message()
376 m.set_payload(text)
377 m['Date'] = formatdate(localtime=True)
378 m['Subject'] = subject
379 m['From'] = st.fromaddr
380 recipients = self.notify_on_missing
381 m['To'] = ", ".join(recipients)
382 d = st.sendMessage(m, recipients)
383
384 return d
385
387 """This is called when our graceful shutdown setting changes"""
388 if graceful:
389 active_builders = [sb for sb in self.slavebuilders.values()
390 if sb.isBusy()]
391 if len(active_builders) == 0:
392
393 self.shutdown()
394
396 """Shutdown the slave"""
397
398
399
400 d = None
401 for b in self.slavebuilders.values():
402 if b.remote:
403 d = b.remote.callRemote("shutdown")
404 break
405
406 if d:
407 log.msg("Shutting down slave: %s" % self.slavename)
408
409
410
411
412
413
414 def _errback(why):
415 if why.check(twisted.spread.pb.PBConnectionLost):
416 log.msg("Lost connection to %s" % self.slavename)
417 else:
418 log.err("Unexpected error when trying to shutdown %s" % self.slavename)
419 d.addErrback(_errback)
420 return d
421 log.err("Couldn't find remote builder to shut down slave")
422 return defer.succeed(None)
423
425
437 def _set_failed(why):
438 log.msg("BuildSlave.sendBuilderList (%s) failed" % self)
439 log.err(why)
440
441
442 d.addCallbacks(_sent, _set_failed)
443 return d
444
449
451 """This is called when a build on this slave is finished."""
452
453
454 if self.slave_status.getGraceful():
455 active_builders = [sb for sb in self.slavebuilders.values()
456 if sb.isBusy()]
457 if len(active_builders) == 0:
458
459 return self.shutdown()
460 return defer.succeed(None)
461
532 d.addCallbacks(stash_reply, clean_up)
533 return d
534
543
548
574
576 assert self.substantiated
577 self._clearBuildWaitTimer()
578 self.building.add(sb.builder_name)
579
581 self.building.remove(sb.builder_name)
582 if not self.building:
583 self._setBuildWaitTimer()
584
590
592 self._clearBuildWaitTimer()
593 self.build_wait_timer = reactor.callLater(
594 self.build_wait_timeout, self._soft_disconnect)
595
606
634
636 d = self._soft_disconnect()
637
638
639 self.botmaster.slaveLost(self)
640
642 res = defer.maybeDeferred(AbstractBuildSlave.stopService, self)
643 if self.slave is not None:
644 d = self._soft_disconnect()
645 res = defer.DeferredList([res, d])
646 return res
647
649 """Called to add or remove builders after the slave has connected.
650
651 Also called after botmaster's builders are initially set.
652
653 @return: a Deferred that indicates when an attached slave has
654 accepted the new builders and/or released the old ones."""
655 for b in self.botmaster.getBuildersForSlave(self.slavename):
656 if b.name not in self.slavebuilders:
657 b.addLatentSlave(self)
658 return AbstractBuildSlave.updateSlave(self)
659
674 def _set_failed(why):
675 log.msg("BuildSlave.sendBuilderList (%s) failed" % self)
676 log.err(why)
677
678
679 if self.substantiation_deferred:
680 self.substantiation_deferred.errback()
681 self.substantiation_deferred = None
682 if self.missing_timer:
683 self.missing_timer.cancel()
684 self.missing_timer = None
685
686 return why
687 d.addCallbacks(_sent, _set_failed)
688 def _substantiated(res):
689 self.substantiated = True
690 if self.substantiation_deferred:
691 d = self.substantiation_deferred
692 del self.substantiation_deferred
693 res = self._start_result
694 del self._start_result
695 d.callback(res)
696
697
698 if not self.building:
699 self._setBuildWaitTimer()
700 d.addCallback(_substantiated)
701 return d
702