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
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 def _info_unavailable(why):
201
202 log.msg("BuildSlave.info_unavailable")
203 log.err(why)
204 d1.addCallbacks(_got_info, _info_unavailable)
205 return d1
206 d.addCallback(_get_info)
207
208 def _get_commands(res):
209 d1 = bot.callRemote("getCommands")
210 def _got_commands(commands):
211 state["slave_commands"] = commands
212 def _commands_unavailable(why):
213
214 log.msg("BuildSlave._commands_unavailable")
215 if why.check(AttributeError):
216 return
217 log.err(why)
218 d1.addCallbacks(_got_commands, _commands_unavailable)
219 return d1
220 d.addCallback(_get_commands)
221
222 def _accept_slave(res):
223 self.slave_status.setAdmin(state.get("admin"))
224 self.slave_status.setHost(state.get("host"))
225 self.slave_status.setConnected(True)
226 self.slave_commands = state.get("slave_commands")
227 self.slave = bot
228 log.msg("bot attached")
229 self.messageReceivedFromSlave()
230 self.stopMissingTimer()
231
232 return self.updateSlave()
233 d.addCallback(_accept_slave)
234 d.addCallback(lambda res: self.botmaster.maybeStartAllBuilds())
235
236
237
238 d.addCallback(lambda res: self)
239 return d
240
245
251
253 """Forcibly disconnect the slave.
254
255 This severs the TCP connection and returns a Deferred that will fire
256 (with None) when the connection is probably gone.
257
258 If the slave is still alive, they will probably try to reconnect
259 again in a moment.
260
261 This is called in two circumstances. The first is when a slave is
262 removed from the config file. In this case, when they try to
263 reconnect, they will be rejected as an unknown slave. The second is
264 when we wind up with two connections for the same slave, in which
265 case we disconnect the older connection.
266 """
267
268 if not self.slave:
269 return defer.succeed(None)
270 log.msg("disconnecting old slave %s now" % self.slavename)
271
272 return self._disconnect(self.slave)
273
275
276
277
278
279
280
281 d = defer.Deferred()
282
283
284
285 def _disconnected(rref):
286 reactor.callLater(0, d.callback, None)
287 slave.notifyOnDisconnect(_disconnected)
288 tport = slave.broker.transport
289
290 tport.loseConnection()
291 try:
292
293
294
295
296
297
298
299 tport.offset = 0
300 tport.dataBuffer = ""
301 except:
302
303
304 log.msg("failed to accelerate the shutdown process")
305 pass
306 log.msg("waiting for slave to finish disconnecting")
307
308 return d
309
315
318
320 if sb.builder_name not in self.slavebuilders:
321 log.msg("%s adding %s" % (self, sb))
322 elif sb is not self.slavebuilders[sb.builder_name]:
323 log.msg("%s replacing %s" % (self, sb))
324 else:
325 return
326 self.slavebuilders[sb.builder_name] = sb
327
329 try:
330 del self.slavebuilders[sb.builder_name]
331 except KeyError:
332 pass
333 else:
334 log.msg("%s removed %s" % (self, sb))
335
337 """
338 I am called when a build is requested to see if this buildslave
339 can start a build. This function can be used to limit overall
340 concurrency on the buildslave.
341 """
342
343
344 if self.slave_status.getGraceful():
345 return False
346
347 if self.max_builds:
348 active_builders = [sb for sb in self.slavebuilders.values()
349 if sb.isBusy()]
350 if len(active_builders) >= self.max_builds:
351 return False
352 return True
353
355
356
357 buildmaster = self.botmaster.parent
358 for st in buildmaster.statusTargets:
359 if isinstance(st, MailNotifier):
360 break
361 else:
362
363
364 log.msg("buildslave-missing msg using default MailNotifier")
365 st = MailNotifier("buildbot")
366
367
368 m = Message()
369 m.set_payload(text)
370 m['Date'] = formatdate(localtime=True)
371 m['Subject'] = subject
372 m['From'] = st.fromaddr
373 recipients = self.notify_on_missing
374 m['To'] = ", ".join(recipients)
375 d = st.sendMessage(m, recipients)
376
377 return d
378
380 """This is called when our graceful shutdown setting changes"""
381 if graceful:
382 active_builders = [sb for sb in self.slavebuilders.values()
383 if sb.isBusy()]
384 if len(active_builders) == 0:
385
386 self.shutdown()
387
389 """Shutdown the slave"""
390
391
392
393 d = None
394 for b in self.slavebuilders.values():
395 if b.remote:
396 d = b.remote.callRemote("shutdown")
397 break
398
399 if d:
400 log.msg("Shutting down slave: %s" % self.slavename)
401
402
403
404
405
406
407 def _errback(why):
408 if why.check(twisted.spread.pb.PBConnectionLost):
409 log.msg("Lost connection to %s" % self.slavename)
410 else:
411 log.err("Unexpected error when trying to shutdown %s" % self.slavename)
412 d.addErrback(_errback)
413 return d
414 log.err("Couldn't find remote builder to shut down slave")
415 return defer.succeed(None)
416
418
430 def _set_failed(why):
431 log.msg("BuildSlave.sendBuilderList (%s) failed" % self)
432 log.err(why)
433
434
435 d.addCallbacks(_sent, _set_failed)
436 return d
437
442
444 """This is called when a build on this slave is finished."""
445
446
447 if self.slave_status.getGraceful():
448 active_builders = [sb for sb in self.slavebuilders.values()
449 if sb.isBusy()]
450 if len(active_builders) == 0:
451
452 return self.shutdown()
453 return defer.succeed(None)
454
525 d.addCallbacks(stash_reply, clean_up)
526 return d
527
536
541
567
572
577
583
588
599
627
633
640
642 """Called to add or remove builders after the slave has connected.
643
644 Also called after botmaster's builders are initially set.
645
646 @return: a Deferred that indicates when an attached slave has
647 accepted the new builders and/or released the old ones."""
648 for b in self.botmaster.getBuildersForSlave(self.slavename):
649 if b.name not in self.slavebuilders:
650 b.addLatentSlave(self)
651 return AbstractBuildSlave.updateSlave(self)
652
667 def _set_failed(why):
668 log.msg("BuildSlave.sendBuilderList (%s) failed" % self)
669 log.err(why)
670
671
672 if self.substantiation_deferred:
673 self.substantiation_deferred.errback()
674 self.substantiation_deferred = None
675 if self.missing_timer:
676 self.missing_timer.cancel()
677 self.missing_timer = None
678
679 return why
680 d.addCallbacks(_sent, _set_failed)
681 def _substantiated(res):
682 self.substantiated = True
683 if self.substantiation_deferred:
684 d = self.substantiation_deferred
685 del self.substantiation_deferred
686 res = self._start_result
687 del self._start_result
688 d.callback(res)
689
690
691 if not self.building:
692 self._setBuildWaitTimer()
693 d.addCallback(_substantiated)
694 return d
695