##// END OF EJS Templates
hooks: new hook support for python3
super-admin -
r5081:fb3d7403 default
parent child Browse files
Show More
@@ -1,538 +1,533 b''
1
1
2
2
3 # Copyright (C) 2013-2020 RhodeCode GmbH
3 # Copyright (C) 2013-2020 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21
21
22 """
22 """
23 Set of hooks run by RhodeCode Enterprise
23 Set of hooks run by RhodeCode Enterprise
24 """
24 """
25
25
26 import os
26 import os
27 import logging
27 import logging
28
28
29 import rhodecode
29 import rhodecode
30 from rhodecode import events
30 from rhodecode import events
31 from rhodecode.lib import helpers as h
31 from rhodecode.lib import helpers as h
32 from rhodecode.lib import audit_logger
32 from rhodecode.lib import audit_logger
33 from rhodecode.lib.utils2 import safe_str, user_agent_normalizer
33 from rhodecode.lib.utils2 import safe_str, user_agent_normalizer
34 from rhodecode.lib.exceptions import (
34 from rhodecode.lib.exceptions import (
35 HTTPLockedRC, HTTPBranchProtected, UserCreationError)
35 HTTPLockedRC, HTTPBranchProtected, UserCreationError)
36 from rhodecode.model.db import Repository, User
36 from rhodecode.model.db import Repository, User
37 from rhodecode.lib.statsd_client import StatsdClient
37 from rhodecode.lib.statsd_client import StatsdClient
38
38
39 log = logging.getLogger(__name__)
39 log = logging.getLogger(__name__)
40
40
41
41
42 class HookResponse(object):
42 class HookResponse(object):
43 def __init__(self, status, output):
43 def __init__(self, status, output):
44 self.status = status
44 self.status = status
45 self.output = output
45 self.output = output
46
46
47 def __add__(self, other):
47 def __add__(self, other):
48 other_status = getattr(other, 'status', 0)
48 other_status = getattr(other, 'status', 0)
49 new_status = max(self.status, other_status)
49 new_status = max(self.status, other_status)
50 other_output = getattr(other, 'output', '')
50 other_output = getattr(other, 'output', '')
51 new_output = self.output + other_output
51 new_output = self.output + other_output
52
52
53 return HookResponse(new_status, new_output)
53 return HookResponse(new_status, new_output)
54
54
55 def __bool__(self):
55 def __bool__(self):
56 return self.status == 0
56 return self.status == 0
57
57
58
58
59 def is_shadow_repo(extras):
59 def is_shadow_repo(extras):
60 """
60 """
61 Returns ``True`` if this is an action executed against a shadow repository.
61 Returns ``True`` if this is an action executed against a shadow repository.
62 """
62 """
63 return extras['is_shadow_repo']
63 return extras['is_shadow_repo']
64
64
65
65
66 def _get_scm_size(alias, root_path):
66 def _get_scm_size(alias, root_path):
67
67
68 if not alias.startswith('.'):
68 if not alias.startswith('.'):
69 alias += '.'
69 alias += '.'
70
70
71 size_scm, size_root = 0, 0
71 size_scm, size_root = 0, 0
72 for path, unused_dirs, files in os.walk(safe_str(root_path)):
72 for path, unused_dirs, files in os.walk(safe_str(root_path)):
73 if path.find(alias) != -1:
73 if path.find(alias) != -1:
74 for f in files:
74 for f in files:
75 try:
75 try:
76 size_scm += os.path.getsize(os.path.join(path, f))
76 size_scm += os.path.getsize(os.path.join(path, f))
77 except OSError:
77 except OSError:
78 pass
78 pass
79 else:
79 else:
80 for f in files:
80 for f in files:
81 try:
81 try:
82 size_root += os.path.getsize(os.path.join(path, f))
82 size_root += os.path.getsize(os.path.join(path, f))
83 except OSError:
83 except OSError:
84 pass
84 pass
85
85
86 size_scm_f = h.format_byte_size_binary(size_scm)
86 size_scm_f = h.format_byte_size_binary(size_scm)
87 size_root_f = h.format_byte_size_binary(size_root)
87 size_root_f = h.format_byte_size_binary(size_root)
88 size_total_f = h.format_byte_size_binary(size_root + size_scm)
88 size_total_f = h.format_byte_size_binary(size_root + size_scm)
89
89
90 return size_scm_f, size_root_f, size_total_f
90 return size_scm_f, size_root_f, size_total_f
91
91
92
92
93 # actual hooks called by Mercurial internally, and GIT by our Python Hooks
93 # actual hooks called by Mercurial internally, and GIT by our Python Hooks
94 def repo_size(extras):
94 def repo_size(extras):
95 """Present size of repository after push."""
95 """Present size of repository after push."""
96 repo = Repository.get_by_repo_name(extras.repository)
96 repo = Repository.get_by_repo_name(extras.repository)
97 vcs_part = safe_str('.%s' % repo.repo_type)
97 vcs_part = f'.{repo.repo_type}'
98 size_vcs, size_root, size_total = _get_scm_size(vcs_part,
98 size_vcs, size_root, size_total = _get_scm_size(vcs_part, repo.repo_full_path)
99 repo.repo_full_path)
99 msg = (f'RhodeCode: `{repo.repo_name}` size summary {vcs_part}:{size_vcs} repo:{size_root} total:{size_total}\n')
100 msg = ('Repository `%s` size summary %s:%s repo:%s total:%s\n'
101 % (repo.repo_name, vcs_part, size_vcs, size_root, size_total))
102 return HookResponse(0, msg)
100 return HookResponse(0, msg)
103
101
104
102
105 def pre_push(extras):
103 def pre_push(extras):
106 """
104 """
107 Hook executed before pushing code.
105 Hook executed before pushing code.
108
106
109 It bans pushing when the repository is locked.
107 It bans pushing when the repository is locked.
110 """
108 """
111
109
112 user = User.get_by_username(extras.username)
110 user = User.get_by_username(extras.username)
113 output = ''
111 output = ''
114 if extras.locked_by[0] and user.user_id != int(extras.locked_by[0]):
112 if extras.locked_by[0] and user.user_id != int(extras.locked_by[0]):
115 locked_by = User.get(extras.locked_by[0]).username
113 locked_by = User.get(extras.locked_by[0]).username
116 reason = extras.locked_by[2]
114 reason = extras.locked_by[2]
117 # this exception is interpreted in git/hg middlewares and based
115 # this exception is interpreted in git/hg middlewares and based
118 # on that proper return code is server to client
116 # on that proper return code is server to client
119 _http_ret = HTTPLockedRC(
117 _http_ret = HTTPLockedRC(
120 _locked_by_explanation(extras.repository, locked_by, reason))
118 _locked_by_explanation(extras.repository, locked_by, reason))
121 if str(_http_ret.code).startswith('2'):
119 if str(_http_ret.code).startswith('2'):
122 # 2xx Codes don't raise exceptions
120 # 2xx Codes don't raise exceptions
123 output = _http_ret.title
121 output = _http_ret.title
124 else:
122 else:
125 raise _http_ret
123 raise _http_ret
126
124
127 hook_response = ''
125 hook_response = ''
128 if not is_shadow_repo(extras):
126 if not is_shadow_repo(extras):
127
129 if extras.commit_ids and extras.check_branch_perms:
128 if extras.commit_ids and extras.check_branch_perms:
130
131 auth_user = user.AuthUser()
129 auth_user = user.AuthUser()
132 repo = Repository.get_by_repo_name(extras.repository)
130 repo = Repository.get_by_repo_name(extras.repository)
133 affected_branches = []
131 affected_branches = []
134 if repo.repo_type == 'hg':
132 if repo.repo_type == 'hg':
135 for entry in extras.commit_ids:
133 for entry in extras.commit_ids:
136 if entry['type'] == 'branch':
134 if entry['type'] == 'branch':
137 is_forced = bool(entry['multiple_heads'])
135 is_forced = bool(entry['multiple_heads'])
138 affected_branches.append([entry['name'], is_forced])
136 affected_branches.append([entry['name'], is_forced])
139 elif repo.repo_type == 'git':
137 elif repo.repo_type == 'git':
140 for entry in extras.commit_ids:
138 for entry in extras.commit_ids:
141 if entry['type'] == 'heads':
139 if entry['type'] == 'heads':
142 is_forced = bool(entry['pruned_sha'])
140 is_forced = bool(entry['pruned_sha'])
143 affected_branches.append([entry['name'], is_forced])
141 affected_branches.append([entry['name'], is_forced])
144
142
145 for branch_name, is_forced in affected_branches:
143 for branch_name, is_forced in affected_branches:
146
144
147 rule, branch_perm = auth_user.get_rule_and_branch_permission(
145 rule, branch_perm = auth_user.get_rule_and_branch_permission(
148 extras.repository, branch_name)
146 extras.repository, branch_name)
149 if not branch_perm:
147 if not branch_perm:
150 # no branch permission found for this branch, just keep checking
148 # no branch permission found for this branch, just keep checking
151 continue
149 continue
152
150
153 if branch_perm == 'branch.push_force':
151 if branch_perm == 'branch.push_force':
154 continue
152 continue
155 elif branch_perm == 'branch.push' and is_forced is False:
153 elif branch_perm == 'branch.push' and is_forced is False:
156 continue
154 continue
157 elif branch_perm == 'branch.push' and is_forced is True:
155 elif branch_perm == 'branch.push' and is_forced is True:
158 halt_message = 'Branch `{}` changes rejected by rule {}. ' \
156 halt_message = f'Branch `{branch_name}` changes rejected by rule {rule}. ' \
159 'FORCE PUSH FORBIDDEN.'.format(branch_name, rule)
157 f'FORCE PUSH FORBIDDEN.'
160 else:
158 else:
161 halt_message = 'Branch `{}` changes rejected by rule {}.'.format(
159 halt_message = f'Branch `{branch_name}` changes rejected by rule {rule}.'
162 branch_name, rule)
163
160
164 if halt_message:
161 if halt_message:
165 _http_ret = HTTPBranchProtected(halt_message)
162 _http_ret = HTTPBranchProtected(halt_message)
166 raise _http_ret
163 raise _http_ret
167
164
168 # Propagate to external components. This is done after checking the
165 # Propagate to external components. This is done after checking the
169 # lock, for consistent behavior.
166 # lock, for consistent behavior.
170 hook_response = pre_push_extension(
167 hook_response = pre_push_extension(
171 repo_store_path=Repository.base_path(), **extras)
168 repo_store_path=Repository.base_path(), **extras)
172 events.trigger(events.RepoPrePushEvent(
169 events.trigger(events.RepoPrePushEvent(
173 repo_name=extras.repository, extras=extras))
170 repo_name=extras.repository, extras=extras))
174
171
175 return HookResponse(0, output) + hook_response
172 return HookResponse(0, output) + hook_response
176
173
177
174
178 def pre_pull(extras):
175 def pre_pull(extras):
179 """
176 """
180 Hook executed before pulling the code.
177 Hook executed before pulling the code.
181
178
182 It bans pulling when the repository is locked.
179 It bans pulling when the repository is locked.
183 """
180 """
184
181
185 output = ''
182 output = ''
186 if extras.locked_by[0]:
183 if extras.locked_by[0]:
187 locked_by = User.get(extras.locked_by[0]).username
184 locked_by = User.get(extras.locked_by[0]).username
188 reason = extras.locked_by[2]
185 reason = extras.locked_by[2]
189 # this exception is interpreted in git/hg middlewares and based
186 # this exception is interpreted in git/hg middlewares and based
190 # on that proper return code is server to client
187 # on that proper return code is server to client
191 _http_ret = HTTPLockedRC(
188 _http_ret = HTTPLockedRC(
192 _locked_by_explanation(extras.repository, locked_by, reason))
189 _locked_by_explanation(extras.repository, locked_by, reason))
193 if str(_http_ret.code).startswith('2'):
190 if str(_http_ret.code).startswith('2'):
194 # 2xx Codes don't raise exceptions
191 # 2xx Codes don't raise exceptions
195 output = _http_ret.title
192 output = _http_ret.title
196 else:
193 else:
197 raise _http_ret
194 raise _http_ret
198
195
199 # Propagate to external components. This is done after checking the
196 # Propagate to external components. This is done after checking the
200 # lock, for consistent behavior.
197 # lock, for consistent behavior.
201 hook_response = ''
198 hook_response = ''
202 if not is_shadow_repo(extras):
199 if not is_shadow_repo(extras):
203 extras.hook_type = extras.hook_type or 'pre_pull'
200 extras.hook_type = extras.hook_type or 'pre_pull'
204 hook_response = pre_pull_extension(
201 hook_response = pre_pull_extension(
205 repo_store_path=Repository.base_path(), **extras)
202 repo_store_path=Repository.base_path(), **extras)
206 events.trigger(events.RepoPrePullEvent(
203 events.trigger(events.RepoPrePullEvent(
207 repo_name=extras.repository, extras=extras))
204 repo_name=extras.repository, extras=extras))
208
205
209 return HookResponse(0, output) + hook_response
206 return HookResponse(0, output) + hook_response
210
207
211
208
212 def post_pull(extras):
209 def post_pull(extras):
213 """Hook executed after client pulls the code."""
210 """Hook executed after client pulls the code."""
214
211
215 audit_user = audit_logger.UserWrap(
212 audit_user = audit_logger.UserWrap(
216 username=extras.username,
213 username=extras.username,
217 ip_addr=extras.ip)
214 ip_addr=extras.ip)
218 repo = audit_logger.RepoWrap(repo_name=extras.repository)
215 repo = audit_logger.RepoWrap(repo_name=extras.repository)
219 audit_logger.store(
216 audit_logger.store(
220 'user.pull', action_data={'user_agent': extras.user_agent},
217 'user.pull', action_data={'user_agent': extras.user_agent},
221 user=audit_user, repo=repo, commit=True)
218 user=audit_user, repo=repo, commit=True)
222
219
223 statsd = StatsdClient.statsd
220 statsd = StatsdClient.statsd
224 if statsd:
221 if statsd:
225 statsd.incr('rhodecode_pull_total', tags=[
222 statsd.incr('rhodecode_pull_total', tags=[
226 'user-agent:{}'.format(user_agent_normalizer(extras.user_agent)),
223 'user-agent:{}'.format(user_agent_normalizer(extras.user_agent)),
227 ])
224 ])
228 output = ''
225 output = ''
229 # make lock is a tri state False, True, None. We only make lock on True
226 # make lock is a tri state False, True, None. We only make lock on True
230 if extras.make_lock is True and not is_shadow_repo(extras):
227 if extras.make_lock is True and not is_shadow_repo(extras):
231 user = User.get_by_username(extras.username)
228 user = User.get_by_username(extras.username)
232 Repository.lock(Repository.get_by_repo_name(extras.repository),
229 Repository.lock(Repository.get_by_repo_name(extras.repository),
233 user.user_id,
230 user.user_id,
234 lock_reason=Repository.LOCK_PULL)
231 lock_reason=Repository.LOCK_PULL)
235 msg = 'Made lock on repo `%s`' % (extras.repository,)
232 msg = 'Made lock on repo `%s`' % (extras.repository,)
236 output += msg
233 output += msg
237
234
238 if extras.locked_by[0]:
235 if extras.locked_by[0]:
239 locked_by = User.get(extras.locked_by[0]).username
236 locked_by = User.get(extras.locked_by[0]).username
240 reason = extras.locked_by[2]
237 reason = extras.locked_by[2]
241 _http_ret = HTTPLockedRC(
238 _http_ret = HTTPLockedRC(
242 _locked_by_explanation(extras.repository, locked_by, reason))
239 _locked_by_explanation(extras.repository, locked_by, reason))
243 if str(_http_ret.code).startswith('2'):
240 if str(_http_ret.code).startswith('2'):
244 # 2xx Codes don't raise exceptions
241 # 2xx Codes don't raise exceptions
245 output += _http_ret.title
242 output += _http_ret.title
246
243
247 # Propagate to external components.
244 # Propagate to external components.
248 hook_response = ''
245 hook_response = ''
249 if not is_shadow_repo(extras):
246 if not is_shadow_repo(extras):
250 extras.hook_type = extras.hook_type or 'post_pull'
247 extras.hook_type = extras.hook_type or 'post_pull'
251 hook_response = post_pull_extension(
248 hook_response = post_pull_extension(
252 repo_store_path=Repository.base_path(), **extras)
249 repo_store_path=Repository.base_path(), **extras)
253 events.trigger(events.RepoPullEvent(
250 events.trigger(events.RepoPullEvent(
254 repo_name=extras.repository, extras=extras))
251 repo_name=extras.repository, extras=extras))
255
252
256 return HookResponse(0, output) + hook_response
253 return HookResponse(0, output) + hook_response
257
254
258
255
259 def post_push(extras):
256 def post_push(extras):
260 """Hook executed after user pushes to the repository."""
257 """Hook executed after user pushes to the repository."""
261 commit_ids = extras.commit_ids
258 commit_ids = extras.commit_ids
262
259
263 # log the push call
260 # log the push call
264 audit_user = audit_logger.UserWrap(
261 audit_user = audit_logger.UserWrap(
265 username=extras.username, ip_addr=extras.ip)
262 username=extras.username, ip_addr=extras.ip)
266 repo = audit_logger.RepoWrap(repo_name=extras.repository)
263 repo = audit_logger.RepoWrap(repo_name=extras.repository)
267 audit_logger.store(
264 audit_logger.store(
268 'user.push', action_data={
265 'user.push', action_data={
269 'user_agent': extras.user_agent,
266 'user_agent': extras.user_agent,
270 'commit_ids': commit_ids[:400]},
267 'commit_ids': commit_ids[:400]},
271 user=audit_user, repo=repo, commit=True)
268 user=audit_user, repo=repo, commit=True)
272
269
273 statsd = StatsdClient.statsd
270 statsd = StatsdClient.statsd
274 if statsd:
271 if statsd:
275 statsd.incr('rhodecode_push_total', tags=[
272 statsd.incr('rhodecode_push_total', tags=[
276 'user-agent:{}'.format(user_agent_normalizer(extras.user_agent)),
273 'user-agent:{}'.format(user_agent_normalizer(extras.user_agent)),
277 ])
274 ])
278
275
279 # Propagate to external components.
276 # Propagate to external components.
280 output = ''
277 output = ''
281 # make lock is a tri state False, True, None. We only release lock on False
278 # make lock is a tri state False, True, None. We only release lock on False
282 if extras.make_lock is False and not is_shadow_repo(extras):
279 if extras.make_lock is False and not is_shadow_repo(extras):
283 Repository.unlock(Repository.get_by_repo_name(extras.repository))
280 Repository.unlock(Repository.get_by_repo_name(extras.repository))
284 msg = 'Released lock on repo `{}`\n'.format(safe_str(extras.repository))
281 msg = f'Released lock on repo `{extras.repository}`\n'
285 output += msg
282 output += msg
286
283
287 if extras.locked_by[0]:
284 if extras.locked_by[0]:
288 locked_by = User.get(extras.locked_by[0]).username
285 locked_by = User.get(extras.locked_by[0]).username
289 reason = extras.locked_by[2]
286 reason = extras.locked_by[2]
290 _http_ret = HTTPLockedRC(
287 _http_ret = HTTPLockedRC(
291 _locked_by_explanation(extras.repository, locked_by, reason))
288 _locked_by_explanation(extras.repository, locked_by, reason))
292 # TODO: johbo: if not?
289 # TODO: johbo: if not?
293 if str(_http_ret.code).startswith('2'):
290 if str(_http_ret.code).startswith('2'):
294 # 2xx Codes don't raise exceptions
291 # 2xx Codes don't raise exceptions
295 output += _http_ret.title
292 output += _http_ret.title
296
293
297 if extras.new_refs:
294 if extras.new_refs:
298 tmpl = '{}/{}/pull-request/new?{{ref_type}}={{ref_name}}'.format(
295 tmpl = '{}/{}/pull-request/new?{{ref_type}}={{ref_name}}'.format(
299 safe_str(extras.server_url), safe_str(extras.repository))
296 safe_str(extras.server_url), safe_str(extras.repository))
300
297
301 for branch_name in extras.new_refs['branches']:
298 for branch_name in extras.new_refs['branches']:
302 output += 'RhodeCode: open pull request link: {}\n'.format(
299 pr_link = tmpl.format(ref_type='branch', ref_name=safe_str(branch_name))
303 tmpl.format(ref_type='branch', ref_name=safe_str(branch_name)))
300 output += f'RhodeCode: open pull request link: {pr_link}\n'
304
301
305 for book_name in extras.new_refs['bookmarks']:
302 for book_name in extras.new_refs['bookmarks']:
306 output += 'RhodeCode: open pull request link: {}\n'.format(
303 pr_link = tmpl.format(ref_type='bookmark', ref_name=safe_str(book_name))
307 tmpl.format(ref_type='bookmark', ref_name=safe_str(book_name)))
304 output += f'RhodeCode: open pull request link: {pr_link}\n'
308
305
309 hook_response = ''
306 hook_response = ''
310 if not is_shadow_repo(extras):
307 if not is_shadow_repo(extras):
311 hook_response = post_push_extension(
308 hook_response = post_push_extension(
312 repo_store_path=Repository.base_path(),
309 repo_store_path=Repository.base_path(),
313 **extras)
310 **extras)
314 events.trigger(events.RepoPushEvent(
311 events.trigger(events.RepoPushEvent(
315 repo_name=extras.repository, pushed_commit_ids=commit_ids, extras=extras))
312 repo_name=extras.repository, pushed_commit_ids=commit_ids, extras=extras))
316
313
317 output += 'RhodeCode: push completed\n'
314 output += 'RhodeCode: push completed\n'
318 return HookResponse(0, output) + hook_response
315 return HookResponse(0, output) + hook_response
319
316
320
317
321 def _locked_by_explanation(repo_name, user_name, reason):
318 def _locked_by_explanation(repo_name, user_name, reason):
322 message = (
319 message = f'Repository `{repo_name}` locked by user `{user_name}`. Reason:`{reason}`'
323 'Repository `%s` locked by user `%s`. Reason:`%s`'
324 % (repo_name, user_name, reason))
325 return message
320 return message
326
321
327
322
328 def check_allowed_create_user(user_dict, created_by, **kwargs):
323 def check_allowed_create_user(user_dict, created_by, **kwargs):
329 # pre create hooks
324 # pre create hooks
330 if pre_create_user.is_active():
325 if pre_create_user.is_active():
331 hook_result = pre_create_user(created_by=created_by, **user_dict)
326 hook_result = pre_create_user(created_by=created_by, **user_dict)
332 allowed = hook_result.status == 0
327 allowed = hook_result.status == 0
333 if not allowed:
328 if not allowed:
334 reason = hook_result.output
329 reason = hook_result.output
335 raise UserCreationError(reason)
330 raise UserCreationError(reason)
336
331
337
332
338 class ExtensionCallback(object):
333 class ExtensionCallback(object):
339 """
334 """
340 Forwards a given call to rcextensions, sanitizes keyword arguments.
335 Forwards a given call to rcextensions, sanitizes keyword arguments.
341
336
342 Does check if there is an extension active for that hook. If it is
337 Does check if there is an extension active for that hook. If it is
343 there, it will forward all `kwargs_keys` keyword arguments to the
338 there, it will forward all `kwargs_keys` keyword arguments to the
344 extension callback.
339 extension callback.
345 """
340 """
346
341
347 def __init__(self, hook_name, kwargs_keys):
342 def __init__(self, hook_name, kwargs_keys):
348 self._hook_name = hook_name
343 self._hook_name = hook_name
349 self._kwargs_keys = set(kwargs_keys)
344 self._kwargs_keys = set(kwargs_keys)
350
345
351 def __call__(self, *args, **kwargs):
346 def __call__(self, *args, **kwargs):
352 log.debug('Calling extension callback for `%s`', self._hook_name)
347 log.debug('Calling extension callback for `%s`', self._hook_name)
353 callback = self._get_callback()
348 callback = self._get_callback()
354 if not callback:
349 if not callback:
355 log.debug('extension callback `%s` not found, skipping...', self._hook_name)
350 log.debug('extension callback `%s` not found, skipping...', self._hook_name)
356 return
351 return
357
352
358 kwargs_to_pass = {}
353 kwargs_to_pass = {}
359 for key in self._kwargs_keys:
354 for key in self._kwargs_keys:
360 try:
355 try:
361 kwargs_to_pass[key] = kwargs[key]
356 kwargs_to_pass[key] = kwargs[key]
362 except KeyError:
357 except KeyError:
363 log.error('Failed to fetch %s key from given kwargs. '
358 log.error('Failed to fetch %s key from given kwargs. '
364 'Expected keys: %s', key, self._kwargs_keys)
359 'Expected keys: %s', key, self._kwargs_keys)
365 raise
360 raise
366
361
367 # backward compat for removed api_key for old hooks. This was it works
362 # backward compat for removed api_key for old hooks. This was it works
368 # with older rcextensions that require api_key present
363 # with older rcextensions that require api_key present
369 if self._hook_name in ['CREATE_USER_HOOK', 'DELETE_USER_HOOK']:
364 if self._hook_name in ['CREATE_USER_HOOK', 'DELETE_USER_HOOK']:
370 kwargs_to_pass['api_key'] = '_DEPRECATED_'
365 kwargs_to_pass['api_key'] = '_DEPRECATED_'
371 return callback(**kwargs_to_pass)
366 return callback(**kwargs_to_pass)
372
367
373 def is_active(self):
368 def is_active(self):
374 return hasattr(rhodecode.EXTENSIONS, self._hook_name)
369 return hasattr(rhodecode.EXTENSIONS, self._hook_name)
375
370
376 def _get_callback(self):
371 def _get_callback(self):
377 return getattr(rhodecode.EXTENSIONS, self._hook_name, None)
372 return getattr(rhodecode.EXTENSIONS, self._hook_name, None)
378
373
379
374
380 pre_pull_extension = ExtensionCallback(
375 pre_pull_extension = ExtensionCallback(
381 hook_name='PRE_PULL_HOOK',
376 hook_name='PRE_PULL_HOOK',
382 kwargs_keys=(
377 kwargs_keys=(
383 'server_url', 'config', 'scm', 'username', 'ip', 'action',
378 'server_url', 'config', 'scm', 'username', 'ip', 'action',
384 'repository', 'hook_type', 'user_agent', 'repo_store_path',))
379 'repository', 'hook_type', 'user_agent', 'repo_store_path',))
385
380
386
381
387 post_pull_extension = ExtensionCallback(
382 post_pull_extension = ExtensionCallback(
388 hook_name='PULL_HOOK',
383 hook_name='PULL_HOOK',
389 kwargs_keys=(
384 kwargs_keys=(
390 'server_url', 'config', 'scm', 'username', 'ip', 'action',
385 'server_url', 'config', 'scm', 'username', 'ip', 'action',
391 'repository', 'hook_type', 'user_agent', 'repo_store_path',))
386 'repository', 'hook_type', 'user_agent', 'repo_store_path',))
392
387
393
388
394 pre_push_extension = ExtensionCallback(
389 pre_push_extension = ExtensionCallback(
395 hook_name='PRE_PUSH_HOOK',
390 hook_name='PRE_PUSH_HOOK',
396 kwargs_keys=(
391 kwargs_keys=(
397 'server_url', 'config', 'scm', 'username', 'ip', 'action',
392 'server_url', 'config', 'scm', 'username', 'ip', 'action',
398 'repository', 'repo_store_path', 'commit_ids', 'hook_type', 'user_agent',))
393 'repository', 'repo_store_path', 'commit_ids', 'hook_type', 'user_agent',))
399
394
400
395
401 post_push_extension = ExtensionCallback(
396 post_push_extension = ExtensionCallback(
402 hook_name='PUSH_HOOK',
397 hook_name='PUSH_HOOK',
403 kwargs_keys=(
398 kwargs_keys=(
404 'server_url', 'config', 'scm', 'username', 'ip', 'action',
399 'server_url', 'config', 'scm', 'username', 'ip', 'action',
405 'repository', 'repo_store_path', 'commit_ids', 'hook_type', 'user_agent',))
400 'repository', 'repo_store_path', 'commit_ids', 'hook_type', 'user_agent',))
406
401
407
402
408 pre_create_user = ExtensionCallback(
403 pre_create_user = ExtensionCallback(
409 hook_name='PRE_CREATE_USER_HOOK',
404 hook_name='PRE_CREATE_USER_HOOK',
410 kwargs_keys=(
405 kwargs_keys=(
411 'username', 'password', 'email', 'firstname', 'lastname', 'active',
406 'username', 'password', 'email', 'firstname', 'lastname', 'active',
412 'admin', 'created_by'))
407 'admin', 'created_by'))
413
408
414
409
415 create_pull_request = ExtensionCallback(
410 create_pull_request = ExtensionCallback(
416 hook_name='CREATE_PULL_REQUEST',
411 hook_name='CREATE_PULL_REQUEST',
417 kwargs_keys=(
412 kwargs_keys=(
418 'server_url', 'config', 'scm', 'username', 'ip', 'action',
413 'server_url', 'config', 'scm', 'username', 'ip', 'action',
419 'repository', 'pull_request_id', 'url', 'title', 'description',
414 'repository', 'pull_request_id', 'url', 'title', 'description',
420 'status', 'created_on', 'updated_on', 'commit_ids', 'review_status',
415 'status', 'created_on', 'updated_on', 'commit_ids', 'review_status',
421 'mergeable', 'source', 'target', 'author', 'reviewers'))
416 'mergeable', 'source', 'target', 'author', 'reviewers'))
422
417
423
418
424 merge_pull_request = ExtensionCallback(
419 merge_pull_request = ExtensionCallback(
425 hook_name='MERGE_PULL_REQUEST',
420 hook_name='MERGE_PULL_REQUEST',
426 kwargs_keys=(
421 kwargs_keys=(
427 'server_url', 'config', 'scm', 'username', 'ip', 'action',
422 'server_url', 'config', 'scm', 'username', 'ip', 'action',
428 'repository', 'pull_request_id', 'url', 'title', 'description',
423 'repository', 'pull_request_id', 'url', 'title', 'description',
429 'status', 'created_on', 'updated_on', 'commit_ids', 'review_status',
424 'status', 'created_on', 'updated_on', 'commit_ids', 'review_status',
430 'mergeable', 'source', 'target', 'author', 'reviewers'))
425 'mergeable', 'source', 'target', 'author', 'reviewers'))
431
426
432
427
433 close_pull_request = ExtensionCallback(
428 close_pull_request = ExtensionCallback(
434 hook_name='CLOSE_PULL_REQUEST',
429 hook_name='CLOSE_PULL_REQUEST',
435 kwargs_keys=(
430 kwargs_keys=(
436 'server_url', 'config', 'scm', 'username', 'ip', 'action',
431 'server_url', 'config', 'scm', 'username', 'ip', 'action',
437 'repository', 'pull_request_id', 'url', 'title', 'description',
432 'repository', 'pull_request_id', 'url', 'title', 'description',
438 'status', 'created_on', 'updated_on', 'commit_ids', 'review_status',
433 'status', 'created_on', 'updated_on', 'commit_ids', 'review_status',
439 'mergeable', 'source', 'target', 'author', 'reviewers'))
434 'mergeable', 'source', 'target', 'author', 'reviewers'))
440
435
441
436
442 review_pull_request = ExtensionCallback(
437 review_pull_request = ExtensionCallback(
443 hook_name='REVIEW_PULL_REQUEST',
438 hook_name='REVIEW_PULL_REQUEST',
444 kwargs_keys=(
439 kwargs_keys=(
445 'server_url', 'config', 'scm', 'username', 'ip', 'action',
440 'server_url', 'config', 'scm', 'username', 'ip', 'action',
446 'repository', 'pull_request_id', 'url', 'title', 'description',
441 'repository', 'pull_request_id', 'url', 'title', 'description',
447 'status', 'created_on', 'updated_on', 'commit_ids', 'review_status',
442 'status', 'created_on', 'updated_on', 'commit_ids', 'review_status',
448 'mergeable', 'source', 'target', 'author', 'reviewers'))
443 'mergeable', 'source', 'target', 'author', 'reviewers'))
449
444
450
445
451 comment_pull_request = ExtensionCallback(
446 comment_pull_request = ExtensionCallback(
452 hook_name='COMMENT_PULL_REQUEST',
447 hook_name='COMMENT_PULL_REQUEST',
453 kwargs_keys=(
448 kwargs_keys=(
454 'server_url', 'config', 'scm', 'username', 'ip', 'action',
449 'server_url', 'config', 'scm', 'username', 'ip', 'action',
455 'repository', 'pull_request_id', 'url', 'title', 'description',
450 'repository', 'pull_request_id', 'url', 'title', 'description',
456 'status', 'comment', 'created_on', 'updated_on', 'commit_ids', 'review_status',
451 'status', 'comment', 'created_on', 'updated_on', 'commit_ids', 'review_status',
457 'mergeable', 'source', 'target', 'author', 'reviewers'))
452 'mergeable', 'source', 'target', 'author', 'reviewers'))
458
453
459
454
460 comment_edit_pull_request = ExtensionCallback(
455 comment_edit_pull_request = ExtensionCallback(
461 hook_name='COMMENT_EDIT_PULL_REQUEST',
456 hook_name='COMMENT_EDIT_PULL_REQUEST',
462 kwargs_keys=(
457 kwargs_keys=(
463 'server_url', 'config', 'scm', 'username', 'ip', 'action',
458 'server_url', 'config', 'scm', 'username', 'ip', 'action',
464 'repository', 'pull_request_id', 'url', 'title', 'description',
459 'repository', 'pull_request_id', 'url', 'title', 'description',
465 'status', 'comment', 'created_on', 'updated_on', 'commit_ids', 'review_status',
460 'status', 'comment', 'created_on', 'updated_on', 'commit_ids', 'review_status',
466 'mergeable', 'source', 'target', 'author', 'reviewers'))
461 'mergeable', 'source', 'target', 'author', 'reviewers'))
467
462
468
463
469 update_pull_request = ExtensionCallback(
464 update_pull_request = ExtensionCallback(
470 hook_name='UPDATE_PULL_REQUEST',
465 hook_name='UPDATE_PULL_REQUEST',
471 kwargs_keys=(
466 kwargs_keys=(
472 'server_url', 'config', 'scm', 'username', 'ip', 'action',
467 'server_url', 'config', 'scm', 'username', 'ip', 'action',
473 'repository', 'pull_request_id', 'url', 'title', 'description',
468 'repository', 'pull_request_id', 'url', 'title', 'description',
474 'status', 'created_on', 'updated_on', 'commit_ids', 'review_status',
469 'status', 'created_on', 'updated_on', 'commit_ids', 'review_status',
475 'mergeable', 'source', 'target', 'author', 'reviewers'))
470 'mergeable', 'source', 'target', 'author', 'reviewers'))
476
471
477
472
478 create_user = ExtensionCallback(
473 create_user = ExtensionCallback(
479 hook_name='CREATE_USER_HOOK',
474 hook_name='CREATE_USER_HOOK',
480 kwargs_keys=(
475 kwargs_keys=(
481 'username', 'full_name_or_username', 'full_contact', 'user_id',
476 'username', 'full_name_or_username', 'full_contact', 'user_id',
482 'name', 'firstname', 'short_contact', 'admin', 'lastname',
477 'name', 'firstname', 'short_contact', 'admin', 'lastname',
483 'ip_addresses', 'extern_type', 'extern_name',
478 'ip_addresses', 'extern_type', 'extern_name',
484 'email', 'api_keys', 'last_login',
479 'email', 'api_keys', 'last_login',
485 'full_name', 'active', 'password', 'emails',
480 'full_name', 'active', 'password', 'emails',
486 'inherit_default_permissions', 'created_by', 'created_on'))
481 'inherit_default_permissions', 'created_by', 'created_on'))
487
482
488
483
489 delete_user = ExtensionCallback(
484 delete_user = ExtensionCallback(
490 hook_name='DELETE_USER_HOOK',
485 hook_name='DELETE_USER_HOOK',
491 kwargs_keys=(
486 kwargs_keys=(
492 'username', 'full_name_or_username', 'full_contact', 'user_id',
487 'username', 'full_name_or_username', 'full_contact', 'user_id',
493 'name', 'firstname', 'short_contact', 'admin', 'lastname',
488 'name', 'firstname', 'short_contact', 'admin', 'lastname',
494 'ip_addresses',
489 'ip_addresses',
495 'email', 'last_login',
490 'email', 'last_login',
496 'full_name', 'active', 'password', 'emails',
491 'full_name', 'active', 'password', 'emails',
497 'inherit_default_permissions', 'deleted_by'))
492 'inherit_default_permissions', 'deleted_by'))
498
493
499
494
500 create_repository = ExtensionCallback(
495 create_repository = ExtensionCallback(
501 hook_name='CREATE_REPO_HOOK',
496 hook_name='CREATE_REPO_HOOK',
502 kwargs_keys=(
497 kwargs_keys=(
503 'repo_name', 'repo_type', 'description', 'private', 'created_on',
498 'repo_name', 'repo_type', 'description', 'private', 'created_on',
504 'enable_downloads', 'repo_id', 'user_id', 'enable_statistics',
499 'enable_downloads', 'repo_id', 'user_id', 'enable_statistics',
505 'clone_uri', 'fork_id', 'group_id', 'created_by'))
500 'clone_uri', 'fork_id', 'group_id', 'created_by'))
506
501
507
502
508 delete_repository = ExtensionCallback(
503 delete_repository = ExtensionCallback(
509 hook_name='DELETE_REPO_HOOK',
504 hook_name='DELETE_REPO_HOOK',
510 kwargs_keys=(
505 kwargs_keys=(
511 'repo_name', 'repo_type', 'description', 'private', 'created_on',
506 'repo_name', 'repo_type', 'description', 'private', 'created_on',
512 'enable_downloads', 'repo_id', 'user_id', 'enable_statistics',
507 'enable_downloads', 'repo_id', 'user_id', 'enable_statistics',
513 'clone_uri', 'fork_id', 'group_id', 'deleted_by', 'deleted_on'))
508 'clone_uri', 'fork_id', 'group_id', 'deleted_by', 'deleted_on'))
514
509
515
510
516 comment_commit_repository = ExtensionCallback(
511 comment_commit_repository = ExtensionCallback(
517 hook_name='COMMENT_COMMIT_REPO_HOOK',
512 hook_name='COMMENT_COMMIT_REPO_HOOK',
518 kwargs_keys=(
513 kwargs_keys=(
519 'repo_name', 'repo_type', 'description', 'private', 'created_on',
514 'repo_name', 'repo_type', 'description', 'private', 'created_on',
520 'enable_downloads', 'repo_id', 'user_id', 'enable_statistics',
515 'enable_downloads', 'repo_id', 'user_id', 'enable_statistics',
521 'clone_uri', 'fork_id', 'group_id',
516 'clone_uri', 'fork_id', 'group_id',
522 'repository', 'created_by', 'comment', 'commit'))
517 'repository', 'created_by', 'comment', 'commit'))
523
518
524 comment_edit_commit_repository = ExtensionCallback(
519 comment_edit_commit_repository = ExtensionCallback(
525 hook_name='COMMENT_EDIT_COMMIT_REPO_HOOK',
520 hook_name='COMMENT_EDIT_COMMIT_REPO_HOOK',
526 kwargs_keys=(
521 kwargs_keys=(
527 'repo_name', 'repo_type', 'description', 'private', 'created_on',
522 'repo_name', 'repo_type', 'description', 'private', 'created_on',
528 'enable_downloads', 'repo_id', 'user_id', 'enable_statistics',
523 'enable_downloads', 'repo_id', 'user_id', 'enable_statistics',
529 'clone_uri', 'fork_id', 'group_id',
524 'clone_uri', 'fork_id', 'group_id',
530 'repository', 'created_by', 'comment', 'commit'))
525 'repository', 'created_by', 'comment', 'commit'))
531
526
532
527
533 create_repository_group = ExtensionCallback(
528 create_repository_group = ExtensionCallback(
534 hook_name='CREATE_REPO_GROUP_HOOK',
529 hook_name='CREATE_REPO_GROUP_HOOK',
535 kwargs_keys=(
530 kwargs_keys=(
536 'group_name', 'group_parent_id', 'group_description',
531 'group_name', 'group_parent_id', 'group_description',
537 'group_id', 'user_id', 'created_by', 'created_on',
532 'group_id', 'user_id', 'created_by', 'created_on',
538 'enable_locking'))
533 'enable_locking'))
@@ -1,364 +1,438 b''
1
1
2 # Copyright (C) 2010-2020 RhodeCode GmbH
2 # Copyright (C) 2010-2020 RhodeCode GmbH
3 #
3 #
4 # This program is free software: you can redistribute it and/or modify
4 # This program is free software: you can redistribute it and/or modify
5 # it under the terms of the GNU Affero General Public License, version 3
5 # it under the terms of the GNU Affero General Public License, version 3
6 # (only), as published by the Free Software Foundation.
6 # (only), as published by the Free Software Foundation.
7 #
7 #
8 # This program is distributed in the hope that it will be useful,
8 # This program is distributed in the hope that it will be useful,
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # GNU General Public License for more details.
11 # GNU General Public License for more details.
12 #
12 #
13 # You should have received a copy of the GNU Affero General Public License
13 # You should have received a copy of the GNU Affero General Public License
14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 #
15 #
16 # This program is dual-licensed. If you wish to learn more about the
16 # This program is dual-licensed. If you wish to learn more about the
17 # RhodeCode Enterprise Edition, including its added features, Support services,
17 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 # and proprietary license terms, please see https://rhodecode.com/licenses/
19
19
20 import os
20 import os
21 import time
21 import time
22 import logging
22 import logging
23 import tempfile
23 import tempfile
24 import traceback
24 import traceback
25 import threading
25 import threading
26 import socket
26 import socket
27 import msgpack
27 import msgpack
28 import gevent
28
29
29 from http.server import BaseHTTPRequestHandler
30 from http.server import BaseHTTPRequestHandler
30 from socketserver import TCPServer
31 from socketserver import TCPServer
31
32
32 import rhodecode
33 import rhodecode
33 from rhodecode.lib.exceptions import HTTPLockedRC, HTTPBranchProtected
34 from rhodecode.lib.exceptions import HTTPLockedRC, HTTPBranchProtected
34 from rhodecode.model import meta
35 from rhodecode.model import meta
35 from rhodecode.lib.base import bootstrap_request, bootstrap_config
36 from rhodecode.lib.base import bootstrap_request, bootstrap_config
36 from rhodecode.lib import hooks_base
37 from rhodecode.lib import hooks_base
37 from rhodecode.lib.utils2 import AttributeDict
38 from rhodecode.lib.utils2 import AttributeDict
38 from rhodecode.lib.ext_json import json
39 from rhodecode.lib.ext_json import json
39 from rhodecode.lib import rc_cache
40 from rhodecode.lib import rc_cache
40
41
41 log = logging.getLogger(__name__)
42 log = logging.getLogger(__name__)
42
43
43
44
44 class HooksHttpHandler(BaseHTTPRequestHandler):
45 class HooksHttpHandler(BaseHTTPRequestHandler):
45
46
47 JSON_HOOKS_PROTO = 'json.v1'
48 MSGPACK_HOOKS_PROTO = 'msgpack.v1'
49 # starting with RhodeCode 5.0.0 MsgPack is the default, prior it used json
50 DEFAULT_HOOKS_PROTO = MSGPACK_HOOKS_PROTO
51
52 @classmethod
53 def serialize_data(cls, data, proto=DEFAULT_HOOKS_PROTO):
54 if proto == cls.MSGPACK_HOOKS_PROTO:
55 return msgpack.packb(data)
56 return json.dumps(data)
57
58 @classmethod
59 def deserialize_data(cls, data, proto=DEFAULT_HOOKS_PROTO):
60 if proto == cls.MSGPACK_HOOKS_PROTO:
61 return msgpack.unpackb(data)
62 return json.loads(data)
63
46 def do_POST(self):
64 def do_POST(self):
47 hooks_proto, method, extras = self._read_request()
65 hooks_proto, method, extras = self._read_request()
48 log.debug('Handling HooksHttpHandler %s with %s proto', method, hooks_proto)
66 log.debug('Handling HooksHttpHandler %s with %s proto', method, hooks_proto)
49
67
50 txn_id = getattr(self.server, 'txn_id', None)
68 txn_id = getattr(self.server, 'txn_id', None)
51 if txn_id:
69 if txn_id:
52 log.debug('Computing TXN_ID based on `%s`:`%s`',
70 log.debug('Computing TXN_ID based on `%s`:`%s`',
53 extras['repository'], extras['txn_id'])
71 extras['repository'], extras['txn_id'])
54 computed_txn_id = rc_cache.utils.compute_key_from_params(
72 computed_txn_id = rc_cache.utils.compute_key_from_params(
55 extras['repository'], extras['txn_id'])
73 extras['repository'], extras['txn_id'])
56 if txn_id != computed_txn_id:
74 if txn_id != computed_txn_id:
57 raise Exception(
75 raise Exception(
58 'TXN ID fail: expected {} got {} instead'.format(
76 'TXN ID fail: expected {} got {} instead'.format(
59 txn_id, computed_txn_id))
77 txn_id, computed_txn_id))
60
78
61 request = getattr(self.server, 'request', None)
79 request = getattr(self.server, 'request', None)
62 try:
80 try:
63 hooks = Hooks(request=request, log_prefix='HOOKS: {} '.format(self.server.server_address))
81 hooks = Hooks(request=request, log_prefix='HOOKS: {} '.format(self.server.server_address))
64 result = self._call_hook_method(hooks, method, extras)
82 result = self._call_hook_method(hooks, method, extras)
83
65 except Exception as e:
84 except Exception as e:
66 exc_tb = traceback.format_exc()
85 exc_tb = traceback.format_exc()
67 result = {
86 result = {
68 'exception': e.__class__.__name__,
87 'exception': e.__class__.__name__,
69 'exception_traceback': exc_tb,
88 'exception_traceback': exc_tb,
70 'exception_args': e.args
89 'exception_args': e.args
71 }
90 }
72 self._write_response(hooks_proto, result)
91 self._write_response(hooks_proto, result)
73
92
74 def _read_request(self):
93 def _read_request(self):
75 length = int(self.headers['Content-Length'])
94 length = int(self.headers['Content-Length'])
76 hooks_proto = self.headers.get('rc-hooks-protocol') or 'json.v1'
95 # respect sent headers, fallback to OLD proto for compatability
77 if hooks_proto == 'msgpack.v1':
96 hooks_proto = self.headers.get('rc-hooks-protocol') or self.JSON_HOOKS_PROTO
97 if hooks_proto == self.MSGPACK_HOOKS_PROTO:
78 # support for new vcsserver msgpack based protocol hooks
98 # support for new vcsserver msgpack based protocol hooks
79 data = msgpack.unpackb(self.rfile.read(length), raw=False)
99 body = self.rfile.read(length)
100 data = self.deserialize_data(body)
80 else:
101 else:
81 body = self.rfile.read(length)
102 body = self.rfile.read(length)
82 data = json.loads(body)
103 data = self.deserialize_data(body)
83
104
84 return hooks_proto, data['method'], data['extras']
105 return hooks_proto, data['method'], data['extras']
85
106
86 def _write_response(self, hooks_proto, result):
107 def _write_response(self, hooks_proto, result):
87 self.send_response(200)
108 self.send_response(200)
88 if hooks_proto == 'msgpack.v1':
109 if hooks_proto == self.MSGPACK_HOOKS_PROTO:
89 self.send_header("Content-type", "application/msgpack")
110 self.send_header("Content-type", "application/msgpack")
90 self.end_headers()
111 self.end_headers()
91 self.wfile.write(msgpack.packb(result))
112 data = self.serialize_data(result)
113 self.wfile.write(data)
92 else:
114 else:
93 self.send_header("Content-type", "text/json")
115 self.send_header("Content-type", "text/json")
94 self.end_headers()
116 self.end_headers()
95 self.wfile.write(json.dumps(result))
117 data = self.serialize_data(result)
118 self.wfile.write(data)
96
119
97 def _call_hook_method(self, hooks, method, extras):
120 def _call_hook_method(self, hooks, method, extras):
98 try:
121 try:
99 result = getattr(hooks, method)(extras)
122 result = getattr(hooks, method)(extras)
100 finally:
123 finally:
101 meta.Session.remove()
124 meta.Session.remove()
102 return result
125 return result
103
126
104 def log_message(self, format, *args):
127 def log_message(self, format, *args):
105 """
128 """
106 This is an overridden method of BaseHTTPRequestHandler which logs using
129 This is an overridden method of BaseHTTPRequestHandler which logs using
107 logging library instead of writing directly to stderr.
130 logging library instead of writing directly to stderr.
108 """
131 """
109
132
110 message = format % args
133 message = format % args
111
134
112 log.debug(
135 log.debug(
113 "HOOKS: %s - - [%s] %s", self.client_address,
136 "HOOKS: client=%s - - [%s] %s", self.client_address,
114 self.log_date_time_string(), message)
137 self.log_date_time_string(), message)
115
138
116
139
117 class DummyHooksCallbackDaemon(object):
140 class DummyHooksCallbackDaemon(object):
118 hooks_uri = ''
141 hooks_uri = ''
119
142
120 def __init__(self):
143 def __init__(self):
121 self.hooks_module = Hooks.__module__
144 self.hooks_module = Hooks.__module__
122
145
123 def __enter__(self):
146 def __enter__(self):
124 log.debug('Running `%s` callback daemon', self.__class__.__name__)
147 log.debug('Running `%s` callback daemon', self.__class__.__name__)
125 return self
148 return self
126
149
127 def __exit__(self, exc_type, exc_val, exc_tb):
150 def __exit__(self, exc_type, exc_val, exc_tb):
128 log.debug('Exiting `%s` callback daemon', self.__class__.__name__)
151 log.debug('Exiting `%s` callback daemon', self.__class__.__name__)
129
152
130
153
131 class ThreadedHookCallbackDaemon(object):
154 class ThreadedHookCallbackDaemon(object):
132
155
133 _callback_thread = None
156 _callback_thread = None
134 _daemon = None
157 _daemon = None
135 _done = False
158 _done = False
159 use_gevent = False
136
160
137 def __init__(self, txn_id=None, host=None, port=None):
161 def __init__(self, txn_id=None, host=None, port=None):
138 self._prepare(txn_id=txn_id, host=host, port=port)
162 self._prepare(txn_id=txn_id, host=host, port=port)
163 if self.use_gevent:
164 self._run_func = self._run_gevent
165 self._stop_func = self._stop_gevent
166 else:
167 self._run_func = self._run
168 self._stop_func = self._stop
139
169
140 def __enter__(self):
170 def __enter__(self):
141 log.debug('Running `%s` callback daemon', self.__class__.__name__)
171 log.debug('Running `%s` callback daemon', self.__class__.__name__)
142 self._run()
172 self._run_func()
143 return self
173 return self
144
174
145 def __exit__(self, exc_type, exc_val, exc_tb):
175 def __exit__(self, exc_type, exc_val, exc_tb):
146 log.debug('Exiting `%s` callback daemon', self.__class__.__name__)
176 log.debug('Exiting `%s` callback daemon', self.__class__.__name__)
147 self._stop()
177 self._stop_func()
148
178
149 def _prepare(self, txn_id=None, host=None, port=None):
179 def _prepare(self, txn_id=None, host=None, port=None):
150 raise NotImplementedError()
180 raise NotImplementedError()
151
181
152 def _run(self):
182 def _run(self):
153 raise NotImplementedError()
183 raise NotImplementedError()
154
184
155 def _stop(self):
185 def _stop(self):
156 raise NotImplementedError()
186 raise NotImplementedError()
157
187
188 def _run_gevent(self):
189 raise NotImplementedError()
190
191 def _stop_gevent(self):
192 raise NotImplementedError()
193
158
194
159 class HttpHooksCallbackDaemon(ThreadedHookCallbackDaemon):
195 class HttpHooksCallbackDaemon(ThreadedHookCallbackDaemon):
160 """
196 """
161 Context manager which will run a callback daemon in a background thread.
197 Context manager which will run a callback daemon in a background thread.
162 """
198 """
163
199
164 hooks_uri = None
200 hooks_uri = None
165
201
166 # From Python docs: Polling reduces our responsiveness to a shutdown
202 # From Python docs: Polling reduces our responsiveness to a shutdown
167 # request and wastes cpu at all other times.
203 # request and wastes cpu at all other times.
168 POLL_INTERVAL = 0.01
204 POLL_INTERVAL = 0.01
169
205
206 use_gevent = False
207
170 @property
208 @property
171 def _hook_prefix(self):
209 def _hook_prefix(self):
172 return 'HOOKS: {} '.format(self.hooks_uri)
210 return 'HOOKS: {} '.format(self.hooks_uri)
173
211
174 def get_hostname(self):
212 def get_hostname(self):
175 return socket.gethostname() or '127.0.0.1'
213 return socket.gethostname() or '127.0.0.1'
176
214
177 def get_available_port(self, min_port=20000, max_port=65535):
215 def get_available_port(self, min_port=20000, max_port=65535):
178 from rhodecode.lib.utils2 import get_available_port as _get_port
216 from rhodecode.lib.utils2 import get_available_port as _get_port
179 return _get_port(min_port, max_port)
217 return _get_port(min_port, max_port)
180
218
181 def _prepare(self, txn_id=None, host=None, port=None):
219 def _prepare(self, txn_id=None, host=None, port=None):
182 from pyramid.threadlocal import get_current_request
220 from pyramid.threadlocal import get_current_request
183
221
184 if not host or host == "*":
222 if not host or host == "*":
185 host = self.get_hostname()
223 host = self.get_hostname()
186 if not port:
224 if not port:
187 port = self.get_available_port()
225 port = self.get_available_port()
188
226
189 server_address = (host, port)
227 server_address = (host, port)
190 self.hooks_uri = '{}:{}'.format(host, port)
228 self.hooks_uri = '{}:{}'.format(host, port)
191 self.txn_id = txn_id
229 self.txn_id = txn_id
192 self._done = False
230 self._done = False
193
231
194 log.debug(
232 log.debug(
195 "%s Preparing HTTP callback daemon registering hook object: %s",
233 "%s Preparing HTTP callback daemon registering hook object: %s",
196 self._hook_prefix, HooksHttpHandler)
234 self._hook_prefix, HooksHttpHandler)
197
235
198 self._daemon = TCPServer(server_address, HooksHttpHandler)
236 self._daemon = TCPServer(server_address, HooksHttpHandler)
199 # inject transaction_id for later verification
237 # inject transaction_id for later verification
200 self._daemon.txn_id = self.txn_id
238 self._daemon.txn_id = self.txn_id
201
239
202 # pass the WEB app request into daemon
240 # pass the WEB app request into daemon
203 self._daemon.request = get_current_request()
241 self._daemon.request = get_current_request()
204
242
205 def _run(self):
243 def _run(self):
206 log.debug("Running event loop of callback daemon in background thread")
244 log.debug("Running thread-based loop of callback daemon in background")
207 callback_thread = threading.Thread(
245 callback_thread = threading.Thread(
208 target=self._daemon.serve_forever,
246 target=self._daemon.serve_forever,
209 kwargs={'poll_interval': self.POLL_INTERVAL})
247 kwargs={'poll_interval': self.POLL_INTERVAL})
210 callback_thread.daemon = True
248 callback_thread.daemon = True
211 callback_thread.start()
249 callback_thread.start()
212 self._callback_thread = callback_thread
250 self._callback_thread = callback_thread
213
251
252 def _run_gevent(self):
253 log.debug("Running gevent-based loop of callback daemon in background")
254 # create a new greenlet for the daemon's serve_forever method
255 callback_greenlet = gevent.spawn(
256 self._daemon.serve_forever,
257 poll_interval=self.POLL_INTERVAL)
258
259 # store reference to greenlet
260 self._callback_greenlet = callback_greenlet
261
262 # switch to this greenlet
263 gevent.sleep(0.01)
264
214 def _stop(self):
265 def _stop(self):
215 log.debug("Waiting for background thread to finish.")
266 log.debug("Waiting for background thread to finish.")
216 self._daemon.shutdown()
267 self._daemon.shutdown()
217 self._callback_thread.join()
268 self._callback_thread.join()
218 self._daemon = None
269 self._daemon = None
219 self._callback_thread = None
270 self._callback_thread = None
220 if self.txn_id:
271 if self.txn_id:
221 txn_id_file = get_txn_id_data_path(self.txn_id)
272 txn_id_file = get_txn_id_data_path(self.txn_id)
222 log.debug('Cleaning up TXN ID %s', txn_id_file)
273 log.debug('Cleaning up TXN ID %s', txn_id_file)
223 if os.path.isfile(txn_id_file):
274 if os.path.isfile(txn_id_file):
224 os.remove(txn_id_file)
275 os.remove(txn_id_file)
225
276
226 log.debug("Background thread done.")
277 log.debug("Background thread done.")
227
278
279 def _stop_gevent(self):
280 log.debug("Waiting for background greenlet to finish.")
281
282 # if greenlet exists and is running
283 if self._callback_greenlet and not self._callback_greenlet.dead:
284 # shutdown daemon if it exists
285 if self._daemon:
286 self._daemon.shutdown()
287
288 # kill the greenlet
289 self._callback_greenlet.kill()
290
291 self._daemon = None
292 self._callback_greenlet = None
293
294 if self.txn_id:
295 txn_id_file = get_txn_id_data_path(self.txn_id)
296 log.debug('Cleaning up TXN ID %s', txn_id_file)
297 if os.path.isfile(txn_id_file):
298 os.remove(txn_id_file)
299
300 log.debug("Background greenlet done.")
301
228
302
229 def get_txn_id_data_path(txn_id):
303 def get_txn_id_data_path(txn_id):
230 import rhodecode
304 import rhodecode
231
305
232 root = rhodecode.CONFIG.get('cache_dir') or tempfile.gettempdir()
306 root = rhodecode.CONFIG.get('cache_dir') or tempfile.gettempdir()
233 final_dir = os.path.join(root, 'svn_txn_id')
307 final_dir = os.path.join(root, 'svn_txn_id')
234
308
235 if not os.path.isdir(final_dir):
309 if not os.path.isdir(final_dir):
236 os.makedirs(final_dir)
310 os.makedirs(final_dir)
237 return os.path.join(final_dir, 'rc_txn_id_{}'.format(txn_id))
311 return os.path.join(final_dir, 'rc_txn_id_{}'.format(txn_id))
238
312
239
313
240 def store_txn_id_data(txn_id, data_dict):
314 def store_txn_id_data(txn_id, data_dict):
241 if not txn_id:
315 if not txn_id:
242 log.warning('Cannot store txn_id because it is empty')
316 log.warning('Cannot store txn_id because it is empty')
243 return
317 return
244
318
245 path = get_txn_id_data_path(txn_id)
319 path = get_txn_id_data_path(txn_id)
246 try:
320 try:
247 with open(path, 'wb') as f:
321 with open(path, 'wb') as f:
248 f.write(json.dumps(data_dict))
322 f.write(json.dumps(data_dict))
249 except Exception:
323 except Exception:
250 log.exception('Failed to write txn_id metadata')
324 log.exception('Failed to write txn_id metadata')
251
325
252
326
253 def get_txn_id_from_store(txn_id):
327 def get_txn_id_from_store(txn_id):
254 """
328 """
255 Reads txn_id from store and if present returns the data for callback manager
329 Reads txn_id from store and if present returns the data for callback manager
256 """
330 """
257 path = get_txn_id_data_path(txn_id)
331 path = get_txn_id_data_path(txn_id)
258 try:
332 try:
259 with open(path, 'rb') as f:
333 with open(path, 'rb') as f:
260 return json.loads(f.read())
334 return json.loads(f.read())
261 except Exception:
335 except Exception:
262 return {}
336 return {}
263
337
264
338
265 def prepare_callback_daemon(extras, protocol, host, use_direct_calls, txn_id=None):
339 def prepare_callback_daemon(extras, protocol, host, use_direct_calls, txn_id=None):
266 txn_details = get_txn_id_from_store(txn_id)
340 txn_details = get_txn_id_from_store(txn_id)
267 port = txn_details.get('port', 0)
341 port = txn_details.get('port', 0)
268 if use_direct_calls:
342 if use_direct_calls:
269 callback_daemon = DummyHooksCallbackDaemon()
343 callback_daemon = DummyHooksCallbackDaemon()
270 extras['hooks_module'] = callback_daemon.hooks_module
344 extras['hooks_module'] = callback_daemon.hooks_module
271 else:
345 else:
272 if protocol == 'http':
346 if protocol == 'http':
273 callback_daemon = HttpHooksCallbackDaemon(
347 callback_daemon = HttpHooksCallbackDaemon(
274 txn_id=txn_id, host=host, port=port)
348 txn_id=txn_id, host=host, port=port)
275 else:
349 else:
276 log.error('Unsupported callback daemon protocol "%s"', protocol)
350 log.error('Unsupported callback daemon protocol "%s"', protocol)
277 raise Exception('Unsupported callback daemon protocol.')
351 raise Exception('Unsupported callback daemon protocol.')
278
352
279 extras['hooks_uri'] = callback_daemon.hooks_uri
353 extras['hooks_uri'] = callback_daemon.hooks_uri
280 extras['hooks_protocol'] = protocol
354 extras['hooks_protocol'] = protocol
281 extras['time'] = time.time()
355 extras['time'] = time.time()
282
356
283 # register txn_id
357 # register txn_id
284 extras['txn_id'] = txn_id
358 extras['txn_id'] = txn_id
285 log.debug('Prepared a callback daemon: %s at url `%s`',
359 log.debug('Prepared a callback daemon: %s at url `%s`',
286 callback_daemon.__class__.__name__, callback_daemon.hooks_uri)
360 callback_daemon.__class__.__name__, callback_daemon.hooks_uri)
287 return callback_daemon, extras
361 return callback_daemon, extras
288
362
289
363
290 class Hooks(object):
364 class Hooks(object):
291 """
365 """
292 Exposes the hooks for remote call backs
366 Exposes the hooks for remote call backs
293 """
367 """
294 def __init__(self, request=None, log_prefix=''):
368 def __init__(self, request=None, log_prefix=''):
295 self.log_prefix = log_prefix
369 self.log_prefix = log_prefix
296 self.request = request
370 self.request = request
297
371
298 def repo_size(self, extras):
372 def repo_size(self, extras):
299 log.debug("%sCalled repo_size of %s object", self.log_prefix, self)
373 log.debug("%sCalled repo_size of %s object", self.log_prefix, self)
300 return self._call_hook(hooks_base.repo_size, extras)
374 return self._call_hook(hooks_base.repo_size, extras)
301
375
302 def pre_pull(self, extras):
376 def pre_pull(self, extras):
303 log.debug("%sCalled pre_pull of %s object", self.log_prefix, self)
377 log.debug("%sCalled pre_pull of %s object", self.log_prefix, self)
304 return self._call_hook(hooks_base.pre_pull, extras)
378 return self._call_hook(hooks_base.pre_pull, extras)
305
379
306 def post_pull(self, extras):
380 def post_pull(self, extras):
307 log.debug("%sCalled post_pull of %s object", self.log_prefix, self)
381 log.debug("%sCalled post_pull of %s object", self.log_prefix, self)
308 return self._call_hook(hooks_base.post_pull, extras)
382 return self._call_hook(hooks_base.post_pull, extras)
309
383
310 def pre_push(self, extras):
384 def pre_push(self, extras):
311 log.debug("%sCalled pre_push of %s object", self.log_prefix, self)
385 log.debug("%sCalled pre_push of %s object", self.log_prefix, self)
312 return self._call_hook(hooks_base.pre_push, extras)
386 return self._call_hook(hooks_base.pre_push, extras)
313
387
314 def post_push(self, extras):
388 def post_push(self, extras):
315 log.debug("%sCalled post_push of %s object", self.log_prefix, self)
389 log.debug("%sCalled post_push of %s object", self.log_prefix, self)
316 return self._call_hook(hooks_base.post_push, extras)
390 return self._call_hook(hooks_base.post_push, extras)
317
391
318 def _call_hook(self, hook, extras):
392 def _call_hook(self, hook, extras):
319 extras = AttributeDict(extras)
393 extras = AttributeDict(extras)
320 server_url = extras['server_url']
394 server_url = extras['server_url']
321
395
322 extras.request = self.request
396 extras.request = self.request
323
397
324 try:
398 try:
325 result = hook(extras)
399 result = hook(extras)
326 if result is None:
400 if result is None:
327 raise Exception(
401 raise Exception(
328 'Failed to obtain hook result from func: {}'.format(hook))
402 'Failed to obtain hook result from func: {}'.format(hook))
329 except HTTPBranchProtected as handled_error:
403 except HTTPBranchProtected as handled_error:
330 # Those special cases doesn't need error reporting. It's a case of
404 # Those special cases doesn't need error reporting. It's a case of
331 # locked repo or protected branch
405 # locked repo or protected branch
332 result = AttributeDict({
406 result = AttributeDict({
333 'status': handled_error.code,
407 'status': handled_error.code,
334 'output': handled_error.explanation
408 'output': handled_error.explanation
335 })
409 })
336 except (HTTPLockedRC, Exception) as error:
410 except (HTTPLockedRC, Exception) as error:
337 # locked needs different handling since we need to also
411 # locked needs different handling since we need to also
338 # handle PULL operations
412 # handle PULL operations
339 exc_tb = ''
413 exc_tb = ''
340 if not isinstance(error, HTTPLockedRC):
414 if not isinstance(error, HTTPLockedRC):
341 exc_tb = traceback.format_exc()
415 exc_tb = traceback.format_exc()
342 log.exception('%sException when handling hook %s', self.log_prefix, hook)
416 log.exception('%sException when handling hook %s', self.log_prefix, hook)
343 error_args = error.args
417 error_args = error.args
344 return {
418 return {
345 'status': 128,
419 'status': 128,
346 'output': '',
420 'output': '',
347 'exception': type(error).__name__,
421 'exception': type(error).__name__,
348 'exception_traceback': exc_tb,
422 'exception_traceback': exc_tb,
349 'exception_args': error_args,
423 'exception_args': error_args,
350 }
424 }
351 finally:
425 finally:
352 meta.Session.remove()
426 meta.Session.remove()
353
427
354 log.debug('%sGot hook call response %s', self.log_prefix, result)
428 log.debug('%sGot hook call response %s', self.log_prefix, result)
355 return {
429 return {
356 'status': result.status,
430 'status': result.status,
357 'output': result.output,
431 'output': result.output,
358 }
432 }
359
433
360 def __enter__(self):
434 def __enter__(self):
361 return self
435 return self
362
436
363 def __exit__(self, exc_type, exc_val, exc_tb):
437 def __exit__(self, exc_type, exc_val, exc_tb):
364 pass
438 pass
General Comments 0
You need to be logged in to leave comments. Login now