##// END OF EJS Templates
hosting: added usage writers for hosting needs.
marcink -
r4473:fbaf80e4 default
parent child Browse files
Show More
@@ -1,761 +1,763 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.celerylib.loader import configure_celery
51 51 from rhodecode.lib.plugins.utils import register_rhodecode_plugin
52 52 from rhodecode.lib.utils2 import aslist as rhodecode_aslist, AttributeDict
53 53 from rhodecode.lib.exc_tracking import store_exception
54 54 from rhodecode.subscribers import (
55 55 scan_repositories_if_enabled, write_js_routes_if_enabled,
56 write_metadata_if_needed, inject_app_settings)
56 write_metadata_if_needed, write_usage_data, inject_app_settings)
57 57
58 58
59 59 log = logging.getLogger(__name__)
60 60
61 61
62 62 def is_http_error(response):
63 63 # error which should have traceback
64 64 return response.status_code > 499
65 65
66 66
67 67 def should_load_all():
68 68 """
69 69 Returns if all application components should be loaded. In some cases it's
70 70 desired to skip apps loading for faster shell script execution
71 71 """
72 72 ssh_cmd = os.environ.get('RC_CMD_SSH_WRAPPER')
73 73 if ssh_cmd:
74 74 return False
75 75
76 76 return True
77 77
78 78
79 79 def make_pyramid_app(global_config, **settings):
80 80 """
81 81 Constructs the WSGI application based on Pyramid.
82 82
83 83 Specials:
84 84
85 85 * The application can also be integrated like a plugin via the call to
86 86 `includeme`. This is accompanied with the other utility functions which
87 87 are called. Changing this should be done with great care to not break
88 88 cases when these fragments are assembled from another place.
89 89
90 90 """
91 91
92 92 # Allows to use format style "{ENV_NAME}" placeholders in the configuration. It
93 93 # will be replaced by the value of the environment variable "NAME" in this case.
94 94 start_time = time.time()
95 95
96 96 debug = asbool(global_config.get('debug'))
97 97 if debug:
98 98 enable_debug()
99 99
100 100 environ = {'ENV_{}'.format(key): value for key, value in os.environ.items()}
101 101
102 102 global_config = _substitute_values(global_config, environ)
103 103 settings = _substitute_values(settings, environ)
104 104
105 105 sanitize_settings_and_apply_defaults(global_config, settings)
106 106
107 107 config = Configurator(settings=settings)
108 108
109 109 # Apply compatibility patches
110 110 patches.inspect_getargspec()
111 111
112 112 load_pyramid_environment(global_config, settings)
113 113
114 114 # Static file view comes first
115 115 includeme_first(config)
116 116
117 117 includeme(config)
118 118
119 119 pyramid_app = config.make_wsgi_app()
120 120 pyramid_app = wrap_app_in_wsgi_middlewares(pyramid_app, config)
121 121 pyramid_app.config = config
122 122
123 123 config.configure_celery(global_config['__file__'])
124 124 # creating the app uses a connection - return it after we are done
125 125 meta.Session.remove()
126 126 total_time = time.time() - start_time
127 127 log.info('Pyramid app `%s` created and configured in %.2fs',
128 128 pyramid_app.func_name, total_time)
129 129
130 130 return pyramid_app
131 131
132 132
133 133 def not_found_view(request):
134 134 """
135 135 This creates the view which should be registered as not-found-view to
136 136 pyramid.
137 137 """
138 138
139 139 if not getattr(request, 'vcs_call', None):
140 140 # handle like regular case with our error_handler
141 141 return error_handler(HTTPNotFound(), request)
142 142
143 143 # handle not found view as a vcs call
144 144 settings = request.registry.settings
145 145 ae_client = getattr(request, 'ae_client', None)
146 146 vcs_app = VCSMiddleware(
147 147 HTTPNotFound(), request.registry, settings,
148 148 appenlight_client=ae_client)
149 149
150 150 return wsgiapp(vcs_app)(None, request)
151 151
152 152
153 153 def error_handler(exception, request):
154 154 import rhodecode
155 155 from rhodecode.lib import helpers
156 156
157 157 rhodecode_title = rhodecode.CONFIG.get('rhodecode_title') or 'RhodeCode'
158 158
159 159 base_response = HTTPInternalServerError()
160 160 # prefer original exception for the response since it may have headers set
161 161 if isinstance(exception, HTTPException):
162 162 base_response = exception
163 163 elif isinstance(exception, VCSCommunicationError):
164 164 base_response = VCSServerUnavailable()
165 165
166 166 if is_http_error(base_response):
167 167 log.exception(
168 168 'error occurred handling this request for path: %s', request.path)
169 169
170 170 error_explanation = base_response.explanation or str(base_response)
171 171 if base_response.status_code == 404:
172 172 error_explanation += " Optionally you don't have permission to access this page."
173 173 c = AttributeDict()
174 174 c.error_message = base_response.status
175 175 c.error_explanation = error_explanation
176 176 c.visual = AttributeDict()
177 177
178 178 c.visual.rhodecode_support_url = (
179 179 request.registry.settings.get('rhodecode_support_url') or
180 180 request.route_url('rhodecode_support')
181 181 )
182 182 c.redirect_time = 0
183 183 c.rhodecode_name = rhodecode_title
184 184 if not c.rhodecode_name:
185 185 c.rhodecode_name = 'Rhodecode'
186 186
187 187 c.causes = []
188 188 if is_http_error(base_response):
189 189 c.causes.append('Server is overloaded.')
190 190 c.causes.append('Server database connection is lost.')
191 191 c.causes.append('Server expected unhandled error.')
192 192
193 193 if hasattr(base_response, 'causes'):
194 194 c.causes = base_response.causes
195 195
196 196 c.messages = helpers.flash.pop_messages(request=request)
197 197
198 198 exc_info = sys.exc_info()
199 199 c.exception_id = id(exc_info)
200 200 c.show_exception_id = isinstance(base_response, VCSServerUnavailable) \
201 201 or base_response.status_code > 499
202 202 c.exception_id_url = request.route_url(
203 203 'admin_settings_exception_tracker_show', exception_id=c.exception_id)
204 204
205 205 if c.show_exception_id:
206 206 store_exception(c.exception_id, exc_info)
207 207
208 208 response = render_to_response(
209 209 '/errors/error_document.mako', {'c': c, 'h': helpers}, request=request,
210 210 response=base_response)
211 211
212 212 return response
213 213
214 214
215 215 def includeme_first(config):
216 216 # redirect automatic browser favicon.ico requests to correct place
217 217 def favicon_redirect(context, request):
218 218 return HTTPFound(
219 219 request.static_path('rhodecode:public/images/favicon.ico'))
220 220
221 221 config.add_view(favicon_redirect, route_name='favicon')
222 222 config.add_route('favicon', '/favicon.ico')
223 223
224 224 def robots_redirect(context, request):
225 225 return HTTPFound(
226 226 request.static_path('rhodecode:public/robots.txt'))
227 227
228 228 config.add_view(robots_redirect, route_name='robots')
229 229 config.add_route('robots', '/robots.txt')
230 230
231 231 config.add_static_view(
232 232 '_static/deform', 'deform:static')
233 233 config.add_static_view(
234 234 '_static/rhodecode', path='rhodecode:public', cache_max_age=3600 * 24)
235 235
236 236
237 237 def includeme(config):
238 238 log.debug('Initializing main includeme from %s', os.path.basename(__file__))
239 239 settings = config.registry.settings
240 240 config.set_request_factory(Request)
241 241
242 242 # plugin information
243 243 config.registry.rhodecode_plugins = collections.OrderedDict()
244 244
245 245 config.add_directive(
246 246 'register_rhodecode_plugin', register_rhodecode_plugin)
247 247
248 248 config.add_directive('configure_celery', configure_celery)
249 249
250 250 if asbool(settings.get('appenlight', 'false')):
251 251 config.include('appenlight_client.ext.pyramid_tween')
252 252
253 253 load_all = should_load_all()
254 254
255 255 # Includes which are required. The application would fail without them.
256 256 config.include('pyramid_mako')
257 257 config.include('rhodecode.lib.rc_beaker')
258 258 config.include('rhodecode.lib.rc_cache')
259 259
260 260 config.include('rhodecode.apps._base.navigation')
261 261 config.include('rhodecode.apps._base.subscribers')
262 262 config.include('rhodecode.tweens')
263 263 config.include('rhodecode.authentication')
264 264
265 265 if load_all:
266 266 config.include('rhodecode.integrations')
267 267
268 268 if load_all:
269 269 # load CE authentication plugins
270 270 config.include('rhodecode.authentication.plugins.auth_crowd')
271 271 config.include('rhodecode.authentication.plugins.auth_headers')
272 272 config.include('rhodecode.authentication.plugins.auth_jasig_cas')
273 273 config.include('rhodecode.authentication.plugins.auth_ldap')
274 274 config.include('rhodecode.authentication.plugins.auth_pam')
275 275 config.include('rhodecode.authentication.plugins.auth_rhodecode')
276 276 config.include('rhodecode.authentication.plugins.auth_token')
277 277
278 278 # Auto discover authentication plugins and include their configuration.
279 279 if asbool(settings.get('auth_plugin.import_legacy_plugins', 'true')):
280 280 from rhodecode.authentication import discover_legacy_plugins
281 281 discover_legacy_plugins(config)
282 282
283 283 # apps
284 284 if load_all:
285 285 config.include('rhodecode.apps._base')
286 286 config.include('rhodecode.apps.hovercards')
287 287 config.include('rhodecode.apps.ops')
288 288 config.include('rhodecode.apps.admin')
289 289 config.include('rhodecode.apps.channelstream')
290 290 config.include('rhodecode.apps.file_store')
291 291 config.include('rhodecode.apps.login')
292 292 config.include('rhodecode.apps.home')
293 293 config.include('rhodecode.apps.journal')
294 294 config.include('rhodecode.apps.repository')
295 295 config.include('rhodecode.apps.repo_group')
296 296 config.include('rhodecode.apps.user_group')
297 297 config.include('rhodecode.apps.search')
298 298 config.include('rhodecode.apps.user_profile')
299 299 config.include('rhodecode.apps.user_group_profile')
300 300 config.include('rhodecode.apps.my_account')
301 301 config.include('rhodecode.apps.svn_support')
302 302 config.include('rhodecode.apps.ssh_support')
303 303 config.include('rhodecode.apps.gist')
304 304 config.include('rhodecode.apps.debug_style')
305 305 config.include('rhodecode.api')
306 306
307 307 config.add_route('rhodecode_support', 'https://rhodecode.com/help/', static=True)
308 308 config.add_translation_dirs('rhodecode:i18n/')
309 309 settings['default_locale_name'] = settings.get('lang', 'en')
310 310
311 311 # Add subscribers.
312 312 if load_all:
313 313 config.add_subscriber(inject_app_settings,
314 314 pyramid.events.ApplicationCreated)
315 315 config.add_subscriber(scan_repositories_if_enabled,
316 316 pyramid.events.ApplicationCreated)
317 317 config.add_subscriber(write_metadata_if_needed,
318 318 pyramid.events.ApplicationCreated)
319 config.add_subscriber(write_usage_data,
320 pyramid.events.ApplicationCreated)
319 321 config.add_subscriber(write_js_routes_if_enabled,
320 322 pyramid.events.ApplicationCreated)
321 323
322 324 # request custom methods
323 325 config.add_request_method(
324 326 'rhodecode.lib.partial_renderer.get_partial_renderer',
325 327 'get_partial_renderer')
326 328
327 329 config.add_request_method(
328 330 'rhodecode.lib.request_counter.get_request_counter',
329 331 'request_count')
330 332
331 333 # Set the authorization policy.
332 334 authz_policy = ACLAuthorizationPolicy()
333 335 config.set_authorization_policy(authz_policy)
334 336
335 337 # Set the default renderer for HTML templates to mako.
336 338 config.add_mako_renderer('.html')
337 339
338 340 config.add_renderer(
339 341 name='json_ext',
340 342 factory='rhodecode.lib.ext_json_renderer.pyramid_ext_json')
341 343
342 344 # include RhodeCode plugins
343 345 includes = aslist(settings.get('rhodecode.includes', []))
344 346 for inc in includes:
345 347 config.include(inc)
346 348
347 349 # custom not found view, if our pyramid app doesn't know how to handle
348 350 # the request pass it to potential VCS handling ap
349 351 config.add_notfound_view(not_found_view)
350 352 if not settings.get('debugtoolbar.enabled', False):
351 353 # disabled debugtoolbar handle all exceptions via the error_handlers
352 354 config.add_view(error_handler, context=Exception)
353 355
354 356 # all errors including 403/404/50X
355 357 config.add_view(error_handler, context=HTTPError)
356 358
357 359
358 360 def wrap_app_in_wsgi_middlewares(pyramid_app, config):
359 361 """
360 362 Apply outer WSGI middlewares around the application.
361 363 """
362 364 registry = config.registry
363 365 settings = registry.settings
364 366
365 367 # enable https redirects based on HTTP_X_URL_SCHEME set by proxy
366 368 pyramid_app = HttpsFixup(pyramid_app, settings)
367 369
368 370 pyramid_app, _ae_client = wrap_in_appenlight_if_enabled(
369 371 pyramid_app, settings)
370 372 registry.ae_client = _ae_client
371 373
372 374 if settings['gzip_responses']:
373 375 pyramid_app = make_gzip_middleware(
374 376 pyramid_app, settings, compress_level=1)
375 377
376 378 # this should be the outer most middleware in the wsgi stack since
377 379 # middleware like Routes make database calls
378 380 def pyramid_app_with_cleanup(environ, start_response):
379 381 try:
380 382 return pyramid_app(environ, start_response)
381 383 finally:
382 384 # Dispose current database session and rollback uncommitted
383 385 # transactions.
384 386 meta.Session.remove()
385 387
386 388 # In a single threaded mode server, on non sqlite db we should have
387 389 # '0 Current Checked out connections' at the end of a request,
388 390 # if not, then something, somewhere is leaving a connection open
389 391 pool = meta.Base.metadata.bind.engine.pool
390 392 log.debug('sa pool status: %s', pool.status())
391 393 log.debug('Request processing finalized')
392 394
393 395 return pyramid_app_with_cleanup
394 396
395 397
396 398 def sanitize_settings_and_apply_defaults(global_config, settings):
397 399 """
398 400 Applies settings defaults and does all type conversion.
399 401
400 402 We would move all settings parsing and preparation into this place, so that
401 403 we have only one place left which deals with this part. The remaining parts
402 404 of the application would start to rely fully on well prepared settings.
403 405
404 406 This piece would later be split up per topic to avoid a big fat monster
405 407 function.
406 408 """
407 409
408 410 settings.setdefault('rhodecode.edition', 'Community Edition')
409 411
410 412 if 'mako.default_filters' not in settings:
411 413 # set custom default filters if we don't have it defined
412 414 settings['mako.imports'] = 'from rhodecode.lib.base import h_filter'
413 415 settings['mako.default_filters'] = 'h_filter'
414 416
415 417 if 'mako.directories' not in settings:
416 418 mako_directories = settings.setdefault('mako.directories', [
417 419 # Base templates of the original application
418 420 'rhodecode:templates',
419 421 ])
420 422 log.debug(
421 423 "Using the following Mako template directories: %s",
422 424 mako_directories)
423 425
424 426 # NOTE(marcink): fix redis requirement for schema of connection since 3.X
425 427 if 'beaker.session.type' in settings and settings['beaker.session.type'] == 'ext:redis':
426 428 raw_url = settings['beaker.session.url']
427 429 if not raw_url.startswith(('redis://', 'rediss://', 'unix://')):
428 430 settings['beaker.session.url'] = 'redis://' + raw_url
429 431
430 432 # Default includes, possible to change as a user
431 433 pyramid_includes = settings.setdefault('pyramid.includes', [])
432 434 log.debug(
433 435 "Using the following pyramid.includes: %s",
434 436 pyramid_includes)
435 437
436 438 # TODO: johbo: Re-think this, usually the call to config.include
437 439 # should allow to pass in a prefix.
438 440 settings.setdefault('rhodecode.api.url', '/_admin/api')
439 441 settings.setdefault('__file__', global_config.get('__file__'))
440 442
441 443 # Sanitize generic settings.
442 444 _list_setting(settings, 'default_encoding', 'UTF-8')
443 445 _bool_setting(settings, 'is_test', 'false')
444 446 _bool_setting(settings, 'gzip_responses', 'false')
445 447
446 448 # Call split out functions that sanitize settings for each topic.
447 449 _sanitize_appenlight_settings(settings)
448 450 _sanitize_vcs_settings(settings)
449 451 _sanitize_cache_settings(settings)
450 452
451 453 # configure instance id
452 454 config_utils.set_instance_id(settings)
453 455
454 456 return settings
455 457
456 458
457 459 def enable_debug():
458 460 """
459 461 Helper to enable debug on running instance
460 462 :return:
461 463 """
462 464 import tempfile
463 465 import textwrap
464 466 import logging.config
465 467
466 468 ini_template = textwrap.dedent("""
467 469 #####################################
468 470 ### DEBUG LOGGING CONFIGURATION ####
469 471 #####################################
470 472 [loggers]
471 473 keys = root, sqlalchemy, beaker, celery, rhodecode, ssh_wrapper
472 474
473 475 [handlers]
474 476 keys = console, console_sql
475 477
476 478 [formatters]
477 479 keys = generic, color_formatter, color_formatter_sql
478 480
479 481 #############
480 482 ## LOGGERS ##
481 483 #############
482 484 [logger_root]
483 485 level = NOTSET
484 486 handlers = console
485 487
486 488 [logger_sqlalchemy]
487 489 level = INFO
488 490 handlers = console_sql
489 491 qualname = sqlalchemy.engine
490 492 propagate = 0
491 493
492 494 [logger_beaker]
493 495 level = DEBUG
494 496 handlers =
495 497 qualname = beaker.container
496 498 propagate = 1
497 499
498 500 [logger_rhodecode]
499 501 level = DEBUG
500 502 handlers =
501 503 qualname = rhodecode
502 504 propagate = 1
503 505
504 506 [logger_ssh_wrapper]
505 507 level = DEBUG
506 508 handlers =
507 509 qualname = ssh_wrapper
508 510 propagate = 1
509 511
510 512 [logger_celery]
511 513 level = DEBUG
512 514 handlers =
513 515 qualname = celery
514 516
515 517
516 518 ##############
517 519 ## HANDLERS ##
518 520 ##############
519 521
520 522 [handler_console]
521 523 class = StreamHandler
522 524 args = (sys.stderr, )
523 525 level = DEBUG
524 526 formatter = color_formatter
525 527
526 528 [handler_console_sql]
527 529 # "level = DEBUG" logs SQL queries and results.
528 530 # "level = INFO" logs SQL queries.
529 531 # "level = WARN" logs neither. (Recommended for production systems.)
530 532 class = StreamHandler
531 533 args = (sys.stderr, )
532 534 level = WARN
533 535 formatter = color_formatter_sql
534 536
535 537 ################
536 538 ## FORMATTERS ##
537 539 ################
538 540
539 541 [formatter_generic]
540 542 class = rhodecode.lib.logging_formatter.ExceptionAwareFormatter
541 543 format = %(asctime)s.%(msecs)03d [%(process)d] %(levelname)-5.5s [%(name)s] %(message)s | %(req_id)s
542 544 datefmt = %Y-%m-%d %H:%M:%S
543 545
544 546 [formatter_color_formatter]
545 547 class = rhodecode.lib.logging_formatter.ColorRequestTrackingFormatter
546 548 format = %(asctime)s.%(msecs)03d [%(process)d] %(levelname)-5.5s [%(name)s] %(message)s | %(req_id)s
547 549 datefmt = %Y-%m-%d %H:%M:%S
548 550
549 551 [formatter_color_formatter_sql]
550 552 class = rhodecode.lib.logging_formatter.ColorFormatterSql
551 553 format = %(asctime)s.%(msecs)03d [%(process)d] %(levelname)-5.5s [%(name)s] %(message)s
552 554 datefmt = %Y-%m-%d %H:%M:%S
553 555 """)
554 556
555 557 with tempfile.NamedTemporaryFile(prefix='rc_debug_logging_', suffix='.ini',
556 558 delete=False) as f:
557 559 log.info('Saved Temporary DEBUG config at %s', f.name)
558 560 f.write(ini_template)
559 561
560 562 logging.config.fileConfig(f.name)
561 563 log.debug('DEBUG MODE ON')
562 564 os.remove(f.name)
563 565
564 566
565 567 def _sanitize_appenlight_settings(settings):
566 568 _bool_setting(settings, 'appenlight', 'false')
567 569
568 570
569 571 def _sanitize_vcs_settings(settings):
570 572 """
571 573 Applies settings defaults and does type conversion for all VCS related
572 574 settings.
573 575 """
574 576 _string_setting(settings, 'vcs.svn.compatible_version', '')
575 577 _string_setting(settings, 'vcs.hooks.protocol', 'http')
576 578 _string_setting(settings, 'vcs.hooks.host', '127.0.0.1')
577 579 _string_setting(settings, 'vcs.scm_app_implementation', 'http')
578 580 _string_setting(settings, 'vcs.server', '')
579 581 _string_setting(settings, 'vcs.server.protocol', 'http')
580 582 _bool_setting(settings, 'startup.import_repos', 'false')
581 583 _bool_setting(settings, 'vcs.hooks.direct_calls', 'false')
582 584 _bool_setting(settings, 'vcs.server.enable', 'true')
583 585 _bool_setting(settings, 'vcs.start_server', 'false')
584 586 _list_setting(settings, 'vcs.backends', 'hg, git, svn')
585 587 _int_setting(settings, 'vcs.connection_timeout', 3600)
586 588
587 589 # Support legacy values of vcs.scm_app_implementation. Legacy
588 590 # configurations may use 'rhodecode.lib.middleware.utils.scm_app_http', or
589 591 # disabled since 4.13 'vcsserver.scm_app' which is now mapped to 'http'.
590 592 scm_app_impl = settings['vcs.scm_app_implementation']
591 593 if scm_app_impl in ['rhodecode.lib.middleware.utils.scm_app_http', 'vcsserver.scm_app']:
592 594 settings['vcs.scm_app_implementation'] = 'http'
593 595
594 596
595 597 def _sanitize_cache_settings(settings):
596 598 temp_store = tempfile.gettempdir()
597 599 default_cache_dir = os.path.join(temp_store, 'rc_cache')
598 600
599 601 # save default, cache dir, and use it for all backends later.
600 602 default_cache_dir = _string_setting(
601 603 settings,
602 604 'cache_dir',
603 605 default_cache_dir, lower=False, default_when_empty=True)
604 606
605 607 # ensure we have our dir created
606 608 if not os.path.isdir(default_cache_dir):
607 609 os.makedirs(default_cache_dir, mode=0o755)
608 610
609 611 # exception store cache
610 612 _string_setting(
611 613 settings,
612 614 'exception_tracker.store_path',
613 615 temp_store, lower=False, default_when_empty=True)
614 616 _bool_setting(
615 617 settings,
616 618 'exception_tracker.send_email',
617 619 'false')
618 620 _string_setting(
619 621 settings,
620 622 'exception_tracker.email_prefix',
621 623 '[RHODECODE ERROR]', lower=False, default_when_empty=True)
622 624
623 625 # cache_perms
624 626 _string_setting(
625 627 settings,
626 628 'rc_cache.cache_perms.backend',
627 629 'dogpile.cache.rc.file_namespace', lower=False)
628 630 _int_setting(
629 631 settings,
630 632 'rc_cache.cache_perms.expiration_time',
631 633 60)
632 634 _string_setting(
633 635 settings,
634 636 'rc_cache.cache_perms.arguments.filename',
635 637 os.path.join(default_cache_dir, 'rc_cache_1'), lower=False)
636 638
637 639 # cache_repo
638 640 _string_setting(
639 641 settings,
640 642 'rc_cache.cache_repo.backend',
641 643 'dogpile.cache.rc.file_namespace', lower=False)
642 644 _int_setting(
643 645 settings,
644 646 'rc_cache.cache_repo.expiration_time',
645 647 60)
646 648 _string_setting(
647 649 settings,
648 650 'rc_cache.cache_repo.arguments.filename',
649 651 os.path.join(default_cache_dir, 'rc_cache_2'), lower=False)
650 652
651 653 # cache_license
652 654 _string_setting(
653 655 settings,
654 656 'rc_cache.cache_license.backend',
655 657 'dogpile.cache.rc.file_namespace', lower=False)
656 658 _int_setting(
657 659 settings,
658 660 'rc_cache.cache_license.expiration_time',
659 661 5*60)
660 662 _string_setting(
661 663 settings,
662 664 'rc_cache.cache_license.arguments.filename',
663 665 os.path.join(default_cache_dir, 'rc_cache_3'), lower=False)
664 666
665 667 # cache_repo_longterm memory, 96H
666 668 _string_setting(
667 669 settings,
668 670 'rc_cache.cache_repo_longterm.backend',
669 671 'dogpile.cache.rc.memory_lru', lower=False)
670 672 _int_setting(
671 673 settings,
672 674 'rc_cache.cache_repo_longterm.expiration_time',
673 675 345600)
674 676 _int_setting(
675 677 settings,
676 678 'rc_cache.cache_repo_longterm.max_size',
677 679 10000)
678 680
679 681 # sql_cache_short
680 682 _string_setting(
681 683 settings,
682 684 'rc_cache.sql_cache_short.backend',
683 685 'dogpile.cache.rc.memory_lru', lower=False)
684 686 _int_setting(
685 687 settings,
686 688 'rc_cache.sql_cache_short.expiration_time',
687 689 30)
688 690 _int_setting(
689 691 settings,
690 692 'rc_cache.sql_cache_short.max_size',
691 693 10000)
692 694
693 695
694 696 def _int_setting(settings, name, default):
695 697 settings[name] = int(settings.get(name, default))
696 698 return settings[name]
697 699
698 700
699 701 def _bool_setting(settings, name, default):
700 702 input_val = settings.get(name, default)
701 703 if isinstance(input_val, unicode):
702 704 input_val = input_val.encode('utf8')
703 705 settings[name] = asbool(input_val)
704 706 return settings[name]
705 707
706 708
707 709 def _list_setting(settings, name, default):
708 710 raw_value = settings.get(name, default)
709 711
710 712 old_separator = ','
711 713 if old_separator in raw_value:
712 714 # If we get a comma separated list, pass it to our own function.
713 715 settings[name] = rhodecode_aslist(raw_value, sep=old_separator)
714 716 else:
715 717 # Otherwise we assume it uses pyramids space/newline separation.
716 718 settings[name] = aslist(raw_value)
717 719 return settings[name]
718 720
719 721
720 722 def _string_setting(settings, name, default, lower=True, default_when_empty=False):
721 723 value = settings.get(name, default)
722 724
723 725 if default_when_empty and not value:
724 726 # use default value when value is empty
725 727 value = default
726 728
727 729 if lower:
728 730 value = value.lower()
729 731 settings[name] = value
730 732 return settings[name]
731 733
732 734
733 735 def _substitute_values(mapping, substitutions):
734 736 result = {}
735 737
736 738 try:
737 739 for key, value in mapping.items():
738 740 # initialize without substitution first
739 741 result[key] = value
740 742
741 743 # Note: Cannot use regular replacements, since they would clash
742 744 # with the implementation of ConfigParser. Using "format" instead.
743 745 try:
744 746 result[key] = value.format(**substitutions)
745 747 except KeyError as e:
746 748 env_var = '{}'.format(e.args[0])
747 749
748 750 msg = 'Failed to substitute: `{key}={{{var}}}` with environment entry. ' \
749 751 'Make sure your environment has {var} set, or remove this ' \
750 752 'variable from config file'.format(key=key, var=env_var)
751 753
752 754 if env_var.startswith('ENV_'):
753 755 raise ValueError(msg)
754 756 else:
755 757 log.warning(msg)
756 758
757 759 except ValueError as e:
758 760 log.warning('Failed to substitute ENV variable: %s', e)
759 761 result = mapping
760 762
761 763 return result
@@ -1,328 +1,395 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 import io
21 import math
21 22 import re
22 23 import os
23 24 import datetime
24 25 import logging
25 26 import Queue
26 27 import subprocess32
27 28
28 29
29 30 from dateutil.parser import parse
30 31 from pyramid.threadlocal import get_current_request
31 32 from pyramid.interfaces import IRoutesMapper
32 33 from pyramid.settings import asbool
33 34 from pyramid.path import AssetResolver
34 35 from threading import Thread
35 36
36 37 from rhodecode.translation import _ as tsf
37 38 from rhodecode.config.jsroutes import generate_jsroutes_content
38 39 from rhodecode.lib import auth
39 40 from rhodecode.lib.base import get_auth_user
40 41
41 42 import rhodecode
42 43
43 44
44 45 log = logging.getLogger(__name__)
45 46
46 47
47 48 def add_renderer_globals(event):
48 49 from rhodecode.lib import helpers
49 50
50 51 # TODO: When executed in pyramid view context the request is not available
51 52 # in the event. Find a better solution to get the request.
52 53 request = event['request'] or get_current_request()
53 54
54 55 # Add Pyramid translation as '_' to context
55 56 event['_'] = request.translate
56 57 event['_ungettext'] = request.plularize
57 58 event['h'] = helpers
58 59
59 60
60 61 def add_localizer(event):
61 62 request = event.request
62 63 localizer = request.localizer
63 64
64 65 def auto_translate(*args, **kwargs):
65 66 return localizer.translate(tsf(*args, **kwargs))
66 67
67 68 request.translate = auto_translate
68 69 request.plularize = localizer.pluralize
69 70
70 71
71 72 def set_user_lang(event):
72 73 request = event.request
73 74 cur_user = getattr(request, 'user', None)
74 75
75 76 if cur_user:
76 77 user_lang = cur_user.get_instance().user_data.get('language')
77 78 if user_lang:
78 79 log.debug('lang: setting current user:%s language to: %s', cur_user, user_lang)
79 80 event.request._LOCALE_ = user_lang
80 81
81 82
82 83 def add_request_user_context(event):
83 84 """
84 85 Adds auth user into request context
85 86 """
86 87 request = event.request
87 88 # access req_id as soon as possible
88 89 req_id = request.req_id
89 90
90 91 if hasattr(request, 'vcs_call'):
91 92 # skip vcs calls
92 93 return
93 94
94 95 if hasattr(request, 'rpc_method'):
95 96 # skip api calls
96 97 return
97 98
98 99 auth_user, auth_token = get_auth_user(request)
99 100 request.user = auth_user
100 101 request.user_auth_token = auth_token
101 102 request.environ['rc_auth_user'] = auth_user
102 103 request.environ['rc_auth_user_id'] = auth_user.user_id
103 104 request.environ['rc_req_id'] = req_id
104 105
105 106
106 107 def inject_app_settings(event):
107 108 settings = event.app.registry.settings
108 109 # inject info about available permissions
109 110 auth.set_available_permissions(settings)
110 111
111 112
112 113 def scan_repositories_if_enabled(event):
113 114 """
114 115 This is subscribed to the `pyramid.events.ApplicationCreated` event. It
115 116 does a repository scan if enabled in the settings.
116 117 """
117 118 settings = event.app.registry.settings
118 119 vcs_server_enabled = settings['vcs.server.enable']
119 120 import_on_startup = settings['startup.import_repos']
120 121 if vcs_server_enabled and import_on_startup:
121 122 from rhodecode.model.scm import ScmModel
122 123 from rhodecode.lib.utils import repo2db_mapper, get_rhodecode_base_path
123 124 repositories = ScmModel().repo_scan(get_rhodecode_base_path())
124 125 repo2db_mapper(repositories, remove_obsolete=False)
125 126
126 127
127 128 def write_metadata_if_needed(event):
128 129 """
129 130 Writes upgrade metadata
130 131 """
131 132 import rhodecode
132 133 from rhodecode.lib import system_info
133 134 from rhodecode.lib import ext_json
134 135
135 136 fname = '.rcmetadata.json'
136 137 ini_loc = os.path.dirname(rhodecode.CONFIG.get('__file__'))
137 138 metadata_destination = os.path.join(ini_loc, fname)
138 139
139 140 def get_update_age():
140 141 now = datetime.datetime.utcnow()
141 142
142 143 with open(metadata_destination, 'rb') as f:
143 144 data = ext_json.json.loads(f.read())
144 145 if 'created_on' in data:
145 146 update_date = parse(data['created_on'])
146 147 diff = now - update_date
147 148 return diff.total_seconds() / 60.0
148 149
149 150 return 0
150 151
151 152 def write():
152 153 configuration = system_info.SysInfo(
153 154 system_info.rhodecode_config)()['value']
154 155 license_token = configuration['config']['license_token']
155 156
156 157 setup = dict(
157 158 workers=configuration['config']['server:main'].get(
158 159 'workers', '?'),
159 160 worker_type=configuration['config']['server:main'].get(
160 161 'worker_class', 'sync'),
161 162 )
162 163 dbinfo = system_info.SysInfo(system_info.database_info)()['value']
163 164 del dbinfo['url']
164 165
165 166 metadata = dict(
166 167 desc='upgrade metadata info',
167 168 license_token=license_token,
168 169 created_on=datetime.datetime.utcnow().isoformat(),
169 170 usage=system_info.SysInfo(system_info.usage_info)()['value'],
170 171 platform=system_info.SysInfo(system_info.platform_type)()['value'],
171 172 database=dbinfo,
172 173 cpu=system_info.SysInfo(system_info.cpu)()['value'],
173 174 memory=system_info.SysInfo(system_info.memory)()['value'],
174 175 setup=setup
175 176 )
176 177
177 178 with open(metadata_destination, 'wb') as f:
178 179 f.write(ext_json.json.dumps(metadata))
179 180
180 181 settings = event.app.registry.settings
181 182 if settings.get('metadata.skip'):
182 183 return
183 184
184 185 # only write this every 24h, workers restart caused unwanted delays
185 186 try:
186 187 age_in_min = get_update_age()
187 188 except Exception:
188 189 age_in_min = 0
189 190
190 191 if age_in_min > 60 * 60 * 24:
191 192 return
192 193
193 194 try:
194 195 write()
195 196 except Exception:
196 197 pass
197 198
198 199
200 def write_usage_data(event):
201 import rhodecode
202 from rhodecode.lib import system_info
203 from rhodecode.lib import ext_json
204
205 settings = event.app.registry.settings
206 instance_tag = settings.get('metadata.write_usage_tag')
207 if not settings.get('metadata.write_usage'):
208 return
209
210 def get_update_age(dest_file):
211 now = datetime.datetime.utcnow()
212
213 with open(dest_file, 'rb') as f:
214 data = ext_json.json.loads(f.read())
215 if 'created_on' in data:
216 update_date = parse(data['created_on'])
217 diff = now - update_date
218 return math.ceil(diff.total_seconds() / 60.0)
219
220 return 0
221
222 utc_date = datetime.datetime.utcnow()
223 hour_quarter = int(math.ceil((utc_date.hour + utc_date.minute/60.0) / 6.))
224 fname = '.rc_usage_{date.year}{date.month:02d}{date.day:02d}_{hour}.json'.format(
225 date=utc_date, hour=hour_quarter)
226 ini_loc = os.path.dirname(rhodecode.CONFIG.get('__file__'))
227
228 usage_dir = os.path.join(ini_loc, '.rcusage')
229 if not os.path.isdir(usage_dir):
230 os.makedirs(usage_dir)
231 usage_metadata_destination = os.path.join(usage_dir, fname)
232
233 try:
234 age_in_min = get_update_age(usage_metadata_destination)
235 except Exception:
236 age_in_min = 0
237
238 # write every 6th hour
239 if age_in_min and age_in_min < 60 * 6:
240 log.debug('Usage file created %s minutes ago, skipping (threashold: %s)...',
241 age_in_min, 60 * 6)
242 return
243
244 def write(dest_file):
245 configuration = system_info.SysInfo(system_info.rhodecode_config)()['value']
246 license_token = configuration['config']['license_token']
247
248 metadata = dict(
249 desc='Usage data',
250 instance_tag=instance_tag,
251 license_token=license_token,
252 created_on=datetime.datetime.utcnow().isoformat(),
253 usage=system_info.SysInfo(system_info.usage_info)()['value'],
254 )
255
256 with open(dest_file, 'wb') as f:
257 f.write(ext_json.json.dumps(metadata, indent=2, sort_keys=True))
258
259 try:
260 log.debug('Writing usage file at: %s', usage_metadata_destination)
261 write(usage_metadata_destination)
262 except Exception:
263 pass
264
265
199 266 def write_js_routes_if_enabled(event):
200 267 registry = event.app.registry
201 268
202 269 mapper = registry.queryUtility(IRoutesMapper)
203 270 _argument_prog = re.compile('\{(.*?)\}|:\((.*)\)')
204 271
205 272 def _extract_route_information(route):
206 273 """
207 274 Convert a route into tuple(name, path, args), eg:
208 275 ('show_user', '/profile/%(username)s', ['username'])
209 276 """
210 277
211 278 routepath = route.pattern
212 279 pattern = route.pattern
213 280
214 281 def replace(matchobj):
215 282 if matchobj.group(1):
216 283 return "%%(%s)s" % matchobj.group(1).split(':')[0]
217 284 else:
218 285 return "%%(%s)s" % matchobj.group(2)
219 286
220 287 routepath = _argument_prog.sub(replace, routepath)
221 288
222 289 if not routepath.startswith('/'):
223 290 routepath = '/'+routepath
224 291
225 292 return (
226 293 route.name,
227 294 routepath,
228 295 [(arg[0].split(':')[0] if arg[0] != '' else arg[1])
229 296 for arg in _argument_prog.findall(pattern)]
230 297 )
231 298
232 299 def get_routes():
233 300 # pyramid routes
234 301 for route in mapper.get_routes():
235 302 if not route.name.startswith('__'):
236 303 yield _extract_route_information(route)
237 304
238 305 if asbool(registry.settings.get('generate_js_files', 'false')):
239 306 static_path = AssetResolver().resolve('rhodecode:public').abspath()
240 307 jsroutes = get_routes()
241 308 jsroutes_file_content = generate_jsroutes_content(jsroutes)
242 309 jsroutes_file_path = os.path.join(
243 310 static_path, 'js', 'rhodecode', 'routes.js')
244 311
245 312 try:
246 313 with io.open(jsroutes_file_path, 'w', encoding='utf-8') as f:
247 314 f.write(jsroutes_file_content)
248 315 except Exception:
249 316 log.exception('Failed to write routes.js into %s', jsroutes_file_path)
250 317
251 318
252 319 class Subscriber(object):
253 320 """
254 321 Base class for subscribers to the pyramid event system.
255 322 """
256 323 def __call__(self, event):
257 324 self.run(event)
258 325
259 326 def run(self, event):
260 327 raise NotImplementedError('Subclass has to implement this.')
261 328
262 329
263 330 class AsyncSubscriber(Subscriber):
264 331 """
265 332 Subscriber that handles the execution of events in a separate task to not
266 333 block the execution of the code which triggers the event. It puts the
267 334 received events into a queue from which the worker process takes them in
268 335 order.
269 336 """
270 337 def __init__(self):
271 338 self._stop = False
272 339 self._eventq = Queue.Queue()
273 340 self._worker = self.create_worker()
274 341 self._worker.start()
275 342
276 343 def __call__(self, event):
277 344 self._eventq.put(event)
278 345
279 346 def create_worker(self):
280 347 worker = Thread(target=self.do_work)
281 348 worker.daemon = True
282 349 return worker
283 350
284 351 def stop_worker(self):
285 352 self._stop = False
286 353 self._eventq.put(None)
287 354 self._worker.join()
288 355
289 356 def do_work(self):
290 357 while not self._stop:
291 358 event = self._eventq.get()
292 359 if event is not None:
293 360 self.run(event)
294 361
295 362
296 363 class AsyncSubprocessSubscriber(AsyncSubscriber):
297 364 """
298 365 Subscriber that uses the subprocess32 module to execute a command if an
299 366 event is received. Events are handled asynchronously.
300 367 """
301 368
302 369 def __init__(self, cmd, timeout=None):
303 370 super(AsyncSubprocessSubscriber, self).__init__()
304 371 self._cmd = cmd
305 372 self._timeout = timeout
306 373
307 374 def run(self, event):
308 375 cmd = self._cmd
309 376 timeout = self._timeout
310 377 log.debug('Executing command %s.', cmd)
311 378
312 379 try:
313 380 output = subprocess32.check_output(
314 381 cmd, timeout=timeout, stderr=subprocess32.STDOUT)
315 382 log.debug('Command finished %s', cmd)
316 383 if output:
317 384 log.debug('Command output: %s', output)
318 385 except subprocess32.TimeoutExpired as e:
319 386 log.exception('Timeout while executing command.')
320 387 if e.output:
321 388 log.error('Command output: %s', e.output)
322 389 except subprocess32.CalledProcessError as e:
323 390 log.exception('Error while executing command.')
324 391 if e.output:
325 392 log.error('Command output: %s', e.output)
326 393 except:
327 394 log.exception(
328 395 'Exception while executing command %s.', cmd)
General Comments 0
You need to be logged in to leave comments. Login now