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
22 from twisted.internet import defer, reactor
23 from twisted.application import service
24 from twisted.spread import pb
25
26 from buildbot.status.builder import SlaveStatus
27 from buildbot.status.mail import MailNotifier
28 from buildbot.interfaces import IBuildSlave, ILatentBuildSlave
29 from buildbot.process.properties import Properties
30 from buildbot.locks import LockAccess
33 """This is the master-side representative for a remote buildbot slave.
34 There is exactly one for each slave described in the config file (the
35 c['slaves'] list). When buildbots connect in (.attach), they get a
36 reference to this instance. The BotMaster object is stashed as the
37 .botmaster attribute. The BotMaster is also our '.parent' Service.
38
39 I represent a build slave -- a remote machine capable of
40 running builds. I am instantiated by the configuration file, and can be
41 subclassed to add extra functionality."""
42
43 implements(IBuildSlave)
44
45 - def __init__(self, name, password, max_builds=None,
46 notify_on_missing=[], missing_timeout=3600,
47 properties={}, locks=None):
48 """
49 @param name: botname this machine will supply when it connects
50 @param password: password this machine will supply when
51 it connects
52 @param max_builds: maximum number of simultaneous builds that will
53 be run concurrently on this buildslave (the
54 default is None for no limit)
55 @param properties: properties that will be applied to builds run on
56 this slave
57 @type properties: dictionary
58 @param locks: A list of locks that must be acquired before this slave
59 can be used
60 @type locks: dictionary
61 """
62 service.MultiService.__init__(self)
63 self.slavename = name
64 self.password = password
65 self.botmaster = None
66 self.slave_status = SlaveStatus(name)
67 self.slave = None
68 self.slave_commands = None
69 self.slavebuilders = {}
70 self.max_builds = max_builds
71 self.access = []
72 if locks:
73 self.access = locks
74
75 self.properties = Properties()
76 self.properties.update(properties, "BuildSlave")
77 self.properties.setProperty("slavename", name, "BuildSlave")
78
79 self.lastMessageReceived = 0
80 if isinstance(notify_on_missing, str):
81 notify_on_missing = [notify_on_missing]
82 self.notify_on_missing = notify_on_missing
83 for i in notify_on_missing:
84 assert isinstance(i, str)
85 self.missing_timeout = missing_timeout
86 self.missing_timer = None
87
89 """
90 Given a new BuildSlave, configure this one identically. Because
91 BuildSlave objects are remotely referenced, we can't replace them
92 without disconnecting the slave, yet there's no reason to do that.
93 """
94
95 assert self.slavename == new.slavename
96 assert self.password == new.password
97 assert self.__class__ == new.__class__
98 self.max_builds = new.max_builds
99 self.access = new.access
100 self.notify_on_missing = new.notify_on_missing
101 self.missing_timeout = new.missing_timeout
102
103 self.properties.updateFromProperties(new.properties)
104
105 if self.botmaster:
106 self.updateLocks()
107
109 if self.botmaster:
110 builders = self.botmaster.getBuildersForSlave(self.slavename)
111 return "<%s '%s', current builders: %s>" % \
112 (self.__class__.__name__, self.slavename,
113 ','.join(map(lambda b: b.name, builders)))
114 else:
115 return "<%s '%s', (no builders yet)>" % \
116 (self.__class__.__name__, self.slavename)
117
127
129 """
130 I am called to see if all the locks I depend on are available,
131 in which I return True, otherwise I return False
132 """
133 if not self.locks:
134 return True
135 for lock, access in self.locks:
136 if not lock.isAvailable(access):
137 return False
138 return True
139
141 """
142 I am called when a build is preparing to run. I try to claim all
143 the locks that are needed for a build to happen. If I can't, then
144 my caller should give up the build and try to get another slave
145 to look at it.
146 """
147 log.msg("acquireLocks(slave %s, locks %s)" % (self, self.locks))
148 if not self.locksAvailable():
149 log.msg("slave %s can't lock, giving up" % (self, ))
150 return False
151
152 for lock, access in self.locks:
153 lock.claim(self, access)
154 return True
155
157 """
158 I am called to release any locks after a build has finished
159 """
160 log.msg("releaseLocks(%s): %s" % (self, self.locks))
161 for lock, access in self.locks:
162 lock.release(self, access)
163
165 assert not self.botmaster, "BuildSlave already has a botmaster"
166 self.botmaster = botmaster
167 self.updateLocks()
168 self.startMissingTimer()
169
171 if self.missing_timer:
172 self.missing_timer.cancel()
173 self.missing_timer = None
174
176 if self.notify_on_missing and self.missing_timeout and self.parent:
177 self.stopMissingTimer()
178 self.missing_timer = reactor.callLater(self.missing_timeout,
179 self._missing_timer_fired)
180
184
187
189 self.missing_timer = None
190
191 if not self.parent:
192 return
193
194 buildmaster = self.botmaster.parent
195 status = buildmaster.getStatus()
196 text = "The Buildbot working for '%s'\n" % status.getProjectName()
197 text += ("has noticed that the buildslave named %s went away\n" %
198 self.slavename)
199 text += "\n"
200 text += ("It last disconnected at %s (buildmaster-local time)\n" %
201 time.ctime(time.time() - self.missing_timeout))
202 text += "\n"
203 text += "The admin on record (as reported by BUILDSLAVE:info/admin)\n"
204 text += "was '%s'.\n" % self.slave_status.getAdmin()
205 text += "\n"
206 text += "Sincerely,\n"
207 text += " The Buildbot\n"
208 text += " %s\n" % status.getProjectURL()
209 subject = "Buildbot: buildslave %s was lost" % self.slavename
210 return self._mail_missing_message(subject, text)
211
212
214 """Called to add or remove builders after the slave has connected.
215
216 @return: a Deferred that indicates when an attached slave has
217 accepted the new builders and/or released the old ones."""
218 if self.slave:
219 return self.sendBuilderList()
220 else:
221 return defer.succeed(None)
222
228
230 """This is called when the slave connects.
231
232 @return: a Deferred that fires when the attachment is complete
233 """
234
235
236 assert not self.isConnected()
237
238
239
240
241
242
243
244 state = {}
245
246
247 self.slave_status.setGraceful(False)
248
249 self.slave_status.addGracefulWatcher(self._gracefulChanged)
250
251 d = defer.succeed(None)
252 def _log_attachment_on_slave(res):
253 d1 = bot.callRemote("print", "attached")
254 d1.addErrback(lambda why: None)
255 return d1
256 d.addCallback(_log_attachment_on_slave)
257
258 def _get_info(res):
259 d1 = bot.callRemote("getSlaveInfo")
260 def _got_info(info):
261 log.msg("Got slaveinfo from '%s'" % self.slavename)
262
263 state["admin"] = info.get("admin")
264 state["host"] = info.get("host")
265 state["access_uri"] = info.get("access_uri", None)
266 state["slave_environ"] = info.get("environ", {})
267 state["slave_basedir"] = info.get("basedir", None)
268 state["slave_system"] = info.get("system", None)
269 def _info_unavailable(why):
270
271 log.msg("BuildSlave.info_unavailable")
272 log.err(why)
273 d1.addCallbacks(_got_info, _info_unavailable)
274 return d1
275 d.addCallback(_get_info)
276
277 def _get_version(res):
278 d1 = bot.callRemote("getVersion")
279 def _got_version(version):
280 state["version"] = version
281 def _version_unavailable(why):
282
283 log.msg("BuildSlave.version_unavailable")
284 log.err(why)
285 d1.addCallbacks(_got_version, _version_unavailable)
286 d.addCallback(_get_version)
287
288 def _get_commands(res):
289 d1 = bot.callRemote("getCommands")
290 def _got_commands(commands):
291 state["slave_commands"] = commands
292 def _commands_unavailable(why):
293
294 log.msg("BuildSlave._commands_unavailable")
295 if why.check(AttributeError):
296 return
297 log.err(why)
298 d1.addCallbacks(_got_commands, _commands_unavailable)
299 return d1
300 d.addCallback(_get_commands)
301
302 def _accept_slave(res):
303 self.slave_status.setAdmin(state.get("admin"))
304 self.slave_status.setHost(state.get("host"))
305 self.slave_status.setAccessURI(state.get("access_uri"))
306 self.slave_status.setVersion(state.get("version"))
307 self.slave_status.setConnected(True)
308 self.slave_commands = state.get("slave_commands")
309 self.slave_environ = state.get("slave_environ")
310 self.slave_basedir = state.get("slave_basedir")
311 self.slave_system = state.get("slave_system")
312 self.slave = bot
313 log.msg("bot attached")
314 self.messageReceivedFromSlave()
315 self.stopMissingTimer()
316 self.botmaster.parent.status.slaveConnected(self.slavename)
317
318 return self.updateSlave()
319 d.addCallback(_accept_slave)
320 d.addCallback(lambda res: self.botmaster.triggerNewBuildCheck())
321
322
323
324 d.addCallback(lambda res: self)
325 return d
326
331
338
340 """Forcibly disconnect the slave.
341
342 This severs the TCP connection and returns a Deferred that will fire
343 (with None) when the connection is probably gone.
344
345 If the slave is still alive, they will probably try to reconnect
346 again in a moment.
347
348 This is called in two circumstances. The first is when a slave is
349 removed from the config file. In this case, when they try to
350 reconnect, they will be rejected as an unknown slave. The second is
351 when we wind up with two connections for the same slave, in which
352 case we disconnect the older connection.
353 """
354
355 if not self.slave:
356 return defer.succeed(None)
357 log.msg("disconnecting old slave %s now" % self.slavename)
358
359 return self._disconnect(self.slave)
360
362
363
364
365
366
367
368 d = defer.Deferred()
369
370
371
372 def _disconnected(rref):
373 reactor.callLater(0, d.callback, None)
374 slave.notifyOnDisconnect(_disconnected)
375 tport = slave.broker.transport
376
377 tport.loseConnection()
378 try:
379
380
381
382
383
384
385
386 tport.offset = 0
387 tport.dataBuffer = ""
388 except:
389
390
391 log.msg("failed to accelerate the shutdown process")
392 log.msg("waiting for slave to finish disconnecting")
393
394 return d
395
397 our_builders = self.botmaster.getBuildersForSlave(self.slavename)
398 blist = [(b.name, b.slavebuilddir) for b in our_builders]
399 d = self.slave.callRemote("setBuilderList", blist)
400 return d
401
404
408
410 self.slavebuilders[sb.builder_name] = sb
411
413 try:
414 del self.slavebuilders[sb.builder_name]
415 except KeyError:
416 pass
417
419 """
420 I am called when a build is requested to see if this buildslave
421 can start a build. This function can be used to limit overall
422 concurrency on the buildslave.
423 """
424
425
426 if self.slave_status.getGraceful():
427 return False
428
429 if self.max_builds:
430 active_builders = [sb for sb in self.slavebuilders.values()
431 if sb.isBusy()]
432 if len(active_builders) >= self.max_builds:
433 return False
434
435 if not self.locksAvailable():
436 return False
437
438 return True
439
441
442
443 buildmaster = self.botmaster.parent
444 for st in buildmaster.statusTargets:
445 if isinstance(st, MailNotifier):
446 break
447 else:
448
449
450 log.msg("buildslave-missing msg using default MailNotifier")
451 st = MailNotifier("buildbot")
452
453
454 m = Message()
455 m.set_payload(text)
456 m['Date'] = formatdate(localtime=True)
457 m['Subject'] = subject
458 m['From'] = st.fromaddr
459 recipients = self.notify_on_missing
460 m['To'] = ", ".join(recipients)
461 d = st.sendMessage(m, recipients)
462
463 return d
464
466 """This is called when our graceful shutdown setting changes"""
467 self.maybeShutdown()
468
469 @defer.deferredGenerator
471 """Shutdown the slave"""
472 if not self.slave:
473 log.msg("no remote; slave is already shut down")
474 return
475
476
477
478
479 def new_way():
480 d = self.slave.callRemote('shutdown')
481 d.addCallback(lambda _ : True)
482 def check_nsm(f):
483 f.trap(pb.NoSuchMethod)
484 return False
485 d.addErrback(check_nsm)
486 def check_connlost(f):
487 f.trap(pb.PBConnectionLost)
488 return True
489 d.addErrback(check_connlost)
490 return d
491
492 wfd = defer.waitForDeferred(new_way())
493 yield wfd
494 if wfd.getResult():
495 return
496
497
498
499
500 def old_way():
501 d = None
502 for b in self.slavebuilders.values():
503 if b.remote:
504 d = b.remote.callRemote("shutdown")
505 break
506
507 if d:
508 log.msg("Shutting down (old) slave: %s" % self.slavename)
509
510
511
512
513
514
515 def _errback(why):
516 if why.check(pb.PBConnectionLost):
517 log.msg("Lost connection to %s" % self.slavename)
518 else:
519 log.err("Unexpected error when trying to shutdown %s" % self.slavename)
520 d.addErrback(_errback)
521 return d
522 log.err("Couldn't find remote builder to shut down slave")
523 return defer.succeed(None)
524
525
526
527
529 """Shut down this slave if it has been asked to shut down gracefully,
530 and has no active builders."""
531 if not self.slave_status.getGraceful():
532 return
533 active_builders = [sb for sb in self.slavebuilders.values()
534 if sb.isBusy()]
535 if active_builders:
536 return
537 d = self.shutdown()
538 d.addErrback(log.err, 'error while shutting down slave')
539
541
553 def _set_failed(why):
554 log.msg("BuildSlave.sendBuilderList (%s) failed" % self)
555 log.err(why)
556
557
558 d.addCallbacks(_sent, _set_failed)
559 return d
560
565
567 """This is called when a build on this slave is finished."""
568
569
570 self.maybeShutdown()
571 return defer.succeed(None)
572
574 """A build slave that will start up a slave instance when needed.
575
576 To use, subclass and implement start_instance and stop_instance.
577
578 See ec2buildslave.py for a concrete example. Also see the stub example in
579 test/test_slaves.py.
580 """
581
582 implements(ILatentBuildSlave)
583
584 substantiated = False
585 substantiation_deferred = None
586 build_wait_timer = None
587 _shutdown_callback_handle = None
588
589 - def __init__(self, name, password, max_builds=None,
590 notify_on_missing=[], missing_timeout=60*20,
591 build_wait_timeout=60*10,
592 properties={}, locks=None):
598
600
601
602
603 raise NotImplementedError
604
606
607 raise NotImplementedError
608
626
639 def clean_up(failure):
640 if self.missing_timer is not None:
641 self.missing_timer.cancel()
642 self._substantiation_failed(failure)
643 if self._shutdown_callback_handle is not None:
644 handle = self._shutdown_callback_handle
645 del self._shutdown_callback_handle
646 reactor.removeSystemEventTrigger(handle)
647 return failure
648 d.addCallbacks(start_instance_result, clean_up)
649 return d
650
659
664
691
693 assert self.substantiated
694 self._clearBuildWaitTimer()
695 self.building.add(sb.builder_name)
696
698 self.building.remove(sb.builder_name)
699 if not self.building:
700 self._setBuildWaitTimer()
701
707
709 self._clearBuildWaitTimer()
710 self.build_wait_timer = reactor.callLater(
711 self.build_wait_timeout, self._soft_disconnect)
712
723
751
753
754 self._soft_disconnect()
755
756
757 self.botmaster.slaveLost(self)
758
760 res = defer.maybeDeferred(AbstractBuildSlave.stopService, self)
761 if self.slave is not None:
762 d = self._soft_disconnect()
763 res = defer.DeferredList([res, d])
764 return res
765
767 """Called to add or remove builders after the slave has connected.
768
769 Also called after botmaster's builders are initially set.
770
771 @return: a Deferred that indicates when an attached slave has
772 accepted the new builders and/or released the old ones."""
773 for b in self.botmaster.getBuildersForSlave(self.slavename):
774 if b.name not in self.slavebuilders:
775 b.addLatentSlave(self)
776 return AbstractBuildSlave.updateSlave(self)
777
792 def _set_failed(why):
793 log.msg("BuildSlave.sendBuilderList (%s) failed" % self)
794 log.err(why)
795
796
797 if self.substantiation_deferred:
798 self.substantiation_deferred.errback()
799 self.substantiation_deferred = None
800 if self.missing_timer:
801 self.missing_timer.cancel()
802 self.missing_timer = None
803
804 return why
805 d.addCallbacks(_sent, _set_failed)
806 def _substantiated(res):
807 log.msg("Slave %s substantiated \o/" % self.slavename)
808 self.substantiated = True
809 if not self.substantiation_deferred:
810 log.msg("No substantiation deferred for %s" % self.slavename)
811 if self.substantiation_deferred:
812 log.msg("Firing %s substantiation deferred with success" % self.slavename)
813 d = self.substantiation_deferred
814 del self.substantiation_deferred
815 d.callback(True)
816
817
818 if not self.building:
819 self._setBuildWaitTimer()
820 d.addCallback(_substantiated)
821 return d
822