##// END OF EJS Templates
metrics: expose exc type for vcsserver
super-admin -
r1014:7b16c3db default
parent child Browse files
Show More
@@ -1,718 +1,720 b''
1 # RhodeCode VCSServer provides access to different vcs backends via network.
1 # RhodeCode VCSServer provides access to different vcs backends via network.
2 # Copyright (C) 2014-2020 RhodeCode GmbH
2 # Copyright (C) 2014-2020 RhodeCode GmbH
3 #
3 #
4 # This program is free software; you can redistribute it and/or modify
4 # This program is free software; you can redistribute it and/or modify
5 # it under the terms of the GNU General Public License as published by
5 # it under the terms of the GNU General Public License as published by
6 # the Free Software Foundation; either version 3 of the License, or
6 # the Free Software Foundation; either version 3 of the License, or
7 # (at your option) any later version.
7 # (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software Foundation,
15 # along with this program; if not, write to the Free Software Foundation,
16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17
17
18 import os
18 import os
19 import sys
19 import sys
20 import base64
20 import base64
21 import locale
21 import locale
22 import logging
22 import logging
23 import uuid
23 import uuid
24 import wsgiref.util
24 import wsgiref.util
25 import traceback
25 import traceback
26 import tempfile
26 import tempfile
27 import psutil
27 import psutil
28 from itertools import chain
28 from itertools import chain
29 from cStringIO import StringIO
29 from cStringIO import StringIO
30
30
31 import simplejson as json
31 import simplejson as json
32 import msgpack
32 import msgpack
33 from pyramid.config import Configurator
33 from pyramid.config import Configurator
34 from pyramid.settings import asbool, aslist
34 from pyramid.settings import asbool, aslist
35 from pyramid.wsgi import wsgiapp
35 from pyramid.wsgi import wsgiapp
36 from pyramid.compat import configparser
36 from pyramid.compat import configparser
37 from pyramid.response import Response
37 from pyramid.response import Response
38
38
39 from vcsserver.utils import safe_int
39 from vcsserver.utils import safe_int
40 from vcsserver.lib.statsd_client import StatsdClient
40 from vcsserver.lib.statsd_client import StatsdClient
41
41
42 log = logging.getLogger(__name__)
42 log = logging.getLogger(__name__)
43
43
44 # due to Mercurial/glibc2.27 problems we need to detect if locale settings are
44 # due to Mercurial/glibc2.27 problems we need to detect if locale settings are
45 # causing problems and "fix" it in case they do and fallback to LC_ALL = C
45 # causing problems and "fix" it in case they do and fallback to LC_ALL = C
46
46
47 try:
47 try:
48 locale.setlocale(locale.LC_ALL, '')
48 locale.setlocale(locale.LC_ALL, '')
49 except locale.Error as e:
49 except locale.Error as e:
50 log.error(
50 log.error(
51 'LOCALE ERROR: failed to set LC_ALL, fallback to LC_ALL=C, org error: %s', e)
51 'LOCALE ERROR: failed to set LC_ALL, fallback to LC_ALL=C, org error: %s', e)
52 os.environ['LC_ALL'] = 'C'
52 os.environ['LC_ALL'] = 'C'
53
53
54 import vcsserver
54 import vcsserver
55 from vcsserver import remote_wsgi, scm_app, settings, hgpatches
55 from vcsserver import remote_wsgi, scm_app, settings, hgpatches
56 from vcsserver.git_lfs.app import GIT_LFS_CONTENT_TYPE, GIT_LFS_PROTO_PAT
56 from vcsserver.git_lfs.app import GIT_LFS_CONTENT_TYPE, GIT_LFS_PROTO_PAT
57 from vcsserver.echo_stub import remote_wsgi as remote_wsgi_stub
57 from vcsserver.echo_stub import remote_wsgi as remote_wsgi_stub
58 from vcsserver.echo_stub.echo_app import EchoApp
58 from vcsserver.echo_stub.echo_app import EchoApp
59 from vcsserver.exceptions import HTTPRepoLocked, HTTPRepoBranchProtected
59 from vcsserver.exceptions import HTTPRepoLocked, HTTPRepoBranchProtected
60 from vcsserver.lib.exc_tracking import store_exception
60 from vcsserver.lib.exc_tracking import store_exception
61 from vcsserver.server import VcsServer
61 from vcsserver.server import VcsServer
62
62
63 try:
63 try:
64 from vcsserver.git import GitFactory, GitRemote
64 from vcsserver.git import GitFactory, GitRemote
65 except ImportError:
65 except ImportError:
66 GitFactory = None
66 GitFactory = None
67 GitRemote = None
67 GitRemote = None
68
68
69 try:
69 try:
70 from vcsserver.hg import MercurialFactory, HgRemote
70 from vcsserver.hg import MercurialFactory, HgRemote
71 except ImportError:
71 except ImportError:
72 MercurialFactory = None
72 MercurialFactory = None
73 HgRemote = None
73 HgRemote = None
74
74
75 try:
75 try:
76 from vcsserver.svn import SubversionFactory, SvnRemote
76 from vcsserver.svn import SubversionFactory, SvnRemote
77 except ImportError:
77 except ImportError:
78 SubversionFactory = None
78 SubversionFactory = None
79 SvnRemote = None
79 SvnRemote = None
80
80
81
81
82 def _is_request_chunked(environ):
82 def _is_request_chunked(environ):
83 stream = environ.get('HTTP_TRANSFER_ENCODING', '') == 'chunked'
83 stream = environ.get('HTTP_TRANSFER_ENCODING', '') == 'chunked'
84 return stream
84 return stream
85
85
86
86
87 def _int_setting(settings, name, default):
87 def _int_setting(settings, name, default):
88 settings[name] = int(settings.get(name, default))
88 settings[name] = int(settings.get(name, default))
89 return settings[name]
89 return settings[name]
90
90
91
91
92 def _bool_setting(settings, name, default):
92 def _bool_setting(settings, name, default):
93 input_val = settings.get(name, default)
93 input_val = settings.get(name, default)
94 if isinstance(input_val, unicode):
94 if isinstance(input_val, unicode):
95 input_val = input_val.encode('utf8')
95 input_val = input_val.encode('utf8')
96 settings[name] = asbool(input_val)
96 settings[name] = asbool(input_val)
97 return settings[name]
97 return settings[name]
98
98
99
99
100 def _list_setting(settings, name, default):
100 def _list_setting(settings, name, default):
101 raw_value = settings.get(name, default)
101 raw_value = settings.get(name, default)
102
102
103 # Otherwise we assume it uses pyramids space/newline separation.
103 # Otherwise we assume it uses pyramids space/newline separation.
104 settings[name] = aslist(raw_value)
104 settings[name] = aslist(raw_value)
105 return settings[name]
105 return settings[name]
106
106
107
107
108 def _string_setting(settings, name, default, lower=True, default_when_empty=False):
108 def _string_setting(settings, name, default, lower=True, default_when_empty=False):
109 value = settings.get(name, default)
109 value = settings.get(name, default)
110
110
111 if default_when_empty and not value:
111 if default_when_empty and not value:
112 # use default value when value is empty
112 # use default value when value is empty
113 value = default
113 value = default
114
114
115 if lower:
115 if lower:
116 value = value.lower()
116 value = value.lower()
117 settings[name] = value
117 settings[name] = value
118 return settings[name]
118 return settings[name]
119
119
120
120
121 def log_max_fd():
121 def log_max_fd():
122 try:
122 try:
123 maxfd = psutil.Process().rlimit(psutil.RLIMIT_NOFILE)[1]
123 maxfd = psutil.Process().rlimit(psutil.RLIMIT_NOFILE)[1]
124 log.info('Max file descriptors value: %s', maxfd)
124 log.info('Max file descriptors value: %s', maxfd)
125 except Exception:
125 except Exception:
126 pass
126 pass
127
127
128
128
129 class VCS(object):
129 class VCS(object):
130 def __init__(self, locale_conf=None, cache_config=None):
130 def __init__(self, locale_conf=None, cache_config=None):
131 self.locale = locale_conf
131 self.locale = locale_conf
132 self.cache_config = cache_config
132 self.cache_config = cache_config
133 self._configure_locale()
133 self._configure_locale()
134
134
135 log_max_fd()
135 log_max_fd()
136
136
137 if GitFactory and GitRemote:
137 if GitFactory and GitRemote:
138 git_factory = GitFactory()
138 git_factory = GitFactory()
139 self._git_remote = GitRemote(git_factory)
139 self._git_remote = GitRemote(git_factory)
140 else:
140 else:
141 log.info("Git client import failed")
141 log.info("Git client import failed")
142
142
143 if MercurialFactory and HgRemote:
143 if MercurialFactory and HgRemote:
144 hg_factory = MercurialFactory()
144 hg_factory = MercurialFactory()
145 self._hg_remote = HgRemote(hg_factory)
145 self._hg_remote = HgRemote(hg_factory)
146 else:
146 else:
147 log.info("Mercurial client import failed")
147 log.info("Mercurial client import failed")
148
148
149 if SubversionFactory and SvnRemote:
149 if SubversionFactory and SvnRemote:
150 svn_factory = SubversionFactory()
150 svn_factory = SubversionFactory()
151
151
152 # hg factory is used for svn url validation
152 # hg factory is used for svn url validation
153 hg_factory = MercurialFactory()
153 hg_factory = MercurialFactory()
154 self._svn_remote = SvnRemote(svn_factory, hg_factory=hg_factory)
154 self._svn_remote = SvnRemote(svn_factory, hg_factory=hg_factory)
155 else:
155 else:
156 log.warning("Subversion client import failed")
156 log.warning("Subversion client import failed")
157
157
158 self._vcsserver = VcsServer()
158 self._vcsserver = VcsServer()
159
159
160 def _configure_locale(self):
160 def _configure_locale(self):
161 if self.locale:
161 if self.locale:
162 log.info('Settings locale: `LC_ALL` to %s', self.locale)
162 log.info('Settings locale: `LC_ALL` to %s', self.locale)
163 else:
163 else:
164 log.info(
164 log.info(
165 'Configuring locale subsystem based on environment variables')
165 'Configuring locale subsystem based on environment variables')
166 try:
166 try:
167 # If self.locale is the empty string, then the locale
167 # If self.locale is the empty string, then the locale
168 # module will use the environment variables. See the
168 # module will use the environment variables. See the
169 # documentation of the package `locale`.
169 # documentation of the package `locale`.
170 locale.setlocale(locale.LC_ALL, self.locale)
170 locale.setlocale(locale.LC_ALL, self.locale)
171
171
172 language_code, encoding = locale.getlocale()
172 language_code, encoding = locale.getlocale()
173 log.info(
173 log.info(
174 'Locale set to language code "%s" with encoding "%s".',
174 'Locale set to language code "%s" with encoding "%s".',
175 language_code, encoding)
175 language_code, encoding)
176 except locale.Error:
176 except locale.Error:
177 log.exception(
177 log.exception(
178 'Cannot set locale, not configuring the locale system')
178 'Cannot set locale, not configuring the locale system')
179
179
180
180
181 class WsgiProxy(object):
181 class WsgiProxy(object):
182 def __init__(self, wsgi):
182 def __init__(self, wsgi):
183 self.wsgi = wsgi
183 self.wsgi = wsgi
184
184
185 def __call__(self, environ, start_response):
185 def __call__(self, environ, start_response):
186 input_data = environ['wsgi.input'].read()
186 input_data = environ['wsgi.input'].read()
187 input_data = msgpack.unpackb(input_data)
187 input_data = msgpack.unpackb(input_data)
188
188
189 error = None
189 error = None
190 try:
190 try:
191 data, status, headers = self.wsgi.handle(
191 data, status, headers = self.wsgi.handle(
192 input_data['environment'], input_data['input_data'],
192 input_data['environment'], input_data['input_data'],
193 *input_data['args'], **input_data['kwargs'])
193 *input_data['args'], **input_data['kwargs'])
194 except Exception as e:
194 except Exception as e:
195 data, status, headers = [], None, None
195 data, status, headers = [], None, None
196 error = {
196 error = {
197 'message': str(e),
197 'message': str(e),
198 '_vcs_kind': getattr(e, '_vcs_kind', None)
198 '_vcs_kind': getattr(e, '_vcs_kind', None)
199 }
199 }
200
200
201 start_response(200, {})
201 start_response(200, {})
202 return self._iterator(error, status, headers, data)
202 return self._iterator(error, status, headers, data)
203
203
204 def _iterator(self, error, status, headers, data):
204 def _iterator(self, error, status, headers, data):
205 initial_data = [
205 initial_data = [
206 error,
206 error,
207 status,
207 status,
208 headers,
208 headers,
209 ]
209 ]
210
210
211 for d in chain(initial_data, data):
211 for d in chain(initial_data, data):
212 yield msgpack.packb(d)
212 yield msgpack.packb(d)
213
213
214
214
215 def not_found(request):
215 def not_found(request):
216 return {'status': '404 NOT FOUND'}
216 return {'status': '404 NOT FOUND'}
217
217
218
218
219 class VCSViewPredicate(object):
219 class VCSViewPredicate(object):
220 def __init__(self, val, config):
220 def __init__(self, val, config):
221 self.remotes = val
221 self.remotes = val
222
222
223 def text(self):
223 def text(self):
224 return 'vcs view method = %s' % (self.remotes.keys(),)
224 return 'vcs view method = %s' % (self.remotes.keys(),)
225
225
226 phash = text
226 phash = text
227
227
228 def __call__(self, context, request):
228 def __call__(self, context, request):
229 """
229 """
230 View predicate that returns true if given backend is supported by
230 View predicate that returns true if given backend is supported by
231 defined remotes.
231 defined remotes.
232 """
232 """
233 backend = request.matchdict.get('backend')
233 backend = request.matchdict.get('backend')
234 return backend in self.remotes
234 return backend in self.remotes
235
235
236
236
237 class HTTPApplication(object):
237 class HTTPApplication(object):
238 ALLOWED_EXCEPTIONS = ('KeyError', 'URLError')
238 ALLOWED_EXCEPTIONS = ('KeyError', 'URLError')
239
239
240 remote_wsgi = remote_wsgi
240 remote_wsgi = remote_wsgi
241 _use_echo_app = False
241 _use_echo_app = False
242
242
243 def __init__(self, settings=None, global_config=None):
243 def __init__(self, settings=None, global_config=None):
244 self._sanitize_settings_and_apply_defaults(settings)
244 self._sanitize_settings_and_apply_defaults(settings)
245
245
246 self.config = Configurator(settings=settings)
246 self.config = Configurator(settings=settings)
247 # Init our statsd at very start
247 # Init our statsd at very start
248 self.config.registry.statsd = StatsdClient.statsd
248 self.config.registry.statsd = StatsdClient.statsd
249
249
250 self.global_config = global_config
250 self.global_config = global_config
251 self.config.include('vcsserver.lib.rc_cache')
251 self.config.include('vcsserver.lib.rc_cache')
252
252
253 settings_locale = settings.get('locale', '') or 'en_US.UTF-8'
253 settings_locale = settings.get('locale', '') or 'en_US.UTF-8'
254 vcs = VCS(locale_conf=settings_locale, cache_config=settings)
254 vcs = VCS(locale_conf=settings_locale, cache_config=settings)
255 self._remotes = {
255 self._remotes = {
256 'hg': vcs._hg_remote,
256 'hg': vcs._hg_remote,
257 'git': vcs._git_remote,
257 'git': vcs._git_remote,
258 'svn': vcs._svn_remote,
258 'svn': vcs._svn_remote,
259 'server': vcs._vcsserver,
259 'server': vcs._vcsserver,
260 }
260 }
261 if settings.get('dev.use_echo_app', 'false').lower() == 'true':
261 if settings.get('dev.use_echo_app', 'false').lower() == 'true':
262 self._use_echo_app = True
262 self._use_echo_app = True
263 log.warning("Using EchoApp for VCS operations.")
263 log.warning("Using EchoApp for VCS operations.")
264 self.remote_wsgi = remote_wsgi_stub
264 self.remote_wsgi = remote_wsgi_stub
265
265
266 self._configure_settings(global_config, settings)
266 self._configure_settings(global_config, settings)
267
267
268 self._configure()
268 self._configure()
269
269
270 def _configure_settings(self, global_config, app_settings):
270 def _configure_settings(self, global_config, app_settings):
271 """
271 """
272 Configure the settings module.
272 Configure the settings module.
273 """
273 """
274 settings_merged = global_config.copy()
274 settings_merged = global_config.copy()
275 settings_merged.update(app_settings)
275 settings_merged.update(app_settings)
276
276
277 git_path = app_settings.get('git_path', None)
277 git_path = app_settings.get('git_path', None)
278 if git_path:
278 if git_path:
279 settings.GIT_EXECUTABLE = git_path
279 settings.GIT_EXECUTABLE = git_path
280 binary_dir = app_settings.get('core.binary_dir', None)
280 binary_dir = app_settings.get('core.binary_dir', None)
281 if binary_dir:
281 if binary_dir:
282 settings.BINARY_DIR = binary_dir
282 settings.BINARY_DIR = binary_dir
283
283
284 # Store the settings to make them available to other modules.
284 # Store the settings to make them available to other modules.
285 vcsserver.PYRAMID_SETTINGS = settings_merged
285 vcsserver.PYRAMID_SETTINGS = settings_merged
286 vcsserver.CONFIG = settings_merged
286 vcsserver.CONFIG = settings_merged
287
287
288 def _sanitize_settings_and_apply_defaults(self, settings):
288 def _sanitize_settings_and_apply_defaults(self, settings):
289 temp_store = tempfile.gettempdir()
289 temp_store = tempfile.gettempdir()
290 default_cache_dir = os.path.join(temp_store, 'rc_cache')
290 default_cache_dir = os.path.join(temp_store, 'rc_cache')
291
291
292 # save default, cache dir, and use it for all backends later.
292 # save default, cache dir, and use it for all backends later.
293 default_cache_dir = _string_setting(
293 default_cache_dir = _string_setting(
294 settings,
294 settings,
295 'cache_dir',
295 'cache_dir',
296 default_cache_dir, lower=False, default_when_empty=True)
296 default_cache_dir, lower=False, default_when_empty=True)
297
297
298 # ensure we have our dir created
298 # ensure we have our dir created
299 if not os.path.isdir(default_cache_dir):
299 if not os.path.isdir(default_cache_dir):
300 os.makedirs(default_cache_dir, mode=0o755)
300 os.makedirs(default_cache_dir, mode=0o755)
301
301
302 # exception store cache
302 # exception store cache
303 _string_setting(
303 _string_setting(
304 settings,
304 settings,
305 'exception_tracker.store_path',
305 'exception_tracker.store_path',
306 temp_store, lower=False, default_when_empty=True)
306 temp_store, lower=False, default_when_empty=True)
307
307
308 # repo_object cache
308 # repo_object cache
309 _string_setting(
309 _string_setting(
310 settings,
310 settings,
311 'rc_cache.repo_object.backend',
311 'rc_cache.repo_object.backend',
312 'dogpile.cache.rc.file_namespace', lower=False)
312 'dogpile.cache.rc.file_namespace', lower=False)
313 _int_setting(
313 _int_setting(
314 settings,
314 settings,
315 'rc_cache.repo_object.expiration_time',
315 'rc_cache.repo_object.expiration_time',
316 30 * 24 * 60 * 60)
316 30 * 24 * 60 * 60)
317 _string_setting(
317 _string_setting(
318 settings,
318 settings,
319 'rc_cache.repo_object.arguments.filename',
319 'rc_cache.repo_object.arguments.filename',
320 os.path.join(default_cache_dir, 'vcsserver_cache_1'), lower=False)
320 os.path.join(default_cache_dir, 'vcsserver_cache_1'), lower=False)
321
321
322 def _configure(self):
322 def _configure(self):
323 self.config.add_renderer(name='msgpack', factory=self._msgpack_renderer_factory)
323 self.config.add_renderer(name='msgpack', factory=self._msgpack_renderer_factory)
324
324
325 self.config.add_route('service', '/_service')
325 self.config.add_route('service', '/_service')
326 self.config.add_route('status', '/status')
326 self.config.add_route('status', '/status')
327 self.config.add_route('hg_proxy', '/proxy/hg')
327 self.config.add_route('hg_proxy', '/proxy/hg')
328 self.config.add_route('git_proxy', '/proxy/git')
328 self.config.add_route('git_proxy', '/proxy/git')
329
329
330 # rpc methods
330 # rpc methods
331 self.config.add_route('vcs', '/{backend}')
331 self.config.add_route('vcs', '/{backend}')
332
332
333 # streaming rpc remote methods
333 # streaming rpc remote methods
334 self.config.add_route('vcs_stream', '/{backend}/stream')
334 self.config.add_route('vcs_stream', '/{backend}/stream')
335
335
336 # vcs operations clone/push as streaming
336 # vcs operations clone/push as streaming
337 self.config.add_route('stream_git', '/stream/git/*repo_name')
337 self.config.add_route('stream_git', '/stream/git/*repo_name')
338 self.config.add_route('stream_hg', '/stream/hg/*repo_name')
338 self.config.add_route('stream_hg', '/stream/hg/*repo_name')
339
339
340 self.config.add_view(self.status_view, route_name='status', renderer='json')
340 self.config.add_view(self.status_view, route_name='status', renderer='json')
341 self.config.add_view(self.service_view, route_name='service', renderer='msgpack')
341 self.config.add_view(self.service_view, route_name='service', renderer='msgpack')
342
342
343 self.config.add_view(self.hg_proxy(), route_name='hg_proxy')
343 self.config.add_view(self.hg_proxy(), route_name='hg_proxy')
344 self.config.add_view(self.git_proxy(), route_name='git_proxy')
344 self.config.add_view(self.git_proxy(), route_name='git_proxy')
345 self.config.add_view(self.vcs_view, route_name='vcs', renderer='msgpack',
345 self.config.add_view(self.vcs_view, route_name='vcs', renderer='msgpack',
346 vcs_view=self._remotes)
346 vcs_view=self._remotes)
347 self.config.add_view(self.vcs_stream_view, route_name='vcs_stream',
347 self.config.add_view(self.vcs_stream_view, route_name='vcs_stream',
348 vcs_view=self._remotes)
348 vcs_view=self._remotes)
349
349
350 self.config.add_view(self.hg_stream(), route_name='stream_hg')
350 self.config.add_view(self.hg_stream(), route_name='stream_hg')
351 self.config.add_view(self.git_stream(), route_name='stream_git')
351 self.config.add_view(self.git_stream(), route_name='stream_git')
352
352
353 self.config.add_view_predicate('vcs_view', VCSViewPredicate)
353 self.config.add_view_predicate('vcs_view', VCSViewPredicate)
354
354
355 self.config.add_notfound_view(not_found, renderer='json')
355 self.config.add_notfound_view(not_found, renderer='json')
356
356
357 self.config.add_view(self.handle_vcs_exception, context=Exception)
357 self.config.add_view(self.handle_vcs_exception, context=Exception)
358
358
359 self.config.add_tween(
359 self.config.add_tween(
360 'vcsserver.tweens.request_wrapper.RequestWrapperTween',
360 'vcsserver.tweens.request_wrapper.RequestWrapperTween',
361 )
361 )
362 self.config.add_request_method(
362 self.config.add_request_method(
363 'vcsserver.lib.request_counter.get_request_counter',
363 'vcsserver.lib.request_counter.get_request_counter',
364 'request_count')
364 'request_count')
365
365
366 def wsgi_app(self):
366 def wsgi_app(self):
367 return self.config.make_wsgi_app()
367 return self.config.make_wsgi_app()
368
368
369 def _vcs_view_params(self, request):
369 def _vcs_view_params(self, request):
370 remote = self._remotes[request.matchdict['backend']]
370 remote = self._remotes[request.matchdict['backend']]
371 payload = msgpack.unpackb(request.body, use_list=True)
371 payload = msgpack.unpackb(request.body, use_list=True)
372 method = payload.get('method')
372 method = payload.get('method')
373 params = payload['params']
373 params = payload['params']
374 wire = params.get('wire')
374 wire = params.get('wire')
375 args = params.get('args')
375 args = params.get('args')
376 kwargs = params.get('kwargs')
376 kwargs = params.get('kwargs')
377 context_uid = None
377 context_uid = None
378
378
379 if wire:
379 if wire:
380 try:
380 try:
381 wire['context'] = context_uid = uuid.UUID(wire['context'])
381 wire['context'] = context_uid = uuid.UUID(wire['context'])
382 except KeyError:
382 except KeyError:
383 pass
383 pass
384 args.insert(0, wire)
384 args.insert(0, wire)
385 repo_state_uid = wire.get('repo_state_uid') if wire else None
385 repo_state_uid = wire.get('repo_state_uid') if wire else None
386
386
387 # NOTE(marcink): trading complexity for slight performance
387 # NOTE(marcink): trading complexity for slight performance
388 if log.isEnabledFor(logging.DEBUG):
388 if log.isEnabledFor(logging.DEBUG):
389 no_args_methods = [
389 no_args_methods = [
390
390
391 ]
391 ]
392 if method in no_args_methods:
392 if method in no_args_methods:
393 call_args = ''
393 call_args = ''
394 else:
394 else:
395 call_args = args[1:]
395 call_args = args[1:]
396
396
397 log.debug('Method requested:`%s` with args:%s kwargs:%s context_uid: %s, repo_state_uid:%s',
397 log.debug('Method requested:`%s` with args:%s kwargs:%s context_uid: %s, repo_state_uid:%s',
398 method, call_args, kwargs, context_uid, repo_state_uid)
398 method, call_args, kwargs, context_uid, repo_state_uid)
399
399
400 statsd = request.registry.statsd
400 statsd = request.registry.statsd
401 if statsd:
401 if statsd:
402 statsd.incr(
402 statsd.incr(
403 'vcsserver_method_total', tags=[
403 'vcsserver_method_total', tags=[
404 "method:{}".format(method),
404 "method:{}".format(method),
405 ])
405 ])
406 return payload, remote, method, args, kwargs
406 return payload, remote, method, args, kwargs
407
407
408 def vcs_view(self, request):
408 def vcs_view(self, request):
409
409
410 payload, remote, method, args, kwargs = self._vcs_view_params(request)
410 payload, remote, method, args, kwargs = self._vcs_view_params(request)
411 payload_id = payload.get('id')
411 payload_id = payload.get('id')
412
412
413 try:
413 try:
414 resp = getattr(remote, method)(*args, **kwargs)
414 resp = getattr(remote, method)(*args, **kwargs)
415 except Exception as e:
415 except Exception as e:
416 exc_info = list(sys.exc_info())
416 exc_info = list(sys.exc_info())
417 exc_type, exc_value, exc_traceback = exc_info
417 exc_type, exc_value, exc_traceback = exc_info
418
418
419 org_exc = getattr(e, '_org_exc', None)
419 org_exc = getattr(e, '_org_exc', None)
420 org_exc_name = None
420 org_exc_name = None
421 org_exc_tb = ''
421 org_exc_tb = ''
422 if org_exc:
422 if org_exc:
423 org_exc_name = org_exc.__class__.__name__
423 org_exc_name = org_exc.__class__.__name__
424 org_exc_tb = getattr(e, '_org_exc_tb', '')
424 org_exc_tb = getattr(e, '_org_exc_tb', '')
425 # replace our "faked" exception with our org
425 # replace our "faked" exception with our org
426 exc_info[0] = org_exc.__class__
426 exc_info[0] = org_exc.__class__
427 exc_info[1] = org_exc
427 exc_info[1] = org_exc
428
428
429 should_store_exc = True
429 should_store_exc = True
430 if org_exc:
430 if org_exc:
431 def get_exc_fqn(_exc_obj):
431 def get_exc_fqn(_exc_obj):
432 module_name = getattr(org_exc.__class__, '__module__', 'UNKNOWN')
432 module_name = getattr(org_exc.__class__, '__module__', 'UNKNOWN')
433 return module_name + '.' + org_exc_name
433 return module_name + '.' + org_exc_name
434
434
435 exc_fqn = get_exc_fqn(org_exc)
435 exc_fqn = get_exc_fqn(org_exc)
436
436
437 if exc_fqn in ['mercurial.error.RepoLookupError',
437 if exc_fqn in ['mercurial.error.RepoLookupError',
438 'vcsserver.exceptions.RefNotFoundException']:
438 'vcsserver.exceptions.RefNotFoundException']:
439 should_store_exc = False
439 should_store_exc = False
440
440
441 if should_store_exc:
441 if should_store_exc:
442 store_exception(id(exc_info), exc_info, request_path=request.path)
442 store_exception(id(exc_info), exc_info, request_path=request.path)
443
443
444 tb_info = ''.join(
444 tb_info = ''.join(
445 traceback.format_exception(exc_type, exc_value, exc_traceback))
445 traceback.format_exception(exc_type, exc_value, exc_traceback))
446
446
447 type_ = e.__class__.__name__
447 type_ = e.__class__.__name__
448 if type_ not in self.ALLOWED_EXCEPTIONS:
448 if type_ not in self.ALLOWED_EXCEPTIONS:
449 type_ = None
449 type_ = None
450
450
451 resp = {
451 resp = {
452 'id': payload_id,
452 'id': payload_id,
453 'error': {
453 'error': {
454 'message': e.message,
454 'message': e.message,
455 'traceback': tb_info,
455 'traceback': tb_info,
456 'org_exc': org_exc_name,
456 'org_exc': org_exc_name,
457 'org_exc_tb': org_exc_tb,
457 'org_exc_tb': org_exc_tb,
458 'type': type_
458 'type': type_
459 }
459 }
460 }
460 }
461
461
462 try:
462 try:
463 resp['error']['_vcs_kind'] = getattr(e, '_vcs_kind', None)
463 resp['error']['_vcs_kind'] = getattr(e, '_vcs_kind', None)
464 except AttributeError:
464 except AttributeError:
465 pass
465 pass
466 else:
466 else:
467 resp = {
467 resp = {
468 'id': payload_id,
468 'id': payload_id,
469 'result': resp
469 'result': resp
470 }
470 }
471
471
472 return resp
472 return resp
473
473
474 def vcs_stream_view(self, request):
474 def vcs_stream_view(self, request):
475 payload, remote, method, args, kwargs = self._vcs_view_params(request)
475 payload, remote, method, args, kwargs = self._vcs_view_params(request)
476 # this method has a stream: marker we remove it here
476 # this method has a stream: marker we remove it here
477 method = method.split('stream:')[-1]
477 method = method.split('stream:')[-1]
478 chunk_size = safe_int(payload.get('chunk_size')) or 4096
478 chunk_size = safe_int(payload.get('chunk_size')) or 4096
479
479
480 try:
480 try:
481 resp = getattr(remote, method)(*args, **kwargs)
481 resp = getattr(remote, method)(*args, **kwargs)
482 except Exception as e:
482 except Exception as e:
483 raise
483 raise
484
484
485 def get_chunked_data(method_resp):
485 def get_chunked_data(method_resp):
486 stream = StringIO(method_resp)
486 stream = StringIO(method_resp)
487 while 1:
487 while 1:
488 chunk = stream.read(chunk_size)
488 chunk = stream.read(chunk_size)
489 if not chunk:
489 if not chunk:
490 break
490 break
491 yield chunk
491 yield chunk
492
492
493 response = Response(app_iter=get_chunked_data(resp))
493 response = Response(app_iter=get_chunked_data(resp))
494 response.content_type = 'application/octet-stream'
494 response.content_type = 'application/octet-stream'
495
495
496 return response
496 return response
497
497
498 def status_view(self, request):
498 def status_view(self, request):
499 import vcsserver
499 import vcsserver
500 return {'status': 'OK', 'vcsserver_version': vcsserver.__version__,
500 return {'status': 'OK', 'vcsserver_version': vcsserver.__version__,
501 'pid': os.getpid()}
501 'pid': os.getpid()}
502
502
503 def service_view(self, request):
503 def service_view(self, request):
504 import vcsserver
504 import vcsserver
505
505
506 payload = msgpack.unpackb(request.body, use_list=True)
506 payload = msgpack.unpackb(request.body, use_list=True)
507 server_config, app_config = {}, {}
507 server_config, app_config = {}, {}
508
508
509 try:
509 try:
510 path = self.global_config['__file__']
510 path = self.global_config['__file__']
511 config = configparser.RawConfigParser()
511 config = configparser.RawConfigParser()
512
512
513 config.read(path)
513 config.read(path)
514
514
515 if config.has_section('server:main'):
515 if config.has_section('server:main'):
516 server_config = dict(config.items('server:main'))
516 server_config = dict(config.items('server:main'))
517 if config.has_section('app:main'):
517 if config.has_section('app:main'):
518 app_config = dict(config.items('app:main'))
518 app_config = dict(config.items('app:main'))
519
519
520 except Exception:
520 except Exception:
521 log.exception('Failed to read .ini file for display')
521 log.exception('Failed to read .ini file for display')
522
522
523 environ = os.environ.items()
523 environ = os.environ.items()
524
524
525 resp = {
525 resp = {
526 'id': payload.get('id'),
526 'id': payload.get('id'),
527 'result': dict(
527 'result': dict(
528 version=vcsserver.__version__,
528 version=vcsserver.__version__,
529 config=server_config,
529 config=server_config,
530 app_config=app_config,
530 app_config=app_config,
531 environ=environ,
531 environ=environ,
532 payload=payload,
532 payload=payload,
533 )
533 )
534 }
534 }
535 return resp
535 return resp
536
536
537 def _msgpack_renderer_factory(self, info):
537 def _msgpack_renderer_factory(self, info):
538 def _render(value, system):
538 def _render(value, system):
539 request = system.get('request')
539 request = system.get('request')
540 if request is not None:
540 if request is not None:
541 response = request.response
541 response = request.response
542 ct = response.content_type
542 ct = response.content_type
543 if ct == response.default_content_type:
543 if ct == response.default_content_type:
544 response.content_type = 'application/x-msgpack'
544 response.content_type = 'application/x-msgpack'
545 return msgpack.packb(value)
545 return msgpack.packb(value)
546 return _render
546 return _render
547
547
548 def set_env_from_config(self, environ, config):
548 def set_env_from_config(self, environ, config):
549 dict_conf = {}
549 dict_conf = {}
550 try:
550 try:
551 for elem in config:
551 for elem in config:
552 if elem[0] == 'rhodecode':
552 if elem[0] == 'rhodecode':
553 dict_conf = json.loads(elem[2])
553 dict_conf = json.loads(elem[2])
554 break
554 break
555 except Exception:
555 except Exception:
556 log.exception('Failed to fetch SCM CONFIG')
556 log.exception('Failed to fetch SCM CONFIG')
557 return
557 return
558
558
559 username = dict_conf.get('username')
559 username = dict_conf.get('username')
560 if username:
560 if username:
561 environ['REMOTE_USER'] = username
561 environ['REMOTE_USER'] = username
562 # mercurial specific, some extension api rely on this
562 # mercurial specific, some extension api rely on this
563 environ['HGUSER'] = username
563 environ['HGUSER'] = username
564
564
565 ip = dict_conf.get('ip')
565 ip = dict_conf.get('ip')
566 if ip:
566 if ip:
567 environ['REMOTE_HOST'] = ip
567 environ['REMOTE_HOST'] = ip
568
568
569 if _is_request_chunked(environ):
569 if _is_request_chunked(environ):
570 # set the compatibility flag for webob
570 # set the compatibility flag for webob
571 environ['wsgi.input_terminated'] = True
571 environ['wsgi.input_terminated'] = True
572
572
573 def hg_proxy(self):
573 def hg_proxy(self):
574 @wsgiapp
574 @wsgiapp
575 def _hg_proxy(environ, start_response):
575 def _hg_proxy(environ, start_response):
576 app = WsgiProxy(self.remote_wsgi.HgRemoteWsgi())
576 app = WsgiProxy(self.remote_wsgi.HgRemoteWsgi())
577 return app(environ, start_response)
577 return app(environ, start_response)
578 return _hg_proxy
578 return _hg_proxy
579
579
580 def git_proxy(self):
580 def git_proxy(self):
581 @wsgiapp
581 @wsgiapp
582 def _git_proxy(environ, start_response):
582 def _git_proxy(environ, start_response):
583 app = WsgiProxy(self.remote_wsgi.GitRemoteWsgi())
583 app = WsgiProxy(self.remote_wsgi.GitRemoteWsgi())
584 return app(environ, start_response)
584 return app(environ, start_response)
585 return _git_proxy
585 return _git_proxy
586
586
587 def hg_stream(self):
587 def hg_stream(self):
588 if self._use_echo_app:
588 if self._use_echo_app:
589 @wsgiapp
589 @wsgiapp
590 def _hg_stream(environ, start_response):
590 def _hg_stream(environ, start_response):
591 app = EchoApp('fake_path', 'fake_name', None)
591 app = EchoApp('fake_path', 'fake_name', None)
592 return app(environ, start_response)
592 return app(environ, start_response)
593 return _hg_stream
593 return _hg_stream
594 else:
594 else:
595 @wsgiapp
595 @wsgiapp
596 def _hg_stream(environ, start_response):
596 def _hg_stream(environ, start_response):
597 log.debug('http-app: handling hg stream')
597 log.debug('http-app: handling hg stream')
598 repo_path = environ['HTTP_X_RC_REPO_PATH']
598 repo_path = environ['HTTP_X_RC_REPO_PATH']
599 repo_name = environ['HTTP_X_RC_REPO_NAME']
599 repo_name = environ['HTTP_X_RC_REPO_NAME']
600 packed_config = base64.b64decode(
600 packed_config = base64.b64decode(
601 environ['HTTP_X_RC_REPO_CONFIG'])
601 environ['HTTP_X_RC_REPO_CONFIG'])
602 config = msgpack.unpackb(packed_config)
602 config = msgpack.unpackb(packed_config)
603 app = scm_app.create_hg_wsgi_app(
603 app = scm_app.create_hg_wsgi_app(
604 repo_path, repo_name, config)
604 repo_path, repo_name, config)
605
605
606 # Consistent path information for hgweb
606 # Consistent path information for hgweb
607 environ['PATH_INFO'] = environ['HTTP_X_RC_PATH_INFO']
607 environ['PATH_INFO'] = environ['HTTP_X_RC_PATH_INFO']
608 environ['REPO_NAME'] = repo_name
608 environ['REPO_NAME'] = repo_name
609 self.set_env_from_config(environ, config)
609 self.set_env_from_config(environ, config)
610
610
611 log.debug('http-app: starting app handler '
611 log.debug('http-app: starting app handler '
612 'with %s and process request', app)
612 'with %s and process request', app)
613 return app(environ, ResponseFilter(start_response))
613 return app(environ, ResponseFilter(start_response))
614 return _hg_stream
614 return _hg_stream
615
615
616 def git_stream(self):
616 def git_stream(self):
617 if self._use_echo_app:
617 if self._use_echo_app:
618 @wsgiapp
618 @wsgiapp
619 def _git_stream(environ, start_response):
619 def _git_stream(environ, start_response):
620 app = EchoApp('fake_path', 'fake_name', None)
620 app = EchoApp('fake_path', 'fake_name', None)
621 return app(environ, start_response)
621 return app(environ, start_response)
622 return _git_stream
622 return _git_stream
623 else:
623 else:
624 @wsgiapp
624 @wsgiapp
625 def _git_stream(environ, start_response):
625 def _git_stream(environ, start_response):
626 log.debug('http-app: handling git stream')
626 log.debug('http-app: handling git stream')
627 repo_path = environ['HTTP_X_RC_REPO_PATH']
627 repo_path = environ['HTTP_X_RC_REPO_PATH']
628 repo_name = environ['HTTP_X_RC_REPO_NAME']
628 repo_name = environ['HTTP_X_RC_REPO_NAME']
629 packed_config = base64.b64decode(
629 packed_config = base64.b64decode(
630 environ['HTTP_X_RC_REPO_CONFIG'])
630 environ['HTTP_X_RC_REPO_CONFIG'])
631 config = msgpack.unpackb(packed_config)
631 config = msgpack.unpackb(packed_config)
632
632
633 environ['PATH_INFO'] = environ['HTTP_X_RC_PATH_INFO']
633 environ['PATH_INFO'] = environ['HTTP_X_RC_PATH_INFO']
634 self.set_env_from_config(environ, config)
634 self.set_env_from_config(environ, config)
635
635
636 content_type = environ.get('CONTENT_TYPE', '')
636 content_type = environ.get('CONTENT_TYPE', '')
637
637
638 path = environ['PATH_INFO']
638 path = environ['PATH_INFO']
639 is_lfs_request = GIT_LFS_CONTENT_TYPE in content_type
639 is_lfs_request = GIT_LFS_CONTENT_TYPE in content_type
640 log.debug(
640 log.debug(
641 'LFS: Detecting if request `%s` is LFS server path based '
641 'LFS: Detecting if request `%s` is LFS server path based '
642 'on content type:`%s`, is_lfs:%s',
642 'on content type:`%s`, is_lfs:%s',
643 path, content_type, is_lfs_request)
643 path, content_type, is_lfs_request)
644
644
645 if not is_lfs_request:
645 if not is_lfs_request:
646 # fallback detection by path
646 # fallback detection by path
647 if GIT_LFS_PROTO_PAT.match(path):
647 if GIT_LFS_PROTO_PAT.match(path):
648 is_lfs_request = True
648 is_lfs_request = True
649 log.debug(
649 log.debug(
650 'LFS: fallback detection by path of: `%s`, is_lfs:%s',
650 'LFS: fallback detection by path of: `%s`, is_lfs:%s',
651 path, is_lfs_request)
651 path, is_lfs_request)
652
652
653 if is_lfs_request:
653 if is_lfs_request:
654 app = scm_app.create_git_lfs_wsgi_app(
654 app = scm_app.create_git_lfs_wsgi_app(
655 repo_path, repo_name, config)
655 repo_path, repo_name, config)
656 else:
656 else:
657 app = scm_app.create_git_wsgi_app(
657 app = scm_app.create_git_wsgi_app(
658 repo_path, repo_name, config)
658 repo_path, repo_name, config)
659
659
660 log.debug('http-app: starting app handler '
660 log.debug('http-app: starting app handler '
661 'with %s and process request', app)
661 'with %s and process request', app)
662
662
663 return app(environ, start_response)
663 return app(environ, start_response)
664
664
665 return _git_stream
665 return _git_stream
666
666
667 def handle_vcs_exception(self, exception, request):
667 def handle_vcs_exception(self, exception, request):
668 _vcs_kind = getattr(exception, '_vcs_kind', '')
668 _vcs_kind = getattr(exception, '_vcs_kind', '')
669 if _vcs_kind == 'repo_locked':
669 if _vcs_kind == 'repo_locked':
670 # Get custom repo-locked status code if present.
670 # Get custom repo-locked status code if present.
671 status_code = request.headers.get('X-RC-Locked-Status-Code')
671 status_code = request.headers.get('X-RC-Locked-Status-Code')
672 return HTTPRepoLocked(
672 return HTTPRepoLocked(
673 title=exception.message, status_code=status_code)
673 title=exception.message, status_code=status_code)
674
674
675 elif _vcs_kind == 'repo_branch_protected':
675 elif _vcs_kind == 'repo_branch_protected':
676 # Get custom repo-branch-protected status code if present.
676 # Get custom repo-branch-protected status code if present.
677 return HTTPRepoBranchProtected(title=exception.message)
677 return HTTPRepoBranchProtected(title=exception.message)
678
678
679 exc_info = request.exc_info
679 exc_info = request.exc_info
680 store_exception(id(exc_info), exc_info)
680 store_exception(id(exc_info), exc_info)
681
681
682 traceback_info = 'unavailable'
682 traceback_info = 'unavailable'
683 if request.exc_info:
683 if request.exc_info:
684 exc_type, exc_value, exc_tb = request.exc_info
684 exc_type, exc_value, exc_tb = request.exc_info
685 traceback_info = ''.join(traceback.format_exception(exc_type, exc_value, exc_tb))
685 traceback_info = ''.join(traceback.format_exception(exc_type, exc_value, exc_tb))
686
686
687 log.error(
687 log.error(
688 'error occurred handling this request for path: %s, \n tb: %s',
688 'error occurred handling this request for path: %s, \n tb: %s',
689 request.path, traceback_info)
689 request.path, traceback_info)
690
690
691 statsd = request.registry.statsd
691 statsd = request.registry.statsd
692 if statsd:
692 if statsd:
693 statsd.incr('vcsserver_exception_total')
693 exc_type = "{}.{}".format(exception.__class__.__module__, exception.__class__.__name__)
694 statsd.incr('vcsserver_exception_total',
695 tags=["type:{}".format(exc_type)])
694 raise exception
696 raise exception
695
697
696
698
697 class ResponseFilter(object):
699 class ResponseFilter(object):
698
700
699 def __init__(self, start_response):
701 def __init__(self, start_response):
700 self._start_response = start_response
702 self._start_response = start_response
701
703
702 def __call__(self, status, response_headers, exc_info=None):
704 def __call__(self, status, response_headers, exc_info=None):
703 headers = tuple(
705 headers = tuple(
704 (h, v) for h, v in response_headers
706 (h, v) for h, v in response_headers
705 if not wsgiref.util.is_hop_by_hop(h))
707 if not wsgiref.util.is_hop_by_hop(h))
706 return self._start_response(status, headers, exc_info)
708 return self._start_response(status, headers, exc_info)
707
709
708
710
709 def main(global_config, **settings):
711 def main(global_config, **settings):
710 if MercurialFactory:
712 if MercurialFactory:
711 hgpatches.patch_largefiles_capabilities()
713 hgpatches.patch_largefiles_capabilities()
712 hgpatches.patch_subrepo_type_mapping()
714 hgpatches.patch_subrepo_type_mapping()
713
715
714 # init and bootstrap StatsdClient
716 # init and bootstrap StatsdClient
715 StatsdClient.setup(settings)
717 StatsdClient.setup(settings)
716
718
717 app = HTTPApplication(settings=settings, global_config=global_config)
719 app = HTTPApplication(settings=settings, global_config=global_config)
718 return app.wsgi_app()
720 return app.wsgi_app()
General Comments 0
You need to be logged in to leave comments. Login now