diff options
Diffstat (limited to 'buildbot/buildbot/status/words.py')
-rw-r--r-- | buildbot/buildbot/status/words.py | 875 |
1 files changed, 0 insertions, 875 deletions
diff --git a/buildbot/buildbot/status/words.py b/buildbot/buildbot/status/words.py deleted file mode 100644 index 0e98651..0000000 --- a/buildbot/buildbot/status/words.py +++ /dev/null @@ -1,875 +0,0 @@ - -# code to deliver build status through twisted.words (instant messaging -# protocols: irc, etc) - -import re, shlex - -from zope.interface import Interface, implements -from twisted.internet import protocol, reactor -from twisted.words.protocols import irc -from twisted.python import log, failure -from twisted.application import internet - -from buildbot import interfaces, util -from buildbot import version -from buildbot.sourcestamp import SourceStamp -from buildbot.process.base import BuildRequest -from buildbot.status import base -from buildbot.status.builder import SUCCESS, WARNINGS, FAILURE, EXCEPTION -from buildbot.scripts.runner import ForceOptions - -from string import join, capitalize, lower - -class UsageError(ValueError): - def __init__(self, string = "Invalid usage", *more): - ValueError.__init__(self, string, *more) - -class IrcBuildRequest: - hasStarted = False - timer = None - - def __init__(self, parent): - self.parent = parent - self.timer = reactor.callLater(5, self.soon) - - def soon(self): - del self.timer - if not self.hasStarted: - self.parent.send("The build has been queued, I'll give a shout" - " when it starts") - - def started(self, c): - self.hasStarted = True - if self.timer: - self.timer.cancel() - del self.timer - s = c.getStatus() - eta = s.getETA() - response = "build #%d forced" % s.getNumber() - if eta is not None: - response = "build forced [ETA %s]" % self.parent.convertTime(eta) - self.parent.send(response) - self.parent.send("I'll give a shout when the build finishes") - d = s.waitUntilFinished() - d.addCallback(self.parent.watchedBuildFinished) - - -class Contact: - """I hold the state for a single user's interaction with the buildbot. - - This base class provides all the basic behavior (the queries and - responses). Subclasses for each channel type (IRC, different IM - protocols) are expected to provide the lower-level send/receive methods. - - There will be one instance of me for each user who interacts personally - with the buildbot. There will be an additional instance for each - 'broadcast contact' (chat rooms, IRC channels as a whole). - """ - - def __init__(self, channel): - self.channel = channel - self.notify_events = {} - self.subscribed = 0 - self.add_notification_events(channel.notify_events) - - silly = { - "What happen ?": "Somebody set up us the bomb.", - "It's You !!": ["How are you gentlemen !!", - "All your base are belong to us.", - "You are on the way to destruction."], - "What you say !!": ["You have no chance to survive make your time.", - "HA HA HA HA ...."], - } - - def getCommandMethod(self, command): - meth = getattr(self, 'command_' + command.upper(), None) - return meth - - def getBuilder(self, which): - try: - b = self.channel.status.getBuilder(which) - except KeyError: - raise UsageError, "no such builder '%s'" % which - return b - - def getControl(self, which): - if not self.channel.control: - raise UsageError("builder control is not enabled") - try: - bc = self.channel.control.getBuilder(which) - except KeyError: - raise UsageError("no such builder '%s'" % which) - return bc - - def getAllBuilders(self): - """ - @rtype: list of L{buildbot.process.builder.Builder} - """ - names = self.channel.status.getBuilderNames(categories=self.channel.categories) - names.sort() - builders = [self.channel.status.getBuilder(n) for n in names] - return builders - - def convertTime(self, seconds): - if seconds < 60: - return "%d seconds" % seconds - minutes = int(seconds / 60) - seconds = seconds - 60*minutes - if minutes < 60: - return "%dm%02ds" % (minutes, seconds) - hours = int(minutes / 60) - minutes = minutes - 60*hours - return "%dh%02dm%02ds" % (hours, minutes, seconds) - - def doSilly(self, message): - response = self.silly[message] - if type(response) != type([]): - response = [response] - when = 0.5 - for r in response: - reactor.callLater(when, self.send, r) - when += 2.5 - - def command_HELLO(self, args, who): - self.send("yes?") - - def command_VERSION(self, args, who): - self.send("buildbot-%s at your service" % version) - - def command_LIST(self, args, who): - args = args.split() - if len(args) == 0: - raise UsageError, "try 'list builders'" - if args[0] == 'builders': - builders = self.getAllBuilders() - str = "Configured builders: " - for b in builders: - str += b.name - state = b.getState()[0] - if state == 'offline': - str += "[offline]" - str += " " - str.rstrip() - self.send(str) - return - command_LIST.usage = "list builders - List configured builders" - - def command_STATUS(self, args, who): - args = args.split() - if len(args) == 0: - which = "all" - elif len(args) == 1: - which = args[0] - else: - raise UsageError, "try 'status <builder>'" - if which == "all": - builders = self.getAllBuilders() - for b in builders: - self.emit_status(b.name) - return - self.emit_status(which) - command_STATUS.usage = "status [<which>] - List status of a builder (or all builders)" - - def validate_notification_event(self, event): - if not re.compile("^(started|finished|success|failure|exception|warnings|(success|warnings|exception|failure)To(Failure|Success|Warnings|Exception))$").match(event): - raise UsageError("try 'notify on|off <EVENT>'") - - def list_notified_events(self): - self.send( "The following events are being notified: %r" % self.notify_events.keys() ) - - def notify_for(self, *events): - for event in events: - if self.notify_events.has_key(event): - return 1 - return 0 - - def subscribe_to_build_events(self): - self.channel.status.subscribe(self) - self.subscribed = 1 - - def unsubscribe_from_build_events(self): - self.channel.status.unsubscribe(self) - self.subscribed = 0 - - def add_notification_events(self, events): - for event in events: - self.validate_notification_event(event) - self.notify_events[event] = 1 - - if not self.subscribed: - self.subscribe_to_build_events() - - def remove_notification_events(self, events): - for event in events: - self.validate_notification_event(event) - del self.notify_events[event] - - if len(self.notify_events) == 0 and self.subscribed: - self.unsubscribe_from_build_events() - - def remove_all_notification_events(self): - self.notify_events = {} - - if self.subscribed: - self.unsubscribe_from_build_events() - - def command_NOTIFY(self, args, who): - args = args.split() - - if not args: - raise UsageError("try 'notify on|off|list <EVENT>'") - action = args.pop(0) - events = args - - if action == "on": - if not events: events = ('started','finished') - self.add_notification_events(events) - - self.list_notified_events() - - elif action == "off": - if events: - self.remove_notification_events(events) - else: - self.remove_all_notification_events() - - self.list_notified_events() - - elif action == "list": - self.list_notified_events() - return - - else: - raise UsageError("try 'notify on|off <EVENT>'") - - command_NOTIFY.usage = "notify on|off|list [<EVENT>] ... - Notify me about build events. event should be one or more of: 'started', 'finished', 'failure', 'success', 'exception' or 'xToY' (where x and Y are one of success, warnings, failure, exception, but Y is capitalized)" - - def command_WATCH(self, args, who): - args = args.split() - if len(args) != 1: - raise UsageError("try 'watch <builder>'") - which = args[0] - b = self.getBuilder(which) - builds = b.getCurrentBuilds() - if not builds: - self.send("there are no builds currently running") - return - for build in builds: - assert not build.isFinished() - d = build.waitUntilFinished() - d.addCallback(self.watchedBuildFinished) - r = "watching build %s #%d until it finishes" \ - % (which, build.getNumber()) - eta = build.getETA() - if eta is not None: - r += " [%s]" % self.convertTime(eta) - r += ".." - self.send(r) - command_WATCH.usage = "watch <which> - announce the completion of an active build" - - def buildsetSubmitted(self, buildset): - log.msg('[Contact] Buildset %s added' % (buildset)) - - def builderAdded(self, builderName, builder): - log.msg('[Contact] Builder %s added' % (builder)) - builder.subscribe(self) - - def builderChangedState(self, builderName, state): - log.msg('[Contact] Builder %s changed state to %s' % (builderName, state)) - - def requestSubmitted(self, brstatus): - log.msg('[Contact] BuildRequest for %s submiitted to Builder %s' % - (brstatus.getSourceStamp(), brstatus.builderName)) - - def builderRemoved(self, builderName): - log.msg('[Contact] Builder %s removed' % (builderName)) - - def buildStarted(self, builderName, build): - builder = build.getBuilder() - log.msg('[Contact] Builder %r in category %s started' % (builder, builder.category)) - - # only notify about builders we are interested in - - if (self.channel.categories != None and - builder.category not in self.channel.categories): - log.msg('Not notifying for a build in the wrong category') - return - - if not self.notify_for('started'): - log.msg('Not notifying for a build when started-notification disabled') - return - - r = "build #%d of %s started" % \ - (build.getNumber(), - builder.getName()) - - r += " including [" + ", ".join(map(lambda c: repr(c.revision), build.getChanges())) + "]" - - self.send(r) - - def buildFinished(self, builderName, build, results): - builder = build.getBuilder() - - results_descriptions = { - SUCCESS: "Success", - WARNINGS: "Warnings", - FAILURE: "Failure", - EXCEPTION: "Exception", - } - - # only notify about builders we are interested in - log.msg('[Contact] builder %r in category %s finished' % (builder, builder.category)) - - if self.notify_for('started'): - return - - if (self.channel.categories != None and - builder.category not in self.channel.categories): - return - - results = build.getResults() - - r = "build #%d of %s is complete: %s" % \ - (build.getNumber(), - builder.getName(), - results_descriptions.get(results, "??")) - r += " [%s]" % " ".join(build.getText()) - buildurl = self.channel.status.getURLForThing(build) - if buildurl: - r += " Build details are at %s" % buildurl - - if self.notify_for('finished') or self.notify_for(lower(results_descriptions.get(results))): - self.send(r) - return - - prevBuild = build.getPreviousBuild() - if prevBuild: - prevResult = prevBuild.getResults() - - required_notification_control_string = join((lower(results_descriptions.get(prevResult)), \ - 'To', \ - capitalize(results_descriptions.get(results))), \ - '') - - if (self.notify_for(required_notification_control_string)): - self.send(r) - - def watchedBuildFinished(self, b): - results = {SUCCESS: "Success", - WARNINGS: "Warnings", - FAILURE: "Failure", - EXCEPTION: "Exception", - } - - # only notify about builders we are interested in - builder = b.getBuilder() - log.msg('builder %r in category %s finished' % (builder, - builder.category)) - if (self.channel.categories != None and - builder.category not in self.channel.categories): - return - - r = "Hey! build %s #%d is complete: %s" % \ - (b.getBuilder().getName(), - b.getNumber(), - results.get(b.getResults(), "??")) - r += " [%s]" % " ".join(b.getText()) - self.send(r) - buildurl = self.channel.status.getURLForThing(b) - if buildurl: - self.send("Build details are at %s" % buildurl) - - def command_FORCE(self, args, who): - args = shlex.split(args) # TODO: this requires python2.3 or newer - if not args: - raise UsageError("try 'force build WHICH <REASON>'") - what = args.pop(0) - if what != "build": - raise UsageError("try 'force build WHICH <REASON>'") - opts = ForceOptions() - opts.parseOptions(args) - - which = opts['builder'] - branch = opts['branch'] - revision = opts['revision'] - reason = opts['reason'] - - if which is None: - raise UsageError("you must provide a Builder, " - "try 'force build WHICH <REASON>'") - - # keep weird stuff out of the branch and revision strings. TODO: - # centralize this somewhere. - if branch and not re.match(r'^[\w\.\-\/]*$', branch): - log.msg("bad branch '%s'" % branch) - self.send("sorry, bad branch '%s'" % branch) - return - if revision and not re.match(r'^[\w\.\-\/]*$', revision): - log.msg("bad revision '%s'" % revision) - self.send("sorry, bad revision '%s'" % revision) - return - - bc = self.getControl(which) - - r = "forced: by %s: %s" % (self.describeUser(who), reason) - # TODO: maybe give certain users the ability to request builds of - # certain branches - s = SourceStamp(branch=branch, revision=revision) - req = BuildRequest(r, s, which) - try: - bc.requestBuildSoon(req) - except interfaces.NoSlaveError: - self.send("sorry, I can't force a build: all slaves are offline") - return - ireq = IrcBuildRequest(self) - req.subscribe(ireq.started) - - - command_FORCE.usage = "force build <which> <reason> - Force a build" - - def command_STOP(self, args, who): - args = args.split(None, 2) - if len(args) < 3 or args[0] != 'build': - raise UsageError, "try 'stop build WHICH <REASON>'" - which = args[1] - reason = args[2] - - buildercontrol = self.getControl(which) - - r = "stopped: by %s: %s" % (self.describeUser(who), reason) - - # find an in-progress build - builderstatus = self.getBuilder(which) - builds = builderstatus.getCurrentBuilds() - if not builds: - self.send("sorry, no build is currently running") - return - for build in builds: - num = build.getNumber() - - # obtain the BuildControl object - buildcontrol = buildercontrol.getBuild(num) - - # make it stop - buildcontrol.stopBuild(r) - - self.send("build %d interrupted" % num) - - command_STOP.usage = "stop build <which> <reason> - Stop a running build" - - def emit_status(self, which): - b = self.getBuilder(which) - str = "%s: " % which - state, builds = b.getState() - str += state - if state == "idle": - last = b.getLastFinishedBuild() - if last: - start,finished = last.getTimes() - str += ", last build %s ago: %s" % \ - (self.convertTime(int(util.now() - finished)), " ".join(last.getText())) - if state == "building": - t = [] - for build in builds: - step = build.getCurrentStep() - if step: - s = "(%s)" % " ".join(step.getText()) - else: - s = "(no current step)" - ETA = build.getETA() - if ETA is not None: - s += " [ETA %s]" % self.convertTime(ETA) - t.append(s) - str += ", ".join(t) - self.send(str) - - def emit_last(self, which): - last = self.getBuilder(which).getLastFinishedBuild() - if not last: - str = "(no builds run since last restart)" - else: - start,finish = last.getTimes() - str = "%s ago: " % (self.convertTime(int(util.now() - finish))) - str += " ".join(last.getText()) - self.send("last build [%s]: %s" % (which, str)) - - def command_LAST(self, args, who): - args = args.split() - if len(args) == 0: - which = "all" - elif len(args) == 1: - which = args[0] - else: - raise UsageError, "try 'last <builder>'" - if which == "all": - builders = self.getAllBuilders() - for b in builders: - self.emit_last(b.name) - return - self.emit_last(which) - command_LAST.usage = "last <which> - list last build status for builder <which>" - - def build_commands(self): - commands = [] - for k in dir(self): - if k.startswith('command_'): - commands.append(k[8:].lower()) - commands.sort() - return commands - - def command_HELP(self, args, who): - args = args.split() - if len(args) == 0: - self.send("Get help on what? (try 'help <foo>', or 'commands' for a command list)") - return - command = args[0] - meth = self.getCommandMethod(command) - if not meth: - raise UsageError, "no such command '%s'" % command - usage = getattr(meth, 'usage', None) - if usage: - self.send("Usage: %s" % usage) - else: - self.send("No usage info for '%s'" % command) - command_HELP.usage = "help <command> - Give help for <command>" - - def command_SOURCE(self, args, who): - banner = "My source can be found at http://buildbot.net/" - self.send(banner) - - def command_COMMANDS(self, args, who): - commands = self.build_commands() - str = "buildbot commands: " + ", ".join(commands) - self.send(str) - command_COMMANDS.usage = "commands - List available commands" - - def command_DESTROY(self, args, who): - self.act("readies phasers") - - def command_DANCE(self, args, who): - reactor.callLater(1.0, self.send, "0-<") - reactor.callLater(3.0, self.send, "0-/") - reactor.callLater(3.5, self.send, "0-\\") - - def command_EXCITED(self, args, who): - # like 'buildbot: destroy the sun!' - self.send("What you say!") - - def handleAction(self, data, user): - # this is sent when somebody performs an action that mentions the - # buildbot (like '/me kicks buildbot'). 'user' is the name/nick/id of - # the person who performed the action, so if their action provokes a - # response, they can be named. - if not data.endswith("s buildbot"): - return - words = data.split() - verb = words[-2] - timeout = 4 - if verb == "kicks": - response = "%s back" % verb - timeout = 1 - else: - response = "%s %s too" % (verb, user) - reactor.callLater(timeout, self.act, response) - -class IRCContact(Contact): - # this is the IRC-specific subclass of Contact - - def __init__(self, channel, dest): - Contact.__init__(self, channel) - # when people send us public messages ("buildbot: command"), - # self.dest is the name of the channel ("#twisted"). When they send - # us private messages (/msg buildbot command), self.dest is their - # username. - self.dest = dest - - def describeUser(self, user): - if self.dest[0] == "#": - return "IRC user <%s> on channel %s" % (user, self.dest) - return "IRC user <%s> (privmsg)" % user - - # userJoined(self, user, channel) - - def send(self, message): - self.channel.msg(self.dest, message.encode("ascii", "replace")) - def act(self, action): - self.channel.me(self.dest, action.encode("ascii", "replace")) - - def command_JOIN(self, args, who): - args = args.split() - to_join = args[0] - self.channel.join(to_join) - self.send("Joined %s" % to_join) - command_JOIN.usage = "join channel - Join another channel" - - def command_LEAVE(self, args, who): - args = args.split() - to_leave = args[0] - self.send("Buildbot has been told to leave %s" % to_leave) - self.channel.part(to_leave) - command_LEAVE.usage = "leave channel - Leave a channel" - - - def handleMessage(self, message, who): - # a message has arrived from 'who'. For broadcast contacts (i.e. when - # people do an irc 'buildbot: command'), this will be a string - # describing the sender of the message in some useful-to-log way, and - # a single Contact may see messages from a variety of users. For - # unicast contacts (i.e. when people do an irc '/msg buildbot - # command'), a single Contact will only ever see messages from a - # single user. - message = message.lstrip() - if self.silly.has_key(message): - return self.doSilly(message) - - parts = message.split(' ', 1) - if len(parts) == 1: - parts = parts + [''] - cmd, args = parts - log.msg("irc command", cmd) - - meth = self.getCommandMethod(cmd) - if not meth and message[-1] == '!': - meth = self.command_EXCITED - - error = None - try: - if meth: - meth(args.strip(), who) - except UsageError, e: - self.send(str(e)) - except: - f = failure.Failure() - log.err(f) - error = "Something bad happened (see logs): %s" % f.type - - if error: - try: - self.send(error) - except: - log.err() - - #self.say(channel, "count %d" % self.counter) - self.channel.counter += 1 - -class IChannel(Interface): - """I represent the buildbot's presence in a particular IM scheme. - - This provides the connection to the IRC server, or represents the - buildbot's account with an IM service. Each Channel will have zero or - more Contacts associated with it. - """ - -class IrcStatusBot(irc.IRCClient): - """I represent the buildbot to an IRC server. - """ - implements(IChannel) - - def __init__(self, nickname, password, channels, status, categories, notify_events): - """ - @type nickname: string - @param nickname: the nickname by which this bot should be known - @type password: string - @param password: the password to use for identifying with Nickserv - @type channels: list of strings - @param channels: the bot will maintain a presence in these channels - @type status: L{buildbot.status.builder.Status} - @param status: the build master's Status object, through which the - bot retrieves all status information - """ - self.nickname = nickname - self.channels = channels - self.password = password - self.status = status - self.categories = categories - self.notify_events = notify_events - self.counter = 0 - self.hasQuit = 0 - self.contacts = {} - - def addContact(self, name, contact): - self.contacts[name] = contact - - def getContact(self, name): - if name in self.contacts: - return self.contacts[name] - new_contact = IRCContact(self, name) - self.contacts[name] = new_contact - return new_contact - - def deleteContact(self, contact): - name = contact.getName() - if name in self.contacts: - assert self.contacts[name] == contact - del self.contacts[name] - - def log(self, msg): - log.msg("%s: %s" % (self, msg)) - - - # the following irc.IRCClient methods are called when we have input - - def privmsg(self, user, channel, message): - user = user.split('!', 1)[0] # rest is ~user@hostname - # channel is '#twisted' or 'buildbot' (for private messages) - channel = channel.lower() - #print "privmsg:", user, channel, message - if channel == self.nickname: - # private message - contact = self.getContact(user) - contact.handleMessage(message, user) - return - # else it's a broadcast message, maybe for us, maybe not. 'channel' - # is '#twisted' or the like. - contact = self.getContact(channel) - if message.startswith("%s:" % self.nickname) or message.startswith("%s," % self.nickname): - message = message[len("%s:" % self.nickname):] - contact.handleMessage(message, user) - # to track users comings and goings, add code here - - def action(self, user, channel, data): - #log.msg("action: %s,%s,%s" % (user, channel, data)) - user = user.split('!', 1)[0] # rest is ~user@hostname - # somebody did an action (/me actions) in the broadcast channel - contact = self.getContact(channel) - if "buildbot" in data: - contact.handleAction(data, user) - - - - def signedOn(self): - if self.password: - self.msg("Nickserv", "IDENTIFY " + self.password) - for c in self.channels: - self.join(c) - - def joined(self, channel): - self.log("I have joined %s" % (channel,)) - def left(self, channel): - self.log("I have left %s" % (channel,)) - def kickedFrom(self, channel, kicker, message): - self.log("I have been kicked from %s by %s: %s" % (channel, - kicker, - message)) - - # we can using the following irc.IRCClient methods to send output. Most - # of these are used by the IRCContact class. - # - # self.say(channel, message) # broadcast - # self.msg(user, message) # unicast - # self.me(channel, action) # send action - # self.away(message='') - # self.quit(message='') - -class ThrottledClientFactory(protocol.ClientFactory): - lostDelay = 2 - failedDelay = 60 - def clientConnectionLost(self, connector, reason): - reactor.callLater(self.lostDelay, connector.connect) - def clientConnectionFailed(self, connector, reason): - reactor.callLater(self.failedDelay, connector.connect) - -class IrcStatusFactory(ThrottledClientFactory): - protocol = IrcStatusBot - - status = None - control = None - shuttingDown = False - p = None - - def __init__(self, nickname, password, channels, categories, notify_events): - #ThrottledClientFactory.__init__(self) # doesn't exist - self.status = None - self.nickname = nickname - self.password = password - self.channels = channels - self.categories = categories - self.notify_events = notify_events - - def __getstate__(self): - d = self.__dict__.copy() - del d['p'] - return d - - def shutdown(self): - self.shuttingDown = True - if self.p: - self.p.quit("buildmaster reconfigured: bot disconnecting") - - def buildProtocol(self, address): - p = self.protocol(self.nickname, self.password, - self.channels, self.status, - self.categories, self.notify_events) - p.factory = self - p.status = self.status - p.control = self.control - self.p = p - return p - - # TODO: I think a shutdown that occurs while the connection is being - # established will make this explode - - def clientConnectionLost(self, connector, reason): - if self.shuttingDown: - log.msg("not scheduling reconnection attempt") - return - ThrottledClientFactory.clientConnectionLost(self, connector, reason) - - def clientConnectionFailed(self, connector, reason): - if self.shuttingDown: - log.msg("not scheduling reconnection attempt") - return - ThrottledClientFactory.clientConnectionFailed(self, connector, reason) - - -class IRC(base.StatusReceiverMultiService): - """I am an IRC bot which can be queried for status information. I - connect to a single IRC server and am known by a single nickname on that - server, however I can join multiple channels.""" - - compare_attrs = ["host", "port", "nick", "password", - "channels", "allowForce", - "categories"] - - def __init__(self, host, nick, channels, port=6667, allowForce=True, - categories=None, password=None, notify_events={}): - base.StatusReceiverMultiService.__init__(self) - - assert allowForce in (True, False) # TODO: implement others - - # need to stash these so we can detect changes later - self.host = host - self.port = port - self.nick = nick - self.channels = channels - self.password = password - self.allowForce = allowForce - self.categories = categories - self.notify_events = notify_events - - # need to stash the factory so we can give it the status object - self.f = IrcStatusFactory(self.nick, self.password, - self.channels, self.categories, self.notify_events) - - c = internet.TCPClient(host, port, self.f) - c.setServiceParent(self) - - def setServiceParent(self, parent): - base.StatusReceiverMultiService.setServiceParent(self, parent) - self.f.status = parent.getStatus() - if self.allowForce: - self.f.control = interfaces.IControl(parent) - - def stopService(self): - # make sure the factory will stop reconnecting - self.f.shutdown() - return base.StatusReceiverMultiService.stopService(self) - - -## buildbot: list builders -# buildbot: watch quick -# print notification when current build in 'quick' finishes -## buildbot: status -## buildbot: status full-2.3 -## building, not, % complete, ETA -## buildbot: force build full-2.3 "reason" |