##// END OF EJS Templates
core: cleanup settings cleanups on pyramid app.
marcink -
r2316:41ee882b default
parent child Browse files
Show More
@@ -1,551 +1,542 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2017 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 """
22 22 Pylons middleware initialization
23 23 """
24 24 import logging
25 25 import traceback
26 26 from collections import OrderedDict
27 27
28 28 from paste.registry import RegistryManager
29 29 from paste.gzipper import make_gzip_middleware
30 30 from pylons.wsgiapp import PylonsApp
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.wsgi import wsgiapp
35 35 from pyramid.httpexceptions import (
36 36 HTTPException, HTTPError, HTTPInternalServerError, HTTPFound)
37 37 from pyramid.events import ApplicationCreated
38 38 from pyramid.renderers import render_to_response
39 39 from routes.middleware import RoutesMiddleware
40 40 import rhodecode
41 41
42 42 from rhodecode.model import meta
43 43 from rhodecode.config import patches
44 44 from rhodecode.config import utils as config_utils
45 45 from rhodecode.config.routing import STATIC_FILE_PREFIX
46 46 from rhodecode.config.environment import (
47 47 load_environment, load_pyramid_environment)
48 48
49 49 from rhodecode.lib.vcs import VCSCommunicationError
50 50 from rhodecode.lib.exceptions import VCSServerUnavailable
51 51 from rhodecode.lib.middleware.appenlight import wrap_in_appenlight_if_enabled
52 52 from rhodecode.lib.middleware.error_handling import (
53 53 PylonsErrorHandlingMiddleware)
54 54 from rhodecode.lib.middleware.https_fixup import HttpsFixup
55 55 from rhodecode.lib.middleware.vcs import VCSMiddleware
56 56 from rhodecode.lib.plugins.utils import register_rhodecode_plugin
57 57 from rhodecode.lib.utils2 import aslist as rhodecode_aslist, AttributeDict
58 58 from rhodecode.subscribers import (
59 59 scan_repositories_if_enabled, write_js_routes_if_enabled,
60 60 write_metadata_if_needed, inject_app_settings)
61 61
62 62
63 63 log = logging.getLogger(__name__)
64 64
65 65
66 66 # this is used to avoid avoid the route lookup overhead in routesmiddleware
67 67 # for certain routes which won't go to pylons to - eg. static files, debugger
68 68 # it is only needed for the pylons migration and can be removed once complete
69 69 class SkippableRoutesMiddleware(RoutesMiddleware):
70 70 """ Routes middleware that allows you to skip prefixes """
71 71
72 72 def __init__(self, *args, **kw):
73 73 self.skip_prefixes = kw.pop('skip_prefixes', [])
74 74 super(SkippableRoutesMiddleware, self).__init__(*args, **kw)
75 75
76 76 def __call__(self, environ, start_response):
77 77 for prefix in self.skip_prefixes:
78 78 if environ['PATH_INFO'].startswith(prefix):
79 79 # added to avoid the case when a missing /_static route falls
80 80 # through to pylons and causes an exception as pylons is
81 81 # expecting wsgiorg.routingargs to be set in the environ
82 82 # by RoutesMiddleware.
83 83 if 'wsgiorg.routing_args' not in environ:
84 84 environ['wsgiorg.routing_args'] = (None, {})
85 85 return self.app(environ, start_response)
86 86
87 87 return super(SkippableRoutesMiddleware, self).__call__(
88 88 environ, start_response)
89 89
90 90
91 91 def make_app(global_conf, static_files=True, **app_conf):
92 92 """Create a Pylons WSGI application and return it
93 93
94 94 ``global_conf``
95 95 The inherited configuration for this application. Normally from
96 96 the [DEFAULT] section of the Paste ini file.
97 97
98 98 ``app_conf``
99 99 The application's local configuration. Normally specified in
100 100 the [app:<name>] section of the Paste ini file (where <name>
101 101 defaults to main).
102 102
103 103 """
104 104 # Apply compatibility patches
105 105 patches.kombu_1_5_1_python_2_7_11()
106 106 patches.inspect_getargspec()
107 107
108 108 # Configure the Pylons environment
109 109 config = load_environment(global_conf, app_conf)
110 110
111 111 # The Pylons WSGI app
112 112 app = PylonsApp(config=config)
113 113
114 114 # Establish the Registry for this application
115 115 app = RegistryManager(app)
116 116
117 117 app.config = config
118 118
119 119 return app
120 120
121 121
122 122 def make_pyramid_app(global_config, **settings):
123 123 """
124 124 Constructs the WSGI application based on Pyramid and wraps the Pylons based
125 125 application.
126 126
127 127 Specials:
128 128
129 129 * We migrate from Pylons to Pyramid. While doing this, we keep both
130 130 frameworks functional. This involves moving some WSGI middlewares around
131 131 and providing access to some data internals, so that the old code is
132 132 still functional.
133 133
134 134 * The application can also be integrated like a plugin via the call to
135 135 `includeme`. This is accompanied with the other utility functions which
136 136 are called. Changing this should be done with great care to not break
137 137 cases when these fragments are assembled from another place.
138 138
139 139 """
140 # The edition string should be available in pylons too, so we add it here
141 # before copying the settings.
142 settings.setdefault('rhodecode.edition', 'Community Edition')
143
144 # As long as our Pylons application does expect "unprepared" settings, make
145 # sure that we keep an unmodified copy. This avoids unintentional change of
146 # behavior in the old application.
147 settings_pylons = settings.copy()
148
149 140 sanitize_settings_and_apply_defaults(settings)
150 141
151 142 config = Configurator(settings=settings)
152 143 load_pyramid_environment(global_config, settings)
153 144
154 add_pylons_compat_data(config.registry, global_config, settings_pylons)
145 add_pylons_compat_data(config.registry, global_config, settings.copy())
155 146
156 147 includeme_first(config)
157 148 includeme(config)
158 149
159 150 pyramid_app = config.make_wsgi_app()
160 151 pyramid_app = wrap_app_in_wsgi_middlewares(pyramid_app, config)
161 152 pyramid_app.config = config
162 153
163 154 # creating the app uses a connection - return it after we are done
164 155 meta.Session.remove()
165 156
166 157 return pyramid_app
167 158
168 159
169 160 def make_not_found_view(config):
170 161 """
171 162 This creates the view which should be registered as not-found-view to
172 163 pyramid. Basically it contains of the old pylons app, converted to a view.
173 164 Additionally it is wrapped by some other middlewares.
174 165 """
175 166 settings = config.registry.settings
176 167 vcs_server_enabled = settings['vcs.server.enable']
177 168
178 169 # Make pylons app from unprepared settings.
179 170 pylons_app = make_app(
180 171 config.registry._pylons_compat_global_config,
181 172 **config.registry._pylons_compat_settings)
182 173 config.registry._pylons_compat_config = pylons_app.config
183 174
184 175 # Appenlight monitoring.
185 176 pylons_app, appenlight_client = wrap_in_appenlight_if_enabled(
186 177 pylons_app, settings)
187 178
188 179 # The pylons app is executed inside of the pyramid 404 exception handler.
189 180 # Exceptions which are raised inside of it are not handled by pyramid
190 181 # again. Therefore we add a middleware that invokes the error handler in
191 182 # case of an exception or error response. This way we return proper error
192 183 # HTML pages in case of an error.
193 184 reraise = (settings.get('debugtoolbar.enabled', False) or
194 185 rhodecode.disable_error_handler)
195 186 pylons_app = PylonsErrorHandlingMiddleware(
196 187 pylons_app, error_handler, reraise)
197 188
198 189 # The VCSMiddleware shall operate like a fallback if pyramid doesn't find a
199 190 # view to handle the request. Therefore it is wrapped around the pylons
200 191 # app. It has to be outside of the error handling otherwise error responses
201 192 # from the vcsserver are converted to HTML error pages. This confuses the
202 193 # command line tools and the user won't get a meaningful error message.
203 194 if vcs_server_enabled:
204 195 pylons_app = VCSMiddleware(
205 196 pylons_app, settings, appenlight_client, registry=config.registry)
206 197
207 198 # Convert WSGI app to pyramid view and return it.
208 199 return wsgiapp(pylons_app)
209 200
210 201
211 202 def add_pylons_compat_data(registry, global_config, settings):
212 203 """
213 204 Attach data to the registry to support the Pylons integration.
214 205 """
215 206 registry._pylons_compat_global_config = global_config
216 207 registry._pylons_compat_settings = settings
217 208
218 209
219 210 def error_handler(exception, request):
220 211 import rhodecode
221 212 from rhodecode.lib import helpers
222 213
223 214 rhodecode_title = rhodecode.CONFIG.get('rhodecode_title') or 'RhodeCode'
224 215
225 216 base_response = HTTPInternalServerError()
226 217 # prefer original exception for the response since it may have headers set
227 218 if isinstance(exception, HTTPException):
228 219 base_response = exception
229 220 elif isinstance(exception, VCSCommunicationError):
230 221 base_response = VCSServerUnavailable()
231 222
232 223 def is_http_error(response):
233 224 # error which should have traceback
234 225 return response.status_code > 499
235 226
236 227 if is_http_error(base_response):
237 228 log.exception(
238 229 'error occurred handling this request for path: %s', request.path)
239 230
240 231 error_explanation = base_response.explanation or str(base_response)
241 232 if base_response.status_code == 404:
242 233 error_explanation += " Or you don't have permission to access it."
243 234 c = AttributeDict()
244 235 c.error_message = base_response.status
245 236 c.error_explanation = error_explanation
246 237 c.visual = AttributeDict()
247 238
248 239 c.visual.rhodecode_support_url = (
249 240 request.registry.settings.get('rhodecode_support_url') or
250 241 request.route_url('rhodecode_support')
251 242 )
252 243 c.redirect_time = 0
253 244 c.rhodecode_name = rhodecode_title
254 245 if not c.rhodecode_name:
255 246 c.rhodecode_name = 'Rhodecode'
256 247
257 248 c.causes = []
258 249 if is_http_error(base_response):
259 250 c.causes.append('Server is overloaded.')
260 251 c.causes.append('Server database connection is lost.')
261 252 c.causes.append('Server expected unhandled error.')
262 253
263 254 if hasattr(base_response, 'causes'):
264 255 c.causes = base_response.causes
265 256
266 257 c.messages = helpers.flash.pop_messages(request=request)
267 258 c.traceback = traceback.format_exc()
268 259 response = render_to_response(
269 260 '/errors/error_document.mako', {'c': c, 'h': helpers}, request=request,
270 261 response=base_response)
271 262
272 263 return response
273 264
274 265
275 266 def includeme(config):
276 267 settings = config.registry.settings
277 268
278 269 # plugin information
279 270 config.registry.rhodecode_plugins = OrderedDict()
280 271
281 272 config.add_directive(
282 273 'register_rhodecode_plugin', register_rhodecode_plugin)
283 274
284 275 if asbool(settings.get('appenlight', 'false')):
285 276 config.include('appenlight_client.ext.pyramid_tween')
286 277
287 if 'mako.default_filters' not in settings:
288 # set custom default filters if we don't have it defined
289 settings['mako.imports'] = 'from rhodecode.lib.base import h_filter'
290 settings['mako.default_filters'] = 'h_filter'
291
292 278 # Includes which are required. The application would fail without them.
293 279 config.include('pyramid_mako')
294 280 config.include('pyramid_beaker')
295 281
296 282 config.include('rhodecode.authentication')
297 283 config.include('rhodecode.integrations')
298 284
299 285 # apps
300 286 config.include('rhodecode.apps._base')
301 287 config.include('rhodecode.apps.ops')
302 288
303 289 config.include('rhodecode.apps.admin')
304 290 config.include('rhodecode.apps.channelstream')
305 291 config.include('rhodecode.apps.login')
306 292 config.include('rhodecode.apps.home')
307 293 config.include('rhodecode.apps.journal')
308 294 config.include('rhodecode.apps.repository')
309 295 config.include('rhodecode.apps.repo_group')
310 296 config.include('rhodecode.apps.user_group')
311 297 config.include('rhodecode.apps.search')
312 298 config.include('rhodecode.apps.user_profile')
313 299 config.include('rhodecode.apps.my_account')
314 300 config.include('rhodecode.apps.svn_support')
315 301 config.include('rhodecode.apps.ssh_support')
316 302 config.include('rhodecode.apps.gist')
317 303
318 304 config.include('rhodecode.apps.debug_style')
319 305 config.include('rhodecode.tweens')
320 306 config.include('rhodecode.api')
321 307
322 308 config.add_route(
323 309 'rhodecode_support', 'https://rhodecode.com/help/', static=True)
324 310
325 311 config.add_translation_dirs('rhodecode:i18n/')
326 312 settings['default_locale_name'] = settings.get('lang', 'en')
327 313
328 314 # Add subscribers.
329 315 config.add_subscriber(inject_app_settings, ApplicationCreated)
330 316 config.add_subscriber(scan_repositories_if_enabled, ApplicationCreated)
331 317 config.add_subscriber(write_metadata_if_needed, ApplicationCreated)
332 318 config.add_subscriber(write_js_routes_if_enabled, ApplicationCreated)
333 319
334 320 config.add_request_method(
335 321 'rhodecode.lib.partial_renderer.get_partial_renderer',
336 322 'get_partial_renderer')
337 323
338 324 # events
339 325 # TODO(marcink): this should be done when pyramid migration is finished
340 326 # config.add_subscriber(
341 327 # 'rhodecode.integrations.integrations_event_handler',
342 328 # 'rhodecode.events.RhodecodeEvent')
343 329
344 330 # Set the authorization policy.
345 331 authz_policy = ACLAuthorizationPolicy()
346 332 config.set_authorization_policy(authz_policy)
347 333
348 334 # Set the default renderer for HTML templates to mako.
349 335 config.add_mako_renderer('.html')
350 336
351 337 config.add_renderer(
352 338 name='json_ext',
353 339 factory='rhodecode.lib.ext_json_renderer.pyramid_ext_json')
354 340
355 341 # include RhodeCode plugins
356 342 includes = aslist(settings.get('rhodecode.includes', []))
357 343 for inc in includes:
358 344 config.include(inc)
359 345
360 346 # This is the glue which allows us to migrate in chunks. By registering the
361 347 # pylons based application as the "Not Found" view in Pyramid, we will
362 348 # fallback to the old application each time the new one does not yet know
363 349 # how to handle a request.
364 350 config.add_notfound_view(make_not_found_view(config))
365 351
366 352 if not settings.get('debugtoolbar.enabled', False):
367 353 # disabled debugtoolbar handle all exceptions via the error_handlers
368 354 config.add_view(error_handler, context=Exception)
369 355
370 356 config.add_view(error_handler, context=HTTPError)
371 357
372 358
373 359 def includeme_first(config):
374 360 # redirect automatic browser favicon.ico requests to correct place
375 361 def favicon_redirect(context, request):
376 362 return HTTPFound(
377 363 request.static_path('rhodecode:public/images/favicon.ico'))
378 364
379 365 config.add_view(favicon_redirect, route_name='favicon')
380 366 config.add_route('favicon', '/favicon.ico')
381 367
382 368 def robots_redirect(context, request):
383 369 return HTTPFound(
384 370 request.static_path('rhodecode:public/robots.txt'))
385 371
386 372 config.add_view(robots_redirect, route_name='robots')
387 373 config.add_route('robots', '/robots.txt')
388 374
389 375 config.add_static_view(
390 376 '_static/deform', 'deform:static')
391 377 config.add_static_view(
392 378 '_static/rhodecode', path='rhodecode:public', cache_max_age=3600 * 24)
393 379
394 380
395 381 def wrap_app_in_wsgi_middlewares(pyramid_app, config):
396 382 """
397 383 Apply outer WSGI middlewares around the application.
398 384
399 385 Part of this has been moved up from the Pylons layer, so that the
400 386 data is also available if old Pylons code is hit through an already ported
401 387 view.
402 388 """
403 389 settings = config.registry.settings
404 390
405 391 # enable https redirects based on HTTP_X_URL_SCHEME set by proxy
406 392 pyramid_app = HttpsFixup(pyramid_app, settings)
407 393
408 394 # Add RoutesMiddleware to support the pylons compatibility tween during
409 395 # migration to pyramid.
410 396
411 397 # TODO(marcink): remove after migration to pyramid
412 398 if hasattr(config.registry, '_pylons_compat_config'):
413 399 routes_map = config.registry._pylons_compat_config['routes.map']
414 400 pyramid_app = SkippableRoutesMiddleware(
415 401 pyramid_app, routes_map,
416 402 skip_prefixes=(STATIC_FILE_PREFIX, '/_debug_toolbar'))
417 403
418 404 pyramid_app, _ = wrap_in_appenlight_if_enabled(pyramid_app, settings)
419 405
420 406 if settings['gzip_responses']:
421 407 pyramid_app = make_gzip_middleware(
422 408 pyramid_app, settings, compress_level=1)
423 409
424 410 # this should be the outer most middleware in the wsgi stack since
425 411 # middleware like Routes make database calls
426 412 def pyramid_app_with_cleanup(environ, start_response):
427 413 try:
428 414 return pyramid_app(environ, start_response)
429 415 finally:
430 416 # Dispose current database session and rollback uncommitted
431 417 # transactions.
432 418 meta.Session.remove()
433 419
434 420 # In a single threaded mode server, on non sqlite db we should have
435 421 # '0 Current Checked out connections' at the end of a request,
436 422 # if not, then something, somewhere is leaving a connection open
437 423 pool = meta.Base.metadata.bind.engine.pool
438 424 log.debug('sa pool status: %s', pool.status())
439 425
440 426 return pyramid_app_with_cleanup
441 427
442 428
443 429 def sanitize_settings_and_apply_defaults(settings):
444 430 """
445 431 Applies settings defaults and does all type conversion.
446 432
447 433 We would move all settings parsing and preparation into this place, so that
448 434 we have only one place left which deals with this part. The remaining parts
449 435 of the application would start to rely fully on well prepared settings.
450 436
451 437 This piece would later be split up per topic to avoid a big fat monster
452 438 function.
453 439 """
454 440
455 # Pyramid's mako renderer has to search in the templates folder so that the
456 # old templates still work. Ported and new templates are expected to use
457 # real asset specifications for the includes.
458 mako_directories = settings.setdefault('mako.directories', [
459 # Base templates of the original Pylons application
460 'rhodecode:templates',
461 ])
462 log.debug(
463 "Using the following Mako template directories: %s",
464 mako_directories)
441 settings.setdefault('rhodecode.edition', 'Community Edition')
442
443 if 'mako.default_filters' not in settings:
444 # set custom default filters if we don't have it defined
445 settings['mako.imports'] = 'from rhodecode.lib.base import h_filter'
446 settings['mako.default_filters'] = 'h_filter'
447
448 if 'mako.directories' not in settings:
449 mako_directories = settings.setdefault('mako.directories', [
450 # Base templates of the original application
451 'rhodecode:templates',
452 ])
453 log.debug(
454 "Using the following Mako template directories: %s",
455 mako_directories)
465 456
466 457 # Default includes, possible to change as a user
467 458 pyramid_includes = settings.setdefault('pyramid.includes', [
468 459 'rhodecode.lib.middleware.request_wrapper',
469 460 ])
470 461 log.debug(
471 462 "Using the following pyramid.includes: %s",
472 463 pyramid_includes)
473 464
474 465 # TODO: johbo: Re-think this, usually the call to config.include
475 466 # should allow to pass in a prefix.
476 467 settings.setdefault('rhodecode.api.url', '/_admin/api')
477 468
478 469 # Sanitize generic settings.
479 470 _list_setting(settings, 'default_encoding', 'UTF-8')
480 471 _bool_setting(settings, 'is_test', 'false')
481 472 _bool_setting(settings, 'gzip_responses', 'false')
482 473
483 474 # Call split out functions that sanitize settings for each topic.
484 475 _sanitize_appenlight_settings(settings)
485 476 _sanitize_vcs_settings(settings)
486 477
487 478 # configure instance id
488 479 config_utils.set_instance_id(settings)
489 480
490 481 return settings
491 482
492 483
493 484 def _sanitize_appenlight_settings(settings):
494 485 _bool_setting(settings, 'appenlight', 'false')
495 486
496 487
497 488 def _sanitize_vcs_settings(settings):
498 489 """
499 490 Applies settings defaults and does type conversion for all VCS related
500 491 settings.
501 492 """
502 493 _string_setting(settings, 'vcs.svn.compatible_version', '')
503 494 _string_setting(settings, 'git_rev_filter', '--all')
504 495 _string_setting(settings, 'vcs.hooks.protocol', 'http')
505 496 _string_setting(settings, 'vcs.scm_app_implementation', 'http')
506 497 _string_setting(settings, 'vcs.server', '')
507 498 _string_setting(settings, 'vcs.server.log_level', 'debug')
508 499 _string_setting(settings, 'vcs.server.protocol', 'http')
509 500 _bool_setting(settings, 'startup.import_repos', 'false')
510 501 _bool_setting(settings, 'vcs.hooks.direct_calls', 'false')
511 502 _bool_setting(settings, 'vcs.server.enable', 'true')
512 503 _bool_setting(settings, 'vcs.start_server', 'false')
513 504 _list_setting(settings, 'vcs.backends', 'hg, git, svn')
514 505 _int_setting(settings, 'vcs.connection_timeout', 3600)
515 506
516 507 # Support legacy values of vcs.scm_app_implementation. Legacy
517 508 # configurations may use 'rhodecode.lib.middleware.utils.scm_app_http'
518 509 # which is now mapped to 'http'.
519 510 scm_app_impl = settings['vcs.scm_app_implementation']
520 511 if scm_app_impl == 'rhodecode.lib.middleware.utils.scm_app_http':
521 512 settings['vcs.scm_app_implementation'] = 'http'
522 513
523 514
524 515 def _int_setting(settings, name, default):
525 516 settings[name] = int(settings.get(name, default))
526 517
527 518
528 519 def _bool_setting(settings, name, default):
529 520 input = settings.get(name, default)
530 521 if isinstance(input, unicode):
531 522 input = input.encode('utf8')
532 523 settings[name] = asbool(input)
533 524
534 525
535 526 def _list_setting(settings, name, default):
536 527 raw_value = settings.get(name, default)
537 528
538 529 old_separator = ','
539 530 if old_separator in raw_value:
540 531 # If we get a comma separated list, pass it to our own function.
541 532 settings[name] = rhodecode_aslist(raw_value, sep=old_separator)
542 533 else:
543 534 # Otherwise we assume it uses pyramids space/newline separation.
544 535 settings[name] = aslist(raw_value)
545 536
546 537
547 538 def _string_setting(settings, name, default, lower=True):
548 539 value = settings.get(name, default)
549 540 if lower:
550 541 value = value.lower()
551 542 settings[name] = value
General Comments 0
You need to be logged in to leave comments. Login now