##// END OF EJS Templates
core: re-implemented the way how configuration can be made...
super-admin -
r1021:a797b226 default
parent child Browse files
Show More
@@ -0,0 +1,53 b''
1 ; #####################
2 ; LOGGING CONFIGURATION
3 ; #####################
4 ; Logging template, used for configure the logging
5 ; some variables here are replaced by RhodeCode to default values
6
7 [loggers]
8 keys = root, vcsserver
9
10 [handlers]
11 keys = console
12
13 [formatters]
14 keys = generic, json
15
16 ; #######
17 ; LOGGERS
18 ; #######
19 [logger_root]
20 level = NOTSET
21 handlers = console
22
23 [logger_vcsserver]
24 level = $RC_LOGGING_LEVEL
25 handlers =
26 qualname = vcsserver
27 propagate = 1
28
29 ; ########
30 ; HANDLERS
31 ; ########
32
33 [handler_console]
34 class = StreamHandler
35 args = (sys.stderr, )
36 level = $RC_LOGGING_LEVEL
37 ; To enable JSON formatted logs replace generic with json
38 ; This allows sending properly formatted logs to grafana loki or elasticsearch
39 #formatter = json
40 #formatter = generic
41 formatter = $RC_LOGGING_FORMATTER
42
43 ; ##########
44 ; FORMATTERS
45 ; ##########
46
47 [formatter_generic]
48 format = %(asctime)s.%(msecs)03d [%(process)d] %(levelname)-5.5s [%(name)s] %(message)s
49 datefmt = %Y-%m-%d %H:%M:%S
50
51 [formatter_json]
52 format = %(timestamp)s %(levelname)s %(name)s %(message)s %(req_id)s
53 class = vcsserver.lib._vendor.jsonlogger.JsonFormatter
1 NO CONTENT: new file 100644
@@ -0,0 +1,177 b''
1 # -*- coding: utf-8 -*-
2
3 # Copyright (C) 2010-2020 RhodeCode GmbH
4 #
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
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 Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
21 import os
22 import textwrap
23 import string
24 import functools
25 import logging
26 import tempfile
27 import logging.config
28 log = logging.getLogger(__name__)
29
30
31 def str2bool(_str):
32 """
33 returns True/False value from given string, it tries to translate the
34 string into boolean
35
36 :param _str: string value to translate into boolean
37 :rtype: boolean
38 :returns: boolean from given string
39 """
40 if _str is None:
41 return False
42 if _str in (True, False):
43 return _str
44 _str = str(_str).strip().lower()
45 return _str in ('t', 'true', 'y', 'yes', 'on', '1')
46
47
48 def aslist(obj, sep=None, strip=True):
49 """
50 Returns given string separated by sep as list
51
52 :param obj:
53 :param sep:
54 :param strip:
55 """
56 if isinstance(obj, (basestring,)):
57 lst = obj.split(sep)
58 if strip:
59 lst = [v.strip() for v in lst]
60 return lst
61 elif isinstance(obj, (list, tuple)):
62 return obj
63 elif obj is None:
64 return []
65 else:
66 return [obj]
67
68
69 class SettingsMaker(object):
70
71 def __init__(self, app_settings):
72 self.settings = app_settings
73
74 @classmethod
75 def _bool_func(cls, input_val):
76 if isinstance(input_val, unicode):
77 input_val = input_val.encode('utf8')
78 return str2bool(input_val)
79
80 @classmethod
81 def _int_func(cls, input_val):
82 return int(input_val)
83
84 @classmethod
85 def _list_func(cls, input_val, sep=','):
86 return aslist(input_val, sep=sep)
87
88 @classmethod
89 def _string_func(cls, input_val, lower=True):
90 if lower:
91 input_val = input_val.lower()
92 return input_val
93
94 @classmethod
95 def _float_func(cls, input_val):
96 return float(input_val)
97
98 @classmethod
99 def _dir_func(cls, input_val, ensure_dir=False, mode=0o755):
100
101 # ensure we have our dir created
102 if not os.path.isdir(input_val) and ensure_dir:
103 os.makedirs(input_val, mode=mode)
104
105 if not os.path.isdir(input_val):
106 raise Exception('Dir at {} does not exist'.format(input_val))
107 return input_val
108
109 @classmethod
110 def _file_path_func(cls, input_val, ensure_dir=False, mode=0o755):
111 dirname = os.path.dirname(input_val)
112 cls._dir_func(dirname, ensure_dir=ensure_dir)
113 return input_val
114
115 @classmethod
116 def _key_transformator(cls, key):
117 return "{}_{}".format('RC'.upper(), key.upper().replace('.', '_').replace('-', '_'))
118
119 def enable_logging(self, logging_conf=None):
120 """
121 Helper to enable debug on running instance
122 :return:
123 """
124 if not str2bool(self.settings.get('logging.autoconfigure')):
125 log.info('logging configuration based on main .ini file')
126 return
127
128 if logging_conf is None:
129 logging_conf = self.settings.get('logging.logging_conf_file') or ''
130
131 if not os.path.isfile(logging_conf):
132 log.error('Unable to setup logging based on %s, file does not exist...', logging_conf)
133 return
134
135 with open(logging_conf, 'rb') as f:
136 ini_template = textwrap.dedent(f.read())
137 ini_template = string.Template(ini_template).safe_substitute(
138 RC_LOGGING_LEVEL=os.environ.get('RC_LOGGING_LEVEL', '') or 'INFO',
139 RC_LOGGING_FORMATTER=os.environ.get('RC_LOGGING_FORMATTER', '') or 'generic'
140 )
141
142 with tempfile.NamedTemporaryFile(prefix='rc_logging_', suffix='.ini', delete=False) as f:
143 log.info('Saved Temporary LOGGING config at %s', f.name)
144 f.write(ini_template)
145
146 logging.config.fileConfig(f.name)
147 os.remove(f.name)
148
149 def make_setting(self, key, default, lower=False, default_when_empty=False, parser=None):
150
151 input_val = self.settings.get(key, default)
152
153 if default_when_empty and not input_val:
154 # use default value when value is set in the config but it is empty
155 input_val = default
156
157 parser_func = {
158 'bool': self._bool_func,
159 'int': self._int_func,
160 'list': self._list_func,
161 'list:newline': functools.partial(self._list_func, sep='/n'),
162 'string': functools.partial(self._string_func, lower=lower),
163 'dir': self._dir_func,
164 'dir:ensured': functools.partial(self._dir_func, ensure_dir=True),
165 'file': self._file_path_func,
166 'file:ensured': functools.partial(self._file_path_func, ensure_dir=True),
167 None: lambda i: i
168 }[parser]
169
170 # now maybe we have this KEY in env, search and use the value with higher priority.
171 transformed_key = self._key_transformator(key)
172 envvar_value = os.environ.get(transformed_key)
173 if envvar_value:
174 log.debug('using `%s` key instead of `%s` key for config', transformed_key, key)
175 input_val = envvar_value
176 self.settings[key] = parser_func(input_val)
177 return self.settings[key]
@@ -1,256 +1,271 b''
1 1 ## -*- coding: utf-8 -*-
2 2
3 3 ; #################################
4 4 ; RHODECODE VCSSERVER CONFIGURATION
5 5 ; #################################
6 6
7 7 [server:main]
8 8 ; COMMON HOST/IP CONFIG
9 9 host = 0.0.0.0
10 10 port = 9900
11 11
12 12 ; ##################################################
13 13 ; WAITRESS WSGI SERVER - Recommended for Development
14 14 ; ##################################################
15 15
16 16 ; use server type
17 17 use = egg:waitress#main
18 18
19 19 ; number of worker threads
20 20 threads = 5
21 21
22 22 ; MAX BODY SIZE 100GB
23 23 max_request_body_size = 107374182400
24 24
25 25 ; Use poll instead of select, fixes file descriptors limits problems.
26 26 ; May not work on old windows systems.
27 27 asyncore_use_poll = true
28 28
29 29
30 30 ; ###########################
31 31 ; GUNICORN APPLICATION SERVER
32 32 ; ###########################
33 33
34 ; run with gunicorn --log-config rhodecode.ini --paste rhodecode.ini
34 ; run with gunicorn --paste rhodecode.ini
35 35
36 36 ; Module to use, this setting shouldn't be changed
37 37 #use = egg:gunicorn#main
38 38
39 39 ; Sets the number of process workers. More workers means more concurrent connections
40 40 ; RhodeCode can handle at the same time. Each additional worker also it increases
41 41 ; memory usage as each has it's own set of caches.
42 42 ; Recommended value is (2 * NUMBER_OF_CPUS + 1), eg 2CPU = 5 workers, but no more
43 43 ; than 8-10 unless for really big deployments .e.g 700-1000 users.
44 44 ; `instance_id = *` must be set in the [app:main] section below (which is the default)
45 45 ; when using more than 1 worker.
46 46 #workers = 2
47 47
48 48 ; Gunicorn access log level
49 49 #loglevel = info
50 50
51 51 ; Process name visible in process list
52 52 #proc_name = rhodecode_vcsserver
53 53
54 54 ; Type of worker class, one of `sync`, `gevent`
55 55 ; currently `sync` is the only option allowed.
56 56 #worker_class = sync
57 57
58 58 ; The maximum number of simultaneous clients. Valid only for gevent
59 59 #worker_connections = 10
60 60
61 61 ; Max number of requests that worker will handle before being gracefully restarted.
62 62 ; Prevents memory leaks, jitter adds variability so not all workers are restarted at once.
63 63 #max_requests = 1000
64 64 #max_requests_jitter = 30
65 65
66 66 ; Amount of time a worker can spend with handling a request before it
67 67 ; gets killed and restarted. By default set to 21600 (6hrs)
68 68 ; Examples: 1800 (30min), 3600 (1hr), 7200 (2hr), 43200 (12h)
69 69 #timeout = 21600
70 70
71 71 ; The maximum size of HTTP request line in bytes.
72 72 ; 0 for unlimited
73 73 #limit_request_line = 0
74 74
75 75 ; Limit the number of HTTP headers fields in a request.
76 76 ; By default this value is 100 and can't be larger than 32768.
77 77 #limit_request_fields = 32768
78 78
79 79 ; Limit the allowed size of an HTTP request header field.
80 80 ; Value is a positive number or 0.
81 81 ; Setting it to 0 will allow unlimited header field sizes.
82 82 #limit_request_field_size = 0
83 83
84 84 ; Timeout for graceful workers restart.
85 85 ; After receiving a restart signal, workers have this much time to finish
86 86 ; serving requests. Workers still alive after the timeout (starting from the
87 87 ; receipt of the restart signal) are force killed.
88 88 ; Examples: 1800 (30min), 3600 (1hr), 7200 (2hr), 43200 (12h)
89 #graceful_timeout = 3600
89 #graceful_timeout = 21600
90 90
91 91 # The number of seconds to wait for requests on a Keep-Alive connection.
92 92 # Generally set in the 1-5 seconds range.
93 93 #keepalive = 2
94 94
95 95 ; Maximum memory usage that each worker can use before it will receive a
96 96 ; graceful restart signal 0 = memory monitoring is disabled
97 97 ; Examples: 268435456 (256MB), 536870912 (512MB)
98 98 ; 1073741824 (1GB), 2147483648 (2GB), 4294967296 (4GB)
99 99 #memory_max_usage = 0
100 100
101 101 ; How often in seconds to check for memory usage for each gunicorn worker
102 102 #memory_usage_check_interval = 60
103 103
104 104 ; Threshold value for which we don't recycle worker if GarbageCollection
105 105 ; frees up enough resources. Before each restart we try to run GC on worker
106 106 ; in case we get enough free memory after that, restart will not happen.
107 107 #memory_usage_recovery_threshold = 0.8
108 108
109 109
110 110 [app:main]
111 111 ; The %(here)s variable will be replaced with the absolute path of parent directory
112 112 ; of this file
113 ; Each option in the app:main can be override by an environmental variable
114 ;
115 ;To override an option:
116 ;
117 ;RC_<KeyName>
118 ;Everything should be uppercase, . and - should be replaced by _.
119 ;For example, if you have these configuration settings:
120 ;rc_cache.repo_object.backend = foo
121 ;can be overridden by
122 ;export RC_CACHE_REPO_OBJECT_BACKEND=foo
123
113 124 use = egg:rhodecode-vcsserver
114 125
115 126
116 127 ; #############
117 128 ; DEBUG OPTIONS
118 129 ; #############
119 130
120 131 # During development the we want to have the debug toolbar enabled
121 132 pyramid.includes =
122 133 pyramid_debugtoolbar
123 134
124 135 debugtoolbar.hosts = 0.0.0.0/0
125 136 debugtoolbar.exclude_prefixes =
126 137 /css
127 138 /fonts
128 139 /images
129 140 /js
130 141
131 142 ; #################
132 143 ; END DEBUG OPTIONS
133 144 ; #################
134 145
135 146 ; Pyramid default locales, we need this to be set
136 pyramid.default_locale_name = en
147 #pyramid.default_locale_name = en
137 148
138 149 ; default locale used by VCS systems
139 locale = en_US.UTF-8
150 #locale = en_US.UTF-8
140 151
141 152 ; path to binaries for vcsserver, it should be set by the installer
142 ; at installation time, e.g /home/user/vcsserver-1/profile/bin
153 ; at installation time, e.g /home/user/.rccontrol/vcsserver-1/profile/bin
143 154 ; it can also be a path to nix-build output in case of development
144 155 core.binary_dir = ""
145 156
146 157 ; Custom exception store path, defaults to TMPDIR
147 158 ; This is used to store exception from RhodeCode in shared directory
148 159 #exception_tracker.store_path =
149 160
150 161 ; #############
151 162 ; DOGPILE CACHE
152 163 ; #############
153 164
154 165 ; Default cache dir for caches. Putting this into a ramdisk can boost performance.
155 166 ; eg. /tmpfs/data_ramdisk, however this directory might require large amount of space
156 cache_dir = %(here)s/data
167 #cache_dir = %(here)s/data
157 168
158 169 ; ***************************************
159 170 ; `repo_object` cache, default file based
160 171 ; ***************************************
161 172
162 173 ; `repo_object` cache settings for vcs methods for repositories
163 rc_cache.repo_object.backend = dogpile.cache.rc.file_namespace
174 #rc_cache.repo_object.backend = dogpile.cache.rc.file_namespace
164 175
165 176 ; cache auto-expires after N seconds
166 177 ; Examples: 86400 (1Day), 604800 (7Days), 1209600 (14Days), 2592000 (30days), 7776000 (90Days)
167 rc_cache.repo_object.expiration_time = 2592000
178 #rc_cache.repo_object.expiration_time = 2592000
168 179
169 180 ; file cache store path. Defaults to `cache_dir =` value or tempdir if both values are not set
170 #rc_cache.repo_object.arguments.filename = /tmp/vcsserver_cache.db
181 #rc_cache.repo_object.arguments.filename = /tmp/vcsserver_cache_repo_object.db
171 182
172 183 ; ***********************************************************
173 184 ; `repo_object` cache with redis backend
174 185 ; recommended for larger instance, and for better performance
175 186 ; ***********************************************************
176 187
177 188 ; `repo_object` cache settings for vcs methods for repositories
178 189 #rc_cache.repo_object.backend = dogpile.cache.rc.redis_msgpack
179 190
180 191 ; cache auto-expires after N seconds
181 192 ; Examples: 86400 (1Day), 604800 (7Days), 1209600 (14Days), 2592000 (30days), 7776000 (90Days)
182 193 #rc_cache.repo_object.expiration_time = 2592000
183 194
184 195 ; redis_expiration_time needs to be greater then expiration_time
185 196 #rc_cache.repo_object.arguments.redis_expiration_time = 3592000
186 197
187 198 #rc_cache.repo_object.arguments.host = localhost
188 199 #rc_cache.repo_object.arguments.port = 6379
189 200 #rc_cache.repo_object.arguments.db = 5
190 201 #rc_cache.repo_object.arguments.socket_timeout = 30
191 202 ; more Redis options: https://dogpilecache.sqlalchemy.org/en/latest/api.html#redis-backends
192 203 #rc_cache.repo_object.arguments.distributed_lock = true
193 204
194 205 ; auto-renew lock to prevent stale locks, slower but safer. Use only if problems happen
195 206 #rc_cache.repo_object.arguments.lock_auto_renewal = true
196 207
197 208 ; Statsd client config, this is used to send metrics to statsd
198 209 ; We recommend setting statsd_exported and scrape them using Promethues
199 210 #statsd.enabled = false
200 211 #statsd.statsd_host = 0.0.0.0
201 212 #statsd.statsd_port = 8125
202 213 #statsd.statsd_prefix =
203 214 #statsd.statsd_ipv6 = false
204 215
216 ; configure logging automatically at server startup set to false
217 ; to use the below custom logging config.
218 #logging.autoconfigure = true
219
220 ; specify your own custom logging config file to configure logging
221 #logging.logging_conf_file = /path/to/custom_logging.ini
222
205 223 ; #####################
206 224 ; LOGGING CONFIGURATION
207 225 ; #####################
208 [loggers]
209 keys = root, vcsserver
226 #[loggers]
227 #keys = root, vcsserver
210 228
211 [handlers]
212 keys = console
229 #[handlers]
230 #keys = console
213 231
214 [formatters]
215 keys = generic
232 #[formatters]
233 #keys = generic
216 234
217 235 ; #######
218 236 ; LOGGERS
219 237 ; #######
220 [logger_root]
221 level = NOTSET
222 handlers = console
238 #[logger_root]
239 #level = NOTSET
240 #handlers = console
223 241
224 [logger_vcsserver]
225 level = DEBUG
226 handlers =
227 qualname = vcsserver
228 propagate = 1
229
242 #[logger_vcsserver]
243 #level = INFO
244 #handlers =
245 #qualname = vcsserver
246 #propagate = 1
230 247
231 248 ; ########
232 249 ; HANDLERS
233 250 ; ########
234 251
235 [handler_console]
236 class = StreamHandler
237 args = (sys.stderr, )
238 level = DEBUG
239 formatter = generic
252 #[handler_console]
253 #class = StreamHandler
254 #args = (sys.stderr, )
255 #level = INFO
240 256 ; To enable JSON formatted logs replace generic with json
241 257 ; This allows sending properly formatted logs to grafana loki or elasticsearch
242 258 #formatter = json
243
259 #formatter = generic
244 260
245 261 ; ##########
246 262 ; FORMATTERS
247 263 ; ##########
248 264
249 [formatter_generic]
250 format = %(asctime)s.%(msecs)03d [%(process)d] %(levelname)-5.5s [%(name)s] %(message)s
251 datefmt = %Y-%m-%d %H:%M:%S
265 #[formatter_generic]
266 #format = %(asctime)s.%(msecs)03d [%(process)d] %(levelname)-5.5s [%(name)s] %(message)s
267 #datefmt = %Y-%m-%d %H:%M:%S
252 268
253 [formatter_json]
254 format = %(timestamp)s %(levelname)s %(name)s %(message)s %(req_id)s
255 class = vcsserver.lib._vendor.jsonlogger.JsonFormatter
256
269 #[formatter_json]
270 #format = %(timestamp)s %(levelname)s %(name)s %(message)s %(req_id)s
271 #class = vcsserver.lib._vendor.jsonlogger.JsonFormatter
@@ -1,269 +1,272 b''
1 1 """
2 2 Gunicorn config extension and hooks. This config file adds some extra settings and memory management.
3 3 Gunicorn configuration should be managed by .ini files entries of RhodeCode or VCSServer
4 4 """
5 5
6 6 import gc
7 7 import os
8 8 import sys
9 9 import math
10 10 import time
11 11 import threading
12 12 import traceback
13 13 import random
14 14 from gunicorn.glogging import Logger
15 15
16 16
17 17 def get_workers():
18 18 import multiprocessing
19 19 return multiprocessing.cpu_count() * 2 + 1
20 20
21 21 # GLOBAL
22 22 errorlog = '-'
23 23 accesslog = '-'
24 24
25 25
26 26 # SERVER MECHANICS
27 27 # None == system temp dir
28 28 # worker_tmp_dir is recommended to be set to some tmpfs
29 29 worker_tmp_dir = None
30 30 tmp_upload_dir = None
31 31
32 32 # Custom log format
33 access_log_format = (
34 '%(t)s %(p)s INFO [GNCRN] %(h)-15s rqt:%(L)s %(s)s %(b)-6s "%(m)s:%(U)s %(q)s" usr:%(u)s "%(f)s" "%(a)s"')
33 #access_log_format = (
34 # '%(t)s %(p)s INFO [GNCRN] %(h)-15s rqt:%(L)s %(s)s %(b)-6s "%(m)s:%(U)s %(q)s" usr:%(u)s "%(f)s" "%(a)s"')
35 35
36 36 # loki format for easier parsing in grafana
37 #access_log_format = (
38 # 'time="%(t)s" pid=%(p)s level="INFO" type="[GNCRN]" ip="%(h)-15s" rqt="%(L)s" response_code="%(s)s" response_bytes="%(b)-6s" uri="%(m)s:%(U)s %(q)s" user=":%(u)s" user_agent="%(a)s"')
37 access_log_format = (
38 'time="%(t)s" pid=%(p)s level="INFO" type="[GNCRN]" ip="%(h)-15s" rqt="%(L)s" response_code="%(s)s" response_bytes="%(b)-6s" uri="%(m)s:%(U)s %(q)s" user=":%(u)s" user_agent="%(a)s"')
39 39
40 40 # self adjust workers based on CPU count
41 41 # workers = get_workers()
42 42
43 43
44 44 def _get_process_rss(pid=None):
45 45 try:
46 46 import psutil
47 47 if pid:
48 48 proc = psutil.Process(pid)
49 49 else:
50 50 proc = psutil.Process()
51 51 return proc.memory_info().rss
52 52 except Exception:
53 53 return None
54 54
55 55
56 56 def _get_config(ini_path):
57 57
58 58 try:
59 59 import configparser
60 60 except ImportError:
61 61 import ConfigParser as configparser
62 62 try:
63 63 config = configparser.RawConfigParser()
64 64 config.read(ini_path)
65 65 return config
66 66 except Exception:
67 67 return None
68 68
69 69
70 70 def _time_with_offset(memory_usage_check_interval):
71 71 return time.time() - random.randint(0, memory_usage_check_interval/2.0)
72 72
73 73
74 74 def pre_fork(server, worker):
75 75 pass
76 76
77 77
78 78 def post_fork(server, worker):
79 79
80 80 # memory spec defaults
81 81 _memory_max_usage = 0
82 82 _memory_usage_check_interval = 60
83 83 _memory_usage_recovery_threshold = 0.8
84 84
85 85 ini_path = os.path.abspath(server.cfg.paste)
86 86 conf = _get_config(ini_path)
87 87
88 88 section = 'server:main'
89 89 if conf and conf.has_section(section):
90 90
91 91 if conf.has_option(section, 'memory_max_usage'):
92 92 _memory_max_usage = conf.getint(section, 'memory_max_usage')
93 93
94 94 if conf.has_option(section, 'memory_usage_check_interval'):
95 95 _memory_usage_check_interval = conf.getint(section, 'memory_usage_check_interval')
96 96
97 97 if conf.has_option(section, 'memory_usage_recovery_threshold'):
98 98 _memory_usage_recovery_threshold = conf.getfloat(section, 'memory_usage_recovery_threshold')
99 99
100 worker._memory_max_usage = _memory_max_usage
101 worker._memory_usage_check_interval = _memory_usage_check_interval
102 worker._memory_usage_recovery_threshold = _memory_usage_recovery_threshold
100 worker._memory_max_usage = int(os.environ.get('RC_GUNICORN_MEMORY_MAX_USAGE', '')
101 or _memory_max_usage)
102 worker._memory_usage_check_interval = int(os.environ.get('RC_GUNICORN_MEMORY_USAGE_CHECK_INTERVAL', '')
103 or _memory_usage_check_interval)
104 worker._memory_usage_recovery_threshold = float(os.environ.get('RC_GUNICORN_MEMORY_USAGE_RECOVERY_THRESHOLD', '')
105 or _memory_usage_recovery_threshold)
103 106
104 107 # register memory last check time, with some random offset so we don't recycle all
105 108 # at once
106 109 worker._last_memory_check_time = _time_with_offset(_memory_usage_check_interval)
107 110
108 111 if _memory_max_usage:
109 112 server.log.info("[%-10s] WORKER spawned with max memory set at %s", worker.pid,
110 113 _format_data_size(_memory_max_usage))
111 114 else:
112 115 server.log.info("[%-10s] WORKER spawned", worker.pid)
113 116
114 117
115 118 def pre_exec(server):
116 119 server.log.info("Forked child, re-executing.")
117 120
118 121
119 122 def on_starting(server):
120 123 server_lbl = '{} {}'.format(server.proc_name, server.address)
121 124 server.log.info("Server %s is starting.", server_lbl)
122 125
123 126
124 127 def when_ready(server):
125 128 server.log.info("Server %s is ready. Spawning workers", server)
126 129
127 130
128 131 def on_reload(server):
129 132 pass
130 133
131 134
132 135 def _format_data_size(size, unit="B", precision=1, binary=True):
133 136 """Format a number using SI units (kilo, mega, etc.).
134 137
135 138 ``size``: The number as a float or int.
136 139
137 140 ``unit``: The unit name in plural form. Examples: "bytes", "B".
138 141
139 142 ``precision``: How many digits to the right of the decimal point. Default
140 143 is 1. 0 suppresses the decimal point.
141 144
142 145 ``binary``: If false, use base-10 decimal prefixes (kilo = K = 1000).
143 146 If true, use base-2 binary prefixes (kibi = Ki = 1024).
144 147
145 148 ``full_name``: If false (default), use the prefix abbreviation ("k" or
146 149 "Ki"). If true, use the full prefix ("kilo" or "kibi"). If false,
147 150 use abbreviation ("k" or "Ki").
148 151
149 152 """
150 153
151 154 if not binary:
152 155 base = 1000
153 156 multiples = ('', 'k', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y')
154 157 else:
155 158 base = 1024
156 159 multiples = ('', 'Ki', 'Mi', 'Gi', 'Ti', 'Pi', 'Ei', 'Zi', 'Yi')
157 160
158 161 sign = ""
159 162 if size > 0:
160 163 m = int(math.log(size, base))
161 164 elif size < 0:
162 165 sign = "-"
163 166 size = -size
164 167 m = int(math.log(size, base))
165 168 else:
166 169 m = 0
167 170 if m > 8:
168 171 m = 8
169 172
170 173 if m == 0:
171 174 precision = '%.0f'
172 175 else:
173 176 precision = '%%.%df' % precision
174 177
175 178 size = precision % (size / math.pow(base, m))
176 179
177 180 return '%s%s %s%s' % (sign, size.strip(), multiples[m], unit)
178 181
179 182
180 183 def _check_memory_usage(worker):
181 184 memory_max_usage = worker._memory_max_usage
182 185 if not memory_max_usage:
183 186 return
184 187
185 188 memory_usage_check_interval = worker._memory_usage_check_interval
186 189 memory_usage_recovery_threshold = memory_max_usage * worker._memory_usage_recovery_threshold
187 190
188 191 elapsed = time.time() - worker._last_memory_check_time
189 192 if elapsed > memory_usage_check_interval:
190 193 mem_usage = _get_process_rss()
191 194 if mem_usage and mem_usage > memory_max_usage:
192 195 worker.log.info(
193 196 "memory usage %s > %s, forcing gc",
194 197 _format_data_size(mem_usage), _format_data_size(memory_max_usage))
195 198 # Try to clean it up by forcing a full collection.
196 199 gc.collect()
197 200 mem_usage = _get_process_rss()
198 201 if mem_usage > memory_usage_recovery_threshold:
199 202 # Didn't clean up enough, we'll have to terminate.
200 203 worker.log.warning(
201 204 "memory usage %s > %s after gc, quitting",
202 205 _format_data_size(mem_usage), _format_data_size(memory_max_usage))
203 206 # This will cause worker to auto-restart itself
204 207 worker.alive = False
205 208 worker._last_memory_check_time = time.time()
206 209
207 210
208 211 def worker_int(worker):
209 212 worker.log.info("[%-10s] worker received INT or QUIT signal", worker.pid)
210 213
211 214 # get traceback info, on worker crash
212 215 id2name = dict([(th.ident, th.name) for th in threading.enumerate()])
213 216 code = []
214 217 for thread_id, stack in sys._current_frames().items():
215 218 code.append(
216 219 "\n# Thread: %s(%d)" % (id2name.get(thread_id, ""), thread_id))
217 220 for fname, lineno, name, line in traceback.extract_stack(stack):
218 221 code.append('File: "%s", line %d, in %s' % (fname, lineno, name))
219 222 if line:
220 223 code.append(" %s" % (line.strip()))
221 224 worker.log.debug("\n".join(code))
222 225
223 226
224 227 def worker_abort(worker):
225 228 worker.log.info("[%-10s] worker received SIGABRT signal", worker.pid)
226 229
227 230
228 231 def worker_exit(server, worker):
229 232 worker.log.info("[%-10s] worker exit", worker.pid)
230 233
231 234
232 235 def child_exit(server, worker):
233 236 worker.log.info("[%-10s] worker child exit", worker.pid)
234 237
235 238
236 239 def pre_request(worker, req):
237 240 worker.start_time = time.time()
238 241 worker.log.debug(
239 242 "GNCRN PRE WORKER [cnt:%s]: %s %s", worker.nr, req.method, req.path)
240 243
241 244
242 245 def post_request(worker, req, environ, resp):
243 246 total_time = time.time() - worker.start_time
244 247 # Gunicorn sometimes has problems with reading the status_code
245 248 status_code = getattr(resp, 'status_code', '')
246 249 worker.log.debug(
247 250 "GNCRN POST WORKER [cnt:%s]: %s %s resp: %s, Load Time: %.4fs",
248 251 worker.nr, req.method, req.path, status_code, total_time)
249 252 _check_memory_usage(worker)
250 253
251 254
252 255 class RhodeCodeLogger(Logger):
253 256 """
254 257 Custom Logger that allows some customization that gunicorn doesn't allow
255 258 """
256 259
257 260 datefmt = r"%Y-%m-%d %H:%M:%S"
258 261
259 262 def __init__(self, cfg):
260 263 Logger.__init__(self, cfg)
261 264
262 265 def now(self):
263 266 """ return date in RhodeCode Log format """
264 267 now = time.time()
265 268 msecs = int((now - long(now)) * 1000)
266 269 return time.strftime(self.datefmt, time.localtime(now)) + '.{0:03d}'.format(msecs)
267 270
268 271
269 272 logger_class = RhodeCodeLogger
@@ -1,219 +1,234 b''
1 1 ## -*- coding: utf-8 -*-
2 2
3 3 ; #################################
4 4 ; RHODECODE VCSSERVER CONFIGURATION
5 5 ; #################################
6 6
7 7 [server:main]
8 8 ; COMMON HOST/IP CONFIG
9 9 host = 127.0.0.1
10 10 port = 9900
11 11
12 12
13 13 ; ###########################
14 14 ; GUNICORN APPLICATION SERVER
15 15 ; ###########################
16 16
17 ; run with gunicorn --log-config rhodecode.ini --paste rhodecode.ini
17 ; run with gunicorn --paste rhodecode.ini
18 18
19 19 ; Module to use, this setting shouldn't be changed
20 20 use = egg:gunicorn#main
21 21
22 22 ; Sets the number of process workers. More workers means more concurrent connections
23 23 ; RhodeCode can handle at the same time. Each additional worker also it increases
24 24 ; memory usage as each has it's own set of caches.
25 25 ; Recommended value is (2 * NUMBER_OF_CPUS + 1), eg 2CPU = 5 workers, but no more
26 26 ; than 8-10 unless for really big deployments .e.g 700-1000 users.
27 27 ; `instance_id = *` must be set in the [app:main] section below (which is the default)
28 28 ; when using more than 1 worker.
29 29 workers = 2
30 30
31 31 ; Gunicorn access log level
32 32 loglevel = info
33 33
34 34 ; Process name visible in process list
35 35 proc_name = rhodecode_vcsserver
36 36
37 37 ; Type of worker class, one of `sync`, `gevent`
38 38 ; currently `sync` is the only option allowed.
39 39 worker_class = sync
40 40
41 41 ; The maximum number of simultaneous clients. Valid only for gevent
42 42 worker_connections = 10
43 43
44 44 ; Max number of requests that worker will handle before being gracefully restarted.
45 45 ; Prevents memory leaks, jitter adds variability so not all workers are restarted at once.
46 46 max_requests = 1000
47 47 max_requests_jitter = 30
48 48
49 49 ; Amount of time a worker can spend with handling a request before it
50 50 ; gets killed and restarted. By default set to 21600 (6hrs)
51 51 ; Examples: 1800 (30min), 3600 (1hr), 7200 (2hr), 43200 (12h)
52 52 timeout = 21600
53 53
54 54 ; The maximum size of HTTP request line in bytes.
55 55 ; 0 for unlimited
56 56 limit_request_line = 0
57 57
58 58 ; Limit the number of HTTP headers fields in a request.
59 59 ; By default this value is 100 and can't be larger than 32768.
60 60 limit_request_fields = 32768
61 61
62 62 ; Limit the allowed size of an HTTP request header field.
63 63 ; Value is a positive number or 0.
64 64 ; Setting it to 0 will allow unlimited header field sizes.
65 65 limit_request_field_size = 0
66 66
67 67 ; Timeout for graceful workers restart.
68 68 ; After receiving a restart signal, workers have this much time to finish
69 69 ; serving requests. Workers still alive after the timeout (starting from the
70 70 ; receipt of the restart signal) are force killed.
71 71 ; Examples: 1800 (30min), 3600 (1hr), 7200 (2hr), 43200 (12h)
72 graceful_timeout = 3600
72 graceful_timeout = 21600
73 73
74 74 # The number of seconds to wait for requests on a Keep-Alive connection.
75 75 # Generally set in the 1-5 seconds range.
76 76 keepalive = 2
77 77
78 78 ; Maximum memory usage that each worker can use before it will receive a
79 79 ; graceful restart signal 0 = memory monitoring is disabled
80 80 ; Examples: 268435456 (256MB), 536870912 (512MB)
81 81 ; 1073741824 (1GB), 2147483648 (2GB), 4294967296 (4GB)
82 82 memory_max_usage = 0
83 83
84 84 ; How often in seconds to check for memory usage for each gunicorn worker
85 85 memory_usage_check_interval = 60
86 86
87 87 ; Threshold value for which we don't recycle worker if GarbageCollection
88 88 ; frees up enough resources. Before each restart we try to run GC on worker
89 89 ; in case we get enough free memory after that, restart will not happen.
90 90 memory_usage_recovery_threshold = 0.8
91 91
92 92
93 93 [app:main]
94 94 ; The %(here)s variable will be replaced with the absolute path of parent directory
95 95 ; of this file
96 ; Each option in the app:main can be override by an environmental variable
97 ;
98 ;To override an option:
99 ;
100 ;RC_<KeyName>
101 ;Everything should be uppercase, . and - should be replaced by _.
102 ;For example, if you have these configuration settings:
103 ;rc_cache.repo_object.backend = foo
104 ;can be overridden by
105 ;export RC_CACHE_REPO_OBJECT_BACKEND=foo
106
96 107 use = egg:rhodecode-vcsserver
97 108
98 109 ; Pyramid default locales, we need this to be set
99 pyramid.default_locale_name = en
110 #pyramid.default_locale_name = en
100 111
101 112 ; default locale used by VCS systems
102 locale = en_US.UTF-8
113 #locale = en_US.UTF-8
103 114
104 115 ; path to binaries for vcsserver, it should be set by the installer
105 ; at installation time, e.g /home/user/vcsserver-1/profile/bin
116 ; at installation time, e.g /home/user/.rccontrol/vcsserver-1/profile/bin
106 117 ; it can also be a path to nix-build output in case of development
107 118 core.binary_dir = ""
108 119
109 120 ; Custom exception store path, defaults to TMPDIR
110 121 ; This is used to store exception from RhodeCode in shared directory
111 122 #exception_tracker.store_path =
112 123
113 124 ; #############
114 125 ; DOGPILE CACHE
115 126 ; #############
116 127
117 128 ; Default cache dir for caches. Putting this into a ramdisk can boost performance.
118 129 ; eg. /tmpfs/data_ramdisk, however this directory might require large amount of space
119 cache_dir = %(here)s/data
130 #cache_dir = %(here)s/data
120 131
121 132 ; ***************************************
122 133 ; `repo_object` cache, default file based
123 134 ; ***************************************
124 135
125 136 ; `repo_object` cache settings for vcs methods for repositories
126 rc_cache.repo_object.backend = dogpile.cache.rc.file_namespace
137 #rc_cache.repo_object.backend = dogpile.cache.rc.file_namespace
127 138
128 139 ; cache auto-expires after N seconds
129 140 ; Examples: 86400 (1Day), 604800 (7Days), 1209600 (14Days), 2592000 (30days), 7776000 (90Days)
130 rc_cache.repo_object.expiration_time = 2592000
141 #rc_cache.repo_object.expiration_time = 2592000
131 142
132 143 ; file cache store path. Defaults to `cache_dir =` value or tempdir if both values are not set
133 #rc_cache.repo_object.arguments.filename = /tmp/vcsserver_cache.db
144 #rc_cache.repo_object.arguments.filename = /tmp/vcsserver_cache_repo_object.db
134 145
135 146 ; ***********************************************************
136 147 ; `repo_object` cache with redis backend
137 148 ; recommended for larger instance, and for better performance
138 149 ; ***********************************************************
139 150
140 151 ; `repo_object` cache settings for vcs methods for repositories
141 152 #rc_cache.repo_object.backend = dogpile.cache.rc.redis_msgpack
142 153
143 154 ; cache auto-expires after N seconds
144 155 ; Examples: 86400 (1Day), 604800 (7Days), 1209600 (14Days), 2592000 (30days), 7776000 (90Days)
145 156 #rc_cache.repo_object.expiration_time = 2592000
146 157
147 158 ; redis_expiration_time needs to be greater then expiration_time
148 159 #rc_cache.repo_object.arguments.redis_expiration_time = 3592000
149 160
150 161 #rc_cache.repo_object.arguments.host = localhost
151 162 #rc_cache.repo_object.arguments.port = 6379
152 163 #rc_cache.repo_object.arguments.db = 5
153 164 #rc_cache.repo_object.arguments.socket_timeout = 30
154 165 ; more Redis options: https://dogpilecache.sqlalchemy.org/en/latest/api.html#redis-backends
155 166 #rc_cache.repo_object.arguments.distributed_lock = true
156 167
157 168 ; auto-renew lock to prevent stale locks, slower but safer. Use only if problems happen
158 169 #rc_cache.repo_object.arguments.lock_auto_renewal = true
159 170
160 171 ; Statsd client config, this is used to send metrics to statsd
161 172 ; We recommend setting statsd_exported and scrape them using Promethues
162 173 #statsd.enabled = false
163 174 #statsd.statsd_host = 0.0.0.0
164 175 #statsd.statsd_port = 8125
165 176 #statsd.statsd_prefix =
166 177 #statsd.statsd_ipv6 = false
167 178
179 ; configure logging automatically at server startup set to false
180 ; to use the below custom logging config.
181 #logging.autoconfigure = true
182
183 ; specify your own custom logging config file to configure logging
184 #logging.logging_conf_file = /path/to/custom_logging.ini
185
168 186 ; #####################
169 187 ; LOGGING CONFIGURATION
170 188 ; #####################
171 [loggers]
172 keys = root, vcsserver
189 #[loggers]
190 #keys = root, vcsserver
173 191
174 [handlers]
175 keys = console
192 #[handlers]
193 #keys = console
176 194
177 [formatters]
178 keys = generic
195 #[formatters]
196 #keys = generic
179 197
180 198 ; #######
181 199 ; LOGGERS
182 200 ; #######
183 [logger_root]
184 level = NOTSET
185 handlers = console
201 #[logger_root]
202 #level = NOTSET
203 #handlers = console
186 204
187 [logger_vcsserver]
188 level = DEBUG
189 handlers =
190 qualname = vcsserver
191 propagate = 1
192
205 #[logger_vcsserver]
206 #level = INFO
207 #handlers =
208 #qualname = vcsserver
209 #propagate = 1
193 210
194 211 ; ########
195 212 ; HANDLERS
196 213 ; ########
197 214
198 [handler_console]
199 class = StreamHandler
200 args = (sys.stderr, )
201 level = INFO
202 formatter = generic
215 #[handler_console]
216 #class = StreamHandler
217 #args = (sys.stderr, )
218 #level = INFO
203 219 ; To enable JSON formatted logs replace generic with json
204 220 ; This allows sending properly formatted logs to grafana loki or elasticsearch
205 221 #formatter = json
206
222 #formatter = generic
207 223
208 224 ; ##########
209 225 ; FORMATTERS
210 226 ; ##########
211 227
212 [formatter_generic]
213 format = %(asctime)s.%(msecs)03d [%(process)d] %(levelname)-5.5s [%(name)s] %(message)s
214 datefmt = %Y-%m-%d %H:%M:%S
228 #[formatter_generic]
229 #format = %(asctime)s.%(msecs)03d [%(process)d] %(levelname)-5.5s [%(name)s] %(message)s
230 #datefmt = %Y-%m-%d %H:%M:%S
215 231
216 [formatter_json]
217 format = %(timestamp)s %(levelname)s %(name)s %(message)s %(req_id)s
218 class = vcsserver.lib._vendor.jsonlogger.JsonFormatter
219
232 #[formatter_json]
233 #format = %(timestamp)s %(levelname)s %(name)s %(message)s %(req_id)s
234 #class = vcsserver.lib._vendor.jsonlogger.JsonFormatter
@@ -1,720 +1,750 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 import time
24 25 import wsgiref.util
25 26 import traceback
26 27 import tempfile
27 28 import psutil
29
28 30 from itertools import chain
29 31 from cStringIO import StringIO
30 32
31 33 import simplejson as json
32 34 import msgpack
33 35 from pyramid.config import Configurator
34 from pyramid.settings import asbool, aslist
35 36 from pyramid.wsgi import wsgiapp
36 37 from pyramid.compat import configparser
37 38 from pyramid.response import Response
38
39 from vcsserver.config.settings_maker import SettingsMaker
39 40 from vcsserver.utils import safe_int
40 41 from vcsserver.lib.statsd_client import StatsdClient
41 42
42 43 log = logging.getLogger(__name__)
43 44
44 45 # due to Mercurial/glibc2.27 problems we need to detect if locale settings are
45 46 # causing problems and "fix" it in case they do and fallback to LC_ALL = C
46 47
47 48 try:
48 49 locale.setlocale(locale.LC_ALL, '')
49 50 except locale.Error as e:
50 51 log.error(
51 52 'LOCALE ERROR: failed to set LC_ALL, fallback to LC_ALL=C, org error: %s', e)
52 53 os.environ['LC_ALL'] = 'C'
53 54
55
54 56 import vcsserver
55 57 from vcsserver import remote_wsgi, scm_app, settings, hgpatches
56 58 from vcsserver.git_lfs.app import GIT_LFS_CONTENT_TYPE, GIT_LFS_PROTO_PAT
57 59 from vcsserver.echo_stub import remote_wsgi as remote_wsgi_stub
58 60 from vcsserver.echo_stub.echo_app import EchoApp
59 61 from vcsserver.exceptions import HTTPRepoLocked, HTTPRepoBranchProtected
60 62 from vcsserver.lib.exc_tracking import store_exception
61 63 from vcsserver.server import VcsServer
62 64
63 65 try:
64 66 from vcsserver.git import GitFactory, GitRemote
65 67 except ImportError:
66 68 GitFactory = None
67 69 GitRemote = None
68 70
69 71 try:
70 72 from vcsserver.hg import MercurialFactory, HgRemote
71 73 except ImportError:
72 74 MercurialFactory = None
73 75 HgRemote = None
74 76
75 77 try:
76 78 from vcsserver.svn import SubversionFactory, SvnRemote
77 79 except ImportError:
78 80 SubversionFactory = None
79 81 SvnRemote = None
80 82
81 83
82 84 def _is_request_chunked(environ):
83 85 stream = environ.get('HTTP_TRANSFER_ENCODING', '') == 'chunked'
84 86 return stream
85 87
86 88
87 def _int_setting(settings, name, default):
88 settings[name] = int(settings.get(name, default))
89 return settings[name]
90
91
92 def _bool_setting(settings, name, default):
93 input_val = settings.get(name, default)
94 if isinstance(input_val, unicode):
95 input_val = input_val.encode('utf8')
96 settings[name] = asbool(input_val)
97 return settings[name]
98
99
100 def _list_setting(settings, name, default):
101 raw_value = settings.get(name, default)
102
103 # Otherwise we assume it uses pyramids space/newline separation.
104 settings[name] = aslist(raw_value)
105 return settings[name]
106
107
108 def _string_setting(settings, name, default, lower=True, default_when_empty=False):
109 value = settings.get(name, default)
110
111 if default_when_empty and not value:
112 # use default value when value is empty
113 value = default
114
115 if lower:
116 value = value.lower()
117 settings[name] = value
118 return settings[name]
119
120
121 89 def log_max_fd():
122 90 try:
123 91 maxfd = psutil.Process().rlimit(psutil.RLIMIT_NOFILE)[1]
124 92 log.info('Max file descriptors value: %s', maxfd)
125 93 except Exception:
126 94 pass
127 95
128 96
129 97 class VCS(object):
130 98 def __init__(self, locale_conf=None, cache_config=None):
131 99 self.locale = locale_conf
132 100 self.cache_config = cache_config
133 101 self._configure_locale()
134 102
135 103 log_max_fd()
136 104
137 105 if GitFactory and GitRemote:
138 106 git_factory = GitFactory()
139 107 self._git_remote = GitRemote(git_factory)
140 108 else:
141 109 log.info("Git client import failed")
142 110
143 111 if MercurialFactory and HgRemote:
144 112 hg_factory = MercurialFactory()
145 113 self._hg_remote = HgRemote(hg_factory)
146 114 else:
147 115 log.info("Mercurial client import failed")
148 116
149 117 if SubversionFactory and SvnRemote:
150 118 svn_factory = SubversionFactory()
151 119
152 120 # hg factory is used for svn url validation
153 121 hg_factory = MercurialFactory()
154 122 self._svn_remote = SvnRemote(svn_factory, hg_factory=hg_factory)
155 123 else:
156 124 log.warning("Subversion client import failed")
157 125
158 126 self._vcsserver = VcsServer()
159 127
160 128 def _configure_locale(self):
161 129 if self.locale:
162 130 log.info('Settings locale: `LC_ALL` to %s', self.locale)
163 131 else:
164 132 log.info(
165 133 'Configuring locale subsystem based on environment variables')
166 134 try:
167 135 # If self.locale is the empty string, then the locale
168 136 # module will use the environment variables. See the
169 137 # documentation of the package `locale`.
170 138 locale.setlocale(locale.LC_ALL, self.locale)
171 139
172 140 language_code, encoding = locale.getlocale()
173 141 log.info(
174 142 'Locale set to language code "%s" with encoding "%s".',
175 143 language_code, encoding)
176 144 except locale.Error:
177 145 log.exception(
178 146 'Cannot set locale, not configuring the locale system')
179 147
180 148
181 149 class WsgiProxy(object):
182 150 def __init__(self, wsgi):
183 151 self.wsgi = wsgi
184 152
185 153 def __call__(self, environ, start_response):
186 154 input_data = environ['wsgi.input'].read()
187 155 input_data = msgpack.unpackb(input_data)
188 156
189 157 error = None
190 158 try:
191 159 data, status, headers = self.wsgi.handle(
192 160 input_data['environment'], input_data['input_data'],
193 161 *input_data['args'], **input_data['kwargs'])
194 162 except Exception as e:
195 163 data, status, headers = [], None, None
196 164 error = {
197 165 'message': str(e),
198 166 '_vcs_kind': getattr(e, '_vcs_kind', None)
199 167 }
200 168
201 169 start_response(200, {})
202 170 return self._iterator(error, status, headers, data)
203 171
204 172 def _iterator(self, error, status, headers, data):
205 173 initial_data = [
206 174 error,
207 175 status,
208 176 headers,
209 177 ]
210 178
211 179 for d in chain(initial_data, data):
212 180 yield msgpack.packb(d)
213 181
214 182
215 183 def not_found(request):
216 184 return {'status': '404 NOT FOUND'}
217 185
218 186
219 187 class VCSViewPredicate(object):
220 188 def __init__(self, val, config):
221 189 self.remotes = val
222 190
223 191 def text(self):
224 192 return 'vcs view method = %s' % (self.remotes.keys(),)
225 193
226 194 phash = text
227 195
228 196 def __call__(self, context, request):
229 197 """
230 198 View predicate that returns true if given backend is supported by
231 199 defined remotes.
232 200 """
233 201 backend = request.matchdict.get('backend')
234 202 return backend in self.remotes
235 203
236 204
237 205 class HTTPApplication(object):
238 206 ALLOWED_EXCEPTIONS = ('KeyError', 'URLError')
239 207
240 208 remote_wsgi = remote_wsgi
241 209 _use_echo_app = False
242 210
243 211 def __init__(self, settings=None, global_config=None):
244 self._sanitize_settings_and_apply_defaults(settings)
245 212
246 213 self.config = Configurator(settings=settings)
247 214 # Init our statsd at very start
248 215 self.config.registry.statsd = StatsdClient.statsd
249 216
250 217 self.global_config = global_config
251 218 self.config.include('vcsserver.lib.rc_cache')
252 219
253 220 settings_locale = settings.get('locale', '') or 'en_US.UTF-8'
254 221 vcs = VCS(locale_conf=settings_locale, cache_config=settings)
255 222 self._remotes = {
256 223 'hg': vcs._hg_remote,
257 224 'git': vcs._git_remote,
258 225 'svn': vcs._svn_remote,
259 226 'server': vcs._vcsserver,
260 227 }
261 228 if settings.get('dev.use_echo_app', 'false').lower() == 'true':
262 229 self._use_echo_app = True
263 230 log.warning("Using EchoApp for VCS operations.")
264 231 self.remote_wsgi = remote_wsgi_stub
265 232
266 233 self._configure_settings(global_config, settings)
267 234
268 235 self._configure()
269 236
270 237 def _configure_settings(self, global_config, app_settings):
271 238 """
272 239 Configure the settings module.
273 240 """
274 241 settings_merged = global_config.copy()
275 242 settings_merged.update(app_settings)
276 243
277 244 git_path = app_settings.get('git_path', None)
278 245 if git_path:
279 246 settings.GIT_EXECUTABLE = git_path
280 247 binary_dir = app_settings.get('core.binary_dir', None)
281 248 if binary_dir:
282 249 settings.BINARY_DIR = binary_dir
283 250
284 251 # Store the settings to make them available to other modules.
285 252 vcsserver.PYRAMID_SETTINGS = settings_merged
286 253 vcsserver.CONFIG = settings_merged
287 254
288 def _sanitize_settings_and_apply_defaults(self, settings):
289 temp_store = tempfile.gettempdir()
290 default_cache_dir = os.path.join(temp_store, 'rc_cache')
291
292 # save default, cache dir, and use it for all backends later.
293 default_cache_dir = _string_setting(
294 settings,
295 'cache_dir',
296 default_cache_dir, lower=False, default_when_empty=True)
297
298 # ensure we have our dir created
299 if not os.path.isdir(default_cache_dir):
300 os.makedirs(default_cache_dir, mode=0o755)
301
302 # exception store cache
303 _string_setting(
304 settings,
305 'exception_tracker.store_path',
306 temp_store, lower=False, default_when_empty=True)
307
308 # repo_object cache
309 _string_setting(
310 settings,
311 'rc_cache.repo_object.backend',
312 'dogpile.cache.rc.file_namespace', lower=False)
313 _int_setting(
314 settings,
315 'rc_cache.repo_object.expiration_time',
316 30 * 24 * 60 * 60)
317 _string_setting(
318 settings,
319 'rc_cache.repo_object.arguments.filename',
320 os.path.join(default_cache_dir, 'vcsserver_cache_1'), lower=False)
321
322 255 def _configure(self):
323 256 self.config.add_renderer(name='msgpack', factory=self._msgpack_renderer_factory)
324 257
325 258 self.config.add_route('service', '/_service')
326 259 self.config.add_route('status', '/status')
327 260 self.config.add_route('hg_proxy', '/proxy/hg')
328 261 self.config.add_route('git_proxy', '/proxy/git')
329 262
330 263 # rpc methods
331 264 self.config.add_route('vcs', '/{backend}')
332 265
333 266 # streaming rpc remote methods
334 267 self.config.add_route('vcs_stream', '/{backend}/stream')
335 268
336 269 # vcs operations clone/push as streaming
337 270 self.config.add_route('stream_git', '/stream/git/*repo_name')
338 271 self.config.add_route('stream_hg', '/stream/hg/*repo_name')
339 272
340 273 self.config.add_view(self.status_view, route_name='status', renderer='json')
341 274 self.config.add_view(self.service_view, route_name='service', renderer='msgpack')
342 275
343 276 self.config.add_view(self.hg_proxy(), route_name='hg_proxy')
344 277 self.config.add_view(self.git_proxy(), route_name='git_proxy')
345 278 self.config.add_view(self.vcs_view, route_name='vcs', renderer='msgpack',
346 279 vcs_view=self._remotes)
347 280 self.config.add_view(self.vcs_stream_view, route_name='vcs_stream',
348 281 vcs_view=self._remotes)
349 282
350 283 self.config.add_view(self.hg_stream(), route_name='stream_hg')
351 284 self.config.add_view(self.git_stream(), route_name='stream_git')
352 285
353 286 self.config.add_view_predicate('vcs_view', VCSViewPredicate)
354 287
355 288 self.config.add_notfound_view(not_found, renderer='json')
356 289
357 290 self.config.add_view(self.handle_vcs_exception, context=Exception)
358 291
359 292 self.config.add_tween(
360 293 'vcsserver.tweens.request_wrapper.RequestWrapperTween',
361 294 )
362 295 self.config.add_request_method(
363 296 'vcsserver.lib.request_counter.get_request_counter',
364 297 'request_count')
365 298
366 299 def wsgi_app(self):
367 300 return self.config.make_wsgi_app()
368 301
369 302 def _vcs_view_params(self, request):
370 303 remote = self._remotes[request.matchdict['backend']]
371 304 payload = msgpack.unpackb(request.body, use_list=True)
372 305 method = payload.get('method')
373 306 params = payload['params']
374 307 wire = params.get('wire')
375 308 args = params.get('args')
376 309 kwargs = params.get('kwargs')
377 310 context_uid = None
378 311
379 312 if wire:
380 313 try:
381 314 wire['context'] = context_uid = uuid.UUID(wire['context'])
382 315 except KeyError:
383 316 pass
384 317 args.insert(0, wire)
385 318 repo_state_uid = wire.get('repo_state_uid') if wire else None
386 319
387 320 # NOTE(marcink): trading complexity for slight performance
388 321 if log.isEnabledFor(logging.DEBUG):
389 322 no_args_methods = [
390 323
391 324 ]
392 325 if method in no_args_methods:
393 326 call_args = ''
394 327 else:
395 328 call_args = args[1:]
396 329
397 330 log.debug('Method requested:`%s` with args:%s kwargs:%s context_uid: %s, repo_state_uid:%s',
398 331 method, call_args, kwargs, context_uid, repo_state_uid)
399 332
400 333 statsd = request.registry.statsd
401 334 if statsd:
402 335 statsd.incr(
403 336 'vcsserver_method_total', tags=[
404 337 "method:{}".format(method),
405 338 ])
406 339 return payload, remote, method, args, kwargs
407 340
408 341 def vcs_view(self, request):
409 342
410 343 payload, remote, method, args, kwargs = self._vcs_view_params(request)
411 344 payload_id = payload.get('id')
412 345
413 346 try:
414 347 resp = getattr(remote, method)(*args, **kwargs)
415 348 except Exception as e:
416 349 exc_info = list(sys.exc_info())
417 350 exc_type, exc_value, exc_traceback = exc_info
418 351
419 352 org_exc = getattr(e, '_org_exc', None)
420 353 org_exc_name = None
421 354 org_exc_tb = ''
422 355 if org_exc:
423 356 org_exc_name = org_exc.__class__.__name__
424 357 org_exc_tb = getattr(e, '_org_exc_tb', '')
425 358 # replace our "faked" exception with our org
426 359 exc_info[0] = org_exc.__class__
427 360 exc_info[1] = org_exc
428 361
429 362 should_store_exc = True
430 363 if org_exc:
431 364 def get_exc_fqn(_exc_obj):
432 365 module_name = getattr(org_exc.__class__, '__module__', 'UNKNOWN')
433 366 return module_name + '.' + org_exc_name
434 367
435 368 exc_fqn = get_exc_fqn(org_exc)
436 369
437 370 if exc_fqn in ['mercurial.error.RepoLookupError',
438 371 'vcsserver.exceptions.RefNotFoundException']:
439 372 should_store_exc = False
440 373
441 374 if should_store_exc:
442 375 store_exception(id(exc_info), exc_info, request_path=request.path)
443 376
444 377 tb_info = ''.join(
445 378 traceback.format_exception(exc_type, exc_value, exc_traceback))
446 379
447 380 type_ = e.__class__.__name__
448 381 if type_ not in self.ALLOWED_EXCEPTIONS:
449 382 type_ = None
450 383
451 384 resp = {
452 385 'id': payload_id,
453 386 'error': {
454 387 'message': e.message,
455 388 'traceback': tb_info,
456 389 'org_exc': org_exc_name,
457 390 'org_exc_tb': org_exc_tb,
458 391 'type': type_
459 392 }
460 393 }
461 394
462 395 try:
463 396 resp['error']['_vcs_kind'] = getattr(e, '_vcs_kind', None)
464 397 except AttributeError:
465 398 pass
466 399 else:
467 400 resp = {
468 401 'id': payload_id,
469 402 'result': resp
470 403 }
471 404
472 405 return resp
473 406
474 407 def vcs_stream_view(self, request):
475 408 payload, remote, method, args, kwargs = self._vcs_view_params(request)
476 409 # this method has a stream: marker we remove it here
477 410 method = method.split('stream:')[-1]
478 411 chunk_size = safe_int(payload.get('chunk_size')) or 4096
479 412
480 413 try:
481 414 resp = getattr(remote, method)(*args, **kwargs)
482 415 except Exception as e:
483 416 raise
484 417
485 418 def get_chunked_data(method_resp):
486 419 stream = StringIO(method_resp)
487 420 while 1:
488 421 chunk = stream.read(chunk_size)
489 422 if not chunk:
490 423 break
491 424 yield chunk
492 425
493 426 response = Response(app_iter=get_chunked_data(resp))
494 427 response.content_type = 'application/octet-stream'
495 428
496 429 return response
497 430
498 431 def status_view(self, request):
499 432 import vcsserver
500 433 return {'status': 'OK', 'vcsserver_version': vcsserver.__version__,
501 434 'pid': os.getpid()}
502 435
503 436 def service_view(self, request):
504 437 import vcsserver
505 438
506 439 payload = msgpack.unpackb(request.body, use_list=True)
507 440 server_config, app_config = {}, {}
508 441
509 442 try:
510 443 path = self.global_config['__file__']
511 444 config = configparser.RawConfigParser()
512 445
513 446 config.read(path)
514 447
515 448 if config.has_section('server:main'):
516 449 server_config = dict(config.items('server:main'))
517 450 if config.has_section('app:main'):
518 451 app_config = dict(config.items('app:main'))
519 452
520 453 except Exception:
521 454 log.exception('Failed to read .ini file for display')
522 455
523 456 environ = os.environ.items()
524 457
525 458 resp = {
526 459 'id': payload.get('id'),
527 460 'result': dict(
528 461 version=vcsserver.__version__,
529 462 config=server_config,
530 463 app_config=app_config,
531 464 environ=environ,
532 465 payload=payload,
533 466 )
534 467 }
535 468 return resp
536 469
537 470 def _msgpack_renderer_factory(self, info):
538 471 def _render(value, system):
539 472 request = system.get('request')
540 473 if request is not None:
541 474 response = request.response
542 475 ct = response.content_type
543 476 if ct == response.default_content_type:
544 477 response.content_type = 'application/x-msgpack'
545 478 return msgpack.packb(value)
546 479 return _render
547 480
548 481 def set_env_from_config(self, environ, config):
549 482 dict_conf = {}
550 483 try:
551 484 for elem in config:
552 485 if elem[0] == 'rhodecode':
553 486 dict_conf = json.loads(elem[2])
554 487 break
555 488 except Exception:
556 489 log.exception('Failed to fetch SCM CONFIG')
557 490 return
558 491
559 492 username = dict_conf.get('username')
560 493 if username:
561 494 environ['REMOTE_USER'] = username
562 495 # mercurial specific, some extension api rely on this
563 496 environ['HGUSER'] = username
564 497
565 498 ip = dict_conf.get('ip')
566 499 if ip:
567 500 environ['REMOTE_HOST'] = ip
568 501
569 502 if _is_request_chunked(environ):
570 503 # set the compatibility flag for webob
571 504 environ['wsgi.input_terminated'] = True
572 505
573 506 def hg_proxy(self):
574 507 @wsgiapp
575 508 def _hg_proxy(environ, start_response):
576 509 app = WsgiProxy(self.remote_wsgi.HgRemoteWsgi())
577 510 return app(environ, start_response)
578 511 return _hg_proxy
579 512
580 513 def git_proxy(self):
581 514 @wsgiapp
582 515 def _git_proxy(environ, start_response):
583 516 app = WsgiProxy(self.remote_wsgi.GitRemoteWsgi())
584 517 return app(environ, start_response)
585 518 return _git_proxy
586 519
587 520 def hg_stream(self):
588 521 if self._use_echo_app:
589 522 @wsgiapp
590 523 def _hg_stream(environ, start_response):
591 524 app = EchoApp('fake_path', 'fake_name', None)
592 525 return app(environ, start_response)
593 526 return _hg_stream
594 527 else:
595 528 @wsgiapp
596 529 def _hg_stream(environ, start_response):
597 530 log.debug('http-app: handling hg stream')
598 531 repo_path = environ['HTTP_X_RC_REPO_PATH']
599 532 repo_name = environ['HTTP_X_RC_REPO_NAME']
600 533 packed_config = base64.b64decode(
601 534 environ['HTTP_X_RC_REPO_CONFIG'])
602 535 config = msgpack.unpackb(packed_config)
603 536 app = scm_app.create_hg_wsgi_app(
604 537 repo_path, repo_name, config)
605 538
606 539 # Consistent path information for hgweb
607 540 environ['PATH_INFO'] = environ['HTTP_X_RC_PATH_INFO']
608 541 environ['REPO_NAME'] = repo_name
609 542 self.set_env_from_config(environ, config)
610 543
611 544 log.debug('http-app: starting app handler '
612 545 'with %s and process request', app)
613 546 return app(environ, ResponseFilter(start_response))
614 547 return _hg_stream
615 548
616 549 def git_stream(self):
617 550 if self._use_echo_app:
618 551 @wsgiapp
619 552 def _git_stream(environ, start_response):
620 553 app = EchoApp('fake_path', 'fake_name', None)
621 554 return app(environ, start_response)
622 555 return _git_stream
623 556 else:
624 557 @wsgiapp
625 558 def _git_stream(environ, start_response):
626 559 log.debug('http-app: handling git stream')
627 560 repo_path = environ['HTTP_X_RC_REPO_PATH']
628 561 repo_name = environ['HTTP_X_RC_REPO_NAME']
629 562 packed_config = base64.b64decode(
630 563 environ['HTTP_X_RC_REPO_CONFIG'])
631 564 config = msgpack.unpackb(packed_config)
632 565
633 566 environ['PATH_INFO'] = environ['HTTP_X_RC_PATH_INFO']
634 567 self.set_env_from_config(environ, config)
635 568
636 569 content_type = environ.get('CONTENT_TYPE', '')
637 570
638 571 path = environ['PATH_INFO']
639 572 is_lfs_request = GIT_LFS_CONTENT_TYPE in content_type
640 573 log.debug(
641 574 'LFS: Detecting if request `%s` is LFS server path based '
642 575 'on content type:`%s`, is_lfs:%s',
643 576 path, content_type, is_lfs_request)
644 577
645 578 if not is_lfs_request:
646 579 # fallback detection by path
647 580 if GIT_LFS_PROTO_PAT.match(path):
648 581 is_lfs_request = True
649 582 log.debug(
650 583 'LFS: fallback detection by path of: `%s`, is_lfs:%s',
651 584 path, is_lfs_request)
652 585
653 586 if is_lfs_request:
654 587 app = scm_app.create_git_lfs_wsgi_app(
655 588 repo_path, repo_name, config)
656 589 else:
657 590 app = scm_app.create_git_wsgi_app(
658 591 repo_path, repo_name, config)
659 592
660 593 log.debug('http-app: starting app handler '
661 594 'with %s and process request', app)
662 595
663 596 return app(environ, start_response)
664 597
665 598 return _git_stream
666 599
667 600 def handle_vcs_exception(self, exception, request):
668 601 _vcs_kind = getattr(exception, '_vcs_kind', '')
669 602 if _vcs_kind == 'repo_locked':
670 603 # Get custom repo-locked status code if present.
671 604 status_code = request.headers.get('X-RC-Locked-Status-Code')
672 605 return HTTPRepoLocked(
673 606 title=exception.message, status_code=status_code)
674 607
675 608 elif _vcs_kind == 'repo_branch_protected':
676 609 # Get custom repo-branch-protected status code if present.
677 610 return HTTPRepoBranchProtected(title=exception.message)
678 611
679 612 exc_info = request.exc_info
680 613 store_exception(id(exc_info), exc_info)
681 614
682 615 traceback_info = 'unavailable'
683 616 if request.exc_info:
684 617 exc_type, exc_value, exc_tb = request.exc_info
685 618 traceback_info = ''.join(traceback.format_exception(exc_type, exc_value, exc_tb))
686 619
687 620 log.error(
688 621 'error occurred handling this request for path: %s, \n tb: %s',
689 622 request.path, traceback_info)
690 623
691 624 statsd = request.registry.statsd
692 625 if statsd:
693 626 exc_type = "{}.{}".format(exception.__class__.__module__, exception.__class__.__name__)
694 627 statsd.incr('vcsserver_exception_total',
695 628 tags=["type:{}".format(exc_type)])
696 629 raise exception
697 630
698 631
699 632 class ResponseFilter(object):
700 633
701 634 def __init__(self, start_response):
702 635 self._start_response = start_response
703 636
704 637 def __call__(self, status, response_headers, exc_info=None):
705 638 headers = tuple(
706 639 (h, v) for h, v in response_headers
707 640 if not wsgiref.util.is_hop_by_hop(h))
708 641 return self._start_response(status, headers, exc_info)
709 642
710 643
644 def sanitize_settings_and_apply_defaults(global_config, settings):
645 global_settings_maker = SettingsMaker(global_config)
646 settings_maker = SettingsMaker(settings)
647
648 settings_maker.make_setting(
649 'logging.autoconfigure',
650 default=True,
651 parser='bool')
652
653 logging_conf = os.path.join(os.path.dirname(global_config.get('__file__')), 'logging.ini')
654 settings_maker.enable_logging(logging_conf)
655
656 # Default includes, possible to change as a user
657 pyramid_includes = settings_maker.make_setting('pyramid.includes', [], parser='list:newline')
658 log.debug(
659 "Using the following pyramid.includes: %s",
660 pyramid_includes)
661
662 settings_maker.make_setting('__file__', global_config.get('__file__'))
663
664 settings_maker.make_setting(
665 'pyramid.default_locale_name',
666 default='en',
667 parser='string')
668 settings_maker.make_setting(
669 'locale',
670 default='en_US.UTF-8',
671 parser='string')
672
673 settings_maker.make_setting(
674 'core.binary_dir',
675 default='',
676 parser='string')
677
678 temp_store = tempfile.gettempdir()
679 default_cache_dir = os.path.join(temp_store, 'rc_cache')
680 # save default, cache dir, and use it for all backends later.
681 default_cache_dir = settings_maker.make_setting(
682 'cache_dir',
683 default=default_cache_dir, default_when_empty=True,
684 parser='dir:ensured')
685
686 # exception store cache
687 settings_maker.make_setting(
688 'exception_tracker.store_path',
689 default=os.path.join(default_cache_dir, 'exc_store'), default_when_empty=True,
690 parser='dir:ensured'
691 )
692
693 # repo_object cache defaults
694 settings_maker.make_setting(
695 'rc_cache.repo_object.backend',
696 default='dogpile.cache.rc.file_namespace',
697 parser='string')
698 settings_maker.make_setting(
699 'rc_cache.repo_object.expiration_time',
700 default=30 * 24 * 60 * 60, # 30days
701 parser='int')
702 settings_maker. make_setting(
703 'rc_cache.repo_object.arguments.filename',
704 default=os.path.join(default_cache_dir, 'vcsserver_cache_repo_object.db'),
705 parser='string')
706
707 # statsd
708 settings_maker. make_setting(
709 'statsd.enabled',
710 default=False,
711 parser='bool')
712 settings_maker. make_setting(
713 'statsd.statsd_host',
714 default='statsd-exporter',
715 parser='string')
716 settings_maker. make_setting(
717 'statsd.statsd_port',
718 default=9125,
719 parser='int')
720 settings_maker. make_setting(
721 'statsd.statsd_prefix',
722 default='',
723 parser='string')
724 settings_maker. make_setting(
725 'statsd.statsd_ipv6',
726 default=False,
727 parser='bool')
728
729
711 730 def main(global_config, **settings):
731 start_time = time.time()
732 log.info('Pyramid app config starting')
733
712 734 if MercurialFactory:
713 735 hgpatches.patch_largefiles_capabilities()
714 736 hgpatches.patch_subrepo_type_mapping()
715 737
738 # Fill in and sanitize the defaults & do ENV expansion
739 sanitize_settings_and_apply_defaults(global_config, settings)
740
716 741 # init and bootstrap StatsdClient
717 742 StatsdClient.setup(settings)
718 743
719 app = HTTPApplication(settings=settings, global_config=global_config)
720 return app.wsgi_app()
744 pyramid_app = HTTPApplication(settings=settings, global_config=global_config).wsgi_app()
745 total_time = time.time() - start_time
746 log.info('Pyramid app `%s` created and configured in %.2fs',
747 getattr(pyramid_app, 'func_name', 'pyramid_app'), total_time)
748 return pyramid_app
749
750
General Comments 0
You need to be logged in to leave comments. Login now