~ubuntu-branches/ubuntu/utopic/buildbot/utopic-proposed

« back to all changes in this revision

Viewing changes to buildbot/status/words.py

  • Committer: Bazaar Package Importer
  • Author(s): Matthias Klose
  • Date: 2006-04-15 21:20:08 UTC
  • Revision ID: james.westby@ubuntu.com-20060415212008-jfj53u29zl30jqi1
Tags: upstream-0.7.2
ImportĀ upstreamĀ versionĀ 0.7.2

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
#! /usr/bin/python
 
2
 
 
3
# code to deliver build status through twisted.words (instant messaging
 
4
# protocols: irc, etc)
 
5
 
 
6
import traceback, StringIO, re, shlex
 
7
 
 
8
from twisted.internet import protocol, reactor
 
9
try:
 
10
    # Twisted-2.0
 
11
    from twisted.words.protocols import irc
 
12
except ImportError:
 
13
    # Twisted-1.3
 
14
    from twisted.protocols import irc
 
15
from twisted.python import log, failure
 
16
from twisted.application import internet
 
17
 
 
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
 
25
 
 
26
class UsageError(ValueError):
 
27
    def __init__(self, string = "Invalid usage", *more):
 
28
        ValueError.__init__(self, string, *more)
 
29
 
 
30
class IrcBuildRequest:
 
31
    hasStarted = False
 
32
    timer = None
 
33
 
 
34
    def __init__(self, parent, reply):
 
35
        self.parent = parent
 
36
        self.reply = reply
 
37
        self.timer = reactor.callLater(5, self.soon)
 
38
 
 
39
    def soon(self):
 
40
        del self.timer
 
41
        if not self.hasStarted:
 
42
            self.parent.reply(self.reply,
 
43
                              "The build has been queued, I'll give a shout"
 
44
                              " when it starts")
 
45
 
 
46
    def started(self, c):
 
47
        self.hasStarted = True
 
48
        if self.timer:
 
49
            self.timer.cancel()
 
50
            del self.timer
 
51
        s = c.getStatus()
 
52
        eta = s.getETA()
 
53
        response = "build #%d forced" % s.getNumber()
 
54
        if eta is not None:
 
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)
 
61
 
 
62
 
 
63
class IrcStatusBot(irc.IRCClient):
 
64
    silly = {
 
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.",
 
70
                            "HA HA HA HA ...."],
 
71
        }
 
72
    def __init__(self, nickname, channels, status, categories):
 
73
        """
 
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
 
81
        """
 
82
        self.nickname = nickname
 
83
        self.channels = channels
 
84
        self.status = status
 
85
        self.categories = categories
 
86
        self.counter = 0
 
87
        self.hasQuit = 0
 
88
 
 
89
    def signedOn(self):
 
90
        for c in self.channels:
 
91
            self.join(c)
 
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,
 
98
                                                          kicker,
 
99
                                                          message))
 
100
 
 
101
    # input
 
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:
 
108
            # private message
 
109
            message = "%s: %s" % (self.nickname, message)
 
110
            reply = user
 
111
        else:
 
112
            reply = channel
 
113
        if message.startswith("%s:" % self.nickname):
 
114
            message = message[len("%s:" % self.nickname):]
 
115
 
 
116
            message = message.lstrip()
 
117
            if self.silly.has_key(message):
 
118
                return self.doSilly(user, reply, message)
 
119
 
 
120
            parts = message.split(' ', 1)
 
121
            if len(parts) == 1:
 
122
                parts = parts + ['']
 
123
            cmd, args = parts
 
124
            log.msg("irc command", cmd)
 
125
 
 
126
            meth = self.getCommandMethod(cmd)
 
127
            if not meth and message[-1] == '!':
 
128
                meth = self.command_EXCITED
 
129
 
 
130
            error = None
 
131
            try:
 
132
                if meth:
 
133
                    meth(user, reply, args.strip())
 
134
            except UsageError, e:
 
135
                self.reply(reply, str(e))
 
