##// END OF EJS Templates
http: Add error handling for the repo-locked exception. Part of #4237...
Martin Bornhold -
r85:a0c3f57b default
parent child Browse files
Show More
@@ -1,56 +1,68 b''
1 1 # RhodeCode VCSServer provides access to different vcs backends via network.
2 2 # Copyright (C) 2014-2016 RodeCode GmbH
3 3 #
4 4 # This program is free software; you can redistribute it and/or modify
5 5 # it under the terms of the GNU General Public License as published by
6 6 # the Free Software Foundation; either version 3 of the License, or
7 7 # (at your option) any later version.
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 General Public License
15 15 # along with this program; if not, write to the Free Software Foundation,
16 16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17 17
18 18 """
19 19 Special exception handling over the wire.
20 20
21 21 Since we cannot assume that our client is able to import our exception classes,
22 22 this module provides a "wrapping" mechanism to raise plain exceptions
23 23 which contain an extra attribute `_vcs_kind` to allow a client to distinguish
24 24 different error conditions.
25 25 """
26 26
27 27 import functools
28 from pyramid.httpexceptions import HTTPLocked
28 29
29 30
30 31 def _make_exception(kind, *args):
31 32 """
32 33 Prepares a base `Exception` instance to be sent over the wire.
33 34
34 35 To give our caller a hint what this is about, it will attach an attribute
35 36 `_vcs_kind` to the exception.
36 37 """
37 38 exc = Exception(*args)
38 39 exc._vcs_kind = kind
39 40 return exc
40 41
41 42
42 43 AbortException = functools.partial(_make_exception, 'abort')
43 44
44 45 ArchiveException = functools.partial(_make_exception, 'archive')
45 46
46 47 LookupException = functools.partial(_make_exception, 'lookup')
47 48
48 49 VcsException = functools.partial(_make_exception, 'error')
49 50
50 51 RepositoryLockedException = functools.partial(_make_exception, 'repo_locked')
51 52
52 53 RequirementException = functools.partial(_make_exception, 'requirement')
53 54
54 55 UnhandledException = functools.partial(_make_exception, 'unhandled')
55 56
56 57 URLError = functools.partial(_make_exception, 'url_error')
58
59
60 class HTTPRepoLocked(HTTPLocked):
61 """
62 Subclass of HTTPLocked response that allows to set the title and status
63 code via constructor arguments.
64 """
65 def __init__(self, title, status_code=None, **kwargs):
66 self.code = status_code or HTTPLocked.code
67 self.title = title
68 super(HTTPRepoLocked, self).__init__(**kwargs)
@@ -1,337 +1,358 b''
1 1 # RhodeCode VCSServer provides access to different vcs backends via network.
2 2 # Copyright (C) 2014-2016 RodeCode GmbH
3 3 #
4 4 # This program is free software; you can redistribute it and/or modify
5 5 # it under the terms of the GNU General Public License as published by
6 6 # the Free Software Foundation; either version 3 of the License, or
7 7 # (at your option) any later version.
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 General Public License
15 15 # along with this program; if not, write to the Free Software Foundation,
16 16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17 17
18 18 import base64
19 19 import locale
20 20 import logging
21 21 import uuid
22 22 import wsgiref.util
23 23 from itertools import chain
24 24
25 25 import msgpack
26 26 from beaker.cache import CacheManager
27 27 from beaker.util import parse_cache_config_options
28 28 from pyramid.config import Configurator
29 29 from pyramid.wsgi import wsgiapp
30 30
31 31 from vcsserver import remote_wsgi, scm_app, settings, hgpatches
32 32 from vcsserver.echo_stub import remote_wsgi as remote_wsgi_stub
33 33 from vcsserver.echo_stub.echo_app import EchoApp
34 from vcsserver.exceptions import HTTPRepoLocked
34 35 from vcsserver.server import VcsServer
35 36
36 37 try:
37 38 from vcsserver.git import GitFactory, GitRemote
38 39 except ImportError:
39 40 GitFactory = None
40 41 GitRemote = None
41 42 try:
42 43 from vcsserver.hg import MercurialFactory, HgRemote
43 44 except ImportError:
44 45 MercurialFactory = None
45 46 HgRemote = None
46 47 try:
47 48 from vcsserver.svn import SubversionFactory, SvnRemote
48 49 except ImportError:
49 50 SubversionFactory = None
50 51 SvnRemote = None
51 52
52 53 log = logging.getLogger(__name__)
53 54
54 55
55 56 class VCS(object):
56 57 def __init__(self, locale=None, cache_config=None):
57 58 self.locale = locale
58 59 self.cache_config = cache_config
59 60 self._configure_locale()
60 61 self._initialize_cache()
61 62
62 63 if GitFactory and GitRemote:
63 64 git_repo_cache = self.cache.get_cache_region(
64 65 'git', region='repo_object')
65 66 git_factory = GitFactory(git_repo_cache)
66 67 self._git_remote = GitRemote(git_factory)
67 68 else:
68 69 log.info("Git client import failed")
69 70
70 71 if MercurialFactory and HgRemote:
71 72 hg_repo_cache = self.cache.get_cache_region(
72 73 'hg', region='repo_object')
73 74 hg_factory = MercurialFactory(hg_repo_cache)
74 75 self._hg_remote = HgRemote(hg_factory)
75 76 else:
76 77 log.info("Mercurial client import failed")
77 78
78 79 if SubversionFactory and SvnRemote:
79 80 svn_repo_cache = self.cache.get_cache_region(
80 81 'svn', region='repo_object')
81 82 svn_factory = SubversionFactory(svn_repo_cache)
82 83 self._svn_remote = SvnRemote(svn_factory, hg_factory=hg_factory)
83 84 else:
84 85 log.info("Subversion client import failed")
85 86
86 87 self._vcsserver = VcsServer()
87 88
88 89 def _initialize_cache(self):
89 90 cache_config = parse_cache_config_options(self.cache_config)
90 91 log.info('Initializing beaker cache: %s' % cache_config)
91 92 self.cache = CacheManager(**cache_config)
92 93
93 94 def _configure_locale(self):
94 95 if self.locale:
95 96 log.info('Settings locale: `LC_ALL` to %s' % self.locale)
96 97 else:
97 98 log.info(
98 99 'Configuring locale subsystem based on environment variables')
99 100 try:
100 101 # If self.locale is the empty string, then the locale
101 102 # module will use the environment variables. See the
102 103 # documentation of the package `locale`.
103 104 locale.setlocale(locale.LC_ALL, self.locale)
104 105
105 106 language_code, encoding = locale.getlocale()
106 107 log.info(
107 108 'Locale set to language code "%s" with encoding "%s".',
108 109 language_code, encoding)
109 110 except locale.Error:
110 111 log.exception(
111 112 'Cannot set locale, not configuring the locale system')
112 113
113 114
114 115 class WsgiProxy(object):
115 116 def __init__(self, wsgi):
116 117 self.wsgi = wsgi
117 118
118 119 def __call__(self, environ, start_response):
119 120 input_data = environ['wsgi.input'].read()
120 121 input_data = msgpack.unpackb(input_data)
121 122
122 123 error = None
123 124 try:
124 125 data, status, headers = self.wsgi.handle(
125 126 input_data['environment'], input_data['input_data'],
126 127 *input_data['args'], **input_data['kwargs'])
127 128 except Exception as e:
128 129 data, status, headers = [], None, None
129 130 error = {
130 131 'message': str(e),
131 132 '_vcs_kind': getattr(e, '_vcs_kind', None)
132 133 }
133 134
134 135 start_response(200, {})
135 136 return self._iterator(error, status, headers, data)
136 137
137 138 def _iterator(self, error, status, headers, data):
138 139 initial_data = [
139 140 error,
140 141 status,
141 142 headers,
142 143 ]
143 144
144 145 for d in chain(initial_data, data):
145 146 yield msgpack.packb(d)
146 147
147 148
148 149 class HTTPApplication(object):
149 150 ALLOWED_EXCEPTIONS = ('KeyError', 'URLError')
150 151
151 152 remote_wsgi = remote_wsgi
152 153 _use_echo_app = False
153 154
154 155 def __init__(self, settings=None):
155 156 self.config = Configurator(settings=settings)
156 157 locale = settings.get('', 'en_US.UTF-8')
157 158 vcs = VCS(locale=locale, cache_config=settings)
158 159 self._remotes = {
159 160 'hg': vcs._hg_remote,
160 161 'git': vcs._git_remote,
161 162 'svn': vcs._svn_remote,
162 163 'server': vcs._vcsserver,
163 164 }
164 165 if settings.get('dev.use_echo_app', 'false').lower() == 'true':
165 166 self._use_echo_app = True
166 167 log.warning("Using EchoApp for VCS operations.")
167 168 self.remote_wsgi = remote_wsgi_stub
168 169 self._configure_settings(settings)
169 170 self._configure()
170 171
171 172 def _configure_settings(self, app_settings):
172 173 """
173 174 Configure the settings module.
174 175 """
175 176 git_path = app_settings.get('git_path', None)
176 177 if git_path:
177 178 settings.GIT_EXECUTABLE = git_path
178 179
179 180 def _configure(self):
180 181 self.config.add_renderer(
181 182 name='msgpack',
182 183 factory=self._msgpack_renderer_factory)
183 184
184 185 self.config.add_route('status', '/status')
185 186 self.config.add_route('hg_proxy', '/proxy/hg')
186 187 self.config.add_route('git_proxy', '/proxy/git')
187 188 self.config.add_route('vcs', '/{backend}')
188 189 self.config.add_route('stream_git', '/stream/git/*repo_name')
189 190 self.config.add_route('stream_hg', '/stream/hg/*repo_name')
190 191
191 192 self.config.add_view(
192 193 self.status_view, route_name='status', renderer='json')
193 194 self.config.add_view(self.hg_proxy(), route_name='hg_proxy')
194 195 self.config.add_view(self.git_proxy(), route_name='git_proxy')
195 196 self.config.add_view(
196 197 self.vcs_view, route_name='vcs', renderer='msgpack')
197 198
198 199 self.config.add_view(self.hg_stream(), route_name='stream_hg')
199 200 self.config.add_view(self.git_stream(), route_name='stream_git')
201 self.config.add_view(
202 self.handle_vcs_exception, context=Exception,
203 custom_predicates=[self.is_vcs_exception])
200 204
201 205 def wsgi_app(self):
202 206 return self.config.make_wsgi_app()
203 207
204 208 def vcs_view(self, request):
205 209 remote = self._remotes[request.matchdict['backend']]
206 210 payload = msgpack.unpackb(request.body, use_list=True)
207 211 method = payload.get('method')
208 212 params = payload.get('params')
209 213 wire = params.get('wire')
210 214 args = params.get('args')
211 215 kwargs = params.get('kwargs')
212 216 if wire:
213 217 try:
214 218 wire['context'] = uuid.UUID(wire['context'])
215 219 except KeyError:
216 220 pass
217 221 args.insert(0, wire)
218 222
219 223 try:
220 224 resp = getattr(remote, method)(*args, **kwargs)
221 225 except Exception as e:
222 226 type_ = e.__class__.__name__
223 227 if type_ not in self.ALLOWED_EXCEPTIONS:
224 228 type_ = None
225 229
226 230 resp = {
227 231 'id': payload.get('id'),
228 232 'error': {
229 233 'message': e.message,
230 234 'type': type_
231 235 }
232 236 }
233 237 try:
234 238 resp['error']['_vcs_kind'] = e._vcs_kind
235 239 except AttributeError:
236 240 pass
237 241 else:
238 242 resp = {
239 243 'id': payload.get('id'),
240 244 'result': resp
241 245 }
242 246
243 247 return resp
244 248
245 249 def status_view(self, request):
246 250 return {'status': 'OK'}
247 251
248 252 def _msgpack_renderer_factory(self, info):
249 253 def _render(value, system):
250 254 value = msgpack.packb(value)
251 255 request = system.get('request')
252 256 if request is not None:
253 257 response = request.response
254 258 ct = response.content_type
255 259 if ct == response.default_content_type:
256 260 response.content_type = 'application/x-msgpack'
257 261 return value
258 262 return _render
259 263
260 264 def hg_proxy(self):
261 265 @wsgiapp
262 266 def _hg_proxy(environ, start_response):
263 267 app = WsgiProxy(self.remote_wsgi.HgRemoteWsgi())
264 268 return app(environ, start_response)
265 269 return _hg_proxy
266 270
267 271 def git_proxy(self):
268 272 @wsgiapp
269 273 def _git_proxy(environ, start_response):
270 274 app = WsgiProxy(self.remote_wsgi.GitRemoteWsgi())
271 275 return app(environ, start_response)
272 276 return _git_proxy
273 277
274 278 def hg_stream(self):
275 279 if self._use_echo_app:
276 280 @wsgiapp
277 281 def _hg_stream(environ, start_response):
278 282 app = EchoApp('fake_path', 'fake_name', None)
279 283 return app(environ, start_response)
280 284 return _hg_stream
281 285 else:
282 286 @wsgiapp
283 287 def _hg_stream(environ, start_response):
284 288 repo_path = environ['HTTP_X_RC_REPO_PATH']
285 289 repo_name = environ['HTTP_X_RC_REPO_NAME']
286 290 packed_config = base64.b64decode(
287 291 environ['HTTP_X_RC_REPO_CONFIG'])
288 292 config = msgpack.unpackb(packed_config)
289 293 app = scm_app.create_hg_wsgi_app(
290 294 repo_path, repo_name, config)
291 295
292 296 # Consitent path information for hgweb
293 297 environ['PATH_INFO'] = environ['HTTP_X_RC_PATH_INFO']
294 298 environ['REPO_NAME'] = repo_name
295 299 return app(environ, ResponseFilter(start_response))
296 300 return _hg_stream
297 301
298 302 def git_stream(self):
299 303 if self._use_echo_app:
300 304 @wsgiapp
301 305 def _git_stream(environ, start_response):
302 306 app = EchoApp('fake_path', 'fake_name', None)
303 307 return app(environ, start_response)
304 308 return _git_stream
305 309 else:
306 310 @wsgiapp
307 311 def _git_stream(environ, start_response):
308 312 repo_path = environ['HTTP_X_RC_REPO_PATH']
309 313 repo_name = environ['HTTP_X_RC_REPO_NAME']
310 314 packed_config = base64.b64decode(
311 315 environ['HTTP_X_RC_REPO_CONFIG'])
312 316 config = msgpack.unpackb(packed_config)
313 317
314 318 environ['PATH_INFO'] = environ['HTTP_X_RC_PATH_INFO']
315 319 app = scm_app.create_git_wsgi_app(
316 320 repo_path, repo_name, config)
317 321 return app(environ, start_response)
318 322 return _git_stream
319 323
324 def is_vcs_exception(self, context, request):
325 """
326 View predicate that returns true if the context object is a VCS
327 exception.
328 """
329 return hasattr(context, '_vcs_kind')
330
331 def handle_vcs_exception(self, exception, request):
332 if exception._vcs_kind == 'repo_locked':
333 # Get custom repo-locked status code if present.
334 status_code = request.headers.get('X-RC-Locked-Status-Code')
335 return HTTPRepoLocked(
336 title=exception.message, status_code=status_code)
337
338 # Re-raise exception if we can not handle it.
339 raise exception
340
320 341
321 342 class ResponseFilter(object):
322 343
323 344 def __init__(self, start_response):
324 345 self._start_response = start_response
325 346
326 347 def __call__(self, status, response_headers, exc_info=None):
327 348 headers = tuple(
328 349 (h, v) for h, v in response_headers
329 350 if not wsgiref.util.is_hop_by_hop(h))
330 351 return self._start_response(status, headers, exc_info)
331 352
332 353
333 354 def main(global_config, **settings):
334 355 if MercurialFactory:
335 356 hgpatches.patch_largefiles_capabilities()
336 357 app = HTTPApplication(settings=settings)
337 358 return app.wsgi_app()
General Comments 0
You need to be logged in to leave comments. Login now