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 345536 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)57 """I hold the state for a single user's interaction with the buildbot. 58 59 This base class provides all the basic behavior (the queries and 60 responses). Subclasses for each channel type (IRC, different IM 61 protocols) are expected to provide the lower-level send/receive methods. 62 63 There will be one instance of me for each user who interacts personally 64 with the buildbot. There will be an additional instance for each 65 'broadcast contact' (chat rooms, IRC channels as a whole). 66 """ 6758269 self.channel = channel 70 self.notify_events = {} 71 self.subscribed = 0 72 self.add_notification_events(channel.notify_events)73 74 silly = { 75 "What happen ?": "Somebody set up us the bomb.", 76 "It's You !!": ["How are you gentlemen !!", 77 "All your base are belong to us.", 78 "You are on the way to destruction."], 79 "What you say !!": ["You have no chance to survive make your time.", 80 "HA HA HA HA ...."], 81 } 82 8688 try: 89 b = self.channel.status.getBuilder(which) 90 except KeyError: 91 raise UsageError, "no such builder '%s'" % which 92 return b9395 if not self.channel.control: 96 raise UsageError("builder control is not enabled") 97 try: 98 bc = self.channel.control.getBuilder(which) 99 except KeyError: 100 raise UsageError("no such builder '%s'" % which) 101 return bc102104 """ 105 @rtype: list of L{buildbot.process.builder.Builder} 106 """ 107 names = self.channel.status.getBuilderNames(categories=self.channel.categories) 108 names.sort() 109 builders = [self.channel.status.getBuilder(n) for n in names] 110 return builders111113 if seconds < 60: 114 return "%d seconds" % seconds 115 minutes = int(seconds / 60) 116 seconds = seconds - 60*minutes 117 if minutes < 60: 118 return "%dm%02ds" % (minutes, seconds) 119 hours = int(minutes / 60) 120 minutes = minutes - 60*hours 121 return "%dh%02dm%02ds" % (hours, minutes, seconds)122124 response = self.silly[message] 125 if type(response) != type([]): 126 response = [response] 127 when = 0.5 128 for r in response: 129 reactor.callLater(when, self.send, r) 130 when += 2.5131133 self.send("yes?")134 137139 args = shlex.split(args) 140 if len(args) == 0: 141 raise UsageError, "try 'list builders'" 142 if args[0] == 'builders': 143 builders = self.getAllBuilders() 144 str = "Configured builders: " 145 for b in builders: 146 str += b.name 147 state = b.getState()[0] 148 if state == 'offline': 149 str += "[offline]" 150 str += " " 151 str.rstrip() 152 self.send(str) 153 return154 command_LIST.usage = "list builders - List configured builders" 155157 args = shlex.split(args) 158 if len(args) == 0: 159 which = "all" 160 elif len(args) == 1: 161 which = args[0] 162 else: 163 raise UsageError, "try 'status <builder>'" 164 if which == "all": 165 builders = self.getAllBuilders() 166 for b in builders: 167 self.emit_status(b.name) 168 return 169 self.emit_status(which)170 command_STATUS.usage = "status [<which>] - List status of a builder (or all builders)" 171173 if not re.compile("^(started|finished|success|failure|exception|warnings|(success|warnings|exception|failure)To(Failure|Success|Warnings|Exception))$").match(event): 174 raise UsageError("try 'notify on|off <EVENT>'")175177 self.send( "The following events are being notified: %r" % self.notify_events.keys() )178 184 188 192194 for event in events: 195 self.validate_notification_event(event) 196 self.notify_events[event] = 1 197 198 if not self.subscribed: 199 self.subscribe_to_build_events()200202 for event in events: 203 self.validate_notification_event(event) 204 del self.notify_events[event] 205 206 if len(self.notify_events) == 0 and self.subscribed: 207 self.unsubscribe_from_build_events()208 214216 args = shlex.split(args) 217 218 if not args: 219 raise UsageError("try 'notify on|off|list <EVENT>'") 220 action = args.pop(0) 221 events = args 222 223 if action == "on": 224 if not events: events = ('started','finished') 225 self.add_notification_events(events) 226 227 self.list_notified_events() 228 229 elif action == "off": 230 if events: 231 self.remove_notification_events(events) 232 else: 233 self.remove_all_notification_events() 234 235 self.list_notified_events() 236 237 elif action == "list": 238 self.list_notified_events() 239 return 240 241 else: 242 raise UsageError("try 'notify on|off <EVENT>'")243 244 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)" 245247 args = shlex.split(args) 248 if len(args) != 1: 249 raise UsageError("try 'watch <builder>'") 250 which = args[0] 251 b = self.getBuilder(which) 252 builds = b.getCurrentBuilds() 253 if not builds: 254 self.send("there are no builds currently running") 255 return 256 for build in builds: 257 assert not build.isFinished() 258 d = build.waitUntilFinished() 259 d.addCallback(self.watchedBuildFinished) 260 r = "watching build %s #%d until it finishes" \ 261 % (which, build.getNumber()) 262 eta = build.getETA() 263 if eta is not None: 264 r += " [%s]" % self.convertTime(eta) 265 r += ".." 266 self.send(r)267 command_WATCH.usage = "watch <which> - announce the completion of an active build" 268 271 275277 log.msg('[Contact] Builder %s changed state to %s' % (builderName, state))278280 log.msg('[Contact] BuildRequest for %s submitted to Builder %s' % 281 (brstatus.getSourceStamp(), brstatus.builderName))282 286288 log.msg('[Contact] Builder %s removed' % (builderName))289291 builder = build.getBuilder() 292 log.msg('[Contact] Builder %r in category %s started' % (builder, builder.category)) 293 294 # only notify about builders we are interested in 295 296 if (self.channel.categories != None and 297 builder.category not in self.channel.categories): 298 log.msg('Not notifying for a build in the wrong category') 299 return 300 301 if not self.notify_for('started'): 302 log.msg('Not notifying for a build when started-notification disabled') 303 return 304 305 r = "build #%d of %s started" % \ 306 (build.getNumber(), 307 builder.getName()) 308 309 r += " including [" + ", ".join(map(lambda c: repr(c.revision), build.getChanges())) + "]" 310 311 self.send(r)312 313 results_descriptions = { 314 SUCCESS: "Success", 315 WARNINGS: "Warnings", 316 FAILURE: "Failure", 317 EXCEPTION: "Exception", 318 } 319321 builder = build.getBuilder() 322 323 # only notify about builders we are interested in 324 log.msg('[Contact] builder %r in category %s finished' % (builder, builder.category)) 325 326 if (self.channel.categories != None and 327 builder.category not in self.channel.categories): 328 return 329 330 if not self.notify_for_finished(build): 331 return 332 333 r = "build #%d of %s is complete: %s" % \ 334 (build.getNumber(), 335 builder.getName(), 336 self.results_descriptions.get(build.getResults(), "??")) 337 r += " [%s]" % " ".join(build.getText()) 338 buildurl = self.channel.status.getURLForThing(build) 339 if buildurl: 340 r += " Build details are at %s" % buildurl 341 342 if self.channel.showBlameList and build.getResults() != SUCCESS and len(build.changes) != 0: 343 r += ' blamelist: ' + ', '.join([c.who for c in build.changes]) 344 345 self.send(r)346348 results = build.getResults() 349 350 if self.notify_for('finished'): 351 return True 352 353 if self.notify_for(lower(self.results_descriptions.get(results))): 354 return True 355 356 prevBuild = build.getPreviousBuild() 357 if prevBuild: 358 prevResult = prevBuild.getResults() 359 360 required_notification_control_string = join((lower(self.results_descriptions.get(prevResult)), \ 361 'To', \ 362 capitalize(self.results_descriptions.get(results))), \ 363 '') 364 365 if (self.notify_for(required_notification_control_string)): 366 return True 367 368 return False369371 372 # only notify about builders we are interested in 373 builder = b.getBuilder() 374 log.msg('builder %r in category %s finished' % (builder, 375 builder.category)) 376 if (self.channel.categories != None and 377 builder.category not in self.channel.categories): 378 return 379 380 r = "Hey! build %s #%d is complete: %s" % \ 381 (b.getBuilder().getName(), 382 b.getNumber(), 383 self.results_descriptions.get(b.getResults(), "??")) 384 r += " [%s]" % " ".join(b.getText()) 385 self.send(r) 386 buildurl = self.channel.status.getURLForThing(b) 387 if buildurl: 388 self.send("Build details are at %s" % buildurl)389391 args = shlex.split(args) # TODO: this requires python2.3 or newer 392 if not args: 393 raise UsageError("try 'force build WHICH <REASON>'") 394 what = args.pop(0) 395 if what != "build": 396 raise UsageError("try 'force build WHICH <REASON>'") 397 opts = ForceOptions() 398 opts.parseOptions(args) 399 400 which = opts['builder'] 401 branch = opts['branch'] 402 revision = opts['revision'] 403 reason = opts['reason'] 404 405 if which is None: 406 raise UsageError("you must provide a Builder, " 407 "try 'force build WHICH <REASON>'") 408 409 # keep weird stuff out of the branch and revision strings. TODO: 410 # centralize this somewhere. 411 if branch and not re.match(r'^[\w\.\-\/]*$', branch): 412 log.msg("bad branch '%s'" % branch) 413 self.send("sorry, bad branch '%s'" % branch) 414 return 415 if revision and not re.match(r'^[\w\.\-\/]*$', revision): 416 log.msg("bad revision '%s'" % revision) 417 self.send("sorry, bad revision '%s'" % revision) 418 return 419 420 bc = self.getControl(which) 421 422 r = "forced: by %s: %s" % (self.describeUser(who), reason) 423 # TODO: maybe give certain users the ability to request builds of 424 # certain branches 425 s = SourceStamp(branch=branch, revision=revision) 426 req = BuildRequest(r, s, which) 427 try: 428 bc.requestBuildSoon(req) 429 except interfaces.NoSlaveError: 430 self.send("sorry, I can't force a build: all slaves are offline") 431 return 432 ireq = IrcBuildRequest(self) 433 req.subscribe(ireq.started)434 435 436 command_FORCE.usage = "force build <which> <reason> - Force a build" 437439 args = shlex.split(args) 440 if len(args) < 3 or args[0] != 'build': 441 raise UsageError, "try 'stop build WHICH <REASON>'" 442 which = args[1] 443 reason = args[2] 444 445 buildercontrol = self.getControl(which) 446 447 r = "stopped: by %s: %s" % (self.describeUser(who), reason) 448 449 # find an in-progress build 450 builderstatus = self.getBuilder(which) 451 builds = builderstatus.getCurrentBuilds() 452 if not builds: 453 self.send("sorry, no build is currently running") 454 return 455 for build in builds: 456 num = build.getNumber() 457 458 # obtain the BuildControl object 459 buildcontrol = buildercontrol.getBuild(num) 460 461 # make it stop 462 buildcontrol.stopBuild(r) 463 464 self.send("build %d interrupted" % num)465 466 command_STOP.usage = "stop build <which> <reason> - Stop a running build" 467469 b = self.getBuilder(which) 470 str = "%s: " % which 471 state, builds = b.getState() 472 str += state 473 if state == "idle": 474 last = b.getLastFinishedBuild() 475 if last: 476 start,finished = last.getTimes() 477 str += ", last build %s ago: %s" % \ 478 (self.convertTime(int(util.now() - finished)), " ".join(last.getText())) 479 if state == "building": 480 t = [] 481 for build in builds: 482 step = build.getCurrentStep() 483 if step: 484 s = "(%s)" % " ".join(step.getText()) 485 else: 486 s = "(no current step)" 487 ETA = build.getETA() 488 if ETA is not None: 489 s += " [ETA %s]" % self.convertTime(ETA) 490 t.append(s) 491 str += ", ".join(t) 492 self.send(str)493495 last = self.getBuilder(which).getLastFinishedBuild() 496 if not last: 497 str = "(no builds run since last restart)" 498 else: 499 start,finish = last.getTimes() 500 str = "%s ago: " % (self.convertTime(int(util.now() - finish))) 501 str += " ".join(last.getText()) 502 self.send("last build [%s]: %s" % (which, str))503505 args = shlex.split(args) 506 if len(args) == 0: 507 which = "all" 508 elif len(args) == 1: 509 which = args[0] 510 else: 511 raise UsageError, "try 'last <builder>'" 512 if which == "all": 513 builders = self.getAllBuilders() 514 for b in builders: 515 self.emit_last(b.name) 516 return 517 self.emit_last(which)518 command_LAST.usage = "last <which> - list last build status for builder <which>" 519521 commands = [] 522 for k in dir(self): 523 if k.startswith('command_'): 524 commands.append(k[8:].lower()) 525 commands.sort() 526 return commands527529 args = shlex.split(args) 530 if len(args) == 0: 531 self.send("Get help on what? (try 'help <foo>', or 'commands' for a command list)") 532 return 533 command = args[0] 534 meth = self.getCommandMethod(command) 535 if not meth: 536 raise UsageError, "no such command '%s'" % command 537 usage = getattr(meth, 'usage', None) 538 if usage: 539 self.send("Usage: %s" % usage) 540 else: 541 self.send("No usage info for '%s'" % command)542 command_HELP.usage = "help <command> - Give help for <command>" 543 547549 commands = self.build_commands() 550 str = "buildbot commands: " + ", ".join(commands) 551 self.send(str)552 command_COMMANDS.usage = "commands - List available commands" 553555 self.act("readies phasers")556558 reactor.callLater(1.0, self.send, "0-<") 559 reactor.callLater(3.0, self.send, "0-/") 560 reactor.callLater(3.5, self.send, "0-\\")561 565567 # this is sent when somebody performs an action that mentions the 568 # buildbot (like '/me kicks buildbot'). 'user' is the name/nick/id of 569 # the person who performed the action, so if their action provokes a 570 # response, they can be named. 571 if not data.endswith("s buildbot"): 572 return 573 words = data.split() 574 verb = words[-2] 575 timeout = 4 576 if verb == "kicks": 577 response = "%s back" % verb 578 timeout = 1 579 else: 580 response = "%s %s too" % (verb, user) 581 reactor.callLater(timeout, self.act, response)584 # this is the IRC-specific subclass of Contact 585663587 Contact.__init__(self, channel) 588 # when people send us public messages ("buildbot: command"), 589 # self.dest is the name of the channel ("#twisted"). When they send 590 # us private messages (/msg buildbot command), self.dest is their 591 # username. 592 self.dest = dest593595 if self.dest[0] == '#': 596 return "IRC user <%s> on channel %s" % (user, self.dest) 597 return "IRC user <%s> (privmsg)" % user598 599 # userJoined(self, user, channel) 600602 self.channel.msgOrNotice(self.dest, message.encode("ascii", "replace"))603605 self.channel.me(self.dest, action.encode("ascii", "replace"))606608 args = shlex.split(args) 609 to_join = args[0] 610 self.channel.join(to_join) 611 self.send("Joined %s" % to_join)612 command_JOIN.usage = "join channel - Join another channel" 613615 args = shlex.split(args) 616 to_leave = args[0] 617 self.send("Buildbot has been told to leave %s" % to_leave) 618 self.channel.part(to_leave)619 command_LEAVE.usage = "leave channel - Leave a channel" 620 621623 # a message has arrived from 'who'. For broadcast contacts (i.e. when 624 # people do an irc 'buildbot: command'), this will be a string 625 # describing the sender of the message in some useful-to-log way, and 626 # a single Contact may see messages from a variety of users. For 627 # unicast contacts (i.e. when people do an irc '/msg buildbot 628 # command'), a single Contact will only ever see messages from a 629 # single user. 630 message = message.lstrip() 631 if self.silly.has_key(message): 632 return self.doSilly(message) 633 634 parts = message.split(' ', 1) 635 if len(parts) == 1: 636 parts = parts + [''] 637 cmd, args = parts 638 log.msg("irc command", cmd) 639 640 meth = self.getCommandMethod(cmd) 641 if not meth and message[-1] == '!': 642 meth = self.command_EXCITED 643 644 error = None 645 try: 646 if meth: 647 meth(args.strip(), who) 648 except UsageError, e: 649 self.send(str(e)) 650 except: 651 f = failure.Failure() 652 log.err(f) 653 error = "Something bad happened (see logs): %s" % f.type 654 655 if error: 656 try: 657 self.send(error) 658 except: 659 log.err() 660 661 #self.say(channel, "count %d" % self.counter) 662 self.channel.counter += 1665 """I represent the buildbot's presence in a particular IM scheme. 666 667 This provides the connection to the IRC server, or represents the 668 buildbot's account with an IM service. Each Channel will have zero or 669 more Contacts associated with it. 670 """671673 """I represent the buildbot to an IRC server. 674 """ 675 implements(IChannel) 676 contactClass = IRCContact 677775 776 # we can using the following irc.IRCClient methods to send output. Most 777 # of these are used by the IRCContact class. 778 # 779 # self.say(channel, message) # broadcast 780 # self.msg(user, message) # unicast 781 # self.me(channel, action) # send action 782 # self.away(message='') 783 # self.quit(message='') 784 792678 - def __init__(self, nickname, password, channels, status, categories, notify_events, noticeOnChannel = False, showBlameList = False):679 """ 680 @type nickname: string 681 @param nickname: the nickname by which this bot should be known 682 @type password: string 683 @param password: the password to use for identifying with Nickserv 684 @type channels: list of strings 685 @param channels: the bot will maintain a presence in these channels 686 @type status: L{buildbot.status.builder.Status} 687 @param status: the build master's Status object, through which the 688 bot retrieves all status information 689 """ 690 self.nickname = nickname 691 self.channels = channels 692 self.password = password 693 self.status = status 694 self.categories = categories 695 self.notify_events = notify_events 696 self.counter = 0 697 self.hasQuit = 0 698 self.contacts = {} 699 self.noticeOnChannel = noticeOnChannel 700 self.showBlameList = showBlameList701703 if self.noticeOnChannel and dest[0] == '#': 704 self.notice(dest, message) 705 else: 706 self.msg(dest, message)707709 self.contacts[name] = contact710712 if name in self.contacts: 713 return self.contacts[name] 714 new_contact = self.contactClass(self, name) 715 self.contacts[name] = new_contact 716 return new_contact717719 name = contact.getName() 720 if name in self.contacts: 721 assert self.contacts[name] == contact 722 del self.contacts[name]723725 log.msg("%s: %s" % (self, msg))726 727 728 # the following irc.IRCClient methods are called when we have input 729731 user = user.split('!', 1)[0] # rest is ~user@hostname 732 # channel is '#twisted' or 'buildbot' (for private messages) 733 channel = channel.lower() 734 #print "privmsg:", user, channel, message 735 if channel == self.nickname: 736 # private message 737 contact = self.getContact(user) 738 contact.handleMessage(message, user) 739 return 740 # else it's a broadcast message, maybe for us, maybe not. 'channel' 741 # is '#twisted' or the like. 742 contact = self.getContact(channel) 743 if message.startswith("%s:" % self.nickname) or message.startswith("%s," % self.nickname): 744 message = message[len("%s:" % self.nickname):] 745 contact.handleMessage(message, user)746 # to track users comings and goings, add code here 747749 #log.msg("action: %s,%s,%s" % (user, channel, data)) 750 user = user.split('!', 1)[0] # rest is ~user@hostname 751 # somebody did an action (/me actions) in the broadcast channel 752 contact = self.getContact(channel) 753 if "buildbot" in data: 754 contact.handleAction(data, user)755 756 757759 if self.password: 760 self.msg("Nickserv", "IDENTIFY " + self.password) 761 for c in self.channels: 762 self.join(c)763765 self.log("I have joined %s" % (channel,)) 766 # trigger contact contructor, which in turn subscribes to notify events 767 self.getContact(channel)768770 self.log("I have left %s" % (channel,))794 protocol = IrcStatusBot 795 796 status = None 797 control = None 798 shuttingDown = False 799 p = None 800848 849801 - def __init__(self, nickname, password, channels, categories, notify_events, noticeOnChannel = False, showBlameList = False):802 #ThrottledClientFactory.__init__(self) # doesn't exist 803 self.status = None 804 self.nickname = nickname 805 self.password = password 806 self.channels = channels 807 self.categories = categories 808 self.notify_events = notify_events 809 self.noticeOnChannel = noticeOnChannel 810 self.showBlameList = showBlameList811 816818 self.shuttingDown = True 819 if self.p: 820 self.p.quit("buildmaster reconfigured: bot disconnecting")821823 p = self.protocol(self.nickname, self.password, 824 self.channels, self.status, 825 self.categories, self.notify_events, 826 noticeOnChannel = self.noticeOnChannel, 827 showBlameList = self.showBlameList) 828 p.factory = self 829 p.status = self.status 830 p.control = self.control 831 self.p = p 832 return p833 834 # TODO: I think a shutdown that occurs while the connection is being 835 # established will make this explode 836838 if self.shuttingDown: 839 log.msg("not scheduling reconnection attempt") 840 return 841 ThrottledClientFactory.clientConnectionLost(self, connector, reason)842844 if self.shuttingDown: 845 log.msg("not scheduling reconnection attempt") 846 return 847 ThrottledClientFactory.clientConnectionFailed(self, connector, reason)851 """I am an IRC bot which can be queried for status information. I 852 connect to a single IRC server and am known by a single nickname on that 853 server, however I can join multiple channels.""" 854 855 compare_attrs = ["host", "port", "nick", "password", 856 "channels", "allowForce", 857 "categories"] 858893 894 895 ## buildbot: list builders 896 # buildbot: watch quick 897 # print notification when current build in 'quick' finishes 898 ## buildbot: status 899 ## buildbot: status full-2.3 900 ## building, not, % complete, ETA 901 ## buildbot: force build full-2.3 "reason" 902859 - def __init__(self, host, nick, channels, port=6667, allowForce=True, 860 categories=None, password=None, notify_events={}, 861 noticeOnChannel = False, showBlameList = True):862 base.StatusReceiverMultiService.__init__(self) 863 864 assert allowForce in (True, False) # TODO: implement others 865 866 # need to stash these so we can detect changes later 867 self.host = host 868 self.port = port 869 self.nick = nick 870 self.channels = channels 871 self.password = password 872 self.allowForce = allowForce 873 self.categories = categories 874 self.notify_events = notify_events 875 log.msg('Notify events %s' % notify_events) 876 self.f = IrcStatusFactory(self.nick, self.password, 877 self.channels, self.categories, self.notify_events, 878 noticeOnChannel = noticeOnChannel, 879 showBlameList = showBlameList) 880 c = internet.TCPClient(self.host, self.port, self.f) 881 c.setServiceParent(self)882884 base.StatusReceiverMultiService.setServiceParent(self, parent) 885 self.f.status = parent.getStatus() 886 if self.allowForce: 887 self.f.control = interfaces.IControl(parent)888890 # make sure the factory will stop reconnecting 891 self.f.shutdown() 892 return base.StatusReceiverMultiService.stopService(self)
Home | Trees | Indices | Help |
|
---|
Generated by Epydoc 3.0.1 on Thu Jan 21 21:26:38 2010 | http://epydoc.sourceforge.net |