136
            except:
 
137
                f = failure.Failure()
 
138
                log.err(f)
 
139
                error = "Something bad happened (see logs): %s" % f.type
 
140
 
 
141
            if error:
 
142
                try:
 
143
                    self.reply(reply, error)
 
144
                except:
 
145
                    log.err()
 
146
 
 
147
            #self.say(channel, "count %d" % self.counter)
 
148
            self.counter += 1
 
149
    def reply(self, dest, message):
 
150
        # maybe self.notice(dest, message) instead?
 
151
        self.msg(dest, message)
 
152
 
 
153
    def getCommandMethod(self, command):
 
154
        meth = getattr(self, 'command_' + command.upper(), None)
 
155
        return meth
 
156
 
 
157
    def getBuilder(self, which):
 
158
        try:
 
159
            b = self.status.getBuilder(which)
 
160
        except KeyError:
 
161
            raise UsageError, "no such builder '%s'" % which
 
162
        return b
 
163
 
 
164
    def getControl(self, which):
 
165
        if not self.control:
 
166
            raise UsageError("builder control is not enabled")
 
167
        try:
 
168
            bc = self.control.getBuilder(which)
 
169
        except KeyError:
 
170
            raise UsageError("no such builder '%s'" % which)
 
171
        return bc
 
172
 
 
173
    def getAllBuilders(self):
 
174
        """
 
175
        @rtype: list of L{buildbot.process.builder.Builder}
 
176
        """
 
177
        names = self.status.getBuilderNames(categories=self.categories)
 
178
        names.sort()
 
179
        builders = [self.status.getBuilder(n) for n in names]
 
180
        return builders
 
181
 
 
182
    def convertTime(self, seconds):
 
183
        if seconds < 60:
 
184
            return "%d seconds" % seconds
 
185
        minutes = int(seconds / 60)
 
186
        seconds = seconds - 60*minutes
 
187
        if minutes < 60:
 
188
            return "%dm%02ds" % (minutes, seconds)
 
189
        hours = int(minutes / 60)
 
190
        minutes = minutes - 60*hours
 
191
        return "%dh%02dm%02ds" % (hours, minutes, seconds)
 
192
 
 
193
    def doSilly(self, user, reply, message):
 
194
        response = self.silly[message]
 
195
        if type(response) != type([]):
 
196
            response = [response]
 
197
        when = 0.5
 
198
        for r in response:
 
199
            reactor.callLater(when, self.reply, reply, r)
 
200
            when += 2.5
 
201
 
 
202
    def command_HELLO(self, user, reply, args):
 
203
        self.reply(reply, "yes?")
 
204
 
 
205
    def command_VERSION(self, user, reply, args):
 
206
        self.reply(reply, "buildbot-%s at your service" % version)
 
207
 
 
208
    def command_LIST(self, user, reply, args):
 
209
        args = args.split()
 
210
        if len(args) == 0:
 
211
            raise UsageError, "try 'list builders'"
 
212
        if args[0] == 'builders':
 
213
            builders = self.getAllBuilders()
 
214
            str = "Configured builders: "
 
215
            for b in builders:
 
216
                str += b.name
 
217
                state = b.getState()[0]
 
218
                if state == 'offline':
 
219
                    str += "[offline]"
 
220
                str += " "
 
221
            str.rstrip()
 
222
            self.reply(reply, str)
 
223
            return
 
224
    command_LIST.usage = "list builders - List configured builders"
 
225
 
 
226
    def command_STATUS(self, user, reply, args):
 
227
        args = args.split()
 
228
        if len(args) == 0:
 
229
            which = "all"
 
230
        elif len(args) == 1:
 
231
            which = args[0]
 
232
        else:
 
233
            raise UsageError, "try 'status <builder>'"
 
234
        if which == "all":
 
235
            builders = self.getAllBuilders()
 
236
            for b in builders:
 
237
                self.emit_status(reply, b.name)
 
238
            return
 
239
        self.emit_status(reply, which)
 
240
    command_STATUS.usage = "status [<which>] - List status of a builder (or all builders)"
 
241
 
 
242
    def command_WATCH(self, user, reply, args):
 
243
        args = args.split()
 
244
        if len(args) != 1:
 
245
            raise UsageError("try 'watch <builder>'")
 
246
        which = args[0]
 
247
        b = self.getBuilder(which)
 
248
        builds = b.getCurrentBuilds()
 
249
        if not builds:
 
250
            self.reply(reply, "there are no builds currently running")
 
251
            return
 
252
        for build in builds:
 
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())
 
258
            eta = build.getETA()
 
259
            if eta is not None:
 
260
                r += " [%s]" % self.convertTime(eta)
 
261
            r += ".."
 
262
            self.reply(reply, r)
 
263
    command_WATCH.usage = "watch <which> - announce the completion of an active build"
 
264
 
 
265
    def buildFinished(self, b, reply):
 
266
        results = {SUCCESS: "Success",
 
267
                   WARNINGS: "Warnings",
 
268
                   FAILURE: "Failure",
 
269
                   EXCEPTION: "Exception",
 
270
                   }
 
271
 
 
272
        # only notify about builders we are interested in
 
273
        builder = b.getBuilder()
 
274
        log.msg('builder %r in category %s finished' % (builder,
 
275
                                                        builder.category))
 
276
        if (self.categories != None and
 
277
            builder.category not in self.categories):
 
278
            return
 
279
 
 
280
        r = "Hey! build %s #%d is complete: %s" % \
 
281
            (b.getBuilder().getName(),
 
282
             b.getNumber(),
 
283
             results.get(b.getResults(), "??"))
 
284
        r += " [%s]" % " ".join(b.getText())
 
285
        self.reply(reply, r)
 
286
        buildurl = self.status.getURLForThing(b)
 
287
        if buildurl:
 
288
            self.reply(reply, "Build details are at %s" % buildurl)
 
289
 
 
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)
 
296
        
 
297
        which = opts['builder']
 
298
        branch = opts['branch']
 
299
        revision = opts['revision']
 
300
        reason = opts['reason']
 
301
 
 
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)
 
307
            return
 
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)
 
311
            return
 
312
 
 
313
        bc = self.getControl(which)
 
314
 
 
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
 
319
        # 'reply' argument.
 
320
        r = "forced: by IRC user <%s>: %s" % (user, reason)
 
321
        # TODO: maybe give certain users the ability to request builds of
 
322
        # certain branches
 
323
        s = SourceStamp(branch=branch, revision=revision)
 
324
        req = BuildRequest(r, s, which)
 
325
        try:
 
326
            bc.requestBuildSoon(req)
 
327
        except interfaces.NoSlaveError:
 
328
            self.reply(reply,
 
329
                       "sorry, I can't force a build: all slaves are offline")
 
330
            return
 
331
        ireq = IrcBuildRequest(self, reply)
 
332
        req.subscribe(ireq.started)
 
333
 
 
334
 
 
335
    command_FORCE.usage = "force build <which> <reason> - Force a build"
 
336
 
 
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>'"
 
341
        which = args[1]
 
342
        reason = args[2]
 
343
 
 
344
        buildercontrol = self.getControl(which)
 
345
 
 
346
        who = None
 
347
        r = "stopped: by IRC user <%s>: %s" % (user, reason)
 
348
 
 
349
        # find an in-progress build
 
350
        builderstatus = self.getBuilder(which)
 
351
        builds = builderstatus.getCurrentBuilds()
 
352
        if not builds:
 
353
            self.reply(reply, "sorry, no build is currently running")
 
354
            return
 
355
        for build in builds:
 
356
            num = build.getNumber()
 
357
 
 
358
            # obtain the BuildControl object
 
359
            buildcontrol = buildercontrol.getBuild(num)
 
360
 
 
361
            # make it stop
 
362
            buildcontrol.stopBuild(r)
 
363
 
 
364
            self.reply(reply, "build %d interrupted" % num)
 
365
 
 
366
    command_STOP.usage = "stop build <which> <reason> - Stop a running build"
 
367
 
 
368
    def emit_status(self, reply, which):
 
369
        b = self.getBuilder(which)
 
370
        str = "%s: " % which
 
371
        state, builds = b.getState()
 
372
        str += state
 
373
        if state == "idle":
 
374
            last = b.getLastFinishedBuild()
 
375
            if last:
 
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":
 
380
            t = []
 
381
            for build in builds:
 
382
                step = build.getCurrentStep()
 
383
                s = "(%s)" % " ".join(step.getText())
 
384
                ETA = build.getETA()
 
385
                if ETA is not None:
 
386
                    s += " [ETA %s]" % self.convertTime(ETA)
 
387
                t.append(s)
 
388
            str += ", ".join(t)
 
389
        self.reply(reply, str)
 
390
 
 
391
    def emit_last(self, reply, which):
 
392
        last = self.getBuilder(which).getLastFinishedBuild()
 
393
        if not last:
 
394
            str = "(no builds run since last restart)"
 
395
        else:
 
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))
 
400
 
 
401
    def command_LAST(self, user, reply, args):
 
402
        args = args.split()
 
403
        if len(args) == 0:
 
404
            which = "all"
 
405
        elif len(args) == 1:
 
406
            which = args[0]
 
407
        else:
 
408
            raise UsageError, "try 'last <builder>'"
 
409
        if which == "all":
 
410
            builders = self.getAllBuilders()
 
411
            for b in builders:
 
412
                self.emit_last(reply, b.name)
 
413
            return
 
414
        self.emit_last(reply, which)
 
415
    command_LAST.usage = "last <which> - list last build status for builder <which>"
 
416
 
 
417
    def build_commands(self):
 
418
        commands = []
 
419
        for k in self.__class__.__dict__.keys():
 
420
            if k.startswith('command_'):
 
421
                commands.append(k[8:].lower())
 
422
        commands.sort()
 
423
        return commands
 
424
 
 
425
    def command_HELP(self, user, reply, args):
 
426
        args = args.split()
 
427
        if len(args) == 0:
 
428
            self.reply(reply, "Get help on what? (try 'help <foo>', or 'commands' for a command list)")
 
429
            return
 
430
        command = args[0]
 
431
        meth = self.getCommandMethod(command)
 
432
        if not meth:
 
433
            raise UsageError, "no such command '%s'" % command
 
434
        usage = getattr(meth, 'usage', None)
 
435
        if usage:
 
436
            self.reply(reply, "Usage: %s" % usage)
 
437
        else:
 
438
            self.reply(reply, "No usage info for '%s'" % command)
 
439
    command_HELP.usage = "help <command> - Give help for <command>"
 
440
 
 
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)
 
444
 
 
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"
 
450
 
 
451
    def command_DESTROY(self, user, reply, args):
 
452
        self.me(reply, "readies phasers")
 
453
 
 
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-\\")
 
458
 
 
459
    def command_EXCITED(self, user, reply, args):
 
460
        # like 'buildbot: destroy the sun!'
 
461
        self.reply(reply, "What you say!")
 
462
 
 
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"):
 
468
            words = data.split()
 
469
            verb = words[-2]
 
470
            timeout = 4
 
471
            if verb == "kicks":
 
472
                response = "%s back" % verb
 
473
                timeout = 1
 
474
            else:
 
475
                response = "%s %s too" % (verb, user)
 
476
            reactor.callLater(timeout, self.me, channel, response)
 
477
    # userJoined(self, user, channel)
 
478
    
 
479
    # output
 
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='')
 
485
    
 
486
class ThrottledClientFactory(protocol.ClientFactory):
 
487
    lostDelay = 2
 
488
    failedDelay = 60
 
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)
 
493
 
 
494
class IrcStatusFactory(ThrottledClientFactory):
 
495
    protocol = IrcStatusBot
 
496
 
 
497
    status = None
 
