##// END OF EJS Templates
http-traffic: mostly use payload from encoded msgpack single header instead of multiple ones....
super-admin -
r1078:beb1039e python3
parent child Browse files
Show More
@@ -1,751 +1,762 b''
1 1 # RhodeCode VCSServer provides access to different vcs backends via network.
2 2 # Copyright (C) 2014-2020 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 io
19 19 import os
20 20 import sys
21 21 import base64
22 22 import locale
23 23 import logging
24 24 import uuid
25 25 import time
26 26 import wsgiref.util
27 27 import traceback
28 28 import tempfile
29 29 import psutil
30 30
31 31 from itertools import chain
32 32
33 33 import msgpack
34 34 import configparser
35 35
36 36 from pyramid.config import Configurator
37 37 from pyramid.wsgi import wsgiapp
38 38 from pyramid.response import Response
39 39
40 40 from vcsserver.lib.rc_json import json
41 41 from vcsserver.config.settings_maker import SettingsMaker
42 42 from vcsserver.str_utils import safe_int, safe_bytes, safe_str
43 43 from vcsserver.lib.statsd_client import StatsdClient
44 44
45 45 log = logging.getLogger(__name__)
46 46
47 47 # due to Mercurial/glibc2.27 problems we need to detect if locale settings are
48 48 # causing problems and "fix" it in case they do and fallback to LC_ALL = C
49 49
50 50 try:
51 51 locale.setlocale(locale.LC_ALL, '')
52 52 except locale.Error as e:
53 53 log.error(
54 54 'LOCALE ERROR: failed to set LC_ALL, fallback to LC_ALL=C, org error: %s', e)
55 55 os.environ['LC_ALL'] = 'C'
56 56
57 57
58 58 import vcsserver
59 59 from vcsserver import remote_wsgi, scm_app, settings, hgpatches
60 60 from vcsserver.git_lfs.app import GIT_LFS_CONTENT_TYPE, GIT_LFS_PROTO_PAT
61 61 from vcsserver.echo_stub import remote_wsgi as remote_wsgi_stub
62 62 from vcsserver.echo_stub.echo_app import EchoApp
63 63 from vcsserver.exceptions import HTTPRepoLocked, HTTPRepoBranchProtected
64 64 from vcsserver.lib.exc_tracking import store_exception
65 65 from vcsserver.server import VcsServer
66 66
67 67 strict_vcs = True
68 68
69 69 git_import_err = None
70 70 try:
71 71 from vcsserver.remote.git import GitFactory, GitRemote
72 72 except ImportError as e:
73 73 GitFactory = None
74 74 GitRemote = None
75 75 git_import_err = e
76 76 if strict_vcs:
77 77 raise
78 78
79 79
80 80 hg_import_err = None
81 81 try:
82 82 from vcsserver.remote.hg import MercurialFactory, HgRemote
83 83 except ImportError as e:
84 84 MercurialFactory = None
85 85 HgRemote = None
86 86 hg_import_err = e
87 87 if strict_vcs:
88 88 raise
89 89
90 90
91 91 svn_import_err = None
92 92 try:
93 93 from vcsserver.remote.svn import SubversionFactory, SvnRemote
94 94 except ImportError as e:
95 95 SubversionFactory = None
96 96 SvnRemote = None
97 97 svn_import_err = e
98 98 if strict_vcs:
99 99 raise
100 100
101 101
102 102 def _is_request_chunked(environ):
103 103 stream = environ.get('HTTP_TRANSFER_ENCODING', '') == 'chunked'
104 104 return stream
105 105
106 106
107 107 def log_max_fd():
108 108 try:
109 109 maxfd = psutil.Process().rlimit(psutil.RLIMIT_NOFILE)[1]
110 110 log.info('Max file descriptors value: %s', maxfd)
111 111 except Exception:
112 112 pass
113 113
114 114
115 115 class VCS(object):
116 116 def __init__(self, locale_conf=None, cache_config=None):
117 117 self.locale = locale_conf
118 118 self.cache_config = cache_config
119 119 self._configure_locale()
120 120
121 121 log_max_fd()
122 122
123 123 if GitFactory and GitRemote:
124 124 git_factory = GitFactory()
125 125 self._git_remote = GitRemote(git_factory)
126 126 else:
127 127 log.error("Git client import failed: %s", git_import_err)
128 128
129 129 if MercurialFactory and HgRemote:
130 130 hg_factory = MercurialFactory()
131 131 self._hg_remote = HgRemote(hg_factory)
132 132 else:
133 133 log.error("Mercurial client import failed: %s", hg_import_err)
134 134
135 135 if SubversionFactory and SvnRemote:
136 136 svn_factory = SubversionFactory()
137 137
138 138 # hg factory is used for svn url validation
139 139 hg_factory = MercurialFactory()
140 140 self._svn_remote = SvnRemote(svn_factory, hg_factory=hg_factory)
141 141 else:
142 142 log.error("Subversion client import failed: %s", svn_import_err)
143 143
144 144 self._vcsserver = VcsServer()
145 145
146 146 def _configure_locale(self):
147 147 if self.locale:
148 148 log.info('Settings locale: `LC_ALL` to %s', self.locale)
149 149 else:
150 150 log.info('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('Cannot set locale, not configuring the locale system')
163 163
164 164
165 165 class WsgiProxy(object):
166 166 def __init__(self, wsgi):
167 167 self.wsgi = wsgi
168 168
169 169 def __call__(self, environ, start_response):
170 170 input_data = environ['wsgi.input'].read()
171 171 input_data = msgpack.unpackb(input_data)
172 172
173 173 error = None
174 174 try:
175 175 data, status, headers = self.wsgi.handle(
176 176 input_data['environment'], input_data['input_data'],
177 177 *input_data['args'], **input_data['kwargs'])
178 178 except Exception as e:
179 179 data, status, headers = [], None, None
180 180 error = {
181 181 'message': str(e),
182 182 '_vcs_kind': getattr(e, '_vcs_kind', None)
183 183 }
184 184
185 185 start_response(200, {})
186 186 return self._iterator(error, status, headers, data)
187 187
188 188 def _iterator(self, error, status, headers, data):
189 189 initial_data = [
190 190 error,
191 191 status,
192 192 headers,
193 193 ]
194 194
195 195 for d in chain(initial_data, data):
196 196 yield msgpack.packb(d)
197 197
198 198
199 199 def not_found(request):
200 200 return {'status': '404 NOT FOUND'}
201 201
202 202
203 203 class VCSViewPredicate(object):
204 204 def __init__(self, val, config):
205 205 self.remotes = val
206 206
207 207 def text(self):
208 208 return 'vcs view method = %s' % (list(self.remotes.keys()),)
209 209
210 210 phash = text
211 211
212 212 def __call__(self, context, request):
213 213 """
214 214 View predicate that returns true if given backend is supported by
215 215 defined remotes.
216 216 """
217 217 backend = request.matchdict.get('backend')
218 218 return backend in self.remotes
219 219
220 220
221 221 class HTTPApplication(object):
222 222 ALLOWED_EXCEPTIONS = ('KeyError', 'URLError')
223 223
224 224 remote_wsgi = remote_wsgi
225 225 _use_echo_app = False
226 226
227 227 def __init__(self, settings=None, global_config=None):
228 228
229 229 self.config = Configurator(settings=settings)
230 230 # Init our statsd at very start
231 231 self.config.registry.statsd = StatsdClient.statsd
232 self.config.registry.vcs_call_context = {}
232 233
233 234 self.global_config = global_config
234 235 self.config.include('vcsserver.lib.rc_cache')
235 236
236 237 settings_locale = settings.get('locale', '') or 'en_US.UTF-8'
237 238 vcs = VCS(locale_conf=settings_locale, cache_config=settings)
238 239 self._remotes = {
239 240 'hg': vcs._hg_remote,
240 241 'git': vcs._git_remote,
241 242 'svn': vcs._svn_remote,
242 243 'server': vcs._vcsserver,
243 244 }
244 245 if settings.get('dev.use_echo_app', 'false').lower() == 'true':
245 246 self._use_echo_app = True
246 247 log.warning("Using EchoApp for VCS operations.")
247 248 self.remote_wsgi = remote_wsgi_stub
248 249
249 250 self._configure_settings(global_config, settings)
250 251
251 252 self._configure()
252 253
253 254 def _configure_settings(self, global_config, app_settings):
254 255 """
255 256 Configure the settings module.
256 257 """
257 258 settings_merged = global_config.copy()
258 259 settings_merged.update(app_settings)
259 260
260 261 git_path = app_settings.get('git_path', None)
261 262 if git_path:
262 263 settings.GIT_EXECUTABLE = git_path
263 264 binary_dir = app_settings.get('core.binary_dir', None)
264 265 if binary_dir:
265 266 settings.BINARY_DIR = binary_dir
266 267
267 268 # Store the settings to make them available to other modules.
268 269 vcsserver.PYRAMID_SETTINGS = settings_merged
269 270 vcsserver.CONFIG = settings_merged
270 271
271 272 def _configure(self):
272 273 self.config.add_renderer(name='msgpack', factory=self._msgpack_renderer_factory)
273 274
274 275 self.config.add_route('service', '/_service')
275 276 self.config.add_route('status', '/status')
276 277 self.config.add_route('hg_proxy', '/proxy/hg')
277 278 self.config.add_route('git_proxy', '/proxy/git')
278 279
279 280 # rpc methods
280 281 self.config.add_route('vcs', '/{backend}')
281 282
282 283 # streaming rpc remote methods
283 284 self.config.add_route('vcs_stream', '/{backend}/stream')
284 285
285 286 # vcs operations clone/push as streaming
286 287 self.config.add_route('stream_git', '/stream/git/*repo_name')
287 288 self.config.add_route('stream_hg', '/stream/hg/*repo_name')
288 289
289 290 self.config.add_view(self.status_view, route_name='status', renderer='json')
290 291 self.config.add_view(self.service_view, route_name='service', renderer='msgpack')
291 292
292 293 self.config.add_view(self.hg_proxy(), route_name='hg_proxy')
293 294 self.config.add_view(self.git_proxy(), route_name='git_proxy')
294 295 self.config.add_view(self.vcs_view, route_name='vcs', renderer='msgpack',
295 296 vcs_view=self._remotes)
296 297 self.config.add_view(self.vcs_stream_view, route_name='vcs_stream',
297 298 vcs_view=self._remotes)
298 299
299 300 self.config.add_view(self.hg_stream(), route_name='stream_hg')
300 301 self.config.add_view(self.git_stream(), route_name='stream_git')
301 302
302 303 self.config.add_view_predicate('vcs_view', VCSViewPredicate)
303 304
304 305 self.config.add_notfound_view(not_found, renderer='json')
305 306
306 307 self.config.add_view(self.handle_vcs_exception, context=Exception)
307 308
308 309 self.config.add_tween(
309 310 'vcsserver.tweens.request_wrapper.RequestWrapperTween',
310 311 )
311 312 self.config.add_request_method(
312 313 'vcsserver.lib.request_counter.get_request_counter',
313 314 'request_count')
314 315
315 316 def wsgi_app(self):
316 317 return self.config.make_wsgi_app()
317 318
318 319 def _vcs_view_params(self, request):
319 320 remote = self._remotes[request.matchdict['backend']]
320 321 payload = msgpack.unpackb(request.body, use_list=True)
321 322
322 323 method = payload.get('method')
323 324 params = payload['params']
324 325 wire = params.get('wire')
325 326 args = params.get('args')
326 327 kwargs = params.get('kwargs')
327 328 context_uid = None
328 329
330 request.registry.vcs_call_context = {
331 'method': method,
332 'repo_name': payload.get('_repo_name')
333 }
334
329 335 if wire:
330 336 try:
331 337 wire['context'] = context_uid = uuid.UUID(wire['context'])
332 338 except KeyError:
333 339 pass
334 340 args.insert(0, wire)
335 341 repo_state_uid = wire.get('repo_state_uid') if wire else None
336 342
337 343 # NOTE(marcink): trading complexity for slight performance
338 344 if log.isEnabledFor(logging.DEBUG):
339 345 # also we SKIP printing out any of those methods args since they maybe excessive
340 346 just_args_methods = {
341 347 'commitctx': ('content', 'removed', 'updated')
342 348 }
343 349 if method in just_args_methods:
344 350 skip_args = just_args_methods[method]
345 351 call_args = ''
346 352 call_kwargs = {}
347 353 for k in kwargs:
348 354 if k in skip_args:
349 355 # replace our skip key with dummy
350 356 call_kwargs[k] = f'RemovedParam({k})'
351 357 else:
352 358 call_kwargs[k] = kwargs[k]
353 359 else:
354 360 call_args = args[1:]
355 361 call_kwargs = kwargs
356 362
357 363 log.debug('Method requested:`%s` with args:%s kwargs:%s context_uid: %s, repo_state_uid:%s',
358 364 method, call_args, call_kwargs, context_uid, repo_state_uid)
359 365
360 366 statsd = request.registry.statsd
361 367 if statsd:
362 368 statsd.incr(
363 369 'vcsserver_method_total', tags=[
364 370 "method:{}".format(method),
365 371 ])
366 372 return payload, remote, method, args, kwargs
367 373
368 374 def vcs_view(self, request):
369 375
370 376 payload, remote, method, args, kwargs = self._vcs_view_params(request)
371 377 payload_id = payload.get('id')
372 378
373 379 try:
374 380 resp = getattr(remote, method)(*args, **kwargs)
375 381 except Exception as e:
376 382 exc_info = list(sys.exc_info())
377 383 exc_type, exc_value, exc_traceback = exc_info
378 384
379 385 org_exc = getattr(e, '_org_exc', None)
380 386 org_exc_name = None
381 387 org_exc_tb = ''
382 388 if org_exc:
383 389 org_exc_name = org_exc.__class__.__name__
384 390 org_exc_tb = getattr(e, '_org_exc_tb', '')
385 391 # replace our "faked" exception with our org
386 392 exc_info[0] = org_exc.__class__
387 393 exc_info[1] = org_exc
388 394
389 395 should_store_exc = True
390 396 if org_exc:
391 397 def get_exc_fqn(_exc_obj):
392 398 module_name = getattr(org_exc.__class__, '__module__', 'UNKNOWN')
393 399 return module_name + '.' + org_exc_name
394 400
395 401 exc_fqn = get_exc_fqn(org_exc)
396 402
397 403 if exc_fqn in ['mercurial.error.RepoLookupError',
398 404 'vcsserver.exceptions.RefNotFoundException']:
399 405 should_store_exc = False
400 406
401 407 if should_store_exc:
402 408 store_exception(id(exc_info), exc_info, request_path=request.path)
403 409
404 410 tb_info = ''.join(
405 411 traceback.format_exception(exc_type, exc_value, exc_traceback))
406 412
407 413 type_ = e.__class__.__name__
408 414 if type_ not in self.ALLOWED_EXCEPTIONS:
409 415 type_ = None
410 416
411 417 resp = {
412 418 'id': payload_id,
413 419 'error': {
414 420 'message': str(e),
415 421 'traceback': tb_info,
416 422 'org_exc': org_exc_name,
417 423 'org_exc_tb': org_exc_tb,
418 424 'type': type_
419 425 }
420 426 }
421 427
422 428 try:
423 429 resp['error']['_vcs_kind'] = getattr(e, '_vcs_kind', None)
424 430 except AttributeError:
425 431 pass
426 432 else:
427 433 resp = {
428 434 'id': payload_id,
429 435 'result': resp
430 436 }
431 437 log.debug('Serving data for method %s', method)
432 438 return resp
433 439
434 440 def vcs_stream_view(self, request):
435 441 payload, remote, method, args, kwargs = self._vcs_view_params(request)
436 442 # this method has a stream: marker we remove it here
437 443 method = method.split('stream:')[-1]
438 444 chunk_size = safe_int(payload.get('chunk_size')) or 4096
439 445
440 446 try:
441 447 resp = getattr(remote, method)(*args, **kwargs)
442 448 except Exception as e:
443 449 raise
444 450
445 451 def get_chunked_data(method_resp):
446 452 stream = io.BytesIO(method_resp)
447 453 while 1:
448 454 chunk = stream.read(chunk_size)
449 455 if not chunk:
450 456 break
451 457 yield chunk
452 458
453 459 response = Response(app_iter=get_chunked_data(resp))
454 460 response.content_type = 'application/octet-stream'
455 461
456 462 return response
457 463
458 464 def status_view(self, request):
459 465 import vcsserver
460 466 return {'status': 'OK', 'vcsserver_version': safe_str(vcsserver.__version__),
461 467 'pid': os.getpid()}
462 468
463 469 def service_view(self, request):
464 470 import vcsserver
465 471
466 472 payload = msgpack.unpackb(request.body, use_list=True)
467 473 server_config, app_config = {}, {}
468 474
469 475 try:
470 476 path = self.global_config['__file__']
471 477 config = configparser.RawConfigParser()
472 478
473 479 config.read(path)
474 480
475 481 if config.has_section('server:main'):
476 482 server_config = dict(config.items('server:main'))
477 483 if config.has_section('app:main'):
478 484 app_config = dict(config.items('app:main'))
479 485
480 486 except Exception:
481 487 log.exception('Failed to read .ini file for display')
482 488
483 489 environ = list(os.environ.items())
484 490
485 491 resp = {
486 492 'id': payload.get('id'),
487 493 'result': dict(
488 494 version=safe_str(vcsserver.__version__),
489 495 config=server_config,
490 496 app_config=app_config,
491 497 environ=environ,
492 498 payload=payload,
493 499 )
494 500 }
495 501 return resp
496 502
497 503 def _msgpack_renderer_factory(self, info):
498 504 def _render(value, system):
499 505 request = system.get('request')
500 506 if request is not None:
501 507 response = request.response
502 508 ct = response.content_type
503 509 if ct == response.default_content_type:
504 510 response.content_type = 'application/x-msgpack'
505 511
506 512 return msgpack.packb(value, use_bin_type=False)
507 513 return _render
508 514
509 515 def set_env_from_config(self, environ, config):
510 516 dict_conf = {}
511 517 try:
512 518 for elem in config:
513 519 if elem[0] == 'rhodecode':
514 520 dict_conf = json.loads(elem[2])
515 521 break
516 522 except Exception:
517 523 log.exception('Failed to fetch SCM CONFIG')
518 524 return
519 525
520 526 username = dict_conf.get('username')
521 527 if username:
522 528 environ['REMOTE_USER'] = username
523 529 # mercurial specific, some extension api rely on this
524 530 environ['HGUSER'] = username
525 531
526 532 ip = dict_conf.get('ip')
527 533 if ip:
528 534 environ['REMOTE_HOST'] = ip
529 535
530 536 if _is_request_chunked(environ):
531 537 # set the compatibility flag for webob
532 538 environ['wsgi.input_terminated'] = True
533 539
534 540 def hg_proxy(self):
535 541 @wsgiapp
536 542 def _hg_proxy(environ, start_response):
537 543 app = WsgiProxy(self.remote_wsgi.HgRemoteWsgi())
538 544 return app(environ, start_response)
539 545 return _hg_proxy
540 546
541 547 def git_proxy(self):
542 548 @wsgiapp
543 549 def _git_proxy(environ, start_response):
544 550 app = WsgiProxy(self.remote_wsgi.GitRemoteWsgi())
545 551 return app(environ, start_response)
546 552 return _git_proxy
547 553
548 554 def hg_stream(self):
549 555 if self._use_echo_app:
550 556 @wsgiapp
551 557 def _hg_stream(environ, start_response):
552 558 app = EchoApp('fake_path', 'fake_name', None)
553 559 return app(environ, start_response)
554 560 return _hg_stream
555 561 else:
556 562 @wsgiapp
557 563 def _hg_stream(environ, start_response):
558 564 log.debug('http-app: handling hg stream')
559 repo_path = environ['HTTP_X_RC_REPO_PATH']
560 repo_name = environ['HTTP_X_RC_REPO_NAME']
561 packed_config = base64.b64decode(
562 environ['HTTP_X_RC_REPO_CONFIG'])
563 config = msgpack.unpackb(packed_config)
565
566 packed_cc = base64.b64decode(environ['HTTP_X_RC_VCS_STREAM_CALL_CONTEXT'])
567 call_context = msgpack.unpackb(packed_cc)
568
569 repo_path = call_context['repo_path']
570 repo_name = call_context['repo_name']
571 config = call_context['repo_config']
572
564 573 app = scm_app.create_hg_wsgi_app(
565 574 repo_path, repo_name, config)
566 575
567 576 # Consistent path information for hgweb
568 environ['PATH_INFO'] = environ['HTTP_X_RC_PATH_INFO']
577 environ['PATH_INFO'] = call_context['path_info']
569 578 environ['REPO_NAME'] = repo_name
570 579 self.set_env_from_config(environ, config)
571 580
572 581 log.debug('http-app: starting app handler '
573 582 'with %s and process request', app)
574 583 return app(environ, ResponseFilter(start_response))
575 584 return _hg_stream
576 585
577 586 def git_stream(self):
578 587 if self._use_echo_app:
579 588 @wsgiapp
580 589 def _git_stream(environ, start_response):
581 590 app = EchoApp('fake_path', 'fake_name', None)
582 591 return app(environ, start_response)
583 592 return _git_stream
584 593 else:
585 594 @wsgiapp
586 595 def _git_stream(environ, start_response):
587 596 log.debug('http-app: handling git stream')
588 repo_path = environ['HTTP_X_RC_REPO_PATH']
589 repo_name = environ['HTTP_X_RC_REPO_NAME']
590 packed_config = base64.b64decode(
591 environ['HTTP_X_RC_REPO_CONFIG'])
592 config = msgpack.unpackb(packed_config)
597
598 packed_cc = base64.b64decode(environ['HTTP_X_RC_VCS_STREAM_CALL_CONTEXT'])
599 call_context = msgpack.unpackb(packed_cc)
593 600
594 environ['PATH_INFO'] = environ['HTTP_X_RC_PATH_INFO']
601 repo_path = call_context['repo_path']
602 repo_name = call_context['repo_name']
603 config = call_context['repo_config']
604
605 environ['PATH_INFO'] = call_context['path_info']
595 606 self.set_env_from_config(environ, config)
596 607
597 608 content_type = environ.get('CONTENT_TYPE', '')
598 609
599 610 path = environ['PATH_INFO']
600 611 is_lfs_request = GIT_LFS_CONTENT_TYPE in content_type
601 612 log.debug(
602 613 'LFS: Detecting if request `%s` is LFS server path based '
603 614 'on content type:`%s`, is_lfs:%s',
604 615 path, content_type, is_lfs_request)
605 616
606 617 if not is_lfs_request:
607 618 # fallback detection by path
608 619 if GIT_LFS_PROTO_PAT.match(path):
609 620 is_lfs_request = True
610 621 log.debug(
611 622 'LFS: fallback detection by path of: `%s`, is_lfs:%s',
612 623 path, is_lfs_request)
613 624
614 625 if is_lfs_request:
615 626 app = scm_app.create_git_lfs_wsgi_app(
616 627 repo_path, repo_name, config)
617 628 else:
618 629 app = scm_app.create_git_wsgi_app(
619 630 repo_path, repo_name, config)
620 631
621 632 log.debug('http-app: starting app handler '
622 633 'with %s and process request', app)
623 634
624 635 return app(environ, start_response)
625 636
626 637 return _git_stream
627 638
628 639 def handle_vcs_exception(self, exception, request):
629 640 _vcs_kind = getattr(exception, '_vcs_kind', '')
630 641 if _vcs_kind == 'repo_locked':
631 642 # Get custom repo-locked status code if present.
632 643 status_code = request.headers.get('X-RC-Locked-Status-Code')
633 644 return HTTPRepoLocked(
634 645 title=exception.message, status_code=status_code)
635 646
636 647 elif _vcs_kind == 'repo_branch_protected':
637 648 # Get custom repo-branch-protected status code if present.
638 649 return HTTPRepoBranchProtected(title=exception.message)
639 650
640 651 exc_info = request.exc_info
641 652 store_exception(id(exc_info), exc_info)
642 653
643 654 traceback_info = 'unavailable'
644 655 if request.exc_info:
645 656 exc_type, exc_value, exc_tb = request.exc_info
646 657 traceback_info = ''.join(traceback.format_exception(exc_type, exc_value, exc_tb))
647 658
648 659 log.error(
649 660 'error occurred handling this request for path: %s, \n tb: %s',
650 661 request.path, traceback_info)
651 662
652 663 statsd = request.registry.statsd
653 664 if statsd:
654 665 exc_type = "{}.{}".format(exception.__class__.__module__, exception.__class__.__name__)
655 666 statsd.incr('vcsserver_exception_total',
656 667 tags=["type:{}".format(exc_type)])
657 668 raise exception
658 669
659 670
660 671 class ResponseFilter(object):
661 672
662 673 def __init__(self, start_response):
663 674 self._start_response = start_response
664 675
665 676 def __call__(self, status, response_headers, exc_info=None):
666 677 headers = tuple(
667 678 (h, v) for h, v in response_headers
668 679 if not wsgiref.util.is_hop_by_hop(h))
669 680 return self._start_response(status, headers, exc_info)
670 681
671 682
672 683 def sanitize_settings_and_apply_defaults(global_config, settings):
673 684 global_settings_maker = SettingsMaker(global_config)
674 685 settings_maker = SettingsMaker(settings)
675 686
676 687 settings_maker.make_setting('logging.autoconfigure', False, parser='bool')
677 688
678 689 logging_conf = os.path.join(os.path.dirname(global_config.get('__file__')), 'logging.ini')
679 690 settings_maker.enable_logging(logging_conf)
680 691
681 692 # Default includes, possible to change as a user
682 693 pyramid_includes = settings_maker.make_setting('pyramid.includes', [], parser='list:newline')
683 694 log.debug("Using the following pyramid.includes: %s", pyramid_includes)
684 695
685 696 settings_maker.make_setting('__file__', global_config.get('__file__'))
686 697
687 698 settings_maker.make_setting('pyramid.default_locale_name', 'en')
688 699 settings_maker.make_setting('locale', 'en_US.UTF-8')
689 700
690 701 settings_maker.make_setting('core.binary_dir', '')
691 702
692 703 temp_store = tempfile.gettempdir()
693 704 default_cache_dir = os.path.join(temp_store, 'rc_cache')
694 705 # save default, cache dir, and use it for all backends later.
695 706 default_cache_dir = settings_maker.make_setting(
696 707 'cache_dir',
697 708 default=default_cache_dir, default_when_empty=True,
698 709 parser='dir:ensured')
699 710
700 711 # exception store cache
701 712 settings_maker.make_setting(
702 713 'exception_tracker.store_path',
703 714 default=os.path.join(default_cache_dir, 'exc_store'), default_when_empty=True,
704 715 parser='dir:ensured'
705 716 )
706 717
707 718 # repo_object cache defaults
708 719 settings_maker.make_setting(
709 720 'rc_cache.repo_object.backend',
710 721 default='dogpile.cache.rc.file_namespace',
711 722 parser='string')
712 723 settings_maker.make_setting(
713 724 'rc_cache.repo_object.expiration_time',
714 725 default=30 * 24 * 60 * 60, # 30days
715 726 parser='int')
716 727 settings_maker.make_setting(
717 728 'rc_cache.repo_object.arguments.filename',
718 729 default=os.path.join(default_cache_dir, 'vcsserver_cache_repo_object.db'),
719 730 parser='string')
720 731
721 732 # statsd
722 733 settings_maker.make_setting('statsd.enabled', False, parser='bool')
723 734 settings_maker.make_setting('statsd.statsd_host', 'statsd-exporter', parser='string')
724 735 settings_maker.make_setting('statsd.statsd_port', 9125, parser='int')
725 736 settings_maker.make_setting('statsd.statsd_prefix', '')
726 737 settings_maker.make_setting('statsd.statsd_ipv6', False, parser='bool')
727 738
728 739 settings_maker.env_expand()
729 740
730 741
731 742 def main(global_config, **settings):
732 743 start_time = time.time()
733 744 log.info('Pyramid app config starting')
734 745
735 746 if MercurialFactory:
736 747 hgpatches.patch_largefiles_capabilities()
737 748 hgpatches.patch_subrepo_type_mapping()
738 749
739 750 # Fill in and sanitize the defaults & do ENV expansion
740 751 sanitize_settings_and_apply_defaults(global_config, settings)
741 752
742 753 # init and bootstrap StatsdClient
743 754 StatsdClient.setup(settings)
744 755
745 756 pyramid_app = HTTPApplication(settings=settings, global_config=global_config).wsgi_app()
746 757 total_time = time.time() - start_time
747 758 log.info('Pyramid app `%s` created and configured in %.2fs',
748 759 getattr(pyramid_app, 'func_name', 'pyramid_app'), total_time)
749 760 return pyramid_app
750 761
751 762
@@ -1,106 +1,114 b''
1 1 # RhodeCode VCSServer provides access to different vcs backends via network.
2 2 # Copyright (C) 2014-2020 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 time
19 19 import logging
20 20
21 import msgpack
22
21 23 import vcsserver
22 24 from vcsserver.str_utils import safe_str, ascii_str
23 25
24 26 log = logging.getLogger(__name__)
25 27
26 28
27 29 def get_access_path(environ):
28 30 path = environ.get('PATH_INFO')
29 31 return path
30 32
31 33
32 34 def get_user_agent(environ):
33 35 return environ.get('HTTP_USER_AGENT')
34 36
35 37
36 def get_vcs_method(environ):
37 return environ.get('HTTP_X_RC_METHOD')
38 def get_call_context(registry) -> dict:
39 cc = {}
40 if hasattr(registry, 'vcs_call_context'):
41 cc.update({
42 'X-RC-Method': registry.vcs_call_context.get('method'),
43 'X-RC-Repo-Name': registry.vcs_call_context.get('repo_name')
44 })
38 45
39
40 def get_vcs_repo(environ):
41 return environ.get('HTTP_X_RC_REPO_NAME')
46 return cc
42 47
43 48
44 49 class RequestWrapperTween(object):
45 50 def __init__(self, handler, registry):
46 51 self.handler = handler
47 52 self.registry = registry
48 53
49 54 # one-time configuration code goes here
50 55
51 56 def __call__(self, request):
52 57 start = time.time()
53 58 log.debug('Starting request time measurement')
54 59 response = None
55 60
56 ua = get_user_agent(request.environ)
57 vcs_method = get_vcs_method(request.environ)
58 repo_name = get_vcs_repo(request.environ)
59
60 61 try:
61 62 response = self.handler(request)
62 63 finally:
64 ua = get_user_agent(request.environ)
65 call_context = get_call_context(request.registry)
66 vcs_method = call_context.get('X-RC-Method', '_NO_VCS_METHOD')
67 repo_name = call_context.get('X-RC-Repo-Name', '')
68
63 69 count = request.request_count()
64 70 _ver_ = ascii_str(vcsserver.__version__)
65 71 _path = safe_str(get_access_path(request.environ))
72
66 73 ip = '127.0.0.1'
67 74 match_route = request.matched_route.name if request.matched_route else "NOT_FOUND"
68 75 resp_code = getattr(response, 'status_code', 'UNDEFINED')
69 76
77 _view_path = f"{repo_name}@{_path}/{vcs_method}"
78
70 79 total = time.time() - start
71 80
72 _view_path = f"{repo_name}@{_path}/{vcs_method}"
73 81 log.info(
74 82 'Req[%4s] IP: %s %s Request to %s time: %.4fs [%s], VCSServer %s',
75 83 count, ip, request.environ.get('REQUEST_METHOD'),
76 84 _view_path, total, ua, _ver_,
77 85 extra={"time": total, "ver": _ver_, "code": resp_code,
78 86 "path": _path, "view_name": match_route, "user_agent": ua,
79 87 "vcs_method": vcs_method, "repo_name": repo_name}
80 88 )
81 89
82 90 statsd = request.registry.statsd
83 91 if statsd:
84 92 match_route = request.matched_route.name if request.matched_route else _path
85 93 elapsed_time_ms = round(1000.0 * total) # use ms only
86 94 statsd.timing(
87 95 "vcsserver_req_timing.histogram", elapsed_time_ms,
88 96 tags=[
89 97 "view_name:{}".format(match_route),
90 98 "code:{}".format(resp_code)
91 99 ],
92 100 use_decimals=False
93 101 )
94 102 statsd.incr(
95 103 "vcsserver_req_total", tags=[
96 104 "view_name:{}".format(match_route),
97 105 "code:{}".format(resp_code)
98 106 ])
99 107
100 108 return response
101 109
102 110
103 111 def includeme(config):
104 112 config.add_tween(
105 113 'vcsserver.tweens.request_wrapper.RequestWrapperTween',
106 114 )
General Comments 0
You need to be logged in to leave comments. Login now