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