2 from twisted
.python
import log
3 from twisted
.internet
import defer
, reactor
5 from buildbot
.pbutil
import NewCredPerspective
6 from buildbot
.status
.builder
import SlaveStatus
8 class BuildSlave(NewCredPerspective
):
9 """This is the master-side representative for a remote buildbot slave.
10 There is exactly one for each slave described in the config file (the
11 c['slaves'] list). When buildbots connect in (.attach), they get a
12 reference to this instance. The BotMaster object is stashed as the
15 I represent a build slave -- a remote machine capable of
16 running builds. I am instantiated by the configuration file, and can be
17 subclassed to add extra functionality."""
19 def __init__(self
, name
, password
, max_builds
=None):
21 @param name: botname this machine will supply when it connects
22 @param password: password this machine will supply when
24 @param max_builds: maximum number of simultaneous builds that will
25 be run concurrently on this buildslave (the
26 default is None for no limit)
30 self
.password
= password
31 self
.botmaster
= None # no buildmaster yet
32 self
.slave_status
= SlaveStatus(name
)
33 self
.slave
= None # a RemoteReference to the Bot, when connected
34 self
.slave_commands
= None
35 self
.slavebuilders
= []
36 self
.max_builds
= max_builds
38 def update(self
, new
):
40 Given a new BuildSlave, configure this one identically. Because
41 BuildSlave objects are remotely referenced, we can't replace them
42 without disconnecting the slave, yet there's no reason to do that.
44 # the reconfiguration logic should guarantee this:
45 assert self
.slavename
== new
.slavename
46 assert self
.password
== new
.password
47 assert self
.__class
__ == new
.__class
__
48 self
.max_builds
= new
.max_builds
51 builders
= self
.botmaster
.getBuildersForSlave(self
.slavename
)
52 return "<BuildSlave '%s', current builders: %s>" % \
53 (self
.slavename
, ','.join(map(lambda b
: b
.name
, builders
)))
55 def setBotmaster(self
, botmaster
):
56 assert not self
.botmaster
, "BuildSlave already has a botmaster"
57 self
.botmaster
= botmaster
59 def updateSlave(self
):
60 """Called to add or remove builders after the slave has connected.
62 @return: a Deferred that indicates when an attached slave has
63 accepted the new builders and/or released the old ones."""
65 return self
.sendBuilderList()
66 return defer
.succeed(None)
68 def attached(self
, bot
):
69 """This is called when the slave connects.
71 @return: a Deferred that fires with a suitable pb.IPerspective to
72 give to the slave (i.e. 'self')"""
75 # uh-oh, we've got a duplicate slave. The most likely
76 # explanation is that the slave is behind a slow link, thinks we
77 # went away, and has attempted to reconnect, so we've got two
78 # "connections" from the same slave, but the previous one is
79 # stale. Give the new one precedence.
80 log
.msg("duplicate slave %s replacing old one" % self
.slavename
)
82 # just in case we've got two identically-configured slaves,
83 # report the IP addresses of both so someone can resolve the
85 tport
= self
.slave
.broker
.transport
86 log
.msg("old slave was connected from", tport
.getPeer())
87 log
.msg("new slave is from", bot
.broker
.transport
.getPeer())
90 d
= defer
.succeed(None)
91 # now we go through a sequence of calls, gathering information, then
92 # tell the Botmaster that it can finally give this slave to all the
93 # Builders that care about it.
95 # we accumulate slave information in this 'state' dictionary, then
96 # set it atomically if we make it far enough through the process
99 def _log_attachment_on_slave(res
):
100 d1
= bot
.callRemote("print", "attached")
101 d1
.addErrback(lambda why
: None)
103 d
.addCallback(_log_attachment_on_slave
)
106 d1
= bot
.callRemote("getSlaveInfo")
108 log
.msg("Got slaveinfo from '%s'" % self
.slavename
)
109 # TODO: info{} might have other keys
110 state
["admin"] = info
.get("admin")
111 state
["host"] = info
.get("host")
112 def _info_unavailable(why
):
113 # maybe an old slave, doesn't implement remote_getSlaveInfo
114 log
.msg("BuildSlave.info_unavailable")
116 d1
.addCallbacks(_got_info
, _info_unavailable
)
118 d
.addCallback(_get_info
)
120 def _get_commands(res
):
121 d1
= bot
.callRemote("getCommands")
122 def _got_commands(commands
):
123 state
["slave_commands"] = commands
124 def _commands_unavailable(why
):
125 # probably an old slave
126 log
.msg("BuildSlave._commands_unavailable")
127 if why
.check(AttributeError):
130 d1
.addCallbacks(_got_commands
, _commands_unavailable
)
132 d
.addCallback(_get_commands
)
134 def _accept_slave(res
):
135 self
.slave_status
.setAdmin(state
.get("admin"))
136 self
.slave_status
.setHost(state
.get("host"))
137 self
.slave_status
.setConnected(True)
138 self
.slave_commands
= state
.get("slave_commands")
140 log
.msg("bot attached")
141 return self
.updateSlave()
142 d
.addCallback(_accept_slave
)
144 # Finally, the slave gets a reference to this BuildSlave. They
145 # receive this later, after we've started using them.
146 d
.addCallback(lambda res
: self
)
149 def detached(self
, mind
):
151 self
.slave_status
.setConnected(False)
152 self
.botmaster
.slaveLost(self
)
153 log
.msg("BuildSlave.detached(%s)" % self
.slavename
)
156 def disconnect(self
):
157 """Forcibly disconnect the slave.
159 This severs the TCP connection and returns a Deferred that will fire
160 (with None) when the connection is probably gone.
162 If the slave is still alive, they will probably try to reconnect
165 This is called in two circumstances. The first is when a slave is
166 removed from the config file. In this case, when they try to
167 reconnect, they will be rejected as an unknown slave. The second is
168 when we wind up with two connections for the same slave, in which
169 case we disconnect the older connection.
173 return defer
.succeed(None)
174 log
.msg("disconnecting old slave %s now" % self
.slavename
)
176 # all kinds of teardown will happen as a result of
177 # loseConnection(), but it happens after a reactor iteration or
178 # two. Hook the actual disconnect so we can know when it is safe
179 # to connect the new slave. We have to wait one additional
180 # iteration (with callLater(0)) to make sure the *other*
181 # notifyOnDisconnect handlers have had a chance to run.
184 # notifyOnDisconnect runs the callback with one argument, the
185 # RemoteReference being disconnected.
186 def _disconnected(rref
):
187 reactor
.callLater(0, d
.callback
, None)
188 self
.slave
.notifyOnDisconnect(_disconnected
)
189 tport
= self
.slave
.broker
.transport
190 # this is the polite way to request that a socket be closed
191 tport
.loseConnection()
193 # but really we don't want to wait for the transmit queue to
194 # drain. The remote end is unlikely to ACK the data, so we'd
195 # probably have to wait for a (20-minute) TCP timeout.
196 #tport._closeSocket()
197 # however, doing _closeSocket (whether before or after
198 # loseConnection) somehow prevents the notifyOnDisconnect
199 # handlers from being run. Bummer.
201 tport
.dataBuffer
= ""
204 # however, these hacks are pretty internal, so don't blow up if
205 # they fail or are unavailable
206 log
.msg("failed to accelerate the shutdown process")
208 log
.msg("waiting for slave to finish disconnecting")
210 # When this Deferred fires, we'll be ready to accept the new slave
213 def sendBuilderList(self
):
214 our_builders
= self
.botmaster
.getBuildersForSlave(self
.slavename
)
215 blist
= [(b
.name
, b
.builddir
) for b
in our_builders
]
216 d
= self
.slave
.callRemote("setBuilderList", blist
)
219 for name
, remote
in slist
.items():
220 # use get() since we might have changed our mind since then
221 b
= self
.botmaster
.builders
.get(name
)
223 d1
= b
.attached(self
, remote
, self
.slave_commands
)
225 return defer
.DeferredList(dl
)
226 def _set_failed(why
):
227 log
.msg("BuildSlave.sendBuilderList (%s) failed" % self
)
229 # TODO: hang up on them?, without setBuilderList we can't use
231 d
.addCallbacks(_sent
, _set_failed
)
234 def perspective_keepalive(self
):
237 def addSlaveBuilder(self
, sb
):
238 log
.msg("%s adding %s" % (self
, sb
))
239 self
.slavebuilders
.append(sb
)
241 def removeSlaveBuilder(self
, sb
):
242 log
.msg("%s removing %s" % (self
, sb
))
243 if sb
in self
.slavebuilders
:
244 self
.slavebuilders
.remove(sb
)
246 def canStartBuild(self
):
248 I am called when a build is requested to see if this buildslave
249 can start a build. This function can be used to limit overall
250 concurrency on the buildslave.
253 active_builders
= [sb
for sb
in self
.slavebuilders
if sb
.isBusy()]
254 if len(active_builders
) >= self
.max_builds
: