##// END OF EJS Templates
system-info: fix reading backends from config....
marcink -
r2314:f4954584 default
parent child Browse files
Show More
@@ -1,727 +1,726 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2017-2017 RhodeCode GmbH
3 # Copyright (C) 2017-2017 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
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
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
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/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21
21
22 import os
22 import os
23 import sys
23 import sys
24 import time
24 import time
25 import platform
25 import platform
26 import pkg_resources
26 import pkg_resources
27 import logging
27 import logging
28 import string
28 import string
29
29
30
30
31 log = logging.getLogger(__name__)
31 log = logging.getLogger(__name__)
32
32
33
33
34 psutil = None
34 psutil = None
35
35
36 try:
36 try:
37 # cygwin cannot have yet psutil support.
37 # cygwin cannot have yet psutil support.
38 import psutil as psutil
38 import psutil as psutil
39 except ImportError:
39 except ImportError:
40 pass
40 pass
41
41
42
42
43 _NA = 'NOT AVAILABLE'
43 _NA = 'NOT AVAILABLE'
44
44
45 STATE_OK = 'ok'
45 STATE_OK = 'ok'
46 STATE_ERR = 'error'
46 STATE_ERR = 'error'
47 STATE_WARN = 'warning'
47 STATE_WARN = 'warning'
48
48
49 STATE_OK_DEFAULT = {'message': '', 'type': STATE_OK}
49 STATE_OK_DEFAULT = {'message': '', 'type': STATE_OK}
50
50
51
51
52 # HELPERS
52 # HELPERS
53 def percentage(part, whole):
53 def percentage(part, whole):
54 whole = float(whole)
54 whole = float(whole)
55 if whole > 0:
55 if whole > 0:
56 return round(100 * float(part) / whole, 1)
56 return round(100 * float(part) / whole, 1)
57 return 0.0
57 return 0.0
58
58
59
59
60 def get_storage_size(storage_path):
60 def get_storage_size(storage_path):
61 sizes = []
61 sizes = []
62 for file_ in os.listdir(storage_path):
62 for file_ in os.listdir(storage_path):
63 storage_file = os.path.join(storage_path, file_)
63 storage_file = os.path.join(storage_path, file_)
64 if os.path.isfile(storage_file):
64 if os.path.isfile(storage_file):
65 try:
65 try:
66 sizes.append(os.path.getsize(storage_file))
66 sizes.append(os.path.getsize(storage_file))
67 except OSError:
67 except OSError:
68 log.exception('Failed to get size of storage file %s',
68 log.exception('Failed to get size of storage file %s',
69 storage_file)
69 storage_file)
70 pass
70 pass
71
71
72 return sum(sizes)
72 return sum(sizes)
73
73
74
74
75 class SysInfoRes(object):
75 class SysInfoRes(object):
76 def __init__(self, value, state=STATE_OK_DEFAULT, human_value=None):
76 def __init__(self, value, state=STATE_OK_DEFAULT, human_value=None):
77 self.value = value
77 self.value = value
78 self.state = state
78 self.state = state
79 self.human_value = human_value or value
79 self.human_value = human_value or value
80
80
81 def __json__(self):
81 def __json__(self):
82 return {
82 return {
83 'value': self.value,
83 'value': self.value,
84 'state': self.state,
84 'state': self.state,
85 'human_value': self.human_value,
85 'human_value': self.human_value,
86 }
86 }
87
87
88 def get_value(self):
88 def get_value(self):
89 return self.__json__()
89 return self.__json__()
90
90
91 def __str__(self):
91 def __str__(self):
92 return '<SysInfoRes({})>'.format(self.__json__())
92 return '<SysInfoRes({})>'.format(self.__json__())
93
93
94
94
95 class SysInfo(object):
95 class SysInfo(object):
96
96
97 def __init__(self, func_name, **kwargs):
97 def __init__(self, func_name, **kwargs):
98 self.func_name = func_name
98 self.func_name = func_name
99 self.value = _NA
99 self.value = _NA
100 self.state = None
100 self.state = None
101 self.kwargs = kwargs or {}
101 self.kwargs = kwargs or {}
102
102
103 def __call__(self):
103 def __call__(self):
104 computed = self.compute(**self.kwargs)
104 computed = self.compute(**self.kwargs)
105 if not isinstance(computed, SysInfoRes):
105 if not isinstance(computed, SysInfoRes):
106 raise ValueError(
106 raise ValueError(
107 'computed value for {} is not instance of '
107 'computed value for {} is not instance of '
108 '{}, got {} instead'.format(
108 '{}, got {} instead'.format(
109 self.func_name, SysInfoRes, type(computed)))
109 self.func_name, SysInfoRes, type(computed)))
110 return computed.__json__()
110 return computed.__json__()
111
111
112 def __str__(self):
112 def __str__(self):
113 return '<SysInfo({})>'.format(self.func_name)
113 return '<SysInfo({})>'.format(self.func_name)
114
114
115 def compute(self, **kwargs):
115 def compute(self, **kwargs):
116 return self.func_name(**kwargs)
116 return self.func_name(**kwargs)
117
117
118
118
119 # SysInfo functions
119 # SysInfo functions
120 def python_info():
120 def python_info():
121 value = dict(version=' '.join(platform._sys_version()),
121 value = dict(version=' '.join(platform._sys_version()),
122 executable=sys.executable)
122 executable=sys.executable)
123 return SysInfoRes(value=value)
123 return SysInfoRes(value=value)
124
124
125
125
126 def py_modules():
126 def py_modules():
127 mods = dict([(p.project_name, p.version)
127 mods = dict([(p.project_name, p.version)
128 for p in pkg_resources.working_set])
128 for p in pkg_resources.working_set])
129 value = sorted(mods.items(), key=lambda k: k[0].lower())
129 value = sorted(mods.items(), key=lambda k: k[0].lower())
130 return SysInfoRes(value=value)
130 return SysInfoRes(value=value)
131
131
132
132
133 def platform_type():
133 def platform_type():
134 from rhodecode.lib.utils import safe_unicode, generate_platform_uuid
134 from rhodecode.lib.utils import safe_unicode, generate_platform_uuid
135
135
136 value = dict(
136 value = dict(
137 name=safe_unicode(platform.platform()),
137 name=safe_unicode(platform.platform()),
138 uuid=generate_platform_uuid()
138 uuid=generate_platform_uuid()
139 )
139 )
140 return SysInfoRes(value=value)
140 return SysInfoRes(value=value)
141
141
142
142
143 def uptime():
143 def uptime():
144 from rhodecode.lib.helpers import age, time_to_datetime
144 from rhodecode.lib.helpers import age, time_to_datetime
145 from rhodecode.translation import TranslationString
145 from rhodecode.translation import TranslationString
146
146
147 value = dict(boot_time=0, uptime=0, text='')
147 value = dict(boot_time=0, uptime=0, text='')
148 state = STATE_OK_DEFAULT
148 state = STATE_OK_DEFAULT
149 if not psutil:
149 if not psutil:
150 return SysInfoRes(value=value, state=state)
150 return SysInfoRes(value=value, state=state)
151
151
152 boot_time = psutil.boot_time()
152 boot_time = psutil.boot_time()
153 value['boot_time'] = boot_time
153 value['boot_time'] = boot_time
154 value['uptime'] = time.time() - boot_time
154 value['uptime'] = time.time() - boot_time
155
155
156 date_or_age = age(time_to_datetime(boot_time))
156 date_or_age = age(time_to_datetime(boot_time))
157 if isinstance(date_or_age, TranslationString):
157 if isinstance(date_or_age, TranslationString):
158 date_or_age = date_or_age.interpolate()
158 date_or_age = date_or_age.interpolate()
159
159
160 human_value = value.copy()
160 human_value = value.copy()
161 human_value['boot_time'] = time_to_datetime(boot_time)
161 human_value['boot_time'] = time_to_datetime(boot_time)
162 human_value['uptime'] = age(time_to_datetime(boot_time), show_suffix=False)
162 human_value['uptime'] = age(time_to_datetime(boot_time), show_suffix=False)
163
163
164 human_value['text'] = u'Server started {}'.format(date_or_age)
164 human_value['text'] = u'Server started {}'.format(date_or_age)
165 return SysInfoRes(value=value, human_value=human_value)
165 return SysInfoRes(value=value, human_value=human_value)
166
166
167
167
168 def memory():
168 def memory():
169 from rhodecode.lib.helpers import format_byte_size_binary
169 from rhodecode.lib.helpers import format_byte_size_binary
170 value = dict(available=0, used=0, used_real=0, cached=0, percent=0,
170 value = dict(available=0, used=0, used_real=0, cached=0, percent=0,
171 percent_used=0, free=0, inactive=0, active=0, shared=0,
171 percent_used=0, free=0, inactive=0, active=0, shared=0,
172 total=0, buffers=0, text='')
172 total=0, buffers=0, text='')
173
173
174 state = STATE_OK_DEFAULT
174 state = STATE_OK_DEFAULT
175 if not psutil:
175 if not psutil:
176 return SysInfoRes(value=value, state=state)
176 return SysInfoRes(value=value, state=state)
177
177
178 value.update(dict(psutil.virtual_memory()._asdict()))
178 value.update(dict(psutil.virtual_memory()._asdict()))
179 value['used_real'] = value['total'] - value['available']
179 value['used_real'] = value['total'] - value['available']
180 value['percent_used'] = psutil._common.usage_percent(
180 value['percent_used'] = psutil._common.usage_percent(
181 value['used_real'], value['total'], 1)
181 value['used_real'], value['total'], 1)
182
182
183 human_value = value.copy()
183 human_value = value.copy()
184 human_value['text'] = '%s/%s, %s%% used' % (
184 human_value['text'] = '%s/%s, %s%% used' % (
185 format_byte_size_binary(value['used_real']),
185 format_byte_size_binary(value['used_real']),
186 format_byte_size_binary(value['total']),
186 format_byte_size_binary(value['total']),
187 value['percent_used'],)
187 value['percent_used'],)
188
188
189 keys = value.keys()[::]
189 keys = value.keys()[::]
190 keys.pop(keys.index('percent'))
190 keys.pop(keys.index('percent'))
191 keys.pop(keys.index('percent_used'))
191 keys.pop(keys.index('percent_used'))
192 keys.pop(keys.index('text'))
192 keys.pop(keys.index('text'))
193 for k in keys:
193 for k in keys:
194 human_value[k] = format_byte_size_binary(value[k])
194 human_value[k] = format_byte_size_binary(value[k])
195
195
196 if state['type'] == STATE_OK and value['percent_used'] > 90:
196 if state['type'] == STATE_OK and value['percent_used'] > 90:
197 msg = 'Critical: your available RAM memory is very low.'
197 msg = 'Critical: your available RAM memory is very low.'
198 state = {'message': msg, 'type': STATE_ERR}
198 state = {'message': msg, 'type': STATE_ERR}
199
199
200 elif state['type'] == STATE_OK and value['percent_used'] > 70:
200 elif state['type'] == STATE_OK and value['percent_used'] > 70:
201 msg = 'Warning: your available RAM memory is running low.'
201 msg = 'Warning: your available RAM memory is running low.'
202 state = {'message': msg, 'type': STATE_WARN}
202 state = {'message': msg, 'type': STATE_WARN}
203
203
204 return SysInfoRes(value=value, state=state, human_value=human_value)
204 return SysInfoRes(value=value, state=state, human_value=human_value)
205
205
206
206
207 def machine_load():
207 def machine_load():
208 value = {'1_min': _NA, '5_min': _NA, '15_min': _NA, 'text': ''}
208 value = {'1_min': _NA, '5_min': _NA, '15_min': _NA, 'text': ''}
209 state = STATE_OK_DEFAULT
209 state = STATE_OK_DEFAULT
210 if not psutil:
210 if not psutil:
211 return SysInfoRes(value=value, state=state)
211 return SysInfoRes(value=value, state=state)
212
212
213 # load averages
213 # load averages
214 if hasattr(psutil.os, 'getloadavg'):
214 if hasattr(psutil.os, 'getloadavg'):
215 value.update(dict(
215 value.update(dict(
216 zip(['1_min', '5_min', '15_min'], psutil.os.getloadavg())))
216 zip(['1_min', '5_min', '15_min'], psutil.os.getloadavg())))
217
217
218 human_value = value.copy()
218 human_value = value.copy()
219 human_value['text'] = '1min: {}, 5min: {}, 15min: {}'.format(
219 human_value['text'] = '1min: {}, 5min: {}, 15min: {}'.format(
220 value['1_min'], value['5_min'], value['15_min'])
220 value['1_min'], value['5_min'], value['15_min'])
221
221
222 if state['type'] == STATE_OK and value['15_min'] > 5:
222 if state['type'] == STATE_OK and value['15_min'] > 5:
223 msg = 'Warning: your machine load is very high.'
223 msg = 'Warning: your machine load is very high.'
224 state = {'message': msg, 'type': STATE_WARN}
224 state = {'message': msg, 'type': STATE_WARN}
225
225
226 return SysInfoRes(value=value, state=state, human_value=human_value)
226 return SysInfoRes(value=value, state=state, human_value=human_value)
227
227
228
228
229 def cpu():
229 def cpu():
230 value = {'cpu': 0, 'cpu_count': 0, 'cpu_usage': []}
230 value = {'cpu': 0, 'cpu_count': 0, 'cpu_usage': []}
231 state = STATE_OK_DEFAULT
231 state = STATE_OK_DEFAULT
232
232
233 if not psutil:
233 if not psutil:
234 return SysInfoRes(value=value, state=state)
234 return SysInfoRes(value=value, state=state)
235
235
236 value['cpu'] = psutil.cpu_percent(0.5)
236 value['cpu'] = psutil.cpu_percent(0.5)
237 value['cpu_usage'] = psutil.cpu_percent(0.5, percpu=True)
237 value['cpu_usage'] = psutil.cpu_percent(0.5, percpu=True)
238 value['cpu_count'] = psutil.cpu_count()
238 value['cpu_count'] = psutil.cpu_count()
239
239
240 human_value = value.copy()
240 human_value = value.copy()
241 human_value['text'] = '{} cores at {} %'.format(
241 human_value['text'] = '{} cores at {} %'.format(
242 value['cpu_count'], value['cpu'])
242 value['cpu_count'], value['cpu'])
243
243
244 return SysInfoRes(value=value, state=state, human_value=human_value)
244 return SysInfoRes(value=value, state=state, human_value=human_value)
245
245
246
246
247 def storage():
247 def storage():
248 from rhodecode.lib.helpers import format_byte_size_binary
248 from rhodecode.lib.helpers import format_byte_size_binary
249 from rhodecode.model.settings import VcsSettingsModel
249 from rhodecode.model.settings import VcsSettingsModel
250 path = VcsSettingsModel().get_repos_location()
250 path = VcsSettingsModel().get_repos_location()
251
251
252 value = dict(percent=0, used=0, total=0, path=path, text='')
252 value = dict(percent=0, used=0, total=0, path=path, text='')
253 state = STATE_OK_DEFAULT
253 state = STATE_OK_DEFAULT
254 if not psutil:
254 if not psutil:
255 return SysInfoRes(value=value, state=state)
255 return SysInfoRes(value=value, state=state)
256
256
257 try:
257 try:
258 value.update(dict(psutil.disk_usage(path)._asdict()))
258 value.update(dict(psutil.disk_usage(path)._asdict()))
259 except Exception as e:
259 except Exception as e:
260 log.exception('Failed to fetch disk info')
260 log.exception('Failed to fetch disk info')
261 state = {'message': str(e), 'type': STATE_ERR}
261 state = {'message': str(e), 'type': STATE_ERR}
262
262
263 human_value = value.copy()
263 human_value = value.copy()
264 human_value['used'] = format_byte_size_binary(value['used'])
264 human_value['used'] = format_byte_size_binary(value['used'])
265 human_value['total'] = format_byte_size_binary(value['total'])
265 human_value['total'] = format_byte_size_binary(value['total'])
266 human_value['text'] = "{}/{}, {}% used".format(
266 human_value['text'] = "{}/{}, {}% used".format(
267 format_byte_size_binary(value['used']),
267 format_byte_size_binary(value['used']),
268 format_byte_size_binary(value['total']),
268 format_byte_size_binary(value['total']),
269 value['percent'])
269 value['percent'])
270
270
271 if state['type'] == STATE_OK and value['percent'] > 90:
271 if state['type'] == STATE_OK and value['percent'] > 90:
272 msg = 'Critical: your disk space is very low.'
272 msg = 'Critical: your disk space is very low.'
273 state = {'message': msg, 'type': STATE_ERR}
273 state = {'message': msg, 'type': STATE_ERR}
274
274
275 elif state['type'] == STATE_OK and value['percent'] > 70:
275 elif state['type'] == STATE_OK and value['percent'] > 70:
276 msg = 'Warning: your disk space is running low.'
276 msg = 'Warning: your disk space is running low.'
277 state = {'message': msg, 'type': STATE_WARN}
277 state = {'message': msg, 'type': STATE_WARN}
278
278
279 return SysInfoRes(value=value, state=state, human_value=human_value)
279 return SysInfoRes(value=value, state=state, human_value=human_value)
280
280
281
281
282 def storage_inodes():
282 def storage_inodes():
283 from rhodecode.model.settings import VcsSettingsModel
283 from rhodecode.model.settings import VcsSettingsModel
284 path = VcsSettingsModel().get_repos_location()
284 path = VcsSettingsModel().get_repos_location()
285
285
286 value = dict(percent=0, free=0, used=0, total=0, path=path, text='')
286 value = dict(percent=0, free=0, used=0, total=0, path=path, text='')
287 state = STATE_OK_DEFAULT
287 state = STATE_OK_DEFAULT
288 if not psutil:
288 if not psutil:
289 return SysInfoRes(value=value, state=state)
289 return SysInfoRes(value=value, state=state)
290
290
291 try:
291 try:
292 i_stat = os.statvfs(path)
292 i_stat = os.statvfs(path)
293 value['free'] = i_stat.f_ffree
293 value['free'] = i_stat.f_ffree
294 value['used'] = i_stat.f_files-i_stat.f_favail
294 value['used'] = i_stat.f_files-i_stat.f_favail
295 value['total'] = i_stat.f_files
295 value['total'] = i_stat.f_files
296 value['percent'] = percentage(value['used'], value['total'])
296 value['percent'] = percentage(value['used'], value['total'])
297 except Exception as e:
297 except Exception as e:
298 log.exception('Failed to fetch disk inodes info')
298 log.exception('Failed to fetch disk inodes info')
299 state = {'message': str(e), 'type': STATE_ERR}
299 state = {'message': str(e), 'type': STATE_ERR}
300
300
301 human_value = value.copy()
301 human_value = value.copy()
302 human_value['text'] = "{}/{}, {}% used".format(
302 human_value['text'] = "{}/{}, {}% used".format(
303 value['used'], value['total'], value['percent'])
303 value['used'], value['total'], value['percent'])
304
304
305 if state['type'] == STATE_OK and value['percent'] > 90:
305 if state['type'] == STATE_OK and value['percent'] > 90:
306 msg = 'Critical: your disk free inodes are very low.'
306 msg = 'Critical: your disk free inodes are very low.'
307 state = {'message': msg, 'type': STATE_ERR}
307 state = {'message': msg, 'type': STATE_ERR}
308
308
309 elif state['type'] == STATE_OK and value['percent'] > 70:
309 elif state['type'] == STATE_OK and value['percent'] > 70:
310 msg = 'Warning: your disk free inodes are running low.'
310 msg = 'Warning: your disk free inodes are running low.'
311 state = {'message': msg, 'type': STATE_WARN}
311 state = {'message': msg, 'type': STATE_WARN}
312
312
313 return SysInfoRes(value=value, state=state, human_value=human_value)
313 return SysInfoRes(value=value, state=state, human_value=human_value)
314
314
315
315
316 def storage_archives():
316 def storage_archives():
317 import rhodecode
317 import rhodecode
318 from rhodecode.lib.utils import safe_str
318 from rhodecode.lib.utils import safe_str
319 from rhodecode.lib.helpers import format_byte_size_binary
319 from rhodecode.lib.helpers import format_byte_size_binary
320
320
321 msg = 'Enable this by setting ' \
321 msg = 'Enable this by setting ' \
322 'archive_cache_dir=/path/to/cache option in the .ini file'
322 'archive_cache_dir=/path/to/cache option in the .ini file'
323 path = safe_str(rhodecode.CONFIG.get('archive_cache_dir', msg))
323 path = safe_str(rhodecode.CONFIG.get('archive_cache_dir', msg))
324
324
325 value = dict(percent=0, used=0, total=0, items=0, path=path, text='')
325 value = dict(percent=0, used=0, total=0, items=0, path=path, text='')
326 state = STATE_OK_DEFAULT
326 state = STATE_OK_DEFAULT
327 try:
327 try:
328 items_count = 0
328 items_count = 0
329 used = 0
329 used = 0
330 for root, dirs, files in os.walk(path):
330 for root, dirs, files in os.walk(path):
331 if root == path:
331 if root == path:
332 items_count = len(files)
332 items_count = len(files)
333
333
334 for f in files:
334 for f in files:
335 try:
335 try:
336 used += os.path.getsize(os.path.join(root, f))
336 used += os.path.getsize(os.path.join(root, f))
337 except OSError:
337 except OSError:
338 pass
338 pass
339 value.update({
339 value.update({
340 'percent': 100,
340 'percent': 100,
341 'used': used,
341 'used': used,
342 'total': used,
342 'total': used,
343 'items': items_count
343 'items': items_count
344 })
344 })
345
345
346 except Exception as e:
346 except Exception as e:
347 log.exception('failed to fetch archive cache storage')
347 log.exception('failed to fetch archive cache storage')
348 state = {'message': str(e), 'type': STATE_ERR}
348 state = {'message': str(e), 'type': STATE_ERR}
349
349
350 human_value = value.copy()
350 human_value = value.copy()
351 human_value['used'] = format_byte_size_binary(value['used'])
351 human_value['used'] = format_byte_size_binary(value['used'])
352 human_value['total'] = format_byte_size_binary(value['total'])
352 human_value['total'] = format_byte_size_binary(value['total'])
353 human_value['text'] = "{} ({} items)".format(
353 human_value['text'] = "{} ({} items)".format(
354 human_value['used'], value['items'])
354 human_value['used'], value['items'])
355
355
356 return SysInfoRes(value=value, state=state, human_value=human_value)
356 return SysInfoRes(value=value, state=state, human_value=human_value)
357
357
358
358
359 def storage_gist():
359 def storage_gist():
360 from rhodecode.model.gist import GIST_STORE_LOC
360 from rhodecode.model.gist import GIST_STORE_LOC
361 from rhodecode.model.settings import VcsSettingsModel
361 from rhodecode.model.settings import VcsSettingsModel
362 from rhodecode.lib.utils import safe_str
362 from rhodecode.lib.utils import safe_str
363 from rhodecode.lib.helpers import format_byte_size_binary
363 from rhodecode.lib.helpers import format_byte_size_binary
364 path = safe_str(os.path.join(
364 path = safe_str(os.path.join(
365 VcsSettingsModel().get_repos_location(), GIST_STORE_LOC))
365 VcsSettingsModel().get_repos_location(), GIST_STORE_LOC))
366
366
367 # gist storage
367 # gist storage
368 value = dict(percent=0, used=0, total=0, items=0, path=path, text='')
368 value = dict(percent=0, used=0, total=0, items=0, path=path, text='')
369 state = STATE_OK_DEFAULT
369 state = STATE_OK_DEFAULT
370
370
371 try:
371 try:
372 items_count = 0
372 items_count = 0
373 used = 0
373 used = 0
374 for root, dirs, files in os.walk(path):
374 for root, dirs, files in os.walk(path):
375 if root == path:
375 if root == path:
376 items_count = len(dirs)
376 items_count = len(dirs)
377
377
378 for f in files:
378 for f in files:
379 try:
379 try:
380 used += os.path.getsize(os.path.join(root, f))
380 used += os.path.getsize(os.path.join(root, f))
381 except OSError:
381 except OSError:
382 pass
382 pass
383 value.update({
383 value.update({
384 'percent': 100,
384 'percent': 100,
385 'used': used,
385 'used': used,
386 'total': used,
386 'total': used,
387 'items': items_count
387 'items': items_count
388 })
388 })
389 except Exception as e:
389 except Exception as e:
390 log.exception('failed to fetch gist storage items')
390 log.exception('failed to fetch gist storage items')
391 state = {'message': str(e), 'type': STATE_ERR}
391 state = {'message': str(e), 'type': STATE_ERR}
392
392
393 human_value = value.copy()
393 human_value = value.copy()
394 human_value['used'] = format_byte_size_binary(value['used'])
394 human_value['used'] = format_byte_size_binary(value['used'])
395 human_value['total'] = format_byte_size_binary(value['total'])
395 human_value['total'] = format_byte_size_binary(value['total'])
396 human_value['text'] = "{} ({} items)".format(
396 human_value['text'] = "{} ({} items)".format(
397 human_value['used'], value['items'])
397 human_value['used'], value['items'])
398
398
399 return SysInfoRes(value=value, state=state, human_value=human_value)
399 return SysInfoRes(value=value, state=state, human_value=human_value)
400
400
401
401
402 def storage_temp():
402 def storage_temp():
403 import tempfile
403 import tempfile
404 from rhodecode.lib.helpers import format_byte_size_binary
404 from rhodecode.lib.helpers import format_byte_size_binary
405
405
406 path = tempfile.gettempdir()
406 path = tempfile.gettempdir()
407 value = dict(percent=0, used=0, total=0, items=0, path=path, text='')
407 value = dict(percent=0, used=0, total=0, items=0, path=path, text='')
408 state = STATE_OK_DEFAULT
408 state = STATE_OK_DEFAULT
409
409
410 if not psutil:
410 if not psutil:
411 return SysInfoRes(value=value, state=state)
411 return SysInfoRes(value=value, state=state)
412
412
413 try:
413 try:
414 value.update(dict(psutil.disk_usage(path)._asdict()))
414 value.update(dict(psutil.disk_usage(path)._asdict()))
415 except Exception as e:
415 except Exception as e:
416 log.exception('Failed to fetch temp dir info')
416 log.exception('Failed to fetch temp dir info')
417 state = {'message': str(e), 'type': STATE_ERR}
417 state = {'message': str(e), 'type': STATE_ERR}
418
418
419 human_value = value.copy()
419 human_value = value.copy()
420 human_value['used'] = format_byte_size_binary(value['used'])
420 human_value['used'] = format_byte_size_binary(value['used'])
421 human_value['total'] = format_byte_size_binary(value['total'])
421 human_value['total'] = format_byte_size_binary(value['total'])
422 human_value['text'] = "{}/{}, {}% used".format(
422 human_value['text'] = "{}/{}, {}% used".format(
423 format_byte_size_binary(value['used']),
423 format_byte_size_binary(value['used']),
424 format_byte_size_binary(value['total']),
424 format_byte_size_binary(value['total']),
425 value['percent'])
425 value['percent'])
426
426
427 return SysInfoRes(value=value, state=state, human_value=human_value)
427 return SysInfoRes(value=value, state=state, human_value=human_value)
428
428
429
429
430 def search_info():
430 def search_info():
431 import rhodecode
431 import rhodecode
432 from rhodecode.lib.index import searcher_from_config
432 from rhodecode.lib.index import searcher_from_config
433
433
434 backend = rhodecode.CONFIG.get('search.module', '')
434 backend = rhodecode.CONFIG.get('search.module', '')
435 location = rhodecode.CONFIG.get('search.location', '')
435 location = rhodecode.CONFIG.get('search.location', '')
436
436
437 try:
437 try:
438 searcher = searcher_from_config(rhodecode.CONFIG)
438 searcher = searcher_from_config(rhodecode.CONFIG)
439 searcher = searcher.__class__.__name__
439 searcher = searcher.__class__.__name__
440 except Exception:
440 except Exception:
441 searcher = None
441 searcher = None
442
442
443 value = dict(
443 value = dict(
444 backend=backend, searcher=searcher, location=location, text='')
444 backend=backend, searcher=searcher, location=location, text='')
445 state = STATE_OK_DEFAULT
445 state = STATE_OK_DEFAULT
446
446
447 human_value = value.copy()
447 human_value = value.copy()
448 human_value['text'] = "backend:`{}`".format(human_value['backend'])
448 human_value['text'] = "backend:`{}`".format(human_value['backend'])
449
449
450 return SysInfoRes(value=value, state=state, human_value=human_value)
450 return SysInfoRes(value=value, state=state, human_value=human_value)
451
451
452
452
453 def git_info():
453 def git_info():
454 from rhodecode.lib.vcs.backends import git
454 from rhodecode.lib.vcs.backends import git
455 state = STATE_OK_DEFAULT
455 state = STATE_OK_DEFAULT
456 value = human_value = ''
456 value = human_value = ''
457 try:
457 try:
458 value = git.discover_git_version(raise_on_exc=True)
458 value = git.discover_git_version(raise_on_exc=True)
459 human_value = 'version reported from VCSServer: {}'.format(value)
459 human_value = 'version reported from VCSServer: {}'.format(value)
460 except Exception as e:
460 except Exception as e:
461 state = {'message': str(e), 'type': STATE_ERR}
461 state = {'message': str(e), 'type': STATE_ERR}
462
462
463 return SysInfoRes(value=value, state=state, human_value=human_value)
463 return SysInfoRes(value=value, state=state, human_value=human_value)
464
464
465
465
466 def hg_info():
466 def hg_info():
467 from rhodecode.lib.vcs.backends import hg
467 from rhodecode.lib.vcs.backends import hg
468 state = STATE_OK_DEFAULT
468 state = STATE_OK_DEFAULT
469 value = human_value = ''
469 value = human_value = ''
470 try:
470 try:
471 value = hg.discover_hg_version(raise_on_exc=True)
471 value = hg.discover_hg_version(raise_on_exc=True)
472 human_value = 'version reported from VCSServer: {}'.format(value)
472 human_value = 'version reported from VCSServer: {}'.format(value)
473 except Exception as e:
473 except Exception as e:
474 state = {'message': str(e), 'type': STATE_ERR}
474 state = {'message': str(e), 'type': STATE_ERR}
475 return SysInfoRes(value=value, state=state, human_value=human_value)
475 return SysInfoRes(value=value, state=state, human_value=human_value)
476
476
477
477
478 def svn_info():
478 def svn_info():
479 from rhodecode.lib.vcs.backends import svn
479 from rhodecode.lib.vcs.backends import svn
480 state = STATE_OK_DEFAULT
480 state = STATE_OK_DEFAULT
481 value = human_value = ''
481 value = human_value = ''
482 try:
482 try:
483 value = svn.discover_svn_version(raise_on_exc=True)
483 value = svn.discover_svn_version(raise_on_exc=True)
484 human_value = 'version reported from VCSServer: {}'.format(value)
484 human_value = 'version reported from VCSServer: {}'.format(value)
485 except Exception as e:
485 except Exception as e:
486 state = {'message': str(e), 'type': STATE_ERR}
486 state = {'message': str(e), 'type': STATE_ERR}
487 return SysInfoRes(value=value, state=state, human_value=human_value)
487 return SysInfoRes(value=value, state=state, human_value=human_value)
488
488
489
489
490 def vcs_backends():
490 def vcs_backends():
491 import rhodecode
491 import rhodecode
492 value = map(
492 value = rhodecode.CONFIG.get('vcs.backends')
493 string.strip, rhodecode.CONFIG.get('vcs.backends', '').split(','))
494 human_value = 'Enabled backends in order: {}'.format(','.join(value))
493 human_value = 'Enabled backends in order: {}'.format(','.join(value))
495 return SysInfoRes(value=value, human_value=human_value)
494 return SysInfoRes(value=value, human_value=human_value)
496
495
497
496
498 def vcs_server():
497 def vcs_server():
499 import rhodecode
498 import rhodecode
500 from rhodecode.lib.vcs.backends import get_vcsserver_service_data
499 from rhodecode.lib.vcs.backends import get_vcsserver_service_data
501
500
502 server_url = rhodecode.CONFIG.get('vcs.server')
501 server_url = rhodecode.CONFIG.get('vcs.server')
503 enabled = rhodecode.CONFIG.get('vcs.server.enable')
502 enabled = rhodecode.CONFIG.get('vcs.server.enable')
504 protocol = rhodecode.CONFIG.get('vcs.server.protocol') or 'http'
503 protocol = rhodecode.CONFIG.get('vcs.server.protocol') or 'http'
505 state = STATE_OK_DEFAULT
504 state = STATE_OK_DEFAULT
506 version = None
505 version = None
507 workers = 0
506 workers = 0
508
507
509 try:
508 try:
510 data = get_vcsserver_service_data()
509 data = get_vcsserver_service_data()
511 if data and 'version' in data:
510 if data and 'version' in data:
512 version = data['version']
511 version = data['version']
513
512
514 if data and 'config' in data:
513 if data and 'config' in data:
515 conf = data['config']
514 conf = data['config']
516 workers = conf.get('workers', 'NOT AVAILABLE')
515 workers = conf.get('workers', 'NOT AVAILABLE')
517
516
518 connection = 'connected'
517 connection = 'connected'
519 except Exception as e:
518 except Exception as e:
520 connection = 'failed'
519 connection = 'failed'
521 state = {'message': str(e), 'type': STATE_ERR}
520 state = {'message': str(e), 'type': STATE_ERR}
522
521
523 value = dict(
522 value = dict(
524 url=server_url,
523 url=server_url,
525 enabled=enabled,
524 enabled=enabled,
526 protocol=protocol,
525 protocol=protocol,
527 connection=connection,
526 connection=connection,
528 version=version,
527 version=version,
529 text='',
528 text='',
530 )
529 )
531
530
532 human_value = value.copy()
531 human_value = value.copy()
533 human_value['text'] = \
532 human_value['text'] = \
534 '{url}@ver:{ver} via {mode} mode[workers:{workers}], connection:{conn}'.format(
533 '{url}@ver:{ver} via {mode} mode[workers:{workers}], connection:{conn}'.format(
535 url=server_url, ver=version, workers=workers, mode=protocol,
534 url=server_url, ver=version, workers=workers, mode=protocol,
536 conn=connection)
535 conn=connection)
537
536
538 return SysInfoRes(value=value, state=state, human_value=human_value)
537 return SysInfoRes(value=value, state=state, human_value=human_value)
539
538
540
539
541 def rhodecode_app_info():
540 def rhodecode_app_info():
542 import rhodecode
541 import rhodecode
543 edition = rhodecode.CONFIG.get('rhodecode.edition')
542 edition = rhodecode.CONFIG.get('rhodecode.edition')
544
543
545 value = dict(
544 value = dict(
546 rhodecode_version=rhodecode.__version__,
545 rhodecode_version=rhodecode.__version__,
547 rhodecode_lib_path=os.path.abspath(rhodecode.__file__),
546 rhodecode_lib_path=os.path.abspath(rhodecode.__file__),
548 text=''
547 text=''
549 )
548 )
550 human_value = value.copy()
549 human_value = value.copy()
551 human_value['text'] = 'RhodeCode {edition}, version {ver}'.format(
550 human_value['text'] = 'RhodeCode {edition}, version {ver}'.format(
552 edition=edition, ver=value['rhodecode_version']
551 edition=edition, ver=value['rhodecode_version']
553 )
552 )
554 return SysInfoRes(value=value, human_value=human_value)
553 return SysInfoRes(value=value, human_value=human_value)
555
554
556
555
557 def rhodecode_config():
556 def rhodecode_config():
558 import rhodecode
557 import rhodecode
559 import ConfigParser as configparser
558 import ConfigParser as configparser
560 path = rhodecode.CONFIG.get('__file__')
559 path = rhodecode.CONFIG.get('__file__')
561 rhodecode_ini_safe = rhodecode.CONFIG.copy()
560 rhodecode_ini_safe = rhodecode.CONFIG.copy()
562
561
563 try:
562 try:
564 config = configparser.ConfigParser()
563 config = configparser.ConfigParser()
565 config.read(path)
564 config.read(path)
566 parsed_ini = config
565 parsed_ini = config
567 if parsed_ini.has_section('server:main'):
566 if parsed_ini.has_section('server:main'):
568 parsed_ini = dict(parsed_ini.items('server:main'))
567 parsed_ini = dict(parsed_ini.items('server:main'))
569 except Exception:
568 except Exception:
570 log.exception('Failed to read .ini file for display')
569 log.exception('Failed to read .ini file for display')
571 parsed_ini = {}
570 parsed_ini = {}
572
571
573 cert_path = os.path.join(
572 cert_path = os.path.join(
574 os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(path)))),
573 os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(path)))),
575 '.rccontrol-profile/etc/ca-bundle.crt')
574 '.rccontrol-profile/etc/ca-bundle.crt')
576
575
577 rhodecode_ini_safe['server:main'] = parsed_ini
576 rhodecode_ini_safe['server:main'] = parsed_ini
578
577
579 blacklist = [
578 blacklist = [
580 'rhodecode_license_key',
579 'rhodecode_license_key',
581 'routes.map',
580 'routes.map',
582 'pylons.h',
581 'pylons.h',
583 'pylons.app_globals',
582 'pylons.app_globals',
584 'pylons.environ_config',
583 'pylons.environ_config',
585 'sqlalchemy.db1.url',
584 'sqlalchemy.db1.url',
586 'channelstream.secret',
585 'channelstream.secret',
587 'beaker.session.secret',
586 'beaker.session.secret',
588 'rhodecode.encrypted_values.secret',
587 'rhodecode.encrypted_values.secret',
589 'rhodecode_auth_github_consumer_key',
588 'rhodecode_auth_github_consumer_key',
590 'rhodecode_auth_github_consumer_secret',
589 'rhodecode_auth_github_consumer_secret',
591 'rhodecode_auth_google_consumer_key',
590 'rhodecode_auth_google_consumer_key',
592 'rhodecode_auth_google_consumer_secret',
591 'rhodecode_auth_google_consumer_secret',
593 'rhodecode_auth_bitbucket_consumer_secret',
592 'rhodecode_auth_bitbucket_consumer_secret',
594 'rhodecode_auth_bitbucket_consumer_key',
593 'rhodecode_auth_bitbucket_consumer_key',
595 'rhodecode_auth_twitter_consumer_secret',
594 'rhodecode_auth_twitter_consumer_secret',
596 'rhodecode_auth_twitter_consumer_key',
595 'rhodecode_auth_twitter_consumer_key',
597
596
598 'rhodecode_auth_twitter_secret',
597 'rhodecode_auth_twitter_secret',
599 'rhodecode_auth_github_secret',
598 'rhodecode_auth_github_secret',
600 'rhodecode_auth_google_secret',
599 'rhodecode_auth_google_secret',
601 'rhodecode_auth_bitbucket_secret',
600 'rhodecode_auth_bitbucket_secret',
602
601
603 'appenlight.api_key',
602 'appenlight.api_key',
604 ('app_conf', 'sqlalchemy.db1.url')
603 ('app_conf', 'sqlalchemy.db1.url')
605 ]
604 ]
606 for k in blacklist:
605 for k in blacklist:
607 if isinstance(k, tuple):
606 if isinstance(k, tuple):
608 section, key = k
607 section, key = k
609 if section in rhodecode_ini_safe:
608 if section in rhodecode_ini_safe:
610 rhodecode_ini_safe[section] = '**OBFUSCATED**'
609 rhodecode_ini_safe[section] = '**OBFUSCATED**'
611 else:
610 else:
612 rhodecode_ini_safe.pop(k, None)
611 rhodecode_ini_safe.pop(k, None)
613
612
614 # TODO: maybe put some CONFIG checks here ?
613 # TODO: maybe put some CONFIG checks here ?
615 return SysInfoRes(value={'config': rhodecode_ini_safe,
614 return SysInfoRes(value={'config': rhodecode_ini_safe,
616 'path': path, 'cert_path': cert_path})
615 'path': path, 'cert_path': cert_path})
617
616
618
617
619 def database_info():
618 def database_info():
620 import rhodecode
619 import rhodecode
621 from sqlalchemy.engine import url as engine_url
620 from sqlalchemy.engine import url as engine_url
622 from rhodecode.model.meta import Base as sql_base, Session
621 from rhodecode.model.meta import Base as sql_base, Session
623 from rhodecode.model.db import DbMigrateVersion
622 from rhodecode.model.db import DbMigrateVersion
624
623
625 state = STATE_OK_DEFAULT
624 state = STATE_OK_DEFAULT
626
625
627 db_migrate = DbMigrateVersion.query().filter(
626 db_migrate = DbMigrateVersion.query().filter(
628 DbMigrateVersion.repository_id == 'rhodecode_db_migrations').one()
627 DbMigrateVersion.repository_id == 'rhodecode_db_migrations').one()
629
628
630 db_url_obj = engine_url.make_url(rhodecode.CONFIG['sqlalchemy.db1.url'])
629 db_url_obj = engine_url.make_url(rhodecode.CONFIG['sqlalchemy.db1.url'])
631
630
632 try:
631 try:
633 engine = sql_base.metadata.bind
632 engine = sql_base.metadata.bind
634 db_server_info = engine.dialect._get_server_version_info(
633 db_server_info = engine.dialect._get_server_version_info(
635 Session.connection(bind=engine))
634 Session.connection(bind=engine))
636 db_version = '.'.join(map(str, db_server_info))
635 db_version = '.'.join(map(str, db_server_info))
637 except Exception:
636 except Exception:
638 log.exception('failed to fetch db version')
637 log.exception('failed to fetch db version')
639 db_version = 'UNKNOWN'
638 db_version = 'UNKNOWN'
640
639
641 db_info = dict(
640 db_info = dict(
642 migrate_version=db_migrate.version,
641 migrate_version=db_migrate.version,
643 type=db_url_obj.get_backend_name(),
642 type=db_url_obj.get_backend_name(),
644 version=db_version,
643 version=db_version,
645 url=repr(db_url_obj)
644 url=repr(db_url_obj)
646 )
645 )
647 current_version = db_migrate.version
646 current_version = db_migrate.version
648 expected_version = rhodecode.__dbversion__
647 expected_version = rhodecode.__dbversion__
649 if state['type'] == STATE_OK and current_version != expected_version:
648 if state['type'] == STATE_OK and current_version != expected_version:
650 msg = 'Critical: database schema mismatch, ' \
649 msg = 'Critical: database schema mismatch, ' \
651 'expected version {}, got {}. ' \
650 'expected version {}, got {}. ' \
652 'Please run migrations on your database.'.format(
651 'Please run migrations on your database.'.format(
653 expected_version, current_version)
652 expected_version, current_version)
654 state = {'message': msg, 'type': STATE_ERR}
653 state = {'message': msg, 'type': STATE_ERR}
655
654
656 human_value = db_info.copy()
655 human_value = db_info.copy()
657 human_value['url'] = "{} @ migration version: {}".format(
656 human_value['url'] = "{} @ migration version: {}".format(
658 db_info['url'], db_info['migrate_version'])
657 db_info['url'], db_info['migrate_version'])
659 human_value['version'] = "{} {}".format(db_info['type'], db_info['version'])
658 human_value['version'] = "{} {}".format(db_info['type'], db_info['version'])
660 return SysInfoRes(value=db_info, state=state, human_value=human_value)
659 return SysInfoRes(value=db_info, state=state, human_value=human_value)
661
660
662
661
663 def server_info(environ):
662 def server_info(environ):
664 import rhodecode
663 import rhodecode
665 from rhodecode.lib.base import get_server_ip_addr, get_server_port
664 from rhodecode.lib.base import get_server_ip_addr, get_server_port
666
665
667 value = {
666 value = {
668 'server_ip': '%s:%s' % (
667 'server_ip': '%s:%s' % (
669 get_server_ip_addr(environ, log_errors=False),
668 get_server_ip_addr(environ, log_errors=False),
670 get_server_port(environ)
669 get_server_port(environ)
671 ),
670 ),
672 'server_id': rhodecode.CONFIG.get('instance_id'),
671 'server_id': rhodecode.CONFIG.get('instance_id'),
673 }
672 }
674 return SysInfoRes(value=value)
673 return SysInfoRes(value=value)
675
674
676
675
677 def usage_info():
676 def usage_info():
678 from rhodecode.model.db import User, Repository
677 from rhodecode.model.db import User, Repository
679 value = {
678 value = {
680 'users': User.query().count(),
679 'users': User.query().count(),
681 'users_active': User.query().filter(User.active == True).count(),
680 'users_active': User.query().filter(User.active == True).count(),
682 'repositories': Repository.query().count(),
681 'repositories': Repository.query().count(),
683 'repository_types': {
682 'repository_types': {
684 'hg': Repository.query().filter(
683 'hg': Repository.query().filter(
685 Repository.repo_type == 'hg').count(),
684 Repository.repo_type == 'hg').count(),
686 'git': Repository.query().filter(
685 'git': Repository.query().filter(
687 Repository.repo_type == 'git').count(),
686 Repository.repo_type == 'git').count(),
688 'svn': Repository.query().filter(
687 'svn': Repository.query().filter(
689 Repository.repo_type == 'svn').count(),
688 Repository.repo_type == 'svn').count(),
690 },
689 },
691 }
690 }
692 return SysInfoRes(value=value)
691 return SysInfoRes(value=value)
693
692
694
693
695 def get_system_info(environ):
694 def get_system_info(environ):
696 environ = environ or {}
695 environ = environ or {}
697 return {
696 return {
698 'rhodecode_app': SysInfo(rhodecode_app_info)(),
697 'rhodecode_app': SysInfo(rhodecode_app_info)(),
699 'rhodecode_config': SysInfo(rhodecode_config)(),
698 'rhodecode_config': SysInfo(rhodecode_config)(),
700 'rhodecode_usage': SysInfo(usage_info)(),
699 'rhodecode_usage': SysInfo(usage_info)(),
701 'python': SysInfo(python_info)(),
700 'python': SysInfo(python_info)(),
702 'py_modules': SysInfo(py_modules)(),
701 'py_modules': SysInfo(py_modules)(),
703
702
704 'platform': SysInfo(platform_type)(),
703 'platform': SysInfo(platform_type)(),
705 'server': SysInfo(server_info, environ=environ)(),
704 'server': SysInfo(server_info, environ=environ)(),
706 'database': SysInfo(database_info)(),
705 'database': SysInfo(database_info)(),
707
706
708 'storage': SysInfo(storage)(),
707 'storage': SysInfo(storage)(),
709 'storage_inodes': SysInfo(storage_inodes)(),
708 'storage_inodes': SysInfo(storage_inodes)(),
710 'storage_archive': SysInfo(storage_archives)(),
709 'storage_archive': SysInfo(storage_archives)(),
711 'storage_gist': SysInfo(storage_gist)(),
710 'storage_gist': SysInfo(storage_gist)(),
712 'storage_temp': SysInfo(storage_temp)(),
711 'storage_temp': SysInfo(storage_temp)(),
713
712
714 'search': SysInfo(search_info)(),
713 'search': SysInfo(search_info)(),
715
714
716 'uptime': SysInfo(uptime)(),
715 'uptime': SysInfo(uptime)(),
717 'load': SysInfo(machine_load)(),
716 'load': SysInfo(machine_load)(),
718 'cpu': SysInfo(cpu)(),
717 'cpu': SysInfo(cpu)(),
719 'memory': SysInfo(memory)(),
718 'memory': SysInfo(memory)(),
720
719
721 'vcs_backends': SysInfo(vcs_backends)(),
720 'vcs_backends': SysInfo(vcs_backends)(),
722 'vcs_server': SysInfo(vcs_server)(),
721 'vcs_server': SysInfo(vcs_server)(),
723
722
724 'git': SysInfo(git_info)(),
723 'git': SysInfo(git_info)(),
725 'hg': SysInfo(hg_info)(),
724 'hg': SysInfo(hg_info)(),
726 'svn': SysInfo(svn_info)(),
725 'svn': SysInfo(svn_info)(),
727 }
726 }
General Comments 0
You need to be logged in to leave comments. Login now