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
246 return self.updateSlave()
247 d.addCallback(_accept_slave)
248 d.addCallback(lambda res: self.botmaster.maybeStartAllBuilds())
249
250
251
252 d.addCallback(lambda res: self)
253 return d
254
259
265
267 """Forcibly disconnect the slave.
268
269 This severs the TCP connection and returns a Deferred that will fire
270 (with None) when the connection is probably gone.
271
272 If the slave is still alive, they will probably try to reconnect
273 again in a moment.
274
275 This is called in two circumstances. The first is when a slave is
276 removed from the config file. In this case, when they try to
277 reconnect, they will be rejected as an unknown slave. The second is
278 when we wind up with two connections for the same slave, in which
279 case we disconnect the older connection.
280 """
281
282 if not self.slave:
283 return defer.succeed(None)
284 log.msg("disconnecting old slave %s now" % self.slavename)
285
286 return self._disconnect(self.slave)
287
289
290
291
292
293
294
295 d = defer.Deferred()
296
297
298
299 def _disconnected(rref):
300 reactor.callLater(0, d.callback, None)
301 slave.notifyOnDisconnect(_disconnected)
302 tport = slave.broker.transport
303
304 tport.loseConnection()
305 try:
306
307
308
309
310
311
312
313 tport.offset = 0
314 tport.dataBuffer = ""
315 except:
316
317
318 log.msg("failed to accelerate the shutdown process")
319 pass
320 log.msg("waiting for slave to finish disconnecting")
321
322 return d
323
325 our_builders = self.botmaster.getBuildersForSlave(self.slavename)
326 blist = [(b.name, b.slavebuilddir) for b in our_builders]
327 d = self.slave.callRemote("setBuilderList", blist)
328 return d
329
332
334 self.slavebuilders[sb.builder_name] = sb
335
337 try:
338 del self.slavebuilders[sb.builder_name]
339 except KeyError:
340 pass
341
343 """
344 I am called when a build is requested to see if this buildslave
345 can start a build. This function can be used to limit overall
346 concurrency on the buildslave.
347 """
348
349
350 if self.slave_status.getGraceful():
351 return False
352
353 if self.max_builds:
354 active_builders = [sb for sb in self.slavebuilders.values()
355 if sb.isBusy()]
356 if len(active_builders) >= self.max_builds:
357 return False
358 return True
359
361
362
363 buildmaster = self.botmaster.parent
364 for st in buildmaster.statusTargets:
365 if isinstance(st, MailNotifier):
366 break
367 else:
368
369
370 log.msg("buildslave-missing msg using default MailNotifier")
371 st = MailNotifier("buildbot")
372
373
374 m = Message()
375 m.set_payload(text)
376 m['Date'] = formatdate(localtime=True)
377 m['Subject'] = subject
378 m['From'] = st.fromaddr
379 recipients = self.notify_on_missing
380 m['To'] = ", ".join(recipients)
381 d = st.sendMessage(m, recipients)
382
383 return d
384
386 """This is called when our graceful shutdown setting changes"""
387 if graceful:
388 active_builders = [sb for sb in self.slavebuilders.values()
389 if sb.isBusy()]
390 if len(active_builders) == 0:
391
392 self.shutdown()
393
395 """Shutdown the slave"""
396
397
398
399 d = None
400 for b in self.slavebuilders.values():
401 if b.remote:
402 d = b.remote.callRemote("shutdown")
403 break
404
405 if d:
406 log.msg("Shutting down slave: %s" % self.slavename)
407
408
409
410
411
412
413 def _errback(why):
414 if why.check(twisted.spread.pb.PBConnectionLost):
415 log.msg("Lost connection to %s" % self.slavename)
416 else:
417 log.err("Unexpected error when trying to shutdown %s" % self.slavename)
418 d.addErrback(_errback)
419 return d
420 log.err("Couldn't find remote builder to shut down slave")
421 return defer.succeed(None)
422
424
436 def _set_failed(why):
437 log.msg("BuildSlave.sendBuilderList (%s) failed" % self)
438 log.err(why)
439
440
441 d.addCallbacks(_sent, _set_failed)
442 return d
443
448
450 """This is called when a build on this slave is finished."""
451
452
453 if self.slave_status.getGraceful():
454 active_builders = [sb for sb in self.slavebuilders.values()
455 if sb.isBusy()]
456 if len(active_builders) == 0:
457
458 return self.shutdown()
459 return defer.succeed(None)
460
531 d.addCallbacks(stash_reply, clean_up)
532 return d
533
542
547
573
575 assert self.substantiated
576 self._clearBuildWaitTimer()
577 self.building.add(sb.builder_name)
578
580 self.building.remove(sb.builder_name)
581 if not self.building:
582 self._setBuildWaitTimer()
583
589
591 self._clearBuildWaitTimer()
592 self.build_wait_timer = reactor.callLater(
593 self.build_wait_timeout, self._soft_disconnect)
594
605
633
635 d = self._soft_disconnect()
636
637
638 self.botmaster.slaveLost(self)
639
641 res = defer.maybeDeferred(AbstractBuildSlave.stopService, self)
642 if self.slave is not None:
643 d = self._soft_disconnect()
644 res = defer.DeferredList([res, d])
645 return res
646
648 """Called to add or remove builders after the slave has connected.
649
650 Also called after botmaster's builders are initially set.
651
652 @return: a Deferred that indicates when an attached slave has
653 accepted the new builders and/or released the old ones."""
654 for b in self.botmaster.getBuildersForSlave(self.slavename):
655 if b.name not in self.slavebuilders:
656 b.addLatentSlave(self)
657 return AbstractBuildSlave.updateSlave(self)
658
673 def _set_failed(why):
674 log.msg("BuildSlave.sendBuilderList (%s) failed" % self)
675 log.err(why)
676
677
678 if self.substantiation_deferred:
679 self.substantiation_deferred.errback()
680 self.substantiation_deferred = None
681 if self.missing_timer:
682 self.missing_timer.cancel()
683 self.missing_timer = None
684
685 return why
686 d.addCallbacks(_sent, _set_failed)
687 def _substantiated(res):
688 self.substantiated = True
689 if self.substantiation_deferred:
690 d = self.substantiation_deferred
691 del self.substantiation_deferred
692 res = self._start_result
693 del self._start_result
694 d.callback(res)
695
696
697 if not self.building:
698 self._setBuildWaitTimer()
699 d.addCallback(_substantiated)
700 return d
701