| OLD | NEW |
| (Empty) |
| 1 # Portions copyright Canonical Ltd. 2009 | |
| 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 | |
| 22 class AbstractBuildSlave(NewCredPerspective, service.MultiService): | |
| 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 # no buildmaster yet | |
| 53 self.slave_status = SlaveStatus(name) | |
| 54 self.slave = None # a RemoteReference to the Bot, when connected | |
| 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 | |
| 72 def update(self, new): | |
| 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 # the reconfiguration logic should guarantee this: | |
| 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 | |
| 84 def __repr__(self): | |
| 85 if self.botmaster: | |
| 86 builders = self.botmaster.getBuildersForSlave(self.slavename) | |
| 87 return "<%s '%s', current builders: %s>" % \ | |
| 88 (self.__class__.__name__, self.slavename, | |
| 89 ','.join(map(lambda b: b.name, builders))) | |
| 90 else: | |
| 91 return "<%s '%s', (no builders yet)>" % \ | |
| 92 (self.__class__.__name__, self.slavename) | |
| 93 | |
| 94 def setBotmaster(self, botmaster): | |
| 95 assert not self.botmaster, "BuildSlave already has a botmaster" | |
| 96 self.botmaster = botmaster | |
| 97 self.startMissingTimer() | |
| 98 | |
| 99 def stopMissingTimer(self): | |
| 100 if self.missing_timer: | |
| 101 self.missing_timer.cancel() | |
| 102 self.missing_timer = None | |
| 103 | |
| 104 def startMissingTimer(self): | |
| 105 if self.notify_on_missing and self.missing_timeout and self.parent: | |
| 106 self.stopMissingTimer() # in case it's already running | |
| 107 self.missing_timer = reactor.callLater(self.missing_timeout, | |
| 108 self._missing_timer_fired) | |
| 109 | |
| 110 def _missing_timer_fired(self): | |
| 111 self.missing_timer = None | |
| 112 # notify people, but only if we're still in the config | |
| 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)) # approx | |
| 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 | |
| 135 def updateSlave(self): | |
| 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 | |
| 145 def updateSlaveStatus(self, buildStarted=None, buildFinished=None): | |
| 146 if buildStarted: | |
| 147 self.slave_status.buildStarted(buildStarted) | |
| 148 if buildFinished: | |
| 149 self.slave_status.buildFinished(buildFinished) | |
| 150 | |
| 151 def attached(self, bot): | |
| 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 # uh-oh, we've got a duplicate slave. The most likely | |
| 159 # explanation is that the slave is behind a slow link, thinks we | |
| 160 # went away, and has attempted to reconnect, so we've got two | |
| 161 # "connections" from the same slave, but the previous one is | |
| 162 # stale. Give the new one precedence. | |
| 163 log.msg("duplicate slave %s replacing old one" % self.slavename) | |
| 164 | |
| 165 # just in case we've got two identically-configured slaves, | |
| 166 # report the IP addresses of both so someone can resolve the | |
| 167 # squabble | |
| 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 # now we go through a sequence of calls, gathering information, then | |
| 175 # tell the Botmaster that it can finally give this slave to all the | |
| 176 # Builders that care about it. | |
| 177 | |
| 178 # we accumulate slave information in this 'state' dictionary, then | |
| 179 # set it atomically if we make it far enough through the process | |
| 180 state = {} | |
| 181 | |
| 182 # Reset graceful shutdown status | |
| 183 self.slave_status.setGraceful(False) | |
| 184 # We want to know when the graceful shutdown flag changes | |
| 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 # TODO: info{} might have other keys | |
| 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 # maybe an old slave, doesn't implement remote_getSlaveInfo | |
| 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 # probably an old slave | |
| 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 # probably an old slave | |
| 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.maybeStartAllBuilds()) | |
| 250 | |
| 251 # Finally, the slave gets a reference to this BuildSlave. They | |
| 252 # receive this later, after we've started using them. | |
| 253 d.addCallback(lambda res: self) | |
| 254 return d | |
| 255 | |
| 256 def messageReceivedFromSlave(self): | |
| 257 now = time.time() | |
| 258 self.lastMessageReceived = now | |
| 259 self.slave_status.setLastMessageReceived(now) | |
| 260 | |
| 261 def detached(self, mind): | |
| 262 self.slave = None | |
| 263 self.slave_status.removeGracefulWatcher(self._gracefulChanged) | |
| 264 self.slave_status.setConnected(False) | |
| 265 log.msg("BuildSlave.detached(%s)" % self.slavename) | |
| 266 self.botmaster.parent.status.slaveDisconnected(self.slavename) | |
| 267 | |
| 268 def disconnect(self): | |
| 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 # When this Deferred fires, we'll be ready to accept the new slave | |
| 288 return self._disconnect(self.slave) | |
| 289 | |
| 290 def _disconnect(self, slave): | |
| 291 # all kinds of teardown will happen as a result of | |
| 292 # loseConnection(), but it happens after a reactor iteration or | |
| 293 # two. Hook the actual disconnect so we can know when it is safe | |
| 294 # to connect the new slave. We have to wait one additional | |
| 295 # iteration (with callLater(0)) to make sure the *other* | |
| 296 # notifyOnDisconnect handlers have had a chance to run. | |
| 297 d = defer.Deferred() | |
| 298 | |
| 299 # notifyOnDisconnect runs the callback with one argument, the | |
| 300 # RemoteReference being disconnected. | |
| 301 def _disconnected(rref): | |
| 302 reactor.callLater(0, d.callback, None) | |
| 303 slave.notifyOnDisconnect(_disconnected) | |
| 304 tport = slave.broker.transport | |
| 305 # this is the polite way to request that a socket be closed | |
| 306 tport.loseConnection() | |
| 307 try: | |
| 308 # but really we don't want to wait for the transmit queue to | |
| 309 # drain. The remote end is unlikely to ACK the data, so we'd | |
| 310 # probably have to wait for a (20-minute) TCP timeout. | |
| 311 #tport._closeSocket() | |
| 312 # however, doing _closeSocket (whether before or after | |
| 313 # loseConnection) somehow prevents the notifyOnDisconnect | |
| 314 # handlers from being run. Bummer. | |
| 315 tport.offset = 0 | |
| 316 tport.dataBuffer = "" | |
| 317 except: | |
| 318 # however, these hacks are pretty internal, so don't blow up if | |
| 319 # they fail or are unavailable | |
| 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 | |
| 326 def sendBuilderList(self): | |
| 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 | |
| 332 def perspective_keepalive(self): | |
| 333 pass | |
| 334 | |
| 335 def addSlaveBuilder(self, sb): | |
| 336 self.slavebuilders[sb.builder_name] = sb | |
| 337 | |
| 338 def removeSlaveBuilder(self, sb): | |
| 339 try: | |
| 340 del self.slavebuilders[sb.builder_name] | |
| 341 except KeyError: | |
| 342 pass | |
| 343 | |
| 344 def canStartBuild(self): | |
| 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 # If we're waiting to shutdown gracefully, then we shouldn't | |
| 351 # accept any new jobs. | |
| 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 | |
| 362 def _mail_missing_message(self, subject, text): | |
| 363 # first, see if we have a MailNotifier we can use. This gives us a | |
| 364 # fromaddr and a relayhost. | |
| 365 buildmaster = self.botmaster.parent | |
| 366 for st in buildmaster.statusTargets: | |
| 367 if isinstance(st, MailNotifier): | |
| 368 break | |
| 369 else: | |
| 370 # if not, they get a default MailNotifier, which always uses SMTP | |
| 371 # to localhost and uses a dummy fromaddr of "buildbot". | |
| 372 log.msg("buildslave-missing msg using default MailNotifier") | |
| 373 st = MailNotifier("buildbot") | |
| 374 # now construct the mail | |
| 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 # return the Deferred for testing purposes | |
| 385 return d | |
| 386 | |
| 387 def _gracefulChanged(self, graceful): | |
| 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 # Shut down! | |
| 394 self.shutdown() | |
| 395 | |
| 396 def shutdown(self): | |
| 397 """Shutdown the slave""" | |
| 398 # Look for a builder with a remote reference to the client side | |
| 399 # slave. If we can find one, then call "shutdown" on the remote | |
| 400 # builder, which will cause the slave buildbot process to exit. | |
| 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 # The remote shutdown call will not complete successfully since the | |
| 410 # buildbot process exits almost immediately after getting the | |
| 411 # shutdown request. | |
| 412 # Here we look at the reason why the remote call failed, and if | |
| 413 # it's because the connection was lost, that means the slave | |
| 414 # shutdown as expected. | |
| 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 | |
| 425 class BuildSlave(AbstractBuildSlave): | |
| 426 | |
| 427 def sendBuilderList(self): | |
| 428 d = AbstractBuildSlave.sendBuilderList(self) | |
| 429 def _sent(slist): | |
| 430 dl = [] | |
| 431 for name, remote in slist.items(): | |
| 432 # use get() since we might have changed our mind since then | |
| 433 b = self.botmaster.builders.get(name) | |
| 434 if b: | |
| 435 d1 = b.attached(self, remote, self.slave_commands) | |
| 436 dl.append(d1) | |
| 437 return defer.DeferredList(dl) | |
| 438 def _set_failed(why): | |
| 439 log.msg("BuildSlave.sendBuilderList (%s) failed" % self) | |
| 440 log.err(why) | |
| 441 # TODO: hang up on them?, without setBuilderList we can't use | |
| 442 # them | |
| 443 d.addCallbacks(_sent, _set_failed) | |
| 444 return d | |
| 445 | |
| 446 def detached(self, mind): | |
| 447 AbstractBuildSlave.detached(self, mind) | |
| 448 self.botmaster.slaveLost(self) | |
| 449 self.startMissingTimer() | |
| 450 | |
| 451 def buildFinished(self, sb): | |
| 452 """This is called when a build on this slave is finished.""" | |
| 453 # If we're gracefully shutting down, and we have no more active | |
| 454 # builders, then it's safe to disconnect | |
| 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 # Shut down! | |
| 460 return self.shutdown() | |
| 461 return defer.succeed(None) | |
| 462 | |
| 463 class AbstractLatentBuildSlave(AbstractBuildSlave): | |
| 464 """A build slave that will start up a slave instance when needed. | |
| 465 | |
| 466 To use, subclass and implement start_instance and stop_instance. | |
| 467 | |
| 468 See ec2buildslave.py for a concrete example. Also see the stub example in | |
| 469 test/test_slaves.py. | |
| 470 """ | |
| 471 | |
| 472 implements(ILatentBuildSlave) | |
| 473 | |
| 474 substantiated = False | |
| 475 substantiation_deferred = None | |
| 476 build_wait_timer = None | |
| 477 _start_result = _shutdown_callback_handle = None | |
| 478 | |
| 479 def __init__(self, name, password, max_builds=None, | |
| 480 notify_on_missing=[], missing_timeout=60*20, | |
| 481 build_wait_timeout=60*10, | |
| 482 properties={}): | |
| 483 AbstractBuildSlave.__init__( | |
| 484 self, name, password, max_builds, notify_on_missing, | |
| 485 missing_timeout, properties) | |
| 486 self.building = set() | |
| 487 self.build_wait_timeout = build_wait_timeout | |
| 488 | |
| 489 def start_instance(self): | |
| 490 # responsible for starting instance that will try to connect with | |
| 491 # this master. Should return deferred. Problems should use an | |
| 492 # errback. | |
| 493 raise NotImplementedError | |
| 494 | |
| 495 def stop_instance(self, fast=False): | |
| 496 # responsible for shutting down instance. | |
| 497 raise NotImplementedError | |
| 498 | |
| 499 def substantiate(self, sb): | |
| 500 if self.substantiated: | |
| 501 self._clearBuildWaitTimer() | |
| 502 self._setBuildWaitTimer() | |
| 503 return defer.succeed(self) | |
| 504 if self.substantiation_deferred is None: | |
| 505 if self.parent and not self.missing_timer: | |
| 506 # start timer. if timer times out, fail deferred | |
| 507 self.missing_timer = reactor.callLater( | |
| 508 self.missing_timeout, | |
| 509 self._substantiation_failed, defer.TimeoutError()) | |
| 510 self.substantiation_deferred = defer.Deferred() | |
| 511 if self.slave is None: | |
| 512 self._substantiate() # start up instance | |
| 513 # else: we're waiting for an old one to detach. the _substantiate | |
| 514 # will be done in ``detached`` below. | |
| 515 return self.substantiation_deferred | |
| 516 | |
| 517 def _substantiate(self): | |
| 518 # register event trigger | |
| 519 d = self.start_instance() | |
| 520 self._shutdown_callback_handle = reactor.addSystemEventTrigger( | |
| 521 'before', 'shutdown', self._soft_disconnect, fast=True) | |
| 522 def stash_reply(result): | |
| 523 self._start_result = result | |
| 524 def clean_up(failure): | |
| 525 if self.missing_timer is not None: | |
| 526 self.missing_timer.cancel() | |
| 527 self._substantiation_failed(failure) | |
| 528 if self._shutdown_callback_handle is not None: | |
| 529 handle = self._shutdown_callback_handle | |
| 530 del self._shutdown_callback_handle | |
| 531 reactor.removeSystemEventTrigger(handle) | |
| 532 return failure | |
| 533 d.addCallbacks(stash_reply, clean_up) | |
| 534 return d | |
| 535 | |
| 536 def attached(self, bot): | |
| 537 if self.substantiation_deferred is None: | |
| 538 msg = 'Slave %s received connection while not trying to ' \ | |
| 539 'substantiate. Disconnecting.' % (self.slavename,) | |
| 540 log.msg(msg) | |
| 541 self._disconnect(bot) | |
| 542 return defer.fail(RuntimeError(msg)) | |
| 543 return AbstractBuildSlave.attached(self, bot) | |
| 544 | |
| 545 def detached(self, mind): | |
| 546 AbstractBuildSlave.detached(self, mind) | |
| 547 if self.substantiation_deferred is not None: | |
| 548 self._substantiate() | |
| 549 | |
| 550 def _substantiation_failed(self, failure): | |
| 551 d = self.substantiation_deferred | |
| 552 self.substantiation_deferred = None | |
| 553 self.missing_timer = None | |
| 554 d.errback(failure) | |
| 555 self.insubstantiate() | |
| 556 # notify people, but only if we're still in the config | |
| 557 if not self.parent or not self.notify_on_missing: | |
| 558 return | |
| 559 | |
| 560 buildmaster = self.botmaster.parent | |
| 561 status = buildmaster.getStatus() | |
| 562 text = "The Buildbot working for '%s'\n" % status.getProjectName() | |
| 563 text += ("has noticed that the latent buildslave named %s \n" % | |
| 564 self.slavename) | |
| 565 text += "never substantiated after a request\n" | |
| 566 text += "\n" | |
| 567 text += ("The request was made at %s (buildmaster-local time)\n" % | |
| 568 time.ctime(time.time() - self.missing_timeout)) # approx | |
| 569 text += "\n" | |
| 570 text += "Sincerely,\n" | |
| 571 text += " The Buildbot\n" | |
| 572 text += " %s\n" % status.getProjectURL() | |
| 573 subject = "Buildbot: buildslave %s never substantiated" % self.slavename | |
| 574 return self._mail_missing_message(subject, text) | |
| 575 | |
| 576 def buildStarted(self, sb): | |
| 577 assert self.substantiated | |
| 578 self._clearBuildWaitTimer() | |
| 579 self.building.add(sb.builder_name) | |
| 580 | |
| 581 def buildFinished(self, sb): | |
| 582 self.building.remove(sb.builder_name) | |
| 583 if not self.building: | |
| 584 self._setBuildWaitTimer() | |
| 585 | |
| 586 def _clearBuildWaitTimer(self): | |
| 587 if self.build_wait_timer is not None: | |
| 588 if self.build_wait_timer.active(): | |
| 589 self.build_wait_timer.cancel() | |
| 590 self.build_wait_timer = None | |
| 591 | |
| 592 def _setBuildWaitTimer(self): | |
| 593 self._clearBuildWaitTimer() | |
| 594 self.build_wait_timer = reactor.callLater( | |
| 595 self.build_wait_timeout, self._soft_disconnect) | |
| 596 | |
| 597 def insubstantiate(self, fast=False): | |
| 598 self._clearBuildWaitTimer() | |
| 599 d = self.stop_instance(fast) | |
| 600 if self._shutdown_callback_handle is not None: | |
| 601 handle = self._shutdown_callback_handle | |
| 602 del self._shutdown_callback_handle | |
| 603 reactor.removeSystemEventTrigger(handle) | |
| 604 self.substantiated = False | |
| 605 self.building.clear() # just to be sure | |
| 606 return d | |
| 607 | |
| 608 def _soft_disconnect(self, fast=False): | |
| 609 d = AbstractBuildSlave.disconnect(self) | |
| 610 if self.slave is not None: | |
| 611 # this could be called when the slave needs to shut down, such as | |
| 612 # in BotMaster.removeSlave, *or* when a new slave requests a | |
| 613 # connection when we already have a slave. It's not clear what to | |
| 614 # do in the second case: this shouldn't happen, and if it | |
| 615 # does...if it's a latent slave, shutting down will probably kill | |
| 616 # something we want...but we can't know what the status is. So, | |
| 617 # here, we just do what should be appropriate for the first case, | |
| 618 # and put our heads in the sand for the second, at least for now. | |
| 619 # The best solution to the odd situation is removing it as a | |
| 620 # possibilty: make the master in charge of connecting to the | |
| 621 # slave, rather than vice versa. TODO. | |
| 622 d = defer.DeferredList([d, self.insubstantiate(fast)]) | |
| 623 else: | |
| 624 if self.substantiation_deferred is not None: | |
| 625 # unlike the previous block, we don't expect this situation when | |
| 626 # ``attached`` calls ``disconnect``, only when we get a simple | |
| 627 # request to "go away". | |
| 628 self.substantiation_deferred.errback() | |
| 629 self.substantiation_deferred = None | |
| 630 if self.missing_timer: | |
| 631 self.missing_timer.cancel() | |
| 632 self.missing_timer = None | |
| 633 self.stop_instance() | |
| 634 return d | |
| 635 | |
| 636 def disconnect(self): | |
| 637 d = self._soft_disconnect() | |
| 638 # this removes the slave from all builders. It won't come back | |
| 639 # without a restart (or maybe a sighup) | |
| 640 self.botmaster.slaveLost(self) | |
| 641 | |
| 642 def stopService(self): | |
| 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 | |
| 649 def updateSlave(self): | |
| 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 | |
| 661 def sendBuilderList(self): | |
| 662 d = AbstractBuildSlave.sendBuilderList(self) | |
| 663 def _sent(slist): | |
| 664 dl = [] | |
| 665 for name, remote in slist.items(): | |
| 666 # use get() since we might have changed our mind since then. | |
| 667 # we're checking on the builder in addition to the | |
| 668 # slavebuilders out of a bit of paranoia. | |
| 669 b = self.botmaster.builders.get(name) | |
| 670 sb = self.slavebuilders.get(name) | |
| 671 if b and sb: | |
| 672 d1 = sb.attached(self, remote, self.slave_commands) | |
| 673 dl.append(d1) | |
| 674 return defer.DeferredList(dl) | |
| 675 def _set_failed(why): | |
| 676 log.msg("BuildSlave.sendBuilderList (%s) failed" % self) | |
| 677 log.err(why) | |
| 678 # TODO: hang up on them?, without setBuilderList we can't use | |
| 679 # them | |
| 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 # TODO: maybe log? send an email? | |
| 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 # note that the missing_timer is already handled within | |
| 698 # ``attached`` | |
| 699 if not self.building: | |
| 700 self._setBuildWaitTimer() | |
| 701 d.addCallback(_substantiated) | |
| 702 return d | |
| OLD | NEW |