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 from twisted.spread import pb
11
12 from buildbot.status.builder import SlaveStatus
13 from buildbot.status.mail import MailNotifier
14 from buildbot.interfaces import IBuildSlave, ILatentBuildSlave
15 from buildbot.process.properties import Properties
16 from buildbot.locks import LockAccess
17
18 import sys
19
21 """This is the master-side representative for a remote buildbot slave.
22 There is exactly one for each slave described in the config file (the
23 c['slaves'] list). When buildbots connect in (.attach), they get a
24 reference to this instance. The BotMaster object is stashed as the
25 .botmaster attribute. The BotMaster is also our '.parent' Service.
26
27 I represent a build slave -- a remote machine capable of
28 running builds. I am instantiated by the configuration file, and can be
29 subclassed to add extra functionality."""
30
31 implements(IBuildSlave)
32
33 - def __init__(self, name, password, max_builds=None,
34 notify_on_missing=[], missing_timeout=3600,
35 properties={}, locks=None):
36 """
37 @param name: botname this machine will supply when it connects
38 @param password: password this machine will supply when
39 it connects
40 @param max_builds: maximum number of simultaneous builds that will
41 be run concurrently on this buildslave (the
42 default is None for no limit)
43 @param properties: properties that will be applied to builds run on
44 this slave
45 @type properties: dictionary
46 @param locks: A list of locks that must be acquired before this slave
47 can be used
48 @type locks: dictionary
49 """
50 service.MultiService.__init__(self)
51 self.slavename = name
52 self.password = password
53 self.botmaster = None
54 self.slave_status = SlaveStatus(name)
55 self.slave = None
56 self.slave_commands = None
57 self.slavebuilders = {}
58 self.max_builds = max_builds
59 self.access = []
60 if locks:
61 self.access = locks
62
63 self.properties = Properties()
64 self.properties.update(properties, "BuildSlave")
65 self.properties.setProperty("slavename", name, "BuildSlave")
66
67 self.lastMessageReceived = 0
68 if isinstance(notify_on_missing, str):
69 notify_on_missing = [notify_on_missing]
70 self.notify_on_missing = notify_on_missing
71 for i in notify_on_missing:
72 assert isinstance(i, str)
73 self.missing_timeout = missing_timeout
74 self.missing_timer = None
75
77 """
78 Given a new BuildSlave, configure this one identically. Because
79 BuildSlave objects are remotely referenced, we can't replace them
80 without disconnecting the slave, yet there's no reason to do that.
81 """
82
83 assert self.slavename == new.slavename
84 assert self.password == new.password
85 assert self.__class__ == new.__class__
86 self.max_builds = new.max_builds
87 self.access = new.access
88 self.notify_on_missing = new.notify_on_missing
89 self.missing_timeout = new.missing_timeout
90
91 self.properties.updateFromProperties(new.properties)
92
93 if self.botmaster:
94 self.updateLocks()
95
105
115
117 """
118 I am called to see if all the locks I depend on are available,
119 in which I return True, otherwise I return False
120 """
121 if not self.locks:
122 return True
123 for lock, access in self.locks:
124 if not lock.isAvailable(access):
125 return False
126 return True
127
129 """
130 I am called when a build is preparing to run. I try to claim all
131 the locks that are needed for a build to happen. If I can't, then
132 my caller should give up the build and try to get another slave
133 to look at it.
134 """
135 log.msg("acquireLocks(slave %s, locks %s)" % (self, self.locks))
136 if not self.locksAvailable():
137 log.msg("slave %s can't lock, giving up" % (self, ))
138 return False
139
140 for lock, access in self.locks:
141 lock.claim(self, access)
142 return True
143
145 """
146 I am called to release any locks after a build has finished
147 """
148 log.msg("releaseLocks(%s): %s" % (self, self.locks))
149 for lock, access in self.locks:
150 lock.release(self, access)
151
153 assert not self.botmaster, "BuildSlave already has a botmaster"
154 self.botmaster = botmaster
155 self.updateLocks()
156 self.startMissingTimer()
157
159 if self.missing_timer:
160 self.missing_timer.cancel()
161 self.missing_timer = None
162
164 if self.notify_on_missing and self.missing_timeout and self.parent:
165 self.stopMissingTimer()
166 self.missing_timer = reactor.callLater(self.missing_timeout,
167 self._missing_timer_fired)
168
172
175
177 self.missing_timer = None
178
179 if not self.parent:
180 return
181
182 buildmaster = self.botmaster.parent
183 status = buildmaster.getStatus()
184 text = "The Buildbot working for '%s'\n" % status.getProjectName()
185 text += ("has noticed that the buildslave named %s went away\n" %
186 self.slavename)
187 text += "\n"
188 text += ("It last disconnected at %s (buildmaster-local time)\n" %
189 time.ctime(time.time() - self.missing_timeout))
190 text += "\n"
191 text += "The admin on record (as reported by BUILDSLAVE:info/admin)\n"
192 text += "was '%s'.\n" % self.slave_status.getAdmin()
193 text += "\n"
194 text += "Sincerely,\n"
195 text += " The Buildbot\n"
196 text += " %s\n" % status.getProjectURL()
197 subject = "Buildbot: buildslave %s was lost" % self.slavename
198 return self._mail_missing_message(subject, text)
199
200
202 """Called to add or remove builders after the slave has connected.
203
204 @return: a Deferred that indicates when an attached slave has
205 accepted the new builders and/or released the old ones."""
206 if self.slave:
207 return self.sendBuilderList()
208 else:
209 return defer.succeed(None)
210
216
218 """This is called when the slave connects.
219
220 @return: a Deferred that fires when the attachment is complete
221 """
222
223
224 assert not self.isConnected()
225
226
227
228
229
230
231
232 state = {}
233
234
235 self.slave_status.setGraceful(False)
236
237 self.slave_status.addGracefulWatcher(self._gracefulChanged)
238
239 d = defer.succeed(None)
240 def _log_attachment_on_slave(res):
241 d1 = bot.callRemote("print", "attached")
242 d1.addErrback(lambda why: None)
243 return d1
244 d.addCallback(_log_attachment_on_slave)
245
246 def _get_info(res):
247 d1 = bot.callRemote("getSlaveInfo")
248 def _got_info(info):
249 log.msg("Got slaveinfo from '%s'" % self.slavename)
250
251 state["admin"] = info.get("admin")
252 state["host"] = info.get("host")
253 state["access_uri"] = info.get("access_uri", None)
254 def _info_unavailable(why):
255
256 log.msg("BuildSlave.info_unavailable")
257 log.err(why)
258 d1.addCallbacks(_got_info, _info_unavailable)
259 return d1
260 d.addCallback(_get_info)
261
262 def _get_version(res):
263 d1 = bot.callRemote("getVersion")
264 def _got_version(version):
265 state["version"] = version
266 def _version_unavailable(why):
267
268 log.msg("BuildSlave.version_unavailable")
269 log.err(why)
270 d1.addCallbacks(_got_version, _version_unavailable)
271 d.addCallback(_get_version)
272
273 def _get_commands(res):
274 d1 = bot.callRemote("getCommands")
275 def _got_commands(commands):
276 state["slave_commands"] = commands
277 def _commands_unavailable(why):
278
279 log.msg("BuildSlave._commands_unavailable")
280 if why.check(AttributeError):
281 return
282 log.err(why)
283 d1.addCallbacks(_got_commands, _commands_unavailable)
284 return d1
285 d.addCallback(_get_commands)
286
287 def _accept_slave(res):
288 self.slave_status.setAdmin(state.get("admin"))
289 self.slave_status.setHost(state.get("host"))
290 self.slave_status.setAccessURI(state.get("access_uri"))
291 self.slave_status.setVersion(state.get("version"))
292 self.slave_status.setConnected(True)
293 self.slave_commands = state.get("slave_commands")
294 self.slave = bot
295 log.msg("bot attached")
296 self.messageReceivedFromSlave()
297 self.stopMissingTimer()
298 self.botmaster.parent.status.slaveConnected(self.slavename)
299
300 return self.updateSlave()
301 d.addCallback(_accept_slave)
302 d.addCallback(lambda res: self.botmaster.triggerNewBuildCheck())
303
304
305
306 d.addCallback(lambda res: self)
307 return d
308
313
320
322 """Forcibly disconnect the slave.
323
324 This severs the TCP connection and returns a Deferred that will fire
325 (with None) when the connection is probably gone.
326
327 If the slave is still alive, they will probably try to reconnect
328 again in a moment.
329
330 This is called in two circumstances. The first is when a slave is
331 removed from the config file. In this case, when they try to
332 reconnect, they will be rejected as an unknown slave. The second is
333 when we wind up with two connections for the same slave, in which
334 case we disconnect the older connection.
335 """
336
337 if not self.slave:
338 return defer.succeed(None)
339 log.msg("disconnecting old slave %s now" % self.slavename)
340
341 return self._disconnect(self.slave)
342
344
345
346
347
348
349
350 d = defer.Deferred()
351
352
353
354 def _disconnected(rref):
355 reactor.callLater(0, d.callback, None)
356 slave.notifyOnDisconnect(_disconnected)
357 tport = slave.broker.transport
358
359 tport.loseConnection()
360 try:
361
362
363
364
365
366
367
368 tport.offset = 0
369 tport.dataBuffer = ""
370 except:
371
372
373 log.msg("failed to accelerate the shutdown process")
374 log.msg("waiting for slave to finish disconnecting")
375
376 return d
377
379 our_builders = self.botmaster.getBuildersForSlave(self.slavename)
380 blist = [(b.name, b.slavebuilddir) for b in our_builders]
381 d = self.slave.callRemote("setBuilderList", blist)
382 return d
383
386
388 self.slavebuilders[sb.builder_name] = sb
389
391 try:
392 del self.slavebuilders[sb.builder_name]
393 except KeyError:
394 pass
395
397 """
398 I am called when a build is requested to see if this buildslave
399 can start a build. This function can be used to limit overall
400 concurrency on the buildslave.
401 """
402
403
404 if self.slave_status.getGraceful():
405 return False
406
407 if self.max_builds:
408 active_builders = [sb for sb in self.slavebuilders.values()
409 if sb.isBusy()]
410 if len(active_builders) >= self.max_builds:
411 return False
412
413 if not self.locksAvailable():
414 return False
415
416 return True
417
419
420
421 buildmaster = self.botmaster.parent
422 for st in buildmaster.statusTargets:
423 if isinstance(st, MailNotifier):
424 break
425 else:
426
427
428 log.msg("buildslave-missing msg using default MailNotifier")
429 st = MailNotifier("buildbot")
430
431
432 m = Message()
433 m.set_payload(text)
434 m['Date'] = formatdate(localtime=True)
435 m['Subject'] = subject
436 m['From'] = st.fromaddr
437 recipients = self.notify_on_missing
438 m['To'] = ", ".join(recipients)
439 d = st.sendMessage(m, recipients)
440
441 return d
442
444 """This is called when our graceful shutdown setting changes"""
445 if graceful:
446 active_builders = [sb for sb in self.slavebuilders.values()
447 if sb.isBusy()]
448 if len(active_builders) == 0:
449
450 self.shutdown()
451
453 """Shutdown the slave"""
454
455
456
457 d = None
458 for b in self.slavebuilders.values():
459 if b.remote:
460 d = b.remote.callRemote("shutdown")
461 break
462
463 if d:
464 log.msg("Shutting down slave: %s" % self.slavename)
465
466
467
468
469
470
471 def _errback(why):
472 if why.check(pb.PBConnectionLost):
473 log.msg("Lost connection to %s" % self.slavename)
474 else:
475 log.err("Unexpected error when trying to shutdown %s" % self.slavename)
476 d.addErrback(_errback)
477 return d
478 log.err("Couldn't find remote builder to shut down slave")
479 return defer.succeed(None)
480
482
494 def _set_failed(why):
495 log.msg("BuildSlave.sendBuilderList (%s) failed" % self)
496 log.err(why)
497
498
499 d.addCallbacks(_sent, _set_failed)
500 return d
501
506
508 """This is called when a build on this slave is finished."""
509
510
511 if self.slave_status.getGraceful():
512 active_builders = [sb for sb in self.slavebuilders.values()
513 if sb.isBusy()]
514 if len(active_builders) == 0:
515
516 return self.shutdown()
517 return defer.succeed(None)
518
520 """A build slave that will start up a slave instance when needed.
521
522 To use, subclass and implement start_instance and stop_instance.
523
524 See ec2buildslave.py for a concrete example. Also see the stub example in
525 test/test_slaves.py.
526 """
527
528 implements(ILatentBuildSlave)
529
530 substantiated = False
531 substantiation_deferred = None
532 build_wait_timer = None
533 _shutdown_callback_handle = None
534
535 - def __init__(self, name, password, max_builds=None,
536 notify_on_missing=[], missing_timeout=60*20,
537 build_wait_timeout=60*10,
538 properties={}, locks=None):
544
546
547
548
549 raise NotImplementedError
550
552
553 raise NotImplementedError
554
572
585 def clean_up(failure):
586 if self.missing_timer is not None:
587 self.missing_timer.cancel()
588 self._substantiation_failed(failure)
589 if self._shutdown_callback_handle is not None:
590 handle = self._shutdown_callback_handle
591 del self._shutdown_callback_handle
592 reactor.removeSystemEventTrigger(handle)
593 return failure
594 d.addCallbacks(start_instance_result, clean_up)
595 return d
596
605
610
637
639 assert self.substantiated
640 self._clearBuildWaitTimer()
641 self.building.add(sb.builder_name)
642
644 self.building.remove(sb.builder_name)
645 if not self.building:
646 self._setBuildWaitTimer()
647
653
655 self._clearBuildWaitTimer()
656 self.build_wait_timer = reactor.callLater(
657 self.build_wait_timeout, self._soft_disconnect)
658
669
697
699 d = self._soft_disconnect()
700
701
702 self.botmaster.slaveLost(self)
703
705 res = defer.maybeDeferred(AbstractBuildSlave.stopService, self)
706 if self.slave is not None:
707 d = self._soft_disconnect()
708 res = defer.DeferredList([res, d])
709 return res
710
712 """Called to add or remove builders after the slave has connected.
713
714 Also called after botmaster's builders are initially set.
715
716 @return: a Deferred that indicates when an attached slave has
717 accepted the new builders and/or released the old ones."""
718 for b in self.botmaster.getBuildersForSlave(self.slavename):
719 if b.name not in self.slavebuilders:
720 b.addLatentSlave(self)
721 return AbstractBuildSlave.updateSlave(self)
722
737 def _set_failed(why):
738 log.msg("BuildSlave.sendBuilderList (%s) failed" % self)
739 log.err(why)
740
741
742 if self.substantiation_deferred:
743 self.substantiation_deferred.errback()
744 self.substantiation_deferred = None
745 if self.missing_timer:
746 self.missing_timer.cancel()
747 self.missing_timer = None
748
749 return why
750 d.addCallbacks(_sent, _set_failed)
751 def _substantiated(res):
752 log.msg("Slave %s substantiated \o/" % self.slavename)
753 self.substantiated = True
754 if not self.substantiation_deferred:
755 log.msg("No substantiation deferred for %s" % self.slavename)
756 if self.substantiation_deferred:
757 log.msg("Firing %s substantiation deferred with success" % self.slavename)
758 d = self.substantiation_deferred
759 del self.substantiation_deferred
760 d.callback(True)
761
762
763 if not self.building:
764 self._setBuildWaitTimer()
765 d.addCallback(_substantiated)
766 return d
767