498
    control = None
 
499
    shuttingDown = False
 
500
    p = None
 
501
 
 
502
    def __init__(self, nickname, channels, categories):
 
503
        #ThrottledClientFactory.__init__(self) # doesn't exist
 
504
        self.status = None
 
505
        self.nickname = nickname
 
506
        self.channels = channels
 
507
        self.categories = categories
 
508
 
 
509
    def __getstate__(self):
 
510
        d = self.__dict__.copy()
 
511
        del d['p']
 
512
        return d
 
513
 
 
514
    def shutdown(self):
 
515
        self.shuttingDown = True
 
516
        if self.p:
 
517
            self.p.quit("buildmaster reconfigured: bot disconnecting")
 
518
 
 
519
    def buildProtocol(self, address):
 
520
        p = self.protocol(self.nickname, self.channels, self.status,
 
521
                          self.categories)
 
522
        p.factory = self
 
523
        p.status = self.status
 
524
        p.control = self.control
 
525
        self.p = p
 
526
        return p
 
527
 
 
528
    # TODO: I think a shutdown that occurs while the connection is being
 
529
    # established will make this explode
 
530
 
 
531
    def clientConnectionLost(self, connector, reason):
 
532
        if self.shuttingDown:
 
533
            log.msg("not scheduling reconnection attempt")
 
534
            return
 
535
        ThrottledClientFactory.clientConnectionLost(self, connector, reason)
 
536
 
 
537
    def clientConnectionFailed(self, connector, reason):
 
538
        if self.shuttingDown:
 
539
            log.msg("not scheduling reconnection attempt")
 
540
            return
 
541
        ThrottledClientFactory.clientConnectionFailed(self, connector, reason)
 
542
 
 
543
 
 
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."""
 
548
 
 
549
    compare_attrs = ["host", "port", "nick", "channels", "allowForce",
 
550
                     "categories"]
 
551
 
 
552
    def __init__(self, host, nick, channels, port=6667, allowForce=True,
 
553
                 categories=None):
 
554
        base.StatusReceiverMultiService.__init__(self)
 
555
 
 
556
        assert allowForce in (True, False) # TODO: implement others
 
557
 
 
558
        # need to stash these so we can detect changes later
 
559
        self.host = host
 
560
        self.port = port
 
561
        self.nick = nick
 
562
        self.channels = channels
 
563
        self.allowForce = allowForce
 
564
        self.categories = categories
 
565
 
 
566
        # need to stash the factory so we can give it the status object
 
567
        self.f = IrcStatusFactory(self.nick, self.channels, self.categories)
 
568
 
 
569
        c = internet.TCPClient(host, port, self.f)
 
570
        c.setServiceParent(self)
 
571
 
 
572
    def setServiceParent(self, parent):
 
573
        base.StatusReceiverMultiService.setServiceParent(self, parent)
 
574
        self.f.status = parent.getStatus()
 
575
        if self.allowForce:
 
576
            self.f.control = interfaces.IControl(parent)
 
577
 
 
578
    def stopService(self):
 
579
        # make sure the factory will stop reconnecting
 
580
        self.f.shutdown()
 
581
        return base.StatusReceiverMultiService.stopService(self)
 
582
 
 
583
 
 
584
def main():
 
585
    from twisted.internet import app
 
586
    a = app.Application("irctest")
 
587
    f = IrcStatusFactory()
 
588
    host = "localhost"
 
589
    port = 6667
 
590
    f.addNetwork((host, port), ["private", "other"])
 
591
    a.connectTCP(host, port, f)
 
592
    a.run(save=0)
 
593
    
 
594
 
 
595
if __name__ == '__main__':
 
596
    main()
 
597
 
 
598
## buildbot: list builders
 
599
# buildbot: watch quick
 
600
#  print notification when current build in 'quick' finishes
 
601
## buildbot: status
 
602
## buildbot: status full-2.3
 
603
##  building, not, % complete, ETA
 
604
## buildbot: force build full-2.3 "reason"