##// END OF EJS Templates
vcs-error: replace disable_vcs middleware with vcs and http exceptions...
dan -
r682:1b4e984a default
parent child Browse files
Show More
@@ -0,0 +1,42 b''
1 # -*- coding: utf-8 -*-
2
3 # Copyright (C) 2010-2016 RhodeCode GmbH
4 #
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
21 import mock
22 import pytest
23 import rhodecode.lib.vcs.client as client
24
25 @pytest.mark.usefixtures('autologin_user', 'app')
26 def test_vcs_available_returns_summary_page(app, backend):
27 url = '/{repo_name}'.format(repo_name=backend.repo.repo_name)
28 response = app.get(url)
29 assert response.status_code == 200
30 assert 'Summary' in response.body
31
32
33 @pytest.mark.usefixtures('autologin_user', 'app')
34 def test_vcs_unavailable_returns_vcs_error_page(app, backend):
35 url = '/{repo_name}'.format(repo_name=backend.repo.repo_name)
36
37 with mock.patch.object(client, '_get_proxy_method') as p:
38 p.side_effect = client.exceptions.PyroVCSCommunicationError()
39 response = app.get(url, expect_errors=True)
40
41 assert response.status_code == 502
42 assert 'Could not connect to VCS Server' in response.body
@@ -1,474 +1,478 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, HTTPFound
35 35 from pyramid.events import ApplicationCreated
36 36 import pyramid.httpexceptions as httpexceptions
37 37 from pyramid.renderers import render_to_response
38 38 from routes.middleware import RoutesMiddleware
39 39 import routes.util
40 40
41 41 import rhodecode
42 42 from rhodecode.config import patches
43 43 from rhodecode.config.routing import STATIC_FILE_PREFIX
44 44 from rhodecode.config.environment import (
45 45 load_environment, load_pyramid_environment)
46 from rhodecode.lib.exceptions import VCSServerUnavailable
47 from rhodecode.lib.vcs.exceptions import VCSCommunicationError
46 48 from rhodecode.lib.middleware import csrf
47 49 from rhodecode.lib.middleware.appenlight import wrap_in_appenlight_if_enabled
48 from rhodecode.lib.middleware.disable_vcs import DisableVCSPagesWrapper
49 50 from rhodecode.lib.middleware.https_fixup import HttpsFixup
50 51 from rhodecode.lib.middleware.vcs import VCSMiddleware
51 52 from rhodecode.lib.plugins.utils import register_rhodecode_plugin
52 53 from rhodecode.lib.utils2 import aslist as rhodecode_aslist
53 54 from rhodecode.subscribers import scan_repositories_if_enabled
54 55
55 56
56 57 log = logging.getLogger(__name__)
57 58
58 59
59 60 # this is used to avoid avoid the route lookup overhead in routesmiddleware
60 61 # for certain routes which won't go to pylons to - eg. static files, debugger
61 62 # it is only needed for the pylons migration and can be removed once complete
62 63 class SkippableRoutesMiddleware(RoutesMiddleware):
63 64 """ Routes middleware that allows you to skip prefixes """
64 65
65 66 def __init__(self, *args, **kw):
66 67 self.skip_prefixes = kw.pop('skip_prefixes', [])
67 68 super(SkippableRoutesMiddleware, self).__init__(*args, **kw)
68 69
69 70 def __call__(self, environ, start_response):
70 71 for prefix in self.skip_prefixes:
71 72 if environ['PATH_INFO'].startswith(prefix):
72 73 # added to avoid the case when a missing /_static route falls
73 74 # through to pylons and causes an exception as pylons is
74 75 # expecting wsgiorg.routingargs to be set in the environ
75 76 # by RoutesMiddleware.
76 77 if 'wsgiorg.routing_args' not in environ:
77 78 environ['wsgiorg.routing_args'] = (None, {})
78 79 return self.app(environ, start_response)
79 80
80 81 return super(SkippableRoutesMiddleware, self).__call__(
81 82 environ, start_response)
82 83
83 84
84 85 def make_app(global_conf, static_files=True, **app_conf):
85 86 """Create a Pylons WSGI application and return it
86 87
87 88 ``global_conf``
88 89 The inherited configuration for this application. Normally from
89 90 the [DEFAULT] section of the Paste ini file.
90 91
91 92 ``app_conf``
92 93 The application's local configuration. Normally specified in
93 94 the [app:<name>] section of the Paste ini file (where <name>
94 95 defaults to main).
95 96
96 97 """
97 98 # Apply compatibility patches
98 99 patches.kombu_1_5_1_python_2_7_11()
99 100 patches.inspect_getargspec()
100 101
101 102 # Configure the Pylons environment
102 103 config = load_environment(global_conf, app_conf)
103 104
104 105 # The Pylons WSGI app
105 106 app = PylonsApp(config=config)
106 107 if rhodecode.is_test:
107 108 app = csrf.CSRFDetector(app)
108 109
109 110 expected_origin = config.get('expected_origin')
110 111 if expected_origin:
111 112 # The API can be accessed from other Origins.
112 113 app = csrf.OriginChecker(app, expected_origin,
113 114 skip_urls=[routes.util.url_for('api')])
114 115
115 116 # Establish the Registry for this application
116 117 app = RegistryManager(app)
117 118
118 119 app.config = config
119 120
120 121 return app
121 122
122 123
123 124 def make_pyramid_app(global_config, **settings):
124 125 """
125 126 Constructs the WSGI application based on Pyramid and wraps the Pylons based
126 127 application.
127 128
128 129 Specials:
129 130
130 131 * We migrate from Pylons to Pyramid. While doing this, we keep both
131 132 frameworks functional. This involves moving some WSGI middlewares around
132 133 and providing access to some data internals, so that the old code is
133 134 still functional.
134 135
135 136 * The application can also be integrated like a plugin via the call to
136 137 `includeme`. This is accompanied with the other utility functions which
137 138 are called. Changing this should be done with great care to not break
138 139 cases when these fragments are assembled from another place.
139 140
140 141 """
141 142 # The edition string should be available in pylons too, so we add it here
142 143 # before copying the settings.
143 144 settings.setdefault('rhodecode.edition', 'Community Edition')
144 145
145 146 # As long as our Pylons application does expect "unprepared" settings, make
146 147 # sure that we keep an unmodified copy. This avoids unintentional change of
147 148 # behavior in the old application.
148 149 settings_pylons = settings.copy()
149 150
150 151 sanitize_settings_and_apply_defaults(settings)
151 152 config = Configurator(settings=settings)
152 153 add_pylons_compat_data(config.registry, global_config, settings_pylons)
153 154
154 155 load_pyramid_environment(global_config, settings)
155 156
156 157 includeme_first(config)
157 158 includeme(config)
158 159 pyramid_app = config.make_wsgi_app()
159 160 pyramid_app = wrap_app_in_wsgi_middlewares(pyramid_app, config)
160 161 pyramid_app.config = config
161 162 return pyramid_app
162 163
163 164
164 165 def make_not_found_view(config):
165 166 """
166 167 This creates the view which should be registered as not-found-view to
167 168 pyramid. Basically it contains of the old pylons app, converted to a view.
168 169 Additionally it is wrapped by some other middlewares.
169 170 """
170 171 settings = config.registry.settings
171 172 vcs_server_enabled = settings['vcs.server.enable']
172 173
173 174 # Make pylons app from unprepared settings.
174 175 pylons_app = make_app(
175 176 config.registry._pylons_compat_global_config,
176 177 **config.registry._pylons_compat_settings)
177 178 config.registry._pylons_compat_config = pylons_app.config
178 179
179 180 # Appenlight monitoring.
180 181 pylons_app, appenlight_client = wrap_in_appenlight_if_enabled(
181 182 pylons_app, settings)
182 183
183 184 # The VCSMiddleware shall operate like a fallback if pyramid doesn't find
184 185 # a view to handle the request. Therefore we wrap it around the pylons app.
185 186 if vcs_server_enabled:
186 187 pylons_app = VCSMiddleware(
187 188 pylons_app, settings, appenlight_client, registry=config.registry)
188 189
189 190 pylons_app_as_view = wsgiapp(pylons_app)
190 191
191 # Protect from VCS Server error related pages when server is not available
192 if not vcs_server_enabled:
193 pylons_app_as_view = DisableVCSPagesWrapper(pylons_app_as_view)
194
195 192 def pylons_app_with_error_handler(context, request):
196 193 """
197 194 Handle exceptions from rc pylons app:
198 195
199 196 - old webob type exceptions get converted to pyramid exceptions
200 197 - pyramid exceptions are passed to the error handler view
201 198 """
202 199 def is_vcs_response(response):
203 200 return 'X-RhodeCode-Backend' in response.headers
204 201
205 202 def is_http_error(response):
206 203 # webob type error responses
207 204 return (400 <= response.status_int <= 599)
208 205
209 206 def is_error_handling_needed(response):
210 207 return is_http_error(response) and not is_vcs_response(response)
211 208
212 209 try:
213 210 response = pylons_app_as_view(context, request)
214 211 if is_error_handling_needed(response):
215 212 response = webob_to_pyramid_http_response(response)
216 213 return error_handler(response, request)
217 214 except HTTPError as e: # pyramid type exceptions
218 215 return error_handler(e, request)
219 except Exception:
216 except Exception as e:
217 log.exception(e)
218
220 219 if settings.get('debugtoolbar.enabled', False):
221 220 raise
221
222 if isinstance(e, VCSCommunicationError):
223 return error_handler(VCSServerUnavailable(), request)
224
222 225 return error_handler(HTTPInternalServerError(), request)
226
223 227 return response
224 228
225 229 return pylons_app_with_error_handler
226 230
227 231
228 232 def add_pylons_compat_data(registry, global_config, settings):
229 233 """
230 234 Attach data to the registry to support the Pylons integration.
231 235 """
232 236 registry._pylons_compat_global_config = global_config
233 237 registry._pylons_compat_settings = settings
234 238
235 239
236 240 def webob_to_pyramid_http_response(webob_response):
237 241 ResponseClass = httpexceptions.status_map[webob_response.status_int]
238 242 pyramid_response = ResponseClass(webob_response.status)
239 243 pyramid_response.status = webob_response.status
240 244 pyramid_response.headers.update(webob_response.headers)
241 245 if pyramid_response.headers['content-type'] == 'text/html':
242 246 pyramid_response.headers['content-type'] = 'text/html; charset=UTF-8'
243 247 return pyramid_response
244 248
245 249
246 250 def error_handler(exception, request):
247 251 # TODO: dan: replace the old pylons error controller with this
248 252 from rhodecode.model.settings import SettingsModel
249 253 from rhodecode.lib.utils2 import AttributeDict
250 254
251 255 try:
252 256 rc_config = SettingsModel().get_all_settings()
253 257 except Exception:
254 258 log.exception('failed to fetch settings')
255 259 rc_config = {}
256 260
257 261 base_response = HTTPInternalServerError()
258 262 # prefer original exception for the response since it may have headers set
259 263 if isinstance(exception, HTTPError):
260 264 base_response = exception
261 265
262 266 c = AttributeDict()
263 267 c.error_message = base_response.status
264 268 c.error_explanation = base_response.explanation or str(base_response)
265 269 c.visual = AttributeDict()
266 270
267 271 c.visual.rhodecode_support_url = (
268 272 request.registry.settings.get('rhodecode_support_url') or
269 273 request.route_url('rhodecode_support')
270 274 )
271 275 c.redirect_time = 0
272 276 c.rhodecode_name = rc_config.get('rhodecode_title', '')
273 277 if not c.rhodecode_name:
274 278 c.rhodecode_name = 'Rhodecode'
275 279
276 280 response = render_to_response(
277 281 '/errors/error_document.html', {'c': c}, request=request,
278 282 response=base_response)
279 283
280 284 return response
281 285
282 286
283 287 def includeme(config):
284 288 settings = config.registry.settings
285 289
286 290 # plugin information
287 291 config.registry.rhodecode_plugins = OrderedDict()
288 292
289 293 config.add_directive(
290 294 'register_rhodecode_plugin', register_rhodecode_plugin)
291 295
292 296 if asbool(settings.get('appenlight', 'false')):
293 297 config.include('appenlight_client.ext.pyramid_tween')
294 298
295 299 # Includes which are required. The application would fail without them.
296 300 config.include('pyramid_mako')
297 301 config.include('pyramid_beaker')
298 302 config.include('rhodecode.channelstream')
299 303 config.include('rhodecode.admin')
300 304 config.include('rhodecode.authentication')
301 305 config.include('rhodecode.integrations')
302 306 config.include('rhodecode.login')
303 307 config.include('rhodecode.tweens')
304 308 config.include('rhodecode.api')
305 309 config.include('rhodecode.svn_support')
306 310 config.add_route(
307 311 'rhodecode_support', 'https://rhodecode.com/help/', static=True)
308 312
309 313 # Add subscribers.
310 314 config.add_subscriber(scan_repositories_if_enabled, ApplicationCreated)
311 315
312 316 # Set the authorization policy.
313 317 authz_policy = ACLAuthorizationPolicy()
314 318 config.set_authorization_policy(authz_policy)
315 319
316 320 # Set the default renderer for HTML templates to mako.
317 321 config.add_mako_renderer('.html')
318 322
319 323 # include RhodeCode plugins
320 324 includes = aslist(settings.get('rhodecode.includes', []))
321 325 for inc in includes:
322 326 config.include(inc)
323 327
324 328 # This is the glue which allows us to migrate in chunks. By registering the
325 329 # pylons based application as the "Not Found" view in Pyramid, we will
326 330 # fallback to the old application each time the new one does not yet know
327 331 # how to handle a request.
328 332 config.add_notfound_view(make_not_found_view(config))
329 333
330 334 if not settings.get('debugtoolbar.enabled', False):
331 335 # if no toolbar, then any exception gets caught and rendered
332 336 config.add_view(error_handler, context=Exception)
333 337
334 338 config.add_view(error_handler, context=HTTPError)
335 339
336 340
337 341 def includeme_first(config):
338 342 # redirect automatic browser favicon.ico requests to correct place
339 343 def favicon_redirect(context, request):
340 344 return HTTPFound(
341 345 request.static_path('rhodecode:public/images/favicon.ico'))
342 346
343 347 config.add_view(favicon_redirect, route_name='favicon')
344 348 config.add_route('favicon', '/favicon.ico')
345 349
346 350 config.add_static_view(
347 351 '_static/deform', 'deform:static')
348 352 config.add_static_view(
349 353 '_static/rhodecode', path='rhodecode:public', cache_max_age=3600 * 24)
350 354
351 355
352 356 def wrap_app_in_wsgi_middlewares(pyramid_app, config):
353 357 """
354 358 Apply outer WSGI middlewares around the application.
355 359
356 360 Part of this has been moved up from the Pylons layer, so that the
357 361 data is also available if old Pylons code is hit through an already ported
358 362 view.
359 363 """
360 364 settings = config.registry.settings
361 365
362 366 # enable https redirects based on HTTP_X_URL_SCHEME set by proxy
363 367 pyramid_app = HttpsFixup(pyramid_app, settings)
364 368
365 369 # Add RoutesMiddleware to support the pylons compatibility tween during
366 370 # migration to pyramid.
367 371 pyramid_app = SkippableRoutesMiddleware(
368 372 pyramid_app, config.registry._pylons_compat_config['routes.map'],
369 373 skip_prefixes=(STATIC_FILE_PREFIX, '/_debug_toolbar'))
370 374
371 375 pyramid_app, _ = wrap_in_appenlight_if_enabled(pyramid_app, settings)
372 376
373 377 if settings['gzip_responses']:
374 378 pyramid_app = make_gzip_middleware(
375 379 pyramid_app, settings, compress_level=1)
376 380
377 381 return pyramid_app
378 382
379 383
380 384 def sanitize_settings_and_apply_defaults(settings):
381 385 """
382 386 Applies settings defaults and does all type conversion.
383 387
384 388 We would move all settings parsing and preparation into this place, so that
385 389 we have only one place left which deals with this part. The remaining parts
386 390 of the application would start to rely fully on well prepared settings.
387 391
388 392 This piece would later be split up per topic to avoid a big fat monster
389 393 function.
390 394 """
391 395
392 396 # Pyramid's mako renderer has to search in the templates folder so that the
393 397 # old templates still work. Ported and new templates are expected to use
394 398 # real asset specifications for the includes.
395 399 mako_directories = settings.setdefault('mako.directories', [
396 400 # Base templates of the original Pylons application
397 401 'rhodecode:templates',
398 402 ])
399 403 log.debug(
400 404 "Using the following Mako template directories: %s",
401 405 mako_directories)
402 406
403 407 # Default includes, possible to change as a user
404 408 pyramid_includes = settings.setdefault('pyramid.includes', [
405 409 'rhodecode.lib.middleware.request_wrapper',
406 410 ])
407 411 log.debug(
408 412 "Using the following pyramid.includes: %s",
409 413 pyramid_includes)
410 414
411 415 # TODO: johbo: Re-think this, usually the call to config.include
412 416 # should allow to pass in a prefix.
413 417 settings.setdefault('rhodecode.api.url', '/_admin/api')
414 418
415 419 # Sanitize generic settings.
416 420 _list_setting(settings, 'default_encoding', 'UTF-8')
417 421 _bool_setting(settings, 'is_test', 'false')
418 422 _bool_setting(settings, 'gzip_responses', 'false')
419 423
420 424 # Call split out functions that sanitize settings for each topic.
421 425 _sanitize_appenlight_settings(settings)
422 426 _sanitize_vcs_settings(settings)
423 427
424 428 return settings
425 429
426 430
427 431 def _sanitize_appenlight_settings(settings):
428 432 _bool_setting(settings, 'appenlight', 'false')
429 433
430 434
431 435 def _sanitize_vcs_settings(settings):
432 436 """
433 437 Applies settings defaults and does type conversion for all VCS related
434 438 settings.
435 439 """
436 440 _string_setting(settings, 'vcs.svn.compatible_version', '')
437 441 _string_setting(settings, 'git_rev_filter', '--all')
438 442 _string_setting(settings, 'vcs.hooks.protocol', 'pyro4')
439 443 _string_setting(settings, 'vcs.server', '')
440 444 _string_setting(settings, 'vcs.server.log_level', 'debug')
441 445 _string_setting(settings, 'vcs.server.protocol', 'pyro4')
442 446 _bool_setting(settings, 'startup.import_repos', 'false')
443 447 _bool_setting(settings, 'vcs.hooks.direct_calls', 'false')
444 448 _bool_setting(settings, 'vcs.server.enable', 'true')
445 449 _bool_setting(settings, 'vcs.start_server', 'false')
446 450 _list_setting(settings, 'vcs.backends', 'hg, git, svn')
447 451 _int_setting(settings, 'vcs.connection_timeout', 3600)
448 452
449 453
450 454 def _int_setting(settings, name, default):
451 455 settings[name] = int(settings.get(name, default))
452 456
453 457
454 458 def _bool_setting(settings, name, default):
455 459 input = settings.get(name, default)
456 460 if isinstance(input, unicode):
457 461 input = input.encode('utf8')
458 462 settings[name] = asbool(input)
459 463
460 464
461 465 def _list_setting(settings, name, default):
462 466 raw_value = settings.get(name, default)
463 467
464 468 old_separator = ','
465 469 if old_separator in raw_value:
466 470 # If we get a comma separated list, pass it to our own function.
467 471 settings[name] = rhodecode_aslist(raw_value, sep=old_separator)
468 472 else:
469 473 # Otherwise we assume it uses pyramids space/newline separation.
470 474 settings[name] = aslist(raw_value)
471 475
472 476
473 477 def _string_setting(settings, name, default):
474 478 settings[name] = settings.get(name, default).lower()
@@ -1,122 +1,134 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 Set of custom exceptions used in RhodeCode
23 23 """
24 24
25 25 from webob.exc import HTTPClientError
26 from pyramid.httpexceptions import HTTPBadGateway
26 27
27 28
28 29 class LdapUsernameError(Exception):
29 30 pass
30 31
31 32
32 33 class LdapPasswordError(Exception):
33 34 pass
34 35
35 36
36 37 class LdapConnectionError(Exception):
37 38 pass
38 39
39 40
40 41 class LdapImportError(Exception):
41 42 pass
42 43
43 44
44 45 class DefaultUserException(Exception):
45 46 pass
46 47
47 48
48 49 class UserOwnsReposException(Exception):
49 50 pass
50 51
51 52
52 53 class UserOwnsRepoGroupsException(Exception):
53 54 pass
54 55
55 56
56 57 class UserOwnsUserGroupsException(Exception):
57 58 pass
58 59
59 60
60 61 class UserGroupAssignedException(Exception):
61 62 pass
62 63
63 64
64 65 class StatusChangeOnClosedPullRequestError(Exception):
65 66 pass
66 67
67 68
68 69 class AttachedForksError(Exception):
69 70 pass
70 71
71 72
72 73 class RepoGroupAssignmentError(Exception):
73 74 pass
74 75
75 76
76 77 class NonRelativePathError(Exception):
77 78 pass
78 79
79 80
80 81 class HTTPRequirementError(HTTPClientError):
81 82 title = explanation = 'Repository Requirement Missing'
82 83 reason = None
83 84
84 85 def __init__(self, message, *args, **kwargs):
85 86 self.title = self.explanation = message
86 87 super(HTTPRequirementError, self).__init__(*args, **kwargs)
87 88 self.args = (message, )
88 89
89 90
90 91 class HTTPLockedRC(HTTPClientError):
91 92 """
92 93 Special Exception For locked Repos in RhodeCode, the return code can
93 94 be overwritten by _code keyword argument passed into constructors
94 95 """
95 96 code = 423
96 97 title = explanation = 'Repository Locked'
97 98 reason = None
98 99
99 100 def __init__(self, message, *args, **kwargs):
100 101 from rhodecode import CONFIG
101 102 from rhodecode.lib.utils2 import safe_int
102 103 _code = CONFIG.get('lock_ret_code')
103 104 self.code = safe_int(_code, self.code)
104 105 self.title = self.explanation = message
105 106 super(HTTPLockedRC, self).__init__(*args, **kwargs)
106 107 self.args = (message, )
107 108
108 109
109 110 class IMCCommitError(Exception):
110 111 pass
111 112
112 113
113 114 class UserCreationError(Exception):
114 115 pass
115 116
116 117
117 118 class NotAllowedToCreateUserError(Exception):
118 119 pass
119 120
120 121
121 122 class RepositoryCreationError(Exception):
122 123 pass
124
125
126 class VCSServerUnavailable(HTTPBadGateway):
127 """ HTTP Exception class for VCS Server errors """
128 code = 502
129 title = 'VCS Server Error'
130 def __init__(self, message=''):
131 self.explanation = 'Could not connect to VCS Server'
132 if message:
133 self.explanation += ': ' + message
134 super(VCSServerUnavailable, self).__init__()
@@ -1,346 +1,346 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2014-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 Provides the implementation of various client utilities to reach the vcsserver.
23 23 """
24 24
25 25
26 26 import copy
27 27 import logging
28 28 import threading
29 29 import urlparse
30 30 import uuid
31 31 import weakref
32 32 from urllib2 import URLError
33 33
34 34 import msgpack
35 35 import Pyro4
36 36 import requests
37 37 from pyramid.threadlocal import get_current_request
38 38 from Pyro4.errors import CommunicationError, ConnectionClosedError, DaemonError
39 39
40 40 from rhodecode.lib.vcs import exceptions
41 41 from rhodecode.lib.vcs.conf import settings
42 42
43 43 log = logging.getLogger(__name__)
44 44
45 45
46 46 # TODO: mikhail: Keep it in sync with vcsserver's
47 47 # HTTPApplication.ALLOWED_EXCEPTIONS
48 48 EXCEPTIONS_MAP = {
49 49 'KeyError': KeyError,
50 50 'URLError': URLError,
51 51 }
52 52
53 53
54 54 class HTTPRepoMaker(object):
55 55 def __init__(self, server_and_port, backend_endpoint):
56 56 self.url = urlparse.urljoin(
57 57 'http://%s' % server_and_port, backend_endpoint)
58 58
59 59 def __call__(self, path, config, with_wire=None):
60 60 log.debug('HTTPRepoMaker call on %s', path)
61 61 return HTTPRemoteRepo(path, config, self.url, with_wire=with_wire)
62 62
63 63 def __getattr__(self, name):
64 64 def f(*args, **kwargs):
65 65 return self._call(name, *args, **kwargs)
66 66 return f
67 67
68 68 @exceptions.map_vcs_exceptions
69 69 def _call(self, name, *args, **kwargs):
70 70 payload = {
71 71 'id': str(uuid.uuid4()),
72 72 'method': name,
73 73 'params': {'args': args, 'kwargs': kwargs}
74 74 }
75 75 return _remote_call(self.url, payload, EXCEPTIONS_MAP)
76 76
77 77
78 78 class VcsHttpProxy(object):
79 79
80 80 CHUNK_SIZE = 16384
81 81
82 82 def __init__(self, server_and_port, backend_endpoint):
83 83 adapter = requests.adapters.HTTPAdapter(max_retries=5)
84 84 self.base_url = urlparse.urljoin(
85 85 'http://%s' % server_and_port, backend_endpoint)
86 86 self.session = requests.Session()
87 87 self.session.mount('http://', adapter)
88 88
89 89 def handle(self, environment, input_data, *args, **kwargs):
90 90 data = {
91 91 'environment': environment,
92 92 'input_data': input_data,
93 93 'args': args,
94 94 'kwargs': kwargs
95 95 }
96 96 result = self.session.post(
97 97 self.base_url, msgpack.packb(data), stream=True)
98 98 return self._get_result(result)
99 99
100 100 def _deserialize_and_raise(self, error):
101 101 exception = Exception(error['message'])
102 102 try:
103 103 exception._vcs_kind = error['_vcs_kind']
104 104 except KeyError:
105 105 pass
106 106 raise exception
107 107
108 108 def _iterate(self, result):
109 109 unpacker = msgpack.Unpacker()
110 110 for line in result.iter_content(chunk_size=self.CHUNK_SIZE):
111 111 unpacker.feed(line)
112 112 for chunk in unpacker:
113 113 yield chunk
114 114
115 115 def _get_result(self, result):
116 116 iterator = self._iterate(result)
117 117 error = iterator.next()
118 118 if error:
119 119 self._deserialize_and_raise(error)
120 120
121 121 status = iterator.next()
122 122 headers = iterator.next()
123 123
124 124 return iterator, status, headers
125 125
126 126
127 127 class HTTPRemoteRepo(object):
128 128 def __init__(self, path, config, url, with_wire=None):
129 129 self.url = url
130 130 self._wire = {
131 131 "path": path,
132 132 "config": config,
133 133 "context": str(uuid.uuid4()),
134 134 }
135 135 if with_wire:
136 136 self._wire.update(with_wire)
137 137
138 138 def __getattr__(self, name):
139 139 def f(*args, **kwargs):
140 140 return self._call(name, *args, **kwargs)
141 141 return f
142 142
143 143 @exceptions.map_vcs_exceptions
144 144 def _call(self, name, *args, **kwargs):
145 145 log.debug('Calling %s@%s', self.url, name)
146 146 # TODO: oliver: This is currently necessary pre-call since the
147 147 # config object is being changed for hooking scenarios
148 148 wire = copy.deepcopy(self._wire)
149 149 wire["config"] = wire["config"].serialize()
150 150 payload = {
151 151 'id': str(uuid.uuid4()),
152 152 'method': name,
153 153 'params': {'wire': wire, 'args': args, 'kwargs': kwargs}
154 154 }
155 155 return _remote_call(self.url, payload, EXCEPTIONS_MAP)
156 156
157 157 def __getitem__(self, key):
158 158 return self.revision(key)
159 159
160 160
161 161 def _remote_call(url, payload, exceptions_map):
162 162 response = requests.post(url, data=msgpack.packb(payload))
163 163 response = msgpack.unpackb(response.content)
164 164 error = response.get('error')
165 165 if error:
166 166 type_ = error.get('type', 'Exception')
167 167 exc = exceptions_map.get(type_, Exception)
168 168 exc = exc(error.get('message'))
169 169 try:
170 170 exc._vcs_kind = error['_vcs_kind']
171 171 except KeyError:
172 172 pass
173 173 raise exc
174 174 return response.get('result')
175 175
176 176
177 177 class RepoMaker(object):
178 178
179 179 def __init__(self, proxy_factory):
180 180 self._proxy_factory = proxy_factory
181 181
182 182 def __call__(self, path, config, with_wire=None):
183 183 log.debug('RepoMaker call on %s', path)
184 184 return RemoteRepo(
185 185 path, config, remote_proxy=self._proxy_factory(),
186 186 with_wire=with_wire)
187 187
188 188 def __getattr__(self, name):
189 189 remote_proxy = self._proxy_factory()
190 190 func = _get_proxy_method(remote_proxy, name)
191 191 return _wrap_remote_call(remote_proxy, func)
192 192
193 193
194 194 class RequestScopeProxyFactory(object):
195 195 """
196 196 This factory returns pyro proxy instances based on a per request scope.
197 197 It returns the same instance if called from within the same request and
198 198 different instances if called from different requests.
199 199 """
200 200
201 201 def __init__(self, remote_uri):
202 202 self._remote_uri = remote_uri
203 203 self._proxy_pool = []
204 204 self._borrowed_proxies = {}
205 205
206 206 def __call__(self, request=None):
207 207 """
208 208 Wrapper around `getProxy`.
209 209 """
210 210 request = request or get_current_request()
211 211 return self.getProxy(request)
212 212
213 213 def getProxy(self, request):
214 214 """
215 215 Call this to get the pyro proxy instance for the request.
216 216 """
217 217
218 218 # If called without a request context we return new proxy instances
219 219 # on every call. This allows to run e.g. invoke tasks.
220 220 if request is None:
221 221 log.info('Creating pyro proxy without request context for '
222 222 'remote_uri=%s', self._remote_uri)
223 223 return Pyro4.Proxy(self._remote_uri)
224 224
225 225 # If there is an already borrowed proxy for the request context we
226 226 # return that instance instead of creating a new one.
227 227 if request in self._borrowed_proxies:
228 228 return self._borrowed_proxies[request]
229 229
230 230 # Get proxy from pool or create new instance.
231 231 try:
232 232 proxy = self._proxy_pool.pop()
233 233 except IndexError:
234 234 log.info('Creating pyro proxy for remote_uri=%s', self._remote_uri)
235 235 proxy = Pyro4.Proxy(self._remote_uri)
236 236
237 237 # Mark proxy as borrowed for the request context and add a callback
238 238 # that returns it when the request processing is finished.
239 239 self._borrowed_proxies[request] = proxy
240 240 request.add_finished_callback(self._returnProxy)
241 241
242 242 return proxy
243 243
244 244 def _returnProxy(self, request):
245 245 """
246 246 Callback that gets called by pyramid when the request is finished.
247 247 It puts the proxy back into the pool.
248 248 """
249 249 if request in self._borrowed_proxies:
250 250 proxy = self._borrowed_proxies.pop(request)
251 251 self._proxy_pool.append(proxy)
252 252 else:
253 253 log.warn('Return proxy for remote_uri=%s but no proxy borrowed '
254 254 'for this request.', self._remote_uri)
255 255
256 256
257 257 class RemoteRepo(object):
258 258
259 259 def __init__(self, path, config, remote_proxy, with_wire=None):
260 260 self._wire = {
261 261 "path": path,
262 262 "config": config,
263 263 "context": self._create_vcs_cache_context(),
264 264 }
265 265 if with_wire:
266 266 self._wire.update(with_wire)
267 267 self._remote_proxy = remote_proxy
268 268 self.refs = RefsWrapper(self)
269 269
270 270 def __getattr__(self, name):
271 271 log.debug('Calling %s@%s', self._remote_proxy, name)
272 272 # TODO: oliver: This is currently necessary pre-call since the
273 273 # config object is being changed for hooking scenarios
274 274 wire = copy.deepcopy(self._wire)
275 275 wire["config"] = wire["config"].serialize()
276 276
277 277 try:
278 278 func = _get_proxy_method(self._remote_proxy, name)
279 279 except DaemonError as e:
280 280 if e.message == 'unknown object':
281 281 raise exceptions.VCSBackendNotSupportedError
282 282 else:
283 283 raise
284 284
285 285 return _wrap_remote_call(self._remote_proxy, func, wire)
286 286
287 287 def __getitem__(self, key):
288 288 return self.revision(key)
289 289
290 290 def _create_vcs_cache_context(self):
291 291 """
292 292 Creates a unique string which is passed to the VCSServer on every
293 293 remote call. It is used as cache key in the VCSServer.
294 294 """
295 295 return str(uuid.uuid4())
296 296
297 297 def invalidate_vcs_cache(self):
298 298 """
299 299 This is a no-op method for the pyro4 backend but we want to have the
300 300 same API for client.RemoteRepo and client_http.RemoteRepo classes.
301 301 """
302 302
303 303
304 304 def _get_proxy_method(proxy, name):
305 305 try:
306 306 return getattr(proxy, name)
307 307 except CommunicationError:
308 raise CommunicationError(
308 raise exceptions.PyroVCSCommunicationError(
309 309 'Unable to connect to remote pyro server %s' % proxy)
310 310
311 311
312 312 def _wrap_remote_call(proxy, func, *args):
313 313 all_args = list(args)
314 314
315 315 @exceptions.map_vcs_exceptions
316 316 def caller(*args, **kwargs):
317 317 all_args.extend(args)
318 318 try:
319 319 return func(*all_args, **kwargs)
320 320 except ConnectionClosedError:
321 321 log.debug('Connection to VCSServer closed, trying to reconnect.')
322 322 proxy._pyroReconnect(tries=settings.PYRO_RECONNECT_TRIES)
323 323
324 324 return func(*all_args, **kwargs)
325 325
326 326 return caller
327 327
328 328
329 329 class RefsWrapper(object):
330 330
331 331 def __init__(self, repo):
332 332 self._repo = weakref.proxy(repo)
333 333
334 334 def __setitem__(self, key, value):
335 335 self._repo._assign_ref(key, value)
336 336
337 337
338 338 class FunctionWrapper(object):
339 339
340 340 def __init__(self, func, wire):
341 341 self._func = func
342 342 self._wire = wire
343 343
344 344 @exceptions.map_vcs_exceptions
345 345 def __call__(self, *args, **kwargs):
346 346 return self._func(self._wire, *args, **kwargs)
@@ -1,250 +1,255 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2016-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 Client for the VCSServer implemented based on HTTP.
23 23
24 24
25 25 Status
26 26 ------
27 27
28 28 This client implementation shall eventually replace the Pyro4 based
29 29 implementation.
30 30 """
31 31
32 32 import copy
33 33 import logging
34 34 import threading
35 35 import urllib2
36 36 import urlparse
37 37 import uuid
38 38
39 import pycurl
39 40 import msgpack
40 41 import requests
41 42
42 43 from . import exceptions, CurlSession
43 44
44 45
45 46 log = logging.getLogger(__name__)
46 47
47 48
48 49 # TODO: mikhail: Keep it in sync with vcsserver's
49 50 # HTTPApplication.ALLOWED_EXCEPTIONS
50 51 EXCEPTIONS_MAP = {
51 52 'KeyError': KeyError,
52 53 'URLError': urllib2.URLError,
53 54 }
54 55
55 56
56 57 class RepoMaker(object):
57 58
58 59 def __init__(self, server_and_port, backend_endpoint, session_factory):
59 60 self.url = urlparse.urljoin(
60 61 'http://%s' % server_and_port, backend_endpoint)
61 62 self._session_factory = session_factory
62 63
63 64 def __call__(self, path, config, with_wire=None):
64 65 log.debug('RepoMaker call on %s', path)
65 66 return RemoteRepo(
66 67 path, config, self.url, self._session_factory(),
67 68 with_wire=with_wire)
68 69
69 70 def __getattr__(self, name):
70 71 def f(*args, **kwargs):
71 72 return self._call(name, *args, **kwargs)
72 73 return f
73 74
74 75 @exceptions.map_vcs_exceptions
75 76 def _call(self, name, *args, **kwargs):
76 77 payload = {
77 78 'id': str(uuid.uuid4()),
78 79 'method': name,
79 80 'params': {'args': args, 'kwargs': kwargs}
80 81 }
81 82 return _remote_call(
82 83 self.url, payload, EXCEPTIONS_MAP, self._session_factory())
83 84
84 85
85 86 class RemoteRepo(object):
86 87
87 88 def __init__(self, path, config, url, session, with_wire=None):
88 89 self.url = url
89 90 self._session = session
90 91 self._wire = {
91 92 "path": path,
92 93 "config": config,
93 94 "context": self._create_vcs_cache_context(),
94 95 }
95 96 if with_wire:
96 97 self._wire.update(with_wire)
97 98
98 99 # johbo: Trading complexity for performance. Avoiding the call to
99 100 # log.debug brings a few percent gain even if is is not active.
100 101 if log.isEnabledFor(logging.DEBUG):
101 102 self._call = self._call_with_logging
102 103
103 104 def __getattr__(self, name):
104 105 def f(*args, **kwargs):
105 106 return self._call(name, *args, **kwargs)
106 107 return f
107 108
108 109 @exceptions.map_vcs_exceptions
109 110 def _call(self, name, *args, **kwargs):
110 111 # TODO: oliver: This is currently necessary pre-call since the
111 112 # config object is being changed for hooking scenarios
112 113 wire = copy.deepcopy(self._wire)
113 114 wire["config"] = wire["config"].serialize()
114 115 payload = {
115 116 'id': str(uuid.uuid4()),
116 117 'method': name,
117 118 'params': {'wire': wire, 'args': args, 'kwargs': kwargs}
118 119 }
119 120 return _remote_call(self.url, payload, EXCEPTIONS_MAP, self._session)
120 121
121 122 def _call_with_logging(self, name, *args, **kwargs):
122 123 log.debug('Calling %s@%s', self.url, name)
123 124 return RemoteRepo._call(self, name, *args, **kwargs)
124 125
125 126 def __getitem__(self, key):
126 127 return self.revision(key)
127 128
128 129 def _create_vcs_cache_context(self):
129 130 """
130 131 Creates a unique string which is passed to the VCSServer on every
131 132 remote call. It is used as cache key in the VCSServer.
132 133 """
133 134 return str(uuid.uuid4())
134 135
135 136 def invalidate_vcs_cache(self):
136 137 """
137 138 This invalidates the context which is sent to the VCSServer on every
138 139 call to a remote method. It forces the VCSServer to create a fresh
139 140 repository instance on the next call to a remote method.
140 141 """
141 142 self._wire['context'] = self._create_vcs_cache_context()
142 143
143 144
144 145 class RemoteObject(object):
145 146
146 147 def __init__(self, url, session):
147 148 self._url = url
148 149 self._session = session
149 150
150 151 # johbo: Trading complexity for performance. Avoiding the call to
151 152 # log.debug brings a few percent gain even if is is not active.
152 153 if log.isEnabledFor(logging.DEBUG):
153 154 self._call = self._call_with_logging
154 155
155 156 def __getattr__(self, name):
156 157 def f(*args, **kwargs):
157 158 return self._call(name, *args, **kwargs)
158 159 return f
159 160
160 161 @exceptions.map_vcs_exceptions
161 162 def _call(self, name, *args, **kwargs):
162 163 payload = {
163 164 'id': str(uuid.uuid4()),
164 165 'method': name,
165 166 'params': {'args': args, 'kwargs': kwargs}
166 167 }
167 168 return _remote_call(self._url, payload, EXCEPTIONS_MAP, self._session)
168 169
169 170 def _call_with_logging(self, name, *args, **kwargs):
170 171 log.debug('Calling %s@%s', self._url, name)
171 172 return RemoteObject._call(self, name, *args, **kwargs)
172 173
173 174
174 175 def _remote_call(url, payload, exceptions_map, session):
175 response = session.post(url, data=msgpack.packb(payload))
176 try:
177 response = session.post(url, data=msgpack.packb(payload))
178 except pycurl.error as e:
179 raise exceptions.HttpVCSCommunicationError(e)
180
176 181 response = msgpack.unpackb(response.content)
177 182 error = response.get('error')
178 183 if error:
179 184 type_ = error.get('type', 'Exception')
180 185 exc = exceptions_map.get(type_, Exception)
181 186 exc = exc(error.get('message'))
182 187 try:
183 188 exc._vcs_kind = error['_vcs_kind']
184 189 except KeyError:
185 190 pass
186 191 raise exc
187 192 return response.get('result')
188 193
189 194
190 195 class VcsHttpProxy(object):
191 196
192 197 CHUNK_SIZE = 16384
193 198
194 199 def __init__(self, server_and_port, backend_endpoint):
195 200 adapter = requests.adapters.HTTPAdapter(max_retries=5)
196 201 self.base_url = urlparse.urljoin(
197 202 'http://%s' % server_and_port, backend_endpoint)
198 203 self.session = requests.Session()
199 204 self.session.mount('http://', adapter)
200 205
201 206 def handle(self, environment, input_data, *args, **kwargs):
202 207 data = {
203 208 'environment': environment,
204 209 'input_data': input_data,
205 210 'args': args,
206 211 'kwargs': kwargs
207 212 }
208 213 result = self.session.post(
209 214 self.base_url, msgpack.packb(data), stream=True)
210 215 return self._get_result(result)
211 216
212 217 def _deserialize_and_raise(self, error):
213 218 exception = Exception(error['message'])
214 219 try:
215 220 exception._vcs_kind = error['_vcs_kind']
216 221 except KeyError:
217 222 pass
218 223 raise exception
219 224
220 225 def _iterate(self, result):
221 226 unpacker = msgpack.Unpacker()
222 227 for line in result.iter_content(chunk_size=self.CHUNK_SIZE):
223 228 unpacker.feed(line)
224 229 for chunk in unpacker:
225 230 yield chunk
226 231
227 232 def _get_result(self, result):
228 233 iterator = self._iterate(result)
229 234 error = iterator.next()
230 235 if error:
231 236 self._deserialize_and_raise(error)
232 237
233 238 status = iterator.next()
234 239 headers = iterator.next()
235 240
236 241 return iterator, status, headers
237 242
238 243
239 244 class ThreadlocalSessionFactory(object):
240 245 """
241 246 Creates one CurlSession per thread on demand.
242 247 """
243 248
244 249 def __init__(self):
245 250 self._thread_local = threading.local()
246 251
247 252 def __call__(self):
248 253 if not hasattr(self._thread_local, 'curl_session'):
249 254 self._thread_local.curl_session = CurlSession()
250 255 return self._thread_local.curl_session
@@ -1,186 +1,197 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2014-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 Custom vcs exceptions module.
23 23 """
24 24
25 25 import functools
26 26 import urllib2
27 import pycurl
28 from Pyro4.errors import CommunicationError
29
30 class VCSCommunicationError(Exception):
31 pass
32
33
34 class PyroVCSCommunicationError(VCSCommunicationError):
35 pass
36
37
38 class HttpVCSCommunicationError(VCSCommunicationError):
39 pass
27 40
28 41
29 42 class VCSError(Exception):
30 43 pass
31 44
32 45
33 46 class RepositoryError(VCSError):
34 47 pass
35 48
36 49
37 50 class RepositoryRequirementError(RepositoryError):
38 51 pass
39 52
40 53
41 54 class VCSBackendNotSupportedError(VCSError):
42 55 """
43 56 Exception raised when VCSServer does not support requested backend
44 57 """
45 58
46 59
47 60 class EmptyRepositoryError(RepositoryError):
48 61 pass
49 62
50 63
51 64 class TagAlreadyExistError(RepositoryError):
52 65 pass
53 66
54 67
55 68 class TagDoesNotExistError(RepositoryError):
56 69 pass
57 70
58 71
59 72 class BranchAlreadyExistError(RepositoryError):
60 73 pass
61 74
62 75
63 76 class BranchDoesNotExistError(RepositoryError):
64 77 pass
65 78
66 79
67 80 class CommitError(RepositoryError):
68 81 """
69 82 Exceptions related to an existing commit
70 83 """
71 84
72 85
73 86 class CommitDoesNotExistError(CommitError):
74 87 pass
75 88
76 89
77 90 class CommittingError(RepositoryError):
78 91 """
79 92 Exceptions happening while creating a new commit
80 93 """
81 94
82 95
83 96 class NothingChangedError(CommittingError):
84 97 pass
85 98
86 99
87 100 class NodeError(VCSError):
88 101 pass
89 102
90 103
91 104 class RemovedFileNodeError(NodeError):
92 105 pass
93 106
94 107
95 108 class NodeAlreadyExistsError(CommittingError):
96 109 pass
97 110
98 111
99 112 class NodeAlreadyChangedError(CommittingError):
100 113 pass
101 114
102 115
103 116 class NodeDoesNotExistError(CommittingError):
104 117 pass
105 118
106 119
107 120 class NodeNotChangedError(CommittingError):
108 121 pass
109 122
110 123
111 124 class NodeAlreadyAddedError(CommittingError):
112 125 pass
113 126
114 127
115 128 class NodeAlreadyRemovedError(CommittingError):
116 129 pass
117 130
118 131
119 132 class ImproperArchiveTypeError(VCSError):
120 133 pass
121 134
122 135
123 136 class CommandError(VCSError):
124 137 pass
125 138
126 139
127 140 class UnhandledException(VCSError):
128 141 """
129 142 Signals that something unexpected went wrong.
130 143
131 144 This usually means we have a programming error on the side of the VCSServer
132 145 and should inspect the logfile of the VCSServer to find more details.
133 146 """
134 147
135 148
136 149 _EXCEPTION_MAP = {
137 150 'abort': RepositoryError,
138 151 'archive': ImproperArchiveTypeError,
139 152 'error': RepositoryError,
140 153 'lookup': CommitDoesNotExistError,
141 154 'repo_locked': RepositoryError,
142 155 'requirement': RepositoryRequirementError,
143 156 'unhandled': UnhandledException,
144 157 # TODO: johbo: Define our own exception for this and stop abusing
145 158 # urllib's exception class.
146 159 'url_error': urllib2.URLError,
147 160 }
148 161
149 162
150 163 def map_vcs_exceptions(func):
151 164 """
152 165 Utility to decorate functions so that plain exceptions are translated.
153 166
154 167 The translation is based on `exc_map` which maps a `str` indicating
155 168 the error type into an exception class representing this error inside
156 169 of the vcs layer.
157 170 """
158 171
159 172 @functools.wraps(func)
160 173 def wrapper(*args, **kwargs):
161 174 try:
162 175 return func(*args, **kwargs)
163 176 except Exception as e:
164
165 177 # The error middleware adds information if it finds
166 178 # __traceback_info__ in a frame object. This way the remote
167 179 # traceback information is made available in error reports.
168 180 remote_tb = getattr(e, '_pyroTraceback', None)
169 181 if remote_tb:
170 182 __traceback_info__ = (
171 183 'Found Pyro4 remote traceback information:\n\n' +
172 184 '\n'.join(remote_tb))
173 185
174 186 # Avoid that remote_tb also appears in the frame
175 187 del remote_tb
176 188
177 189 # Special vcs errors had an attribute "_vcs_kind" which is used
178 190 # to translate them to the proper exception class in the vcs
179 191 # client layer.
180 192 kind = getattr(e, '_vcs_kind', None)
181 193 if kind:
182 194 raise _EXCEPTION_MAP[kind](*e.args)
183 195 else:
184 196 raise
185
186 197 return wrapper
1 NO CONTENT: file was removed
1 NO CONTENT: file was removed
General Comments 0
You need to be logged in to leave comments. Login now