##// END OF EJS Templates
fix(settings): fixed string:noquote parser, and make core.binary dir default better
super-admin -
r1217:68ec845c default
parent child Browse files
Show More
@@ -1,184 +1,185 b''
1 1 # Copyright (C) 2010-2023 RhodeCode GmbH
2 2 #
3 3 # This program is free software: you can redistribute it and/or modify
4 4 # it under the terms of the GNU Affero General Public License, version 3
5 5 # (only), as published by the Free Software Foundation.
6 6 #
7 7 # This program is distributed in the hope that it will be useful,
8 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 # GNU General Public License for more details.
11 11 #
12 12 # You should have received a copy of the GNU Affero General Public License
13 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 #
15 15 # This program is dual-licensed. If you wish to learn more about the
16 16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 18
19 19 import os
20 20 import textwrap
21 21 import string
22 22 import functools
23 23 import logging
24 24 import tempfile
25 25 import logging.config
26 26
27 27 from vcsserver.type_utils import str2bool, aslist
28 28
29 29 log = logging.getLogger(__name__)
30 30
31 31
32 32 # skip keys, that are set here, so we don't double process those
33 33 set_keys = {
34 34 '__file__': ''
35 35 }
36 36
37 37
38 38 class SettingsMaker:
39 39
40 40 def __init__(self, app_settings):
41 41 self.settings = app_settings
42 42
43 43 @classmethod
44 44 def _bool_func(cls, input_val):
45 45 if isinstance(input_val, bytes):
46 46 # decode to str
47 47 input_val = input_val.decode('utf8')
48 48 return str2bool(input_val)
49 49
50 50 @classmethod
51 51 def _int_func(cls, input_val):
52 52 return int(input_val)
53 53
54 54 @classmethod
55 55 def _float_func(cls, input_val):
56 56 return float(input_val)
57 57
58 58 @classmethod
59 59 def _list_func(cls, input_val, sep=','):
60 60 return aslist(input_val, sep=sep)
61 61
62 62 @classmethod
63 63 def _string_func(cls, input_val, lower=True):
64 64 if lower:
65 65 input_val = input_val.lower()
66 66 return input_val
67 67
68 68 @classmethod
69 69 def _string_no_quote_func(cls, input_val, lower=True):
70 70 """
71 71 Special case string function that detects if value is set to empty quote string
72 72 e.g.
73 73
74 core.binar_dir = ""
74 core.binary_dir = ""
75 75 """
76 76
77 77 input_val = cls._string_func(input_val, lower=lower)
78 78 if input_val in ['""', "''"]:
79 79 return ''
80 return input_val
80 81
81 82 @classmethod
82 83 def _dir_func(cls, input_val, ensure_dir=False, mode=0o755):
83 84
84 85 # ensure we have our dir created
85 86 if not os.path.isdir(input_val) and ensure_dir:
86 87 os.makedirs(input_val, mode=mode, exist_ok=True)
87 88
88 89 if not os.path.isdir(input_val):
89 90 raise Exception(f'Dir at {input_val} does not exist')
90 91 return input_val
91 92
92 93 @classmethod
93 94 def _file_path_func(cls, input_val, ensure_dir=False, mode=0o755):
94 95 dirname = os.path.dirname(input_val)
95 96 cls._dir_func(dirname, ensure_dir=ensure_dir)
96 97 return input_val
97 98
98 99 @classmethod
99 100 def _key_transformator(cls, key):
100 101 return "{}_{}".format('RC'.upper(), key.upper().replace('.', '_').replace('-', '_'))
101 102
102 103 def maybe_env_key(self, key):
103 104 # now maybe we have this KEY in env, search and use the value with higher priority.
104 105 transformed_key = self._key_transformator(key)
105 106 envvar_value = os.environ.get(transformed_key)
106 107 if envvar_value:
107 108 log.debug('using `%s` key instead of `%s` key for config', transformed_key, key)
108 109
109 110 return envvar_value
110 111
111 112 def env_expand(self):
112 113 replaced = {}
113 114 for k, v in self.settings.items():
114 115 if k not in set_keys:
115 116 envvar_value = self.maybe_env_key(k)
116 117 if envvar_value:
117 118 replaced[k] = envvar_value
118 119 set_keys[k] = envvar_value
119 120
120 121 # replace ALL keys updated
121 122 self.settings.update(replaced)
122 123
123 124 def enable_logging(self, logging_conf=None, level='INFO', formatter='generic'):
124 125 """
125 126 Helper to enable debug on running instance
126 127 :return:
127 128 """
128 129
129 130 if not str2bool(self.settings.get('logging.autoconfigure')):
130 131 log.info('logging configuration based on main .ini file')
131 132 return
132 133
133 134 if logging_conf is None:
134 135 logging_conf = self.settings.get('logging.logging_conf_file') or ''
135 136
136 137 if not os.path.isfile(logging_conf):
137 138 log.error('Unable to setup logging based on %s, '
138 139 'file does not exist.... specify path using logging.logging_conf_file= config setting. ', logging_conf)
139 140 return
140 141
141 142 with open(logging_conf, 'rt') as f:
142 143 ini_template = textwrap.dedent(f.read())
143 144 ini_template = string.Template(ini_template).safe_substitute(
144 145 RC_LOGGING_LEVEL=os.environ.get('RC_LOGGING_LEVEL', '') or level,
145 146 RC_LOGGING_FORMATTER=os.environ.get('RC_LOGGING_FORMATTER', '') or formatter
146 147 )
147 148
148 149 with tempfile.NamedTemporaryFile(prefix='rc_logging_', suffix='.ini', delete=False) as f:
149 150 log.info('Saved Temporary LOGGING config at %s', f.name)
150 151 f.write(ini_template)
151 152
152 153 logging.config.fileConfig(f.name)
153 154 os.remove(f.name)
154 155
155 156 def make_setting(self, key, default, lower=False, default_when_empty=False, parser=None):
156 157 input_val = self.settings.get(key, default)
157 158
158 159 if default_when_empty and not input_val:
159 160 # use default value when value is set in the config but it is empty
160 161 input_val = default
161 162
162 163 parser_func = {
163 164 'bool': self._bool_func,
164 165 'int': self._int_func,
165 166 'float': self._float_func,
166 167 'list': self._list_func,
167 168 'list:newline': functools.partial(self._list_func, sep='/n'),
168 169 'list:spacesep': functools.partial(self._list_func, sep=' '),
169 170 'string': functools.partial(self._string_func, lower=lower),
170 171 'string:noquote': functools.partial(self._string_no_quote_func, lower=lower),
171 172 'dir': self._dir_func,
172 173 'dir:ensured': functools.partial(self._dir_func, ensure_dir=True),
173 174 'file': self._file_path_func,
174 175 'file:ensured': functools.partial(self._file_path_func, ensure_dir=True),
175 176 None: lambda i: i
176 177 }[parser]
177 178
178 179 envvar_value = self.maybe_env_key(key)
179 180 if envvar_value:
180 181 input_val = envvar_value
181 182 set_keys[key] = input_val
182 183
183 184 self.settings[key] = parser_func(input_val)
184 185 return self.settings[key]
@@ -1,777 +1,779 b''
1 1 # RhodeCode VCSServer provides access to different vcs backends via network.
2 2 # Copyright (C) 2014-2023 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 platform
21 21 import sys
22 22 import locale
23 23 import logging
24 24 import uuid
25 25 import time
26 26 import wsgiref.util
27 27 import tempfile
28 28 import psutil
29 29
30 30 from itertools import chain
31 31
32 32 import msgpack
33 33 import configparser
34 34
35 35 from pyramid.config import Configurator
36 36 from pyramid.wsgi import wsgiapp
37 37 from pyramid.response import Response
38 38
39 39 from vcsserver.base import BytesEnvelope, BinaryEnvelope
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
43 43 from vcsserver.lib.statsd_client import StatsdClient
44 44 from vcsserver.tweens.request_wrapper import get_headers_call_context
45 45
46 46 import vcsserver
47 47 from vcsserver import remote_wsgi, scm_app, settings, hgpatches
48 48 from vcsserver.git_lfs.app import GIT_LFS_CONTENT_TYPE, GIT_LFS_PROTO_PAT
49 49 from vcsserver.echo_stub import remote_wsgi as remote_wsgi_stub
50 50 from vcsserver.echo_stub.echo_app import EchoApp
51 51 from vcsserver.exceptions import HTTPRepoLocked, HTTPRepoBranchProtected
52 52 from vcsserver.lib.exc_tracking import store_exception, format_exc
53 53 from vcsserver.server import VcsServer
54 54
55 55 strict_vcs = True
56 56
57 57 git_import_err = None
58 58 try:
59 59 from vcsserver.remote.git_remote import GitFactory, GitRemote
60 60 except ImportError as e:
61 61 GitFactory = None
62 62 GitRemote = None
63 63 git_import_err = e
64 64 if strict_vcs:
65 65 raise
66 66
67 67
68 68 hg_import_err = None
69 69 try:
70 70 from vcsserver.remote.hg_remote import MercurialFactory, HgRemote
71 71 except ImportError as e:
72 72 MercurialFactory = None
73 73 HgRemote = None
74 74 hg_import_err = e
75 75 if strict_vcs:
76 76 raise
77 77
78 78
79 79 svn_import_err = None
80 80 try:
81 81 from vcsserver.remote.svn_remote import SubversionFactory, SvnRemote
82 82 except ImportError as e:
83 83 SubversionFactory = None
84 84 SvnRemote = None
85 85 svn_import_err = e
86 86 if strict_vcs:
87 87 raise
88 88
89 89 log = logging.getLogger(__name__)
90 90
91 91 # due to Mercurial/glibc2.27 problems we need to detect if locale settings are
92 92 # causing problems and "fix" it in case they do and fallback to LC_ALL = C
93 93
94 94 try:
95 95 locale.setlocale(locale.LC_ALL, '')
96 96 except locale.Error as e:
97 97 log.error(
98 98 'LOCALE ERROR: failed to set LC_ALL, fallback to LC_ALL=C, org error: %s', e)
99 99 os.environ['LC_ALL'] = 'C'
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:
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:
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:
204 204 def __init__(self, val, config):
205 205 self.remotes = val
206 206
207 207 def text(self):
208 208 return f'vcs view method = {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:
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 232 self.config.registry.vcs_call_context = {}
233 233
234 234 self.global_config = global_config
235 235 self.config.include('vcsserver.lib.rc_cache')
236 236 self.config.include('vcsserver.lib.rc_cache.archive_cache')
237 237
238 238 settings_locale = settings.get('locale', '') or 'en_US.UTF-8'
239 239 vcs = VCS(locale_conf=settings_locale, cache_config=settings)
240 240 self._remotes = {
241 241 'hg': vcs._hg_remote,
242 242 'git': vcs._git_remote,
243 243 'svn': vcs._svn_remote,
244 244 'server': vcs._vcsserver,
245 245 }
246 246 if settings.get('dev.use_echo_app', 'false').lower() == 'true':
247 247 self._use_echo_app = True
248 248 log.warning("Using EchoApp for VCS operations.")
249 249 self.remote_wsgi = remote_wsgi_stub
250 250
251 251 self._configure_settings(global_config, settings)
252 252
253 253 self._configure()
254 254
255 255 def _configure_settings(self, global_config, app_settings):
256 256 """
257 257 Configure the settings module.
258 258 """
259 259 settings_merged = global_config.copy()
260 260 settings_merged.update(app_settings)
261 261
262 262 binary_dir = app_settings['core.binary_dir']
263 263
264 264 settings.BINARY_DIR = binary_dir
265 265
266 266 # from core.binary dir we set executable paths
267 267 settings.GIT_EXECUTABLE = os.path.join(binary_dir, settings.GIT_EXECUTABLE)
268 268 settings.SVN_EXECUTABLE = os.path.join(binary_dir, settings.SVN_EXECUTABLE)
269 269 settings.SVNLOOK_EXECUTABLE = os.path.join(binary_dir, settings.SVNLOOK_EXECUTABLE)
270 270
271 271 # Store the settings to make them available to other modules.
272 272 vcsserver.PYRAMID_SETTINGS = settings_merged
273 273 vcsserver.CONFIG = settings_merged
274 274
275 275 def _configure(self):
276 276 self.config.add_renderer(name='msgpack', factory=self._msgpack_renderer_factory)
277 277
278 278 self.config.add_route('service', '/_service')
279 279 self.config.add_route('status', '/status')
280 280 self.config.add_route('hg_proxy', '/proxy/hg')
281 281 self.config.add_route('git_proxy', '/proxy/git')
282 282
283 283 # rpc methods
284 284 self.config.add_route('vcs', '/{backend}')
285 285
286 286 # streaming rpc remote methods
287 287 self.config.add_route('vcs_stream', '/{backend}/stream')
288 288
289 289 # vcs operations clone/push as streaming
290 290 self.config.add_route('stream_git', '/stream/git/*repo_name')
291 291 self.config.add_route('stream_hg', '/stream/hg/*repo_name')
292 292
293 293 self.config.add_view(self.status_view, route_name='status', renderer='json')
294 294 self.config.add_view(self.service_view, route_name='service', renderer='msgpack')
295 295
296 296 self.config.add_view(self.hg_proxy(), route_name='hg_proxy')
297 297 self.config.add_view(self.git_proxy(), route_name='git_proxy')
298 298 self.config.add_view(self.vcs_view, route_name='vcs', renderer='msgpack',
299 299 vcs_view=self._remotes)
300 300 self.config.add_view(self.vcs_stream_view, route_name='vcs_stream',
301 301 vcs_view=self._remotes)
302 302
303 303 self.config.add_view(self.hg_stream(), route_name='stream_hg')
304 304 self.config.add_view(self.git_stream(), route_name='stream_git')
305 305
306 306 self.config.add_view_predicate('vcs_view', VCSViewPredicate)
307 307
308 308 self.config.add_notfound_view(not_found, renderer='json')
309 309
310 310 self.config.add_view(self.handle_vcs_exception, context=Exception)
311 311
312 312 self.config.add_tween(
313 313 'vcsserver.tweens.request_wrapper.RequestWrapperTween',
314 314 )
315 315 self.config.add_request_method(
316 316 'vcsserver.lib.request_counter.get_request_counter',
317 317 'request_count')
318 318
319 319 def wsgi_app(self):
320 320 return self.config.make_wsgi_app()
321 321
322 322 def _vcs_view_params(self, request):
323 323 remote = self._remotes[request.matchdict['backend']]
324 324 payload = msgpack.unpackb(request.body, use_list=True)
325 325
326 326 method = payload.get('method')
327 327 params = payload['params']
328 328 wire = params.get('wire')
329 329 args = params.get('args')
330 330 kwargs = params.get('kwargs')
331 331 context_uid = None
332 332
333 333 request.registry.vcs_call_context = {
334 334 'method': method,
335 335 'repo_name': payload.get('_repo_name'),
336 336 }
337 337
338 338 if wire:
339 339 try:
340 340 wire['context'] = context_uid = uuid.UUID(wire['context'])
341 341 except KeyError:
342 342 pass
343 343 args.insert(0, wire)
344 344 repo_state_uid = wire.get('repo_state_uid') if wire else None
345 345
346 346 # NOTE(marcink): trading complexity for slight performance
347 347 if log.isEnabledFor(logging.DEBUG):
348 348 # also we SKIP printing out any of those methods args since they maybe excessive
349 349 just_args_methods = {
350 350 'commitctx': ('content', 'removed', 'updated'),
351 351 'commit': ('content', 'removed', 'updated')
352 352 }
353 353 if method in just_args_methods:
354 354 skip_args = just_args_methods[method]
355 355 call_args = ''
356 356 call_kwargs = {}
357 357 for k in kwargs:
358 358 if k in skip_args:
359 359 # replace our skip key with dummy
360 360 call_kwargs[k] = f'RemovedParam({k})'
361 361 else:
362 362 call_kwargs[k] = kwargs[k]
363 363 else:
364 364 call_args = args[1:]
365 365 call_kwargs = kwargs
366 366
367 367 log.debug('Method requested:`%s` with args:%s kwargs:%s context_uid: %s, repo_state_uid:%s',
368 368 method, call_args, call_kwargs, context_uid, repo_state_uid)
369 369
370 370 statsd = request.registry.statsd
371 371 if statsd:
372 372 statsd.incr(
373 373 'vcsserver_method_total', tags=[
374 374 f"method:{method}",
375 375 ])
376 376 return payload, remote, method, args, kwargs
377 377
378 378 def vcs_view(self, request):
379 379
380 380 payload, remote, method, args, kwargs = self._vcs_view_params(request)
381 381 payload_id = payload.get('id')
382 382
383 383 try:
384 384 resp = getattr(remote, method)(*args, **kwargs)
385 385 except Exception as e:
386 386 exc_info = list(sys.exc_info())
387 387 exc_type, exc_value, exc_traceback = exc_info
388 388
389 389 org_exc = getattr(e, '_org_exc', None)
390 390 org_exc_name = None
391 391 org_exc_tb = ''
392 392 if org_exc:
393 393 org_exc_name = org_exc.__class__.__name__
394 394 org_exc_tb = getattr(e, '_org_exc_tb', '')
395 395 # replace our "faked" exception with our org
396 396 exc_info[0] = org_exc.__class__
397 397 exc_info[1] = org_exc
398 398
399 399 should_store_exc = True
400 400 if org_exc:
401 401 def get_exc_fqn(_exc_obj):
402 402 module_name = getattr(org_exc.__class__, '__module__', 'UNKNOWN')
403 403 return module_name + '.' + org_exc_name
404 404
405 405 exc_fqn = get_exc_fqn(org_exc)
406 406
407 407 if exc_fqn in ['mercurial.error.RepoLookupError',
408 408 'vcsserver.exceptions.RefNotFoundException']:
409 409 should_store_exc = False
410 410
411 411 if should_store_exc:
412 412 store_exception(id(exc_info), exc_info, request_path=request.path)
413 413
414 414 tb_info = format_exc(exc_info)
415 415
416 416 type_ = e.__class__.__name__
417 417 if type_ not in self.ALLOWED_EXCEPTIONS:
418 418 type_ = None
419 419
420 420 resp = {
421 421 'id': payload_id,
422 422 'error': {
423 423 'message': str(e),
424 424 'traceback': tb_info,
425 425 'org_exc': org_exc_name,
426 426 'org_exc_tb': org_exc_tb,
427 427 'type': type_
428 428 }
429 429 }
430 430
431 431 try:
432 432 resp['error']['_vcs_kind'] = getattr(e, '_vcs_kind', None)
433 433 except AttributeError:
434 434 pass
435 435 else:
436 436 resp = {
437 437 'id': payload_id,
438 438 'result': resp
439 439 }
440 440 log.debug('Serving data for method %s', method)
441 441 return resp
442 442
443 443 def vcs_stream_view(self, request):
444 444 payload, remote, method, args, kwargs = self._vcs_view_params(request)
445 445 # this method has a stream: marker we remove it here
446 446 method = method.split('stream:')[-1]
447 447 chunk_size = safe_int(payload.get('chunk_size')) or 4096
448 448
449 449 resp = getattr(remote, method)(*args, **kwargs)
450 450
451 451 def get_chunked_data(method_resp):
452 452 stream = io.BytesIO(method_resp)
453 453 while 1:
454 454 chunk = stream.read(chunk_size)
455 455 if not chunk:
456 456 break
457 457 yield chunk
458 458
459 459 response = Response(app_iter=get_chunked_data(resp))
460 460 response.content_type = 'application/octet-stream'
461 461
462 462 return response
463 463
464 464 def status_view(self, request):
465 465 import vcsserver
466 466 _platform_id = platform.uname()[1] or 'instance'
467 467
468 468 return {
469 469 "status": "OK",
470 470 "vcsserver_version": vcsserver.get_version(),
471 471 "platform": _platform_id,
472 472 "pid": os.getpid(),
473 473 }
474 474
475 475 def service_view(self, request):
476 476 import vcsserver
477 477
478 478 payload = msgpack.unpackb(request.body, use_list=True)
479 479 server_config, app_config = {}, {}
480 480
481 481 try:
482 482 path = self.global_config['__file__']
483 483 config = configparser.RawConfigParser()
484 484
485 485 config.read(path)
486 486
487 487 if config.has_section('server:main'):
488 488 server_config = dict(config.items('server:main'))
489 489 if config.has_section('app:main'):
490 490 app_config = dict(config.items('app:main'))
491 491
492 492 except Exception:
493 493 log.exception('Failed to read .ini file for display')
494 494
495 495 environ = list(os.environ.items())
496 496
497 497 resp = {
498 498 'id': payload.get('id'),
499 499 'result': dict(
500 500 version=vcsserver.get_version(),
501 501 config=server_config,
502 502 app_config=app_config,
503 503 environ=environ,
504 504 payload=payload,
505 505 )
506 506 }
507 507 return resp
508 508
509 509 def _msgpack_renderer_factory(self, info):
510 510
511 511 def _render(value, system):
512 512 bin_type = False
513 513 res = value.get('result')
514 514 if isinstance(res, BytesEnvelope):
515 515 log.debug('Result is wrapped in BytesEnvelope type')
516 516 bin_type = True
517 517 elif isinstance(res, BinaryEnvelope):
518 518 log.debug('Result is wrapped in BinaryEnvelope type')
519 519 value['result'] = res.val
520 520 bin_type = True
521 521
522 522 request = system.get('request')
523 523 if request is not None:
524 524 response = request.response
525 525 ct = response.content_type
526 526 if ct == response.default_content_type:
527 527 response.content_type = 'application/x-msgpack'
528 528 if bin_type:
529 529 response.content_type = 'application/x-msgpack-bin'
530 530
531 531 return msgpack.packb(value, use_bin_type=bin_type)
532 532 return _render
533 533
534 534 def set_env_from_config(self, environ, config):
535 535 dict_conf = {}
536 536 try:
537 537 for elem in config:
538 538 if elem[0] == 'rhodecode':
539 539 dict_conf = json.loads(elem[2])
540 540 break
541 541 except Exception:
542 542 log.exception('Failed to fetch SCM CONFIG')
543 543 return
544 544
545 545 username = dict_conf.get('username')
546 546 if username:
547 547 environ['REMOTE_USER'] = username
548 548 # mercurial specific, some extension api rely on this
549 549 environ['HGUSER'] = username
550 550
551 551 ip = dict_conf.get('ip')
552 552 if ip:
553 553 environ['REMOTE_HOST'] = ip
554 554
555 555 if _is_request_chunked(environ):
556 556 # set the compatibility flag for webob
557 557 environ['wsgi.input_terminated'] = True
558 558
559 559 def hg_proxy(self):
560 560 @wsgiapp
561 561 def _hg_proxy(environ, start_response):
562 562 app = WsgiProxy(self.remote_wsgi.HgRemoteWsgi())
563 563 return app(environ, start_response)
564 564 return _hg_proxy
565 565
566 566 def git_proxy(self):
567 567 @wsgiapp
568 568 def _git_proxy(environ, start_response):
569 569 app = WsgiProxy(self.remote_wsgi.GitRemoteWsgi())
570 570 return app(environ, start_response)
571 571 return _git_proxy
572 572
573 573 def hg_stream(self):
574 574 if self._use_echo_app:
575 575 @wsgiapp
576 576 def _hg_stream(environ, start_response):
577 577 app = EchoApp('fake_path', 'fake_name', None)
578 578 return app(environ, start_response)
579 579 return _hg_stream
580 580 else:
581 581 @wsgiapp
582 582 def _hg_stream(environ, start_response):
583 583 log.debug('http-app: handling hg stream')
584 584 call_context = get_headers_call_context(environ)
585 585
586 586 repo_path = call_context['repo_path']
587 587 repo_name = call_context['repo_name']
588 588 config = call_context['repo_config']
589 589
590 590 app = scm_app.create_hg_wsgi_app(
591 591 repo_path, repo_name, config)
592 592
593 593 # Consistent path information for hgweb
594 594 environ['PATH_INFO'] = call_context['path_info']
595 595 environ['REPO_NAME'] = repo_name
596 596 self.set_env_from_config(environ, config)
597 597
598 598 log.debug('http-app: starting app handler '
599 599 'with %s and process request', app)
600 600 return app(environ, ResponseFilter(start_response))
601 601 return _hg_stream
602 602
603 603 def git_stream(self):
604 604 if self._use_echo_app:
605 605 @wsgiapp
606 606 def _git_stream(environ, start_response):
607 607 app = EchoApp('fake_path', 'fake_name', None)
608 608 return app(environ, start_response)
609 609 return _git_stream
610 610 else:
611 611 @wsgiapp
612 612 def _git_stream(environ, start_response):
613 613 log.debug('http-app: handling git stream')
614 614
615 615 call_context = get_headers_call_context(environ)
616 616
617 617 repo_path = call_context['repo_path']
618 618 repo_name = call_context['repo_name']
619 619 config = call_context['repo_config']
620 620
621 621 environ['PATH_INFO'] = call_context['path_info']
622 622 self.set_env_from_config(environ, config)
623 623
624 624 content_type = environ.get('CONTENT_TYPE', '')
625 625
626 626 path = environ['PATH_INFO']
627 627 is_lfs_request = GIT_LFS_CONTENT_TYPE in content_type
628 628 log.debug(
629 629 'LFS: Detecting if request `%s` is LFS server path based '
630 630 'on content type:`%s`, is_lfs:%s',
631 631 path, content_type, is_lfs_request)
632 632
633 633 if not is_lfs_request:
634 634 # fallback detection by path
635 635 if GIT_LFS_PROTO_PAT.match(path):
636 636 is_lfs_request = True
637 637 log.debug(
638 638 'LFS: fallback detection by path of: `%s`, is_lfs:%s',
639 639 path, is_lfs_request)
640 640
641 641 if is_lfs_request:
642 642 app = scm_app.create_git_lfs_wsgi_app(
643 643 repo_path, repo_name, config)
644 644 else:
645 645 app = scm_app.create_git_wsgi_app(
646 646 repo_path, repo_name, config)
647 647
648 648 log.debug('http-app: starting app handler '
649 649 'with %s and process request', app)
650 650
651 651 return app(environ, start_response)
652 652
653 653 return _git_stream
654 654
655 655 def handle_vcs_exception(self, exception, request):
656 656 _vcs_kind = getattr(exception, '_vcs_kind', '')
657 657
658 658 if _vcs_kind == 'repo_locked':
659 659 headers_call_context = get_headers_call_context(request.environ)
660 660 status_code = safe_int(headers_call_context['locked_status_code'])
661 661
662 662 return HTTPRepoLocked(
663 663 title=str(exception), status_code=status_code, headers=[('X-Rc-Locked', '1')])
664 664
665 665 elif _vcs_kind == 'repo_branch_protected':
666 666 # Get custom repo-branch-protected status code if present.
667 667 return HTTPRepoBranchProtected(
668 668 title=str(exception), headers=[('X-Rc-Branch-Protection', '1')])
669 669
670 670 exc_info = request.exc_info
671 671 store_exception(id(exc_info), exc_info)
672 672
673 673 traceback_info = 'unavailable'
674 674 if request.exc_info:
675 675 traceback_info = format_exc(request.exc_info)
676 676
677 677 log.error(
678 678 'error occurred handling this request for path: %s, \n%s',
679 679 request.path, traceback_info)
680 680
681 681 statsd = request.registry.statsd
682 682 if statsd:
683 683 exc_type = f"{exception.__class__.__module__}.{exception.__class__.__name__}"
684 684 statsd.incr('vcsserver_exception_total',
685 685 tags=[f"type:{exc_type}"])
686 686 raise exception
687 687
688 688
689 689 class ResponseFilter:
690 690
691 691 def __init__(self, start_response):
692 692 self._start_response = start_response
693 693
694 694 def __call__(self, status, response_headers, exc_info=None):
695 695 headers = tuple(
696 696 (h, v) for h, v in response_headers
697 697 if not wsgiref.util.is_hop_by_hop(h))
698 698 return self._start_response(status, headers, exc_info)
699 699
700 700
701 701 def sanitize_settings_and_apply_defaults(global_config, settings):
702 702 _global_settings_maker = SettingsMaker(global_config)
703 703 settings_maker = SettingsMaker(settings)
704 704
705 705 settings_maker.make_setting('logging.autoconfigure', False, parser='bool')
706 706
707 707 logging_conf = os.path.join(os.path.dirname(global_config.get('__file__')), 'logging.ini')
708 708 settings_maker.enable_logging(logging_conf)
709 709
710 710 # Default includes, possible to change as a user
711 711 pyramid_includes = settings_maker.make_setting('pyramid.includes', [], parser='list:newline')
712 712 log.debug("Using the following pyramid.includes: %s", pyramid_includes)
713 713
714 714 settings_maker.make_setting('__file__', global_config.get('__file__'))
715 715
716 716 settings_maker.make_setting('pyramid.default_locale_name', 'en')
717 717 settings_maker.make_setting('locale', 'en_US.UTF-8')
718 718
719 settings_maker.make_setting('core.binary_dir', '/usr/local/bin/rhodecode_bin/vcs_bin', parser='string:noquote')
719 settings_maker.make_setting(
720 'core.binary_dir', '/usr/local/bin/rhodecode_bin/vcs_bin',
721 default_when_empty=True, parser='string:noquote')
720 722
721 723 temp_store = tempfile.gettempdir()
722 724 default_cache_dir = os.path.join(temp_store, 'rc_cache')
723 725 # save default, cache dir, and use it for all backends later.
724 726 default_cache_dir = settings_maker.make_setting(
725 727 'cache_dir',
726 728 default=default_cache_dir, default_when_empty=True,
727 729 parser='dir:ensured')
728 730
729 731 # exception store cache
730 732 settings_maker.make_setting(
731 733 'exception_tracker.store_path',
732 734 default=os.path.join(default_cache_dir, 'exc_store'), default_when_empty=True,
733 735 parser='dir:ensured'
734 736 )
735 737
736 738 # repo_object cache defaults
737 739 settings_maker.make_setting(
738 740 'rc_cache.repo_object.backend',
739 741 default='dogpile.cache.rc.file_namespace',
740 742 parser='string')
741 743 settings_maker.make_setting(
742 744 'rc_cache.repo_object.expiration_time',
743 745 default=30 * 24 * 60 * 60, # 30days
744 746 parser='int')
745 747 settings_maker.make_setting(
746 748 'rc_cache.repo_object.arguments.filename',
747 749 default=os.path.join(default_cache_dir, 'vcsserver_cache_repo_object.db'),
748 750 parser='string')
749 751
750 752 # statsd
751 753 settings_maker.make_setting('statsd.enabled', False, parser='bool')
752 754 settings_maker.make_setting('statsd.statsd_host', 'statsd-exporter', parser='string')
753 755 settings_maker.make_setting('statsd.statsd_port', 9125, parser='int')
754 756 settings_maker.make_setting('statsd.statsd_prefix', '')
755 757 settings_maker.make_setting('statsd.statsd_ipv6', False, parser='bool')
756 758
757 759 settings_maker.env_expand()
758 760
759 761
760 762 def main(global_config, **settings):
761 763 start_time = time.time()
762 764 log.info('Pyramid app config starting')
763 765
764 766 if MercurialFactory:
765 767 hgpatches.patch_largefiles_capabilities()
766 768 hgpatches.patch_subrepo_type_mapping()
767 769
768 770 # Fill in and sanitize the defaults & do ENV expansion
769 771 sanitize_settings_and_apply_defaults(global_config, settings)
770 772
771 773 # init and bootstrap StatsdClient
772 774 StatsdClient.setup(settings)
773 775
774 776 pyramid_app = HTTPApplication(settings=settings, global_config=global_config).wsgi_app()
775 777 total_time = time.time() - start_time
776 778 log.info('Pyramid app created and configured in %.2fs', total_time)
777 779 return pyramid_app
General Comments 0
You need to be logged in to leave comments. Login now