master/contrib/webhook_status.py
author ffxbld
Thu, 08 Jan 2015 23:25:35 -0500
branchproduction-0.8
changeset 1221 3d67bdd68e436955cba59a437572dbf3e251fe33
parent 97 c2e02e5bbfdb1c7a463cb44d75b06d45070597d3
permissions -rw-r--r--
Added FIREFOX_35_0_RELEASE FIREFOX_35_0_BUILD3 tag(s) for changeset production-0.8. DONTBUILD CLOSED TREE a=release

import urllib

from twisted.python import log
from twisted.internet import reactor
from twisted.web import client, error

from buildbot import status

MAX_ATTEMPTS     = 10
RETRY_MULTIPLIER = 5

class WebHookTransmitter(status.base.StatusReceiverMultiService):
    """
    A webhook status listener for buildbot.

    WebHookTransmitter listens for build events and sends the events
    as http POSTs to one or more webhook URLs.

    The easiest way to deploy this is to place it next to your
    master.cfg and do something like this (assuming you've got a
    postbin URL for purposes of demonstration):

      from webhook_status import WebHookTransmitter
      c['status'].append(WebHookTransmitter('http://www.postbin.org/xxxxxxx'))

    Alternatively, you may provide a list of URLs and each one will
    receive information on every event.

    The following optional parameters influence when and what data is
    transmitted:

    categories:       If provided, only events belonging to one of the
                      categories listed will be transmitted.

    extra_params:     Additional parameters to be supplied with every request.

    max_attempts:     The maximum number of times to retry transmission
                      on failure.          Default: 10

    retry_multiplier: A value multiplied by the retry number to wait before
                      attempting a retry.  Default 5
    """

    agent = 'buildbot webhook'

    def __init__(self, url, categories=None, extra_params={},
                 max_attempts=MAX_ATTEMPTS, retry_multiplier=RETRY_MULTIPLIER):
        status.base.StatusReceiverMultiService.__init__(self)
        if isinstance(url, basestring):
            self.urls = [url]
        else:
            self.urls = url
        self.categories = categories
        self.extra_params = extra_params
        self.max_attempts = max_attempts
        self.retry_multiplier = retry_multiplier

    def _transmit(self, event, params={}):

        cat = dict(params).get('category', None)
        if (cat and self.categories) and cat not in self.categories:
            log.msg("Ignoring request for unhandled category:  %s" % cat)
            return

        new_params = [('event', event)]
        new_params.extend(list(self.extra_params.items()))
        if hasattr(params, "items"):
            new_params.extend(params.items())
        else:
            new_params.extend(params)
        encoded_params = urllib.urlencode(new_params)

        log.msg("WebHookTransmitter announcing a %s event" % event)
        for u in self.urls:
            self._retrying_fetch(u, encoded_params, event, 0)

    def _retrying_fetch(self, u, data, event, attempt):
        d = client.getPage(u, method='POST', agent=self.agent,
                           postdata=data, followRedirect=0)

        def _maybe_retry(e):
            log.err()
            if attempt < self.max_attempts:
                reactor.callLater(attempt * self.retry_multiplier,
                                  self._retrying_fetch, u, data, event, attempt + 1)
            else:
                return e

        def _trap_status(x, *acceptable):
            x.trap(error.Error)
            if int(x.value.status) in acceptable:
                log.msg("Terminating retries of event %s with a %s response"
                        % (event, x.value.status))
                return None
            else:
                return x

        # Any sort of redirect is considered success
        d.addErrback(lambda x: x.trap(error.PageRedirect))

        # Any of these status values are considered delivered, or at
        # least not something that should be retried.
        d.addErrback(_trap_status,
                     # These are all actually successes
                     201, 202, 204,
                     # These tell me I'm sending stuff it doesn't want.
                     400, 401, 403, 405, 406, 407, 410, 413, 414, 415,
                     # This tells me the server can't deal with what I sent
                     501)

        d.addCallback(lambda x: log.msg("Completed %s event hook on attempt %d" %
                                        (event, attempt+1)))
        d.addErrback(_maybe_retry)
        d.addErrback(lambda e: log.err("Giving up delivering %s to %s" % (event, u)))

    def builderAdded(self, builderName, builder):
        builder.subscribe(self)
        self._transmit('builderAdded',
                       {'builder': builderName,
                        'category': builder.getCategory()})

    def builderRemoved(self, builderName, builder):
        self._transmit('builderRemoved',
                       {'builder': builderName,
                        'category': builder.getCategory()})

    def buildStarted(self, builderName, build):
        build.subscribe(self)

        args = {'builder': builderName,
                'category': build.getBuilder().getCategory(),
                'reason': build.getReason(),
                'revision': build.getSourceStamp().revision,
                'buildNumber': build.getNumber()}

        if build.getSourceStamp().patch:
            args['patch'] = build.getSourceStamp().patch[1]

        self._transmit('buildStarted', args)

    def buildFinished(self, builderName, build, results):
        self._transmit('buildFinished',
                       {'builder': builderName,
                        'category': build.getBuilder().getCategory(),
                        'result': status.builder.Results[results],
                        'revision': build.getSourceStamp().revision,
                        'had_patch': bool(build.getSourceStamp().patch),
                        'buildNumber': build.getNumber()})

    def stepStarted(self, build, step):
        step.subscribe(self)
        self._transmit('stepStarted',
                       [('builder', build.getBuilder().getName()),
                        ('category', build.getBuilder().getCategory()),
                        ('buildNumber', build.getNumber()),
                        ('step', step.getName())])

    def stepFinished(self, build, step, results):
        gu = self.status.getURLForThing
        self._transmit('stepFinished',
                       [('builder', build.getBuilder().getName()),
                        ('category', build.getBuilder().getCategory()),
                        ('buildNumber', build.getNumber()),
                        ('resultStatus', status.builder.Results[results[0]]),
                        ('resultString', ' '.join(results[1])),
                        ('step', step.getName())]
                       + [('logFile', gu(l)) for l in step.getLogs()])

    def _subscribe(self):
        self.status.subscribe(self)

    def setServiceParent(self, parent):
        status.base.StatusReceiverMultiService.setServiceParent(self, parent)
        self.status = parent.getStatus()

        self._transmit('startup')

        self._subscribe()