##// END OF EJS Templates
metrics: expose exc_type in consistent format
super-admin -
r4808:e4831d78 default
parent child Browse files
Show More
@@ -1,576 +1,578 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2011-2020 RhodeCode GmbH
3 # Copyright (C) 2011-2020 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 import itertools
21 import itertools
22 import logging
22 import logging
23 import sys
23 import sys
24 import types
24 import types
25 import fnmatch
25 import fnmatch
26
26
27 import decorator
27 import decorator
28 import venusian
28 import venusian
29 from collections import OrderedDict
29 from collections import OrderedDict
30
30
31 from pyramid.exceptions import ConfigurationError
31 from pyramid.exceptions import ConfigurationError
32 from pyramid.renderers import render
32 from pyramid.renderers import render
33 from pyramid.response import Response
33 from pyramid.response import Response
34 from pyramid.httpexceptions import HTTPNotFound
34 from pyramid.httpexceptions import HTTPNotFound
35
35
36 from rhodecode.api.exc import (
36 from rhodecode.api.exc import (
37 JSONRPCBaseError, JSONRPCError, JSONRPCForbidden, JSONRPCValidationError)
37 JSONRPCBaseError, JSONRPCError, JSONRPCForbidden, JSONRPCValidationError)
38 from rhodecode.apps._base import TemplateArgs
38 from rhodecode.apps._base import TemplateArgs
39 from rhodecode.lib.auth import AuthUser
39 from rhodecode.lib.auth import AuthUser
40 from rhodecode.lib.base import get_ip_addr, attach_context_attributes
40 from rhodecode.lib.base import get_ip_addr, attach_context_attributes
41 from rhodecode.lib.exc_tracking import store_exception
41 from rhodecode.lib.exc_tracking import store_exception
42 from rhodecode.lib.ext_json import json
42 from rhodecode.lib.ext_json import json
43 from rhodecode.lib.utils2 import safe_str
43 from rhodecode.lib.utils2 import safe_str
44 from rhodecode.lib.plugins.utils import get_plugin_settings
44 from rhodecode.lib.plugins.utils import get_plugin_settings
45 from rhodecode.model.db import User, UserApiKeys
45 from rhodecode.model.db import User, UserApiKeys
46
46
47 log = logging.getLogger(__name__)
47 log = logging.getLogger(__name__)
48
48
49 DEFAULT_RENDERER = 'jsonrpc_renderer'
49 DEFAULT_RENDERER = 'jsonrpc_renderer'
50 DEFAULT_URL = '/_admin/apiv2'
50 DEFAULT_URL = '/_admin/apiv2'
51
51
52
52
53 def find_methods(jsonrpc_methods, pattern):
53 def find_methods(jsonrpc_methods, pattern):
54 matches = OrderedDict()
54 matches = OrderedDict()
55 if not isinstance(pattern, (list, tuple)):
55 if not isinstance(pattern, (list, tuple)):
56 pattern = [pattern]
56 pattern = [pattern]
57
57
58 for single_pattern in pattern:
58 for single_pattern in pattern:
59 for method_name, method in jsonrpc_methods.items():
59 for method_name, method in jsonrpc_methods.items():
60 if fnmatch.fnmatch(method_name, single_pattern):
60 if fnmatch.fnmatch(method_name, single_pattern):
61 matches[method_name] = method
61 matches[method_name] = method
62 return matches
62 return matches
63
63
64
64
65 class ExtJsonRenderer(object):
65 class ExtJsonRenderer(object):
66 """
66 """
67 Custom renderer that mkaes use of our ext_json lib
67 Custom renderer that mkaes use of our ext_json lib
68
68
69 """
69 """
70
70
71 def __init__(self, serializer=json.dumps, **kw):
71 def __init__(self, serializer=json.dumps, **kw):
72 """ Any keyword arguments will be passed to the ``serializer``
72 """ Any keyword arguments will be passed to the ``serializer``
73 function."""
73 function."""
74 self.serializer = serializer
74 self.serializer = serializer
75 self.kw = kw
75 self.kw = kw
76
76
77 def __call__(self, info):
77 def __call__(self, info):
78 """ Returns a plain JSON-encoded string with content-type
78 """ Returns a plain JSON-encoded string with content-type
79 ``application/json``. The content-type may be overridden by
79 ``application/json``. The content-type may be overridden by
80 setting ``request.response.content_type``."""
80 setting ``request.response.content_type``."""
81
81
82 def _render(value, system):
82 def _render(value, system):
83 request = system.get('request')
83 request = system.get('request')
84 if request is not None:
84 if request is not None:
85 response = request.response
85 response = request.response
86 ct = response.content_type
86 ct = response.content_type
87 if ct == response.default_content_type:
87 if ct == response.default_content_type:
88 response.content_type = 'application/json'
88 response.content_type = 'application/json'
89
89
90 return self.serializer(value, **self.kw)
90 return self.serializer(value, **self.kw)
91
91
92 return _render
92 return _render
93
93
94
94
95 def jsonrpc_response(request, result):
95 def jsonrpc_response(request, result):
96 rpc_id = getattr(request, 'rpc_id', None)
96 rpc_id = getattr(request, 'rpc_id', None)
97 response = request.response
97 response = request.response
98
98
99 # store content_type before render is called
99 # store content_type before render is called
100 ct = response.content_type
100 ct = response.content_type
101
101
102 ret_value = ''
102 ret_value = ''
103 if rpc_id:
103 if rpc_id:
104 ret_value = {
104 ret_value = {
105 'id': rpc_id,
105 'id': rpc_id,
106 'result': result,
106 'result': result,
107 'error': None,
107 'error': None,
108 }
108 }
109
109
110 # fetch deprecation warnings, and store it inside results
110 # fetch deprecation warnings, and store it inside results
111 deprecation = getattr(request, 'rpc_deprecation', None)
111 deprecation = getattr(request, 'rpc_deprecation', None)
112 if deprecation:
112 if deprecation:
113 ret_value['DEPRECATION_WARNING'] = deprecation
113 ret_value['DEPRECATION_WARNING'] = deprecation
114
114
115 raw_body = render(DEFAULT_RENDERER, ret_value, request=request)
115 raw_body = render(DEFAULT_RENDERER, ret_value, request=request)
116 response.body = safe_str(raw_body, response.charset)
116 response.body = safe_str(raw_body, response.charset)
117
117
118 if ct == response.default_content_type:
118 if ct == response.default_content_type:
119 response.content_type = 'application/json'
119 response.content_type = 'application/json'
120
120
121 return response
121 return response
122
122
123
123
124 def jsonrpc_error(request, message, retid=None, code=None, headers=None):
124 def jsonrpc_error(request, message, retid=None, code=None, headers=None):
125 """
125 """
126 Generate a Response object with a JSON-RPC error body
126 Generate a Response object with a JSON-RPC error body
127
127
128 :param code:
128 :param code:
129 :param retid:
129 :param retid:
130 :param message:
130 :param message:
131 """
131 """
132 err_dict = {'id': retid, 'result': None, 'error': message}
132 err_dict = {'id': retid, 'result': None, 'error': message}
133 body = render(DEFAULT_RENDERER, err_dict, request=request).encode('utf-8')
133 body = render(DEFAULT_RENDERER, err_dict, request=request).encode('utf-8')
134
134
135 return Response(
135 return Response(
136 body=body,
136 body=body,
137 status=code,
137 status=code,
138 content_type='application/json',
138 content_type='application/json',
139 headerlist=headers
139 headerlist=headers
140 )
140 )
141
141
142
142
143 def exception_view(exc, request):
143 def exception_view(exc, request):
144 rpc_id = getattr(request, 'rpc_id', None)
144 rpc_id = getattr(request, 'rpc_id', None)
145
145
146 if isinstance(exc, JSONRPCError):
146 if isinstance(exc, JSONRPCError):
147 fault_message = safe_str(exc.message)
147 fault_message = safe_str(exc.message)
148 log.debug('json-rpc error rpc_id:%s "%s"', rpc_id, fault_message)
148 log.debug('json-rpc error rpc_id:%s "%s"', rpc_id, fault_message)
149 elif isinstance(exc, JSONRPCValidationError):
149 elif isinstance(exc, JSONRPCValidationError):
150 colander_exc = exc.colander_exception
150 colander_exc = exc.colander_exception
151 # TODO(marcink): think maybe of nicer way to serialize errors ?
151 # TODO(marcink): think maybe of nicer way to serialize errors ?
152 fault_message = colander_exc.asdict()
152 fault_message = colander_exc.asdict()
153 log.debug('json-rpc colander error rpc_id:%s "%s"', rpc_id, fault_message)
153 log.debug('json-rpc colander error rpc_id:%s "%s"', rpc_id, fault_message)
154 elif isinstance(exc, JSONRPCForbidden):
154 elif isinstance(exc, JSONRPCForbidden):
155 fault_message = 'Access was denied to this resource.'
155 fault_message = 'Access was denied to this resource.'
156 log.warning('json-rpc forbidden call rpc_id:%s "%s"', rpc_id, fault_message)
156 log.warning('json-rpc forbidden call rpc_id:%s "%s"', rpc_id, fault_message)
157 elif isinstance(exc, HTTPNotFound):
157 elif isinstance(exc, HTTPNotFound):
158 method = request.rpc_method
158 method = request.rpc_method
159 log.debug('json-rpc method `%s` not found in list of '
159 log.debug('json-rpc method `%s` not found in list of '
160 'api calls: %s, rpc_id:%s',
160 'api calls: %s, rpc_id:%s',
161 method, request.registry.jsonrpc_methods.keys(), rpc_id)
161 method, request.registry.jsonrpc_methods.keys(), rpc_id)
162
162
163 similar = 'none'
163 similar = 'none'
164 try:
164 try:
165 similar_paterns = ['*{}*'.format(x) for x in method.split('_')]
165 similar_paterns = ['*{}*'.format(x) for x in method.split('_')]
166 similar_found = find_methods(
166 similar_found = find_methods(
167 request.registry.jsonrpc_methods, similar_paterns)
167 request.registry.jsonrpc_methods, similar_paterns)
168 similar = ', '.join(similar_found.keys()) or similar
168 similar = ', '.join(similar_found.keys()) or similar
169 except Exception:
169 except Exception:
170 # make the whole above block safe
170 # make the whole above block safe
171 pass
171 pass
172
172
173 fault_message = "No such method: {}. Similar methods: {}".format(
173 fault_message = "No such method: {}. Similar methods: {}".format(
174 method, similar)
174 method, similar)
175 else:
175 else:
176 fault_message = 'undefined error'
176 fault_message = 'undefined error'
177 exc_info = exc.exc_info()
177 exc_info = exc.exc_info()
178 store_exception(id(exc_info), exc_info, prefix='rhodecode-api')
178 store_exception(id(exc_info), exc_info, prefix='rhodecode-api')
179
179
180 statsd = request.registry.statsd
180 statsd = request.registry.statsd
181 if statsd:
181 if statsd:
182 statsd.incr('rhodecode_exception_total', tags=["exc_source:api", "type:{}".format(exc_info.type)])
182 exc_type = "{}.{}".format(exc.__class__.__module__, exc.__class__.__name__)
183 statsd.incr('rhodecode_exception_total',
184 tags=["exc_source:api", "type:{}".format(exc_type)])
183
185
184 return jsonrpc_error(request, fault_message, rpc_id)
186 return jsonrpc_error(request, fault_message, rpc_id)
185
187
186
188
187 def request_view(request):
189 def request_view(request):
188 """
190 """
189 Main request handling method. It handles all logic to call a specific
191 Main request handling method. It handles all logic to call a specific
190 exposed method
192 exposed method
191 """
193 """
192 # cython compatible inspect
194 # cython compatible inspect
193 from rhodecode.config.patches import inspect_getargspec
195 from rhodecode.config.patches import inspect_getargspec
194 inspect = inspect_getargspec()
196 inspect = inspect_getargspec()
195
197
196 # check if we can find this session using api_key, get_by_auth_token
198 # check if we can find this session using api_key, get_by_auth_token
197 # search not expired tokens only
199 # search not expired tokens only
198 try:
200 try:
199 api_user = User.get_by_auth_token(request.rpc_api_key)
201 api_user = User.get_by_auth_token(request.rpc_api_key)
200
202
201 if api_user is None:
203 if api_user is None:
202 return jsonrpc_error(
204 return jsonrpc_error(
203 request, retid=request.rpc_id, message='Invalid API KEY')
205 request, retid=request.rpc_id, message='Invalid API KEY')
204
206
205 if not api_user.active:
207 if not api_user.active:
206 return jsonrpc_error(
208 return jsonrpc_error(
207 request, retid=request.rpc_id,
209 request, retid=request.rpc_id,
208 message='Request from this user not allowed')
210 message='Request from this user not allowed')
209
211
210 # check if we are allowed to use this IP
212 # check if we are allowed to use this IP
211 auth_u = AuthUser(
213 auth_u = AuthUser(
212 api_user.user_id, request.rpc_api_key, ip_addr=request.rpc_ip_addr)
214 api_user.user_id, request.rpc_api_key, ip_addr=request.rpc_ip_addr)
213 if not auth_u.ip_allowed:
215 if not auth_u.ip_allowed:
214 return jsonrpc_error(
216 return jsonrpc_error(
215 request, retid=request.rpc_id,
217 request, retid=request.rpc_id,
216 message='Request from IP:%s not allowed' % (
218 message='Request from IP:%s not allowed' % (
217 request.rpc_ip_addr,))
219 request.rpc_ip_addr,))
218 else:
220 else:
219 log.info('Access for IP:%s allowed', request.rpc_ip_addr)
221 log.info('Access for IP:%s allowed', request.rpc_ip_addr)
220
222
221 # register our auth-user
223 # register our auth-user
222 request.rpc_user = auth_u
224 request.rpc_user = auth_u
223 request.environ['rc_auth_user_id'] = auth_u.user_id
225 request.environ['rc_auth_user_id'] = auth_u.user_id
224
226
225 # now check if token is valid for API
227 # now check if token is valid for API
226 auth_token = request.rpc_api_key
228 auth_token = request.rpc_api_key
227 token_match = api_user.authenticate_by_token(
229 token_match = api_user.authenticate_by_token(
228 auth_token, roles=[UserApiKeys.ROLE_API])
230 auth_token, roles=[UserApiKeys.ROLE_API])
229 invalid_token = not token_match
231 invalid_token = not token_match
230
232
231 log.debug('Checking if API KEY is valid with proper role')
233 log.debug('Checking if API KEY is valid with proper role')
232 if invalid_token:
234 if invalid_token:
233 return jsonrpc_error(
235 return jsonrpc_error(
234 request, retid=request.rpc_id,
236 request, retid=request.rpc_id,
235 message='API KEY invalid or, has bad role for an API call')
237 message='API KEY invalid or, has bad role for an API call')
236
238
237 except Exception:
239 except Exception:
238 log.exception('Error on API AUTH')
240 log.exception('Error on API AUTH')
239 return jsonrpc_error(
241 return jsonrpc_error(
240 request, retid=request.rpc_id, message='Invalid API KEY')
242 request, retid=request.rpc_id, message='Invalid API KEY')
241
243
242 method = request.rpc_method
244 method = request.rpc_method
243 func = request.registry.jsonrpc_methods[method]
245 func = request.registry.jsonrpc_methods[method]
244
246
245 # now that we have a method, add request._req_params to
247 # now that we have a method, add request._req_params to
246 # self.kargs and dispatch control to WGIController
248 # self.kargs and dispatch control to WGIController
247 argspec = inspect.getargspec(func)
249 argspec = inspect.getargspec(func)
248 arglist = argspec[0]
250 arglist = argspec[0]
249 defaults = map(type, argspec[3] or [])
251 defaults = map(type, argspec[3] or [])
250 default_empty = types.NotImplementedType
252 default_empty = types.NotImplementedType
251
253
252 # kw arguments required by this method
254 # kw arguments required by this method
253 func_kwargs = dict(itertools.izip_longest(
255 func_kwargs = dict(itertools.izip_longest(
254 reversed(arglist), reversed(defaults), fillvalue=default_empty))
256 reversed(arglist), reversed(defaults), fillvalue=default_empty))
255
257
256 # This attribute will need to be first param of a method that uses
258 # This attribute will need to be first param of a method that uses
257 # api_key, which is translated to instance of user at that name
259 # api_key, which is translated to instance of user at that name
258 user_var = 'apiuser'
260 user_var = 'apiuser'
259 request_var = 'request'
261 request_var = 'request'
260
262
261 for arg in [user_var, request_var]:
263 for arg in [user_var, request_var]:
262 if arg not in arglist:
264 if arg not in arglist:
263 return jsonrpc_error(
265 return jsonrpc_error(
264 request,
266 request,
265 retid=request.rpc_id,
267 retid=request.rpc_id,
266 message='This method [%s] does not support '
268 message='This method [%s] does not support '
267 'required parameter `%s`' % (func.__name__, arg))
269 'required parameter `%s`' % (func.__name__, arg))
268
270
269 # get our arglist and check if we provided them as args
271 # get our arglist and check if we provided them as args
270 for arg, default in func_kwargs.items():
272 for arg, default in func_kwargs.items():
271 if arg in [user_var, request_var]:
273 if arg in [user_var, request_var]:
272 # user_var and request_var are pre-hardcoded parameters and we
274 # user_var and request_var are pre-hardcoded parameters and we
273 # don't need to do any translation
275 # don't need to do any translation
274 continue
276 continue
275
277
276 # skip the required param check if it's default value is
278 # skip the required param check if it's default value is
277 # NotImplementedType (default_empty)
279 # NotImplementedType (default_empty)
278 if default == default_empty and arg not in request.rpc_params:
280 if default == default_empty and arg not in request.rpc_params:
279 return jsonrpc_error(
281 return jsonrpc_error(
280 request,
282 request,
281 retid=request.rpc_id,
283 retid=request.rpc_id,
282 message=('Missing non optional `%s` arg in JSON DATA' % arg)
284 message=('Missing non optional `%s` arg in JSON DATA' % arg)
283 )
285 )
284
286
285 # sanitize extra passed arguments
287 # sanitize extra passed arguments
286 for k in request.rpc_params.keys()[:]:
288 for k in request.rpc_params.keys()[:]:
287 if k not in func_kwargs:
289 if k not in func_kwargs:
288 del request.rpc_params[k]
290 del request.rpc_params[k]
289
291
290 call_params = request.rpc_params
292 call_params = request.rpc_params
291 call_params.update({
293 call_params.update({
292 'request': request,
294 'request': request,
293 'apiuser': auth_u
295 'apiuser': auth_u
294 })
296 })
295
297
296 # register some common functions for usage
298 # register some common functions for usage
297 attach_context_attributes(TemplateArgs(), request, request.rpc_user.user_id)
299 attach_context_attributes(TemplateArgs(), request, request.rpc_user.user_id)
298
300
299 statsd = request.registry.statsd
301 statsd = request.registry.statsd
300
302
301 try:
303 try:
302 ret_value = func(**call_params)
304 ret_value = func(**call_params)
303 resp = jsonrpc_response(request, ret_value)
305 resp = jsonrpc_response(request, ret_value)
304 if statsd:
306 if statsd:
305 statsd.incr('rhodecode_api_call_success_total')
307 statsd.incr('rhodecode_api_call_success_total')
306 return resp
308 return resp
307 except JSONRPCBaseError:
309 except JSONRPCBaseError:
308 raise
310 raise
309 except Exception:
311 except Exception:
310 log.exception('Unhandled exception occurred on api call: %s', func)
312 log.exception('Unhandled exception occurred on api call: %s', func)
311 exc_info = sys.exc_info()
313 exc_info = sys.exc_info()
312 exc_id, exc_type_name = store_exception(
314 exc_id, exc_type_name = store_exception(
313 id(exc_info), exc_info, prefix='rhodecode-api')
315 id(exc_info), exc_info, prefix='rhodecode-api')
314 error_headers = [('RhodeCode-Exception-Id', str(exc_id)),
316 error_headers = [('RhodeCode-Exception-Id', str(exc_id)),
315 ('RhodeCode-Exception-Type', str(exc_type_name))]
317 ('RhodeCode-Exception-Type', str(exc_type_name))]
316 err_resp = jsonrpc_error(
318 err_resp = jsonrpc_error(
317 request, retid=request.rpc_id, message='Internal server error',
319 request, retid=request.rpc_id, message='Internal server error',
318 headers=error_headers)
320 headers=error_headers)
319 if statsd:
321 if statsd:
320 statsd.incr('rhodecode_api_call_fail_total')
322 statsd.incr('rhodecode_api_call_fail_total')
321 return err_resp
323 return err_resp
322
324
323
325
324 def setup_request(request):
326 def setup_request(request):
325 """
327 """
326 Parse a JSON-RPC request body. It's used inside the predicates method
328 Parse a JSON-RPC request body. It's used inside the predicates method
327 to validate and bootstrap requests for usage in rpc calls.
329 to validate and bootstrap requests for usage in rpc calls.
328
330
329 We need to raise JSONRPCError here if we want to return some errors back to
331 We need to raise JSONRPCError here if we want to return some errors back to
330 user.
332 user.
331 """
333 """
332
334
333 log.debug('Executing setup request: %r', request)
335 log.debug('Executing setup request: %r', request)
334 request.rpc_ip_addr = get_ip_addr(request.environ)
336 request.rpc_ip_addr = get_ip_addr(request.environ)
335 # TODO(marcink): deprecate GET at some point
337 # TODO(marcink): deprecate GET at some point
336 if request.method not in ['POST', 'GET']:
338 if request.method not in ['POST', 'GET']:
337 log.debug('unsupported request method "%s"', request.method)
339 log.debug('unsupported request method "%s"', request.method)
338 raise JSONRPCError(
340 raise JSONRPCError(
339 'unsupported request method "%s". Please use POST' % request.method)
341 'unsupported request method "%s". Please use POST' % request.method)
340
342
341 if 'CONTENT_LENGTH' not in request.environ:
343 if 'CONTENT_LENGTH' not in request.environ:
342 log.debug("No Content-Length")
344 log.debug("No Content-Length")
343 raise JSONRPCError("Empty body, No Content-Length in request")
345 raise JSONRPCError("Empty body, No Content-Length in request")
344
346
345 else:
347 else:
346 length = request.environ['CONTENT_LENGTH']
348 length = request.environ['CONTENT_LENGTH']
347 log.debug('Content-Length: %s', length)
349 log.debug('Content-Length: %s', length)
348
350
349 if length == 0:
351 if length == 0:
350 log.debug("Content-Length is 0")
352 log.debug("Content-Length is 0")
351 raise JSONRPCError("Content-Length is 0")
353 raise JSONRPCError("Content-Length is 0")
352
354
353 raw_body = request.body
355 raw_body = request.body
354 log.debug("Loading JSON body now")
356 log.debug("Loading JSON body now")
355 try:
357 try:
356 json_body = json.loads(raw_body)
358 json_body = json.loads(raw_body)
357 except ValueError as e:
359 except ValueError as e:
358 # catch JSON errors Here
360 # catch JSON errors Here
359 raise JSONRPCError("JSON parse error ERR:%s RAW:%r" % (e, raw_body))
361 raise JSONRPCError("JSON parse error ERR:%s RAW:%r" % (e, raw_body))
360
362
361 request.rpc_id = json_body.get('id')
363 request.rpc_id = json_body.get('id')
362 request.rpc_method = json_body.get('method')
364 request.rpc_method = json_body.get('method')
363
365
364 # check required base parameters
366 # check required base parameters
365 try:
367 try:
366 api_key = json_body.get('api_key')
368 api_key = json_body.get('api_key')
367 if not api_key:
369 if not api_key:
368 api_key = json_body.get('auth_token')
370 api_key = json_body.get('auth_token')
369
371
370 if not api_key:
372 if not api_key:
371 raise KeyError('api_key or auth_token')
373 raise KeyError('api_key or auth_token')
372
374
373 # TODO(marcink): support passing in token in request header
375 # TODO(marcink): support passing in token in request header
374
376
375 request.rpc_api_key = api_key
377 request.rpc_api_key = api_key
376 request.rpc_id = json_body['id']
378 request.rpc_id = json_body['id']
377 request.rpc_method = json_body['method']
379 request.rpc_method = json_body['method']
378 request.rpc_params = json_body['args'] \
380 request.rpc_params = json_body['args'] \
379 if isinstance(json_body['args'], dict) else {}
381 if isinstance(json_body['args'], dict) else {}
380
382
381 log.debug('method: %s, params: %.10240r', request.rpc_method, request.rpc_params)
383 log.debug('method: %s, params: %.10240r', request.rpc_method, request.rpc_params)
382 except KeyError as e:
384 except KeyError as e:
383 raise JSONRPCError('Incorrect JSON data. Missing %s' % e)
385 raise JSONRPCError('Incorrect JSON data. Missing %s' % e)
384
386
385 log.debug('setup complete, now handling method:%s rpcid:%s',
387 log.debug('setup complete, now handling method:%s rpcid:%s',
386 request.rpc_method, request.rpc_id, )
388 request.rpc_method, request.rpc_id, )
387
389
388
390
389 class RoutePredicate(object):
391 class RoutePredicate(object):
390 def __init__(self, val, config):
392 def __init__(self, val, config):
391 self.val = val
393 self.val = val
392
394
393 def text(self):
395 def text(self):
394 return 'jsonrpc route = %s' % self.val
396 return 'jsonrpc route = %s' % self.val
395
397
396 phash = text
398 phash = text
397
399
398 def __call__(self, info, request):
400 def __call__(self, info, request):
399 if self.val:
401 if self.val:
400 # potentially setup and bootstrap our call
402 # potentially setup and bootstrap our call
401 setup_request(request)
403 setup_request(request)
402
404
403 # Always return True so that even if it isn't a valid RPC it
405 # Always return True so that even if it isn't a valid RPC it
404 # will fall through to the underlaying handlers like notfound_view
406 # will fall through to the underlaying handlers like notfound_view
405 return True
407 return True
406
408
407
409
408 class NotFoundPredicate(object):
410 class NotFoundPredicate(object):
409 def __init__(self, val, config):
411 def __init__(self, val, config):
410 self.val = val
412 self.val = val
411 self.methods = config.registry.jsonrpc_methods
413 self.methods = config.registry.jsonrpc_methods
412
414
413 def text(self):
415 def text(self):
414 return 'jsonrpc method not found = {}.'.format(self.val)
416 return 'jsonrpc method not found = {}.'.format(self.val)
415
417
416 phash = text
418 phash = text
417
419
418 def __call__(self, info, request):
420 def __call__(self, info, request):
419 return hasattr(request, 'rpc_method')
421 return hasattr(request, 'rpc_method')
420
422
421
423
422 class MethodPredicate(object):
424 class MethodPredicate(object):
423 def __init__(self, val, config):
425 def __init__(self, val, config):
424 self.method = val
426 self.method = val
425
427
426 def text(self):
428 def text(self):
427 return 'jsonrpc method = %s' % self.method
429 return 'jsonrpc method = %s' % self.method
428
430
429 phash = text
431 phash = text
430
432
431 def __call__(self, context, request):
433 def __call__(self, context, request):
432 # we need to explicitly return False here, so pyramid doesn't try to
434 # we need to explicitly return False here, so pyramid doesn't try to
433 # execute our view directly. We need our main handler to execute things
435 # execute our view directly. We need our main handler to execute things
434 return getattr(request, 'rpc_method') == self.method
436 return getattr(request, 'rpc_method') == self.method
435
437
436
438
437 def add_jsonrpc_method(config, view, **kwargs):
439 def add_jsonrpc_method(config, view, **kwargs):
438 # pop the method name
440 # pop the method name
439 method = kwargs.pop('method', None)
441 method = kwargs.pop('method', None)
440
442
441 if method is None:
443 if method is None:
442 raise ConfigurationError(
444 raise ConfigurationError(
443 'Cannot register a JSON-RPC method without specifying the "method"')
445 'Cannot register a JSON-RPC method without specifying the "method"')
444
446
445 # we define custom predicate, to enable to detect conflicting methods,
447 # we define custom predicate, to enable to detect conflicting methods,
446 # those predicates are kind of "translation" from the decorator variables
448 # those predicates are kind of "translation" from the decorator variables
447 # to internal predicates names
449 # to internal predicates names
448
450
449 kwargs['jsonrpc_method'] = method
451 kwargs['jsonrpc_method'] = method
450
452
451 # register our view into global view store for validation
453 # register our view into global view store for validation
452 config.registry.jsonrpc_methods[method] = view
454 config.registry.jsonrpc_methods[method] = view
453
455
454 # we're using our main request_view handler, here, so each method
456 # we're using our main request_view handler, here, so each method
455 # has a unified handler for itself
457 # has a unified handler for itself
456 config.add_view(request_view, route_name='apiv2', **kwargs)
458 config.add_view(request_view, route_name='apiv2', **kwargs)
457
459
458
460
459 class jsonrpc_method(object):
461 class jsonrpc_method(object):
460 """
462 """
461 decorator that works similar to @add_view_config decorator,
463 decorator that works similar to @add_view_config decorator,
462 but tailored for our JSON RPC
464 but tailored for our JSON RPC
463 """
465 """
464
466
465 venusian = venusian # for testing injection
467 venusian = venusian # for testing injection
466
468
467 def __init__(self, method=None, **kwargs):
469 def __init__(self, method=None, **kwargs):
468 self.method = method
470 self.method = method
469 self.kwargs = kwargs
471 self.kwargs = kwargs
470
472
471 def __call__(self, wrapped):
473 def __call__(self, wrapped):
472 kwargs = self.kwargs.copy()
474 kwargs = self.kwargs.copy()
473 kwargs['method'] = self.method or wrapped.__name__
475 kwargs['method'] = self.method or wrapped.__name__
474 depth = kwargs.pop('_depth', 0)
476 depth = kwargs.pop('_depth', 0)
475
477
476 def callback(context, name, ob):
478 def callback(context, name, ob):
477 config = context.config.with_package(info.module)
479 config = context.config.with_package(info.module)
478 config.add_jsonrpc_method(view=ob, **kwargs)
480 config.add_jsonrpc_method(view=ob, **kwargs)
479
481
480 info = venusian.attach(wrapped, callback, category='pyramid',
482 info = venusian.attach(wrapped, callback, category='pyramid',
481 depth=depth + 1)
483 depth=depth + 1)
482 if info.scope == 'class':
484 if info.scope == 'class':
483 # ensure that attr is set if decorating a class method
485 # ensure that attr is set if decorating a class method
484 kwargs.setdefault('attr', wrapped.__name__)
486 kwargs.setdefault('attr', wrapped.__name__)
485
487
486 kwargs['_info'] = info.codeinfo # fbo action_method
488 kwargs['_info'] = info.codeinfo # fbo action_method
487 return wrapped
489 return wrapped
488
490
489
491
490 class jsonrpc_deprecated_method(object):
492 class jsonrpc_deprecated_method(object):
491 """
493 """
492 Marks method as deprecated, adds log.warning, and inject special key to
494 Marks method as deprecated, adds log.warning, and inject special key to
493 the request variable to mark method as deprecated.
495 the request variable to mark method as deprecated.
494 Also injects special docstring that extract_docs will catch to mark
496 Also injects special docstring that extract_docs will catch to mark
495 method as deprecated.
497 method as deprecated.
496
498
497 :param use_method: specify which method should be used instead of
499 :param use_method: specify which method should be used instead of
498 the decorated one
500 the decorated one
499
501
500 Use like::
502 Use like::
501
503
502 @jsonrpc_method()
504 @jsonrpc_method()
503 @jsonrpc_deprecated_method(use_method='new_func', deprecated_at_version='3.0.0')
505 @jsonrpc_deprecated_method(use_method='new_func', deprecated_at_version='3.0.0')
504 def old_func(request, apiuser, arg1, arg2):
506 def old_func(request, apiuser, arg1, arg2):
505 ...
507 ...
506 """
508 """
507
509
508 def __init__(self, use_method, deprecated_at_version):
510 def __init__(self, use_method, deprecated_at_version):
509 self.use_method = use_method
511 self.use_method = use_method
510 self.deprecated_at_version = deprecated_at_version
512 self.deprecated_at_version = deprecated_at_version
511 self.deprecated_msg = ''
513 self.deprecated_msg = ''
512
514
513 def __call__(self, func):
515 def __call__(self, func):
514 self.deprecated_msg = 'Please use method `{method}` instead.'.format(
516 self.deprecated_msg = 'Please use method `{method}` instead.'.format(
515 method=self.use_method)
517 method=self.use_method)
516
518
517 docstring = """\n
519 docstring = """\n
518 .. deprecated:: {version}
520 .. deprecated:: {version}
519
521
520 {deprecation_message}
522 {deprecation_message}
521
523
522 {original_docstring}
524 {original_docstring}
523 """
525 """
524 func.__doc__ = docstring.format(
526 func.__doc__ = docstring.format(
525 version=self.deprecated_at_version,
527 version=self.deprecated_at_version,
526 deprecation_message=self.deprecated_msg,
528 deprecation_message=self.deprecated_msg,
527 original_docstring=func.__doc__)
529 original_docstring=func.__doc__)
528 return decorator.decorator(self.__wrapper, func)
530 return decorator.decorator(self.__wrapper, func)
529
531
530 def __wrapper(self, func, *fargs, **fkwargs):
532 def __wrapper(self, func, *fargs, **fkwargs):
531 log.warning('DEPRECATED API CALL on function %s, please '
533 log.warning('DEPRECATED API CALL on function %s, please '
532 'use `%s` instead', func, self.use_method)
534 'use `%s` instead', func, self.use_method)
533 # alter function docstring to mark as deprecated, this is picked up
535 # alter function docstring to mark as deprecated, this is picked up
534 # via fabric file that generates API DOC.
536 # via fabric file that generates API DOC.
535 result = func(*fargs, **fkwargs)
537 result = func(*fargs, **fkwargs)
536
538
537 request = fargs[0]
539 request = fargs[0]
538 request.rpc_deprecation = 'DEPRECATED METHOD ' + self.deprecated_msg
540 request.rpc_deprecation = 'DEPRECATED METHOD ' + self.deprecated_msg
539 return result
541 return result
540
542
541
543
542 def add_api_methods(config):
544 def add_api_methods(config):
543 from rhodecode.api.views import (
545 from rhodecode.api.views import (
544 deprecated_api, gist_api, pull_request_api, repo_api, repo_group_api,
546 deprecated_api, gist_api, pull_request_api, repo_api, repo_group_api,
545 server_api, search_api, testing_api, user_api, user_group_api)
547 server_api, search_api, testing_api, user_api, user_group_api)
546
548
547 config.scan('rhodecode.api.views')
549 config.scan('rhodecode.api.views')
548
550
549
551
550 def includeme(config):
552 def includeme(config):
551 plugin_module = 'rhodecode.api'
553 plugin_module = 'rhodecode.api'
552 plugin_settings = get_plugin_settings(
554 plugin_settings = get_plugin_settings(
553 plugin_module, config.registry.settings)
555 plugin_module, config.registry.settings)
554
556
555 if not hasattr(config.registry, 'jsonrpc_methods'):
557 if not hasattr(config.registry, 'jsonrpc_methods'):
556 config.registry.jsonrpc_methods = OrderedDict()
558 config.registry.jsonrpc_methods = OrderedDict()
557
559
558 # match filter by given method only
560 # match filter by given method only
559 config.add_view_predicate('jsonrpc_method', MethodPredicate)
561 config.add_view_predicate('jsonrpc_method', MethodPredicate)
560 config.add_view_predicate('jsonrpc_method_not_found', NotFoundPredicate)
562 config.add_view_predicate('jsonrpc_method_not_found', NotFoundPredicate)
561
563
562 config.add_renderer(DEFAULT_RENDERER, ExtJsonRenderer(
564 config.add_renderer(DEFAULT_RENDERER, ExtJsonRenderer(
563 serializer=json.dumps, indent=4))
565 serializer=json.dumps, indent=4))
564 config.add_directive('add_jsonrpc_method', add_jsonrpc_method)
566 config.add_directive('add_jsonrpc_method', add_jsonrpc_method)
565
567
566 config.add_route_predicate(
568 config.add_route_predicate(
567 'jsonrpc_call', RoutePredicate)
569 'jsonrpc_call', RoutePredicate)
568
570
569 config.add_route(
571 config.add_route(
570 'apiv2', plugin_settings.get('url', DEFAULT_URL), jsonrpc_call=True)
572 'apiv2', plugin_settings.get('url', DEFAULT_URL), jsonrpc_call=True)
571
573
572 # register some exception handling view
574 # register some exception handling view
573 config.add_view(exception_view, context=JSONRPCBaseError)
575 config.add_view(exception_view, context=JSONRPCBaseError)
574 config.add_notfound_view(exception_view, jsonrpc_method_not_found=True)
576 config.add_notfound_view(exception_view, jsonrpc_method_not_found=True)
575
577
576 add_api_methods(config)
578 add_api_methods(config)
@@ -1,797 +1,800 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2020 RhodeCode GmbH
3 # Copyright (C) 2010-2020 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 import os
21 import os
22 import sys
22 import sys
23 import logging
23 import logging
24 import collections
24 import collections
25 import tempfile
25 import tempfile
26 import time
26 import time
27
27
28 from paste.gzipper import make_gzip_middleware
28 from paste.gzipper import make_gzip_middleware
29 import pyramid.events
29 import pyramid.events
30 from pyramid.wsgi import wsgiapp
30 from pyramid.wsgi import wsgiapp
31 from pyramid.authorization import ACLAuthorizationPolicy
31 from pyramid.authorization import ACLAuthorizationPolicy
32 from pyramid.config import Configurator
32 from pyramid.config import Configurator
33 from pyramid.settings import asbool, aslist
33 from pyramid.settings import asbool, aslist
34 from pyramid.httpexceptions import (
34 from pyramid.httpexceptions import (
35 HTTPException, HTTPError, HTTPInternalServerError, HTTPFound, HTTPNotFound)
35 HTTPException, HTTPError, HTTPInternalServerError, HTTPFound, HTTPNotFound)
36 from pyramid.renderers import render_to_response
36 from pyramid.renderers import render_to_response
37
37
38 from rhodecode.model import meta
38 from rhodecode.model import meta
39 from rhodecode.config import patches
39 from rhodecode.config import patches
40 from rhodecode.config import utils as config_utils
40 from rhodecode.config import utils as config_utils
41 from rhodecode.config.environment import load_pyramid_environment
41 from rhodecode.config.environment import load_pyramid_environment
42
42
43 import rhodecode.events
43 import rhodecode.events
44 from rhodecode.lib.middleware.vcs import VCSMiddleware
44 from rhodecode.lib.middleware.vcs import VCSMiddleware
45 from rhodecode.lib.request import Request
45 from rhodecode.lib.request import Request
46 from rhodecode.lib.vcs import VCSCommunicationError
46 from rhodecode.lib.vcs import VCSCommunicationError
47 from rhodecode.lib.exceptions import VCSServerUnavailable
47 from rhodecode.lib.exceptions import VCSServerUnavailable
48 from rhodecode.lib.middleware.appenlight import wrap_in_appenlight_if_enabled
48 from rhodecode.lib.middleware.appenlight import wrap_in_appenlight_if_enabled
49 from rhodecode.lib.middleware.https_fixup import HttpsFixup
49 from rhodecode.lib.middleware.https_fixup import HttpsFixup
50 from rhodecode.lib.plugins.utils import register_rhodecode_plugin
50 from rhodecode.lib.plugins.utils import register_rhodecode_plugin
51 from rhodecode.lib.utils2 import aslist as rhodecode_aslist, AttributeDict
51 from rhodecode.lib.utils2 import aslist as rhodecode_aslist, AttributeDict
52 from rhodecode.lib.exc_tracking import store_exception
52 from rhodecode.lib.exc_tracking import store_exception
53 from rhodecode.subscribers import (
53 from rhodecode.subscribers import (
54 scan_repositories_if_enabled, write_js_routes_if_enabled,
54 scan_repositories_if_enabled, write_js_routes_if_enabled,
55 write_metadata_if_needed, write_usage_data)
55 write_metadata_if_needed, write_usage_data)
56 from rhodecode.lib.statsd_client import StatsdClient
56 from rhodecode.lib.statsd_client import StatsdClient
57
57
58 log = logging.getLogger(__name__)
58 log = logging.getLogger(__name__)
59
59
60
60
61 def is_http_error(response):
61 def is_http_error(response):
62 # error which should have traceback
62 # error which should have traceback
63 return response.status_code > 499
63 return response.status_code > 499
64
64
65
65
66 def should_load_all():
66 def should_load_all():
67 """
67 """
68 Returns if all application components should be loaded. In some cases it's
68 Returns if all application components should be loaded. In some cases it's
69 desired to skip apps loading for faster shell script execution
69 desired to skip apps loading for faster shell script execution
70 """
70 """
71 ssh_cmd = os.environ.get('RC_CMD_SSH_WRAPPER')
71 ssh_cmd = os.environ.get('RC_CMD_SSH_WRAPPER')
72 if ssh_cmd:
72 if ssh_cmd:
73 return False
73 return False
74
74
75 return True
75 return True
76
76
77
77
78 def make_pyramid_app(global_config, **settings):
78 def make_pyramid_app(global_config, **settings):
79 """
79 """
80 Constructs the WSGI application based on Pyramid.
80 Constructs the WSGI application based on Pyramid.
81
81
82 Specials:
82 Specials:
83
83
84 * The application can also be integrated like a plugin via the call to
84 * The application can also be integrated like a plugin via the call to
85 `includeme`. This is accompanied with the other utility functions which
85 `includeme`. This is accompanied with the other utility functions which
86 are called. Changing this should be done with great care to not break
86 are called. Changing this should be done with great care to not break
87 cases when these fragments are assembled from another place.
87 cases when these fragments are assembled from another place.
88
88
89 """
89 """
90
90
91 # Allows to use format style "{ENV_NAME}" placeholders in the configuration. It
91 # Allows to use format style "{ENV_NAME}" placeholders in the configuration. It
92 # will be replaced by the value of the environment variable "NAME" in this case.
92 # will be replaced by the value of the environment variable "NAME" in this case.
93 start_time = time.time()
93 start_time = time.time()
94 log.info('Pyramid app config starting')
94 log.info('Pyramid app config starting')
95
95
96 # init and bootstrap StatsdClient
96 # init and bootstrap StatsdClient
97 StatsdClient.setup(settings)
97 StatsdClient.setup(settings)
98
98
99 debug = asbool(global_config.get('debug'))
99 debug = asbool(global_config.get('debug'))
100 if debug:
100 if debug:
101 enable_debug()
101 enable_debug()
102
102
103 environ = {'ENV_{}'.format(key): value for key, value in os.environ.items()}
103 environ = {'ENV_{}'.format(key): value for key, value in os.environ.items()}
104
104
105 global_config = _substitute_values(global_config, environ)
105 global_config = _substitute_values(global_config, environ)
106 settings = _substitute_values(settings, environ)
106 settings = _substitute_values(settings, environ)
107
107
108 sanitize_settings_and_apply_defaults(global_config, settings)
108 sanitize_settings_and_apply_defaults(global_config, settings)
109
109
110 config = Configurator(settings=settings)
110 config = Configurator(settings=settings)
111 # Init our statsd at very start
111 # Init our statsd at very start
112 config.registry.statsd = StatsdClient.statsd
112 config.registry.statsd = StatsdClient.statsd
113
113
114 # Apply compatibility patches
114 # Apply compatibility patches
115 patches.inspect_getargspec()
115 patches.inspect_getargspec()
116
116
117 load_pyramid_environment(global_config, settings)
117 load_pyramid_environment(global_config, settings)
118
118
119 # Static file view comes first
119 # Static file view comes first
120 includeme_first(config)
120 includeme_first(config)
121
121
122 includeme(config)
122 includeme(config)
123
123
124 pyramid_app = config.make_wsgi_app()
124 pyramid_app = config.make_wsgi_app()
125 pyramid_app = wrap_app_in_wsgi_middlewares(pyramid_app, config)
125 pyramid_app = wrap_app_in_wsgi_middlewares(pyramid_app, config)
126 pyramid_app.config = config
126 pyramid_app.config = config
127
127
128 config.configure_celery(global_config['__file__'])
128 config.configure_celery(global_config['__file__'])
129
129
130 # creating the app uses a connection - return it after we are done
130 # creating the app uses a connection - return it after we are done
131 meta.Session.remove()
131 meta.Session.remove()
132 statsd = StatsdClient.statsd
132 statsd = StatsdClient.statsd
133
133
134 total_time = time.time() - start_time
134 total_time = time.time() - start_time
135 log.info('Pyramid app `%s` created and configured in %.2fs',
135 log.info('Pyramid app `%s` created and configured in %.2fs',
136 pyramid_app.func_name, total_time)
136 pyramid_app.func_name, total_time)
137 if statsd:
137 if statsd:
138 elapsed_time_ms = round(1000.0 * total_time) # use ms only rounded time
138 elapsed_time_ms = round(1000.0 * total_time) # use ms only rounded time
139 statsd.timing('rhodecode_app_bootstrap_timing', elapsed_time_ms, tags=[
139 statsd.timing('rhodecode_app_bootstrap_timing', elapsed_time_ms, tags=[
140 "pyramid_app:{}".format(pyramid_app.func_name)
140 "pyramid_app:{}".format(pyramid_app.func_name)
141 ], use_decimals=False)
141 ], use_decimals=False)
142 return pyramid_app
142 return pyramid_app
143
143
144
144
145 def not_found_view(request):
145 def not_found_view(request):
146 """
146 """
147 This creates the view which should be registered as not-found-view to
147 This creates the view which should be registered as not-found-view to
148 pyramid.
148 pyramid.
149 """
149 """
150
150
151 if not getattr(request, 'vcs_call', None):
151 if not getattr(request, 'vcs_call', None):
152 # handle like regular case with our error_handler
152 # handle like regular case with our error_handler
153 return error_handler(HTTPNotFound(), request)
153 return error_handler(HTTPNotFound(), request)
154
154
155 # handle not found view as a vcs call
155 # handle not found view as a vcs call
156 settings = request.registry.settings
156 settings = request.registry.settings
157 ae_client = getattr(request, 'ae_client', None)
157 ae_client = getattr(request, 'ae_client', None)
158 vcs_app = VCSMiddleware(
158 vcs_app = VCSMiddleware(
159 HTTPNotFound(), request.registry, settings,
159 HTTPNotFound(), request.registry, settings,
160 appenlight_client=ae_client)
160 appenlight_client=ae_client)
161
161
162 return wsgiapp(vcs_app)(None, request)
162 return wsgiapp(vcs_app)(None, request)
163
163
164
164
165 def error_handler(exception, request):
165 def error_handler(exception, request):
166 import rhodecode
166 import rhodecode
167 from rhodecode.lib import helpers
167 from rhodecode.lib import helpers
168 from rhodecode.lib.utils2 import str2bool
168 from rhodecode.lib.utils2 import str2bool
169
169
170 rhodecode_title = rhodecode.CONFIG.get('rhodecode_title') or 'RhodeCode'
170 rhodecode_title = rhodecode.CONFIG.get('rhodecode_title') or 'RhodeCode'
171
171
172 base_response = HTTPInternalServerError()
172 base_response = HTTPInternalServerError()
173 # prefer original exception for the response since it may have headers set
173 # prefer original exception for the response since it may have headers set
174 if isinstance(exception, HTTPException):
174 if isinstance(exception, HTTPException):
175 base_response = exception
175 base_response = exception
176 elif isinstance(exception, VCSCommunicationError):
176 elif isinstance(exception, VCSCommunicationError):
177 base_response = VCSServerUnavailable()
177 base_response = VCSServerUnavailable()
178
178
179 if is_http_error(base_response):
179 if is_http_error(base_response):
180 log.exception(
180 log.exception(
181 'error occurred handling this request for path: %s', request.path)
181 'error occurred handling this request for path: %s', request.path)
182
182
183 statsd = request.registry.statsd
184 if statsd and base_response.status_code > 499:
185 statsd.incr('rhodecode_exception_total',
186 tags=["exc_source:web", "type:{}".format(base_response.status_code)])
187
188 error_explanation = base_response.explanation or str(base_response)
183 error_explanation = base_response.explanation or str(base_response)
189 if base_response.status_code == 404:
184 if base_response.status_code == 404:
190 error_explanation += " Optionally you don't have permission to access this page."
185 error_explanation += " Optionally you don't have permission to access this page."
191 c = AttributeDict()
186 c = AttributeDict()
192 c.error_message = base_response.status
187 c.error_message = base_response.status
193 c.error_explanation = error_explanation
188 c.error_explanation = error_explanation
194 c.visual = AttributeDict()
189 c.visual = AttributeDict()
195
190
196 c.visual.rhodecode_support_url = (
191 c.visual.rhodecode_support_url = (
197 request.registry.settings.get('rhodecode_support_url') or
192 request.registry.settings.get('rhodecode_support_url') or
198 request.route_url('rhodecode_support')
193 request.route_url('rhodecode_support')
199 )
194 )
200 c.redirect_time = 0
195 c.redirect_time = 0
201 c.rhodecode_name = rhodecode_title
196 c.rhodecode_name = rhodecode_title
202 if not c.rhodecode_name:
197 if not c.rhodecode_name:
203 c.rhodecode_name = 'Rhodecode'
198 c.rhodecode_name = 'Rhodecode'
204
199
205 c.causes = []
200 c.causes = []
206 if is_http_error(base_response):
201 if is_http_error(base_response):
207 c.causes.append('Server is overloaded.')
202 c.causes.append('Server is overloaded.')
208 c.causes.append('Server database connection is lost.')
203 c.causes.append('Server database connection is lost.')
209 c.causes.append('Server expected unhandled error.')
204 c.causes.append('Server expected unhandled error.')
210
205
211 if hasattr(base_response, 'causes'):
206 if hasattr(base_response, 'causes'):
212 c.causes = base_response.causes
207 c.causes = base_response.causes
213
208
214 c.messages = helpers.flash.pop_messages(request=request)
209 c.messages = helpers.flash.pop_messages(request=request)
215
210
216 exc_info = sys.exc_info()
211 exc_info = sys.exc_info()
217 c.exception_id = id(exc_info)
212 c.exception_id = id(exc_info)
218 c.show_exception_id = isinstance(base_response, VCSServerUnavailable) \
213 c.show_exception_id = isinstance(base_response, VCSServerUnavailable) \
219 or base_response.status_code > 499
214 or base_response.status_code > 499
220 c.exception_id_url = request.route_url(
215 c.exception_id_url = request.route_url(
221 'admin_settings_exception_tracker_show', exception_id=c.exception_id)
216 'admin_settings_exception_tracker_show', exception_id=c.exception_id)
222
217
223 if c.show_exception_id:
218 if c.show_exception_id:
224 store_exception(c.exception_id, exc_info)
219 store_exception(c.exception_id, exc_info)
225 c.exception_debug = str2bool(rhodecode.CONFIG.get('debug'))
220 c.exception_debug = str2bool(rhodecode.CONFIG.get('debug'))
226 c.exception_config_ini = rhodecode.CONFIG.get('__file__')
221 c.exception_config_ini = rhodecode.CONFIG.get('__file__')
227
222
228 response = render_to_response(
223 response = render_to_response(
229 '/errors/error_document.mako', {'c': c, 'h': helpers}, request=request,
224 '/errors/error_document.mako', {'c': c, 'h': helpers}, request=request,
230 response=base_response)
225 response=base_response)
231
226
227 statsd = request.registry.statsd
228 if statsd and base_response.status_code > 499:
229 exc_type = "{}.{}".format(exception.__class__.__module__, exception.__class__.__name__)
230 statsd.incr('rhodecode_exception_total',
231 tags=["exc_source:web",
232 "http_code:{}".format(base_response.status_code),
233 "type:{}".format(exc_type)])
234
232 return response
235 return response
233
236
234
237
235 def includeme_first(config):
238 def includeme_first(config):
236 # redirect automatic browser favicon.ico requests to correct place
239 # redirect automatic browser favicon.ico requests to correct place
237 def favicon_redirect(context, request):
240 def favicon_redirect(context, request):
238 return HTTPFound(
241 return HTTPFound(
239 request.static_path('rhodecode:public/images/favicon.ico'))
242 request.static_path('rhodecode:public/images/favicon.ico'))
240
243
241 config.add_view(favicon_redirect, route_name='favicon')
244 config.add_view(favicon_redirect, route_name='favicon')
242 config.add_route('favicon', '/favicon.ico')
245 config.add_route('favicon', '/favicon.ico')
243
246
244 def robots_redirect(context, request):
247 def robots_redirect(context, request):
245 return HTTPFound(
248 return HTTPFound(
246 request.static_path('rhodecode:public/robots.txt'))
249 request.static_path('rhodecode:public/robots.txt'))
247
250
248 config.add_view(robots_redirect, route_name='robots')
251 config.add_view(robots_redirect, route_name='robots')
249 config.add_route('robots', '/robots.txt')
252 config.add_route('robots', '/robots.txt')
250
253
251 config.add_static_view(
254 config.add_static_view(
252 '_static/deform', 'deform:static')
255 '_static/deform', 'deform:static')
253 config.add_static_view(
256 config.add_static_view(
254 '_static/rhodecode', path='rhodecode:public', cache_max_age=3600 * 24)
257 '_static/rhodecode', path='rhodecode:public', cache_max_age=3600 * 24)
255
258
256
259
257 def includeme(config, auth_resources=None):
260 def includeme(config, auth_resources=None):
258 from rhodecode.lib.celerylib.loader import configure_celery
261 from rhodecode.lib.celerylib.loader import configure_celery
259 log.debug('Initializing main includeme from %s', os.path.basename(__file__))
262 log.debug('Initializing main includeme from %s', os.path.basename(__file__))
260 settings = config.registry.settings
263 settings = config.registry.settings
261 config.set_request_factory(Request)
264 config.set_request_factory(Request)
262
265
263 # plugin information
266 # plugin information
264 config.registry.rhodecode_plugins = collections.OrderedDict()
267 config.registry.rhodecode_plugins = collections.OrderedDict()
265
268
266 config.add_directive(
269 config.add_directive(
267 'register_rhodecode_plugin', register_rhodecode_plugin)
270 'register_rhodecode_plugin', register_rhodecode_plugin)
268
271
269 config.add_directive('configure_celery', configure_celery)
272 config.add_directive('configure_celery', configure_celery)
270
273
271 if asbool(settings.get('appenlight', 'false')):
274 if asbool(settings.get('appenlight', 'false')):
272 config.include('appenlight_client.ext.pyramid_tween')
275 config.include('appenlight_client.ext.pyramid_tween')
273
276
274 load_all = should_load_all()
277 load_all = should_load_all()
275
278
276 # Includes which are required. The application would fail without them.
279 # Includes which are required. The application would fail without them.
277 config.include('pyramid_mako')
280 config.include('pyramid_mako')
278 config.include('rhodecode.lib.rc_beaker')
281 config.include('rhodecode.lib.rc_beaker')
279 config.include('rhodecode.lib.rc_cache')
282 config.include('rhodecode.lib.rc_cache')
280 config.include('rhodecode.apps._base.navigation')
283 config.include('rhodecode.apps._base.navigation')
281 config.include('rhodecode.apps._base.subscribers')
284 config.include('rhodecode.apps._base.subscribers')
282 config.include('rhodecode.tweens')
285 config.include('rhodecode.tweens')
283 config.include('rhodecode.authentication')
286 config.include('rhodecode.authentication')
284
287
285 if load_all:
288 if load_all:
286 ce_auth_resources = [
289 ce_auth_resources = [
287 'rhodecode.authentication.plugins.auth_crowd',
290 'rhodecode.authentication.plugins.auth_crowd',
288 'rhodecode.authentication.plugins.auth_headers',
291 'rhodecode.authentication.plugins.auth_headers',
289 'rhodecode.authentication.plugins.auth_jasig_cas',
292 'rhodecode.authentication.plugins.auth_jasig_cas',
290 'rhodecode.authentication.plugins.auth_ldap',
293 'rhodecode.authentication.plugins.auth_ldap',
291 'rhodecode.authentication.plugins.auth_pam',
294 'rhodecode.authentication.plugins.auth_pam',
292 'rhodecode.authentication.plugins.auth_rhodecode',
295 'rhodecode.authentication.plugins.auth_rhodecode',
293 'rhodecode.authentication.plugins.auth_token',
296 'rhodecode.authentication.plugins.auth_token',
294 ]
297 ]
295
298
296 # load CE authentication plugins
299 # load CE authentication plugins
297
300
298 if auth_resources:
301 if auth_resources:
299 ce_auth_resources.extend(auth_resources)
302 ce_auth_resources.extend(auth_resources)
300
303
301 for resource in ce_auth_resources:
304 for resource in ce_auth_resources:
302 config.include(resource)
305 config.include(resource)
303
306
304 # Auto discover authentication plugins and include their configuration.
307 # Auto discover authentication plugins and include their configuration.
305 if asbool(settings.get('auth_plugin.import_legacy_plugins', 'true')):
308 if asbool(settings.get('auth_plugin.import_legacy_plugins', 'true')):
306 from rhodecode.authentication import discover_legacy_plugins
309 from rhodecode.authentication import discover_legacy_plugins
307 discover_legacy_plugins(config)
310 discover_legacy_plugins(config)
308
311
309 # apps
312 # apps
310 if load_all:
313 if load_all:
311 config.include('rhodecode.api')
314 config.include('rhodecode.api')
312 config.include('rhodecode.apps._base')
315 config.include('rhodecode.apps._base')
313 config.include('rhodecode.apps.hovercards')
316 config.include('rhodecode.apps.hovercards')
314 config.include('rhodecode.apps.ops')
317 config.include('rhodecode.apps.ops')
315 config.include('rhodecode.apps.channelstream')
318 config.include('rhodecode.apps.channelstream')
316 config.include('rhodecode.apps.file_store')
319 config.include('rhodecode.apps.file_store')
317 config.include('rhodecode.apps.admin')
320 config.include('rhodecode.apps.admin')
318 config.include('rhodecode.apps.login')
321 config.include('rhodecode.apps.login')
319 config.include('rhodecode.apps.home')
322 config.include('rhodecode.apps.home')
320 config.include('rhodecode.apps.journal')
323 config.include('rhodecode.apps.journal')
321
324
322 config.include('rhodecode.apps.repository')
325 config.include('rhodecode.apps.repository')
323 config.include('rhodecode.apps.repo_group')
326 config.include('rhodecode.apps.repo_group')
324 config.include('rhodecode.apps.user_group')
327 config.include('rhodecode.apps.user_group')
325 config.include('rhodecode.apps.search')
328 config.include('rhodecode.apps.search')
326 config.include('rhodecode.apps.user_profile')
329 config.include('rhodecode.apps.user_profile')
327 config.include('rhodecode.apps.user_group_profile')
330 config.include('rhodecode.apps.user_group_profile')
328 config.include('rhodecode.apps.my_account')
331 config.include('rhodecode.apps.my_account')
329 config.include('rhodecode.apps.gist')
332 config.include('rhodecode.apps.gist')
330
333
331 config.include('rhodecode.apps.svn_support')
334 config.include('rhodecode.apps.svn_support')
332 config.include('rhodecode.apps.ssh_support')
335 config.include('rhodecode.apps.ssh_support')
333 config.include('rhodecode.apps.debug_style')
336 config.include('rhodecode.apps.debug_style')
334
337
335 if load_all:
338 if load_all:
336 config.include('rhodecode.integrations')
339 config.include('rhodecode.integrations')
337
340
338 config.add_route('rhodecode_support', 'https://rhodecode.com/help/', static=True)
341 config.add_route('rhodecode_support', 'https://rhodecode.com/help/', static=True)
339 config.add_translation_dirs('rhodecode:i18n/')
342 config.add_translation_dirs('rhodecode:i18n/')
340 settings['default_locale_name'] = settings.get('lang', 'en')
343 settings['default_locale_name'] = settings.get('lang', 'en')
341
344
342 # Add subscribers.
345 # Add subscribers.
343 if load_all:
346 if load_all:
344 config.add_subscriber(scan_repositories_if_enabled,
347 config.add_subscriber(scan_repositories_if_enabled,
345 pyramid.events.ApplicationCreated)
348 pyramid.events.ApplicationCreated)
346 config.add_subscriber(write_metadata_if_needed,
349 config.add_subscriber(write_metadata_if_needed,
347 pyramid.events.ApplicationCreated)
350 pyramid.events.ApplicationCreated)
348 config.add_subscriber(write_usage_data,
351 config.add_subscriber(write_usage_data,
349 pyramid.events.ApplicationCreated)
352 pyramid.events.ApplicationCreated)
350 config.add_subscriber(write_js_routes_if_enabled,
353 config.add_subscriber(write_js_routes_if_enabled,
351 pyramid.events.ApplicationCreated)
354 pyramid.events.ApplicationCreated)
352
355
353 # request custom methods
356 # request custom methods
354 config.add_request_method(
357 config.add_request_method(
355 'rhodecode.lib.partial_renderer.get_partial_renderer',
358 'rhodecode.lib.partial_renderer.get_partial_renderer',
356 'get_partial_renderer')
359 'get_partial_renderer')
357
360
358 config.add_request_method(
361 config.add_request_method(
359 'rhodecode.lib.request_counter.get_request_counter',
362 'rhodecode.lib.request_counter.get_request_counter',
360 'request_count')
363 'request_count')
361
364
362 # Set the authorization policy.
365 # Set the authorization policy.
363 authz_policy = ACLAuthorizationPolicy()
366 authz_policy = ACLAuthorizationPolicy()
364 config.set_authorization_policy(authz_policy)
367 config.set_authorization_policy(authz_policy)
365
368
366 # Set the default renderer for HTML templates to mako.
369 # Set the default renderer for HTML templates to mako.
367 config.add_mako_renderer('.html')
370 config.add_mako_renderer('.html')
368
371
369 config.add_renderer(
372 config.add_renderer(
370 name='json_ext',
373 name='json_ext',
371 factory='rhodecode.lib.ext_json_renderer.pyramid_ext_json')
374 factory='rhodecode.lib.ext_json_renderer.pyramid_ext_json')
372
375
373 config.add_renderer(
376 config.add_renderer(
374 name='string_html',
377 name='string_html',
375 factory='rhodecode.lib.string_renderer.html')
378 factory='rhodecode.lib.string_renderer.html')
376
379
377 # include RhodeCode plugins
380 # include RhodeCode plugins
378 includes = aslist(settings.get('rhodecode.includes', []))
381 includes = aslist(settings.get('rhodecode.includes', []))
379 for inc in includes:
382 for inc in includes:
380 config.include(inc)
383 config.include(inc)
381
384
382 # custom not found view, if our pyramid app doesn't know how to handle
385 # custom not found view, if our pyramid app doesn't know how to handle
383 # the request pass it to potential VCS handling ap
386 # the request pass it to potential VCS handling ap
384 config.add_notfound_view(not_found_view)
387 config.add_notfound_view(not_found_view)
385 if not settings.get('debugtoolbar.enabled', False):
388 if not settings.get('debugtoolbar.enabled', False):
386 # disabled debugtoolbar handle all exceptions via the error_handlers
389 # disabled debugtoolbar handle all exceptions via the error_handlers
387 config.add_view(error_handler, context=Exception)
390 config.add_view(error_handler, context=Exception)
388
391
389 # all errors including 403/404/50X
392 # all errors including 403/404/50X
390 config.add_view(error_handler, context=HTTPError)
393 config.add_view(error_handler, context=HTTPError)
391
394
392
395
393 def wrap_app_in_wsgi_middlewares(pyramid_app, config):
396 def wrap_app_in_wsgi_middlewares(pyramid_app, config):
394 """
397 """
395 Apply outer WSGI middlewares around the application.
398 Apply outer WSGI middlewares around the application.
396 """
399 """
397 registry = config.registry
400 registry = config.registry
398 settings = registry.settings
401 settings = registry.settings
399
402
400 # enable https redirects based on HTTP_X_URL_SCHEME set by proxy
403 # enable https redirects based on HTTP_X_URL_SCHEME set by proxy
401 pyramid_app = HttpsFixup(pyramid_app, settings)
404 pyramid_app = HttpsFixup(pyramid_app, settings)
402
405
403 pyramid_app, _ae_client = wrap_in_appenlight_if_enabled(
406 pyramid_app, _ae_client = wrap_in_appenlight_if_enabled(
404 pyramid_app, settings)
407 pyramid_app, settings)
405 registry.ae_client = _ae_client
408 registry.ae_client = _ae_client
406
409
407 if settings['gzip_responses']:
410 if settings['gzip_responses']:
408 pyramid_app = make_gzip_middleware(
411 pyramid_app = make_gzip_middleware(
409 pyramid_app, settings, compress_level=1)
412 pyramid_app, settings, compress_level=1)
410
413
411 # this should be the outer most middleware in the wsgi stack since
414 # this should be the outer most middleware in the wsgi stack since
412 # middleware like Routes make database calls
415 # middleware like Routes make database calls
413 def pyramid_app_with_cleanup(environ, start_response):
416 def pyramid_app_with_cleanup(environ, start_response):
414 try:
417 try:
415 return pyramid_app(environ, start_response)
418 return pyramid_app(environ, start_response)
416 finally:
419 finally:
417 # Dispose current database session and rollback uncommitted
420 # Dispose current database session and rollback uncommitted
418 # transactions.
421 # transactions.
419 meta.Session.remove()
422 meta.Session.remove()
420
423
421 # In a single threaded mode server, on non sqlite db we should have
424 # In a single threaded mode server, on non sqlite db we should have
422 # '0 Current Checked out connections' at the end of a request,
425 # '0 Current Checked out connections' at the end of a request,
423 # if not, then something, somewhere is leaving a connection open
426 # if not, then something, somewhere is leaving a connection open
424 pool = meta.Base.metadata.bind.engine.pool
427 pool = meta.Base.metadata.bind.engine.pool
425 log.debug('sa pool status: %s', pool.status())
428 log.debug('sa pool status: %s', pool.status())
426 log.debug('Request processing finalized')
429 log.debug('Request processing finalized')
427
430
428 return pyramid_app_with_cleanup
431 return pyramid_app_with_cleanup
429
432
430
433
431 def sanitize_settings_and_apply_defaults(global_config, settings):
434 def sanitize_settings_and_apply_defaults(global_config, settings):
432 """
435 """
433 Applies settings defaults and does all type conversion.
436 Applies settings defaults and does all type conversion.
434
437
435 We would move all settings parsing and preparation into this place, so that
438 We would move all settings parsing and preparation into this place, so that
436 we have only one place left which deals with this part. The remaining parts
439 we have only one place left which deals with this part. The remaining parts
437 of the application would start to rely fully on well prepared settings.
440 of the application would start to rely fully on well prepared settings.
438
441
439 This piece would later be split up per topic to avoid a big fat monster
442 This piece would later be split up per topic to avoid a big fat monster
440 function.
443 function.
441 """
444 """
442
445
443 settings.setdefault('rhodecode.edition', 'Community Edition')
446 settings.setdefault('rhodecode.edition', 'Community Edition')
444 settings.setdefault('rhodecode.edition_id', 'CE')
447 settings.setdefault('rhodecode.edition_id', 'CE')
445
448
446 if 'mako.default_filters' not in settings:
449 if 'mako.default_filters' not in settings:
447 # set custom default filters if we don't have it defined
450 # set custom default filters if we don't have it defined
448 settings['mako.imports'] = 'from rhodecode.lib.base import h_filter'
451 settings['mako.imports'] = 'from rhodecode.lib.base import h_filter'
449 settings['mako.default_filters'] = 'h_filter'
452 settings['mako.default_filters'] = 'h_filter'
450
453
451 if 'mako.directories' not in settings:
454 if 'mako.directories' not in settings:
452 mako_directories = settings.setdefault('mako.directories', [
455 mako_directories = settings.setdefault('mako.directories', [
453 # Base templates of the original application
456 # Base templates of the original application
454 'rhodecode:templates',
457 'rhodecode:templates',
455 ])
458 ])
456 log.debug(
459 log.debug(
457 "Using the following Mako template directories: %s",
460 "Using the following Mako template directories: %s",
458 mako_directories)
461 mako_directories)
459
462
460 # NOTE(marcink): fix redis requirement for schema of connection since 3.X
463 # NOTE(marcink): fix redis requirement for schema of connection since 3.X
461 if 'beaker.session.type' in settings and settings['beaker.session.type'] == 'ext:redis':
464 if 'beaker.session.type' in settings and settings['beaker.session.type'] == 'ext:redis':
462 raw_url = settings['beaker.session.url']
465 raw_url = settings['beaker.session.url']
463 if not raw_url.startswith(('redis://', 'rediss://', 'unix://')):
466 if not raw_url.startswith(('redis://', 'rediss://', 'unix://')):
464 settings['beaker.session.url'] = 'redis://' + raw_url
467 settings['beaker.session.url'] = 'redis://' + raw_url
465
468
466 # Default includes, possible to change as a user
469 # Default includes, possible to change as a user
467 pyramid_includes = settings.setdefault('pyramid.includes', [])
470 pyramid_includes = settings.setdefault('pyramid.includes', [])
468 log.debug(
471 log.debug(
469 "Using the following pyramid.includes: %s",
472 "Using the following pyramid.includes: %s",
470 pyramid_includes)
473 pyramid_includes)
471
474
472 # TODO: johbo: Re-think this, usually the call to config.include
475 # TODO: johbo: Re-think this, usually the call to config.include
473 # should allow to pass in a prefix.
476 # should allow to pass in a prefix.
474 settings.setdefault('rhodecode.api.url', '/_admin/api')
477 settings.setdefault('rhodecode.api.url', '/_admin/api')
475 settings.setdefault('__file__', global_config.get('__file__'))
478 settings.setdefault('__file__', global_config.get('__file__'))
476
479
477 # Sanitize generic settings.
480 # Sanitize generic settings.
478 _list_setting(settings, 'default_encoding', 'UTF-8')
481 _list_setting(settings, 'default_encoding', 'UTF-8')
479 _bool_setting(settings, 'is_test', 'false')
482 _bool_setting(settings, 'is_test', 'false')
480 _bool_setting(settings, 'gzip_responses', 'false')
483 _bool_setting(settings, 'gzip_responses', 'false')
481
484
482 # Call split out functions that sanitize settings for each topic.
485 # Call split out functions that sanitize settings for each topic.
483 _sanitize_appenlight_settings(settings)
486 _sanitize_appenlight_settings(settings)
484 _sanitize_vcs_settings(settings)
487 _sanitize_vcs_settings(settings)
485 _sanitize_cache_settings(settings)
488 _sanitize_cache_settings(settings)
486
489
487 # configure instance id
490 # configure instance id
488 config_utils.set_instance_id(settings)
491 config_utils.set_instance_id(settings)
489
492
490 return settings
493 return settings
491
494
492
495
493 def enable_debug():
496 def enable_debug():
494 """
497 """
495 Helper to enable debug on running instance
498 Helper to enable debug on running instance
496 :return:
499 :return:
497 """
500 """
498 import tempfile
501 import tempfile
499 import textwrap
502 import textwrap
500 import logging.config
503 import logging.config
501
504
502 ini_template = textwrap.dedent("""
505 ini_template = textwrap.dedent("""
503 #####################################
506 #####################################
504 ### DEBUG LOGGING CONFIGURATION ####
507 ### DEBUG LOGGING CONFIGURATION ####
505 #####################################
508 #####################################
506 [loggers]
509 [loggers]
507 keys = root, sqlalchemy, beaker, celery, rhodecode, ssh_wrapper
510 keys = root, sqlalchemy, beaker, celery, rhodecode, ssh_wrapper
508
511
509 [handlers]
512 [handlers]
510 keys = console, console_sql
513 keys = console, console_sql
511
514
512 [formatters]
515 [formatters]
513 keys = generic, color_formatter, color_formatter_sql
516 keys = generic, color_formatter, color_formatter_sql
514
517
515 #############
518 #############
516 ## LOGGERS ##
519 ## LOGGERS ##
517 #############
520 #############
518 [logger_root]
521 [logger_root]
519 level = NOTSET
522 level = NOTSET
520 handlers = console
523 handlers = console
521
524
522 [logger_sqlalchemy]
525 [logger_sqlalchemy]
523 level = INFO
526 level = INFO
524 handlers = console_sql
527 handlers = console_sql
525 qualname = sqlalchemy.engine
528 qualname = sqlalchemy.engine
526 propagate = 0
529 propagate = 0
527
530
528 [logger_beaker]
531 [logger_beaker]
529 level = DEBUG
532 level = DEBUG
530 handlers =
533 handlers =
531 qualname = beaker.container
534 qualname = beaker.container
532 propagate = 1
535 propagate = 1
533
536
534 [logger_rhodecode]
537 [logger_rhodecode]
535 level = DEBUG
538 level = DEBUG
536 handlers =
539 handlers =
537 qualname = rhodecode
540 qualname = rhodecode
538 propagate = 1
541 propagate = 1
539
542
540 [logger_ssh_wrapper]
543 [logger_ssh_wrapper]
541 level = DEBUG
544 level = DEBUG
542 handlers =
545 handlers =
543 qualname = ssh_wrapper
546 qualname = ssh_wrapper
544 propagate = 1
547 propagate = 1
545
548
546 [logger_celery]
549 [logger_celery]
547 level = DEBUG
550 level = DEBUG
548 handlers =
551 handlers =
549 qualname = celery
552 qualname = celery
550
553
551
554
552 ##############
555 ##############
553 ## HANDLERS ##
556 ## HANDLERS ##
554 ##############
557 ##############
555
558
556 [handler_console]
559 [handler_console]
557 class = StreamHandler
560 class = StreamHandler
558 args = (sys.stderr, )
561 args = (sys.stderr, )
559 level = DEBUG
562 level = DEBUG
560 formatter = color_formatter
563 formatter = color_formatter
561
564
562 [handler_console_sql]
565 [handler_console_sql]
563 # "level = DEBUG" logs SQL queries and results.
566 # "level = DEBUG" logs SQL queries and results.
564 # "level = INFO" logs SQL queries.
567 # "level = INFO" logs SQL queries.
565 # "level = WARN" logs neither. (Recommended for production systems.)
568 # "level = WARN" logs neither. (Recommended for production systems.)
566 class = StreamHandler
569 class = StreamHandler
567 args = (sys.stderr, )
570 args = (sys.stderr, )
568 level = WARN
571 level = WARN
569 formatter = color_formatter_sql
572 formatter = color_formatter_sql
570
573
571 ################
574 ################
572 ## FORMATTERS ##
575 ## FORMATTERS ##
573 ################
576 ################
574
577
575 [formatter_generic]
578 [formatter_generic]
576 class = rhodecode.lib.logging_formatter.ExceptionAwareFormatter
579 class = rhodecode.lib.logging_formatter.ExceptionAwareFormatter
577 format = %(asctime)s.%(msecs)03d [%(process)d] %(levelname)-5.5s [%(name)s] %(message)s | %(req_id)s
580 format = %(asctime)s.%(msecs)03d [%(process)d] %(levelname)-5.5s [%(name)s] %(message)s | %(req_id)s
578 datefmt = %Y-%m-%d %H:%M:%S
581 datefmt = %Y-%m-%d %H:%M:%S
579
582
580 [formatter_color_formatter]
583 [formatter_color_formatter]
581 class = rhodecode.lib.logging_formatter.ColorRequestTrackingFormatter
584 class = rhodecode.lib.logging_formatter.ColorRequestTrackingFormatter
582 format = %(asctime)s.%(msecs)03d [%(process)d] %(levelname)-5.5s [%(name)s] %(message)s | %(req_id)s
585 format = %(asctime)s.%(msecs)03d [%(process)d] %(levelname)-5.5s [%(name)s] %(message)s | %(req_id)s
583 datefmt = %Y-%m-%d %H:%M:%S
586 datefmt = %Y-%m-%d %H:%M:%S
584
587
585 [formatter_color_formatter_sql]
588 [formatter_color_formatter_sql]
586 class = rhodecode.lib.logging_formatter.ColorFormatterSql
589 class = rhodecode.lib.logging_formatter.ColorFormatterSql
587 format = %(asctime)s.%(msecs)03d [%(process)d] %(levelname)-5.5s [%(name)s] %(message)s
590 format = %(asctime)s.%(msecs)03d [%(process)d] %(levelname)-5.5s [%(name)s] %(message)s
588 datefmt = %Y-%m-%d %H:%M:%S
591 datefmt = %Y-%m-%d %H:%M:%S
589 """)
592 """)
590
593
591 with tempfile.NamedTemporaryFile(prefix='rc_debug_logging_', suffix='.ini',
594 with tempfile.NamedTemporaryFile(prefix='rc_debug_logging_', suffix='.ini',
592 delete=False) as f:
595 delete=False) as f:
593 log.info('Saved Temporary DEBUG config at %s', f.name)
596 log.info('Saved Temporary DEBUG config at %s', f.name)
594 f.write(ini_template)
597 f.write(ini_template)
595
598
596 logging.config.fileConfig(f.name)
599 logging.config.fileConfig(f.name)
597 log.debug('DEBUG MODE ON')
600 log.debug('DEBUG MODE ON')
598 os.remove(f.name)
601 os.remove(f.name)
599
602
600
603
601 def _sanitize_appenlight_settings(settings):
604 def _sanitize_appenlight_settings(settings):
602 _bool_setting(settings, 'appenlight', 'false')
605 _bool_setting(settings, 'appenlight', 'false')
603
606
604
607
605 def _sanitize_vcs_settings(settings):
608 def _sanitize_vcs_settings(settings):
606 """
609 """
607 Applies settings defaults and does type conversion for all VCS related
610 Applies settings defaults and does type conversion for all VCS related
608 settings.
611 settings.
609 """
612 """
610 _string_setting(settings, 'vcs.svn.compatible_version', '')
613 _string_setting(settings, 'vcs.svn.compatible_version', '')
611 _string_setting(settings, 'vcs.hooks.protocol', 'http')
614 _string_setting(settings, 'vcs.hooks.protocol', 'http')
612 _string_setting(settings, 'vcs.hooks.host', '127.0.0.1')
615 _string_setting(settings, 'vcs.hooks.host', '127.0.0.1')
613 _string_setting(settings, 'vcs.scm_app_implementation', 'http')
616 _string_setting(settings, 'vcs.scm_app_implementation', 'http')
614 _string_setting(settings, 'vcs.server', '')
617 _string_setting(settings, 'vcs.server', '')
615 _string_setting(settings, 'vcs.server.protocol', 'http')
618 _string_setting(settings, 'vcs.server.protocol', 'http')
616 _bool_setting(settings, 'startup.import_repos', 'false')
619 _bool_setting(settings, 'startup.import_repos', 'false')
617 _bool_setting(settings, 'vcs.hooks.direct_calls', 'false')
620 _bool_setting(settings, 'vcs.hooks.direct_calls', 'false')
618 _bool_setting(settings, 'vcs.server.enable', 'true')
621 _bool_setting(settings, 'vcs.server.enable', 'true')
619 _bool_setting(settings, 'vcs.start_server', 'false')
622 _bool_setting(settings, 'vcs.start_server', 'false')
620 _list_setting(settings, 'vcs.backends', 'hg, git, svn')
623 _list_setting(settings, 'vcs.backends', 'hg, git, svn')
621 _int_setting(settings, 'vcs.connection_timeout', 3600)
624 _int_setting(settings, 'vcs.connection_timeout', 3600)
622
625
623 # Support legacy values of vcs.scm_app_implementation. Legacy
626 # Support legacy values of vcs.scm_app_implementation. Legacy
624 # configurations may use 'rhodecode.lib.middleware.utils.scm_app_http', or
627 # configurations may use 'rhodecode.lib.middleware.utils.scm_app_http', or
625 # disabled since 4.13 'vcsserver.scm_app' which is now mapped to 'http'.
628 # disabled since 4.13 'vcsserver.scm_app' which is now mapped to 'http'.
626 scm_app_impl = settings['vcs.scm_app_implementation']
629 scm_app_impl = settings['vcs.scm_app_implementation']
627 if scm_app_impl in ['rhodecode.lib.middleware.utils.scm_app_http', 'vcsserver.scm_app']:
630 if scm_app_impl in ['rhodecode.lib.middleware.utils.scm_app_http', 'vcsserver.scm_app']:
628 settings['vcs.scm_app_implementation'] = 'http'
631 settings['vcs.scm_app_implementation'] = 'http'
629
632
630
633
631 def _sanitize_cache_settings(settings):
634 def _sanitize_cache_settings(settings):
632 temp_store = tempfile.gettempdir()
635 temp_store = tempfile.gettempdir()
633 default_cache_dir = os.path.join(temp_store, 'rc_cache')
636 default_cache_dir = os.path.join(temp_store, 'rc_cache')
634
637
635 # save default, cache dir, and use it for all backends later.
638 # save default, cache dir, and use it for all backends later.
636 default_cache_dir = _string_setting(
639 default_cache_dir = _string_setting(
637 settings,
640 settings,
638 'cache_dir',
641 'cache_dir',
639 default_cache_dir, lower=False, default_when_empty=True)
642 default_cache_dir, lower=False, default_when_empty=True)
640
643
641 # ensure we have our dir created
644 # ensure we have our dir created
642 if not os.path.isdir(default_cache_dir):
645 if not os.path.isdir(default_cache_dir):
643 os.makedirs(default_cache_dir, mode=0o755)
646 os.makedirs(default_cache_dir, mode=0o755)
644
647
645 # exception store cache
648 # exception store cache
646 _string_setting(
649 _string_setting(
647 settings,
650 settings,
648 'exception_tracker.store_path',
651 'exception_tracker.store_path',
649 temp_store, lower=False, default_when_empty=True)
652 temp_store, lower=False, default_when_empty=True)
650 _bool_setting(
653 _bool_setting(
651 settings,
654 settings,
652 'exception_tracker.send_email',
655 'exception_tracker.send_email',
653 'false')
656 'false')
654 _string_setting(
657 _string_setting(
655 settings,
658 settings,
656 'exception_tracker.email_prefix',
659 'exception_tracker.email_prefix',
657 '[RHODECODE ERROR]', lower=False, default_when_empty=True)
660 '[RHODECODE ERROR]', lower=False, default_when_empty=True)
658
661
659 # cache_perms
662 # cache_perms
660 _string_setting(
663 _string_setting(
661 settings,
664 settings,
662 'rc_cache.cache_perms.backend',
665 'rc_cache.cache_perms.backend',
663 'dogpile.cache.rc.file_namespace', lower=False)
666 'dogpile.cache.rc.file_namespace', lower=False)
664 _int_setting(
667 _int_setting(
665 settings,
668 settings,
666 'rc_cache.cache_perms.expiration_time',
669 'rc_cache.cache_perms.expiration_time',
667 60)
670 60)
668 _string_setting(
671 _string_setting(
669 settings,
672 settings,
670 'rc_cache.cache_perms.arguments.filename',
673 'rc_cache.cache_perms.arguments.filename',
671 os.path.join(default_cache_dir, 'rc_cache_1'), lower=False)
674 os.path.join(default_cache_dir, 'rc_cache_1'), lower=False)
672
675
673 # cache_repo
676 # cache_repo
674 _string_setting(
677 _string_setting(
675 settings,
678 settings,
676 'rc_cache.cache_repo.backend',
679 'rc_cache.cache_repo.backend',
677 'dogpile.cache.rc.file_namespace', lower=False)
680 'dogpile.cache.rc.file_namespace', lower=False)
678 _int_setting(
681 _int_setting(
679 settings,
682 settings,
680 'rc_cache.cache_repo.expiration_time',
683 'rc_cache.cache_repo.expiration_time',
681 60)
684 60)
682 _string_setting(
685 _string_setting(
683 settings,
686 settings,
684 'rc_cache.cache_repo.arguments.filename',
687 'rc_cache.cache_repo.arguments.filename',
685 os.path.join(default_cache_dir, 'rc_cache_2'), lower=False)
688 os.path.join(default_cache_dir, 'rc_cache_2'), lower=False)
686
689
687 # cache_license
690 # cache_license
688 _string_setting(
691 _string_setting(
689 settings,
692 settings,
690 'rc_cache.cache_license.backend',
693 'rc_cache.cache_license.backend',
691 'dogpile.cache.rc.file_namespace', lower=False)
694 'dogpile.cache.rc.file_namespace', lower=False)
692 _int_setting(
695 _int_setting(
693 settings,
696 settings,
694 'rc_cache.cache_license.expiration_time',
697 'rc_cache.cache_license.expiration_time',
695 5*60)
698 5*60)
696 _string_setting(
699 _string_setting(
697 settings,
700 settings,
698 'rc_cache.cache_license.arguments.filename',
701 'rc_cache.cache_license.arguments.filename',
699 os.path.join(default_cache_dir, 'rc_cache_3'), lower=False)
702 os.path.join(default_cache_dir, 'rc_cache_3'), lower=False)
700
703
701 # cache_repo_longterm memory, 96H
704 # cache_repo_longterm memory, 96H
702 _string_setting(
705 _string_setting(
703 settings,
706 settings,
704 'rc_cache.cache_repo_longterm.backend',
707 'rc_cache.cache_repo_longterm.backend',
705 'dogpile.cache.rc.memory_lru', lower=False)
708 'dogpile.cache.rc.memory_lru', lower=False)
706 _int_setting(
709 _int_setting(
707 settings,
710 settings,
708 'rc_cache.cache_repo_longterm.expiration_time',
711 'rc_cache.cache_repo_longterm.expiration_time',
709 345600)
712 345600)
710 _int_setting(
713 _int_setting(
711 settings,
714 settings,
712 'rc_cache.cache_repo_longterm.max_size',
715 'rc_cache.cache_repo_longterm.max_size',
713 10000)
716 10000)
714
717
715 # sql_cache_short
718 # sql_cache_short
716 _string_setting(
719 _string_setting(
717 settings,
720 settings,
718 'rc_cache.sql_cache_short.backend',
721 'rc_cache.sql_cache_short.backend',
719 'dogpile.cache.rc.memory_lru', lower=False)
722 'dogpile.cache.rc.memory_lru', lower=False)
720 _int_setting(
723 _int_setting(
721 settings,
724 settings,
722 'rc_cache.sql_cache_short.expiration_time',
725 'rc_cache.sql_cache_short.expiration_time',
723 30)
726 30)
724 _int_setting(
727 _int_setting(
725 settings,
728 settings,
726 'rc_cache.sql_cache_short.max_size',
729 'rc_cache.sql_cache_short.max_size',
727 10000)
730 10000)
728
731
729
732
730 def _int_setting(settings, name, default):
733 def _int_setting(settings, name, default):
731 settings[name] = int(settings.get(name, default))
734 settings[name] = int(settings.get(name, default))
732 return settings[name]
735 return settings[name]
733
736
734
737
735 def _bool_setting(settings, name, default):
738 def _bool_setting(settings, name, default):
736 input_val = settings.get(name, default)
739 input_val = settings.get(name, default)
737 if isinstance(input_val, unicode):
740 if isinstance(input_val, unicode):
738 input_val = input_val.encode('utf8')
741 input_val = input_val.encode('utf8')
739 settings[name] = asbool(input_val)
742 settings[name] = asbool(input_val)
740 return settings[name]
743 return settings[name]
741
744
742
745
743 def _list_setting(settings, name, default):
746 def _list_setting(settings, name, default):
744 raw_value = settings.get(name, default)
747 raw_value = settings.get(name, default)
745
748
746 old_separator = ','
749 old_separator = ','
747 if old_separator in raw_value:
750 if old_separator in raw_value:
748 # If we get a comma separated list, pass it to our own function.
751 # If we get a comma separated list, pass it to our own function.
749 settings[name] = rhodecode_aslist(raw_value, sep=old_separator)
752 settings[name] = rhodecode_aslist(raw_value, sep=old_separator)
750 else:
753 else:
751 # Otherwise we assume it uses pyramids space/newline separation.
754 # Otherwise we assume it uses pyramids space/newline separation.
752 settings[name] = aslist(raw_value)
755 settings[name] = aslist(raw_value)
753 return settings[name]
756 return settings[name]
754
757
755
758
756 def _string_setting(settings, name, default, lower=True, default_when_empty=False):
759 def _string_setting(settings, name, default, lower=True, default_when_empty=False):
757 value = settings.get(name, default)
760 value = settings.get(name, default)
758
761
759 if default_when_empty and not value:
762 if default_when_empty and not value:
760 # use default value when value is empty
763 # use default value when value is empty
761 value = default
764 value = default
762
765
763 if lower:
766 if lower:
764 value = value.lower()
767 value = value.lower()
765 settings[name] = value
768 settings[name] = value
766 return settings[name]
769 return settings[name]
767
770
768
771
769 def _substitute_values(mapping, substitutions):
772 def _substitute_values(mapping, substitutions):
770 result = {}
773 result = {}
771
774
772 try:
775 try:
773 for key, value in mapping.items():
776 for key, value in mapping.items():
774 # initialize without substitution first
777 # initialize without substitution first
775 result[key] = value
778 result[key] = value
776
779
777 # Note: Cannot use regular replacements, since they would clash
780 # Note: Cannot use regular replacements, since they would clash
778 # with the implementation of ConfigParser. Using "format" instead.
781 # with the implementation of ConfigParser. Using "format" instead.
779 try:
782 try:
780 result[key] = value.format(**substitutions)
783 result[key] = value.format(**substitutions)
781 except KeyError as e:
784 except KeyError as e:
782 env_var = '{}'.format(e.args[0])
785 env_var = '{}'.format(e.args[0])
783
786
784 msg = 'Failed to substitute: `{key}={{{var}}}` with environment entry. ' \
787 msg = 'Failed to substitute: `{key}={{{var}}}` with environment entry. ' \
785 'Make sure your environment has {var} set, or remove this ' \
788 'Make sure your environment has {var} set, or remove this ' \
786 'variable from config file'.format(key=key, var=env_var)
789 'variable from config file'.format(key=key, var=env_var)
787
790
788 if env_var.startswith('ENV_'):
791 if env_var.startswith('ENV_'):
789 raise ValueError(msg)
792 raise ValueError(msg)
790 else:
793 else:
791 log.warning(msg)
794 log.warning(msg)
792
795
793 except ValueError as e:
796 except ValueError as e:
794 log.warning('Failed to substitute ENV variable: %s', e)
797 log.warning('Failed to substitute ENV variable: %s', e)
795 result = mapping
798 result = mapping
796
799
797 return result
800 return result
@@ -1,310 +1,312 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2020 RhodeCode GmbH
3 # Copyright (C) 2010-2020 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 """
20 """
21 Celery loader, run with::
21 Celery loader, run with::
22
22
23 celery worker \
23 celery worker \
24 --beat \
24 --beat \
25 --app rhodecode.lib.celerylib.loader \
25 --app rhodecode.lib.celerylib.loader \
26 --scheduler rhodecode.lib.celerylib.scheduler.RcScheduler \
26 --scheduler rhodecode.lib.celerylib.scheduler.RcScheduler \
27 --loglevel DEBUG --ini=._dev/dev.ini
27 --loglevel DEBUG --ini=._dev/dev.ini
28 """
28 """
29 import os
29 import os
30 import logging
30 import logging
31 import importlib
31 import importlib
32
32
33 from celery import Celery
33 from celery import Celery
34 from celery import signals
34 from celery import signals
35 from celery import Task
35 from celery import Task
36 from celery import exceptions # pragma: no cover
36 from celery import exceptions # pragma: no cover
37 from kombu.serialization import register
37 from kombu.serialization import register
38 from pyramid.threadlocal import get_current_request
38 from pyramid.threadlocal import get_current_request
39
39
40 import rhodecode
40 import rhodecode
41
41
42 from rhodecode.lib.auth import AuthUser
42 from rhodecode.lib.auth import AuthUser
43 from rhodecode.lib.celerylib.utils import get_ini_config, parse_ini_vars, ping_db
43 from rhodecode.lib.celerylib.utils import get_ini_config, parse_ini_vars, ping_db
44 from rhodecode.lib.ext_json import json
44 from rhodecode.lib.ext_json import json
45 from rhodecode.lib.pyramid_utils import bootstrap, setup_logging, prepare_request
45 from rhodecode.lib.pyramid_utils import bootstrap, setup_logging, prepare_request
46 from rhodecode.lib.utils2 import str2bool
46 from rhodecode.lib.utils2 import str2bool
47 from rhodecode.model import meta
47 from rhodecode.model import meta
48
48
49
49
50 register('json_ext', json.dumps, json.loads,
50 register('json_ext', json.dumps, json.loads,
51 content_type='application/x-json-ext',
51 content_type='application/x-json-ext',
52 content_encoding='utf-8')
52 content_encoding='utf-8')
53
53
54 log = logging.getLogger('celery.rhodecode.loader')
54 log = logging.getLogger('celery.rhodecode.loader')
55
55
56
56
57 def add_preload_arguments(parser):
57 def add_preload_arguments(parser):
58 parser.add_argument(
58 parser.add_argument(
59 '--ini', default=None,
59 '--ini', default=None,
60 help='Path to ini configuration file.'
60 help='Path to ini configuration file.'
61 )
61 )
62 parser.add_argument(
62 parser.add_argument(
63 '--ini-var', default=None,
63 '--ini-var', default=None,
64 help='Comma separated list of key=value to pass to ini.'
64 help='Comma separated list of key=value to pass to ini.'
65 )
65 )
66
66
67
67
68 def get_logger(obj):
68 def get_logger(obj):
69 custom_log = logging.getLogger(
69 custom_log = logging.getLogger(
70 'rhodecode.task.{}'.format(obj.__class__.__name__))
70 'rhodecode.task.{}'.format(obj.__class__.__name__))
71
71
72 if rhodecode.CELERY_ENABLED:
72 if rhodecode.CELERY_ENABLED:
73 try:
73 try:
74 custom_log = obj.get_logger()
74 custom_log = obj.get_logger()
75 except Exception:
75 except Exception:
76 pass
76 pass
77
77
78 return custom_log
78 return custom_log
79
79
80
80
81 imports = ['rhodecode.lib.celerylib.tasks']
81 imports = ['rhodecode.lib.celerylib.tasks']
82
82
83 try:
83 try:
84 # try if we have EE tasks available
84 # try if we have EE tasks available
85 importlib.import_module('rc_ee')
85 importlib.import_module('rc_ee')
86 imports.append('rc_ee.lib.celerylib.tasks')
86 imports.append('rc_ee.lib.celerylib.tasks')
87 except ImportError:
87 except ImportError:
88 pass
88 pass
89
89
90
90
91 base_celery_config = {
91 base_celery_config = {
92 'result_backend': 'rpc://',
92 'result_backend': 'rpc://',
93 'result_expires': 60 * 60 * 24,
93 'result_expires': 60 * 60 * 24,
94 'result_persistent': True,
94 'result_persistent': True,
95 'imports': imports,
95 'imports': imports,
96 'worker_max_tasks_per_child': 100,
96 'worker_max_tasks_per_child': 100,
97 'accept_content': ['json_ext'],
97 'accept_content': ['json_ext'],
98 'task_serializer': 'json_ext',
98 'task_serializer': 'json_ext',
99 'result_serializer': 'json_ext',
99 'result_serializer': 'json_ext',
100 'worker_hijack_root_logger': False,
100 'worker_hijack_root_logger': False,
101 'database_table_names': {
101 'database_table_names': {
102 'task': 'beat_taskmeta',
102 'task': 'beat_taskmeta',
103 'group': 'beat_groupmeta',
103 'group': 'beat_groupmeta',
104 }
104 }
105 }
105 }
106 # init main celery app
106 # init main celery app
107 celery_app = Celery()
107 celery_app = Celery()
108 celery_app.user_options['preload'].add(add_preload_arguments)
108 celery_app.user_options['preload'].add(add_preload_arguments)
109 ini_file_glob = None
109 ini_file_glob = None
110
110
111
111
112 @signals.setup_logging.connect
112 @signals.setup_logging.connect
113 def setup_logging_callback(**kwargs):
113 def setup_logging_callback(**kwargs):
114 setup_logging(ini_file_glob)
114 setup_logging(ini_file_glob)
115
115
116
116
117 @signals.user_preload_options.connect
117 @signals.user_preload_options.connect
118 def on_preload_parsed(options, **kwargs):
118 def on_preload_parsed(options, **kwargs):
119 ini_location = options['ini']
119 ini_location = options['ini']
120 ini_vars = options['ini_var']
120 ini_vars = options['ini_var']
121 celery_app.conf['INI_PYRAMID'] = options['ini']
121 celery_app.conf['INI_PYRAMID'] = options['ini']
122
122
123 if ini_location is None:
123 if ini_location is None:
124 print('You must provide the paste --ini argument')
124 print('You must provide the paste --ini argument')
125 exit(-1)
125 exit(-1)
126
126
127 options = None
127 options = None
128 if ini_vars is not None:
128 if ini_vars is not None:
129 options = parse_ini_vars(ini_vars)
129 options = parse_ini_vars(ini_vars)
130
130
131 global ini_file_glob
131 global ini_file_glob
132 ini_file_glob = ini_location
132 ini_file_glob = ini_location
133
133
134 log.debug('Bootstrapping RhodeCode application...')
134 log.debug('Bootstrapping RhodeCode application...')
135 env = bootstrap(ini_location, options=options)
135 env = bootstrap(ini_location, options=options)
136
136
137 setup_celery_app(
137 setup_celery_app(
138 app=env['app'], root=env['root'], request=env['request'],
138 app=env['app'], root=env['root'], request=env['request'],
139 registry=env['registry'], closer=env['closer'],
139 registry=env['registry'], closer=env['closer'],
140 ini_location=ini_location)
140 ini_location=ini_location)
141
141
142 # fix the global flag even if it's disabled via .ini file because this
142 # fix the global flag even if it's disabled via .ini file because this
143 # is a worker code that doesn't need this to be disabled.
143 # is a worker code that doesn't need this to be disabled.
144 rhodecode.CELERY_ENABLED = True
144 rhodecode.CELERY_ENABLED = True
145
145
146
146
147 @signals.task_prerun.connect
147 @signals.task_prerun.connect
148 def task_prerun_signal(task_id, task, args, **kwargs):
148 def task_prerun_signal(task_id, task, args, **kwargs):
149 ping_db()
149 ping_db()
150
150
151
151
152 @signals.task_success.connect
152 @signals.task_success.connect
153 def task_success_signal(result, **kwargs):
153 def task_success_signal(result, **kwargs):
154 meta.Session.commit()
154 meta.Session.commit()
155 closer = celery_app.conf['PYRAMID_CLOSER']
155 closer = celery_app.conf['PYRAMID_CLOSER']
156 if closer:
156 if closer:
157 closer()
157 closer()
158
158
159
159
160 @signals.task_retry.connect
160 @signals.task_retry.connect
161 def task_retry_signal(
161 def task_retry_signal(
162 request, reason, einfo, **kwargs):
162 request, reason, einfo, **kwargs):
163 meta.Session.remove()
163 meta.Session.remove()
164 closer = celery_app.conf['PYRAMID_CLOSER']
164 closer = celery_app.conf['PYRAMID_CLOSER']
165 if closer:
165 if closer:
166 closer()
166 closer()
167
167
168
168
169 @signals.task_failure.connect
169 @signals.task_failure.connect
170 def task_failure_signal(
170 def task_failure_signal(
171 task_id, exception, args, kwargs, traceback, einfo, **kargs):
171 task_id, exception, args, kwargs, traceback, einfo, **kargs):
172 from rhodecode.lib.exc_tracking import store_exception
172 from rhodecode.lib.exc_tracking import store_exception
173 from rhodecode.lib.statsd_client import StatsdClient
173 from rhodecode.lib.statsd_client import StatsdClient
174
174
175 meta.Session.remove()
175 meta.Session.remove()
176
176
177 # simulate sys.exc_info()
177 # simulate sys.exc_info()
178 exc_info = (einfo.type, einfo.exception, einfo.tb)
178 exc_info = (einfo.type, einfo.exception, einfo.tb)
179 store_exception(id(exc_info), exc_info, prefix='rhodecode-celery')
179 store_exception(id(exc_info), exc_info, prefix='rhodecode-celery')
180 statsd = StatsdClient.statsd
180 statsd = StatsdClient.statsd
181 if statsd:
181 if statsd:
182 statsd.incr('rhodecode_exception_total', tags=["exc_source:celery", "type:{}".format(einfo.type)])
182 exc_type = "{}.{}".format(einfo.__class__.__module__, einfo.__class__.__name__)
183 statsd.incr('rhodecode_exception_total',
184 tags=["exc_source:celery", "type:{}".format(exc_type)])
183
185
184 closer = celery_app.conf['PYRAMID_CLOSER']
186 closer = celery_app.conf['PYRAMID_CLOSER']
185 if closer:
187 if closer:
186 closer()
188 closer()
187
189
188
190
189 @signals.task_revoked.connect
191 @signals.task_revoked.connect
190 def task_revoked_signal(
192 def task_revoked_signal(
191 request, terminated, signum, expired, **kwargs):
193 request, terminated, signum, expired, **kwargs):
192 closer = celery_app.conf['PYRAMID_CLOSER']
194 closer = celery_app.conf['PYRAMID_CLOSER']
193 if closer:
195 if closer:
194 closer()
196 closer()
195
197
196
198
197 def setup_celery_app(app, root, request, registry, closer, ini_location):
199 def setup_celery_app(app, root, request, registry, closer, ini_location):
198 ini_dir = os.path.dirname(os.path.abspath(ini_location))
200 ini_dir = os.path.dirname(os.path.abspath(ini_location))
199 celery_config = base_celery_config
201 celery_config = base_celery_config
200 celery_config.update({
202 celery_config.update({
201 # store celerybeat scheduler db where the .ini file is
203 # store celerybeat scheduler db where the .ini file is
202 'beat_schedule_filename': os.path.join(ini_dir, 'celerybeat-schedule'),
204 'beat_schedule_filename': os.path.join(ini_dir, 'celerybeat-schedule'),
203 })
205 })
204 ini_settings = get_ini_config(ini_location)
206 ini_settings = get_ini_config(ini_location)
205 log.debug('Got custom celery conf: %s', ini_settings)
207 log.debug('Got custom celery conf: %s', ini_settings)
206
208
207 celery_config.update(ini_settings)
209 celery_config.update(ini_settings)
208 celery_app.config_from_object(celery_config)
210 celery_app.config_from_object(celery_config)
209
211
210 celery_app.conf.update({'PYRAMID_APP': app})
212 celery_app.conf.update({'PYRAMID_APP': app})
211 celery_app.conf.update({'PYRAMID_ROOT': root})
213 celery_app.conf.update({'PYRAMID_ROOT': root})
212 celery_app.conf.update({'PYRAMID_REQUEST': request})
214 celery_app.conf.update({'PYRAMID_REQUEST': request})
213 celery_app.conf.update({'PYRAMID_REGISTRY': registry})
215 celery_app.conf.update({'PYRAMID_REGISTRY': registry})
214 celery_app.conf.update({'PYRAMID_CLOSER': closer})
216 celery_app.conf.update({'PYRAMID_CLOSER': closer})
215
217
216
218
217 def configure_celery(config, ini_location):
219 def configure_celery(config, ini_location):
218 """
220 """
219 Helper that is called from our application creation logic. It gives
221 Helper that is called from our application creation logic. It gives
220 connection info into running webapp and allows execution of tasks from
222 connection info into running webapp and allows execution of tasks from
221 RhodeCode itself
223 RhodeCode itself
222 """
224 """
223 # store some globals into rhodecode
225 # store some globals into rhodecode
224 rhodecode.CELERY_ENABLED = str2bool(
226 rhodecode.CELERY_ENABLED = str2bool(
225 config.registry.settings.get('use_celery'))
227 config.registry.settings.get('use_celery'))
226 if rhodecode.CELERY_ENABLED:
228 if rhodecode.CELERY_ENABLED:
227 log.info('Configuring celery based on `%s` file', ini_location)
229 log.info('Configuring celery based on `%s` file', ini_location)
228 setup_celery_app(
230 setup_celery_app(
229 app=None, root=None, request=None, registry=config.registry,
231 app=None, root=None, request=None, registry=config.registry,
230 closer=None, ini_location=ini_location)
232 closer=None, ini_location=ini_location)
231
233
232
234
233 def maybe_prepare_env(req):
235 def maybe_prepare_env(req):
234 environ = {}
236 environ = {}
235 try:
237 try:
236 environ.update({
238 environ.update({
237 'PATH_INFO': req.environ['PATH_INFO'],
239 'PATH_INFO': req.environ['PATH_INFO'],
238 'SCRIPT_NAME': req.environ['SCRIPT_NAME'],
240 'SCRIPT_NAME': req.environ['SCRIPT_NAME'],
239 'HTTP_HOST':req.environ.get('HTTP_HOST', req.environ['SERVER_NAME']),
241 'HTTP_HOST':req.environ.get('HTTP_HOST', req.environ['SERVER_NAME']),
240 'SERVER_NAME': req.environ['SERVER_NAME'],
242 'SERVER_NAME': req.environ['SERVER_NAME'],
241 'SERVER_PORT': req.environ['SERVER_PORT'],
243 'SERVER_PORT': req.environ['SERVER_PORT'],
242 'wsgi.url_scheme': req.environ['wsgi.url_scheme'],
244 'wsgi.url_scheme': req.environ['wsgi.url_scheme'],
243 })
245 })
244 except Exception:
246 except Exception:
245 pass
247 pass
246
248
247 return environ
249 return environ
248
250
249
251
250 class RequestContextTask(Task):
252 class RequestContextTask(Task):
251 """
253 """
252 This is a celery task which will create a rhodecode app instance context
254 This is a celery task which will create a rhodecode app instance context
253 for the task, patch pyramid with the original request
255 for the task, patch pyramid with the original request
254 that created the task and also add the user to the context.
256 that created the task and also add the user to the context.
255 """
257 """
256
258
257 def apply_async(self, args=None, kwargs=None, task_id=None, producer=None,
259 def apply_async(self, args=None, kwargs=None, task_id=None, producer=None,
258 link=None, link_error=None, shadow=None, **options):
260 link=None, link_error=None, shadow=None, **options):
259 """ queue the job to run (we are in web request context here) """
261 """ queue the job to run (we are in web request context here) """
260
262
261 req = get_current_request()
263 req = get_current_request()
262
264
263 # web case
265 # web case
264 if hasattr(req, 'user'):
266 if hasattr(req, 'user'):
265 ip_addr = req.user.ip_addr
267 ip_addr = req.user.ip_addr
266 user_id = req.user.user_id
268 user_id = req.user.user_id
267
269
268 # api case
270 # api case
269 elif hasattr(req, 'rpc_user'):
271 elif hasattr(req, 'rpc_user'):
270 ip_addr = req.rpc_user.ip_addr
272 ip_addr = req.rpc_user.ip_addr
271 user_id = req.rpc_user.user_id
273 user_id = req.rpc_user.user_id
272 else:
274 else:
273 raise Exception(
275 raise Exception(
274 'Unable to fetch required data from request: {}. \n'
276 'Unable to fetch required data from request: {}. \n'
275 'This task is required to be executed from context of '
277 'This task is required to be executed from context of '
276 'request in a webapp'.format(repr(req)))
278 'request in a webapp'.format(repr(req)))
277
279
278 if req:
280 if req:
279 # we hook into kwargs since it is the only way to pass our data to
281 # we hook into kwargs since it is the only way to pass our data to
280 # the celery worker
282 # the celery worker
281 environ = maybe_prepare_env(req)
283 environ = maybe_prepare_env(req)
282 options['headers'] = options.get('headers', {})
284 options['headers'] = options.get('headers', {})
283 options['headers'].update({
285 options['headers'].update({
284 'rhodecode_proxy_data': {
286 'rhodecode_proxy_data': {
285 'environ': environ,
287 'environ': environ,
286 'auth_user': {
288 'auth_user': {
287 'ip_addr': ip_addr,
289 'ip_addr': ip_addr,
288 'user_id': user_id
290 'user_id': user_id
289 },
291 },
290 }
292 }
291 })
293 })
292
294
293 return super(RequestContextTask, self).apply_async(
295 return super(RequestContextTask, self).apply_async(
294 args, kwargs, task_id, producer, link, link_error, shadow, **options)
296 args, kwargs, task_id, producer, link, link_error, shadow, **options)
295
297
296 def __call__(self, *args, **kwargs):
298 def __call__(self, *args, **kwargs):
297 """ rebuild the context and then run task on celery worker """
299 """ rebuild the context and then run task on celery worker """
298
300
299 proxy_data = getattr(self.request, 'rhodecode_proxy_data', None)
301 proxy_data = getattr(self.request, 'rhodecode_proxy_data', None)
300 if not proxy_data:
302 if not proxy_data:
301 return super(RequestContextTask, self).__call__(*args, **kwargs)
303 return super(RequestContextTask, self).__call__(*args, **kwargs)
302
304
303 log.debug('using celery proxy data to run task: %r', proxy_data)
305 log.debug('using celery proxy data to run task: %r', proxy_data)
304 # re-inject and register threadlocals for proper routing support
306 # re-inject and register threadlocals for proper routing support
305 request = prepare_request(proxy_data['environ'])
307 request = prepare_request(proxy_data['environ'])
306 request.user = AuthUser(user_id=proxy_data['auth_user']['user_id'],
308 request.user = AuthUser(user_id=proxy_data['auth_user']['user_id'],
307 ip_addr=proxy_data['auth_user']['ip_addr'])
309 ip_addr=proxy_data['auth_user']['ip_addr'])
308
310
309 return super(RequestContextTask, self).__call__(*args, **kwargs)
311 return super(RequestContextTask, self).__call__(*args, **kwargs)
310
312
@@ -1,89 +1,90 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2016-2020 RhodeCode GmbH
3 # Copyright (C) 2016-2020 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 import time
21 import time
22 import logging
22 import logging
23
23
24 import rhodecode
24 import rhodecode
25 from rhodecode.lib.auth import AuthUser
25 from rhodecode.lib.auth import AuthUser
26 from rhodecode.lib.base import get_ip_addr, get_access_path, get_user_agent
26 from rhodecode.lib.base import get_ip_addr, get_access_path, get_user_agent
27 from rhodecode.lib.utils2 import safe_str, get_current_rhodecode_user
27 from rhodecode.lib.utils2 import safe_str, get_current_rhodecode_user
28
28
29
29
30 log = logging.getLogger(__name__)
30 log = logging.getLogger(__name__)
31
31
32
32
33 class RequestWrapperTween(object):
33 class RequestWrapperTween(object):
34 def __init__(self, handler, registry):
34 def __init__(self, handler, registry):
35 self.handler = handler
35 self.handler = handler
36 self.registry = registry
36 self.registry = registry
37
37
38 # one-time configuration code goes here
38 # one-time configuration code goes here
39
39
40 def _get_user_info(self, request):
40 def _get_user_info(self, request):
41 user = get_current_rhodecode_user(request)
41 user = get_current_rhodecode_user(request)
42 if not user:
42 if not user:
43 user = AuthUser.repr_user(ip=get_ip_addr(request.environ))
43 user = AuthUser.repr_user(ip=get_ip_addr(request.environ))
44 return user
44 return user
45
45
46 def __call__(self, request):
46 def __call__(self, request):
47 start = time.time()
47 start = time.time()
48 log.debug('Starting request time measurement')
48 log.debug('Starting request time measurement')
49 response = None
49 try:
50 try:
50 response = self.handler(request)
51 response = self.handler(request)
51 finally:
52 finally:
52 count = request.request_count()
53 count = request.request_count()
53 _ver_ = rhodecode.__version__
54 _ver_ = rhodecode.__version__
54 _path = safe_str(get_access_path(request.environ))
55 _path = safe_str(get_access_path(request.environ))
55 _auth_user = self._get_user_info(request)
56 _auth_user = self._get_user_info(request)
56
57
57 total = time.time() - start
58 total = time.time() - start
58 log.info(
59 log.info(
59 'Req[%4s] %s %s Request to %s time: %.4fs [%s], RhodeCode %s',
60 'Req[%4s] %s %s Request to %s time: %.4fs [%s], RhodeCode %s',
60 count, _auth_user, request.environ.get('REQUEST_METHOD'),
61 count, _auth_user, request.environ.get('REQUEST_METHOD'),
61 _path, total, get_user_agent(request. environ), _ver_
62 _path, total, get_user_agent(request. environ), _ver_
62 )
63 )
63
64
64 statsd = request.registry.statsd
65 statsd = request.registry.statsd
65 if statsd:
66 if statsd:
66 match_route = request.matched_route.name if request.matched_route else _path
67 match_route = request.matched_route.name if request.matched_route else _path
67 resp_code = response.status_code
68 resp_code = getattr(response, 'status_code', 'UNDEFINED')
68 elapsed_time_ms = round(1000.0 * total) # use ms only
69 elapsed_time_ms = round(1000.0 * total) # use ms only
69 statsd.timing(
70 statsd.timing(
70 "rhodecode_req_timing.histogram", elapsed_time_ms,
71 "rhodecode_req_timing.histogram", elapsed_time_ms,
71 tags=[
72 tags=[
72 "view_name:{}".format(match_route),
73 "view_name:{}".format(match_route),
73 "code:{}".format(resp_code)
74 "code:{}".format(resp_code)
74 ],
75 ],
75 use_decimals=False
76 use_decimals=False
76 )
77 )
77 statsd.incr(
78 statsd.incr(
78 'rhodecode_req_total', tags=[
79 'rhodecode_req_total', tags=[
79 "view_name:{}".format(match_route),
80 "view_name:{}".format(match_route),
80 "code:{}".format(resp_code)
81 "code:{}".format(resp_code)
81 ])
82 ])
82
83
83 return response
84 return response
84
85
85
86
86 def includeme(config):
87 def includeme(config):
87 config.add_tween(
88 config.add_tween(
88 'rhodecode.lib.middleware.request_wrapper.RequestWrapperTween',
89 'rhodecode.lib.middleware.request_wrapper.RequestWrapperTween',
89 )
90 )
General Comments 0
You need to be logged in to leave comments. Login now