1# This file is part of Buildbot. Buildbot is free software: you can 2# redistribute it and/or modify it under the terms of the GNU General Public 3# License as published by the Free Software Foundation, version 2. 4# 5# This program is distributed in the hope that it will be useful, but WITHOUT 6# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 7# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more 8# details. 9# 10# You should have received a copy of the GNU General Public License along with 11# this program; if not, write to the Free Software Foundation, Inc., 51 12# Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 13# 14# Copyright Buildbot Team Members 15# 16# code inspired/copied from contrib/github_buildbot 17# and inspired from code from the Chromium project 18# otherwise, Andrew Melo <andrew.melo@gmail.com> wrote the rest 19# but "the rest" is pretty minimal 20 21import re 22from datetime import datetime 23 24from twisted.internet import defer 25from twisted.python import log 26from twisted.web import server 27 28from buildbot.plugins.db import get_plugins 29from buildbot.util import bytes2unicode 30from buildbot.util import datetime2epoch 31from buildbot.util import unicode2bytes 32from buildbot.www import resource 33 34 35class ChangeHookResource(resource.Resource): 36 # this is a cheap sort of template thingy 37 contentType = "text/html; charset=utf-8" 38 children = {} 39 needsReconfig = True 40 41 def __init__(self, dialects=None, master=None): 42 """ 43 The keys of 'dialects' select a modules to load under 44 master/buildbot/www/hooks/ 45 The value is passed to the module's getChanges function, providing 46 configuration options to the dialect. 47 """ 48 super().__init__(master) 49 50 if dialects is None: 51 dialects = {} 52 self.dialects = dialects 53 self._dialect_handlers = {} 54 self.request_dialect = None 55 self._plugins = get_plugins("webhooks") 56 57 def reconfigResource(self, new_config): 58 self.dialects = new_config.www.get('change_hook_dialects', {}) 59 60 def getChild(self, name, request): 61 return self 62 63 def render_GET(self, request): 64 """ 65 Responds to events and starts the build process 66 different implementations can decide on what methods they will accept 67 """ 68 return self.render_POST(request) 69 70 def render_POST(self, request): 71 """ 72 Responds to events and starts the build process 73 different implementations can decide on what methods they will accept 74 75 :arguments: 76 request 77 the http request object 78 """ 79 try: 80 d = self.getAndSubmitChanges(request) 81 except Exception: 82 d = defer.fail() 83 84 def ok(_): 85 request.setResponseCode(202) 86 request.finish() 87 88 def err(why): 89 code = 500 90 if why.check(ValueError): 91 code = 400 92 msg = unicode2bytes(why.getErrorMessage()) 93 else: 94 log.err(why, "adding changes from web hook") 95 msg = b'Error processing changes.' 96 request.setResponseCode(code, msg) 97 request.write(msg) 98 request.finish() 99 100 d.addCallbacks(ok, err) 101 102 return server.NOT_DONE_YET 103 104 @defer.inlineCallbacks 105 def getAndSubmitChanges(self, request): 106 changes, src = yield self.getChanges(request) 107 if not changes: 108 request.write(b"no change found") 109 else: 110 yield self.submitChanges(changes, request, src) 111 request.write(unicode2bytes("{} change found".format(len(changes)))) 112 113 def makeHandler(self, dialect): 114 """create and cache the handler object for this dialect""" 115 if dialect not in self.dialects: 116 m = "The dialect specified, '{}', wasn't whitelisted in change_hook".format(dialect) 117 log.msg(m) 118 log.msg("Note: if dialect is 'base' then it's possible your URL is " 119 "malformed and we didn't regex it properly") 120 raise ValueError(m) 121 122 if dialect not in self._dialect_handlers: 123 if dialect not in self._plugins: 124 m = ("The dialect specified, '{}', is not registered as " 125 "a buildbot.webhook plugin").format(dialect) 126 log.msg(m) 127 raise ValueError(m) 128 options = self.dialects[dialect] 129 if isinstance(options, dict) and 'custom_class' in options: 130 klass = options['custom_class'] 131 else: 132 klass = self._plugins.get(dialect) 133 self._dialect_handlers[dialect] = klass(self.master, self.dialects[dialect]) 134 135 return self._dialect_handlers[dialect] 136 137 @defer.inlineCallbacks 138 def getChanges(self, request): 139 """ 140 Take the logic from the change hook, and then delegate it 141 to the proper handler 142 143 We use the buildbot plugin mechanisms to find out about dialects 144 145 and call getChanges() 146 147 the return value is a list of changes 148 149 if DIALECT is unspecified, a sample implementation is provided 150 """ 151 uriRE = re.search(r'^/change_hook/?([a-zA-Z0-9_]*)', bytes2unicode(request.uri)) 152 153 if not uriRE: 154 msg = "URI doesn't match change_hook regex: {}".format(request.uri) 155 log.msg(msg) 156 raise ValueError(msg) 157 158 changes = [] 159 src = None 160 161 # Was there a dialect provided? 162 if uriRE.group(1): 163 dialect = uriRE.group(1) 164 else: 165 dialect = 'base' 166 167 handler = self.makeHandler(dialect) 168 changes, src = yield handler.getChanges(request) 169 return (changes, src) 170 171 @defer.inlineCallbacks 172 def submitChanges(self, changes, request, src): 173 for chdict in changes: 174 when_timestamp = chdict.get('when_timestamp') 175 if isinstance(when_timestamp, datetime): 176 chdict['when_timestamp'] = datetime2epoch(when_timestamp) 177 # unicodify stuff 178 for k in ('comments', 'author', 'committer', 'revision', 'branch', 'category', 179 'revlink', 'repository', 'codebase', 'project'): 180 if k in chdict: 181 chdict[k] = bytes2unicode(chdict[k]) 182 if chdict.get('files'): 183 chdict['files'] = [bytes2unicode(f) 184 for f in chdict['files']] 185 if chdict.get('properties'): 186 chdict['properties'] = dict((bytes2unicode(k), v) 187 for k, v in chdict['properties'].items()) 188 chid = yield self.master.data.updates.addChange(src=bytes2unicode(src), **chdict) 189 log.msg("injected change {}".format(chid)) 190