3
# code to deliver build status through twisted.words (instant messaging
6
import traceback, StringIO, re, shlex
8
from twisted.internet import protocol, reactor
11
from twisted.words.protocols import irc
14
from twisted.protocols import irc
15
from twisted.python import log, failure
16
from twisted.application import internet
18
from buildbot import interfaces, util
19
from buildbot import version
20
from buildbot.sourcestamp import SourceStamp
21
from buildbot.process.base import BuildRequest
22
from buildbot.status import base
23
from buildbot.status.builder import SUCCESS, WARNINGS, FAILURE, EXCEPTION
24
from buildbot.scripts.runner import ForceOptions
26
class UsageError(ValueError):
27
def __init__(self, string = "Invalid usage", *more):
28
ValueError.__init__(self, string, *more)
30
class IrcBuildRequest:
34
def __init__(self, parent, reply):
37
self.timer = reactor.callLater(5, self.soon)
41
if not self.hasStarted:
42
self.parent.reply(self.reply,
43
"The build has been queued, I'll give a shout"
47
self.hasStarted = True
53
response = "build #%d forced" % s.getNumber()
55
response = "build forced [ETA %s]" % self.parent.convertTime(eta)
56
self.parent.reply(self.reply, response)
57
self.parent.reply(self.reply,
58
"I'll give a shout when the build finishes")
59
d = s.waitUntilFinished()
60
d.addCallback(self.parent.buildFinished, self.reply)
63
class IrcStatusBot(irc.IRCClient):
65
"What happen ?": "Somebody set up us the bomb.",
66
"It's You !!": ["How are you gentlemen !!",
67
"All your base are belong to us.",
68
"You are on the way to destruction."],
69
"What you say !!": ["You have no chance to survive make your time.",
72
def __init__(self, nickname, channels, status, categories):
74
@type nickname: string
75
@param nickname: the nickname by which this bot should be known
76
@type channels: list of strings
77
@param channels: the bot will maintain a presence in these channels
78
@type status: L{buildbot.status.builder.Status}
79
@param status: the build master's Status object, through which the
80
bot retrieves all status information
82
self.nickname = nickname
83
self.channels = channels
85
self.categories = categories
90
for c in self.channels:
92
def joined(self, channel):
93
log.msg("I have joined", channel)
94
def left(self, channel):
95
log.msg("I have left", channel)
96
def kickedFrom(self, channel, kicker, message):
97
log.msg("I have been kicked from %s by %s: %s" % (channel,
102
def privmsg(self, user, channel, message):
103
user = user.split('!', 1)[0] # rest is ~user@hostname
104
# channel is '#twisted' or 'buildbot' (for private messages)
105
channel = channel.lower()
106
#print "privmsg:", user, channel, message
107
if channel == self.nickname:
109
message = "%s: %s" % (self.nickname, message)
113
if message.startswith("%s:" % self.nickname):
114
message = message[len("%s:" % self.nickname):]
116
message = message.lstrip()
117
if self.silly.has_key(message):
118
return self.doSilly(user, reply, message)
120
parts = message.split(' ', 1)
124
log.msg("irc command", cmd)
126
meth = self.getCommandMethod(cmd)
127
if not meth and message[-1] == '!':
128
meth = self.command_EXCITED
133
meth(user, reply, args.strip())
134
except UsageError, e:
135
self.reply(reply, str(e))
137
f = failure.Failure()
139
error = "Something bad happened (see logs): %s" % f.type
143
self.reply(reply, error)
147
#self.say(channel, "count %d" % self.counter)
149
def reply(self, dest, message):
150
# maybe self.notice(dest, message) instead?
151
self.msg(dest, message)
153
def getCommandMethod(self, command):
154
meth = getattr(self, 'command_' + command.upper(), None)
157
def getBuilder(self, which):
159
b = self.status.getBuilder(which)
161
raise UsageError, "no such builder '%s'" % which
164
def getControl(self, which):
166
raise UsageError("builder control is not enabled")
168
bc = self.control.getBuilder(which)
170
raise UsageError("no such builder '%s'" % which)
173
def getAllBuilders(self):
175
@rtype: list of L{buildbot.process.builder.Builder}
177
names = self.status.getBuilderNames(categories=self.categories)
179
builders = [self.status.getBuilder(n) for n in names]
182
def convertTime(self, seconds):
184
return "%d seconds" % seconds
185
minutes = int(seconds / 60)
186
seconds = seconds - 60*minutes
188
return "%dm%02ds" % (minutes, seconds)
189
hours = int(minutes / 60)
190
minutes = minutes - 60*hours
191
return "%dh%02dm%02ds" % (hours, minutes, seconds)
193
def doSilly(self, user, reply, message):
194
response = self.silly[message]
195
if type(response) != type([]):
196
response = [response]
199
reactor.callLater(when, self.reply, reply, r)
202
def command_HELLO(self, user, reply, args):
203
self.reply(reply, "yes?")
205
def command_VERSION(self, user, reply, args):
206
self.reply(reply, "buildbot-%s at your service" % version)
208
def command_LIST(self, user, reply, args):
211
raise UsageError, "try 'list builders'"
212
if args[0] == 'builders':
213
builders = self.getAllBuilders()
214
str = "Configured builders: "
217
state = b.getState()[0]
218
if state == 'offline':
222
self.reply(reply, str)
224
command_LIST.usage = "list builders - List configured builders"
226
def command_STATUS(self, user, reply, args):
233
raise UsageError, "try 'status <builder>'"
235
builders = self.getAllBuilders()
237
self.emit_status(reply, b.name)
239
self.emit_status(reply, which)
240
command_STATUS.usage = "status [<which>] - List status of a builder (or all builders)"
242
def command_WATCH(self, user, reply, args):
245
raise UsageError("try 'watch <builder>'")
247
b = self.getBuilder(which)
248
builds = b.getCurrentBuilds()
250
self.reply(reply, "there are no builds currently running")
253
assert not build.isFinished()
254
d = build.waitUntilFinished()
255
d.addCallback(self.buildFinished, reply)
256
r = "watching build %s #%d until it finishes" \
257
% (which, build.getNumber())
260
r += " [%s]" % self.convertTime(eta)
263
command_WATCH.usage = "watch <which> - announce the completion of an active build"
265
def buildFinished(self, b, reply):
266
results = {SUCCESS: "Success",
267
WARNINGS: "Warnings",
269
EXCEPTION: "Exception",
272
# only notify about builders we are interested in
273
builder = b.getBuilder()
274
log.msg('builder %r in category %s finished' % (builder,
276
if (self.categories != None and
277
builder.category not in self.categories):
280
r = "Hey! build %s #%d is complete: %s" % \
281
(b.getBuilder().getName(),
283
results.get(b.getResults(), "??"))
284
r += " [%s]" % " ".join(b.getText())
286
buildurl = self.status.getURLForThing(b)
288
self.reply(reply, "Build details are at %s" % buildurl)
290
def command_FORCE(self, user, reply, args):
291
args = shlex.split(args) # TODO: this requires python2.3 or newer
292
if args.pop(0) != "build":
293
raise UsageError("try 'force build WHICH <REASON>'")
294
opts = ForceOptions()
295
opts.parseOptions(args)
297
which = opts['builder']
298
branch = opts['branch']
299
revision = opts['revision']
300
reason = opts['reason']
302
# keep weird stuff out of the branch and revision strings. TODO:
303
# centralize this somewhere.
304
if branch and not re.match(r'^[\w\.\-\/]*$', branch):
305
log.msg("bad branch '%s'" % branch)
306
self.reply(reply, "sorry, bad branch '%s'" % branch)
308
if revision and not re.match(r'^[\w\.\-\/]*$', revision):
309
log.msg("bad revision '%s'" % revision)
310
self.reply(reply, "sorry, bad revision '%s'" % revision)
313
bc = self.getControl(which)
315
who = None # TODO: if we can authenticate that a particular User
316
# asked for this, use User Name instead of None so they'll
317
# be informed of the results.
318
# TODO: or, monitor this build and announce the results through the
320
r = "forced: by IRC user <%s>: %s" % (user, reason)
321
# TODO: maybe give certain users the ability to request builds of
323
s = SourceStamp(branch=branch, revision=revision)
324
req = BuildRequest(r, s, which)
326
bc.requestBuildSoon(req)
327
except interfaces.NoSlaveError:
329
"sorry, I can't force a build: all slaves are offline")
331
ireq = IrcBuildRequest(self, reply)
332
req.subscribe(ireq.started)
335
command_FORCE.usage = "force build <which> <reason> - Force a build"
337
def command_STOP(self, user, reply, args):
338
args = args.split(None, 2)
339
if len(args) < 3 or args[0] != 'build':
340
raise UsageError, "try 'stop build WHICH <REASON>'"
344
buildercontrol = self.getControl(which)
347
r = "stopped: by IRC user <%s>: %s" % (user, reason)
349
# find an in-progress build
350
builderstatus = self.getBuilder(which)
351
builds = builderstatus.getCurrentBuilds()
353
self.reply(reply, "sorry, no build is currently running")
356
num = build.getNumber()
358
# obtain the BuildControl object
359
buildcontrol = buildercontrol.getBuild(num)
362
buildcontrol.stopBuild(r)
364
self.reply(reply, "build %d interrupted" % num)
366
command_STOP.usage = "stop build <which> <reason> - Stop a running build"
368
def emit_status(self, reply, which):
369
b = self.getBuilder(which)
371
state, builds = b.getState()
374
last = b.getLastFinishedBuild()
376
start,finished = last.getTimes()
377
str += ", last build %s secs ago: %s" % \
378
(int(util.now() - finished), " ".join(last.getText()))
379
if state == "building":
382
step = build.getCurrentStep()
383
s = "(%s)" % " ".join(step.getText())
386
s += " [ETA %s]" % self.convertTime(ETA)
389
self.reply(reply, str)
391
def emit_last(self, reply, which):
392
last = self.getBuilder(which).getLastFinishedBuild()
394
str = "(no builds run since last restart)"
396
start,finish = last.getTimes()
397
str = "%s secs ago: " % (int(util.now() - finish))
398
str += " ".join(last.getText())
399
self.reply(reply, "last build [%s]: %s" % (which, str))
401
def command_LAST(self, user, reply, args):
408
raise UsageError, "try 'last <builder>'"
410
builders = self.getAllBuilders()
412
self.emit_last(reply, b.name)
414
self.emit_last(reply, which)
415
command_LAST.usage = "last <which> - list last build status for builder <which>"
417
def build_commands(self):
419
for k in self.__class__.__dict__.keys():
420
if k.startswith('command_'):
421
commands.append(k[8:].lower())
425
def command_HELP(self, user, reply, args):
428
self.reply(reply, "Get help on what? (try 'help <foo>', or 'commands' for a command list)")
431
meth = self.getCommandMethod(command)
433
raise UsageError, "no such command '%s'" % command
434
usage = getattr(meth, 'usage', None)
436
self.reply(reply, "Usage: %s" % usage)
438
self.reply(reply, "No usage info for '%s'" % command)
439
command_HELP.usage = "help <command> - Give help for <command>"
441
def command_SOURCE(self, user, reply, args):
442
banner = "My source can be found at http://buildbot.sourceforge.net/"
443
self.reply(reply, banner)
445
def command_COMMANDS(self, user, reply, args):
446
commands = self.build_commands()
447
str = "buildbot commands: " + ", ".join(commands)
448
self.reply(reply, str)
449
command_COMMANDS.usage = "commands - List available commands"
451
def command_DESTROY(self, user, reply, args):
452
self.me(reply, "readies phasers")
454
def command_DANCE(self, user, reply, args):
455
reactor.callLater(1.0, self.reply, reply, "0-<")
456
reactor.callLater(3.0, self.reply, reply, "0-/")
457
reactor.callLater(3.5, self.reply, reply, "0-\\")
459
def command_EXCITED(self, user, reply, args):
460
# like 'buildbot: destroy the sun!'
461
self.reply(reply, "What you say!")
463
def action(self, user, channel, data):
464
#log.msg("action: %s,%s,%s" % (user, channel, data))
465
user = user.split('!', 1)[0] # rest is ~user@hostname
466
# somebody did an action (/me actions)
467
if data.endswith("s buildbot"):
472
response = "%s back" % verb
475
response = "%s %s too" % (verb, user)
476
reactor.callLater(timeout, self.me, channel, response)
477
# userJoined(self, user, channel)
480
# self.say(channel, message) # broadcast
481
# self.msg(user, message) # unicast
482
# self.me(channel, action) # send action
483
# self.away(message='')
484
# self.quit(message='')
486
class ThrottledClientFactory(protocol.ClientFactory):
489
def clientConnectionLost(self, connector, reason):
490
reactor.callLater(self.lostDelay, connector.connect)
491
def clientConnectionFailed(self, connector, reason):
492
reactor.callLater(self.failedDelay, connector.connect)
494
class IrcStatusFactory(ThrottledClientFactory):
495
protocol = IrcStatusBot
502
def __init__(self, nickname, channels, categories):
503
#ThrottledClientFactory.__init__(self) # doesn't exist
505
self.nickname = nickname
506
self.channels = channels
507
self.categories = categories
509
def __getstate__(self):
510
d = self.__dict__.copy()
515
self.shuttingDown = True
517
self.p.quit("buildmaster reconfigured: bot disconnecting")
519
def buildProtocol(self, address):
520
p = self.protocol(self.nickname, self.channels, self.status,
523
p.status = self.status
524
p.control = self.control
528
# TODO: I think a shutdown that occurs while the connection is being
529
# established will make this explode
531
def clientConnectionLost(self, connector, reason):
532
if self.shuttingDown:
533
log.msg("not scheduling reconnection attempt")
535
ThrottledClientFactory.clientConnectionLost(self, connector, reason)
537
def clientConnectionFailed(self, connector, reason):
538
if self.shuttingDown:
539
log.msg("not scheduling reconnection attempt")
541
ThrottledClientFactory.clientConnectionFailed(self, connector, reason)
544
class IRC(base.StatusReceiverMultiService):
545
"""I am an IRC bot which can be queried for status information. I
546
connect to a single IRC server and am known by a single nickname on that
547
server, however I can join multiple channels."""
549
compare_attrs = ["host", "port", "nick", "channels", "allowForce",
552
def __init__(self, host, nick, channels, port=6667, allowForce=True,
554
base.StatusReceiverMultiService.__init__(self)
556
assert allowForce in (True, False) # TODO: implement others
558
# need to stash these so we can detect changes later
562
self.channels = channels
563
self.allowForce = allowForce
564
self.categories = categories
566
# need to stash the factory so we can give it the status object
567
self.f = IrcStatusFactory(self.nick, self.channels, self.categories)
569
c = internet.TCPClient(host, port, self.f)
570
c.setServiceParent(self)
572
def setServiceParent(self, parent):
573
base.StatusReceiverMultiService.setServiceParent(self, parent)
574
self.f.status = parent.getStatus()
576
self.f.control = interfaces.IControl(parent)
578
def stopService(self):
579
# make sure the factory will stop reconnecting
581
return base.StatusReceiverMultiService.stopService(self)
585
from twisted.internet import app
586
a = app.Application("irctest")
587
f = IrcStatusFactory()
590
f.addNetwork((host, port), ["private", "other"])
591
a.connectTCP(host, port, f)
595
if __name__ == '__main__':
598
## buildbot: list builders
599
# buildbot: watch quick
600
# print notification when current build in 'quick' finishes
602
## buildbot: status full-2.3
603
## building, not, % complete, ETA
604
## buildbot: force build full-2.3 "reason"