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