1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17 import time
18 from email.Message import Message
19 from email.Utils import formatdate
20 from zope.interface import implements
21 from twisted.python import log, failure
22 from twisted.internet import defer, reactor
23 from twisted.application import service
24 from twisted.spread import pb
25 from twisted.python.reflect import namedModule
26
27 from buildbot.status.slave import SlaveStatus
28 from buildbot.status.mail import MailNotifier
29 from buildbot.process import metrics
30 from buildbot.interfaces import IBuildSlave, ILatentBuildSlave
31 from buildbot.process.properties import Properties
32 from buildbot.locks import LockAccess
35 """This is the master-side representative for a remote buildbot slave.
36 There is exactly one for each slave described in the config file (the
37 c['slaves'] list). When buildbots connect in (.attach), they get a
38 reference to this instance. The BotMaster object is stashed as the
39 .botmaster attribute. The BotMaster is also our '.parent' Service.
40
41 I represent a build slave -- a remote machine capable of
42 running builds. I am instantiated by the configuration file, and can be
43 subclassed to add extra functionality."""
44
45 implements(IBuildSlave)
46 keepalive_timer = None
47 keepalive_interval = None
48
49 - def __init__(self, name, password, max_builds=None,
50 notify_on_missing=[], missing_timeout=3600,
51 properties={}, locks=None, keepalive_interval=3600):
52 """
53 @param name: botname this machine will supply when it connects
54 @param password: password this machine will supply when
55 it connects
56 @param max_builds: maximum number of simultaneous builds that will
57 be run concurrently on this buildslave (the
58 default is None for no limit)
59 @param properties: properties that will be applied to builds run on
60 this slave
61 @type properties: dictionary
62 @param locks: A list of locks that must be acquired before this slave
63 can be used
64 @type locks: dictionary
65 """
66 service.MultiService.__init__(self)
67 self.slavename = name
68 self.password = password
69 self.botmaster = None
70 self.slave_status = SlaveStatus(name)
71 self.slave = None
72 self.slave_commands = None
73 self.slavebuilders = {}
74 self.max_builds = max_builds
75 self.access = []
76 if locks:
77 self.access = locks
78
79 self.properties = Properties()
80 self.properties.update(properties, "BuildSlave")
81 self.properties.setProperty("slavename", name, "BuildSlave")
82
83 self.lastMessageReceived = 0
84 if isinstance(notify_on_missing, str):
85 notify_on_missing = [notify_on_missing]
86 self.notify_on_missing = notify_on_missing
87 for i in notify_on_missing:
88 assert isinstance(i, str)
89 self.missing_timeout = missing_timeout
90 self.missing_timer = None
91 self.keepalive_interval = keepalive_interval
92
93 self._old_builder_list = None
94
96 """
97 Return a tuple describing this slave. After reconfiguration a
98 new slave with the same identity will update this one, rather
99 than replacing it, thereby avoiding an interruption of current
100 activity.
101 """
102 return (self.slavename, self.password,
103 '%s.%s' % (self.__class__.__module__,
104 self.__class__.__name__))
105
127
137
147
149 """
150 I am called to see if all the locks I depend on are available,
151 in which I return True, otherwise I return False
152 """
153 if not self.locks:
154 return True
155 for lock, access in self.locks:
156 if not lock.isAvailable(access):
157 return False
158 return True
159
161 """
162 I am called when a build is preparing to run. I try to claim all
163 the locks that are needed for a build to happen. If I can't, then
164 my caller should give up the build and try to get another slave
165 to look at it.
166 """
167 log.msg("acquireLocks(slave %s, locks %s)" % (self, self.locks))
168 if not self.locksAvailable():
169 log.msg("slave %s can't lock, giving up" % (self, ))
170 return False
171
172 for lock, access in self.locks:
173 lock.claim(self, access)
174 return True
175
177 """
178 I am called to release any locks after a build has finished
179 """
180 log.msg("releaseLocks(%s): %s" % (self, self.locks))
181 for lock, access in self.locks:
182 lock.release(self, access)
183
189
191 if self.missing_timer:
192 self.missing_timer.cancel()
193 self.missing_timer = None
194
196 if self.notify_on_missing and self.missing_timeout and self.parent:
197 self.stopMissingTimer()
198 self.missing_timer = reactor.callLater(self.missing_timeout,
199 self._missing_timer_fired)
200
208
212
218
222
225
227 self.missing_timer = None
228
229 if not self.parent:
230 return
231
232 buildmaster = self.botmaster.parent
233 status = buildmaster.getStatus()
234 text = "The Buildbot working for '%s'\n" % status.getTitle()
235 text += ("has noticed that the buildslave named %s went away\n" %
236 self.slavename)
237 text += "\n"
238 text += ("It last disconnected at %s (buildmaster-local time)\n" %
239 time.ctime(time.time() - self.missing_timeout))
240 text += "\n"
241 text += "The admin on record (as reported by BUILDSLAVE:info/admin)\n"
242 text += "was '%s'.\n" % self.slave_status.getAdmin()
243 text += "\n"
244 text += "Sincerely,\n"
245 text += " The Buildbot\n"
246 text += " %s\n" % status.getTitleURL()
247 subject = "Buildbot: buildslave %s was lost" % self.slavename
248 return self._mail_missing_message(subject, text)
249
250
252 """Called to add or remove builders after the slave has connected.
253
254 @return: a Deferred that indicates when an attached slave has
255 accepted the new builders and/or released the old ones."""
256 if self.slave:
257 return self.sendBuilderList()
258 else:
259 return defer.succeed(None)
260
266
267 @metrics.countMethod('AbstractBuildSlave.attached()')
269 """This is called when the slave connects.
270
271 @return: a Deferred that fires when the attachment is complete
272 """
273
274
275 assert not self.isConnected()
276
277 metrics.MetricCountEvent.log("AbstractBuildSlave.attached_slaves", 1)
278
279
280
281
282
283
284
285 state = {}
286
287
288 self.slave_status.setGraceful(False)
289
290 self.slave_status.addGracefulWatcher(self._gracefulChanged)
291
292 d = defer.succeed(None)
293 def _log_attachment_on_slave(res):
294 d1 = bot.callRemote("print", "attached")
295 d1.addErrback(lambda why: None)
296 return d1
297 d.addCallback(_log_attachment_on_slave)
298
299 def _get_info(res):
300 d1 = bot.callRemote("getSlaveInfo")
301 def _got_info(info):
302 log.msg("Got slaveinfo from '%s'" % self.slavename)
303
304 state["admin"] = info.get("admin")
305 state["host"] = info.get("host")
306 state["access_uri"] = info.get("access_uri", None)
307 state["slave_environ"] = info.get("environ", {})
308 state["slave_basedir"] = info.get("basedir", None)
309 state["slave_system"] = info.get("system", None)
310 def _info_unavailable(why):
311 why.trap(pb.NoSuchMethod)
312
313 log.msg("BuildSlave.info_unavailable")
314 log.err(why)
315 d1.addCallbacks(_got_info, _info_unavailable)
316 return d1
317 d.addCallback(_get_info)
318 self.startKeepaliveTimer()
319
320 def _get_version(res):
321 d = bot.callRemote("getVersion")
322 def _got_version(version):
323 state["version"] = version
324 def _version_unavailable(why):
325 why.trap(pb.NoSuchMethod)
326
327 state["version"] = '(unknown)'
328 d.addCallbacks(_got_version, _version_unavailable)
329 return d
330 d.addCallback(_get_version)
331
332 def _get_commands(res):
333 d1 = bot.callRemote("getCommands")
334 def _got_commands(commands):
335 state["slave_commands"] = commands
336 def _commands_unavailable(why):
337
338 log.msg("BuildSlave._commands_unavailable")
339 if why.check(AttributeError):
340 return
341 log.err(why)
342 d1.addCallbacks(_got_commands, _commands_unavailable)
343 return d1
344 d.addCallback(_get_commands)
345
346 def _accept_slave(res):
347 self.slave_status.setAdmin(state.get("admin"))
348 self.slave_status.setHost(state.get("host"))
349 self.slave_status.setAccessURI(state.get("access_uri"))
350 self.slave_status.setVersion(state.get("version"))
351 self.slave_status.setConnected(True)
352 self.slave_commands = state.get("slave_commands")
353 self.slave_environ = state.get("slave_environ")
354 self.slave_basedir = state.get("slave_basedir")
355 self.slave_system = state.get("slave_system")
356 self.slave = bot
357 if self.slave_system == "win32":
358 self.path_module = namedModule("win32path")
359 else:
360
361
362 self.path_module = namedModule("posixpath")
363 log.msg("bot attached")
364 self.messageReceivedFromSlave()
365 self.stopMissingTimer()
366 self.botmaster.parent.status.slaveConnected(self.slavename)
367
368 return self.updateSlave()
369 d.addCallback(_accept_slave)
370 d.addCallback(lambda _:
371 self.botmaster.maybeStartBuildsForSlave(self.slavename))
372
373
374
375 d.addCallback(lambda _: self)
376 return d
377
382
392
394 """Forcibly disconnect the slave.
395
396 This severs the TCP connection and returns a Deferred that will fire
397 (with None) when the connection is probably gone.
398
399 If the slave is still alive, they will probably try to reconnect
400 again in a moment.
401
402 This is called in two circumstances. The first is when a slave is
403 removed from the config file. In this case, when they try to
404 reconnect, they will be rejected as an unknown slave. The second is
405 when we wind up with two connections for the same slave, in which
406 case we disconnect the older connection.
407 """
408
409 if not self.slave:
410 return defer.succeed(None)
411 log.msg("disconnecting old slave %s now" % self.slavename)
412
413 return self._disconnect(self.slave)
414
416
417
418
419
420
421
422 d = defer.Deferred()
423
424
425
426 def _disconnected(rref):
427 reactor.callLater(0, d.callback, None)
428 slave.notifyOnDisconnect(_disconnected)
429 tport = slave.broker.transport
430
431 tport.loseConnection()
432 try:
433
434
435
436
437
438
439
440 tport.offset = 0
441 tport.dataBuffer = ""
442 except:
443
444
445 log.msg("failed to accelerate the shutdown process")
446 log.msg("waiting for slave to finish disconnecting")
447
448 return d
449
451 our_builders = self.botmaster.getBuildersForSlave(self.slavename)
452 blist = [(b.name, b.slavebuilddir) for b in our_builders]
453 if blist == self._old_builder_list:
454 log.msg("Builder list is unchanged; not calling setBuilderList")
455 return defer.succeed(None)
456
457 d = self.slave.callRemote("setBuilderList", blist)
458 def sentBuilderList(ign):
459 self._old_builder_list = blist
460 return ign
461 d.addCallback(sentBuilderList)
462 return d
463
466
470
472 self.slavebuilders[sb.builder_name] = sb
473
475 try:
476 del self.slavebuilders[sb.builder_name]
477 except KeyError:
478 pass
479
483
485 """
486 I am called when a build is requested to see if this buildslave
487 can start a build. This function can be used to limit overall
488 concurrency on the buildslave.
489 """
490
491
492 if self.slave_status.getGraceful():
493 return False
494
495 if self.max_builds:
496 active_builders = [sb for sb in self.slavebuilders.values()
497 if sb.isBusy()]
498 if len(active_builders) >= self.max_builds:
499 return False
500
501 if not self.locksAvailable():
502 return False
503
504 return True
505
507
508
509 buildmaster = self.botmaster.parent
510 for st in buildmaster.statusTargets:
511 if isinstance(st, MailNotifier):
512 break
513 else:
514
515
516 log.msg("buildslave-missing msg using default MailNotifier")
517 st = MailNotifier("buildbot")
518
519
520 m = Message()
521 m.set_payload(text)
522 m['Date'] = formatdate(localtime=True)
523 m['Subject'] = subject
524 m['From'] = st.fromaddr
525 recipients = self.notify_on_missing
526 m['To'] = ", ".join(recipients)
527 d = st.sendMessage(m, recipients)
528
529 return d
530
532 """This is called when our graceful shutdown setting changes"""
533 self.maybeShutdown()
534
535 @defer.deferredGenerator
537 """Shutdown the slave"""
538 if not self.slave:
539 log.msg("no remote; slave is already shut down")
540 return
541
542
543
544
545 def new_way():
546 d = self.slave.callRemote('shutdown')
547 d.addCallback(lambda _ : True)
548 def check_nsm(f):
549 f.trap(pb.NoSuchMethod)
550 return False
551 d.addErrback(check_nsm)
552 def check_connlost(f):
553 f.trap(pb.PBConnectionLost)
554 return True
555 d.addErrback(check_connlost)
556 return d
557
558 wfd = defer.waitForDeferred(new_way())
559 yield wfd
560 if wfd.getResult():
561 return
562
563
564
565
566 def old_way():
567 d = None
568 for b in self.slavebuilders.values():
569 if b.remote:
570 d = b.remote.callRemote("shutdown")
571 break
572
573 if d:
574 log.msg("Shutting down (old) slave: %s" % self.slavename)
575
576
577
578
579
580
581 def _errback(why):
582 if why.check(pb.PBConnectionLost):
583 log.msg("Lost connection to %s" % self.slavename)
584 else:
585 log.err("Unexpected error when trying to shutdown %s" % self.slavename)
586 d.addErrback(_errback)
587 return d
588 log.err("Couldn't find remote builder to shut down slave")
589 return defer.succeed(None)
590 wfd = defer.waitForDeferred(old_way())
591 yield wfd
592 wfd.getResult()
593
595 """Shut down this slave if it has been asked to shut down gracefully,
596 and has no active builders."""
597 if not self.slave_status.getGraceful():
598 return
599 active_builders = [sb for sb in self.slavebuilders.values()
600 if sb.isBusy()]
601 if active_builders:
602 return
603 d = self.shutdown()
604 d.addErrback(log.err, 'error while shutting down slave')
605
607
622 def _set_failed(why):
623 log.msg("BuildSlave.sendBuilderList (%s) failed" % self)
624 log.err(why)
625
626
627 d.addCallbacks(_sent, _set_failed)
628 return d
629
634
642
723 d.addCallbacks(start_instance_result, clean_up)
724 return d
725
734
740
768
770 assert self.substantiated
771 self._clearBuildWaitTimer()
772 self.building.add(sb.builder_name)
773
780
786
788 self._clearBuildWaitTimer()
789 self.build_wait_timer = reactor.callLater(
790 self.build_wait_timeout, self._soft_disconnect)
791
802
833
840
842 res = defer.maybeDeferred(AbstractBuildSlave.stopService, self)
843 if self.slave is not None:
844 d = self._soft_disconnect()
845 res = defer.DeferredList([res, d])
846 return res
847
849 """Called to add or remove builders after the slave has connected.
850
851 Also called after botmaster's builders are initially set.
852
853 @return: a Deferred that indicates when an attached slave has
854 accepted the new builders and/or released the old ones."""
855 for b in self.botmaster.getBuildersForSlave(self.slavename):
856 if b.name not in self.slavebuilders:
857 b.addLatentSlave(self)
858 return AbstractBuildSlave.updateSlave(self)
859
876 def _set_failed(why):
877 log.msg("BuildSlave.sendBuilderList (%s) failed" % self)
878 log.err(why)
879
880
881 if self.substantiation_deferred:
882 d = self.substantiation_deferred
883 self.substantiation_deferred = None
884 self.substantiation_build = None
885 d.errback(why)
886 if self.missing_timer:
887 self.missing_timer.cancel()
888 self.missing_timer = None
889
890 return why
891 d.addCallbacks(_sent, _set_failed)
892 def _substantiated(res):
893 log.msg("Slave %s substantiated \o/" % self.slavename)
894 self.substantiated = True
895 if not self.substantiation_deferred:
896 log.msg("No substantiation deferred for %s" % self.slavename)
897 if self.substantiation_deferred:
898 log.msg("Firing %s substantiation deferred with success" % self.slavename)
899 d = self.substantiation_deferred
900 self.substantiation_deferred = None
901 self.substantiation_build = None
902 d.callback(True)
903
904
905 if not self.building:
906 self._setBuildWaitTimer()
907 d.addCallback(_substantiated)
908 return d
909