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