##// END OF EJS Templates
logging: skip large attribute expansion on archive_repo. We don't need arguments there
marcink -
r742:45bacab2 default
parent child Browse files
Show More
@@ -1,611 +1,620 b''
1 1 # RhodeCode VCSServer provides access to different vcs backends via network.
2 2 # Copyright (C) 2014-2019 RhodeCode 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 os
19 19 import sys
20 20 import base64
21 21 import locale
22 22 import logging
23 23 import uuid
24 24 import wsgiref.util
25 25 import traceback
26 26 import tempfile
27 27 from itertools import chain
28 28
29 29 import simplejson as json
30 30 import msgpack
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.compat import configparser
35 35
36 36
37 37 log = logging.getLogger(__name__)
38 38
39 39 # due to Mercurial/glibc2.27 problems we need to detect if locale settings are
40 40 # causing problems and "fix" it in case they do and fallback to LC_ALL = C
41 41
42 42 try:
43 43 locale.setlocale(locale.LC_ALL, '')
44 44 except locale.Error as e:
45 45 log.error(
46 46 'LOCALE ERROR: failed to set LC_ALL, fallback to LC_ALL=C, org error: %s', e)
47 47 os.environ['LC_ALL'] = 'C'
48 48
49 49 import vcsserver
50 50 from vcsserver import remote_wsgi, scm_app, settings, hgpatches
51 51 from vcsserver.git_lfs.app import GIT_LFS_CONTENT_TYPE, GIT_LFS_PROTO_PAT
52 52 from vcsserver.echo_stub import remote_wsgi as remote_wsgi_stub
53 53 from vcsserver.echo_stub.echo_app import EchoApp
54 54 from vcsserver.exceptions import HTTPRepoLocked, HTTPRepoBranchProtected
55 55 from vcsserver.lib.exc_tracking import store_exception
56 56 from vcsserver.server import VcsServer
57 57
58 58 try:
59 59 from vcsserver.git import GitFactory, GitRemote
60 60 except ImportError:
61 61 GitFactory = None
62 62 GitRemote = None
63 63
64 64 try:
65 65 from vcsserver.hg import MercurialFactory, HgRemote
66 66 except ImportError:
67 67 MercurialFactory = None
68 68 HgRemote = None
69 69
70 70 try:
71 71 from vcsserver.svn import SubversionFactory, SvnRemote
72 72 except ImportError:
73 73 SubversionFactory = None
74 74 SvnRemote = None
75 75
76 76
77 77 def _is_request_chunked(environ):
78 78 stream = environ.get('HTTP_TRANSFER_ENCODING', '') == 'chunked'
79 79 return stream
80 80
81 81
82 82 def _int_setting(settings, name, default):
83 83 settings[name] = int(settings.get(name, default))
84 84 return settings[name]
85 85
86 86
87 87 def _bool_setting(settings, name, default):
88 88 input_val = settings.get(name, default)
89 89 if isinstance(input_val, unicode):
90 90 input_val = input_val.encode('utf8')
91 91 settings[name] = asbool(input_val)
92 92 return settings[name]
93 93
94 94
95 95 def _list_setting(settings, name, default):
96 96 raw_value = settings.get(name, default)
97 97
98 98 # Otherwise we assume it uses pyramids space/newline separation.
99 99 settings[name] = aslist(raw_value)
100 100 return settings[name]
101 101
102 102
103 103 def _string_setting(settings, name, default, lower=True, default_when_empty=False):
104 104 value = settings.get(name, default)
105 105
106 106 if default_when_empty and not value:
107 107 # use default value when value is empty
108 108 value = default
109 109
110 110 if lower:
111 111 value = value.lower()
112 112 settings[name] = value
113 113 return settings[name]
114 114
115 115
116 116 class VCS(object):
117 117 def __init__(self, locale=None, cache_config=None):
118 118 self.locale = locale
119 119 self.cache_config = cache_config
120 120 self._configure_locale()
121 121
122 122 if GitFactory and GitRemote:
123 123 git_factory = GitFactory()
124 124 self._git_remote = GitRemote(git_factory)
125 125 else:
126 126 log.info("Git client import failed")
127 127
128 128 if MercurialFactory and HgRemote:
129 129 hg_factory = MercurialFactory()
130 130 self._hg_remote = HgRemote(hg_factory)
131 131 else:
132 132 log.info("Mercurial client import failed")
133 133
134 134 if SubversionFactory and SvnRemote:
135 135 svn_factory = SubversionFactory()
136 136
137 137 # hg factory is used for svn url validation
138 138 hg_factory = MercurialFactory()
139 139 self._svn_remote = SvnRemote(svn_factory, hg_factory=hg_factory)
140 140 else:
141 141 log.info("Subversion client import failed")
142 142
143 143 self._vcsserver = VcsServer()
144 144
145 145 def _configure_locale(self):
146 146 if self.locale:
147 147 log.info('Settings locale: `LC_ALL` to %s', self.locale)
148 148 else:
149 149 log.info(
150 150 'Configuring locale subsystem based on environment variables')
151 151 try:
152 152 # If self.locale is the empty string, then the locale
153 153 # module will use the environment variables. See the
154 154 # documentation of the package `locale`.
155 155 locale.setlocale(locale.LC_ALL, self.locale)
156 156
157 157 language_code, encoding = locale.getlocale()
158 158 log.info(
159 159 'Locale set to language code "%s" with encoding "%s".',
160 160 language_code, encoding)
161 161 except locale.Error:
162 162 log.exception(
163 163 'Cannot set locale, not configuring the locale system')
164 164
165 165
166 166 class WsgiProxy(object):
167 167 def __init__(self, wsgi):
168 168 self.wsgi = wsgi
169 169
170 170 def __call__(self, environ, start_response):
171 171 input_data = environ['wsgi.input'].read()
172 172 input_data = msgpack.unpackb(input_data)
173 173
174 174 error = None
175 175 try:
176 176 data, status, headers = self.wsgi.handle(
177 177 input_data['environment'], input_data['input_data'],
178 178 *input_data['args'], **input_data['kwargs'])
179 179 except Exception as e:
180 180 data, status, headers = [], None, None
181 181 error = {
182 182 'message': str(e),
183 183 '_vcs_kind': getattr(e, '_vcs_kind', None)
184 184 }
185 185
186 186 start_response(200, {})
187 187 return self._iterator(error, status, headers, data)
188 188
189 189 def _iterator(self, error, status, headers, data):
190 190 initial_data = [
191 191 error,
192 192 status,
193 193 headers,
194 194 ]
195 195
196 196 for d in chain(initial_data, data):
197 197 yield msgpack.packb(d)
198 198
199 199
200 200 def not_found(request):
201 201 return {'status': '404 NOT FOUND'}
202 202
203 203
204 204 class VCSViewPredicate(object):
205 205 def __init__(self, val, config):
206 206 self.remotes = val
207 207
208 208 def text(self):
209 209 return 'vcs view method = %s' % (self.remotes.keys(),)
210 210
211 211 phash = text
212 212
213 213 def __call__(self, context, request):
214 214 """
215 215 View predicate that returns true if given backend is supported by
216 216 defined remotes.
217 217 """
218 218 backend = request.matchdict.get('backend')
219 219 return backend in self.remotes
220 220
221 221
222 222 class HTTPApplication(object):
223 223 ALLOWED_EXCEPTIONS = ('KeyError', 'URLError')
224 224
225 225 remote_wsgi = remote_wsgi
226 226 _use_echo_app = False
227 227
228 228 def __init__(self, settings=None, global_config=None):
229 229 self._sanitize_settings_and_apply_defaults(settings)
230 230
231 231 self.config = Configurator(settings=settings)
232 232 self.global_config = global_config
233 233 self.config.include('vcsserver.lib.rc_cache')
234 234
235 235 locale = settings.get('locale', '') or 'en_US.UTF-8'
236 236 vcs = VCS(locale=locale, cache_config=settings)
237 237 self._remotes = {
238 238 'hg': vcs._hg_remote,
239 239 'git': vcs._git_remote,
240 240 'svn': vcs._svn_remote,
241 241 'server': vcs._vcsserver,
242 242 }
243 243 if settings.get('dev.use_echo_app', 'false').lower() == 'true':
244 244 self._use_echo_app = True
245 245 log.warning("Using EchoApp for VCS operations.")
246 246 self.remote_wsgi = remote_wsgi_stub
247 247
248 248 self._configure_settings(global_config, settings)
249 249 self._configure()
250 250
251 251 def _configure_settings(self, global_config, app_settings):
252 252 """
253 253 Configure the settings module.
254 254 """
255 255 settings_merged = global_config.copy()
256 256 settings_merged.update(app_settings)
257 257
258 258 git_path = app_settings.get('git_path', None)
259 259 if git_path:
260 260 settings.GIT_EXECUTABLE = git_path
261 261 binary_dir = app_settings.get('core.binary_dir', None)
262 262 if binary_dir:
263 263 settings.BINARY_DIR = binary_dir
264 264
265 265 # Store the settings to make them available to other modules.
266 266 vcsserver.PYRAMID_SETTINGS = settings_merged
267 267 vcsserver.CONFIG = settings_merged
268 268
269 269 def _sanitize_settings_and_apply_defaults(self, settings):
270 270 temp_store = tempfile.gettempdir()
271 271 default_cache_dir = os.path.join(temp_store, 'rc_cache')
272 272
273 273 # save default, cache dir, and use it for all backends later.
274 274 default_cache_dir = _string_setting(
275 275 settings,
276 276 'cache_dir',
277 277 default_cache_dir, lower=False, default_when_empty=True)
278 278
279 279 # ensure we have our dir created
280 280 if not os.path.isdir(default_cache_dir):
281 281 os.makedirs(default_cache_dir, mode=0o755)
282 282
283 283 # exception store cache
284 284 _string_setting(
285 285 settings,
286 286 'exception_tracker.store_path',
287 287 temp_store, lower=False, default_when_empty=True)
288 288
289 289 # repo_object cache
290 290 _string_setting(
291 291 settings,
292 292 'rc_cache.repo_object.backend',
293 293 'dogpile.cache.rc.memory_lru')
294 294 _int_setting(
295 295 settings,
296 296 'rc_cache.repo_object.expiration_time',
297 297 300)
298 298 _int_setting(
299 299 settings,
300 300 'rc_cache.repo_object.max_size',
301 301 1024)
302 302
303 303 def _configure(self):
304 304 self.config.add_renderer(name='msgpack', factory=self._msgpack_renderer_factory)
305 305
306 306 self.config.add_route('service', '/_service')
307 307 self.config.add_route('status', '/status')
308 308 self.config.add_route('hg_proxy', '/proxy/hg')
309 309 self.config.add_route('git_proxy', '/proxy/git')
310 310 self.config.add_route('vcs', '/{backend}')
311 311 self.config.add_route('stream_git', '/stream/git/*repo_name')
312 312 self.config.add_route('stream_hg', '/stream/hg/*repo_name')
313 313
314 314 self.config.add_view(self.status_view, route_name='status', renderer='json')
315 315 self.config.add_view(self.service_view, route_name='service', renderer='msgpack')
316 316
317 317 self.config.add_view(self.hg_proxy(), route_name='hg_proxy')
318 318 self.config.add_view(self.git_proxy(), route_name='git_proxy')
319 319 self.config.add_view(self.vcs_view, route_name='vcs', renderer='msgpack',
320 320 vcs_view=self._remotes)
321 321
322 322 self.config.add_view(self.hg_stream(), route_name='stream_hg')
323 323 self.config.add_view(self.git_stream(), route_name='stream_git')
324 324
325 325 self.config.add_view_predicate('vcs_view', VCSViewPredicate)
326 326
327 327 self.config.add_notfound_view(not_found, renderer='json')
328 328
329 329 self.config.add_view(self.handle_vcs_exception, context=Exception)
330 330
331 331 self.config.add_tween(
332 332 'vcsserver.tweens.RequestWrapperTween',
333 333 )
334 334
335 335 def wsgi_app(self):
336 336 return self.config.make_wsgi_app()
337 337
338 338 def vcs_view(self, request):
339 339 remote = self._remotes[request.matchdict['backend']]
340 340 payload = msgpack.unpackb(request.body, use_list=True)
341 341 method = payload.get('method')
342 342 params = payload.get('params')
343 343 wire = params.get('wire')
344 344 args = params.get('args')
345 345 kwargs = params.get('kwargs')
346 346 context_uid = None
347 347
348 348 if wire:
349 349 try:
350 350 wire['context'] = context_uid = uuid.UUID(wire['context'])
351 351 except KeyError:
352 352 pass
353 353 args.insert(0, wire)
354 354
355 log.debug('method called:%s with args:%s kwargs:%s context_uid: %s',
356 method, args[1:], kwargs, context_uid)
355 # NOTE(marcink): trading complexity for slight performance
356 if log.isEnabledFor(logging.DEBUG):
357 no_args_methods = [
358 'archive_repo'
359 ]
360 if method in no_args_methods:
361 call_args = ''
362 else:
363 call_args = args[1:]
364 log.debug('method called:%s with args:%s kwargs:%s context_uid: %s',
365 method, call_args, kwargs, context_uid)
357 366
358 367 try:
359 368 resp = getattr(remote, method)(*args, **kwargs)
360 369 except Exception as e:
361 370 exc_info = list(sys.exc_info())
362 371 exc_type, exc_value, exc_traceback = exc_info
363 372
364 373 org_exc = getattr(e, '_org_exc', None)
365 374 org_exc_name = None
366 375 org_exc_tb = ''
367 376 if org_exc:
368 377 org_exc_name = org_exc.__class__.__name__
369 378 org_exc_tb = getattr(e, '_org_exc_tb', '')
370 379 # replace our "faked" exception with our org
371 380 exc_info[0] = org_exc.__class__
372 381 exc_info[1] = org_exc
373 382
374 383 store_exception(id(exc_info), exc_info)
375 384
376 385 tb_info = ''.join(
377 386 traceback.format_exception(exc_type, exc_value, exc_traceback))
378 387
379 388 type_ = e.__class__.__name__
380 389 if type_ not in self.ALLOWED_EXCEPTIONS:
381 390 type_ = None
382 391
383 392 resp = {
384 393 'id': payload.get('id'),
385 394 'error': {
386 395 'message': e.message,
387 396 'traceback': tb_info,
388 397 'org_exc': org_exc_name,
389 398 'org_exc_tb': org_exc_tb,
390 399 'type': type_
391 400 }
392 401 }
393 402 try:
394 403 resp['error']['_vcs_kind'] = getattr(e, '_vcs_kind', None)
395 404 except AttributeError:
396 405 pass
397 406 else:
398 407 resp = {
399 408 'id': payload.get('id'),
400 409 'result': resp
401 410 }
402 411
403 412 return resp
404 413
405 414 def status_view(self, request):
406 415 import vcsserver
407 416 return {'status': 'OK', 'vcsserver_version': vcsserver.__version__,
408 417 'pid': os.getpid()}
409 418
410 419 def service_view(self, request):
411 420 import vcsserver
412 421
413 422 payload = msgpack.unpackb(request.body, use_list=True)
414 423
415 424 try:
416 425 path = self.global_config['__file__']
417 426 config = configparser.ConfigParser()
418 427 config.read(path)
419 428 parsed_ini = config
420 429 if parsed_ini.has_section('server:main'):
421 430 parsed_ini = dict(parsed_ini.items('server:main'))
422 431 except Exception:
423 432 log.exception('Failed to read .ini file for display')
424 433 parsed_ini = {}
425 434
426 435 resp = {
427 436 'id': payload.get('id'),
428 437 'result': dict(
429 438 version=vcsserver.__version__,
430 439 config=parsed_ini,
431 440 payload=payload,
432 441 )
433 442 }
434 443 return resp
435 444
436 445 def _msgpack_renderer_factory(self, info):
437 446 def _render(value, system):
438 447 value = msgpack.packb(value)
439 448 request = system.get('request')
440 449 if request is not None:
441 450 response = request.response
442 451 ct = response.content_type
443 452 if ct == response.default_content_type:
444 453 response.content_type = 'application/x-msgpack'
445 454 return value
446 455 return _render
447 456
448 457 def set_env_from_config(self, environ, config):
449 458 dict_conf = {}
450 459 try:
451 460 for elem in config:
452 461 if elem[0] == 'rhodecode':
453 462 dict_conf = json.loads(elem[2])
454 463 break
455 464 except Exception:
456 465 log.exception('Failed to fetch SCM CONFIG')
457 466 return
458 467
459 468 username = dict_conf.get('username')
460 469 if username:
461 470 environ['REMOTE_USER'] = username
462 471 # mercurial specific, some extension api rely on this
463 472 environ['HGUSER'] = username
464 473
465 474 ip = dict_conf.get('ip')
466 475 if ip:
467 476 environ['REMOTE_HOST'] = ip
468 477
469 478 if _is_request_chunked(environ):
470 479 # set the compatibility flag for webob
471 480 environ['wsgi.input_terminated'] = True
472 481
473 482 def hg_proxy(self):
474 483 @wsgiapp
475 484 def _hg_proxy(environ, start_response):
476 485 app = WsgiProxy(self.remote_wsgi.HgRemoteWsgi())
477 486 return app(environ, start_response)
478 487 return _hg_proxy
479 488
480 489 def git_proxy(self):
481 490 @wsgiapp
482 491 def _git_proxy(environ, start_response):
483 492 app = WsgiProxy(self.remote_wsgi.GitRemoteWsgi())
484 493 return app(environ, start_response)
485 494 return _git_proxy
486 495
487 496 def hg_stream(self):
488 497 if self._use_echo_app:
489 498 @wsgiapp
490 499 def _hg_stream(environ, start_response):
491 500 app = EchoApp('fake_path', 'fake_name', None)
492 501 return app(environ, start_response)
493 502 return _hg_stream
494 503 else:
495 504 @wsgiapp
496 505 def _hg_stream(environ, start_response):
497 506 log.debug('http-app: handling hg stream')
498 507 repo_path = environ['HTTP_X_RC_REPO_PATH']
499 508 repo_name = environ['HTTP_X_RC_REPO_NAME']
500 509 packed_config = base64.b64decode(
501 510 environ['HTTP_X_RC_REPO_CONFIG'])
502 511 config = msgpack.unpackb(packed_config)
503 512 app = scm_app.create_hg_wsgi_app(
504 513 repo_path, repo_name, config)
505 514
506 515 # Consistent path information for hgweb
507 516 environ['PATH_INFO'] = environ['HTTP_X_RC_PATH_INFO']
508 517 environ['REPO_NAME'] = repo_name
509 518 self.set_env_from_config(environ, config)
510 519
511 520 log.debug('http-app: starting app handler '
512 521 'with %s and process request', app)
513 522 return app(environ, ResponseFilter(start_response))
514 523 return _hg_stream
515 524
516 525 def git_stream(self):
517 526 if self._use_echo_app:
518 527 @wsgiapp
519 528 def _git_stream(environ, start_response):
520 529 app = EchoApp('fake_path', 'fake_name', None)
521 530 return app(environ, start_response)
522 531 return _git_stream
523 532 else:
524 533 @wsgiapp
525 534 def _git_stream(environ, start_response):
526 535 log.debug('http-app: handling git stream')
527 536 repo_path = environ['HTTP_X_RC_REPO_PATH']
528 537 repo_name = environ['HTTP_X_RC_REPO_NAME']
529 538 packed_config = base64.b64decode(
530 539 environ['HTTP_X_RC_REPO_CONFIG'])
531 540 config = msgpack.unpackb(packed_config)
532 541
533 542 environ['PATH_INFO'] = environ['HTTP_X_RC_PATH_INFO']
534 543 self.set_env_from_config(environ, config)
535 544
536 545 content_type = environ.get('CONTENT_TYPE', '')
537 546
538 547 path = environ['PATH_INFO']
539 548 is_lfs_request = GIT_LFS_CONTENT_TYPE in content_type
540 549 log.debug(
541 550 'LFS: Detecting if request `%s` is LFS server path based '
542 551 'on content type:`%s`, is_lfs:%s',
543 552 path, content_type, is_lfs_request)
544 553
545 554 if not is_lfs_request:
546 555 # fallback detection by path
547 556 if GIT_LFS_PROTO_PAT.match(path):
548 557 is_lfs_request = True
549 558 log.debug(
550 559 'LFS: fallback detection by path of: `%s`, is_lfs:%s',
551 560 path, is_lfs_request)
552 561
553 562 if is_lfs_request:
554 563 app = scm_app.create_git_lfs_wsgi_app(
555 564 repo_path, repo_name, config)
556 565 else:
557 566 app = scm_app.create_git_wsgi_app(
558 567 repo_path, repo_name, config)
559 568
560 569 log.debug('http-app: starting app handler '
561 570 'with %s and process request', app)
562 571
563 572 return app(environ, start_response)
564 573
565 574 return _git_stream
566 575
567 576 def handle_vcs_exception(self, exception, request):
568 577 _vcs_kind = getattr(exception, '_vcs_kind', '')
569 578 if _vcs_kind == 'repo_locked':
570 579 # Get custom repo-locked status code if present.
571 580 status_code = request.headers.get('X-RC-Locked-Status-Code')
572 581 return HTTPRepoLocked(
573 582 title=exception.message, status_code=status_code)
574 583
575 584 elif _vcs_kind == 'repo_branch_protected':
576 585 # Get custom repo-branch-protected status code if present.
577 586 return HTTPRepoBranchProtected(title=exception.message)
578 587
579 588 exc_info = request.exc_info
580 589 store_exception(id(exc_info), exc_info)
581 590
582 591 traceback_info = 'unavailable'
583 592 if request.exc_info:
584 593 exc_type, exc_value, exc_tb = request.exc_info
585 594 traceback_info = ''.join(traceback.format_exception(exc_type, exc_value, exc_tb))
586 595
587 596 log.error(
588 597 'error occurred handling this request for path: %s, \n tb: %s',
589 598 request.path, traceback_info)
590 599 raise exception
591 600
592 601
593 602 class ResponseFilter(object):
594 603
595 604 def __init__(self, start_response):
596 605 self._start_response = start_response
597 606
598 607 def __call__(self, status, response_headers, exc_info=None):
599 608 headers = tuple(
600 609 (h, v) for h, v in response_headers
601 610 if not wsgiref.util.is_hop_by_hop(h))
602 611 return self._start_response(status, headers, exc_info)
603 612
604 613
605 614 def main(global_config, **settings):
606 615 if MercurialFactory:
607 616 hgpatches.patch_largefiles_capabilities()
608 617 hgpatches.patch_subrepo_type_mapping()
609 618
610 619 app = HTTPApplication(settings=settings, global_config=global_config)
611 620 return app.wsgi_app()
General Comments 0
You need to be logged in to leave comments. Login now