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 pass
322 log.msg("waiting for slave to finish disconnecting")
323
324 return d
325
327 our_builders = self.botmaster.getBuildersForSlave(self.slavename)
328 blist = [(b.name, b.slavebuilddir) for b in our_builders]
329 d = self.slave.callRemote("setBuilderList", blist)
330 return d
331
334
336 self.slavebuilders[sb.builder_name] = sb
337
339 try:
340 del self.slavebuilders[sb.builder_name]
341 except KeyError:
342 pass
343
345 """
346 I am called when a build is requested to see if this buildslave
347 can start a build. This function can be used to limit overall
348 concurrency on the buildslave.
349 """
350
351
352 if self.slave_status.getGraceful():
353 return False
354
355 if self.max_builds:
356 active_builders = [sb for sb in self.slavebuilders.values()
357 if sb.isBusy()]
358 if len(active_builders) >= self.max_builds:
359 return False
360 return True
361
363
364
365 buildmaster = self.botmaster.parent
366 for st in buildmaster.statusTargets:
367 if isinstance(st, MailNotifier):
368 break
369 else:
370
371
372 log.msg("buildslave-missing msg using default MailNotifier")
373 st = MailNotifier("buildbot")
374
375
376 m = Message()
377 m.set_payload(text)
378 m['Date'] = formatdate(localtime=True)
379 m['Subject'] = subject
380 m['From'] = st.fromaddr
381 recipients = self.notify_on_missing
382 m['To'] = ", ".join(recipients)
383 d = st.sendMessage(m, recipients)
384
385 return d
386
388 """This is called when our graceful shutdown setting changes"""
389 if graceful:
390 active_builders = [sb for sb in self.slavebuilders.values()
391 if sb.isBusy()]
392 if len(active_builders) == 0:
393
394 self.shutdown()
395
397 """Shutdown the slave"""
398
399
400
401 d = None
402 for b in self.slavebuilders.values():
403 if b.remote:
404 d = b.remote.callRemote("shutdown")
405 break
406
407 if d:
408 log.msg("Shutting down slave: %s" % self.slavename)
409
410
411
412
413
414
415 def _errback(why):
416 if why.check(twisted.spread.pb.PBConnectionLost):
417 log.msg("Lost connection to %s" % self.slavename)
418 else:
419 log.err("Unexpected error when trying to shutdown %s" % self.slavename)
420 d.addErrback(_errback)
421 return d
422 log.err("Couldn't find remote builder to shut down slave")
423 return defer.succeed(None)
424
426
438 def _set_failed(why):
439 log.msg("BuildSlave.sendBuilderList (%s) failed" % self)
440 log.err(why)
441
442
443 d.addCallbacks(_sent, _set_failed)
444 return d
445
450
452 """This is called when a build on this slave is finished."""
453
454
455 if self.slave_status.getGraceful():
456 active_builders = [sb for sb in self.slavebuilders.values()
457 if sb.isBusy()]
458 if len(active_builders) == 0:
459
460 return self.shutdown()
461 return defer.succeed(None)
462
533 d.addCallbacks(stash_reply, clean_up)
534 return d
535
544
549
575
577 assert self.substantiated
578 self._clearBuildWaitTimer()
579 self.building.add(sb.builder_name)
580
582 self.building.remove(sb.builder_name)
583 if not self.building:
584 self._setBuildWaitTimer()
585
591
593 self._clearBuildWaitTimer()
594 self.build_wait_timer = reactor.callLater(
595 self.build_wait_timeout, self._soft_disconnect)
596
607
635
637 d = self._soft_disconnect()
638
639
640 self.botmaster.slaveLost(self)
641
643 res = defer.maybeDeferred(AbstractBuildSlave.stopService, self)
644 if self.slave is not None:
645 d = self._soft_disconnect()
646 res = defer.DeferredList([res, d])
647 return res
648
650 """Called to add or remove builders after the slave has connected.
651
652 Also called after botmaster's builders are initially set.
653
654 @return: a Deferred that indicates when an attached slave has
655 accepted the new builders and/or released the old ones."""
656 for b in self.botmaster.getBuildersForSlave(self.slavename):
657 if b.name not in self.slavebuilders:
658 b.addLatentSlave(self)
659 return AbstractBuildSlave.updateSlave(self)
660
675 def _set_failed(why):
676 log.msg("BuildSlave.sendBuilderList (%s) failed" % self)
677 log.err(why)
678
679
680 if self.substantiation_deferred:
681 self.substantiation_deferred.errback()
682 self.substantiation_deferred = None
683 if self.missing_timer:
684 self.missing_timer.cancel()
685 self.missing_timer = None
686
687 return why
688 d.addCallbacks(_sent, _set_failed)
689 def _substantiated(res):
690 self.substantiated = True
691 if self.substantiation_deferred:
692 d = self.substantiation_deferred
693 del self.substantiation_deferred
694 res = self._start_result
695 del self._start_result
696 d.callback(res)
697
698
699 if not self.building:
700 self._setBuildWaitTimer()
701 d.addCallback(_substantiated)
702 return d
703