##// END OF EJS Templates
pyramid: add config to make_pyramid_app for inspection afterwards
dan -
r619:2097f436 default
parent child Browse files
Show More
@@ -1,470 +1,471 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2016 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 from collections import OrderedDict
26 26
27 27 from paste.registry import RegistryManager
28 28 from paste.gzipper import make_gzip_middleware
29 29 from pylons.wsgiapp import PylonsApp
30 30 from pyramid.authorization import ACLAuthorizationPolicy
31 31 from pyramid.config import Configurator
32 32 from pyramid.settings import asbool, aslist
33 33 from pyramid.wsgi import wsgiapp
34 34 from pyramid.httpexceptions import HTTPError, HTTPInternalServerError
35 35 from pylons.controllers.util import redirect
36 36 from pyramid.events import ApplicationCreated
37 37 import pyramid.httpexceptions as httpexceptions
38 38 from pyramid.renderers import render_to_response
39 39 from routes.middleware import RoutesMiddleware
40 40 import routes.util
41 41
42 42 import rhodecode
43 43 import rhodecode.integrations # do not remove this as it registers celery tasks
44 44 from rhodecode.config import patches
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 from rhodecode.lib.middleware import csrf
49 49 from rhodecode.lib.middleware.appenlight import wrap_in_appenlight_if_enabled
50 50 from rhodecode.lib.middleware.disable_vcs import DisableVCSPagesWrapper
51 51 from rhodecode.lib.middleware.https_fixup import HttpsFixup
52 52 from rhodecode.lib.middleware.vcs import VCSMiddleware
53 53 from rhodecode.lib.plugins.utils import register_rhodecode_plugin
54 54 from rhodecode.lib.utils2 import aslist as rhodecode_aslist
55 55 from rhodecode.subscribers import scan_repositories_if_enabled
56 56
57 57
58 58 log = logging.getLogger(__name__)
59 59
60 60
61 61 # this is used to avoid avoid the route lookup overhead in routesmiddleware
62 62 # for certain routes which won't go to pylons to - eg. static files, debugger
63 63 # it is only needed for the pylons migration and can be removed once complete
64 64 class SkippableRoutesMiddleware(RoutesMiddleware):
65 65 """ Routes middleware that allows you to skip prefixes """
66 66
67 67 def __init__(self, *args, **kw):
68 68 self.skip_prefixes = kw.pop('skip_prefixes', [])
69 69 super(SkippableRoutesMiddleware, self).__init__(*args, **kw)
70 70
71 71 def __call__(self, environ, start_response):
72 72 for prefix in self.skip_prefixes:
73 73 if environ['PATH_INFO'].startswith(prefix):
74 74 # added to avoid the case when a missing /_static route falls
75 75 # through to pylons and causes an exception as pylons is
76 76 # expecting wsgiorg.routingargs to be set in the environ
77 77 # by RoutesMiddleware.
78 78 if 'wsgiorg.routing_args' not in environ:
79 79 environ['wsgiorg.routing_args'] = (None, {})
80 80 return self.app(environ, start_response)
81 81
82 82 return super(SkippableRoutesMiddleware, self).__call__(
83 83 environ, start_response)
84 84
85 85
86 86 def make_app(global_conf, static_files=True, **app_conf):
87 87 """Create a Pylons WSGI application and return it
88 88
89 89 ``global_conf``
90 90 The inherited configuration for this application. Normally from
91 91 the [DEFAULT] section of the Paste ini file.
92 92
93 93 ``app_conf``
94 94 The application's local configuration. Normally specified in
95 95 the [app:<name>] section of the Paste ini file (where <name>
96 96 defaults to main).
97 97
98 98 """
99 99 # Apply compatibility patches
100 100 patches.kombu_1_5_1_python_2_7_11()
101 101 patches.inspect_getargspec()
102 102
103 103 # Configure the Pylons environment
104 104 config = load_environment(global_conf, app_conf)
105 105
106 106 # The Pylons WSGI app
107 107 app = PylonsApp(config=config)
108 108 if rhodecode.is_test:
109 109 app = csrf.CSRFDetector(app)
110 110
111 111 expected_origin = config.get('expected_origin')
112 112 if expected_origin:
113 113 # The API can be accessed from other Origins.
114 114 app = csrf.OriginChecker(app, expected_origin,
115 115 skip_urls=[routes.util.url_for('api')])
116 116
117 117 # Establish the Registry for this application
118 118 app = RegistryManager(app)
119 119
120 120 app.config = config
121 121
122 122 return app
123 123
124 124
125 125 def make_pyramid_app(global_config, **settings):
126 126 """
127 127 Constructs the WSGI application based on Pyramid and wraps the Pylons based
128 128 application.
129 129
130 130 Specials:
131 131
132 132 * We migrate from Pylons to Pyramid. While doing this, we keep both
133 133 frameworks functional. This involves moving some WSGI middlewares around
134 134 and providing access to some data internals, so that the old code is
135 135 still functional.
136 136
137 137 * The application can also be integrated like a plugin via the call to
138 138 `includeme`. This is accompanied with the other utility functions which
139 139 are called. Changing this should be done with great care to not break
140 140 cases when these fragments are assembled from another place.
141 141
142 142 """
143 143 # The edition string should be available in pylons too, so we add it here
144 144 # before copying the settings.
145 145 settings.setdefault('rhodecode.edition', 'Community Edition')
146 146
147 147 # As long as our Pylons application does expect "unprepared" settings, make
148 148 # sure that we keep an unmodified copy. This avoids unintentional change of
149 149 # behavior in the old application.
150 150 settings_pylons = settings.copy()
151 151
152 152 sanitize_settings_and_apply_defaults(settings)
153 153 config = Configurator(settings=settings)
154 154 add_pylons_compat_data(config.registry, global_config, settings_pylons)
155 155
156 156 load_pyramid_environment(global_config, settings)
157 157
158 158 includeme_first(config)
159 159 includeme(config)
160 160 pyramid_app = config.make_wsgi_app()
161 161 pyramid_app = wrap_app_in_wsgi_middlewares(pyramid_app, config)
162 pyramid_app.config = config
162 163 return pyramid_app
163 164
164 165
165 166 def make_not_found_view(config):
166 167 """
167 168 This creates the view which should be registered as not-found-view to
168 169 pyramid. Basically it contains of the old pylons app, converted to a view.
169 170 Additionally it is wrapped by some other middlewares.
170 171 """
171 172 settings = config.registry.settings
172 173 vcs_server_enabled = settings['vcs.server.enable']
173 174
174 175 # Make pylons app from unprepared settings.
175 176 pylons_app = make_app(
176 177 config.registry._pylons_compat_global_config,
177 178 **config.registry._pylons_compat_settings)
178 179 config.registry._pylons_compat_config = pylons_app.config
179 180
180 181 # Appenlight monitoring.
181 182 pylons_app, appenlight_client = wrap_in_appenlight_if_enabled(
182 183 pylons_app, settings)
183 184
184 185 # The VCSMiddleware shall operate like a fallback if pyramid doesn't find
185 186 # a view to handle the request. Therefore we wrap it around the pylons app.
186 187 if vcs_server_enabled:
187 188 pylons_app = VCSMiddleware(
188 189 pylons_app, settings, appenlight_client, registry=config.registry)
189 190
190 191 pylons_app_as_view = wsgiapp(pylons_app)
191 192
192 193 # Protect from VCS Server error related pages when server is not available
193 194 if not vcs_server_enabled:
194 195 pylons_app_as_view = DisableVCSPagesWrapper(pylons_app_as_view)
195 196
196 197 def pylons_app_with_error_handler(context, request):
197 198 """
198 199 Handle exceptions from rc pylons app:
199 200
200 201 - old webob type exceptions get converted to pyramid exceptions
201 202 - pyramid exceptions are passed to the error handler view
202 203 """
203 204 def is_vcs_response(response):
204 205 return 'X-RhodeCode-Backend' in response.headers
205 206
206 207 def is_http_error(response):
207 208 # webob type error responses
208 209 return (400 <= response.status_int <= 599)
209 210
210 211 def is_error_handling_needed(response):
211 212 return is_http_error(response) and not is_vcs_response(response)
212 213
213 214 try:
214 215 response = pylons_app_as_view(context, request)
215 216 if is_error_handling_needed(response):
216 217 response = webob_to_pyramid_http_response(response)
217 218 return error_handler(response, request)
218 219 except HTTPError as e: # pyramid type exceptions
219 220 return error_handler(e, request)
220 221 except Exception:
221 222 if settings.get('debugtoolbar.enabled', False):
222 223 raise
223 224 return error_handler(HTTPInternalServerError(), request)
224 225 return response
225 226
226 227 return pylons_app_with_error_handler
227 228
228 229
229 230 def add_pylons_compat_data(registry, global_config, settings):
230 231 """
231 232 Attach data to the registry to support the Pylons integration.
232 233 """
233 234 registry._pylons_compat_global_config = global_config
234 235 registry._pylons_compat_settings = settings
235 236
236 237
237 238 def webob_to_pyramid_http_response(webob_response):
238 239 ResponseClass = httpexceptions.status_map[webob_response.status_int]
239 240 pyramid_response = ResponseClass(webob_response.status)
240 241 pyramid_response.status = webob_response.status
241 242 pyramid_response.headers.update(webob_response.headers)
242 243 if pyramid_response.headers['content-type'] == 'text/html':
243 244 pyramid_response.headers['content-type'] = 'text/html; charset=UTF-8'
244 245 return pyramid_response
245 246
246 247
247 248 def error_handler(exception, request):
248 249 # TODO: dan: replace the old pylons error controller with this
249 250 from rhodecode.model.settings import SettingsModel
250 251 from rhodecode.lib.utils2 import AttributeDict
251 252
252 253 try:
253 254 rc_config = SettingsModel().get_all_settings()
254 255 except Exception:
255 256 log.exception('failed to fetch settings')
256 257 rc_config = {}
257 258
258 259 base_response = HTTPInternalServerError()
259 260 # prefer original exception for the response since it may have headers set
260 261 if isinstance(exception, HTTPError):
261 262 base_response = exception
262 263
263 264 c = AttributeDict()
264 265 c.error_message = base_response.status
265 266 c.error_explanation = base_response.explanation or str(base_response)
266 267 c.visual = AttributeDict()
267 268
268 269 c.visual.rhodecode_support_url = (
269 270 request.registry.settings.get('rhodecode_support_url') or
270 271 request.route_url('rhodecode_support')
271 272 )
272 273 c.redirect_time = 0
273 274 c.rhodecode_name = rc_config.get('rhodecode_title', '')
274 275 if not c.rhodecode_name:
275 276 c.rhodecode_name = 'Rhodecode'
276 277
277 278 response = render_to_response(
278 279 '/errors/error_document.html', {'c': c}, request=request,
279 280 response=base_response)
280 281
281 282 return response
282 283
283 284
284 285 def includeme(config):
285 286 settings = config.registry.settings
286 287
287 288 # plugin information
288 289 config.registry.rhodecode_plugins = OrderedDict()
289 290
290 291 config.add_directive(
291 292 'register_rhodecode_plugin', register_rhodecode_plugin)
292 293
293 294 if asbool(settings.get('appenlight', 'false')):
294 295 config.include('appenlight_client.ext.pyramid_tween')
295 296
296 297 # Includes which are required. The application would fail without them.
297 298 config.include('pyramid_mako')
298 299 config.include('pyramid_beaker')
299 300 config.include('rhodecode.channelstream')
300 301 config.include('rhodecode.admin')
301 302 config.include('rhodecode.authentication')
302 303 config.include('rhodecode.integrations')
303 304 config.include('rhodecode.login')
304 305 config.include('rhodecode.tweens')
305 306 config.include('rhodecode.api')
306 307 config.include('rhodecode.svn_support')
307 308 config.add_route(
308 309 'rhodecode_support', 'https://rhodecode.com/help/', static=True)
309 310
310 311 # Add subscribers.
311 312 config.add_subscriber(scan_repositories_if_enabled, ApplicationCreated)
312 313
313 314 # Set the authorization policy.
314 315 authz_policy = ACLAuthorizationPolicy()
315 316 config.set_authorization_policy(authz_policy)
316 317
317 318 # Set the default renderer for HTML templates to mako.
318 319 config.add_mako_renderer('.html')
319 320
320 321 # include RhodeCode plugins
321 322 includes = aslist(settings.get('rhodecode.includes', []))
322 323 for inc in includes:
323 324 config.include(inc)
324 325
325 326 # This is the glue which allows us to migrate in chunks. By registering the
326 327 # pylons based application as the "Not Found" view in Pyramid, we will
327 328 # fallback to the old application each time the new one does not yet know
328 329 # how to handle a request.
329 330 config.add_notfound_view(make_not_found_view(config))
330 331
331 332 if not settings.get('debugtoolbar.enabled', False):
332 333 # if no toolbar, then any exception gets caught and rendered
333 334 config.add_view(error_handler, context=Exception)
334 335
335 336 config.add_view(error_handler, context=HTTPError)
336 337
337 338
338 339 def includeme_first(config):
339 340 # redirect automatic browser favicon.ico requests to correct place
340 341 def favicon_redirect(context, request):
341 342 return redirect(
342 343 request.static_path('rhodecode:public/images/favicon.ico'))
343 344
344 345 config.add_view(favicon_redirect, route_name='favicon')
345 346 config.add_route('favicon', '/favicon.ico')
346 347
347 348 config.add_static_view(
348 349 '_static/deform', 'deform:static')
349 350 config.add_static_view(
350 351 '_static/rhodecode', path='rhodecode:public', cache_max_age=3600 * 24)
351 352
352 353
353 354 def wrap_app_in_wsgi_middlewares(pyramid_app, config):
354 355 """
355 356 Apply outer WSGI middlewares around the application.
356 357
357 358 Part of this has been moved up from the Pylons layer, so that the
358 359 data is also available if old Pylons code is hit through an already ported
359 360 view.
360 361 """
361 362 settings = config.registry.settings
362 363
363 364 # enable https redirects based on HTTP_X_URL_SCHEME set by proxy
364 365 pyramid_app = HttpsFixup(pyramid_app, settings)
365 366
366 367 # Add RoutesMiddleware to support the pylons compatibility tween during
367 368 # migration to pyramid.
368 369 pyramid_app = SkippableRoutesMiddleware(
369 370 pyramid_app, config.registry._pylons_compat_config['routes.map'],
370 371 skip_prefixes=(STATIC_FILE_PREFIX, '/_debug_toolbar'))
371 372
372 373 pyramid_app, _ = wrap_in_appenlight_if_enabled(pyramid_app, settings)
373 374
374 375 if settings['gzip_responses']:
375 376 pyramid_app = make_gzip_middleware(
376 377 pyramid_app, settings, compress_level=1)
377 378
378 379 return pyramid_app
379 380
380 381
381 382 def sanitize_settings_and_apply_defaults(settings):
382 383 """
383 384 Applies settings defaults and does all type conversion.
384 385
385 386 We would move all settings parsing and preparation into this place, so that
386 387 we have only one place left which deals with this part. The remaining parts
387 388 of the application would start to rely fully on well prepared settings.
388 389
389 390 This piece would later be split up per topic to avoid a big fat monster
390 391 function.
391 392 """
392 393
393 394 # Pyramid's mako renderer has to search in the templates folder so that the
394 395 # old templates still work. Ported and new templates are expected to use
395 396 # real asset specifications for the includes.
396 397 mako_directories = settings.setdefault('mako.directories', [
397 398 # Base templates of the original Pylons application
398 399 'rhodecode:templates',
399 400 ])
400 401 log.debug(
401 402 "Using the following Mako template directories: %s",
402 403 mako_directories)
403 404
404 405 # Default includes, possible to change as a user
405 406 pyramid_includes = settings.setdefault('pyramid.includes', [
406 407 'rhodecode.lib.middleware.request_wrapper',
407 408 ])
408 409 log.debug(
409 410 "Using the following pyramid.includes: %s",
410 411 pyramid_includes)
411 412
412 413 # TODO: johbo: Re-think this, usually the call to config.include
413 414 # should allow to pass in a prefix.
414 415 settings.setdefault('rhodecode.api.url', '/_admin/api')
415 416
416 417 # Sanitize generic settings.
417 418 _list_setting(settings, 'default_encoding', 'UTF-8')
418 419 _bool_setting(settings, 'is_test', 'false')
419 420 _bool_setting(settings, 'gzip_responses', 'false')
420 421
421 422 # Call split out functions that sanitize settings for each topic.
422 423 _sanitize_appenlight_settings(settings)
423 424 _sanitize_vcs_settings(settings)
424 425
425 426 return settings
426 427
427 428
428 429 def _sanitize_appenlight_settings(settings):
429 430 _bool_setting(settings, 'appenlight', 'false')
430 431
431 432
432 433 def _sanitize_vcs_settings(settings):
433 434 """
434 435 Applies settings defaults and does type conversion for all VCS related
435 436 settings.
436 437 """
437 438 _string_setting(settings, 'vcs.svn.compatible_version', '')
438 439 _string_setting(settings, 'git_rev_filter', '--all')
439 440 _string_setting(settings, 'vcs.hooks.protocol', 'pyro4')
440 441 _string_setting(settings, 'vcs.server', '')
441 442 _string_setting(settings, 'vcs.server.log_level', 'debug')
442 443 _string_setting(settings, 'vcs.server.protocol', 'pyro4')
443 444 _bool_setting(settings, 'startup.import_repos', 'false')
444 445 _bool_setting(settings, 'vcs.hooks.direct_calls', 'false')
445 446 _bool_setting(settings, 'vcs.server.enable', 'true')
446 447 _bool_setting(settings, 'vcs.start_server', 'false')
447 448 _list_setting(settings, 'vcs.backends', 'hg, git, svn')
448 449
449 450
450 451 def _bool_setting(settings, name, default):
451 452 input = settings.get(name, default)
452 453 if isinstance(input, unicode):
453 454 input = input.encode('utf8')
454 455 settings[name] = asbool(input)
455 456
456 457
457 458 def _list_setting(settings, name, default):
458 459 raw_value = settings.get(name, default)
459 460
460 461 old_separator = ','
461 462 if old_separator in raw_value:
462 463 # If we get a comma separated list, pass it to our own function.
463 464 settings[name] = rhodecode_aslist(raw_value, sep=old_separator)
464 465 else:
465 466 # Otherwise we assume it uses pyramids space/newline separation.
466 467 settings[name] = aslist(raw_value)
467 468
468 469
469 470 def _string_setting(settings, name, default):
470 471 settings[name] = settings.get(name, default).lower()
General Comments 0
You need to be logged in to leave comments. Login now