Home | Trees | Indices | Help |
---|
|
1 2 # code to deliver build status through twisted.words (instant messaging 3 # protocols: irc, etc) 4 5 import re, shlex 6 7 from zope.interface import Interface, implements 8 from twisted.internet import protocol, reactor 9 from twisted.words.protocols import irc 10 from twisted.python import log, failure 11 from twisted.application import internet 12 13 from buildbot import interfaces, util 14 from buildbot import version 15 from buildbot.sourcestamp import SourceStamp 16 from buildbot.process.base import BuildRequest 17 from buildbot.status import base 18 from buildbot.status.builder import SUCCESS, WARNINGS, FAILURE, EXCEPTION 19 from buildbot.scripts.runner import ForceOptions 20 21 from string import join, capitalize, lower 222625 ValueError.__init__(self, string, *more)28 hasStarted = False 29 timer = None 30 3455 5636 del self.timer 37 if not self.hasStarted: 38 self.parent.send("The build has been queued, I'll give a shout" 39 " when it starts")4042 self.hasStarted = True 43 if self.timer: 44 self.timer.cancel() 45 del self.timer 46 s = c.getStatus() 47 eta = s.getETA() 48 response = "build #%d forced" % s.getNumber() 49 if eta is not None: 50 response = "build forced [ETA %s]" % self.parent.convertTime(eta) 51 self.parent.send(response) 52 self.parent.send("I'll give a shout when the build finishes") 53 d = s.waitUntilFinished() 54 d.addCallback(self.parent.watchedBuildFinished)58 """I hold the state for a single user's interaction with the buildbot. 59 60 This base class provides all the basic behavior (the queries and 61 responses). Subclasses for each channel type (IRC, different IM 62 protocols) are expected to provide the lower-level send/receive methods. 63 64 There will be one instance of me for each user who interacts personally 65 with the buildbot. There will be an additional instance for each 66 'broadcast contact' (chat rooms, IRC channels as a whole). 67 """ 6857170 self.channel = channel 71 self.notify_events = {} 72 self.subscribed = 0 73 self.add_notification_events(channel.notify_events)74 75 silly = { 76 "What happen ?": "Somebody set up us the bomb.", 77 "It's You !!": ["How are you gentlemen !!", 78 "All your base are belong to us.", 79 "You are on the way to destruction."], 80 "What you say !!": ["You have no chance to survive make your time.", 81 "HA HA HA HA ...."], 82 } 83 8789 try: 90 b = self.channel.status.getBuilder(which) 91 except KeyError: 92 raise UsageError, "no such builder '%s'" % which 93 return b9496 if not self.channel.control: 97 raise UsageError("builder control is not enabled") 98 try: 99 bc = self.channel.control.getBuilder(which) 100 except KeyError: 101 raise UsageError("no such builder '%s'" % which) 102 return bc103105 """ 106 @rtype: list of L{buildbot.process.builder.Builder} 107 """ 108 names = self.channel.status.getBuilderNames(categories=self.channel.categories) 109 names.sort() 110 builders = [self.channel.status.getBuilder(n) for n in names] 111 return builders112114 if seconds < 60: 115 return "%d seconds" % seconds 116 minutes = int(seconds / 60) 117 seconds = seconds - 60*minutes 118 if minutes < 60: 119 return "%dm%02ds" % (minutes, seconds) 120 hours = int(minutes / 60) 121 minutes = minutes - 60*hours 122 return "%dh%02dm%02ds" % (hours, minutes, seconds)123125 response = self.silly[message] 126 if type(response) != type([]): 127 response = [response] 128 when = 0.5 129 for r in response: 130 reactor.callLater(when, self.send, r) 131 when += 2.5132134 self.send("yes?")135 138140 args = args.split() 141 if len(args) == 0: 142 raise UsageError, "try 'list builders'" 143 if args[0] == 'builders': 144 builders = self.getAllBuilders() 145 str = "Configured builders: " 146 for b in builders: 147 str += b.name 148 state = b.getState()[0] 149 if state == 'offline': 150 str += "[offline]" 151 str += " " 152 str.rstrip() 153 self.send(str) 154 return155 command_LIST.usage = "list builders - List configured builders" 156158 args = args.split() 159 if len(args) == 0: 160 which = "all" 161 elif len(args) == 1: 162 which = args[0] 163 else: 164 raise UsageError, "try 'status <builder>'" 165 if which == "all": 166 builders = self.getAllBuilders() 167 for b in builders: 168 self.emit_status(b.name) 169 return 170 self.emit_status(which)171 command_STATUS.usage = "status [<which>] - List status of a builder (or all builders)" 172174 if not re.compile("^(started|finished|success|failure|exception|warnings|(success|warnings|exception|failure)To(Failure|Success|Warnings|Exception))$").match(event): 175 raise UsageError("try 'notify on|off <EVENT>'")176178 self.send( "The following events are being notified: %r" % self.notify_events.keys() )179 185 189 193195 for event in events: 196 self.validate_notification_event(event) 197 self.notify_events[event] = 1 198 199 if not self.subscribed: 200 self.subscribe_to_build_events()201203 for event in events: 204 self.validate_notification_event(event) 205 del self.notify_events[event] 206 207 if len(self.notify_events) == 0 and self.subscribed: 208 self.unsubscribe_from_build_events()209 215217 args = args.split() 218 219 if not args: 220 raise UsageError("try 'notify on|off|list <EVENT>'") 221 action = args.pop(0) 222 events = args 223 224 if action == "on": 225 if not events: events = ('started','finished') 226 self.add_notification_events(events) 227 228 self.list_notified_events() 229 230 elif action == "off": 231 if events: 232 self.remove_notification_events(events) 233 else: 234 self.remove_all_notification_events() 235 236 self.list_notified_events() 237 238 elif action == "list": 239 self.list_notified_events() 240 return 241 242 else: 243 raise UsageError("try 'notify on|off <EVENT>'")244 245 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)" 246248 args = args.split() 249 if len(args) != 1: 250 raise UsageError("try 'watch <builder>'") 251 which = args[0] 252 b = self.getBuilder(which) 253 builds = b.getCurrentBuilds() 254 if not builds: 255 self.send("there are no builds currently running") 256 return 257 for build in builds: 258 assert not build.isFinished() 259 d = build.waitUntilFinished() 260 d.addCallback(self.watchedBuildFinished) 261 r = "watching build %s #%d until it finishes" \ 262 % (which, build.getNumber()) 263 eta = build.getETA() 264 if eta is not None: 265 r += " [%s]" % self.convertTime(eta) 266 r += ".." 267 self.send(r)268 command_WATCH.usage = "watch <which> - announce the completion of an active build" 269 272 276278 log.msg('[Contact] Builder %s changed state to %s' % (builderName, state))279281 log.msg('[Contact] BuildRequest for %s submitted to Builder %s' % 282 (brstatus.getSourceStamp(), brstatus.builderName))283285 log.msg('[Contact] Builder %s removed' % (builderName))286288 builder = build.getBuilder() 289 log.msg('[Contact] Builder %r in category %s started' % (builder, builder.category)) 290 291 # only notify about builders we are interested in 292 293 if (self.channel.categories != None and 294 builder.category not in self.channel.categories): 295 log.msg('Not notifying for a build in the wrong category') 296 return 297 298 if not self.notify_for('started'): 299 log.msg('Not notifying for a build when started-notification disabled') 300 return 301 302 r = "build #%d of %s started" % \ 303 (build.getNumber(), 304 builder.getName()) 305 306 r += " including [" + ", ".join(map(lambda c: repr(c.revision), build.getChanges())) + "]" 307 308 self.send(r)309311 builder = build.getBuilder() 312 313 results_descriptions = { 314 SUCCESS: "Success", 315 WARNINGS: "Warnings", 316 FAILURE: "Failure", 317 EXCEPTION: "Exception", 318 } 319 320 # only notify about builders we are interested in 321 log.msg('[Contact] builder %r in category %s finished' % (builder, builder.category)) 322 323 if (self.channel.categories != None and 324 builder.category not in self.channel.categories): 325 return 326 327 results = build.getResults() 328 329 r = "build #%d of %s is complete: %s" % \ 330 (build.getNumber(), 331 builder.getName(), 332 results_descriptions.get(results, "??")) 333 r += " [%s]" % " ".join(build.getText()) 334 buildurl = self.channel.status.getURLForThing(build) 335 if buildurl: 336 r += " Build details are at %s" % buildurl 337 338 if self.notify_for('finished') or self.notify_for(lower(results_descriptions.get(results))): 339 self.send(r) 340 return 341 342 prevBuild = build.getPreviousBuild() 343 if prevBuild: 344 prevResult = prevBuild.getResults() 345 346 required_notification_control_string = join((lower(results_descriptions.get(prevResult)), \ 347 'To', \ 348 capitalize(results_descriptions.get(results))), \ 349 '') 350 351 if (self.notify_for(required_notification_control_string)): 352 self.send(r)353355 results = {SUCCESS: "Success", 356 WARNINGS: "Warnings", 357 FAILURE: "Failure", 358 EXCEPTION: "Exception", 359 } 360 361 # only notify about builders we are interested in 362 builder = b.getBuilder() 363 log.msg('builder %r in category %s finished' % (builder, 364 builder.category)) 365 if (self.channel.categories != None and 366 builder.category not in self.channel.categories): 367 return 368 369 r = "Hey! build %s #%d is complete: %s" % \ 370 (b.getBuilder().getName(), 371 b.getNumber(), 372 results.get(b.getResults(), "??")) 373 r += " [%s]" % " ".join(b.getText()) 374 self.send(r) 375 buildurl = self.channel.status.getURLForThing(b) 376 if buildurl: 377 self.send("Build details are at %s" % buildurl)378380 args = shlex.split(args) # TODO: this requires python2.3 or newer 381 if not args: 382 raise UsageError("try 'force build WHICH <REASON>'") 383 what = args.pop(0) 384 if what != "build": 385 raise UsageError("try 'force build WHICH <REASON>'") 386 opts = ForceOptions() 387 opts.parseOptions(args) 388 389 which = opts['builder'] 390 branch = opts['branch'] 391 revision = opts['revision'] 392 reason = opts['reason'] 393 394 if which is None: 395 raise UsageError("you must provide a Builder, " 396 "try 'force build WHICH <REASON>'") 397 398 # keep weird stuff out of the branch and revision strings. TODO: 399 # centralize this somewhere. 400 if branch and not re.match(r'^[\w\.\-\/]*$', branch): 401 log.msg("bad branch '%s'" % branch) 402 self.send("sorry, bad branch '%s'" % branch) 403 return 404 if revision and not re.match(r'^[\w\.\-\/]*$', revision): 405 log.msg("bad revision '%s'" % revision) 406 self.send("sorry, bad revision '%s'" % revision) 407 return 408 409 bc = self.getControl(which) 410 411 r = "forced: by %s: %s" % (self.describeUser(who), reason) 412 # TODO: maybe give certain users the ability to request builds of 413 # certain branches 414 s = SourceStamp(branch=branch, revision=revision) 415 req = BuildRequest(r, s, which) 416 try: 417 bc.requestBuildSoon(req) 418 except interfaces.NoSlaveError: 419 self.send("sorry, I can't force a build: all slaves are offline") 420 return 421 ireq = IrcBuildRequest(self) 422 req.subscribe(ireq.started)423 424 425 command_FORCE.usage = "force build <which> <reason> - Force a build" 426428 args = args.split(None, 2) 429 if len(args) < 3 or args[0] != 'build': 430 raise UsageError, "try 'stop build WHICH <REASON>'" 431 which = args[1] 432 reason = args[2] 433 434 buildercontrol = self.getControl(which) 435 436 r = "stopped: by %s: %s" % (self.describeUser(who), reason) 437 438 # find an in-progress build 439 builderstatus = self.getBuilder(which) 440 builds = builderstatus.getCurrentBuilds() 441 if not builds: 442 self.send("sorry, no build is currently running") 443 return 444 for build in builds: 445 num = build.getNumber() 446 447 # obtain the BuildControl object 448 buildcontrol = buildercontrol.getBuild(num) 449 450 # make it stop 451 buildcontrol.stopBuild(r) 452 453 self.send("build %d interrupted" % num)454 455 command_STOP.usage = "stop build <which> <reason> - Stop a running build" 456458 b = self.getBuilder(which) 459 str = "%s: " % which 460 state, builds = b.getState() 461 str += state 462 if state == "idle": 463 last = b.getLastFinishedBuild() 464 if last: 465 start,finished = last.getTimes() 466 str += ", last build %s ago: %s" % \ 467 (self.convertTime(int(util.now() - finished)), " ".join(last.getText())) 468 if state == "building": 469 t = [] 470 for build in builds: 471 step = build.getCurrentStep() 472 if step: 473 s = "(%s)" % " ".join(step.getText()) 474 else: 475 s = "(no current step)" 476 ETA = build.getETA() 477 if ETA is not None: 478 s += " [ETA %s]" % self.convertTime(ETA) 479 t.append(s) 480 str += ", ".join(t) 481 self.send(str)482484 last = self.getBuilder(which).getLastFinishedBuild() 485 if not last: 486 str = "(no builds run since last restart)" 487 else: 488 start,finish = last.getTimes() 489 str = "%s ago: " % (self.convertTime(int(util.now() - finish))) 490 str += " ".join(last.getText()) 491 self.send("last build [%s]: %s" % (which, str))492494 args = args.split() 495 if len(args) == 0: 496 which = "all" 497 elif len(args) == 1: 498 which = args[0] 499 else: 500 raise UsageError, "try 'last <builder>'" 501 if which == "all": 502 builders = self.getAllBuilders() 503 for b in builders: 504 self.emit_last(b.name) 505 return 506 self.emit_last(which)507 command_LAST.usage = "last <which> - list last build status for builder <which>" 508510 commands = [] 511 for k in dir(self): 512 if k.startswith('command_'): 513 commands.append(k[8:].lower()) 514 commands.sort() 515 return commands516518 args = args.split() 519 if len(args) == 0: 520 self.send("Get help on what? (try 'help <foo>', or 'commands' for a command list)") 521 return 522 command = args[0] 523 meth = self.getCommandMethod(command) 524 if not meth: 525 raise UsageError, "no such command '%s'" % command 526 usage = getattr(meth, 'usage', None) 527 if usage: 528 self.send("Usage: %s" % usage) 529 else: 530 self.send("No usage info for '%s'" % command)531 command_HELP.usage = "help <command> - Give help for <command>" 532 536538 commands = self.build_commands() 539 str = "buildbot commands: " + ", ".join(commands) 540 self.send(str)541 command_COMMANDS.usage = "commands - List available commands" 542544 self.act("readies phasers")545547 reactor.callLater(1.0, self.send, "0-<") 548 reactor.callLater(3.0, self.send, "0-/") 549 reactor.callLater(3.5, self.send, "0-\\")550 554556 # this is sent when somebody performs an action that mentions the 557 # buildbot (like '/me kicks buildbot'). 'user' is the name/nick/id of 558 # the person who performed the action, so if their action provokes a 559 # response, they can be named. 560 if not data.endswith("s buildbot"): 561 return 562 words = data.split() 563 verb = words[-2] 564 timeout = 4 565 if verb == "kicks": 566 response = "%s back" % verb 567 timeout = 1 568 else: 569 response = "%s %s too" % (verb, user) 570 reactor.callLater(timeout, self.act, response)573 # this is the IRC-specific subclass of Contact 574651576 Contact.__init__(self, channel) 577 # when people send us public messages ("buildbot: command"), 578 # self.dest is the name of the channel ("#twisted"). When they send 579 # us private messages (/msg buildbot command), self.dest is their 580 # username. 581 self.dest = dest582584 if self.dest[0] == "#": 585 return "IRC user <%s> on channel %s" % (user, self.dest) 586 return "IRC user <%s> (privmsg)" % user587 588 # userJoined(self, user, channel) 589591 self.channel.msg(self.dest, message.encode("ascii", "replace"))593 self.channel.me(self.dest, action.encode("ascii", "replace"))594596 args = args.split() 597 to_join = args[0] 598 self.channel.join(to_join) 599 self.send("Joined %s" % to_join)600 command_JOIN.usage = "join channel - Join another channel" 601603 args = args.split() 604 to_leave = args[0] 605 self.send("Buildbot has been told to leave %s" % to_leave) 606 self.channel.part(to_leave)607 command_LEAVE.usage = "leave channel - Leave a channel" 608 609611 # a message has arrived from 'who'. For broadcast contacts (i.e. when 612 # people do an irc 'buildbot: command'), this will be a string 613 # describing the sender of the message in some useful-to-log way, and 614 # a single Contact may see messages from a variety of users. For 615 # unicast contacts (i.e. when people do an irc '/msg buildbot 616 # command'), a single Contact will only ever see messages from a 617 # single user. 618 message = message.lstrip() 619 if self.silly.has_key(message): 620 return self.doSilly(message) 621 622 parts = message.split(' ', 1) 623 if len(parts) == 1: 624 parts = parts + [''] 625 cmd, args = parts 626 log.msg("irc command", cmd) 627 628 meth = self.getCommandMethod(cmd) 629 if not meth and message[-1] == '!': 630 meth = self.command_EXCITED 631 632 error = None 633 try: 634 if meth: 635 meth(args.strip(), who) 636 except UsageError, e: 637 self.send(str(e)) 638 except: 639 f = failure.Failure() 640 log.err(f) 641 error = "Something bad happened (see logs): %s" % f.type 642 643 if error: 644 try: 645 self.send(error) 646 except: 647 log.err() 648 649 #self.say(channel, "count %d" % self.counter) 650 self.channel.counter += 1653 """I represent the buildbot's presence in a particular IM scheme. 654 655 This provides the connection to the IRC server, or represents the 656 buildbot's account with an IM service. Each Channel will have zero or 657 more Contacts associated with it. 658 """659661 """I represent the buildbot to an IRC server. 662 """ 663 implements(IChannel) 664751 752 # we can using the following irc.IRCClient methods to send output. Most 753 # of these are used by the IRCContact class. 754 # 755 # self.say(channel, message) # broadcast 756 # self.msg(user, message) # unicast 757 # self.me(channel, action) # send action 758 # self.away(message='') 759 # self.quit(message='') 760 768666 """ 667 @type nickname: string 668 @param nickname: the nickname by which this bot should be known 669 @type password: string 670 @param password: the password to use for identifying with Nickserv 671 @type channels: list of strings 672 @param channels: the bot will maintain a presence in these channels 673 @type status: L{buildbot.status.builder.Status} 674 @param status: the build master's Status object, through which the 675 bot retrieves all status information 676 """ 677 self.nickname = nickname 678 self.channels = channels 679 self.password = password 680 self.status = status 681 self.categories = categories 682 self.notify_events = notify_events 683 self.counter = 0 684 self.hasQuit = 0 685 self.contacts = {}686688 self.contacts[name] = contact689691 if name in self.contacts: 692 return self.contacts[name] 693 new_contact = IRCContact(self, name) 694 self.contacts[name] = new_contact 695 return new_contact696698 name = contact.getName() 699 if name in self.contacts: 700 assert self.contacts[name] == contact 701 del self.contacts[name]702704 log.msg("%s: %s" % (self, msg))705 706 707 # the following irc.IRCClient methods are called when we have input 708710 user = user.split('!', 1)[0] # rest is ~user@hostname 711 # channel is '#twisted' or 'buildbot' (for private messages) 712 channel = channel.lower() 713 #print "privmsg:", user, channel, message 714 if channel == self.nickname: 715 # private message 716 contact = self.getContact(user) 717 contact.handleMessage(message, user) 718 return 719 # else it's a broadcast message, maybe for us, maybe not. 'channel' 720 # is '#twisted' or the like. 721 contact = self.getContact(channel) 722 if message.startswith("%s:" % self.nickname) or message.startswith("%s," % self.nickname): 723 message = message[len("%s:" % self.nickname):] 724 contact.handleMessage(message, user)725 # to track users comings and goings, add code here 726728 #log.msg("action: %s,%s,%s" % (user, channel, data)) 729 user = user.split('!', 1)[0] # rest is ~user@hostname 730 # somebody did an action (/me actions) in the broadcast channel 731 contact = self.getContact(channel) 732 if "buildbot" in data: 733 contact.handleAction(data, user)734 735 736738 if self.password: 739 self.msg("Nickserv", "IDENTIFY " + self.password) 740 for c in self.channels: 741 self.join(c)742744 self.log("I have joined %s" % (channel,))746 self.log("I have left %s" % (channel,))770 protocol = IrcStatusBot 771 772 status = None 773 control = None 774 shuttingDown = False 775 p = None 776820 821778 #ThrottledClientFactory.__init__(self) # doesn't exist 779 self.status = None 780 self.nickname = nickname 781 self.password = password 782 self.channels = channels 783 self.categories = categories 784 self.notify_events = notify_events785 790792 self.shuttingDown = True 793 if self.p: 794 self.p.quit("buildmaster reconfigured: bot disconnecting")795797 p = self.protocol(self.nickname, self.password, 798 self.channels, self.status, 799 self.categories, self.notify_events) 800 p.factory = self 801 p.status = self.status 802 p.control = self.control 803 self.p = p 804 return p805 806 # TODO: I think a shutdown that occurs while the connection is being 807 # established will make this explode 808810 if self.shuttingDown: 811 log.msg("not scheduling reconnection attempt") 812 return 813 ThrottledClientFactory.clientConnectionLost(self, connector, reason)814816 if self.shuttingDown: 817 log.msg("not scheduling reconnection attempt") 818 return 819 ThrottledClientFactory.clientConnectionFailed(self, connector, reason)823 """I am an IRC bot which can be queried for status information. I 824 connect to a single IRC server and am known by a single nickname on that 825 server, however I can join multiple channels.""" 826 827 compare_attrs = ["host", "port", "nick", "password", 828 "channels", "allowForce", 829 "categories"] 830864 865 866 ## buildbot: list builders 867 # buildbot: watch quick 868 # print notification when current build in 'quick' finishes 869 ## buildbot: status 870 ## buildbot: status full-2.3 871 ## building, not, % complete, ETA 872 ## buildbot: force build full-2.3 "reason" 873831 - def __init__(self, host, nick, channels, port=6667, allowForce=True, 832 categories=None, password=None, notify_events={}):833 base.StatusReceiverMultiService.__init__(self) 834 835 assert allowForce in (True, False) # TODO: implement others 836 837 # need to stash these so we can detect changes later 838 self.host = host 839 self.port = port 840 self.nick = nick 841 self.channels = channels 842 self.password = password 843 self.allowForce = allowForce 844 self.categories = categories 845 self.notify_events = notify_events 846 847 # need to stash the factory so we can give it the status object 848 self.f = IrcStatusFactory(self.nick, self.password, 849 self.channels, self.categories, self.notify_events) 850 851 c = internet.TCPClient(host, port, self.f) 852 c.setServiceParent(self)853855 base.StatusReceiverMultiService.setServiceParent(self, parent) 856 self.f.status = parent.getStatus() 857 if self.allowForce: 858 self.f.control = interfaces.IControl(parent)859861 # make sure the factory will stop reconnecting 862 self.f.shutdown() 863 return base.StatusReceiverMultiService.stopService(self)
Home | Trees | Indices | Help |
---|
Generated by Epydoc 3.0.1 on Fri Jul 31 16:52:08 2009 | http://epydoc.sourceforge.net |