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