diff options
Diffstat (limited to 'buildbot/contrib/bzr_buildbot.py')
-rwxr-xr-x | buildbot/contrib/bzr_buildbot.py | 467 |
1 files changed, 467 insertions, 0 deletions
diff --git a/buildbot/contrib/bzr_buildbot.py b/buildbot/contrib/bzr_buildbot.py new file mode 100755 index 0000000..cc32350 --- /dev/null +++ b/buildbot/contrib/bzr_buildbot.py @@ -0,0 +1,467 @@ +# Copyright (C) 2008-2009 Canonical +# +# This program 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, either version 3 of the License, or +# (at your option) any later version. +# +# 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, see <http://www.gnu.org/licenses/>. + +"""\ +bzr buildbot integration +======================== + +This file contains both bzr commit/change hooks and a bzr poller. + +------------ +Requirements +------------ + +This has been tested with buildbot 0.7.9, bzr 1.10, and Twisted 8.1.0. It +should work in subsequent releases. + +For the hook to work, Twisted must be installed in the same Python that bzr +uses. + +----- +Hooks +----- + +To install, put this file in a bzr plugins directory (e.g., +~/.bazaar/plugins). Then, in one of your bazaar conf files (e.g., +~/.bazaar/locations.conf), set the location you want to connect with buildbot +with these keys: + +- buildbot_on: one of 'commit', 'push, or 'change'. Turns the plugin on to + report changes via commit, changes via push, or any changes to the trunk. + 'change' is recommended. + +- buildbot_server: (required to send to a buildbot master) the URL of the + buildbot master to which you will connect (as of this writing, the same + server and port to which slaves connect). + +- buildbot_port: (optional, defaults to 9989) the port of the buildbot master + to which you will connect (as of this writing, the same server and port to + which slaves connect) + +- buildbot_pqm: (optional, defaults to not pqm) Normally, the user that + commits the revision is the user that is responsible for the change. When + run in a pqm (Patch Queue Manager, see https://launchpad.net/pqm) + environment, the user that commits is the Patch Queue Manager, and the user + that committed the *parent* revision is responsible for the change. To turn + on the pqm mode, set this value to any of (case-insensitive) "Yes", "Y", + "True", or "T". + +- buildbot_dry_run: (optional, defaults to not a dry run) Normally, the + post-commit hook will attempt to communicate with the configured buildbot + server and port. If this parameter is included and any of (case-insensitive) + "Yes", "Y", "True", or "T", then the hook will simply print what it would + have sent, but not attempt to contact the buildbot master. + +- buildbot_send_branch_name: (optional, defaults to not sending the branch + name) If your buildbot's bzr source build step uses a repourl, do + *not* turn this on. If your buildbot's bzr build step uses a baseURL, then + you may set this value to any of (case-insensitive) "Yes", "Y", "True", or + "T" to have the buildbot master append the branch name to the baseURL. + +When buildbot no longer has a hardcoded password, it will be a configuration +option here as well. + +------ +Poller +------ + +Put this file somewhere that your buildbot configuration can import it. Even +in the same directory as the master.cfg should work. Install the poller in +the buildbot configuration as with any other change source. Minimally, +provide a URL that you want to poll (bzr://, bzr+ssh://, or lp:), though make +sure the buildbot user has necessary privileges. You may also want to specify +these optional values. + +poll_interval: the number of seconds to wait between polls. Defaults to 10 + minutes. + +branch_name: any value to be used as the branch name. Defaults to None, or + specify a string, or specify the constants from this file SHORT + or FULL to get the short branch name or full branch address. + +blame_merge_author: normally, the user that commits the revision is the user + that is responsible for the change. When run in a pqm + (Patch Queue Manager, see https://launchpad.net/pqm) + environment, the user that commits is the Patch Queue + Manager, and the user that committed the merged, *parent* + revision is responsible for the change. set this value to + True if this is pointed against a PQM-managed branch. + +------------------- +Contact Information +------------------- + +Maintainer/author: gary.poster@canonical.com +""" + +try: + import buildbot.util + import buildbot.changes.base + import buildbot.changes.changes +except ImportError: + DEFINE_POLLER = False +else: + DEFINE_POLLER = True +import bzrlib.branch +import bzrlib.errors +import bzrlib.trace +import twisted.cred.credentials +import twisted.internet.base +import twisted.internet.defer +import twisted.internet.reactor +import twisted.internet.selectreactor +import twisted.internet.task +import twisted.internet.threads +import twisted.python.log +import twisted.spread.pb + + +############################################################################# +# This is the code that the poller and the hooks share. + +def generate_change(branch, + old_revno=None, old_revid=None, + new_revno=None, new_revid=None, + blame_merge_author=False): + """Return a dict of information about a change to the branch. + + Dict has keys of "files", "who", "comments", and "revision", as used by + the buildbot Change (and the PBChangeSource). + + If only the branch is given, the most recent change is returned. + + If only the new_revno is given, the comparison is expected to be between + it and the previous revno (new_revno -1) in the branch. + + Passing old_revid and new_revid is only an optimization, included because + bzr hooks usually provide this information. + + blame_merge_author means that the author of the merged branch is + identified as the "who", not the person who committed the branch itself. + This is typically used for PQM. + """ + change = {} # files, who, comments, revision; NOT branch (= branch.nick) + if new_revno is None: + new_revno = branch.revno() + if new_revid is None: + new_revid = branch.get_rev_id(new_revno) + # TODO: This falls over if this is the very first revision + if old_revno is None: + old_revno = new_revno -1 + if old_revid is None: + old_revid = branch.get_rev_id(old_revno) + repository = branch.repository + new_rev = repository.get_revision(new_revid) + if blame_merge_author: + # this is a pqm commit or something like it + change['who'] = repository.get_revision( + new_rev.parent_ids[-1]).get_apparent_author() + else: + change['who'] = new_rev.get_apparent_author() + # maybe useful to know: + # name, email = bzrtools.config.parse_username(change['who']) + change['comments'] = new_rev.message + change['revision'] = new_revno + files = change['files'] = [] + changes = repository.revision_tree(new_revid).changes_from( + repository.revision_tree(old_revid)) + for (collection, name) in ((changes.added, 'ADDED'), + (changes.removed, 'REMOVED'), + (changes.modified, 'MODIFIED')): + for info in collection: + path = info[0] + kind = info[2] + files.append(' '.join([path, kind, name])) + for info in changes.renamed: + oldpath, newpath, id, kind, text_modified, meta_modified = info + elements = [oldpath, kind,'RENAMED', newpath] + if text_modified or meta_modified: + elements.append('MODIFIED') + files.append(' '.join(elements)) + return change + +############################################################################# +# poller + +# We don't want to make the hooks unnecessarily depend on buildbot being +# installed locally, so we conditionally create the BzrPoller class. +if DEFINE_POLLER: + + FULL = object() + SHORT = object() + + + class BzrPoller(buildbot.changes.base.ChangeSource, + buildbot.util.ComparableMixin): + + compare_attrs = ['url'] + + def __init__(self, url, poll_interval=10*60, blame_merge_author=False, + branch_name=None): + # poll_interval is in seconds, so default poll_interval is 10 + # minutes. + # bzr+ssh://bazaar.launchpad.net/~launchpad-pqm/launchpad/devel/ + # works, lp:~launchpad-pqm/launchpad/devel/ doesn't without help. + if url.startswith('lp:'): + url = 'bzr+ssh://bazaar.launchpad.net/' + url[3:] + self.url = url + self.poll_interval = poll_interval + self.loop = twisted.internet.task.LoopingCall(self.poll) + self.blame_merge_author = blame_merge_author + self.branch_name = branch_name + + def startService(self): + twisted.python.log.msg("BzrPoller(%s) starting" % self.url) + buildbot.changes.base.ChangeSource.startService(self) + twisted.internet.reactor.callWhenRunning( + self.loop.start, self.poll_interval) + for change in reversed(self.parent.changes): + if change.branch == self.url: + self.last_revision = change.revision + break + else: + self.last_revision = None + self.polling = False + + def stopService(self): + twisted.python.log.msg("BzrPoller(%s) shutting down" % self.url) + self.loop.stop() + return buildbot.changes.base.ChangeSource.stopService(self) + + def describe(self): + return "BzrPoller watching %s" % self.url + + @twisted.internet.defer.inlineCallbacks + def poll(self): + if self.polling: # this is called in a loop, and the loop might + # conceivably overlap. + return + self.polling = True + try: + # On a big tree, even individual elements of the bzr commands + # can take awhile. So we just push the bzr work off to a + # thread. + try: + changes = yield twisted.internet.threads.deferToThread( + self.getRawChanges) + except (SystemExit, KeyboardInterrupt): + raise + except: + # we'll try again next poll. Meanwhile, let's report. + twisted.python.log.err() + else: + for change in changes: + yield self.addChange( + buildbot.changes.changes.Change(**change)) + self.last_revision = change['revision'] + finally: + self.polling = False + + def getRawChanges(self): + branch = bzrlib.branch.Branch.open_containing(self.url)[0] + if self.branch_name is FULL: + branch_name = self.url + elif self.branch_name is SHORT: + branch_name = branch.nick + else: # presumably a string or maybe None + branch_name = self.branch_name + changes = [] + change = generate_change( + branch, blame_merge_author=self.blame_merge_author) + if (self.last_revision is None or + change['revision'] > self.last_revision): + change['branch'] = branch_name + changes.append(change) + if self.last_revision is not None: + while self.last_revision + 1 < change['revision']: + change = generate_change( + branch, new_revno=change['revision']-1, + blame_merge_author=self.blame_merge_author) + change['branch'] = branch_name + changes.append(change) + changes.reverse() + return changes + + def addChange(self, change): + d = twisted.internet.defer.Deferred() + def _add_change(): + d.callback( + self.parent.addChange(change)) + twisted.internet.reactor.callLater(0, _add_change) + return d + +############################################################################# +# hooks + +HOOK_KEY = 'buildbot_on' +SERVER_KEY = 'buildbot_server' +PORT_KEY = 'buildbot_port' +DRYRUN_KEY = 'buildbot_dry_run' +PQM_KEY = 'buildbot_pqm' +SEND_BRANCHNAME_KEY = 'buildbot_send_branch_name' + +PUSH_VALUE = 'push' +COMMIT_VALUE = 'commit' +CHANGE_VALUE = 'change' + +def _is_true(config, key): + val = config.get_user_option(key) + return val is not None and val.lower().strip() in ( + 'y', 'yes', 't', 'true') + +def _installed_hook(branch): + value = branch.get_config().get_user_option(HOOK_KEY) + if value is not None: + value = value.strip().lower() + if value not in (PUSH_VALUE, COMMIT_VALUE, CHANGE_VALUE): + raise bzrlib.errors.BzrError( + '%s, if set, must be one of %s, %s, or %s' % ( + HOOK_KEY, PUSH_VALUE, COMMIT_VALUE, CHANGE_VALUE)) + return value + +########################## +# Work around Twisted bug. +# See http://twistedmatrix.com/trac/ticket/3591 +import operator +import socket +from twisted.internet import defer +from twisted.python import failure + +# replaces twisted.internet.thread equivalent +def _putResultInDeferred(reactor, deferred, f, args, kwargs): + """ + Run a function and give results to a Deferred. + """ + try: + result = f(*args, **kwargs) + except: + f = failure.Failure() + reactor.callFromThread(deferred.errback, f) + else: + reactor.callFromThread(deferred.callback, result) + +# would be a proposed addition. deferToThread could use it +def deferToThreadInReactor(reactor, f, *args, **kwargs): + """ + Run function in thread and return result as Deferred. + """ + d = defer.Deferred() + reactor.callInThread(_putResultInDeferred, reactor, d, f, args, kwargs) + return d + +# uses its own reactor for the threaded calls, unlike Twisted's +class ThreadedResolver(twisted.internet.base.ThreadedResolver): + def getHostByName(self, name, timeout = (1, 3, 11, 45)): + if timeout: + timeoutDelay = reduce(operator.add, timeout) + else: + timeoutDelay = 60 + userDeferred = defer.Deferred() + lookupDeferred = deferToThreadInReactor( + self.reactor, socket.gethostbyname, name) + cancelCall = self.reactor.callLater( + timeoutDelay, self._cleanup, name, lookupDeferred) + self._runningQueries[lookupDeferred] = (userDeferred, cancelCall) + lookupDeferred.addBoth(self._checkTimeout, name, lookupDeferred) + return userDeferred +########################## + +def send_change(branch, old_revno, old_revid, new_revno, new_revid, hook): + config = branch.get_config() + server = config.get_user_option(SERVER_KEY) + if not server: + bzrlib.trace.warning( + 'bzr_buildbot: ERROR. If %s is set, %s must be set', + HOOK_KEY, SERVER_KEY) + return + change = generate_change( + branch, old_revno, old_revid, new_revno, new_revid, + blame_merge_author=_is_true(config, PQM_KEY)) + if _is_true(config, SEND_BRANCHNAME_KEY): + change['branch'] = branch.nick + # as of this writing (in Buildbot 0.7.9), 9989 is the default port when + # you make a buildbot master. + port = int(config.get_user_option(PORT_KEY) or 9989) + # if dry run, stop. + if _is_true(config, DRYRUN_KEY): + bzrlib.trace.note("bzr_buildbot DRY RUN " + "(*not* sending changes to %s:%d on %s)", + server, port, hook) + keys = change.keys() + keys.sort() + for k in keys: + bzrlib.trace.note("[%10s]: %s", k, change[k]) + return + # We instantiate our own reactor so that this can run within a server. + reactor = twisted.internet.selectreactor.SelectReactor() + # See other reference to http://twistedmatrix.com/trac/ticket/3591 + # above. This line can go away with a release of Twisted that addresses + # this issue. + reactor.resolver = ThreadedResolver(reactor) + pbcf = twisted.spread.pb.PBClientFactory() + reactor.connectTCP(server, port, pbcf) + deferred = pbcf.login( + twisted.cred.credentials.UsernamePassword('change', 'changepw')) + + def sendChanges(remote): + """Send changes to buildbot.""" + bzrlib.trace.mutter("bzrbuildout sending changes: %s", change) + return remote.callRemote('addChange', change) + + deferred.addCallback(sendChanges) + + def quit(ignore, msg): + bzrlib.trace.note("bzrbuildout: %s", msg) + reactor.stop() + + def failed(failure): + bzrlib.trace.warning("bzrbuildout: FAILURE\n %s", failure) + reactor.stop() + + deferred.addCallback(quit, "SUCCESS") + deferred.addErrback(failed) + reactor.callLater(60, quit, None, "TIMEOUT") + bzrlib.trace.note( + "bzr_buildbot: SENDING CHANGES to buildbot master %s:%d on %s", + server, port, hook) + reactor.run(installSignalHandlers=False) # run in a thread when in server + +def post_commit(local_branch, master_branch, # branch is the master_branch + old_revno, old_revid, new_revno, new_revid): + if _installed_hook(master_branch) == COMMIT_VALUE: + send_change(master_branch, + old_revid, old_revid, new_revno, new_revid, COMMIT_VALUE) + +def post_push(result): + if _installed_hook(result.target_branch) == PUSH_VALUE: + send_change(result.target_branch, + result.old_revid, result.old_revid, + result.new_revno, result.new_revid, PUSH_VALUE) + +def post_change_branch_tip(result): + if _installed_hook(result.branch) == CHANGE_VALUE: + send_change(result.branch, + result.old_revid, result.old_revid, + result.new_revno, result.new_revid, CHANGE_VALUE) + +bzrlib.branch.Branch.hooks.install_named_hook( + 'post_commit', post_commit, + 'send change to buildbot master') +bzrlib.branch.Branch.hooks.install_named_hook( + 'post_push', post_push, + 'send change to buildbot master') +bzrlib.branch.Branch.hooks.install_named_hook( + 'post_change_branch_tip', post_change_branch_tip, + 'send change to buildbot master') |