##// END OF EJS Templates
fix(mailing): patch repozesendmail to properly use CRLF for newlines. Fixes RFC5322 Issues with mailing
super-admin -
r5511:95e59c41 default
parent child Browse files
Show More
@@ -1,581 +1,581 b''
1 # Copyright (C) 2011-2023 RhodeCode GmbH
1 # Copyright (C) 2011-2023 RhodeCode GmbH
2 #
2 #
3 # This program is free software: you can redistribute it and/or modify
3 # This program is free software: you can redistribute it and/or modify
4 # it under the terms of the GNU Affero General Public License, version 3
4 # it under the terms of the GNU Affero General Public License, version 3
5 # (only), as published by the Free Software Foundation.
5 # (only), as published by the Free Software Foundation.
6 #
6 #
7 # This program is distributed in the hope that it will be useful,
7 # This program is distributed in the hope that it will be useful,
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # GNU General Public License for more details.
10 # GNU General Public License for more details.
11 #
11 #
12 # You should have received a copy of the GNU Affero General Public License
12 # You should have received a copy of the GNU Affero General Public License
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 #
14 #
15 # This program is dual-licensed. If you wish to learn more about the
15 # This program is dual-licensed. If you wish to learn more about the
16 # RhodeCode Enterprise Edition, including its added features, Support services,
16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18
18
19 import itertools
19 import itertools
20 import logging
20 import logging
21 import sys
21 import sys
22 import fnmatch
22 import fnmatch
23
23
24 import decorator
24 import decorator
25 import venusian
25 import venusian
26 from collections import OrderedDict
26 from collections import OrderedDict
27
27
28 from pyramid.exceptions import ConfigurationError
28 from pyramid.exceptions import ConfigurationError
29 from pyramid.renderers import render
29 from pyramid.renderers import render
30 from pyramid.response import Response
30 from pyramid.response import Response
31 from pyramid.httpexceptions import HTTPNotFound
31 from pyramid.httpexceptions import HTTPNotFound
32
32
33 from rhodecode.api.exc import (
33 from rhodecode.api.exc import (
34 JSONRPCBaseError, JSONRPCError, JSONRPCForbidden, JSONRPCValidationError)
34 JSONRPCBaseError, JSONRPCError, JSONRPCForbidden, JSONRPCValidationError)
35 from rhodecode.apps._base import TemplateArgs
35 from rhodecode.apps._base import TemplateArgs
36 from rhodecode.lib.auth import AuthUser
36 from rhodecode.lib.auth import AuthUser
37 from rhodecode.lib.base import get_ip_addr, attach_context_attributes
37 from rhodecode.lib.base import get_ip_addr, attach_context_attributes
38 from rhodecode.lib.exc_tracking import store_exception
38 from rhodecode.lib.exc_tracking import store_exception
39 from rhodecode.lib import ext_json
39 from rhodecode.lib import ext_json
40 from rhodecode.lib.utils2 import safe_str
40 from rhodecode.lib.utils2 import safe_str
41 from rhodecode.lib.plugins.utils import get_plugin_settings
41 from rhodecode.lib.plugins.utils import get_plugin_settings
42 from rhodecode.model.db import User, UserApiKeys
42 from rhodecode.model.db import User, UserApiKeys
43 from rhodecode.config.patches import inspect_getargspec
43
44
44 log = logging.getLogger(__name__)
45 log = logging.getLogger(__name__)
45
46
46 DEFAULT_RENDERER = 'jsonrpc_renderer'
47 DEFAULT_RENDERER = 'jsonrpc_renderer'
47 DEFAULT_URL = '/_admin/api'
48 DEFAULT_URL = '/_admin/api'
48 SERVICE_API_IDENTIFIER = 'service_'
49 SERVICE_API_IDENTIFIER = 'service_'
49
50
50
51
51 def find_methods(jsonrpc_methods, pattern):
52 def find_methods(jsonrpc_methods, pattern):
52 matches = OrderedDict()
53 matches = OrderedDict()
53 if not isinstance(pattern, (list, tuple)):
54 if not isinstance(pattern, (list, tuple)):
54 pattern = [pattern]
55 pattern = [pattern]
55
56
56 for single_pattern in pattern:
57 for single_pattern in pattern:
57 for method_name, method in filter(
58 for method_name, method in filter(
58 lambda x: not x[0].startswith(SERVICE_API_IDENTIFIER), jsonrpc_methods.items()
59 lambda x: not x[0].startswith(SERVICE_API_IDENTIFIER), jsonrpc_methods.items()
59 ):
60 ):
60 if fnmatch.fnmatch(method_name, single_pattern):
61 if fnmatch.fnmatch(method_name, single_pattern):
61 matches[method_name] = method
62 matches[method_name] = method
62 return matches
63 return matches
63
64
64
65
65 class ExtJsonRenderer(object):
66 class ExtJsonRenderer(object):
66 """
67 """
67 Custom renderer that makes use of our ext_json lib
68 Custom renderer that makes use of our ext_json lib
68
69
69 """
70 """
70
71
71 def __init__(self):
72 def __init__(self):
72 self.serializer = ext_json.formatted_json
73 self.serializer = ext_json.formatted_json
73
74
74 def __call__(self, info):
75 def __call__(self, info):
75 """ Returns a plain JSON-encoded string with content-type
76 """ Returns a plain JSON-encoded string with content-type
76 ``application/json``. The content-type may be overridden by
77 ``application/json``. The content-type may be overridden by
77 setting ``request.response.content_type``."""
78 setting ``request.response.content_type``."""
78
79
79 def _render(value, system):
80 def _render(value, system):
80 request = system.get('request')
81 request = system.get('request')
81 if request is not None:
82 if request is not None:
82 response = request.response
83 response = request.response
83 ct = response.content_type
84 ct = response.content_type
84 if ct == response.default_content_type:
85 if ct == response.default_content_type:
85 response.content_type = 'application/json'
86 response.content_type = 'application/json'
86
87
87 return self.serializer(value)
88 return self.serializer(value)
88
89
89 return _render
90 return _render
90
91
91
92
92 def jsonrpc_response(request, result):
93 def jsonrpc_response(request, result):
93 rpc_id = getattr(request, 'rpc_id', None)
94 rpc_id = getattr(request, 'rpc_id', None)
94
95
95 ret_value = ''
96 ret_value = ''
96 if rpc_id:
97 if rpc_id:
97 ret_value = {'id': rpc_id, 'result': result, 'error': None}
98 ret_value = {'id': rpc_id, 'result': result, 'error': None}
98
99
99 # fetch deprecation warnings, and store it inside results
100 # fetch deprecation warnings, and store it inside results
100 deprecation = getattr(request, 'rpc_deprecation', None)
101 deprecation = getattr(request, 'rpc_deprecation', None)
101 if deprecation:
102 if deprecation:
102 ret_value['DEPRECATION_WARNING'] = deprecation
103 ret_value['DEPRECATION_WARNING'] = deprecation
103
104
104 raw_body = render(DEFAULT_RENDERER, ret_value, request=request)
105 raw_body = render(DEFAULT_RENDERER, ret_value, request=request)
105 content_type = 'application/json'
106 content_type = 'application/json'
106 content_type_header = 'Content-Type'
107 content_type_header = 'Content-Type'
107 headers = {
108 headers = {
108 content_type_header: content_type
109 content_type_header: content_type
109 }
110 }
110 return Response(
111 return Response(
111 body=raw_body,
112 body=raw_body,
112 content_type=content_type,
113 content_type=content_type,
113 headerlist=[(k, v) for k, v in headers.items()]
114 headerlist=[(k, v) for k, v in headers.items()]
114 )
115 )
115
116
116
117
117 def jsonrpc_error(request, message, retid=None, code: int | None = None, headers: dict | None = None):
118 def jsonrpc_error(request, message, retid=None, code: int | None = None, headers: dict | None = None):
118 """
119 """
119 Generate a Response object with a JSON-RPC error body
120 Generate a Response object with a JSON-RPC error body
120 """
121 """
121 headers = headers or {}
122 headers = headers or {}
122 content_type = 'application/json'
123 content_type = 'application/json'
123 content_type_header = 'Content-Type'
124 content_type_header = 'Content-Type'
124 if content_type_header not in headers:
125 if content_type_header not in headers:
125 headers[content_type_header] = content_type
126 headers[content_type_header] = content_type
126
127
127 err_dict = {'id': retid, 'result': None, 'error': message}
128 err_dict = {'id': retid, 'result': None, 'error': message}
128 raw_body = render(DEFAULT_RENDERER, err_dict, request=request)
129 raw_body = render(DEFAULT_RENDERER, err_dict, request=request)
129
130
130 return Response(
131 return Response(
131 body=raw_body,
132 body=raw_body,
132 status=code,
133 status=code,
133 content_type=content_type,
134 content_type=content_type,
134 headerlist=[(k, v) for k, v in headers.items()]
135 headerlist=[(k, v) for k, v in headers.items()]
135 )
136 )
136
137
137
138
138 def exception_view(exc, request):
139 def exception_view(exc, request):
139 rpc_id = getattr(request, 'rpc_id', None)
140 rpc_id = getattr(request, 'rpc_id', None)
140
141
141 if isinstance(exc, JSONRPCError):
142 if isinstance(exc, JSONRPCError):
142 fault_message = safe_str(exc)
143 fault_message = safe_str(exc)
143 log.debug('json-rpc error rpc_id:%s "%s"', rpc_id, fault_message)
144 log.debug('json-rpc error rpc_id:%s "%s"', rpc_id, fault_message)
144 elif isinstance(exc, JSONRPCValidationError):
145 elif isinstance(exc, JSONRPCValidationError):
145 colander_exc = exc.colander_exception
146 colander_exc = exc.colander_exception
146 # TODO(marcink): think maybe of nicer way to serialize errors ?
147 # TODO(marcink): think maybe of nicer way to serialize errors ?
147 fault_message = colander_exc.asdict()
148 fault_message = colander_exc.asdict()
148 log.debug('json-rpc colander error rpc_id:%s "%s"', rpc_id, fault_message)
149 log.debug('json-rpc colander error rpc_id:%s "%s"', rpc_id, fault_message)
149 elif isinstance(exc, JSONRPCForbidden):
150 elif isinstance(exc, JSONRPCForbidden):
150 fault_message = 'Access was denied to this resource.'
151 fault_message = 'Access was denied to this resource.'
151 log.warning('json-rpc forbidden call rpc_id:%s "%s"', rpc_id, fault_message)
152 log.warning('json-rpc forbidden call rpc_id:%s "%s"', rpc_id, fault_message)
152 elif isinstance(exc, HTTPNotFound):
153 elif isinstance(exc, HTTPNotFound):
153 method = request.rpc_method
154 method = request.rpc_method
154 log.debug('json-rpc method `%s` not found in list of '
155 log.debug('json-rpc method `%s` not found in list of '
155 'api calls: %s, rpc_id:%s',
156 'api calls: %s, rpc_id:%s',
156 method, list(request.registry.jsonrpc_methods.keys()), rpc_id)
157 method, list(request.registry.jsonrpc_methods.keys()), rpc_id)
157
158
158 similar = 'none'
159 similar = 'none'
159 try:
160 try:
160 similar_paterns = [f'*{x}*' for x in method.split('_')]
161 similar_paterns = [f'*{x}*' for x in method.split('_')]
161 similar_found = find_methods(
162 similar_found = find_methods(
162 request.registry.jsonrpc_methods, similar_paterns)
163 request.registry.jsonrpc_methods, similar_paterns)
163 similar = ', '.join(similar_found.keys()) or similar
164 similar = ', '.join(similar_found.keys()) or similar
164 except Exception:
165 except Exception:
165 # make the whole above block safe
166 # make the whole above block safe
166 pass
167 pass
167
168
168 fault_message = f"No such method: {method}. Similar methods: {similar}"
169 fault_message = f"No such method: {method}. Similar methods: {similar}"
169 else:
170 else:
170 fault_message = 'undefined error'
171 fault_message = 'undefined error'
171 exc_info = exc.exc_info()
172 exc_info = exc.exc_info()
172 store_exception(id(exc_info), exc_info, prefix='rhodecode-api')
173 store_exception(id(exc_info), exc_info, prefix='rhodecode-api')
173
174
174 statsd = request.registry.statsd
175 statsd = request.registry.statsd
175 if statsd:
176 if statsd:
176 exc_type = f"{exc.__class__.__module__}.{exc.__class__.__name__}"
177 exc_type = f"{exc.__class__.__module__}.{exc.__class__.__name__}"
177 statsd.incr('rhodecode_exception_total',
178 statsd.incr('rhodecode_exception_total',
178 tags=["exc_source:api", f"type:{exc_type}"])
179 tags=["exc_source:api", f"type:{exc_type}"])
179
180
180 return jsonrpc_error(request, fault_message, rpc_id)
181 return jsonrpc_error(request, fault_message, rpc_id)
181
182
182
183
183 def request_view(request):
184 def request_view(request):
184 """
185 """
185 Main request handling method. It handles all logic to call a specific
186 Main request handling method. It handles all logic to call a specific
186 exposed method
187 exposed method
187 """
188 """
188 # cython compatible inspect
189 # cython compatible inspect
189 from rhodecode.config.patches import inspect_getargspec
190 inspect = inspect_getargspec()
190 inspect = inspect_getargspec()
191
191
192 # check if we can find this session using api_key, get_by_auth_token
192 # check if we can find this session using api_key, get_by_auth_token
193 # search not expired tokens only
193 # search not expired tokens only
194 try:
194 try:
195 if not request.rpc_method.startswith(SERVICE_API_IDENTIFIER):
195 if not request.rpc_method.startswith(SERVICE_API_IDENTIFIER):
196 api_user = User.get_by_auth_token(request.rpc_api_key)
196 api_user = User.get_by_auth_token(request.rpc_api_key)
197
197
198 if api_user is None:
198 if api_user is None:
199 return jsonrpc_error(
199 return jsonrpc_error(
200 request, retid=request.rpc_id, message='Invalid API KEY')
200 request, retid=request.rpc_id, message='Invalid API KEY')
201
201
202 if not api_user.active:
202 if not api_user.active:
203 return jsonrpc_error(
203 return jsonrpc_error(
204 request, retid=request.rpc_id,
204 request, retid=request.rpc_id,
205 message='Request from this user not allowed')
205 message='Request from this user not allowed')
206
206
207 # check if we are allowed to use this IP
207 # check if we are allowed to use this IP
208 auth_u = AuthUser(
208 auth_u = AuthUser(
209 api_user.user_id, request.rpc_api_key, ip_addr=request.rpc_ip_addr)
209 api_user.user_id, request.rpc_api_key, ip_addr=request.rpc_ip_addr)
210 if not auth_u.ip_allowed:
210 if not auth_u.ip_allowed:
211 return jsonrpc_error(
211 return jsonrpc_error(
212 request, retid=request.rpc_id,
212 request, retid=request.rpc_id,
213 message='Request from IP:{} not allowed'.format(
213 message='Request from IP:{} not allowed'.format(
214 request.rpc_ip_addr))
214 request.rpc_ip_addr))
215 else:
215 else:
216 log.info('Access for IP:%s allowed', request.rpc_ip_addr)
216 log.info('Access for IP:%s allowed', request.rpc_ip_addr)
217
217
218 # register our auth-user
218 # register our auth-user
219 request.rpc_user = auth_u
219 request.rpc_user = auth_u
220 request.environ['rc_auth_user_id'] = str(auth_u.user_id)
220 request.environ['rc_auth_user_id'] = str(auth_u.user_id)
221
221
222 # now check if token is valid for API
222 # now check if token is valid for API
223 auth_token = request.rpc_api_key
223 auth_token = request.rpc_api_key
224 token_match = api_user.authenticate_by_token(
224 token_match = api_user.authenticate_by_token(
225 auth_token, roles=[UserApiKeys.ROLE_API])
225 auth_token, roles=[UserApiKeys.ROLE_API])
226 invalid_token = not token_match
226 invalid_token = not token_match
227
227
228 log.debug('Checking if API KEY is valid with proper role')
228 log.debug('Checking if API KEY is valid with proper role')
229 if invalid_token:
229 if invalid_token:
230 return jsonrpc_error(
230 return jsonrpc_error(
231 request, retid=request.rpc_id,
231 request, retid=request.rpc_id,
232 message='API KEY invalid or, has bad role for an API call')
232 message='API KEY invalid or, has bad role for an API call')
233 else:
233 else:
234 auth_u = 'service'
234 auth_u = 'service'
235 if request.rpc_api_key != request.registry.settings['app.service_api.token']:
235 if request.rpc_api_key != request.registry.settings['app.service_api.token']:
236 raise Exception("Provided service secret is not recognized!")
236 raise Exception("Provided service secret is not recognized!")
237
237
238 except Exception:
238 except Exception:
239 log.exception('Error on API AUTH')
239 log.exception('Error on API AUTH')
240 return jsonrpc_error(
240 return jsonrpc_error(
241 request, retid=request.rpc_id, message='Invalid API KEY')
241 request, retid=request.rpc_id, message='Invalid API KEY')
242
242
243 method = request.rpc_method
243 method = request.rpc_method
244 func = request.registry.jsonrpc_methods[method]
244 func = request.registry.jsonrpc_methods[method]
245
245
246 # now that we have a method, add request._req_params to
246 # now that we have a method, add request._req_params to
247 # self.kargs and dispatch control to WGIController
247 # self.kargs and dispatch control to WGIController
248
248
249 argspec = inspect.getargspec(func)
249 argspec = inspect.getargspec(func)
250 arglist = argspec[0]
250 arglist = argspec[0]
251 defs = argspec[3] or []
251 defs = argspec[3] or []
252 defaults = [type(a) for a in defs]
252 defaults = [type(a) for a in defs]
253 default_empty = type(NotImplemented)
253 default_empty = type(NotImplemented)
254
254
255 # kw arguments required by this method
255 # kw arguments required by this method
256 func_kwargs = dict(itertools.zip_longest(
256 func_kwargs = dict(itertools.zip_longest(
257 reversed(arglist), reversed(defaults), fillvalue=default_empty))
257 reversed(arglist), reversed(defaults), fillvalue=default_empty))
258
258
259 # This attribute will need to be first param of a method that uses
259 # This attribute will need to be first param of a method that uses
260 # api_key, which is translated to instance of user at that name
260 # api_key, which is translated to instance of user at that name
261 user_var = 'apiuser'
261 user_var = 'apiuser'
262 request_var = 'request'
262 request_var = 'request'
263
263
264 for arg in [user_var, request_var]:
264 for arg in [user_var, request_var]:
265 if arg not in arglist:
265 if arg not in arglist:
266 return jsonrpc_error(
266 return jsonrpc_error(
267 request,
267 request,
268 retid=request.rpc_id,
268 retid=request.rpc_id,
269 message='This method [%s] does not support '
269 message='This method [%s] does not support '
270 'required parameter `%s`' % (func.__name__, arg))
270 'required parameter `%s`' % (func.__name__, arg))
271
271
272 # get our arglist and check if we provided them as args
272 # get our arglist and check if we provided them as args
273 for arg, default in func_kwargs.items():
273 for arg, default in func_kwargs.items():
274 if arg in [user_var, request_var]:
274 if arg in [user_var, request_var]:
275 # user_var and request_var are pre-hardcoded parameters and we
275 # user_var and request_var are pre-hardcoded parameters and we
276 # don't need to do any translation
276 # don't need to do any translation
277 continue
277 continue
278
278
279 # skip the required param check if it's default value is
279 # skip the required param check if it's default value is
280 # NotImplementedType (default_empty)
280 # NotImplementedType (default_empty)
281 if default == default_empty and arg not in request.rpc_params:
281 if default == default_empty and arg not in request.rpc_params:
282 return jsonrpc_error(
282 return jsonrpc_error(
283 request,
283 request,
284 retid=request.rpc_id,
284 retid=request.rpc_id,
285 message=('Missing non optional `%s` arg in JSON DATA' % arg)
285 message=('Missing non optional `%s` arg in JSON DATA' % arg)
286 )
286 )
287
287
288 # sanitize extra passed arguments
288 # sanitize extra passed arguments
289 for k in list(request.rpc_params.keys()):
289 for k in list(request.rpc_params.keys()):
290 if k not in func_kwargs:
290 if k not in func_kwargs:
291 del request.rpc_params[k]
291 del request.rpc_params[k]
292
292
293 call_params = request.rpc_params
293 call_params = request.rpc_params
294 call_params.update({
294 call_params.update({
295 'request': request,
295 'request': request,
296 'apiuser': auth_u
296 'apiuser': auth_u
297 })
297 })
298
298
299 # register some common functions for usage
299 # register some common functions for usage
300 rpc_user = request.rpc_user.user_id if hasattr(request, 'rpc_user') else None
300 rpc_user = request.rpc_user.user_id if hasattr(request, 'rpc_user') else None
301 attach_context_attributes(TemplateArgs(), request, rpc_user)
301 attach_context_attributes(TemplateArgs(), request, rpc_user)
302
302
303 statsd = request.registry.statsd
303 statsd = request.registry.statsd
304
304
305 try:
305 try:
306 ret_value = func(**call_params)
306 ret_value = func(**call_params)
307 resp = jsonrpc_response(request, ret_value)
307 resp = jsonrpc_response(request, ret_value)
308 if statsd:
308 if statsd:
309 statsd.incr('rhodecode_api_call_success_total')
309 statsd.incr('rhodecode_api_call_success_total')
310 return resp
310 return resp
311 except JSONRPCBaseError:
311 except JSONRPCBaseError:
312 raise
312 raise
313 except Exception:
313 except Exception:
314 log.exception('Unhandled exception occurred on api call: %s', func)
314 log.exception('Unhandled exception occurred on api call: %s', func)
315 exc_info = sys.exc_info()
315 exc_info = sys.exc_info()
316 exc_id, exc_type_name = store_exception(
316 exc_id, exc_type_name = store_exception(
317 id(exc_info), exc_info, prefix='rhodecode-api')
317 id(exc_info), exc_info, prefix='rhodecode-api')
318 error_headers = {
318 error_headers = {
319 'RhodeCode-Exception-Id': str(exc_id),
319 'RhodeCode-Exception-Id': str(exc_id),
320 'RhodeCode-Exception-Type': str(exc_type_name)
320 'RhodeCode-Exception-Type': str(exc_type_name)
321 }
321 }
322 err_resp = jsonrpc_error(
322 err_resp = jsonrpc_error(
323 request, retid=request.rpc_id, message='Internal server error',
323 request, retid=request.rpc_id, message='Internal server error',
324 headers=error_headers)
324 headers=error_headers)
325 if statsd:
325 if statsd:
326 statsd.incr('rhodecode_api_call_fail_total')
326 statsd.incr('rhodecode_api_call_fail_total')
327 return err_resp
327 return err_resp
328
328
329
329
330 def setup_request(request):
330 def setup_request(request):
331 """
331 """
332 Parse a JSON-RPC request body. It's used inside the predicates method
332 Parse a JSON-RPC request body. It's used inside the predicates method
333 to validate and bootstrap requests for usage in rpc calls.
333 to validate and bootstrap requests for usage in rpc calls.
334
334
335 We need to raise JSONRPCError here if we want to return some errors back to
335 We need to raise JSONRPCError here if we want to return some errors back to
336 user.
336 user.
337 """
337 """
338
338
339 log.debug('Executing setup request: %r', request)
339 log.debug('Executing setup request: %r', request)
340 request.rpc_ip_addr = get_ip_addr(request.environ)
340 request.rpc_ip_addr = get_ip_addr(request.environ)
341 # TODO(marcink): deprecate GET at some point
341 # TODO(marcink): deprecate GET at some point
342 if request.method not in ['POST', 'GET']:
342 if request.method not in ['POST', 'GET']:
343 log.debug('unsupported request method "%s"', request.method)
343 log.debug('unsupported request method "%s"', request.method)
344 raise JSONRPCError(
344 raise JSONRPCError(
345 'unsupported request method "%s". Please use POST' % request.method)
345 'unsupported request method "%s". Please use POST' % request.method)
346
346
347 if 'CONTENT_LENGTH' not in request.environ:
347 if 'CONTENT_LENGTH' not in request.environ:
348 log.debug("No Content-Length")
348 log.debug("No Content-Length")
349 raise JSONRPCError("Empty body, No Content-Length in request")
349 raise JSONRPCError("Empty body, No Content-Length in request")
350
350
351 else:
351 else:
352 length = request.environ['CONTENT_LENGTH']
352 length = request.environ['CONTENT_LENGTH']
353 log.debug('Content-Length: %s', length)
353 log.debug('Content-Length: %s', length)
354
354
355 if length == 0:
355 if length == 0:
356 log.debug("Content-Length is 0")
356 log.debug("Content-Length is 0")
357 raise JSONRPCError("Content-Length is 0")
357 raise JSONRPCError("Content-Length is 0")
358
358
359 raw_body = request.body
359 raw_body = request.body
360 log.debug("Loading JSON body now")
360 log.debug("Loading JSON body now")
361 try:
361 try:
362 json_body = ext_json.json.loads(raw_body)
362 json_body = ext_json.json.loads(raw_body)
363 except ValueError as e:
363 except ValueError as e:
364 # catch JSON errors Here
364 # catch JSON errors Here
365 raise JSONRPCError(f"JSON parse error ERR:{e} RAW:{raw_body!r}")
365 raise JSONRPCError(f"JSON parse error ERR:{e} RAW:{raw_body!r}")
366
366
367 request.rpc_id = json_body.get('id')
367 request.rpc_id = json_body.get('id')
368 request.rpc_method = json_body.get('method')
368 request.rpc_method = json_body.get('method')
369
369
370 # check required base parameters
370 # check required base parameters
371 try:
371 try:
372 api_key = json_body.get('api_key')
372 api_key = json_body.get('api_key')
373 if not api_key:
373 if not api_key:
374 api_key = json_body.get('auth_token')
374 api_key = json_body.get('auth_token')
375
375
376 if not api_key:
376 if not api_key:
377 raise KeyError('api_key or auth_token')
377 raise KeyError('api_key or auth_token')
378
378
379 # TODO(marcink): support passing in token in request header
379 # TODO(marcink): support passing in token in request header
380
380
381 request.rpc_api_key = api_key
381 request.rpc_api_key = api_key
382 request.rpc_id = json_body['id']
382 request.rpc_id = json_body['id']
383 request.rpc_method = json_body['method']
383 request.rpc_method = json_body['method']
384 request.rpc_params = json_body['args'] \
384 request.rpc_params = json_body['args'] \
385 if isinstance(json_body['args'], dict) else {}
385 if isinstance(json_body['args'], dict) else {}
386
386
387 log.debug('method: %s, params: %.10240r', request.rpc_method, request.rpc_params)
387 log.debug('method: %s, params: %.10240r', request.rpc_method, request.rpc_params)
388 except KeyError as e:
388 except KeyError as e:
389 raise JSONRPCError(f'Incorrect JSON data. Missing {e}')
389 raise JSONRPCError(f'Incorrect JSON data. Missing {e}')
390
390
391 log.debug('setup complete, now handling method:%s rpcid:%s',
391 log.debug('setup complete, now handling method:%s rpcid:%s',
392 request.rpc_method, request.rpc_id, )
392 request.rpc_method, request.rpc_id, )
393
393
394
394
395 class RoutePredicate(object):
395 class RoutePredicate(object):
396 def __init__(self, val, config):
396 def __init__(self, val, config):
397 self.val = val
397 self.val = val
398
398
399 def text(self):
399 def text(self):
400 return f'jsonrpc route = {self.val}'
400 return f'jsonrpc route = {self.val}'
401
401
402 phash = text
402 phash = text
403
403
404 def __call__(self, info, request):
404 def __call__(self, info, request):
405 if self.val:
405 if self.val:
406 # potentially setup and bootstrap our call
406 # potentially setup and bootstrap our call
407 setup_request(request)
407 setup_request(request)
408
408
409 # Always return True so that even if it isn't a valid RPC it
409 # Always return True so that even if it isn't a valid RPC it
410 # will fall through to the underlaying handlers like notfound_view
410 # will fall through to the underlaying handlers like notfound_view
411 return True
411 return True
412
412
413
413
414 class NotFoundPredicate(object):
414 class NotFoundPredicate(object):
415 def __init__(self, val, config):
415 def __init__(self, val, config):
416 self.val = val
416 self.val = val
417 self.methods = config.registry.jsonrpc_methods
417 self.methods = config.registry.jsonrpc_methods
418
418
419 def text(self):
419 def text(self):
420 return f'jsonrpc method not found = {self.val}'
420 return f'jsonrpc method not found = {self.val}'
421
421
422 phash = text
422 phash = text
423
423
424 def __call__(self, info, request):
424 def __call__(self, info, request):
425 return hasattr(request, 'rpc_method')
425 return hasattr(request, 'rpc_method')
426
426
427
427
428 class MethodPredicate(object):
428 class MethodPredicate(object):
429 def __init__(self, val, config):
429 def __init__(self, val, config):
430 self.method = val
430 self.method = val
431
431
432 def text(self):
432 def text(self):
433 return f'jsonrpc method = {self.method}'
433 return f'jsonrpc method = {self.method}'
434
434
435 phash = text
435 phash = text
436
436
437 def __call__(self, context, request):
437 def __call__(self, context, request):
438 # we need to explicitly return False here, so pyramid doesn't try to
438 # we need to explicitly return False here, so pyramid doesn't try to
439 # execute our view directly. We need our main handler to execute things
439 # execute our view directly. We need our main handler to execute things
440 return getattr(request, 'rpc_method') == self.method
440 return getattr(request, 'rpc_method') == self.method
441
441
442
442
443 def add_jsonrpc_method(config, view, **kwargs):
443 def add_jsonrpc_method(config, view, **kwargs):
444 # pop the method name
444 # pop the method name
445 method = kwargs.pop('method', None)
445 method = kwargs.pop('method', None)
446
446
447 if method is None:
447 if method is None:
448 raise ConfigurationError(
448 raise ConfigurationError(
449 'Cannot register a JSON-RPC method without specifying the "method"')
449 'Cannot register a JSON-RPC method without specifying the "method"')
450
450
451 # we define custom predicate, to enable to detect conflicting methods,
451 # we define custom predicate, to enable to detect conflicting methods,
452 # those predicates are kind of "translation" from the decorator variables
452 # those predicates are kind of "translation" from the decorator variables
453 # to internal predicates names
453 # to internal predicates names
454
454
455 kwargs['jsonrpc_method'] = method
455 kwargs['jsonrpc_method'] = method
456
456
457 # register our view into global view store for validation
457 # register our view into global view store for validation
458 config.registry.jsonrpc_methods[method] = view
458 config.registry.jsonrpc_methods[method] = view
459
459
460 # we're using our main request_view handler, here, so each method
460 # we're using our main request_view handler, here, so each method
461 # has a unified handler for itself
461 # has a unified handler for itself
462 config.add_view(request_view, route_name='apiv2', **kwargs)
462 config.add_view(request_view, route_name='apiv2', **kwargs)
463
463
464
464
465 class jsonrpc_method(object):
465 class jsonrpc_method(object):
466 """
466 """
467 decorator that works similar to @add_view_config decorator,
467 decorator that works similar to @add_view_config decorator,
468 but tailored for our JSON RPC
468 but tailored for our JSON RPC
469 """
469 """
470
470
471 venusian = venusian # for testing injection
471 venusian = venusian # for testing injection
472
472
473 def __init__(self, method=None, **kwargs):
473 def __init__(self, method=None, **kwargs):
474 self.method = method
474 self.method = method
475 self.kwargs = kwargs
475 self.kwargs = kwargs
476
476
477 def __call__(self, wrapped):
477 def __call__(self, wrapped):
478 kwargs = self.kwargs.copy()
478 kwargs = self.kwargs.copy()
479 kwargs['method'] = self.method or wrapped.__name__
479 kwargs['method'] = self.method or wrapped.__name__
480 depth = kwargs.pop('_depth', 0)
480 depth = kwargs.pop('_depth', 0)
481
481
482 def callback(context, name, ob):
482 def callback(context, name, ob):
483 config = context.config.with_package(info.module)
483 config = context.config.with_package(info.module)
484 config.add_jsonrpc_method(view=ob, **kwargs)
484 config.add_jsonrpc_method(view=ob, **kwargs)
485
485
486 info = venusian.attach(wrapped, callback, category='pyramid',
486 info = venusian.attach(wrapped, callback, category='pyramid',
487 depth=depth + 1)
487 depth=depth + 1)
488 if info.scope == 'class':
488 if info.scope == 'class':
489 # ensure that attr is set if decorating a class method
489 # ensure that attr is set if decorating a class method
490 kwargs.setdefault('attr', wrapped.__name__)
490 kwargs.setdefault('attr', wrapped.__name__)
491
491
492 kwargs['_info'] = info.codeinfo # fbo action_method
492 kwargs['_info'] = info.codeinfo # fbo action_method
493 return wrapped
493 return wrapped
494
494
495
495
496 class jsonrpc_deprecated_method(object):
496 class jsonrpc_deprecated_method(object):
497 """
497 """
498 Marks method as deprecated, adds log.warning, and inject special key to
498 Marks method as deprecated, adds log.warning, and inject special key to
499 the request variable to mark method as deprecated.
499 the request variable to mark method as deprecated.
500 Also injects special docstring that extract_docs will catch to mark
500 Also injects special docstring that extract_docs will catch to mark
501 method as deprecated.
501 method as deprecated.
502
502
503 :param use_method: specify which method should be used instead of
503 :param use_method: specify which method should be used instead of
504 the decorated one
504 the decorated one
505
505
506 Use like::
506 Use like::
507
507
508 @jsonrpc_method()
508 @jsonrpc_method()
509 @jsonrpc_deprecated_method(use_method='new_func', deprecated_at_version='3.0.0')
509 @jsonrpc_deprecated_method(use_method='new_func', deprecated_at_version='3.0.0')
510 def old_func(request, apiuser, arg1, arg2):
510 def old_func(request, apiuser, arg1, arg2):
511 ...
511 ...
512 """
512 """
513
513
514 def __init__(self, use_method, deprecated_at_version):
514 def __init__(self, use_method, deprecated_at_version):
515 self.use_method = use_method
515 self.use_method = use_method
516 self.deprecated_at_version = deprecated_at_version
516 self.deprecated_at_version = deprecated_at_version
517 self.deprecated_msg = ''
517 self.deprecated_msg = ''
518
518
519 def __call__(self, func):
519 def __call__(self, func):
520 self.deprecated_msg = 'Please use method `{method}` instead.'.format(
520 self.deprecated_msg = 'Please use method `{method}` instead.'.format(
521 method=self.use_method)
521 method=self.use_method)
522
522
523 docstring = """\n
523 docstring = """\n
524 .. deprecated:: {version}
524 .. deprecated:: {version}
525
525
526 {deprecation_message}
526 {deprecation_message}
527
527
528 {original_docstring}
528 {original_docstring}
529 """
529 """
530 func.__doc__ = docstring.format(
530 func.__doc__ = docstring.format(
531 version=self.deprecated_at_version,
531 version=self.deprecated_at_version,
532 deprecation_message=self.deprecated_msg,
532 deprecation_message=self.deprecated_msg,
533 original_docstring=func.__doc__)
533 original_docstring=func.__doc__)
534 return decorator.decorator(self.__wrapper, func)
534 return decorator.decorator(self.__wrapper, func)
535
535
536 def __wrapper(self, func, *fargs, **fkwargs):
536 def __wrapper(self, func, *fargs, **fkwargs):
537 log.warning('DEPRECATED API CALL on function %s, please '
537 log.warning('DEPRECATED API CALL on function %s, please '
538 'use `%s` instead', func, self.use_method)
538 'use `%s` instead', func, self.use_method)
539 # alter function docstring to mark as deprecated, this is picked up
539 # alter function docstring to mark as deprecated, this is picked up
540 # via fabric file that generates API DOC.
540 # via fabric file that generates API DOC.
541 result = func(*fargs, **fkwargs)
541 result = func(*fargs, **fkwargs)
542
542
543 request = fargs[0]
543 request = fargs[0]
544 request.rpc_deprecation = 'DEPRECATED METHOD ' + self.deprecated_msg
544 request.rpc_deprecation = 'DEPRECATED METHOD ' + self.deprecated_msg
545 return result
545 return result
546
546
547
547
548 def add_api_methods(config):
548 def add_api_methods(config):
549 from rhodecode.api.views import (
549 from rhodecode.api.views import (
550 deprecated_api, gist_api, pull_request_api, repo_api, repo_group_api,
550 deprecated_api, gist_api, pull_request_api, repo_api, repo_group_api,
551 server_api, search_api, testing_api, user_api, user_group_api)
551 server_api, search_api, testing_api, user_api, user_group_api)
552
552
553 config.scan('rhodecode.api.views')
553 config.scan('rhodecode.api.views')
554
554
555
555
556 def includeme(config):
556 def includeme(config):
557 plugin_module = 'rhodecode.api'
557 plugin_module = 'rhodecode.api'
558 plugin_settings = get_plugin_settings(
558 plugin_settings = get_plugin_settings(
559 plugin_module, config.registry.settings)
559 plugin_module, config.registry.settings)
560
560
561 if not hasattr(config.registry, 'jsonrpc_methods'):
561 if not hasattr(config.registry, 'jsonrpc_methods'):
562 config.registry.jsonrpc_methods = OrderedDict()
562 config.registry.jsonrpc_methods = OrderedDict()
563
563
564 # match filter by given method only
564 # match filter by given method only
565 config.add_view_predicate('jsonrpc_method', MethodPredicate)
565 config.add_view_predicate('jsonrpc_method', MethodPredicate)
566 config.add_view_predicate('jsonrpc_method_not_found', NotFoundPredicate)
566 config.add_view_predicate('jsonrpc_method_not_found', NotFoundPredicate)
567
567
568 config.add_renderer(DEFAULT_RENDERER, ExtJsonRenderer())
568 config.add_renderer(DEFAULT_RENDERER, ExtJsonRenderer())
569 config.add_directive('add_jsonrpc_method', add_jsonrpc_method)
569 config.add_directive('add_jsonrpc_method', add_jsonrpc_method)
570
570
571 config.add_route_predicate(
571 config.add_route_predicate(
572 'jsonrpc_call', RoutePredicate)
572 'jsonrpc_call', RoutePredicate)
573
573
574 config.add_route(
574 config.add_route(
575 'apiv2', plugin_settings.get('url', DEFAULT_URL), jsonrpc_call=True)
575 'apiv2', plugin_settings.get('url', DEFAULT_URL), jsonrpc_call=True)
576
576
577 # register some exception handling view
577 # register some exception handling view
578 config.add_view(exception_view, context=JSONRPCBaseError)
578 config.add_view(exception_view, context=JSONRPCBaseError)
579 config.add_notfound_view(exception_view, jsonrpc_method_not_found=True)
579 config.add_notfound_view(exception_view, jsonrpc_method_not_found=True)
580
580
581 add_api_methods(config)
581 add_api_methods(config)
@@ -1,423 +1,423 b''
1 # Copyright (C) 2011-2023 RhodeCode GmbH
1 # Copyright (C) 2011-2023 RhodeCode GmbH
2 #
2 #
3 # This program is free software: you can redistribute it and/or modify
3 # This program is free software: you can redistribute it and/or modify
4 # it under the terms of the GNU Affero General Public License, version 3
4 # it under the terms of the GNU Affero General Public License, version 3
5 # (only), as published by the Free Software Foundation.
5 # (only), as published by the Free Software Foundation.
6 #
6 #
7 # This program is distributed in the hope that it will be useful,
7 # This program is distributed in the hope that it will be useful,
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # GNU General Public License for more details.
10 # GNU General Public License for more details.
11 #
11 #
12 # You should have received a copy of the GNU Affero General Public License
12 # You should have received a copy of the GNU Affero General Public License
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 #
14 #
15 # This program is dual-licensed. If you wish to learn more about the
15 # This program is dual-licensed. If you wish to learn more about the
16 # RhodeCode Enterprise Edition, including its added features, Support services,
16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18
18
19 import logging
19 import logging
20 import itertools
20 import itertools
21 import base64
21 import base64
22
22
23 from rhodecode.api import (
23 from rhodecode.api import (
24 jsonrpc_method, JSONRPCError, JSONRPCForbidden, find_methods)
24 jsonrpc_method, JSONRPCError, JSONRPCForbidden, find_methods)
25
25
26 from rhodecode.api.utils import (
26 from rhodecode.api.utils import (
27 Optional, OAttr, has_superadmin_permission, get_user_or_error)
27 Optional, OAttr, has_superadmin_permission, get_user_or_error)
28 from rhodecode.lib.utils import repo2db_mapper, get_rhodecode_repo_store_path
28 from rhodecode.lib.utils import repo2db_mapper, get_rhodecode_repo_store_path
29 from rhodecode.lib import system_info
29 from rhodecode.lib import system_info
30 from rhodecode.lib import user_sessions
30 from rhodecode.lib import user_sessions
31 from rhodecode.lib import exc_tracking
31 from rhodecode.lib import exc_tracking
32 from rhodecode.lib.ext_json import json
32 from rhodecode.lib.ext_json import json
33 from rhodecode.lib.utils2 import safe_int
33 from rhodecode.lib.utils2 import safe_int
34 from rhodecode.model.db import UserIpMap
34 from rhodecode.model.db import UserIpMap
35 from rhodecode.model.scm import ScmModel
35 from rhodecode.model.scm import ScmModel
36 from rhodecode.apps.file_store import utils
36 from rhodecode.apps.file_store import utils
37 from rhodecode.apps.file_store.exceptions import FileNotAllowedException, \
37 from rhodecode.apps.file_store.exceptions import FileNotAllowedException, \
38 FileOverSizeException
38 FileOverSizeException
39
39
40 log = logging.getLogger(__name__)
40 log = logging.getLogger(__name__)
41
41
42
42
43 @jsonrpc_method()
43 @jsonrpc_method()
44 def get_server_info(request, apiuser):
44 def get_server_info(request, apiuser):
45 """
45 """
46 Returns the |RCE| server information.
46 Returns the |RCE| server information.
47
47
48 This includes the running version of |RCE| and all installed
48 This includes the running version of |RCE| and all installed
49 packages. This command takes the following options:
49 packages. This command takes the following options:
50
50
51 :param apiuser: This is filled automatically from the |authtoken|.
51 :param apiuser: This is filled automatically from the |authtoken|.
52 :type apiuser: AuthUser
52 :type apiuser: AuthUser
53
53
54 Example output:
54 Example output:
55
55
56 .. code-block:: bash
56 .. code-block:: bash
57
57
58 id : <id_given_in_input>
58 id : <id_given_in_input>
59 result : {
59 result : {
60 'modules': [<module name>,...]
60 'modules': [<module name>,...]
61 'py_version': <python version>,
61 'py_version': <python version>,
62 'platform': <platform type>,
62 'platform': <platform type>,
63 'rhodecode_version': <rhodecode version>
63 'rhodecode_version': <rhodecode version>
64 }
64 }
65 error : null
65 error : null
66 """
66 """
67
67
68 if not has_superadmin_permission(apiuser):
68 if not has_superadmin_permission(apiuser):
69 raise JSONRPCForbidden()
69 raise JSONRPCForbidden()
70
70
71 server_info = ScmModel().get_server_info(request.environ)
71 server_info = ScmModel().get_server_info(request.environ)
72 # rhodecode-index requires those
72 # rhodecode-index requires those
73
73
74 server_info['index_storage'] = server_info['search']['value']['location']
74 server_info['index_storage'] = server_info['search']['value']['location']
75 server_info['storage'] = server_info['storage']['value']['path']
75 server_info['storage'] = server_info['storage']['value']['path']
76
76
77 return server_info
77 return server_info
78
78
79
79
80 @jsonrpc_method()
80 @jsonrpc_method()
81 def get_repo_store(request, apiuser):
81 def get_repo_store(request, apiuser):
82 """
82 """
83 Returns the |RCE| repository storage information.
83 Returns the |RCE| repository storage information.
84
84
85 :param apiuser: This is filled automatically from the |authtoken|.
85 :param apiuser: This is filled automatically from the |authtoken|.
86 :type apiuser: AuthUser
86 :type apiuser: AuthUser
87
87
88 Example output:
88 Example output:
89
89
90 .. code-block:: bash
90 .. code-block:: bash
91
91
92 id : <id_given_in_input>
92 id : <id_given_in_input>
93 result : {
93 result : {
94 'modules': [<module name>,...]
94 'modules': [<module name>,...]
95 'py_version': <python version>,
95 'py_version': <python version>,
96 'platform': <platform type>,
96 'platform': <platform type>,
97 'rhodecode_version': <rhodecode version>
97 'rhodecode_version': <rhodecode version>
98 }
98 }
99 error : null
99 error : null
100 """
100 """
101
101
102 if not has_superadmin_permission(apiuser):
102 if not has_superadmin_permission(apiuser):
103 raise JSONRPCForbidden()
103 raise JSONRPCForbidden()
104
104
105 path = get_rhodecode_repo_store_path()
105 path = get_rhodecode_repo_store_path()
106 return {"path": path}
106 return {"path": path}
107
107
108
108
109 @jsonrpc_method()
109 @jsonrpc_method()
110 def get_ip(request, apiuser, userid=Optional(OAttr('apiuser'))):
110 def get_ip(request, apiuser, userid=Optional(OAttr('apiuser'))):
111 """
111 """
112 Displays the IP Address as seen from the |RCE| server.
112 Displays the IP Address as seen from the |RCE| server.
113
113
114 * This command displays the IP Address, as well as all the defined IP
114 * This command displays the IP Address, as well as all the defined IP
115 addresses for the specified user. If the ``userid`` is not set, the
115 addresses for the specified user. If the ``userid`` is not set, the
116 data returned is for the user calling the method.
116 data returned is for the user calling the method.
117
117
118 This command can only be run using an |authtoken| with admin rights to
118 This command can only be run using an |authtoken| with admin rights to
119 the specified repository.
119 the specified repository.
120
120
121 This command takes the following options:
121 This command takes the following options:
122
122
123 :param apiuser: This is filled automatically from |authtoken|.
123 :param apiuser: This is filled automatically from |authtoken|.
124 :type apiuser: AuthUser
124 :type apiuser: AuthUser
125 :param userid: Sets the userid for which associated IP Address data
125 :param userid: Sets the userid for which associated IP Address data
126 is returned.
126 is returned.
127 :type userid: Optional(str or int)
127 :type userid: Optional(str or int)
128
128
129 Example output:
129 Example output:
130
130
131 .. code-block:: bash
131 .. code-block:: bash
132
132
133 id : <id_given_in_input>
133 id : <id_given_in_input>
134 result : {
134 result : {
135 "server_ip_addr": "<ip_from_clien>",
135 "server_ip_addr": "<ip_from_clien>",
136 "user_ips": [
136 "user_ips": [
137 {
137 {
138 "ip_addr": "<ip_with_mask>",
138 "ip_addr": "<ip_with_mask>",
139 "ip_range": ["<start_ip>", "<end_ip>"],
139 "ip_range": ["<start_ip>", "<end_ip>"],
140 },
140 },
141 ...
141 ...
142 ]
142 ]
143 }
143 }
144
144
145 """
145 """
146 if not has_superadmin_permission(apiuser):
146 if not has_superadmin_permission(apiuser):
147 raise JSONRPCForbidden()
147 raise JSONRPCForbidden()
148
148
149 userid = Optional.extract(userid, evaluate_locals=locals())
149 userid = Optional.extract(userid, evaluate_locals=locals())
150 userid = getattr(userid, 'user_id', userid)
150 userid = getattr(userid, 'user_id', userid)
151
151
152 user = get_user_or_error(userid)
152 user = get_user_or_error(userid)
153 ips = UserIpMap.query().filter(UserIpMap.user == user).all()
153 ips = UserIpMap.query().filter(UserIpMap.user == user).all()
154 return {
154 return {
155 'server_ip_addr': request.rpc_ip_addr,
155 'server_ip_addr': request.rpc_ip_addr,
156 'user_ips': ips
156 'user_ips': ips
157 }
157 }
158
158
159
159
160 @jsonrpc_method()
160 @jsonrpc_method()
161 def rescan_repos(request, apiuser, remove_obsolete=Optional(False)):
161 def rescan_repos(request, apiuser, remove_obsolete=Optional(False)):
162 """
162 """
163 Triggers a rescan of the specified repositories.
163 Triggers a rescan of the specified repositories.
164
164
165 * If the ``remove_obsolete`` option is set, it also deletes repositories
165 * If the ``remove_obsolete`` option is set, it also deletes repositories
166 that are found in the database but not on the file system, so called
166 that are found in the database but not on the file system, so called
167 "clean zombies".
167 "clean zombies".
168
168
169 This command can only be run using an |authtoken| with admin rights to
169 This command can only be run using an |authtoken| with admin rights to
170 the specified repository.
170 the specified repository.
171
171
172 This command takes the following options:
172 This command takes the following options:
173
173
174 :param apiuser: This is filled automatically from the |authtoken|.
174 :param apiuser: This is filled automatically from the |authtoken|.
175 :type apiuser: AuthUser
175 :type apiuser: AuthUser
176 :param remove_obsolete: Deletes repositories from the database that
176 :param remove_obsolete: Deletes repositories from the database that
177 are not found on the filesystem.
177 are not found on the filesystem.
178 :type remove_obsolete: Optional(``True`` | ``False``)
178 :type remove_obsolete: Optional(``True`` | ``False``)
179
179
180 Example output:
180 Example output:
181
181
182 .. code-block:: bash
182 .. code-block:: bash
183
183
184 id : <id_given_in_input>
184 id : <id_given_in_input>
185 result : {
185 result : {
186 'added': [<added repository name>,...]
186 'added': [<added repository name>,...]
187 'removed': [<removed repository name>,...]
187 'removed': [<removed repository name>,...]
188 }
188 }
189 error : null
189 error : null
190
190
191 Example error output:
191 Example error output:
192
192
193 .. code-block:: bash
193 .. code-block:: bash
194
194
195 id : <id_given_in_input>
195 id : <id_given_in_input>
196 result : null
196 result : null
197 error : {
197 error : {
198 'Error occurred during rescan repositories action'
198 'Error occurred during rescan repositories action'
199 }
199 }
200
200
201 """
201 """
202 if not has_superadmin_permission(apiuser):
202 if not has_superadmin_permission(apiuser):
203 raise JSONRPCForbidden()
203 raise JSONRPCForbidden()
204
204
205 try:
205 try:
206 rm_obsolete = Optional.extract(remove_obsolete)
206 rm_obsolete = Optional.extract(remove_obsolete)
207 added, removed = repo2db_mapper(ScmModel().repo_scan(),
207 added, removed = repo2db_mapper(ScmModel().repo_scan(),
208 remove_obsolete=rm_obsolete, force_hooks_rebuild=True)
208 remove_obsolete=rm_obsolete, force_hooks_rebuild=True)
209 return {'added': added, 'removed': removed}
209 return {'added': added, 'removed': removed}
210 except Exception:
210 except Exception:
211 log.exception('Failed to run repo rescann')
211 log.exception('Failed to run repo rescann')
212 raise JSONRPCError(
212 raise JSONRPCError(
213 'Error occurred during rescan repositories action'
213 'Error occurred during rescan repositories action'
214 )
214 )
215
215
216
216
217 @jsonrpc_method()
217 @jsonrpc_method()
218 def cleanup_sessions(request, apiuser, older_then=Optional(60)):
218 def cleanup_sessions(request, apiuser, older_then=Optional(60)):
219 """
219 """
220 Triggers a session cleanup action.
220 Triggers a session cleanup action.
221
221
222 If the ``older_then`` option is set, only sessions that hasn't been
222 If the ``older_then`` option is set, only sessions that hasn't been
223 accessed in the given number of days will be removed.
223 accessed in the given number of days will be removed.
224
224
225 This command can only be run using an |authtoken| with admin rights to
225 This command can only be run using an |authtoken| with admin rights to
226 the specified repository.
226 the specified repository.
227
227
228 This command takes the following options:
228 This command takes the following options:
229
229
230 :param apiuser: This is filled automatically from the |authtoken|.
230 :param apiuser: This is filled automatically from the |authtoken|.
231 :type apiuser: AuthUser
231 :type apiuser: AuthUser
232 :param older_then: Deletes session that hasn't been accessed
232 :param older_then: Deletes session that hasn't been accessed
233 in given number of days.
233 in given number of days.
234 :type older_then: Optional(int)
234 :type older_then: Optional(int)
235
235
236 Example output:
236 Example output:
237
237
238 .. code-block:: bash
238 .. code-block:: bash
239
239
240 id : <id_given_in_input>
240 id : <id_given_in_input>
241 result: {
241 result: {
242 "backend": "<type of backend>",
242 "backend": "<type of backend>",
243 "sessions_removed": <number_of_removed_sessions>
243 "sessions_removed": <number_of_removed_sessions>
244 }
244 }
245 error : null
245 error : null
246
246
247 Example error output:
247 Example error output:
248
248
249 .. code-block:: bash
249 .. code-block:: bash
250
250
251 id : <id_given_in_input>
251 id : <id_given_in_input>
252 result : null
252 result : null
253 error : {
253 error : {
254 'Error occurred during session cleanup'
254 'Error occurred during session cleanup'
255 }
255 }
256
256
257 """
257 """
258 if not has_superadmin_permission(apiuser):
258 if not has_superadmin_permission(apiuser):
259 raise JSONRPCForbidden()
259 raise JSONRPCForbidden()
260
260
261 older_then = safe_int(Optional.extract(older_then)) or 60
261 older_then = safe_int(Optional.extract(older_then)) or 60
262 older_than_seconds = 60 * 60 * 24 * older_then
262 older_than_seconds = 60 * 60 * 24 * older_then
263
263
264 config = system_info.rhodecode_config().get_value()['value']['config']
264 config = system_info.rhodecode_config().get_value()['value']['config']
265 session_model = user_sessions.get_session_handler(
265 session_model = user_sessions.get_session_handler(
266 config.get('beaker.session.type', 'memory'))(config)
266 config.get('beaker.session.type', 'memory'))(config)
267
267
268 backend = session_model.SESSION_TYPE
268 backend = session_model.SESSION_TYPE
269 try:
269 try:
270 cleaned = session_model.clean_sessions(
270 cleaned = session_model.clean_sessions(
271 older_than_seconds=older_than_seconds)
271 older_than_seconds=older_than_seconds)
272 return {'sessions_removed': cleaned, 'backend': backend}
272 return {'sessions_removed': cleaned, 'backend': backend}
273 except user_sessions.CleanupCommand as msg:
273 except user_sessions.CleanupCommand as msg:
274 return {'cleanup_command': str(msg), 'backend': backend}
274 return {'cleanup_command': str(msg), 'backend': backend}
275 except Exception as e:
275 except Exception as e:
276 log.exception('Failed session cleanup')
276 log.exception('Failed session cleanup')
277 raise JSONRPCError(
277 raise JSONRPCError(
278 'Error occurred during session cleanup'
278 'Error occurred during session cleanup'
279 )
279 )
280
280
281
281
282 @jsonrpc_method()
282 @jsonrpc_method()
283 def get_method(request, apiuser, pattern=Optional('*')):
283 def get_method(request, apiuser, pattern=Optional('*')):
284 """
284 """
285 Returns list of all available API methods. By default match pattern
285 Returns list of all available API methods. By default match pattern
286 os "*" but any other pattern can be specified. eg *comment* will return
286 os "*" but any other pattern can be specified. eg *comment* will return
287 all methods with comment inside them. If just single method is matched
287 all methods with comment inside them. If just single method is matched
288 returned data will also include method specification
288 returned data will also include method specification
289
289
290 This command can only be run using an |authtoken| with admin rights to
290 This command can only be run using an |authtoken| with admin rights to
291 the specified repository.
291 the specified repository.
292
292
293 This command takes the following options:
293 This command takes the following options:
294
294
295 :param apiuser: This is filled automatically from the |authtoken|.
295 :param apiuser: This is filled automatically from the |authtoken|.
296 :type apiuser: AuthUser
296 :type apiuser: AuthUser
297 :param pattern: pattern to match method names against
297 :param pattern: pattern to match method names against
298 :type pattern: Optional("*")
298 :type pattern: Optional("*")
299
299
300 Example output:
300 Example output:
301
301
302 .. code-block:: bash
302 .. code-block:: bash
303
303
304 id : <id_given_in_input>
304 id : <id_given_in_input>
305 "result": [
305 "result": [
306 "changeset_comment",
306 "changeset_comment",
307 "comment_pull_request",
307 "comment_pull_request",
308 "comment_commit"
308 "comment_commit"
309 ]
309 ]
310 error : null
310 error : null
311
311
312 .. code-block:: bash
312 .. code-block:: bash
313
313
314 id : <id_given_in_input>
314 id : <id_given_in_input>
315 "result": [
315 "result": [
316 "comment_commit",
316 "comment_commit",
317 {
317 {
318 "apiuser": "<RequiredType>",
318 "apiuser": "<RequiredType>",
319 "comment_type": "<Optional:u'note'>",
319 "comment_type": "<Optional:u'note'>",
320 "commit_id": "<RequiredType>",
320 "commit_id": "<RequiredType>",
321 "message": "<RequiredType>",
321 "message": "<RequiredType>",
322 "repoid": "<RequiredType>",
322 "repoid": "<RequiredType>",
323 "request": "<RequiredType>",
323 "request": "<RequiredType>",
324 "resolves_comment_id": "<Optional:None>",
324 "resolves_comment_id": "<Optional:None>",
325 "status": "<Optional:None>",
325 "status": "<Optional:None>",
326 "userid": "<Optional:<OptionalAttr:apiuser>>"
326 "userid": "<Optional:<OptionalAttr:apiuser>>"
327 }
327 }
328 ]
328 ]
329 error : null
329 error : null
330 """
330 """
331 from rhodecode.config.patches import inspect_getargspec
331 from rhodecode.config import patches
332 inspect = inspect_getargspec()
332 inspect = patches.inspect_getargspec()
333
333
334 if not has_superadmin_permission(apiuser):
334 if not has_superadmin_permission(apiuser):
335 raise JSONRPCForbidden()
335 raise JSONRPCForbidden()
336
336
337 pattern = Optional.extract(pattern)
337 pattern = Optional.extract(pattern)
338
338
339 matches = find_methods(request.registry.jsonrpc_methods, pattern)
339 matches = find_methods(request.registry.jsonrpc_methods, pattern)
340
340
341 args_desc = []
341 args_desc = []
342 matches_keys = list(matches.keys())
342 matches_keys = list(matches.keys())
343 if len(matches_keys) == 1:
343 if len(matches_keys) == 1:
344 func = matches[matches_keys[0]]
344 func = matches[matches_keys[0]]
345
345
346 argspec = inspect.getargspec(func)
346 argspec = inspect.getargspec(func)
347 arglist = argspec[0]
347 arglist = argspec[0]
348 defaults = list(map(repr, argspec[3] or []))
348 defaults = list(map(repr, argspec[3] or []))
349
349
350 default_empty = '<RequiredType>'
350 default_empty = '<RequiredType>'
351
351
352 # kw arguments required by this method
352 # kw arguments required by this method
353 func_kwargs = dict(itertools.zip_longest(
353 func_kwargs = dict(itertools.zip_longest(
354 reversed(arglist), reversed(defaults), fillvalue=default_empty))
354 reversed(arglist), reversed(defaults), fillvalue=default_empty))
355 args_desc.append(func_kwargs)
355 args_desc.append(func_kwargs)
356
356
357 return matches_keys + args_desc
357 return matches_keys + args_desc
358
358
359
359
360 @jsonrpc_method()
360 @jsonrpc_method()
361 def store_exception(request, apiuser, exc_data_json, prefix=Optional('rhodecode')):
361 def store_exception(request, apiuser, exc_data_json, prefix=Optional('rhodecode')):
362 """
362 """
363 Stores sent exception inside the built-in exception tracker in |RCE| server.
363 Stores sent exception inside the built-in exception tracker in |RCE| server.
364
364
365 This command can only be run using an |authtoken| with admin rights to
365 This command can only be run using an |authtoken| with admin rights to
366 the specified repository.
366 the specified repository.
367
367
368 This command takes the following options:
368 This command takes the following options:
369
369
370 :param apiuser: This is filled automatically from the |authtoken|.
370 :param apiuser: This is filled automatically from the |authtoken|.
371 :type apiuser: AuthUser
371 :type apiuser: AuthUser
372
372
373 :param exc_data_json: JSON data with exception e.g
373 :param exc_data_json: JSON data with exception e.g
374 {"exc_traceback": "Value `1` is not allowed", "exc_type_name": "ValueError"}
374 {"exc_traceback": "Value `1` is not allowed", "exc_type_name": "ValueError"}
375 :type exc_data_json: JSON data
375 :type exc_data_json: JSON data
376
376
377 :param prefix: prefix for error type, e.g 'rhodecode', 'vcsserver', 'rhodecode-tools'
377 :param prefix: prefix for error type, e.g 'rhodecode', 'vcsserver', 'rhodecode-tools'
378 :type prefix: Optional("rhodecode")
378 :type prefix: Optional("rhodecode")
379
379
380 Example output:
380 Example output:
381
381
382 .. code-block:: bash
382 .. code-block:: bash
383
383
384 id : <id_given_in_input>
384 id : <id_given_in_input>
385 "result": {
385 "result": {
386 "exc_id": 139718459226384,
386 "exc_id": 139718459226384,
387 "exc_url": "http://localhost:8080/_admin/settings/exceptions/139718459226384"
387 "exc_url": "http://localhost:8080/_admin/settings/exceptions/139718459226384"
388 }
388 }
389 error : null
389 error : null
390 """
390 """
391 if not has_superadmin_permission(apiuser):
391 if not has_superadmin_permission(apiuser):
392 raise JSONRPCForbidden()
392 raise JSONRPCForbidden()
393
393
394 prefix = Optional.extract(prefix)
394 prefix = Optional.extract(prefix)
395 exc_id = exc_tracking.generate_id()
395 exc_id = exc_tracking.generate_id()
396
396
397 try:
397 try:
398 exc_data = json.loads(exc_data_json)
398 exc_data = json.loads(exc_data_json)
399 except Exception:
399 except Exception:
400 log.error('Failed to parse JSON: %r', exc_data_json)
400 log.error('Failed to parse JSON: %r', exc_data_json)
401 raise JSONRPCError('Failed to parse JSON data from exc_data_json field. '
401 raise JSONRPCError('Failed to parse JSON data from exc_data_json field. '
402 'Please make sure it contains a valid JSON.')
402 'Please make sure it contains a valid JSON.')
403
403
404 try:
404 try:
405 exc_traceback = exc_data['exc_traceback']
405 exc_traceback = exc_data['exc_traceback']
406 exc_type_name = exc_data['exc_type_name']
406 exc_type_name = exc_data['exc_type_name']
407 exc_value = ''
407 exc_value = ''
408 except KeyError as err:
408 except KeyError as err:
409 raise JSONRPCError(
409 raise JSONRPCError(
410 f'Missing exc_traceback, or exc_type_name '
410 f'Missing exc_traceback, or exc_type_name '
411 f'in exc_data_json field. Missing: {err}')
411 f'in exc_data_json field. Missing: {err}')
412
412
413 class ExcType:
413 class ExcType:
414 __name__ = exc_type_name
414 __name__ = exc_type_name
415
415
416 exc_info = (ExcType(), exc_value, exc_traceback)
416 exc_info = (ExcType(), exc_value, exc_traceback)
417
417
418 exc_tracking._store_exception(
418 exc_tracking._store_exception(
419 exc_id=exc_id, exc_info=exc_info, prefix=prefix)
419 exc_id=exc_id, exc_info=exc_info, prefix=prefix)
420
420
421 exc_url = request.route_url(
421 exc_url = request.route_url(
422 'admin_settings_exception_tracker_show', exception_id=exc_id)
422 'admin_settings_exception_tracker_show', exception_id=exc_id)
423 return {'exc_id': exc_id, 'exc_url': exc_url}
423 return {'exc_id': exc_id, 'exc_url': exc_url}
@@ -1,466 +1,467 b''
1 # Copyright (C) 2010-2023 RhodeCode GmbH
1 # Copyright (C) 2010-2023 RhodeCode GmbH
2 #
2 #
3 # This program is free software: you can redistribute it and/or modify
3 # This program is free software: you can redistribute it and/or modify
4 # it under the terms of the GNU Affero General Public License, version 3
4 # it under the terms of the GNU Affero General Public License, version 3
5 # (only), as published by the Free Software Foundation.
5 # (only), as published by the Free Software Foundation.
6 #
6 #
7 # This program is distributed in the hope that it will be useful,
7 # This program is distributed in the hope that it will be useful,
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # GNU General Public License for more details.
10 # GNU General Public License for more details.
11 #
11 #
12 # You should have received a copy of the GNU Affero General Public License
12 # You should have received a copy of the GNU Affero General Public License
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 #
14 #
15 # This program is dual-licensed. If you wish to learn more about the
15 # This program is dual-licensed. If you wish to learn more about the
16 # RhodeCode Enterprise Edition, including its added features, Support services,
16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18
18
19 import os
19 import os
20 import sys
20 import sys
21 import collections
21 import collections
22
22
23 import time
23 import time
24 import logging.config
24 import logging.config
25
25
26 from paste.gzipper import make_gzip_middleware
26 from paste.gzipper import make_gzip_middleware
27 import pyramid.events
27 import pyramid.events
28 from pyramid.wsgi import wsgiapp
28 from pyramid.wsgi import wsgiapp
29 from pyramid.config import Configurator
29 from pyramid.config import Configurator
30 from pyramid.settings import asbool, aslist
30 from pyramid.settings import asbool, aslist
31 from pyramid.httpexceptions import (
31 from pyramid.httpexceptions import (
32 HTTPException, HTTPError, HTTPInternalServerError, HTTPFound, HTTPNotFound)
32 HTTPException, HTTPError, HTTPInternalServerError, HTTPFound, HTTPNotFound)
33 from pyramid.renderers import render_to_response
33 from pyramid.renderers import render_to_response
34
34
35 from rhodecode.model import meta
35 from rhodecode.model import meta
36 from rhodecode.config import patches
36 from rhodecode.config import patches
37
37
38 from rhodecode.config.environment import load_pyramid_environment
38 from rhodecode.config.environment import load_pyramid_environment, propagate_rhodecode_config
39
39
40 import rhodecode.events
40 import rhodecode.events
41 from rhodecode.config.config_maker import sanitize_settings_and_apply_defaults
41 from rhodecode.config.config_maker import sanitize_settings_and_apply_defaults
42 from rhodecode.lib.middleware.vcs import VCSMiddleware
42 from rhodecode.lib.middleware.vcs import VCSMiddleware
43 from rhodecode.lib.request import Request
43 from rhodecode.lib.request import Request
44 from rhodecode.lib.vcs import VCSCommunicationError
44 from rhodecode.lib.vcs import VCSCommunicationError
45 from rhodecode.lib.exceptions import VCSServerUnavailable
45 from rhodecode.lib.exceptions import VCSServerUnavailable
46 from rhodecode.lib.middleware.appenlight import wrap_in_appenlight_if_enabled
46 from rhodecode.lib.middleware.appenlight import wrap_in_appenlight_if_enabled
47 from rhodecode.lib.middleware.https_fixup import HttpsFixup
47 from rhodecode.lib.middleware.https_fixup import HttpsFixup
48 from rhodecode.lib.plugins.utils import register_rhodecode_plugin
48 from rhodecode.lib.plugins.utils import register_rhodecode_plugin
49 from rhodecode.lib.utils2 import AttributeDict
49 from rhodecode.lib.utils2 import AttributeDict
50 from rhodecode.lib.exc_tracking import store_exception, format_exc
50 from rhodecode.lib.exc_tracking import store_exception, format_exc
51 from rhodecode.subscribers import (
51 from rhodecode.subscribers import (
52 scan_repositories_if_enabled, write_js_routes_if_enabled,
52 scan_repositories_if_enabled, write_js_routes_if_enabled,
53 write_metadata_if_needed, write_usage_data)
53 write_metadata_if_needed, write_usage_data)
54 from rhodecode.lib.statsd_client import StatsdClient
54 from rhodecode.lib.statsd_client import StatsdClient
55
55
56 log = logging.getLogger(__name__)
56 log = logging.getLogger(__name__)
57
57
58
58
59 def is_http_error(response):
59 def is_http_error(response):
60 # error which should have traceback
60 # error which should have traceback
61 return response.status_code > 499
61 return response.status_code > 499
62
62
63
63
64 def should_load_all():
64 def should_load_all():
65 """
65 """
66 Returns if all application components should be loaded. In some cases it's
66 Returns if all application components should be loaded. In some cases it's
67 desired to skip apps loading for faster shell script execution
67 desired to skip apps loading for faster shell script execution
68 """
68 """
69 ssh_cmd = os.environ.get('RC_CMD_SSH_WRAPPER')
69 ssh_cmd = os.environ.get('RC_CMD_SSH_WRAPPER')
70 if ssh_cmd:
70 if ssh_cmd:
71 return False
71 return False
72
72
73 return True
73 return True
74
74
75
75
76 def make_pyramid_app(global_config, **settings):
76 def make_pyramid_app(global_config, **settings):
77 """
77 """
78 Constructs the WSGI application based on Pyramid.
78 Constructs the WSGI application based on Pyramid.
79
79
80 Specials:
80 Specials:
81
81
82 * The application can also be integrated like a plugin via the call to
82 * The application can also be integrated like a plugin via the call to
83 `includeme`. This is accompanied with the other utility functions which
83 `includeme`. This is accompanied with the other utility functions which
84 are called. Changing this should be done with great care to not break
84 are called. Changing this should be done with great care to not break
85 cases when these fragments are assembled from another place.
85 cases when these fragments are assembled from another place.
86
86
87 """
87 """
88 start_time = time.time()
88 start_time = time.time()
89 log.info('Pyramid app config starting')
89 log.info('Pyramid app config starting')
90
90
91 sanitize_settings_and_apply_defaults(global_config, settings)
91 sanitize_settings_and_apply_defaults(global_config, settings)
92
92
93 # init and bootstrap StatsdClient
93 # init and bootstrap StatsdClient
94 StatsdClient.setup(settings)
94 StatsdClient.setup(settings)
95
95
96 config = Configurator(settings=settings)
96 config = Configurator(settings=settings)
97 # Init our statsd at very start
97 # Init our statsd at very start
98 config.registry.statsd = StatsdClient.statsd
98 config.registry.statsd = StatsdClient.statsd
99
99
100 # Apply compatibility patches
100 # Apply compatibility patches
101 patches.inspect_getargspec()
101 patches.inspect_getargspec()
102 patches.repoze_sendmail_lf_fix()
102
103
103 load_pyramid_environment(global_config, settings)
104 load_pyramid_environment(global_config, settings)
104
105
105 # Static file view comes first
106 # Static file view comes first
106 includeme_first(config)
107 includeme_first(config)
107
108
108 includeme(config)
109 includeme(config)
109
110
110 pyramid_app = config.make_wsgi_app()
111 pyramid_app = config.make_wsgi_app()
111 pyramid_app = wrap_app_in_wsgi_middlewares(pyramid_app, config)
112 pyramid_app = wrap_app_in_wsgi_middlewares(pyramid_app, config)
112 pyramid_app.config = config
113 pyramid_app.config = config
113
114
114 celery_settings = get_celery_config(settings)
115 celery_settings = get_celery_config(settings)
115 config.configure_celery(celery_settings)
116 config.configure_celery(celery_settings)
116
117
117 # creating the app uses a connection - return it after we are done
118 # creating the app uses a connection - return it after we are done
118 meta.Session.remove()
119 meta.Session.remove()
119
120
120 total_time = time.time() - start_time
121 total_time = time.time() - start_time
121 log.info('Pyramid app created and configured in %.2fs', total_time)
122 log.info('Pyramid app created and configured in %.2fs', total_time)
122 return pyramid_app
123 return pyramid_app
123
124
124
125
125 def get_celery_config(settings):
126 def get_celery_config(settings):
126 """
127 """
127 Converts basic ini configuration into celery 4.X options
128 Converts basic ini configuration into celery 4.X options
128 """
129 """
129
130
130 def key_converter(key_name):
131 def key_converter(key_name):
131 pref = 'celery.'
132 pref = 'celery.'
132 if key_name.startswith(pref):
133 if key_name.startswith(pref):
133 return key_name[len(pref):].replace('.', '_').lower()
134 return key_name[len(pref):].replace('.', '_').lower()
134
135
135 def type_converter(parsed_key, value):
136 def type_converter(parsed_key, value):
136 # cast to int
137 # cast to int
137 if value.isdigit():
138 if value.isdigit():
138 return int(value)
139 return int(value)
139
140
140 # cast to bool
141 # cast to bool
141 if value.lower() in ['true', 'false', 'True', 'False']:
142 if value.lower() in ['true', 'false', 'True', 'False']:
142 return value.lower() == 'true'
143 return value.lower() == 'true'
143 return value
144 return value
144
145
145 celery_config = {}
146 celery_config = {}
146 for k, v in settings.items():
147 for k, v in settings.items():
147 pref = 'celery.'
148 pref = 'celery.'
148 if k.startswith(pref):
149 if k.startswith(pref):
149 celery_config[key_converter(k)] = type_converter(key_converter(k), v)
150 celery_config[key_converter(k)] = type_converter(key_converter(k), v)
150
151
151 # TODO:rethink if we want to support celerybeat based file config, probably NOT
152 # TODO:rethink if we want to support celerybeat based file config, probably NOT
152 # beat_config = {}
153 # beat_config = {}
153 # for section in parser.sections():
154 # for section in parser.sections():
154 # if section.startswith('celerybeat:'):
155 # if section.startswith('celerybeat:'):
155 # name = section.split(':', 1)[1]
156 # name = section.split(':', 1)[1]
156 # beat_config[name] = get_beat_config(parser, section)
157 # beat_config[name] = get_beat_config(parser, section)
157
158
158 # final compose of settings
159 # final compose of settings
159 celery_settings = {}
160 celery_settings = {}
160
161
161 if celery_config:
162 if celery_config:
162 celery_settings.update(celery_config)
163 celery_settings.update(celery_config)
163 # if beat_config:
164 # if beat_config:
164 # celery_settings.update({'beat_schedule': beat_config})
165 # celery_settings.update({'beat_schedule': beat_config})
165
166
166 return celery_settings
167 return celery_settings
167
168
168
169
169 def not_found_view(request):
170 def not_found_view(request):
170 """
171 """
171 This creates the view which should be registered as not-found-view to
172 This creates the view which should be registered as not-found-view to
172 pyramid.
173 pyramid.
173 """
174 """
174
175
175 if not getattr(request, 'vcs_call', None):
176 if not getattr(request, 'vcs_call', None):
176 # handle like regular case with our error_handler
177 # handle like regular case with our error_handler
177 return error_handler(HTTPNotFound(), request)
178 return error_handler(HTTPNotFound(), request)
178
179
179 # handle not found view as a vcs call
180 # handle not found view as a vcs call
180 settings = request.registry.settings
181 settings = request.registry.settings
181 ae_client = getattr(request, 'ae_client', None)
182 ae_client = getattr(request, 'ae_client', None)
182 vcs_app = VCSMiddleware(
183 vcs_app = VCSMiddleware(
183 HTTPNotFound(), request.registry, settings,
184 HTTPNotFound(), request.registry, settings,
184 appenlight_client=ae_client)
185 appenlight_client=ae_client)
185
186
186 return wsgiapp(vcs_app)(None, request)
187 return wsgiapp(vcs_app)(None, request)
187
188
188
189
189 def error_handler(exception, request):
190 def error_handler(exception, request):
190 import rhodecode
191 import rhodecode
191 from rhodecode.lib import helpers
192 from rhodecode.lib import helpers
192
193
193 rhodecode_title = rhodecode.CONFIG.get('rhodecode_title') or 'RhodeCode'
194 rhodecode_title = rhodecode.CONFIG.get('rhodecode_title') or 'RhodeCode'
194
195
195 base_response = HTTPInternalServerError()
196 base_response = HTTPInternalServerError()
196 # prefer original exception for the response since it may have headers set
197 # prefer original exception for the response since it may have headers set
197 if isinstance(exception, HTTPException):
198 if isinstance(exception, HTTPException):
198 base_response = exception
199 base_response = exception
199 elif isinstance(exception, VCSCommunicationError):
200 elif isinstance(exception, VCSCommunicationError):
200 base_response = VCSServerUnavailable()
201 base_response = VCSServerUnavailable()
201
202
202 if is_http_error(base_response):
203 if is_http_error(base_response):
203 traceback_info = format_exc(request.exc_info)
204 traceback_info = format_exc(request.exc_info)
204 log.error(
205 log.error(
205 'error occurred handling this request for path: %s, \n%s',
206 'error occurred handling this request for path: %s, \n%s',
206 request.path, traceback_info)
207 request.path, traceback_info)
207
208
208 error_explanation = base_response.explanation or str(base_response)
209 error_explanation = base_response.explanation or str(base_response)
209 if base_response.status_code == 404:
210 if base_response.status_code == 404:
210 error_explanation += " Optionally you don't have permission to access this page."
211 error_explanation += " Optionally you don't have permission to access this page."
211 c = AttributeDict()
212 c = AttributeDict()
212 c.error_message = base_response.status
213 c.error_message = base_response.status
213 c.error_explanation = error_explanation
214 c.error_explanation = error_explanation
214 c.visual = AttributeDict()
215 c.visual = AttributeDict()
215
216
216 c.visual.rhodecode_support_url = (
217 c.visual.rhodecode_support_url = (
217 request.registry.settings.get('rhodecode_support_url') or
218 request.registry.settings.get('rhodecode_support_url') or
218 request.route_url('rhodecode_support')
219 request.route_url('rhodecode_support')
219 )
220 )
220 c.redirect_time = 0
221 c.redirect_time = 0
221 c.rhodecode_name = rhodecode_title
222 c.rhodecode_name = rhodecode_title
222 if not c.rhodecode_name:
223 if not c.rhodecode_name:
223 c.rhodecode_name = 'Rhodecode'
224 c.rhodecode_name = 'Rhodecode'
224
225
225 c.causes = []
226 c.causes = []
226 if is_http_error(base_response):
227 if is_http_error(base_response):
227 c.causes.append('Server is overloaded.')
228 c.causes.append('Server is overloaded.')
228 c.causes.append('Server database connection is lost.')
229 c.causes.append('Server database connection is lost.')
229 c.causes.append('Server expected unhandled error.')
230 c.causes.append('Server expected unhandled error.')
230
231
231 if hasattr(base_response, 'causes'):
232 if hasattr(base_response, 'causes'):
232 c.causes = base_response.causes
233 c.causes = base_response.causes
233
234
234 c.messages = helpers.flash.pop_messages(request=request)
235 c.messages = helpers.flash.pop_messages(request=request)
235 exc_info = sys.exc_info()
236 exc_info = sys.exc_info()
236 c.exception_id = id(exc_info)
237 c.exception_id = id(exc_info)
237 c.show_exception_id = isinstance(base_response, VCSServerUnavailable) \
238 c.show_exception_id = isinstance(base_response, VCSServerUnavailable) \
238 or base_response.status_code > 499
239 or base_response.status_code > 499
239 c.exception_id_url = request.route_url(
240 c.exception_id_url = request.route_url(
240 'admin_settings_exception_tracker_show', exception_id=c.exception_id)
241 'admin_settings_exception_tracker_show', exception_id=c.exception_id)
241
242
242 debug_mode = rhodecode.ConfigGet().get_bool('debug')
243 debug_mode = rhodecode.ConfigGet().get_bool('debug')
243 if c.show_exception_id:
244 if c.show_exception_id:
244 store_exception(c.exception_id, exc_info)
245 store_exception(c.exception_id, exc_info)
245 c.exception_debug = debug_mode
246 c.exception_debug = debug_mode
246 c.exception_config_ini = rhodecode.CONFIG.get('__file__')
247 c.exception_config_ini = rhodecode.CONFIG.get('__file__')
247
248
248 if debug_mode:
249 if debug_mode:
249 try:
250 try:
250 from rich.traceback import install
251 from rich.traceback import install
251 install(show_locals=True)
252 install(show_locals=True)
252 log.debug('Installing rich tracebacks...')
253 log.debug('Installing rich tracebacks...')
253 except ImportError:
254 except ImportError:
254 pass
255 pass
255
256
256 response = render_to_response(
257 response = render_to_response(
257 '/errors/error_document.mako', {'c': c, 'h': helpers}, request=request,
258 '/errors/error_document.mako', {'c': c, 'h': helpers}, request=request,
258 response=base_response)
259 response=base_response)
259
260
260 response.headers["X-RC-Exception-Id"] = str(c.exception_id)
261 response.headers["X-RC-Exception-Id"] = str(c.exception_id)
261
262
262 statsd = request.registry.statsd
263 statsd = request.registry.statsd
263 if statsd and base_response.status_code > 499:
264 if statsd and base_response.status_code > 499:
264 exc_type = f"{exception.__class__.__module__}.{exception.__class__.__name__}"
265 exc_type = f"{exception.__class__.__module__}.{exception.__class__.__name__}"
265 statsd.incr('rhodecode_exception_total',
266 statsd.incr('rhodecode_exception_total',
266 tags=["exc_source:web",
267 tags=["exc_source:web",
267 f"http_code:{base_response.status_code}",
268 f"http_code:{base_response.status_code}",
268 f"type:{exc_type}"])
269 f"type:{exc_type}"])
269
270
270 return response
271 return response
271
272
272
273
273 def includeme_first(config):
274 def includeme_first(config):
274 # redirect automatic browser favicon.ico requests to correct place
275 # redirect automatic browser favicon.ico requests to correct place
275 def favicon_redirect(context, request):
276 def favicon_redirect(context, request):
276 return HTTPFound(
277 return HTTPFound(
277 request.static_path('rhodecode:public/images/favicon.ico'))
278 request.static_path('rhodecode:public/images/favicon.ico'))
278
279
279 config.add_view(favicon_redirect, route_name='favicon')
280 config.add_view(favicon_redirect, route_name='favicon')
280 config.add_route('favicon', '/favicon.ico')
281 config.add_route('favicon', '/favicon.ico')
281
282
282 def robots_redirect(context, request):
283 def robots_redirect(context, request):
283 return HTTPFound(
284 return HTTPFound(
284 request.static_path('rhodecode:public/robots.txt'))
285 request.static_path('rhodecode:public/robots.txt'))
285
286
286 config.add_view(robots_redirect, route_name='robots')
287 config.add_view(robots_redirect, route_name='robots')
287 config.add_route('robots', '/robots.txt')
288 config.add_route('robots', '/robots.txt')
288
289
289 config.add_static_view(
290 config.add_static_view(
290 '_static/deform', 'deform:static')
291 '_static/deform', 'deform:static')
291 config.add_static_view(
292 config.add_static_view(
292 '_static/rhodecode', path='rhodecode:public', cache_max_age=3600 * 24)
293 '_static/rhodecode', path='rhodecode:public', cache_max_age=3600 * 24)
293
294
294
295
295 ce_auth_resources = [
296 ce_auth_resources = [
296 'rhodecode.authentication.plugins.auth_crowd',
297 'rhodecode.authentication.plugins.auth_crowd',
297 'rhodecode.authentication.plugins.auth_headers',
298 'rhodecode.authentication.plugins.auth_headers',
298 'rhodecode.authentication.plugins.auth_jasig_cas',
299 'rhodecode.authentication.plugins.auth_jasig_cas',
299 'rhodecode.authentication.plugins.auth_ldap',
300 'rhodecode.authentication.plugins.auth_ldap',
300 'rhodecode.authentication.plugins.auth_pam',
301 'rhodecode.authentication.plugins.auth_pam',
301 'rhodecode.authentication.plugins.auth_rhodecode',
302 'rhodecode.authentication.plugins.auth_rhodecode',
302 'rhodecode.authentication.plugins.auth_token',
303 'rhodecode.authentication.plugins.auth_token',
303 ]
304 ]
304
305
305
306
306 def includeme(config, auth_resources=None):
307 def includeme(config, auth_resources=None):
307 from rhodecode.lib.celerylib.loader import configure_celery
308 from rhodecode.lib.celerylib.loader import configure_celery
308 log.debug('Initializing main includeme from %s', os.path.basename(__file__))
309 log.debug('Initializing main includeme from %s', os.path.basename(__file__))
309 settings = config.registry.settings
310 settings = config.registry.settings
310 config.set_request_factory(Request)
311 config.set_request_factory(Request)
311
312
312 # plugin information
313 # plugin information
313 config.registry.rhodecode_plugins = collections.OrderedDict()
314 config.registry.rhodecode_plugins = collections.OrderedDict()
314
315
315 config.add_directive(
316 config.add_directive(
316 'register_rhodecode_plugin', register_rhodecode_plugin)
317 'register_rhodecode_plugin', register_rhodecode_plugin)
317
318
318 config.add_directive('configure_celery', configure_celery)
319 config.add_directive('configure_celery', configure_celery)
319
320
320 if settings.get('appenlight', False):
321 if settings.get('appenlight', False):
321 config.include('appenlight_client.ext.pyramid_tween')
322 config.include('appenlight_client.ext.pyramid_tween')
322
323
323 load_all = should_load_all()
324 load_all = should_load_all()
324
325
325 # Includes which are required. The application would fail without them.
326 # Includes which are required. The application would fail without them.
326 config.include('pyramid_mako')
327 config.include('pyramid_mako')
327 config.include('rhodecode.lib.rc_beaker')
328 config.include('rhodecode.lib.rc_beaker')
328 config.include('rhodecode.lib.rc_cache')
329 config.include('rhodecode.lib.rc_cache')
329 config.include('rhodecode.lib.archive_cache')
330 config.include('rhodecode.lib.archive_cache')
330
331
331 config.include('rhodecode.apps._base.navigation')
332 config.include('rhodecode.apps._base.navigation')
332 config.include('rhodecode.apps._base.subscribers')
333 config.include('rhodecode.apps._base.subscribers')
333 config.include('rhodecode.tweens')
334 config.include('rhodecode.tweens')
334 config.include('rhodecode.authentication')
335 config.include('rhodecode.authentication')
335
336
336 if load_all:
337 if load_all:
337
338
338 # load CE authentication plugins
339 # load CE authentication plugins
339
340
340 if auth_resources:
341 if auth_resources:
341 ce_auth_resources.extend(auth_resources)
342 ce_auth_resources.extend(auth_resources)
342
343
343 for resource in ce_auth_resources:
344 for resource in ce_auth_resources:
344 config.include(resource)
345 config.include(resource)
345
346
346 # Auto discover authentication plugins and include their configuration.
347 # Auto discover authentication plugins and include their configuration.
347 if asbool(settings.get('auth_plugin.import_legacy_plugins', 'true')):
348 if asbool(settings.get('auth_plugin.import_legacy_plugins', 'true')):
348 from rhodecode.authentication import discover_legacy_plugins
349 from rhodecode.authentication import discover_legacy_plugins
349 discover_legacy_plugins(config)
350 discover_legacy_plugins(config)
350
351
351 # apps
352 # apps
352 if load_all:
353 if load_all:
353 log.debug('Starting config.include() calls')
354 log.debug('Starting config.include() calls')
354 config.include('rhodecode.api.includeme')
355 config.include('rhodecode.api.includeme')
355 config.include('rhodecode.apps._base.includeme')
356 config.include('rhodecode.apps._base.includeme')
356 config.include('rhodecode.apps._base.navigation.includeme')
357 config.include('rhodecode.apps._base.navigation.includeme')
357 config.include('rhodecode.apps._base.subscribers.includeme')
358 config.include('rhodecode.apps._base.subscribers.includeme')
358 config.include('rhodecode.apps.hovercards.includeme')
359 config.include('rhodecode.apps.hovercards.includeme')
359 config.include('rhodecode.apps.ops.includeme')
360 config.include('rhodecode.apps.ops.includeme')
360 config.include('rhodecode.apps.channelstream.includeme')
361 config.include('rhodecode.apps.channelstream.includeme')
361 config.include('rhodecode.apps.file_store.includeme')
362 config.include('rhodecode.apps.file_store.includeme')
362 config.include('rhodecode.apps.admin.includeme')
363 config.include('rhodecode.apps.admin.includeme')
363 config.include('rhodecode.apps.login.includeme')
364 config.include('rhodecode.apps.login.includeme')
364 config.include('rhodecode.apps.home.includeme')
365 config.include('rhodecode.apps.home.includeme')
365 config.include('rhodecode.apps.journal.includeme')
366 config.include('rhodecode.apps.journal.includeme')
366
367
367 config.include('rhodecode.apps.repository.includeme')
368 config.include('rhodecode.apps.repository.includeme')
368 config.include('rhodecode.apps.repo_group.includeme')
369 config.include('rhodecode.apps.repo_group.includeme')
369 config.include('rhodecode.apps.user_group.includeme')
370 config.include('rhodecode.apps.user_group.includeme')
370 config.include('rhodecode.apps.search.includeme')
371 config.include('rhodecode.apps.search.includeme')
371 config.include('rhodecode.apps.user_profile.includeme')
372 config.include('rhodecode.apps.user_profile.includeme')
372 config.include('rhodecode.apps.user_group_profile.includeme')
373 config.include('rhodecode.apps.user_group_profile.includeme')
373 config.include('rhodecode.apps.my_account.includeme')
374 config.include('rhodecode.apps.my_account.includeme')
374 config.include('rhodecode.apps.gist.includeme')
375 config.include('rhodecode.apps.gist.includeme')
375
376
376 config.include('rhodecode.apps.svn_support.includeme')
377 config.include('rhodecode.apps.svn_support.includeme')
377 config.include('rhodecode.apps.ssh_support.includeme')
378 config.include('rhodecode.apps.ssh_support.includeme')
378 config.include('rhodecode.apps.debug_style')
379 config.include('rhodecode.apps.debug_style')
379
380
380 if load_all:
381 if load_all:
381 config.include('rhodecode.integrations.includeme')
382 config.include('rhodecode.integrations.includeme')
382 config.include('rhodecode.integrations.routes.includeme')
383 config.include('rhodecode.integrations.routes.includeme')
383
384
384 config.add_route('rhodecode_support', 'https://rhodecode.com/help/', static=True)
385 config.add_route('rhodecode_support', 'https://rhodecode.com/help/', static=True)
385 settings['default_locale_name'] = settings.get('lang', 'en')
386 settings['default_locale_name'] = settings.get('lang', 'en')
386 config.add_translation_dirs('rhodecode:i18n/')
387 config.add_translation_dirs('rhodecode:i18n/')
387
388
388 # Add subscribers.
389 # Add subscribers.
389 if load_all:
390 if load_all:
390 log.debug('Adding subscribers...')
391 log.debug('Adding subscribers...')
391 config.add_subscriber(scan_repositories_if_enabled,
392 config.add_subscriber(scan_repositories_if_enabled,
392 pyramid.events.ApplicationCreated)
393 pyramid.events.ApplicationCreated)
393 config.add_subscriber(write_metadata_if_needed,
394 config.add_subscriber(write_metadata_if_needed,
394 pyramid.events.ApplicationCreated)
395 pyramid.events.ApplicationCreated)
395 config.add_subscriber(write_usage_data,
396 config.add_subscriber(write_usage_data,
396 pyramid.events.ApplicationCreated)
397 pyramid.events.ApplicationCreated)
397 config.add_subscriber(write_js_routes_if_enabled,
398 config.add_subscriber(write_js_routes_if_enabled,
398 pyramid.events.ApplicationCreated)
399 pyramid.events.ApplicationCreated)
399
400
400
401
401 # Set the default renderer for HTML templates to mako.
402 # Set the default renderer for HTML templates to mako.
402 config.add_mako_renderer('.html')
403 config.add_mako_renderer('.html')
403
404
404 config.add_renderer(
405 config.add_renderer(
405 name='json_ext',
406 name='json_ext',
406 factory='rhodecode.lib.ext_json_renderer.pyramid_ext_json')
407 factory='rhodecode.lib.ext_json_renderer.pyramid_ext_json')
407
408
408 config.add_renderer(
409 config.add_renderer(
409 name='string_html',
410 name='string_html',
410 factory='rhodecode.lib.string_renderer.html')
411 factory='rhodecode.lib.string_renderer.html')
411
412
412 # include RhodeCode plugins
413 # include RhodeCode plugins
413 includes = aslist(settings.get('rhodecode.includes', []))
414 includes = aslist(settings.get('rhodecode.includes', []))
414 log.debug('processing rhodecode.includes data...')
415 log.debug('processing rhodecode.includes data...')
415 for inc in includes:
416 for inc in includes:
416 config.include(inc)
417 config.include(inc)
417
418
418 # custom not found view, if our pyramid app doesn't know how to handle
419 # custom not found view, if our pyramid app doesn't know how to handle
419 # the request pass it to potential VCS handling ap
420 # the request pass it to potential VCS handling ap
420 config.add_notfound_view(not_found_view)
421 config.add_notfound_view(not_found_view)
421 if not settings.get('debugtoolbar.enabled', False):
422 if not settings.get('debugtoolbar.enabled', False):
422 # disabled debugtoolbar handle all exceptions via the error_handlers
423 # disabled debugtoolbar handle all exceptions via the error_handlers
423 config.add_view(error_handler, context=Exception)
424 config.add_view(error_handler, context=Exception)
424
425
425 # all errors including 403/404/50X
426 # all errors including 403/404/50X
426 config.add_view(error_handler, context=HTTPError)
427 config.add_view(error_handler, context=HTTPError)
427
428
428
429
429 def wrap_app_in_wsgi_middlewares(pyramid_app, config):
430 def wrap_app_in_wsgi_middlewares(pyramid_app, config):
430 """
431 """
431 Apply outer WSGI middlewares around the application.
432 Apply outer WSGI middlewares around the application.
432 """
433 """
433 registry = config.registry
434 registry = config.registry
434 settings = registry.settings
435 settings = registry.settings
435
436
436 # enable https redirects based on HTTP_X_URL_SCHEME set by proxy
437 # enable https redirects based on HTTP_X_URL_SCHEME set by proxy
437 pyramid_app = HttpsFixup(pyramid_app, settings)
438 pyramid_app = HttpsFixup(pyramid_app, settings)
438
439
439 pyramid_app, _ae_client = wrap_in_appenlight_if_enabled(
440 pyramid_app, _ae_client = wrap_in_appenlight_if_enabled(
440 pyramid_app, settings)
441 pyramid_app, settings)
441 registry.ae_client = _ae_client
442 registry.ae_client = _ae_client
442
443
443 if settings['gzip_responses']:
444 if settings['gzip_responses']:
444 pyramid_app = make_gzip_middleware(
445 pyramid_app = make_gzip_middleware(
445 pyramid_app, settings, compress_level=1)
446 pyramid_app, settings, compress_level=1)
446
447
447 # this should be the outer most middleware in the wsgi stack since
448 # this should be the outer most middleware in the wsgi stack since
448 # middleware like Routes make database calls
449 # middleware like Routes make database calls
449 def pyramid_app_with_cleanup(environ, start_response):
450 def pyramid_app_with_cleanup(environ, start_response):
450 start = time.time()
451 start = time.time()
451 try:
452 try:
452 return pyramid_app(environ, start_response)
453 return pyramid_app(environ, start_response)
453 finally:
454 finally:
454 # Dispose current database session and rollback uncommitted
455 # Dispose current database session and rollback uncommitted
455 # transactions.
456 # transactions.
456 meta.Session.remove()
457 meta.Session.remove()
457
458
458 # In a single threaded mode server, on non sqlite db we should have
459 # In a single threaded mode server, on non sqlite db we should have
459 # '0 Current Checked out connections' at the end of a request,
460 # '0 Current Checked out connections' at the end of a request,
460 # if not, then something, somewhere is leaving a connection open
461 # if not, then something, somewhere is leaving a connection open
461 pool = meta.get_engine().pool
462 pool = meta.get_engine().pool
462 log.debug('sa pool status: %s', pool.status())
463 log.debug('sa pool status: %s', pool.status())
463 total = time.time() - start
464 total = time.time() - start
464 log.debug('Request processing finalized: %.4fs', total)
465 log.debug('Request processing finalized: %.4fs', total)
465
466
466 return pyramid_app_with_cleanup
467 return pyramid_app_with_cleanup
@@ -1,160 +1,167 b''
1 # Copyright (C) 2016-2023 RhodeCode GmbH
1 # Copyright (C) 2016-2023 RhodeCode GmbH
2 #
2 #
3 # This program is free software: you can redistribute it and/or modify
3 # This program is free software: you can redistribute it and/or modify
4 # it under the terms of the GNU Affero General Public License, version 3
4 # it under the terms of the GNU Affero General Public License, version 3
5 # (only), as published by the Free Software Foundation.
5 # (only), as published by the Free Software Foundation.
6 #
6 #
7 # This program is distributed in the hope that it will be useful,
7 # This program is distributed in the hope that it will be useful,
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # GNU General Public License for more details.
10 # GNU General Public License for more details.
11 #
11 #
12 # You should have received a copy of the GNU Affero General Public License
12 # You should have received a copy of the GNU Affero General Public License
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 #
14 #
15 # This program is dual-licensed. If you wish to learn more about the
15 # This program is dual-licensed. If you wish to learn more about the
16 # RhodeCode Enterprise Edition, including its added features, Support services,
16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18
18
19 """
19 """
20 Compatibility patches.
20 Compatibility patches.
21
21
22 Please keep the following principles in mind:
22 Please keep the following principles in mind:
23
23
24 * Keep imports local, so that importing this module does not cause too many
24 * Keep imports local, so that importing this module does not cause too many
25 side effects by itself.
25 side effects by itself.
26
26
27 * Try to make patches idempotent, calling them multiple times should not do
27 * Try to make patches idempotent, calling them multiple times should not do
28 harm. If that is not possible, ensure that the second call explodes.
28 harm. If that is not possible, ensure that the second call explodes.
29
29
30 """
30 """
31
31
32
32
33 def inspect_formatargspec():
33 def inspect_formatargspec():
34
34
35 import inspect
35 import inspect
36 from inspect import formatannotation
36 from inspect import formatannotation
37
37
38 def backport_inspect_formatargspec(
38 def backport_inspect_formatargspec(
39 args, varargs=None, varkw=None, defaults=None,
39 args, varargs=None, varkw=None, defaults=None,
40 kwonlyargs=(), kwonlydefaults={}, annotations={},
40 kwonlyargs=(), kwonlydefaults={}, annotations={},
41 formatarg=str,
41 formatarg=str,
42 formatvarargs=lambda name: '*' + name,
42 formatvarargs=lambda name: '*' + name,
43 formatvarkw=lambda name: '**' + name,
43 formatvarkw=lambda name: '**' + name,
44 formatvalue=lambda value: '=' + repr(value),
44 formatvalue=lambda value: '=' + repr(value),
45 formatreturns=lambda text: ' -> ' + text,
45 formatreturns=lambda text: ' -> ' + text,
46 formatannotation=formatannotation):
46 formatannotation=formatannotation):
47 """Copy formatargspec from python 3.7 standard library.
47 """Copy formatargspec from python 3.7 standard library.
48 Python 3 has deprecated formatargspec and requested that Signature
48 Python 3 has deprecated formatargspec and requested that Signature
49 be used instead, however this requires a full reimplementation
49 be used instead, however this requires a full reimplementation
50 of formatargspec() in terms of creating Parameter objects and such.
50 of formatargspec() in terms of creating Parameter objects and such.
51 Instead of introducing all the object-creation overhead and having
51 Instead of introducing all the object-creation overhead and having
52 to reinvent from scratch, just copy their compatibility routine.
52 to reinvent from scratch, just copy their compatibility routine.
53 Utimately we would need to rewrite our "decorator" routine completely
53 Utimately we would need to rewrite our "decorator" routine completely
54 which is not really worth it right now, until all Python 2.x support
54 which is not really worth it right now, until all Python 2.x support
55 is dropped.
55 is dropped.
56 """
56 """
57
57
58 def formatargandannotation(arg):
58 def formatargandannotation(arg):
59 result = formatarg(arg)
59 result = formatarg(arg)
60 if arg in annotations:
60 if arg in annotations:
61 result += ': ' + formatannotation(annotations[arg])
61 result += ': ' + formatannotation(annotations[arg])
62 return result
62 return result
63
63
64 specs = []
64 specs = []
65 if defaults:
65 if defaults:
66 firstdefault = len(args) - len(defaults)
66 firstdefault = len(args) - len(defaults)
67 for i, arg in enumerate(args):
67 for i, arg in enumerate(args):
68 spec = formatargandannotation(arg)
68 spec = formatargandannotation(arg)
69 if defaults and i >= firstdefault:
69 if defaults and i >= firstdefault:
70 spec = spec + formatvalue(defaults[i - firstdefault])
70 spec = spec + formatvalue(defaults[i - firstdefault])
71 specs.append(spec)
71 specs.append(spec)
72 if varargs is not None:
72 if varargs is not None:
73 specs.append(formatvarargs(formatargandannotation(varargs)))
73 specs.append(formatvarargs(formatargandannotation(varargs)))
74 else:
74 else:
75 if kwonlyargs:
75 if kwonlyargs:
76 specs.append('*')
76 specs.append('*')
77 if kwonlyargs:
77 if kwonlyargs:
78 for kwonlyarg in kwonlyargs:
78 for kwonlyarg in kwonlyargs:
79 spec = formatargandannotation(kwonlyarg)
79 spec = formatargandannotation(kwonlyarg)
80 if kwonlydefaults and kwonlyarg in kwonlydefaults:
80 if kwonlydefaults and kwonlyarg in kwonlydefaults:
81 spec += formatvalue(kwonlydefaults[kwonlyarg])
81 spec += formatvalue(kwonlydefaults[kwonlyarg])
82 specs.append(spec)
82 specs.append(spec)
83 if varkw is not None:
83 if varkw is not None:
84 specs.append(formatvarkw(formatargandannotation(varkw)))
84 specs.append(formatvarkw(formatargandannotation(varkw)))
85 result = '(' + ', '.join(specs) + ')'
85 result = '(' + ', '.join(specs) + ')'
86 if 'return' in annotations:
86 if 'return' in annotations:
87 result += formatreturns(formatannotation(annotations['return']))
87 result += formatreturns(formatannotation(annotations['return']))
88 return result
88 return result
89
89
90 # NOTE: inject for python3.11
90 # NOTE: inject for python3.11
91 inspect.formatargspec = backport_inspect_formatargspec
91 inspect.formatargspec = backport_inspect_formatargspec
92 return inspect
92 return inspect
93
93
94
94
95 def inspect_getargspec():
95 def inspect_getargspec():
96 """
96 """
97 Pyramid rely on inspect.getargspec to lookup the signature of
97 Pyramid rely on inspect.getargspec to lookup the signature of
98 view functions. This is not compatible with cython, therefore we replace
98 view functions. This is not compatible with cython, therefore we replace
99 getargspec with a custom version.
99 getargspec with a custom version.
100 Code is inspired by the inspect module from Python-3.4
100 Code is inspired by the inspect module from Python-3.4
101 """
101 """
102 import inspect
102 import inspect
103
103
104 def _isCython(func):
104 def _isCython(func):
105 """
105 """
106 Private helper that checks if a function is a cython function.
106 Private helper that checks if a function is a cython function.
107 """
107 """
108 return func.__class__.__name__ == 'cython_function_or_method'
108 return func.__class__.__name__ == 'cython_function_or_method'
109
109
110 def unwrap(func):
110 def unwrap(func):
111 """
111 """
112 Get the object wrapped by *func*.
112 Get the object wrapped by *func*.
113
113
114 Follows the chain of :attr:`__wrapped__` attributes returning the last
114 Follows the chain of :attr:`__wrapped__` attributes returning the last
115 object in the chain.
115 object in the chain.
116
116
117 *stop* is an optional callback accepting an object in the wrapper chain
117 *stop* is an optional callback accepting an object in the wrapper chain
118 as its sole argument that allows the unwrapping to be terminated early
118 as its sole argument that allows the unwrapping to be terminated early
119 if the callback returns a true value. If the callback never returns a
119 if the callback returns a true value. If the callback never returns a
120 true value, the last object in the chain is returned as usual. For
120 true value, the last object in the chain is returned as usual. For
121 example, :func:`signature` uses this to stop unwrapping if any object
121 example, :func:`signature` uses this to stop unwrapping if any object
122 in the chain has a ``__signature__`` attribute defined.
122 in the chain has a ``__signature__`` attribute defined.
123
123
124 :exc:`ValueError` is raised if a cycle is encountered.
124 :exc:`ValueError` is raised if a cycle is encountered.
125 """
125 """
126 f = func # remember the original func for error reporting
126 f = func # remember the original func for error reporting
127 memo = {id(f)} # Memoise by id to tolerate non-hashable objects
127 memo = {id(f)} # Memoise by id to tolerate non-hashable objects
128 while hasattr(func, '__wrapped__'):
128 while hasattr(func, '__wrapped__'):
129 func = func.__wrapped__
129 func = func.__wrapped__
130 id_func = id(func)
130 id_func = id(func)
131 if id_func in memo:
131 if id_func in memo:
132 raise ValueError(f'wrapper loop when unwrapping {f!r}')
132 raise ValueError(f'wrapper loop when unwrapping {f!r}')
133 memo.add(id_func)
133 memo.add(id_func)
134 return func
134 return func
135
135
136 def custom_getargspec(func):
136 def custom_getargspec(func):
137 """
137 """
138 Get the names and default values of a function's arguments.
138 Get the names and default values of a function's arguments.
139
139
140 A tuple of four things is returned: (args, varargs, varkw, defaults).
140 A tuple of four things is returned: (args, varargs, varkw, defaults).
141 'args' is a list of the argument names (it may contain nested lists).
141 'args' is a list of the argument names (it may contain nested lists).
142 'varargs' and 'varkw' are the names of the * and ** arguments or None.
142 'varargs' and 'varkw' are the names of the * and ** arguments or None.
143 'defaults' is an n-tuple of the default values of the last n arguments.
143 'defaults' is an n-tuple of the default values of the last n arguments.
144 """
144 """
145
145
146 func = unwrap(func)
146 func = unwrap(func)
147
147
148 if inspect.ismethod(func):
148 if inspect.ismethod(func):
149 func = func.im_func
149 func = func.im_func
150 if not inspect.isfunction(func):
150 if not inspect.isfunction(func):
151 if not _isCython(func):
151 if not _isCython(func):
152 raise TypeError('{!r} is not a Python or Cython function'
152 raise TypeError('{!r} is not a Python or Cython function'
153 .format(func))
153 .format(func))
154 args, varargs, varkw = inspect.getargs(func.func_code)
154 args, varargs, varkw = inspect.getargs(func.func_code)
155 return inspect.ArgSpec(args, varargs, varkw, func.func_defaults)
155 return inspect.ArgSpec(args, varargs, varkw, func.func_defaults)
156
156
157 # NOTE: inject for python3.11
157 # NOTE: inject for python3.11
158 inspect.getargspec = inspect.getfullargspec
158 inspect.getargspec = inspect.getfullargspec
159
159
160 return inspect
160 return inspect
161
162
163 def repoze_sendmail_lf_fix():
164 from repoze.sendmail import encoding
165 from email.policy import SMTP
166
167 encoding.encode_message = lambda message, *args, **kwargs: message.as_bytes(policy=SMTP)
@@ -1,372 +1,373 b''
1 # Copyright (C) 2010-2023 RhodeCode GmbH
1 # Copyright (C) 2010-2023 RhodeCode GmbH
2 #
2 #
3 # This program is free software: you can redistribute it and/or modify
3 # This program is free software: you can redistribute it and/or modify
4 # it under the terms of the GNU Affero General Public License, version 3
4 # it under the terms of the GNU Affero General Public License, version 3
5 # (only), as published by the Free Software Foundation.
5 # (only), as published by the Free Software Foundation.
6 #
6 #
7 # This program is distributed in the hope that it will be useful,
7 # This program is distributed in the hope that it will be useful,
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # GNU General Public License for more details.
10 # GNU General Public License for more details.
11 #
11 #
12 # You should have received a copy of the GNU Affero General Public License
12 # You should have received a copy of the GNU Affero General Public License
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 #
14 #
15 # This program is dual-licensed. If you wish to learn more about the
15 # This program is dual-licensed. If you wish to learn more about the
16 # RhodeCode Enterprise Edition, including its added features, Support services,
16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 """
18 """
19 Celery loader, run with::
19 Celery loader, run with::
20
20
21 celery worker \
21 celery worker \
22 --task-events \
22 --task-events \
23 --beat \
23 --beat \
24 --autoscale=20,2 \
24 --autoscale=20,2 \
25 --max-tasks-per-child 1 \
25 --max-tasks-per-child 1 \
26 --app rhodecode.lib.celerylib.loader \
26 --app rhodecode.lib.celerylib.loader \
27 --scheduler rhodecode.lib.celerylib.scheduler.RcScheduler \
27 --scheduler rhodecode.lib.celerylib.scheduler.RcScheduler \
28 --loglevel DEBUG --ini=.dev/dev.ini
28 --loglevel DEBUG --ini=.dev/dev.ini
29 """
29 """
30 from rhodecode.config.patches import inspect_getargspec, inspect_formatargspec
30 from rhodecode.config import patches
31 inspect_getargspec()
31 patches.inspect_getargspec()
32 inspect_formatargspec()
32 patches.inspect_formatargspec()
33 # python3.11 inspect patches for backward compat on `paste` code
33 # python3.11 inspect patches for backward compat on `paste` code
34 patches.repoze_sendmail_lf_fix()
34
35
35 import sys
36 import sys
36 import logging
37 import logging
37 import importlib
38 import importlib
38
39
39 import click
40 import click
40 from celery import Celery
41 from celery import Celery
41 from celery import signals
42 from celery import signals
42 from celery import Task
43 from celery import Task
43 from celery import exceptions # noqa
44 from celery import exceptions # noqa
44
45
45 import rhodecode
46 import rhodecode
46
47
47 from rhodecode.lib.statsd_client import StatsdClient
48 from rhodecode.lib.statsd_client import StatsdClient
48 from rhodecode.lib.celerylib.utils import parse_ini_vars, ping_db
49 from rhodecode.lib.celerylib.utils import parse_ini_vars, ping_db
49 from rhodecode.lib.ext_json import json
50 from rhodecode.lib.ext_json import json
50 from rhodecode.lib.pyramid_utils import bootstrap, setup_logging
51 from rhodecode.lib.pyramid_utils import bootstrap, setup_logging
51 from rhodecode.lib.utils2 import str2bool
52 from rhodecode.lib.utils2 import str2bool
52 from rhodecode.model import meta
53 from rhodecode.model import meta
53
54
54 log = logging.getLogger('celery.rhodecode.loader')
55 log = logging.getLogger('celery.rhodecode.loader')
55
56
56
57
57 imports = ['rhodecode.lib.celerylib.tasks']
58 imports = ['rhodecode.lib.celerylib.tasks']
58
59
59 try:
60 try:
60 # try if we have EE tasks available
61 # try if we have EE tasks available
61 importlib.import_module('rc_ee')
62 importlib.import_module('rc_ee')
62 imports.append('rc_ee.lib.celerylib.tasks')
63 imports.append('rc_ee.lib.celerylib.tasks')
63 except ImportError:
64 except ImportError:
64 pass
65 pass
65
66
66
67
67 base_celery_config = {
68 base_celery_config = {
68 'result_backend': 'rpc://',
69 'result_backend': 'rpc://',
69 'result_expires': 60 * 60 * 24,
70 'result_expires': 60 * 60 * 24,
70 'result_persistent': True,
71 'result_persistent': True,
71 'imports': imports,
72 'imports': imports,
72 'worker_max_tasks_per_child': 100,
73 'worker_max_tasks_per_child': 100,
73 'worker_hijack_root_logger': False,
74 'worker_hijack_root_logger': False,
74 'worker_prefetch_multiplier': 1,
75 'worker_prefetch_multiplier': 1,
75 'task_serializer': 'json',
76 'task_serializer': 'json',
76 'accept_content': ['json', 'msgpack'],
77 'accept_content': ['json', 'msgpack'],
77 'result_serializer': 'json',
78 'result_serializer': 'json',
78 'result_accept_content': ['json', 'msgpack'],
79 'result_accept_content': ['json', 'msgpack'],
79
80
80 'broker_connection_retry_on_startup': True,
81 'broker_connection_retry_on_startup': True,
81 'database_table_names': {
82 'database_table_names': {
82 'task': 'beat_taskmeta',
83 'task': 'beat_taskmeta',
83 'group': 'beat_groupmeta',
84 'group': 'beat_groupmeta',
84 }
85 }
85 }
86 }
86
87
87
88
88 preload_option_ini = click.Option(
89 preload_option_ini = click.Option(
89 ('--ini',),
90 ('--ini',),
90 help='Path to ini configuration file.'
91 help='Path to ini configuration file.'
91 )
92 )
92
93
93 preload_option_ini_var = click.Option(
94 preload_option_ini_var = click.Option(
94 ('--ini-var',),
95 ('--ini-var',),
95 help='Comma separated list of key=value to pass to ini.'
96 help='Comma separated list of key=value to pass to ini.'
96 )
97 )
97
98
98
99
99 def get_logger(obj):
100 def get_logger(obj):
100 custom_log = logging.getLogger(
101 custom_log = logging.getLogger(
101 'rhodecode.task.{}'.format(obj.__class__.__name__))
102 'rhodecode.task.{}'.format(obj.__class__.__name__))
102
103
103 if rhodecode.CELERY_ENABLED:
104 if rhodecode.CELERY_ENABLED:
104 try:
105 try:
105 custom_log = obj.get_logger()
106 custom_log = obj.get_logger()
106 except Exception:
107 except Exception:
107 pass
108 pass
108
109
109 return custom_log
110 return custom_log
110
111
111
112
112 # init main celery app
113 # init main celery app
113 celery_app = Celery()
114 celery_app = Celery()
114 celery_app.user_options['preload'].add(preload_option_ini)
115 celery_app.user_options['preload'].add(preload_option_ini)
115 celery_app.user_options['preload'].add(preload_option_ini_var)
116 celery_app.user_options['preload'].add(preload_option_ini_var)
116
117
117
118
118 @signals.setup_logging.connect
119 @signals.setup_logging.connect
119 def setup_logging_callback(**kwargs):
120 def setup_logging_callback(**kwargs):
120
121
121 if 'RC_INI_FILE' in celery_app.conf:
122 if 'RC_INI_FILE' in celery_app.conf:
122 ini_file = celery_app.conf['RC_INI_FILE']
123 ini_file = celery_app.conf['RC_INI_FILE']
123 else:
124 else:
124 ini_file = celery_app.user_options['RC_INI_FILE']
125 ini_file = celery_app.user_options['RC_INI_FILE']
125
126
126 setup_logging(ini_file)
127 setup_logging(ini_file)
127
128
128
129
129 @signals.user_preload_options.connect
130 @signals.user_preload_options.connect
130 def on_preload_parsed(options, **kwargs):
131 def on_preload_parsed(options, **kwargs):
131
132
132 ini_file = options['ini']
133 ini_file = options['ini']
133 ini_vars = options['ini_var']
134 ini_vars = options['ini_var']
134
135
135 if ini_file is None:
136 if ini_file is None:
136 print('You must provide the --ini argument to start celery')
137 print('You must provide the --ini argument to start celery')
137 exit(-1)
138 exit(-1)
138
139
139 options = None
140 options = None
140 if ini_vars is not None:
141 if ini_vars is not None:
141 options = parse_ini_vars(ini_vars)
142 options = parse_ini_vars(ini_vars)
142
143
143 celery_app.conf['RC_INI_FILE'] = ini_file
144 celery_app.conf['RC_INI_FILE'] = ini_file
144 celery_app.user_options['RC_INI_FILE'] = ini_file
145 celery_app.user_options['RC_INI_FILE'] = ini_file
145
146
146 celery_app.conf['RC_INI_OPTIONS'] = options
147 celery_app.conf['RC_INI_OPTIONS'] = options
147 celery_app.user_options['RC_INI_OPTIONS'] = options
148 celery_app.user_options['RC_INI_OPTIONS'] = options
148
149
149 setup_logging(ini_file)
150 setup_logging(ini_file)
150
151
151
152
152 def _init_celery(app_type=''):
153 def _init_celery(app_type=''):
153 from rhodecode.config.middleware import get_celery_config
154 from rhodecode.config.middleware import get_celery_config
154
155
155 log.debug('Bootstrapping RhodeCode application for %s...', app_type)
156 log.debug('Bootstrapping RhodeCode application for %s...', app_type)
156
157
157 ini_file = celery_app.conf['RC_INI_FILE']
158 ini_file = celery_app.conf['RC_INI_FILE']
158 options = celery_app.conf['RC_INI_OPTIONS']
159 options = celery_app.conf['RC_INI_OPTIONS']
159
160
160 env = None
161 env = None
161 try:
162 try:
162 env = bootstrap(ini_file, options=options)
163 env = bootstrap(ini_file, options=options)
163 except Exception:
164 except Exception:
164 log.exception('Failed to bootstrap RhodeCode APP. '
165 log.exception('Failed to bootstrap RhodeCode APP. '
165 'Probably there is another error present that prevents from running pyramid app')
166 'Probably there is another error present that prevents from running pyramid app')
166
167
167 if not env:
168 if not env:
168 # we use sys.exit here since we need to signal app startup failure for docker to restart the container and re-try
169 # we use sys.exit here since we need to signal app startup failure for docker to restart the container and re-try
169 sys.exit(1)
170 sys.exit(1)
170
171
171 log.debug('Got Pyramid ENV: %s', env)
172 log.debug('Got Pyramid ENV: %s', env)
172
173
173 settings = env['registry'].settings
174 settings = env['registry'].settings
174 celery_settings = get_celery_config(settings)
175 celery_settings = get_celery_config(settings)
175
176
176 # init and bootstrap StatsdClient
177 # init and bootstrap StatsdClient
177 StatsdClient.setup(settings)
178 StatsdClient.setup(settings)
178
179
179 setup_celery_app(
180 setup_celery_app(
180 app=env['app'], root=env['root'], request=env['request'],
181 app=env['app'], root=env['root'], request=env['request'],
181 registry=env['registry'], closer=env['closer'],
182 registry=env['registry'], closer=env['closer'],
182 celery_settings=celery_settings)
183 celery_settings=celery_settings)
183
184
184
185
185 @signals.celeryd_init.connect
186 @signals.celeryd_init.connect
186 def on_celeryd_init(sender=None, conf=None, **kwargs):
187 def on_celeryd_init(sender=None, conf=None, **kwargs):
187 _init_celery('celery worker')
188 _init_celery('celery worker')
188
189
189 # fix the global flag even if it's disabled via .ini file because this
190 # fix the global flag even if it's disabled via .ini file because this
190 # is a worker code that doesn't need this to be disabled.
191 # is a worker code that doesn't need this to be disabled.
191 rhodecode.CELERY_ENABLED = True
192 rhodecode.CELERY_ENABLED = True
192
193
193
194
194 @signals.beat_init.connect
195 @signals.beat_init.connect
195 def on_beat_init(sender=None, conf=None, **kwargs):
196 def on_beat_init(sender=None, conf=None, **kwargs):
196 _init_celery('celery beat')
197 _init_celery('celery beat')
197
198
198
199
199 @signals.task_prerun.connect
200 @signals.task_prerun.connect
200 def task_prerun_signal(task_id, task, args, **kwargs):
201 def task_prerun_signal(task_id, task, args, **kwargs):
201 ping_db()
202 ping_db()
202 statsd = StatsdClient.statsd
203 statsd = StatsdClient.statsd
203
204
204 if statsd:
205 if statsd:
205 task_repr = getattr(task, 'name', task)
206 task_repr = getattr(task, 'name', task)
206 statsd.incr('rhodecode_celery_task_total', tags=[
207 statsd.incr('rhodecode_celery_task_total', tags=[
207 f'task:{task_repr}',
208 f'task:{task_repr}',
208 'mode:async'
209 'mode:async'
209 ])
210 ])
210
211
211
212
212 @signals.task_success.connect
213 @signals.task_success.connect
213 def task_success_signal(result, **kwargs):
214 def task_success_signal(result, **kwargs):
214 meta.Session.commit()
215 meta.Session.commit()
215 closer = celery_app.conf['PYRAMID_CLOSER']
216 closer = celery_app.conf['PYRAMID_CLOSER']
216 if closer:
217 if closer:
217 closer()
218 closer()
218
219
219
220
220 @signals.task_retry.connect
221 @signals.task_retry.connect
221 def task_retry_signal(
222 def task_retry_signal(
222 request, reason, einfo, **kwargs):
223 request, reason, einfo, **kwargs):
223 meta.Session.remove()
224 meta.Session.remove()
224 closer = celery_app.conf['PYRAMID_CLOSER']
225 closer = celery_app.conf['PYRAMID_CLOSER']
225 if closer:
226 if closer:
226 closer()
227 closer()
227
228
228
229
229 @signals.task_failure.connect
230 @signals.task_failure.connect
230 def task_failure_signal(
231 def task_failure_signal(
231 task_id, exception, args, kwargs, traceback, einfo, **kargs):
232 task_id, exception, args, kwargs, traceback, einfo, **kargs):
232
233
233 log.error('Task: %s failed !! exc_info: %s', task_id, einfo)
234 log.error('Task: %s failed !! exc_info: %s', task_id, einfo)
234 from rhodecode.lib.exc_tracking import store_exception
235 from rhodecode.lib.exc_tracking import store_exception
235 from rhodecode.lib.statsd_client import StatsdClient
236 from rhodecode.lib.statsd_client import StatsdClient
236
237
237 meta.Session.remove()
238 meta.Session.remove()
238
239
239 # simulate sys.exc_info()
240 # simulate sys.exc_info()
240 exc_info = (einfo.type, einfo.exception, einfo.tb)
241 exc_info = (einfo.type, einfo.exception, einfo.tb)
241 store_exception(id(exc_info), exc_info, prefix='rhodecode-celery')
242 store_exception(id(exc_info), exc_info, prefix='rhodecode-celery')
242 statsd = StatsdClient.statsd
243 statsd = StatsdClient.statsd
243 if statsd:
244 if statsd:
244 exc_type = "{}.{}".format(einfo.__class__.__module__, einfo.__class__.__name__)
245 exc_type = "{}.{}".format(einfo.__class__.__module__, einfo.__class__.__name__)
245 statsd.incr('rhodecode_exception_total',
246 statsd.incr('rhodecode_exception_total',
246 tags=["exc_source:celery", "type:{}".format(exc_type)])
247 tags=["exc_source:celery", "type:{}".format(exc_type)])
247
248
248 closer = celery_app.conf['PYRAMID_CLOSER']
249 closer = celery_app.conf['PYRAMID_CLOSER']
249 if closer:
250 if closer:
250 closer()
251 closer()
251
252
252
253
253 @signals.task_revoked.connect
254 @signals.task_revoked.connect
254 def task_revoked_signal(
255 def task_revoked_signal(
255 request, terminated, signum, expired, **kwargs):
256 request, terminated, signum, expired, **kwargs):
256 closer = celery_app.conf['PYRAMID_CLOSER']
257 closer = celery_app.conf['PYRAMID_CLOSER']
257 if closer:
258 if closer:
258 closer()
259 closer()
259
260
260
261
261 class UNSET(object):
262 class UNSET(object):
262 pass
263 pass
263
264
264
265
265 _unset = UNSET()
266 _unset = UNSET()
266
267
267
268
268 def set_celery_conf(app=_unset, root=_unset, request=_unset, registry=_unset, closer=_unset):
269 def set_celery_conf(app=_unset, root=_unset, request=_unset, registry=_unset, closer=_unset):
269
270
270 if request is not UNSET:
271 if request is not UNSET:
271 celery_app.conf.update({'PYRAMID_REQUEST': request})
272 celery_app.conf.update({'PYRAMID_REQUEST': request})
272
273
273 if registry is not UNSET:
274 if registry is not UNSET:
274 celery_app.conf.update({'PYRAMID_REGISTRY': registry})
275 celery_app.conf.update({'PYRAMID_REGISTRY': registry})
275
276
276
277
277 def setup_celery_app(app, root, request, registry, closer, celery_settings):
278 def setup_celery_app(app, root, request, registry, closer, celery_settings):
278 log.debug('Got custom celery conf: %s', celery_settings)
279 log.debug('Got custom celery conf: %s', celery_settings)
279 celery_config = base_celery_config
280 celery_config = base_celery_config
280 celery_config.update({
281 celery_config.update({
281 # store celerybeat scheduler db where the .ini file is
282 # store celerybeat scheduler db where the .ini file is
282 'beat_schedule_filename': registry.settings['celerybeat-schedule.path'],
283 'beat_schedule_filename': registry.settings['celerybeat-schedule.path'],
283 })
284 })
284
285
285 celery_config.update(celery_settings)
286 celery_config.update(celery_settings)
286 celery_app.config_from_object(celery_config)
287 celery_app.config_from_object(celery_config)
287
288
288 celery_app.conf.update({'PYRAMID_APP': app})
289 celery_app.conf.update({'PYRAMID_APP': app})
289 celery_app.conf.update({'PYRAMID_ROOT': root})
290 celery_app.conf.update({'PYRAMID_ROOT': root})
290 celery_app.conf.update({'PYRAMID_REQUEST': request})
291 celery_app.conf.update({'PYRAMID_REQUEST': request})
291 celery_app.conf.update({'PYRAMID_REGISTRY': registry})
292 celery_app.conf.update({'PYRAMID_REGISTRY': registry})
292 celery_app.conf.update({'PYRAMID_CLOSER': closer})
293 celery_app.conf.update({'PYRAMID_CLOSER': closer})
293
294
294
295
295 def configure_celery(config, celery_settings):
296 def configure_celery(config, celery_settings):
296 """
297 """
297 Helper that is called from our application creation logic. It gives
298 Helper that is called from our application creation logic. It gives
298 connection info into running webapp and allows execution of tasks from
299 connection info into running webapp and allows execution of tasks from
299 RhodeCode itself
300 RhodeCode itself
300 """
301 """
301 # store some globals into rhodecode
302 # store some globals into rhodecode
302 rhodecode.CELERY_ENABLED = str2bool(
303 rhodecode.CELERY_ENABLED = str2bool(
303 config.registry.settings.get('use_celery'))
304 config.registry.settings.get('use_celery'))
304 if rhodecode.CELERY_ENABLED:
305 if rhodecode.CELERY_ENABLED:
305 log.info('Configuring celery based on `%s` settings', celery_settings)
306 log.info('Configuring celery based on `%s` settings', celery_settings)
306 setup_celery_app(
307 setup_celery_app(
307 app=None, root=None, request=None, registry=config.registry,
308 app=None, root=None, request=None, registry=config.registry,
308 closer=None, celery_settings=celery_settings)
309 closer=None, celery_settings=celery_settings)
309
310
310
311
311 def maybe_prepare_env(req):
312 def maybe_prepare_env(req):
312 environ = {}
313 environ = {}
313 try:
314 try:
314 environ.update({
315 environ.update({
315 'PATH_INFO': req.environ['PATH_INFO'],
316 'PATH_INFO': req.environ['PATH_INFO'],
316 'SCRIPT_NAME': req.environ['SCRIPT_NAME'],
317 'SCRIPT_NAME': req.environ['SCRIPT_NAME'],
317 'HTTP_HOST': req.environ.get('HTTP_HOST', req.environ['SERVER_NAME']),
318 'HTTP_HOST': req.environ.get('HTTP_HOST', req.environ['SERVER_NAME']),
318 'SERVER_NAME': req.environ['SERVER_NAME'],
319 'SERVER_NAME': req.environ['SERVER_NAME'],
319 'SERVER_PORT': req.environ['SERVER_PORT'],
320 'SERVER_PORT': req.environ['SERVER_PORT'],
320 'wsgi.url_scheme': req.environ['wsgi.url_scheme'],
321 'wsgi.url_scheme': req.environ['wsgi.url_scheme'],
321 })
322 })
322 except Exception:
323 except Exception:
323 pass
324 pass
324
325
325 return environ
326 return environ
326
327
327
328
328 class RequestContextTask(Task):
329 class RequestContextTask(Task):
329 """
330 """
330 This is a celery task which will create a rhodecode app instance context
331 This is a celery task which will create a rhodecode app instance context
331 for the task, patch pyramid with the original request
332 for the task, patch pyramid with the original request
332 that created the task and also add the user to the context.
333 that created the task and also add the user to the context.
333 """
334 """
334
335
335 def apply_async(self, args=None, kwargs=None, task_id=None, producer=None,
336 def apply_async(self, args=None, kwargs=None, task_id=None, producer=None,
336 link=None, link_error=None, shadow=None, **options):
337 link=None, link_error=None, shadow=None, **options):
337 """ queue the job to run (we are in web request context here) """
338 """ queue the job to run (we are in web request context here) """
338 from rhodecode.lib.base import get_ip_addr
339 from rhodecode.lib.base import get_ip_addr
339
340
340 req = self.app.conf['PYRAMID_REQUEST']
341 req = self.app.conf['PYRAMID_REQUEST']
341 if not req:
342 if not req:
342 raise ValueError('celery_app.conf is having empty PYRAMID_REQUEST key')
343 raise ValueError('celery_app.conf is having empty PYRAMID_REQUEST key')
343
344
344 log.debug('Running Task with class: %s. Request Class: %s',
345 log.debug('Running Task with class: %s. Request Class: %s',
345 self.__class__, req.__class__)
346 self.__class__, req.__class__)
346
347
347 user_id = 0
348 user_id = 0
348
349
349 # web case
350 # web case
350 if hasattr(req, 'user'):
351 if hasattr(req, 'user'):
351 user_id = req.user.user_id
352 user_id = req.user.user_id
352
353
353 # api case
354 # api case
354 elif hasattr(req, 'rpc_user'):
355 elif hasattr(req, 'rpc_user'):
355 user_id = req.rpc_user.user_id
356 user_id = req.rpc_user.user_id
356
357
357 # we hook into kwargs since it is the only way to pass our data to
358 # we hook into kwargs since it is the only way to pass our data to
358 # the celery worker
359 # the celery worker
359 environ = maybe_prepare_env(req)
360 environ = maybe_prepare_env(req)
360 options['headers'] = options.get('headers', {})
361 options['headers'] = options.get('headers', {})
361 options['headers'].update({
362 options['headers'].update({
362 'rhodecode_proxy_data': {
363 'rhodecode_proxy_data': {
363 'environ': environ,
364 'environ': environ,
364 'auth_user': {
365 'auth_user': {
365 'ip_addr': get_ip_addr(req.environ),
366 'ip_addr': get_ip_addr(req.environ),
366 'user_id': user_id
367 'user_id': user_id
367 },
368 },
368 }
369 }
369 })
370 })
370
371
371 return super(RequestContextTask, self).apply_async(
372 return super(RequestContextTask, self).apply_async(
372 args, kwargs, task_id, producer, link, link_error, shadow, **options)
373 args, kwargs, task_id, producer, link, link_error, shadow, **options)
General Comments 0
You need to be logged in to leave comments. Login now