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