Changelog update.
[debian_buildbot.git] / master / contrib / webhook_status.py
blobf9876716e2277167ad275fbcd0f2ead820e1c242
1 import urllib
3 from twisted.python import log
4 from twisted.internet import reactor
5 from twisted.web import client, error
7 from buildbot import status
9 MAX_ATTEMPTS = 10
10 RETRY_MULTIPLIER = 5
12 class WebHookTransmitter(status.base.StatusReceiverMultiService):
13 """
14 A webhook status listener for buildbot.
16 WebHookTransmitter listens for build events and sends the events
17 as http POSTs to one or more webhook URLs.
19 The easiest way to deploy this is to place it next to your
20 master.cfg and do something like this (assuming you've got a
21 postbin URL for purposes of demonstration):
23 from webhook_status import WebHookTransmitter
24 c['status'].append(WebHookTransmitter('http://www.postbin.org/xxxxxxx'))
26 Alternatively, you may provide a list of URLs and each one will
27 receive information on every event.
29 The following optional parameters influence when and what data is
30 transmitted:
32 categories: If provided, only events belonging to one of the
33 categories listed will be transmitted.
35 extra_params: Additional parameters to be supplied with every request.
37 max_attempts: The maximum number of times to retry transmission
38 on failure. Default: 10
40 retry_multiplier: A value multiplied by the retry number to wait before
41 attempting a retry. Default 5
42 """
44 agent = 'buildbot webhook'
46 def __init__(self, url, categories=None, extra_params={},
47 max_attempts=MAX_ATTEMPTS, retry_multiplier=RETRY_MULTIPLIER):
48 status.base.StatusReceiverMultiService.__init__(self)
49 if isinstance(url, basestring):
50 self.urls = [url]
51 else:
52 self.urls = url
53 self.categories = categories
54 self.extra_params = extra_params
55 self.max_attempts = max_attempts
56 self.retry_multiplier = retry_multiplier
58 def _transmit(self, event, params={}):
60 cat = dict(params).get('category', None)
61 if (cat and self.categories) and cat not in self.categories:
62 log.msg("Ignoring request for unhandled category: %s" % cat)
63 return
65 new_params = [('event', event)]
66 new_params.extend(list(self.extra_params.items()))
67 if hasattr(params, "items"):
68 new_params.extend(params.items())
69 else:
70 new_params.extend(params)
71 encoded_params = urllib.urlencode(new_params)
73 log.msg("WebHookTransmitter announcing a %s event" % event)
74 for u in self.urls:
75 self._retrying_fetch(u, encoded_params, event, 0)
77 def _retrying_fetch(self, u, data, event, attempt):
78 d = client.getPage(u, method='POST', agent=self.agent,
79 postdata=data, followRedirect=0)
81 def _maybe_retry(e):
82 log.err()
83 if attempt < self.max_attempts:
84 reactor.callLater(attempt * self.retry_multiplier,
85 self._retrying_fetch, u, data, event, attempt + 1)
86 else:
87 return e
89 def _trap_status(x, *acceptable):
90 x.trap(error.Error)
91 if int(x.value.status) in acceptable:
92 log.msg("Terminating retries of event %s with a %s response"
93 % (event, x.value.status))
94 return None
95 else:
96 return x
98 # Any sort of redirect is considered success
99 d.addErrback(lambda x: x.trap(error.PageRedirect))
101 # Any of these status values are considered delivered, or at
102 # least not something that should be retried.
103 d.addErrback(_trap_status,
104 # These are all actually successes
105 201, 202, 204,
106 # These tell me I'm sending stuff it doesn't want.
107 400, 401, 403, 405, 406, 407, 410, 413, 414, 415,
108 # This tells me the server can't deal with what I sent
109 501)
111 d.addCallback(lambda x: log.msg("Completed %s event hook on attempt %d" %
112 (event, attempt+1)))
113 d.addErrback(_maybe_retry)
114 d.addErrback(lambda e: log.err("Giving up delivering %s to %s" % (event, u)))
116 def builderAdded(self, builderName, builder):
117 builder.subscribe(self)
118 self._transmit('builderAdded',
119 {'builder': builderName,
120 'category': builder.getCategory()})
122 def builderRemoved(self, builderName, builder):
123 self._transmit('builderRemoved',
124 {'builder': builderName,
125 'category': builder.getCategory()})
127 def buildStarted(self, builderName, build):
128 build.subscribe(self)
130 args = {'builder': builderName,
131 'category': build.getBuilder().getCategory(),
132 'reason': build.getReason(),
133 'revision': build.getSourceStamp().revision,
134 'buildNumber': build.getNumber()}
136 if build.getSourceStamp().patch:
137 args['patch'] = build.getSourceStamp().patch[1]
139 self._transmit('buildStarted', args)
141 def buildFinished(self, builderName, build, results):
142 self._transmit('buildFinished',
143 {'builder': builderName,
144 'category': build.getBuilder().getCategory(),
145 'result': status.builder.Results[results],
146 'revision': build.getSourceStamp().revision,
147 'had_patch': bool(build.getSourceStamp().patch),
148 'buildNumber': build.getNumber()})
150 def stepStarted(self, build, step):
151 step.subscribe(self)
152 self._transmit('stepStarted',
153 [('builder', build.getBuilder().getName()),
154 ('category', build.getBuilder().getCategory()),
155 ('buildNumber', build.getNumber()),
156 ('step', step.getName())])
158 def stepFinished(self, build, step, results):
159 gu = self.status.getURLForThing
160 self._transmit('stepFinished',
161 [('builder', build.getBuilder().getName()),
162 ('category', build.getBuilder().getCategory()),
163 ('buildNumber', build.getNumber()),
164 ('resultStatus', status.builder.Results[results[0]]),
165 ('resultString', ' '.join(results[1])),
166 ('step', step.getName())]
167 + [('logFile', gu(l)) for l in step.getLogs()])
169 def _subscribe(self):
170 self.status.subscribe(self)
172 def setServiceParent(self, parent):
173 status.base.StatusReceiverMultiService.setServiceParent(self, parent)
174 self.status = parent.getStatus()
176 self._transmit('startup')
178 self._subscribe()