|
# This file is part of Buildbot. Buildbot is free software: you can # redistribute it and/or modify it under the terms of the GNU General Public # License as published by the Free Software Foundation, version 2. # # This program is distributed in the hope that it will be useful, but WITHOUT # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS # FOR A PARTICULAR PURPOSE. See the GNU General Public License for more # details. # # You should have received a copy of the GNU General Public License along with # this program; if not, write to the Free Software Foundation, Inc., 51 # Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. # # Copyright Buildbot Team Members
# twisted.internet.ssl requires PyOpenSSL, so be resilient if it's missing have_ssl = True
irc_colors = [ 'WHITE', 'BLACK', 'NAVY_BLUE', 'GREEN', 'RED', 'BROWN', 'PURPLE', 'OLIVE', 'YELLOW', 'LIME_GREEN', 'TEAL', 'AQUA_LIGHT', 'ROYAL_BLUE', 'HOT_PINK', 'DARK_GRAY', 'LIGHT_GRAY' ]
if useColors: return "%c%d%s%c" % (3, irc_colors.index(color), text, 3) else: return text
["builder", None, None, "which Builder to start"], ["branch", None, None, "which branch to build"], ["revision", None, None, "which revision to build"], ["reason", None, None, "the reason for starting the build"], ["props", None, None, "A set of properties made available in the build environment, " "format is --properties=prop1=value1,prop2=value2,.. " "option can be specified multiple times."], ]
args = list(args) if len(args) > 0: if self['builder'] is not None: raise UsageError("--builder provided in two ways") self['builder'] = args.pop(0) if len(args) > 0: if self['reason'] is not None: raise UsageError("--reason provided in two ways") self['reason'] = " ".join(args)
self.parent = parent self.useRevisions = useRevisions self.useColors = useColors self.timer = reactor.callLater(5, self.soon)
del self.timer if not self.hasStarted: self.parent.send("The build has been queued, I'll give a shout" " when it starts")
self.hasStarted = True if self.timer: self.timer.cancel() del self.timer eta = s.getETA() if self.useRevisions: response = "build containing revision(s) [%s] forced" % s.getRevisions() else: 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)
"""I hold the state for a single user's interaction with the buildbot.
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). """
# 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.
# silliness
"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 ...."], }
try: b = self.bot.status.getBuilder(which) except KeyError: raise UsageError, "no such builder '%s'" % which return b
if not self.bot.control: raise UsageError("builder control is not enabled") try: bc = self.bot.control.getBuilder(which) except KeyError: raise UsageError("no such builder '%s'" % which) return bc
""" @rtype: list of L{buildbot.process.builder.Builder} """ names = self.bot.status.getBuilderNames(categories=self.bot.categories) names.sort() builders = [self.bot.status.getBuilder(n) for n in names] return builders
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)
"""Returns True if this build should be reported for this contact (eliminating duplicates), and also records the report for later""" for w, b, n in self.reported_builds: if b == builder and n == buildnum: return False self.reported_builds.append([util.now(), builder, buildnum])
# clean the reported builds horizon = util.now() - 60 while self.reported_builds and self.reported_builds[0][0] < horizon: self.reported_builds.pop(0)
# and return True, since this is a new one return True
self.send("yes?")
self.send("buildbot-%s at your service" % version)
args = shlex.split(args) 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
args = shlex.split(args) 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)
raise UsageError("try 'notify on|off <EVENT>'")
self.send( "The following events are being notified: %r" % self.notify_events.keys() )
for event in events: if self.notify_events.has_key(event): return 1 return 0
self.bot.status.unsubscribe(self) self.subscribed = 0
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()
self.notify_events = {}
if self.subscribed: self.unsubscribe_from_build_events()
args = shlex.split(args)
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>'")
args = shlex.split(args) 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) if self.useRevisions: r = "watching build %s containing revision(s) [%s] until it finishes" \ % (which, build.getRevisions()) else: 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)
if (self.bot.categories != None and builder.category not in self.bot.categories): return
log.msg('[Contact] Builder %s added' % (builder)) builder.subscribe(self)
log.msg('[Contact] Builder %s removed' % (builderName))
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.bot.categories != None and builder.category not in self.bot.categories): log.msg('Not notifying for a build in the wrong category') return
if not self.notify_for('started'): return
if self.useRevisions: r = "build containing revision(s) [%s] on %s started" % \ (build.getRevisions(), builder.getName()) else: r = "build #%d of %s started, including [%s]" % \ (build.getNumber(), builder.getName(), ", ".join([str(c.revision) for c in build.getChanges()]) )
self.send(r)
SUCCESS: ("Success", 'GREEN'), WARNINGS: ("Warnings", 'YELLOW'), FAILURE: ("Failure", 'RED'), EXCEPTION: ("Exception", 'PURPLE'), RETRY: ("Retry", 'AQUA_LIGHT'), }
return self.results_descriptions.get(results, ("??",'RED'))
builder = build.getBuilder()
if (self.bot.categories != None and builder.category not in self.bot.categories): return
if not self.notify_for_finished(build): return
builder_name = builder.getName() buildnum = build.getNumber() buildrevs = build.getRevisions()
results = self.getResultsDescriptionAndColor(build.getResults()) if self.reportBuild(builder_name, buildnum): if self.useRevisions: r = "build containing revision(s) [%s] on %s is complete: %s" % \ (buildrevs, builder_name, results[0]) else: r = "build #%d of %s is complete: %s" % \ (buildnum, builder_name, results[0])
r += ' [%s]' % maybeColorize(" ".join(build.getText()), results[1], self.useColors) buildurl = self.bot.status.getURLForThing(build) if buildurl: r += " Build details are at %s" % buildurl
if self.bot.showBlameList and build.getResults() != SUCCESS and len(build.changes) != 0: r += ' blamelist: ' + ', '.join(list(set([c.who for c in build.changes])))
self.send(r)
results = build.getResults()
if self.notify_for('finished'): return True
if self.notify_for(lower(self.results_descriptions.get(results)[0])): return True
prevBuild = build.getPreviousBuild() if prevBuild: prevResult = prevBuild.getResults()
required_notification_control_string = join((lower(self.results_descriptions.get(prevResult)[0]), \ 'To', \ capitalize(self.results_descriptions.get(results)[0])), \ '')
if (self.notify_for(required_notification_control_string)): return True
return False
# only notify about builders we are interested in builder = b.getBuilder() if (self.bot.categories != None and builder.category not in self.bot.categories): return
builder_name = builder.getName() buildnum = b.getNumber() buildrevs = b.getRevisions()
results = self.getResultsDescriptionAndColor(b.getResults()) if self.reportBuild(builder_name, buildnum): if self.useRevisions: r = "Hey! build %s containing revision(s) [%s] is complete: %s" % \ (builder_name, buildrevs, results[0]) else: r = "Hey! build %s #%d is complete: %s" % \ (builder_name, buildnum, results[0])
r += ' [%s]' % maybeColorize(" ".join(b.getText()), results[1], self.useColors) self.send(r) buildurl = self.bot.status.getURLForThing(b) if buildurl: self.send("Build details are at %s" % buildurl)
errReply = "try 'force build [--branch=BRANCH] [--revision=REVISION] [--props=PROP1=VAL1,PROP2=VAL2...] <WHICH> <REASON>'" args = shlex.split(args) if not args: raise UsageError(errReply) what = args.pop(0) if what != "build": raise UsageError(errReply) opts = ForceOptions() opts.parseOptions(args)
which = opts['builder'] branch = opts['branch'] revision = opts['revision'] reason = opts['reason'] props = opts['props']
if which is None: raise UsageError("you must provide a Builder, " + errReply)
# keep weird stuff out of the branch, revision, and properties args. branch_validate = self.master.config.validation['branch'] revision_validate = self.master.config.validation['revision'] pname_validate = self.master.config.validation['property_name'] pval_validate = self.master.config.validation['property_value'] if branch and not branch_validate.match(branch): log.msg("bad branch '%s'" % branch) self.send("sorry, bad branch '%s'" % branch) return if revision and not revision_validate.match(revision): log.msg("bad revision '%s'" % revision) self.send("sorry, bad revision '%s'" % revision) return
properties = Properties() if props: # split props into name:value dict pdict = {} propertylist = props.split(",") for i in range(0,len(propertylist)): splitproperty = propertylist[i].split("=", 1) pdict[splitproperty[0]] = splitproperty[1]
# set properties for prop in pdict: pname = prop pvalue = pdict[prop] if not pname_validate.match(pname) \ or not pval_validate.match(pvalue): log.msg("bad property name='%s', value='%s'" % (pname, pvalue)) self.send("sorry, bad property name='%s', value='%s'" % (pname, pvalue)) return properties.setProperty(pname, pvalue, "Force Build IRC")
bc = self.getControl(which)
reason = "forced: by %s: %s" % (self.describeUser(who), reason) ss = SourceStamp(branch=branch, revision=revision) d = bc.submitBuildRequest(ss, reason, props=properties.asDict()) def subscribe(buildreq): ireq = IrcBuildRequest(self, self.useRevisions) buildreq.subscribe(ireq.started) d.addCallback(subscribe) d.addErrback(log.err, "while forcing a build")
args = shlex.split(args) 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() revs = build.getRevisions()
# obtain the BuildControl object buildcontrol = buildercontrol.getBuild(num)
# make it stop buildcontrol.stopBuild(r)
if self.useRevisions: response = "build containing revision(s) [%s] interrupted" % revs else: response = "build %d interrupted" % num self.send(response)
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)
args = shlex.split(args)
if len(args) == 0: which = "all" elif len(args) == 1: which = args[0] else: raise UsageError, "try 'last <builder>'"
def emit_last(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))
if which == "all": builders = self.getAllBuilders() for b in builders: emit_last(b.name) return emit_last(which)
if self.dest[0] == '#': return "IRC user <%s> on channel %s" % (user, self.dest) return "IRC user <%s> (privmsg)" % user
# commands
# The order of these is important! ;)
# The order of these is important! ;) else:
"or 'commands' for a command list)") else:
"https://github.com/buildbot/buildbot")
# communication with the user
# main dispatchers for incoming messages
# 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.
def usageError(f): def logErr(f): return defer.succeed(None)
# 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. This is 100% silly. else:
"""I represent the buildbot to an IRC server. """
categories, notify_events, noticeOnChannel=False, useRevisions=False, showBlameList=False, useColors=True):
irc.IRCClient.connectionMade(self) self._keepAliveCall.start(60)
if self._keepAliveCall.running: self._keepAliveCall.stop() irc.IRCClient.connectionLost(self, reason)
else:
# the following irc.IRCClient methods are called when we have input
# channel is '#twisted' or 'buildbot' (for private messages) # private message # else it's a broadcast message, maybe for us, maybe not. 'channel' # is '#twisted' or the like.
# somebody did an action (/me actions) in the broadcast channel
else:
# trigger contact contructor, which in turn subscribes to notify events
kicker, message))
reactor.callLater(self.lostDelay, connector.connect)
reactor.callLater(self.failedDelay, connector.connect)
noticeOnChannel=False, useRevisions=False, showBlameList=False, lostDelay=None, failedDelay=None, useColors=True): failedDelay=failedDelay)
d = self.__dict__.copy() del d['p'] return d
self.p.quit("buildmaster reconfigured: bot disconnecting")
self.channels, self.pm_to_nicks, self.status, self.categories, self.notify_events, noticeOnChannel = self.noticeOnChannel, useColors = self.useColors, useRevisions = self.useRevisions, showBlameList = self.showBlameList)
# TODO: I think a shutdown that occurs while the connection is being # established will make this explode
if self.shuttingDown: log.msg("not scheduling reconnection attempt") return ThrottledClientFactory.clientConnectionLost(self, connector, reason)
if self.shuttingDown: log.msg("not scheduling reconnection attempt") return ThrottledClientFactory.clientConnectionFailed(self, connector, reason)
"channels", "pm_to_nicks", "allowForce", "useSSL", "useRevisions", "categories", "useColors", "lostDelay", "failedDelay"]
allowForce=False, categories=None, password=None, notify_events={}, noticeOnChannel = False, showBlameList = True, useRevisions=False, useSSL=False, lostDelay=None, failedDelay=None, useColors=True):
# need to stash these so we can detect changes later
self.channels, self.pm_to_nicks, self.categories, self.notify_events, noticeOnChannel = noticeOnChannel, useRevisions = useRevisions, showBlameList = showBlameList, lostDelay = lostDelay, failedDelay = failedDelay, useColors = useColors)
# SSL client needs a ClientContextFactory for some SSL mumbo-jumbo if not have_ssl: raise RuntimeError("useSSL requires PyOpenSSL") cf = ssl.ClientContextFactory() c = internet.SSLClient(self.host, self.port, self.f, cf) else:
base.StatusReceiverMultiService.setServiceParent(self, parent) self.f.status = parent if self.allowForce: self.f.control = interfaces.IControl(self.master)
# make sure the factory will stop reconnecting
|