##// END OF EJS Templates
feat(system-info): expose rhodecode config for better visibility of set settings
super-admin -
r5552:f3356a66 default
parent child Browse files
Show More
@@ -1,249 +1,253 b''
1 1
2 2
3 3 # Copyright (C) 2016-2023 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
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 Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import logging
22 22 import urllib.request
23 23 import urllib.error
24 24 import urllib.parse
25 25 import os
26 26
27 27 import rhodecode
28 28 from rhodecode.apps._base import BaseAppView
29 29 from rhodecode.apps._base.navigation import navigation_list
30 30 from rhodecode.lib import helpers as h
31 31 from rhodecode.lib.auth import (LoginRequired, HasPermissionAllDecorator)
32 32 from rhodecode.lib.utils2 import str2bool
33 33 from rhodecode.lib import system_info
34 34 from rhodecode.model.update import UpdateModel
35 35
36 36 log = logging.getLogger(__name__)
37 37
38 38
39 39 class AdminSystemInfoSettingsView(BaseAppView):
40 40 def load_default_context(self):
41 41 c = self._get_local_tmpl_context()
42 42 return c
43 43
44 44 def get_env_data(self):
45 45 black_list = [
46 46 'NIX_LDFLAGS',
47 47 'NIX_CFLAGS_COMPILE',
48 48 'propagatedBuildInputs',
49 49 'propagatedNativeBuildInputs',
50 50 'postInstall',
51 51 'buildInputs',
52 52 'buildPhase',
53 53 'preShellHook',
54 54 'preShellHook',
55 55 'preCheck',
56 56 'preBuild',
57 57 'postShellHook',
58 58 'postFixup',
59 59 'postCheck',
60 60 'nativeBuildInputs',
61 61 'installPhase',
62 62 'installCheckPhase',
63 63 'checkPhase',
64 64 'configurePhase',
65 65 'shellHook'
66 66 ]
67 67 secret_list = [
68 68 'RHODECODE_USER_PASS'
69 69 ]
70 70
71 71 for k, v in sorted(os.environ.items()):
72 72 if k in black_list:
73 73 continue
74 74 if k in secret_list:
75 75 v = '*****'
76 76 yield k, v
77 77
78 78 @LoginRequired()
79 79 @HasPermissionAllDecorator('hg.admin')
80 80 def settings_system_info(self):
81 81 _ = self.request.translate
82 82 c = self.load_default_context()
83 83
84 84 c.active = 'system'
85 85 c.navlist = navigation_list(self.request)
86 86
87 87 # TODO(marcink), figure out how to allow only selected users to do this
88 88 c.allowed_to_snapshot = self._rhodecode_user.admin
89 89
90 90 snapshot = str2bool(self.request.params.get('snapshot'))
91 91
92 92 c.rhodecode_update_url = UpdateModel().get_update_url()
93 93 c.env_data = self.get_env_data()
94 94 server_info = system_info.get_system_info(self.request.environ)
95 95
96 96 for key, val in server_info.items():
97 97 setattr(c, key, val)
98 98
99 99 def val(name, subkey='human_value'):
100 100 return server_info[name][subkey]
101 101
102 102 def state(name):
103 103 return server_info[name]['state']
104 104
105 105 def val2(name):
106 106 val = server_info[name]['human_value']
107 107 state = server_info[name]['state']
108 108 return val, state
109 109
110 110 update_info_msg = _('Note: please make sure this server can '
111 111 'access `${url}` for the update link to work',
112 112 mapping=dict(url=c.rhodecode_update_url))
113 113 version = UpdateModel().get_stored_version()
114 114 is_outdated = UpdateModel().is_outdated(
115 115 rhodecode.__version__, version)
116 116 update_state = {
117 117 'type': 'warning',
118 118 'message': 'New version available: {}'.format(version)
119 119 } \
120 120 if is_outdated else {}
121 121 c.data_items = [
122 122 # update info
123 123 (_('Update info'), h.literal(
124 124 '<span class="link" id="check_for_update" >%s.</span>' % (
125 125 _('Check for updates')) +
126 126 '<br/> <span >%s.</span>' % (update_info_msg)
127 127 ), ''),
128 128
129 129 # RhodeCode specific
130 130 (_('RhodeCode Version'), val('rhodecode_app')['text'], state('rhodecode_app')),
131 131 (_('Latest version'), version, update_state),
132 132 (_('RhodeCode Base URL'), val('rhodecode_config')['config'].get('app.base_url'), state('rhodecode_config')),
133 133 (_('RhodeCode Server IP'), val('server')['server_ip'], state('server')),
134 134 (_('RhodeCode Server ID'), val('server')['server_id'], state('server')),
135 135 (_('RhodeCode Configuration'), val('rhodecode_config')['path'], state('rhodecode_config')),
136 136 (_('RhodeCode Certificate'), val('rhodecode_config')['cert_path'], state('rhodecode_config')),
137 137 (_('Workers'), val('rhodecode_config')['config']['server:main'].get('workers', '?'), state('rhodecode_config')),
138 138 (_('Worker Type'), val('rhodecode_config')['config']['server:main'].get('worker_class', 'sync'), state('rhodecode_config')),
139 139 ('', '', ''), # spacer
140 140
141 141 # Database
142 142 (_('Database'), val('database')['url'], state('database')),
143 143 (_('Database version'), val('database')['version'], state('database')),
144 144 ('', '', ''), # spacer
145 145
146 146 # Platform/Python
147 147 (_('Platform'), val('platform')['name'], state('platform')),
148 148 (_('Platform UUID'), val('platform')['uuid'], state('platform')),
149 149 (_('Lang'), val('locale'), state('locale')),
150 150 (_('Python version'), val('python')['version'], state('python')),
151 151 (_('Python path'), val('python')['executable'], state('python')),
152 152 ('', '', ''), # spacer
153 153
154 154 # Systems stats
155 155 (_('CPU'), val('cpu')['text'], state('cpu')),
156 156 (_('Load'), val('load')['text'], state('load')),
157 157 (_('Memory'), val('memory')['text'], state('memory')),
158 158 (_('Uptime'), val('uptime')['text'], state('uptime')),
159 159 ('', '', ''), # spacer
160 160
161 161 # ulimit
162 162 (_('Ulimit'), val('ulimit')['text'], state('ulimit')),
163 163
164 164 # Repo storage
165 165 (_('Storage location'), val('storage')['path'], state('storage')),
166 166 (_('Storage info'), val('storage')['text'], state('storage')),
167 167 (_('Storage inodes'), val('storage_inodes')['text'], state('storage_inodes')),
168 168 ('', '', ''), # spacer
169 169
170 170 (_('Gist storage location'), val('storage_gist')['path'], state('storage_gist')),
171 171 (_('Gist storage info'), val('storage_gist')['text'], state('storage_gist')),
172 172 ('', '', ''), # spacer
173 173
174 174 (_('Artifacts storage backend'), val('storage_artifacts')['type'], state('storage_artifacts')),
175 175 (_('Artifacts storage location'), val('storage_artifacts')['path'], state('storage_artifacts')),
176 176 (_('Artifacts info'), val('storage_artifacts')['text'], state('storage_artifacts')),
177 177 ('', '', ''), # spacer
178 178
179 179 (_('Archive cache storage backend'), val('storage_archive')['type'], state('storage_archive')),
180 180 (_('Archive cache storage location'), val('storage_archive')['path'], state('storage_archive')),
181 181 (_('Archive cache info'), val('storage_archive')['text'], state('storage_archive')),
182 182 ('', '', ''), # spacer
183 183
184 184
185 185 (_('Temp storage location'), val('storage_temp')['path'], state('storage_temp')),
186 186 (_('Temp storage info'), val('storage_temp')['text'], state('storage_temp')),
187 187 ('', '', ''), # spacer
188 188
189 189 (_('Search info'), val('search')['text'], state('search')),
190 190 (_('Search location'), val('search')['location'], state('search')),
191 191 ('', '', ''), # spacer
192 192
193 193 # VCS specific
194 194 (_('VCS Backends'), val('vcs_backends'), state('vcs_backends')),
195 195 (_('VCS Server'), val('vcs_server')['text'], state('vcs_server')),
196 196 (_('GIT'), val('git'), state('git')),
197 197 (_('HG'), val('hg'), state('hg')),
198 198 (_('SVN'), val('svn'), state('svn')),
199 199
200 200 ]
201 201
202 c.rhodecode_data_items = [
203 (k, v) for k, v in sorted((val('rhodecode_server_config') or {}).items(), key=lambda x: x[0].lower())
204 ]
205
202 206 c.vcsserver_data_items = [
203 (k, v) for k, v in (val('vcs_server_config') or {}).items()
207 (k, v) for k, v in sorted((val('vcs_server_config') or {}).items(), key=lambda x: x[0].lower())
204 208 ]
205 209
206 210 if snapshot:
207 211 if c.allowed_to_snapshot:
208 212 c.data_items.pop(0) # remove server info
209 213 self.request.override_renderer = 'admin/settings/settings_system_snapshot.mako'
210 214 else:
211 215 h.flash('You are not allowed to do this', category='warning')
212 216 return self._get_template_context(c)
213 217
214 218 @LoginRequired()
215 219 @HasPermissionAllDecorator('hg.admin')
216 220 def settings_system_info_check_update(self):
217 221 _ = self.request.translate
218 222 c = self.load_default_context()
219 223
220 224 update_url = UpdateModel().get_update_url()
221 225
222 226 def _err(s):
223 227 return f'<div style="color:#ff8888; padding:4px 0px">{s}</div>'
224 228
225 229 try:
226 230 data = UpdateModel().get_update_data(update_url)
227 231 except urllib.error.URLError as e:
228 232 log.exception("Exception contacting upgrade server")
229 233 self.request.override_renderer = 'string'
230 234 return _err('Failed to contact upgrade server: %r' % e)
231 235 except ValueError as e:
232 236 log.exception("Bad data sent from update server")
233 237 self.request.override_renderer = 'string'
234 238 return _err('Bad data sent from update server')
235 239
236 240 latest = data['versions'][0]
237 241
238 242 c.update_url = update_url
239 243 c.latest_data = latest
240 244 c.latest_ver = (latest['version'] or '').strip()
241 245 c.cur_ver = self.request.GET.get('ver') or rhodecode.__version__
242 246 c.should_upgrade = False
243 247
244 248 is_outdated = UpdateModel().is_outdated(c.cur_ver, c.latest_ver)
245 249 if is_outdated:
246 250 c.should_upgrade = True
247 251 c.important_notices = latest['general']
248 252 UpdateModel().store_version(latest['version'])
249 253 return self._get_template_context(c)
@@ -1,866 +1,893 b''
1 1 # Copyright (C) 2017-2023 RhodeCode GmbH
2 2 #
3 3 # This program is free software: you can redistribute it and/or modify
4 4 # it under the terms of the GNU Affero General Public License, version 3
5 5 # (only), as published by the Free Software Foundation.
6 6 #
7 7 # This program is distributed in the hope that it will be useful,
8 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 # GNU General Public License for more details.
11 11 #
12 12 # You should have received a copy of the GNU Affero General Public License
13 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 #
15 15 # This program is dual-licensed. If you wish to learn more about the
16 16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 18
19 19
20 20 import os
21 21 import sys
22 22 import time
23 23 import platform
24 24 import collections
25 25 import psutil
26 26 from functools import wraps
27 27
28 28 import pkg_resources
29 29 import logging
30 30 import resource
31 31
32 32 import configparser
33 33
34 34 from rc_license.models import LicenseModel
35 35 from rhodecode.lib.str_utils import safe_str
36 36
37 37 log = logging.getLogger(__name__)
38 38
39 39
40 40 _NA = 'NOT AVAILABLE'
41 41 _NA_FLOAT = 0.0
42 42
43 43 STATE_OK = 'ok'
44 44 STATE_ERR = 'error'
45 45 STATE_WARN = 'warning'
46 46
47 47 STATE_OK_DEFAULT = {'message': '', 'type': STATE_OK}
48 48
49 49
50 50 registered_helpers = {}
51 51
52 52
53 53 def register_sysinfo(func):
54 54 """
55 55 @register_helper
56 56 def db_check():
57 57 pass
58 58
59 59 db_check == registered_helpers['db_check']
60 60 """
61 61 global registered_helpers
62 62 registered_helpers[func.__name__] = func
63 63
64 64 @wraps(func)
65 65 def _wrapper(*args, **kwargs):
66 66 return func(*args, **kwargs)
67 67 return _wrapper
68 68
69 69
70 70 # HELPERS
71 71 def percentage(part: (int, float), whole: (int, float)):
72 72 whole = float(whole)
73 73 if whole > 0:
74 74 return round(100 * float(part) / whole, 1)
75 75 return 0.0
76 76
77 77
78 78 def get_storage_size(storage_path):
79 79 sizes = []
80 80 for file_ in os.listdir(storage_path):
81 81 storage_file = os.path.join(storage_path, file_)
82 82 if os.path.isfile(storage_file):
83 83 try:
84 84 sizes.append(os.path.getsize(storage_file))
85 85 except OSError:
86 86 log.exception('Failed to get size of storage file %s', storage_file)
87 87 pass
88 88
89 89 return sum(sizes)
90 90
91 91
92 92 def get_resource(resource_type):
93 93 try:
94 94 return resource.getrlimit(resource_type)
95 95 except Exception:
96 96 return 'NOT_SUPPORTED'
97 97
98 98
99 99 def get_cert_path(ini_path):
100 100 default = '/etc/ssl/certs/ca-certificates.crt'
101 101 control_ca_bundle = os.path.join(
102 102 os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(ini_path)))),
103 103 '/etc/ssl/certs/ca-certificates.crt')
104 104 if os.path.isfile(control_ca_bundle):
105 105 default = control_ca_bundle
106 106
107 107 return default
108 108
109 109
110 110 class SysInfoRes(object):
111 111 def __init__(self, value, state=None, human_value=None):
112 112 self.value = value
113 113 self.state = state or STATE_OK_DEFAULT
114 114 self.human_value = human_value or value
115 115
116 116 def __json__(self):
117 117 return {
118 118 'value': self.value,
119 119 'state': self.state,
120 120 'human_value': self.human_value,
121 121 }
122 122
123 123 def get_value(self):
124 124 return self.__json__()
125 125
126 126 def __str__(self):
127 127 return f'<SysInfoRes({self.__json__()})>'
128 128
129 129
130 130 class SysInfo(object):
131 131
132 132 def __init__(self, func_name, **kwargs):
133 133 self.function_name = func_name
134 134 self.value = _NA
135 135 self.state = None
136 136 self.kwargs = kwargs or {}
137 137
138 138 def __call__(self):
139 139 computed = self.compute(**self.kwargs)
140 140 if not isinstance(computed, SysInfoRes):
141 141 raise ValueError(
142 142 'computed value for {} is not instance of '
143 143 '{}, got {} instead'.format(
144 144 self.function_name, SysInfoRes, type(computed)))
145 145 return computed.__json__()
146 146
147 147 def __str__(self):
148 148 return f'<SysInfo({self.function_name})>'
149 149
150 150 def compute(self, **kwargs):
151 151 return self.function_name(**kwargs)
152 152
153 153
154 154 # SysInfo functions
155 155 @register_sysinfo
156 156 def python_info():
157 157 value = dict(version=f'{platform.python_version()}:{platform.python_implementation()}',
158 158 executable=sys.executable)
159 159 return SysInfoRes(value=value)
160 160
161 161
162 162 @register_sysinfo
163 163 def py_modules():
164 164 mods = dict([(p.project_name, {'version': p.version, 'location': p.location})
165 165 for p in pkg_resources.working_set])
166 166
167 167 value = sorted(mods.items(), key=lambda k: k[0].lower())
168 168 return SysInfoRes(value=value)
169 169
170 170
171 171 @register_sysinfo
172 172 def platform_type():
173 173 from rhodecode.lib.utils import generate_platform_uuid
174 174
175 175 value = dict(
176 176 name=safe_str(platform.platform()),
177 177 uuid=generate_platform_uuid()
178 178 )
179 179 return SysInfoRes(value=value)
180 180
181 181
182 182 @register_sysinfo
183 183 def locale_info():
184 184 import locale
185 185
186 186 def safe_get_locale(locale_name):
187 187 try:
188 188 locale.getlocale(locale_name)
189 189 except TypeError:
190 190 return f'FAILED_LOCALE_GET:{locale_name}'
191 191
192 192 value = dict(
193 193 locale_default=locale.getlocale(),
194 194 locale_lc_all=safe_get_locale(locale.LC_ALL),
195 195 locale_lc_ctype=safe_get_locale(locale.LC_CTYPE),
196 196 lang_env=os.environ.get('LANG'),
197 197 lc_all_env=os.environ.get('LC_ALL'),
198 198 local_archive_env=os.environ.get('LOCALE_ARCHIVE'),
199 199 )
200 200 human_value = \
201 201 f"LANG: {value['lang_env']}, \
202 202 locale LC_ALL: {value['locale_lc_all']}, \
203 203 locale LC_CTYPE: {value['locale_lc_ctype']}, \
204 204 Default locales: {value['locale_default']}"
205 205
206 206 return SysInfoRes(value=value, human_value=human_value)
207 207
208 208
209 209 @register_sysinfo
210 210 def ulimit_info():
211 211 data = collections.OrderedDict([
212 212 ('cpu time (seconds)', get_resource(resource.RLIMIT_CPU)),
213 213 ('file size', get_resource(resource.RLIMIT_FSIZE)),
214 214 ('stack size', get_resource(resource.RLIMIT_STACK)),
215 215 ('core file size', get_resource(resource.RLIMIT_CORE)),
216 216 ('address space size', get_resource(resource.RLIMIT_AS)),
217 217 ('locked in mem size', get_resource(resource.RLIMIT_MEMLOCK)),
218 218 ('heap size', get_resource(resource.RLIMIT_DATA)),
219 219 ('rss size', get_resource(resource.RLIMIT_RSS)),
220 220 ('number of processes', get_resource(resource.RLIMIT_NPROC)),
221 221 ('open files', get_resource(resource.RLIMIT_NOFILE)),
222 222 ])
223 223
224 224 text = ', '.join(f'{k}:{v}' for k, v in data.items())
225 225
226 226 value = {
227 227 'limits': data,
228 228 'text': text,
229 229 }
230 230 return SysInfoRes(value=value)
231 231
232 232
233 233 @register_sysinfo
234 234 def uptime():
235 235 from rhodecode.lib.helpers import age, time_to_datetime
236 236 from rhodecode.translation import TranslationString
237 237
238 238 value = dict(boot_time=0, uptime=0, text='')
239 239 state = STATE_OK_DEFAULT
240 240
241 241 boot_time = psutil.boot_time()
242 242 value['boot_time'] = boot_time
243 243 value['uptime'] = time.time() - boot_time
244 244
245 245 date_or_age = age(time_to_datetime(boot_time))
246 246 if isinstance(date_or_age, TranslationString):
247 247 date_or_age = date_or_age.interpolate()
248 248
249 249 human_value = value.copy()
250 250 human_value['boot_time'] = time_to_datetime(boot_time)
251 251 human_value['uptime'] = age(time_to_datetime(boot_time), show_suffix=False)
252 252
253 253 human_value['text'] = f'Server started {date_or_age}'
254 254 return SysInfoRes(value=value, human_value=human_value)
255 255
256 256
257 257 @register_sysinfo
258 258 def memory():
259 259 from rhodecode.lib.helpers import format_byte_size_binary
260 260 value = dict(available=0, used=0, used_real=0, cached=0, percent=0,
261 261 percent_used=0, free=0, inactive=0, active=0, shared=0,
262 262 total=0, buffers=0, text='')
263 263
264 264 state = STATE_OK_DEFAULT
265 265
266 266 value.update(dict(psutil.virtual_memory()._asdict()))
267 267 value['used_real'] = value['total'] - value['available']
268 268 value['percent_used'] = psutil._common.usage_percent(value['used_real'], value['total'], 1)
269 269
270 270 human_value = value.copy()
271 271 human_value['text'] = '{}/{}, {}% used'.format(
272 272 format_byte_size_binary(value['used_real']),
273 273 format_byte_size_binary(value['total']),
274 274 value['percent_used'])
275 275
276 276 keys = list(value.keys())[::]
277 277 keys.pop(keys.index('percent'))
278 278 keys.pop(keys.index('percent_used'))
279 279 keys.pop(keys.index('text'))
280 280 for k in keys:
281 281 human_value[k] = format_byte_size_binary(value[k])
282 282
283 283 if state['type'] == STATE_OK and value['percent_used'] > 90:
284 284 msg = 'Critical: your available RAM memory is very low.'
285 285 state = {'message': msg, 'type': STATE_ERR}
286 286
287 287 elif state['type'] == STATE_OK and value['percent_used'] > 70:
288 288 msg = 'Warning: your available RAM memory is running low.'
289 289 state = {'message': msg, 'type': STATE_WARN}
290 290
291 291 return SysInfoRes(value=value, state=state, human_value=human_value)
292 292
293 293
294 294 @register_sysinfo
295 295 def machine_load():
296 296 value = {'1_min': _NA_FLOAT, '5_min': _NA_FLOAT, '15_min': _NA_FLOAT, 'text': ''}
297 297 state = STATE_OK_DEFAULT
298 298
299 299 # load averages
300 300 if hasattr(psutil.os, 'getloadavg'):
301 301 value.update(dict(
302 302 list(zip(['1_min', '5_min', '15_min'], psutil.os.getloadavg()))
303 303 ))
304 304
305 305 human_value = value.copy()
306 306 human_value['text'] = '1min: {}, 5min: {}, 15min: {}'.format(
307 307 value['1_min'], value['5_min'], value['15_min'])
308 308
309 309 if state['type'] == STATE_OK and value['15_min'] > 5.0:
310 310 msg = 'Warning: your machine load is very high.'
311 311 state = {'message': msg, 'type': STATE_WARN}
312 312
313 313 return SysInfoRes(value=value, state=state, human_value=human_value)
314 314
315 315
316 316 @register_sysinfo
317 317 def cpu():
318 318 value = {'cpu': 0, 'cpu_count': 0, 'cpu_usage': []}
319 319 state = STATE_OK_DEFAULT
320 320
321 321 value['cpu'] = psutil.cpu_percent(0.5)
322 322 value['cpu_usage'] = psutil.cpu_percent(0.5, percpu=True)
323 323 value['cpu_count'] = psutil.cpu_count()
324 324
325 325 human_value = value.copy()
326 326 human_value['text'] = f'{value["cpu_count"]} cores at {value["cpu"]} %'
327 327
328 328 return SysInfoRes(value=value, state=state, human_value=human_value)
329 329
330 330
331 331 @register_sysinfo
332 332 def storage():
333 333 from rhodecode.lib.helpers import format_byte_size_binary
334 334 from rhodecode.lib.utils import get_rhodecode_repo_store_path
335 335 path = get_rhodecode_repo_store_path()
336 336
337 337 value = dict(percent=0, used=0, total=0, path=path, text='')
338 338 state = STATE_OK_DEFAULT
339 339
340 340 try:
341 341 value.update(dict(psutil.disk_usage(path)._asdict()))
342 342 except Exception as e:
343 343 log.exception('Failed to fetch disk info')
344 344 state = {'message': str(e), 'type': STATE_ERR}
345 345
346 346 human_value = value.copy()
347 347 human_value['used'] = format_byte_size_binary(value['used'])
348 348 human_value['total'] = format_byte_size_binary(value['total'])
349 349 human_value['text'] = "{}/{}, {}% used".format(
350 350 format_byte_size_binary(value['used']),
351 351 format_byte_size_binary(value['total']),
352 352 value['percent'])
353 353
354 354 if state['type'] == STATE_OK and value['percent'] > 90:
355 355 msg = 'Critical: your disk space is very low.'
356 356 state = {'message': msg, 'type': STATE_ERR}
357 357
358 358 elif state['type'] == STATE_OK and value['percent'] > 70:
359 359 msg = 'Warning: your disk space is running low.'
360 360 state = {'message': msg, 'type': STATE_WARN}
361 361
362 362 return SysInfoRes(value=value, state=state, human_value=human_value)
363 363
364 364
365 365 @register_sysinfo
366 366 def storage_inodes():
367 367 from rhodecode.lib.utils import get_rhodecode_repo_store_path
368 368 path = get_rhodecode_repo_store_path()
369 369
370 370 value = dict(percent=0.0, free=0, used=0, total=0, path=path, text='')
371 371 state = STATE_OK_DEFAULT
372 372
373 373 try:
374 374 i_stat = os.statvfs(path)
375 375 value['free'] = i_stat.f_ffree
376 376 value['used'] = i_stat.f_files-i_stat.f_favail
377 377 value['total'] = i_stat.f_files
378 378 value['percent'] = percentage(value['used'], value['total'])
379 379 except Exception as e:
380 380 log.exception('Failed to fetch disk inodes info')
381 381 state = {'message': str(e), 'type': STATE_ERR}
382 382
383 383 human_value = value.copy()
384 384 human_value['text'] = "{}/{}, {}% used".format(
385 385 value['used'], value['total'], value['percent'])
386 386
387 387 if state['type'] == STATE_OK and value['percent'] > 90:
388 388 msg = 'Critical: your disk free inodes are very low.'
389 389 state = {'message': msg, 'type': STATE_ERR}
390 390
391 391 elif state['type'] == STATE_OK and value['percent'] > 70:
392 392 msg = 'Warning: your disk free inodes are running low.'
393 393 state = {'message': msg, 'type': STATE_WARN}
394 394
395 395 return SysInfoRes(value=value, state=state, human_value=human_value)
396 396
397 397
398 398 @register_sysinfo
399 399 def storage_artifacts():
400 400 import rhodecode
401 401 from rhodecode.lib.helpers import format_byte_size_binary
402 402 from rhodecode.lib.archive_cache import get_archival_cache_store
403 403
404 404 backend_type = rhodecode.ConfigGet().get_str('archive_cache.backend.type')
405 405
406 406 value = dict(percent=0, used=0, total=0, items=0, path='', text='', type=backend_type)
407 407 state = STATE_OK_DEFAULT
408 408 try:
409 409 d_cache = get_archival_cache_store(config=rhodecode.CONFIG)
410 410 backend_type = str(d_cache)
411 411
412 412 total_files, total_size, _directory_stats = d_cache.get_statistics()
413 413
414 414 value.update({
415 415 'percent': 100,
416 416 'used': total_size,
417 417 'total': total_size,
418 418 'items': total_files,
419 419 'path': d_cache.storage_path,
420 420 'type': backend_type
421 421 })
422 422
423 423 except Exception as e:
424 424 log.exception('failed to fetch archive cache storage')
425 425 state = {'message': str(e), 'type': STATE_ERR}
426 426
427 427 human_value = value.copy()
428 428 human_value['used'] = format_byte_size_binary(value['used'])
429 429 human_value['total'] = format_byte_size_binary(value['total'])
430 430 human_value['text'] = f"{human_value['used']} ({value['items']} items)"
431 431
432 432 return SysInfoRes(value=value, state=state, human_value=human_value)
433 433
434 434
435 435 @register_sysinfo
436 436 def storage_archives():
437 437 import rhodecode
438 438 from rhodecode.lib.helpers import format_byte_size_binary
439 439 import rhodecode.apps.file_store.utils as store_utils
440 440 from rhodecode import CONFIG
441 441
442 442 backend_type = rhodecode.ConfigGet().get_str(store_utils.config_keys.backend_type)
443 443
444 444 value = dict(percent=0, used=0, total=0, items=0, path='', text='', type=backend_type)
445 445 state = STATE_OK_DEFAULT
446 446 try:
447 447 f_store = store_utils.get_filestore_backend(config=CONFIG)
448 448 backend_type = str(f_store)
449 449 total_files, total_size, _directory_stats = f_store.get_statistics()
450 450
451 451 value.update({
452 452 'percent': 100,
453 453 'used': total_size,
454 454 'total': total_size,
455 455 'items': total_files,
456 456 'path': f_store.storage_path,
457 457 'type': backend_type
458 458 })
459 459
460 460 except Exception as e:
461 461 log.exception('failed to fetch archive cache storage')
462 462 state = {'message': str(e), 'type': STATE_ERR}
463 463
464 464 human_value = value.copy()
465 465 human_value['used'] = format_byte_size_binary(value['used'])
466 466 human_value['total'] = format_byte_size_binary(value['total'])
467 467 human_value['text'] = f"{human_value['used']} ({value['items']} items)"
468 468
469 469 return SysInfoRes(value=value, state=state, human_value=human_value)
470 470
471 471
472 472 @register_sysinfo
473 473 def storage_gist():
474 474 from rhodecode.model.gist import GIST_STORE_LOC
475 475 from rhodecode.lib.utils import safe_str, get_rhodecode_repo_store_path
476 476 from rhodecode.lib.helpers import format_byte_size_binary, get_directory_statistics
477 477
478 478 path = safe_str(os.path.join(
479 479 get_rhodecode_repo_store_path(), GIST_STORE_LOC))
480 480
481 481 # gist storage
482 482 value = dict(percent=0, used=0, total=0, items=0, path=path, text='')
483 483 state = STATE_OK_DEFAULT
484 484
485 485 try:
486 486 total_files, total_size, _directory_stats = get_directory_statistics(path)
487 487 value.update({
488 488 'percent': 100,
489 489 'used': total_size,
490 490 'total': total_size,
491 491 'items': total_files
492 492 })
493 493 except Exception as e:
494 494 log.exception('failed to fetch gist storage items')
495 495 state = {'message': str(e), 'type': STATE_ERR}
496 496
497 497 human_value = value.copy()
498 498 human_value['used'] = format_byte_size_binary(value['used'])
499 499 human_value['total'] = format_byte_size_binary(value['total'])
500 500 human_value['text'] = "{} ({} items)".format(
501 501 human_value['used'], value['items'])
502 502
503 503 return SysInfoRes(value=value, state=state, human_value=human_value)
504 504
505 505
506 506 @register_sysinfo
507 507 def storage_temp():
508 508 import tempfile
509 509 from rhodecode.lib.helpers import format_byte_size_binary
510 510
511 511 path = tempfile.gettempdir()
512 512 value = dict(percent=0, used=0, total=0, items=0, path=path, text='')
513 513 state = STATE_OK_DEFAULT
514 514
515 515 if not psutil:
516 516 return SysInfoRes(value=value, state=state)
517 517
518 518 try:
519 519 value.update(dict(psutil.disk_usage(path)._asdict()))
520 520 except Exception as e:
521 521 log.exception('Failed to fetch temp dir info')
522 522 state = {'message': str(e), 'type': STATE_ERR}
523 523
524 524 human_value = value.copy()
525 525 human_value['used'] = format_byte_size_binary(value['used'])
526 526 human_value['total'] = format_byte_size_binary(value['total'])
527 527 human_value['text'] = "{}/{}, {}% used".format(
528 528 format_byte_size_binary(value['used']),
529 529 format_byte_size_binary(value['total']),
530 530 value['percent'])
531 531
532 532 return SysInfoRes(value=value, state=state, human_value=human_value)
533 533
534 534
535 535 @register_sysinfo
536 536 def search_info():
537 537 import rhodecode
538 538 from rhodecode.lib.index import searcher_from_config
539 539
540 540 backend = rhodecode.CONFIG.get('search.module', '')
541 541 location = rhodecode.CONFIG.get('search.location', '')
542 542
543 543 try:
544 544 searcher = searcher_from_config(rhodecode.CONFIG)
545 545 searcher = searcher.__class__.__name__
546 546 except Exception:
547 547 searcher = None
548 548
549 549 value = dict(
550 550 backend=backend, searcher=searcher, location=location, text='')
551 551 state = STATE_OK_DEFAULT
552 552
553 553 human_value = value.copy()
554 554 human_value['text'] = "backend:`{}`".format(human_value['backend'])
555 555
556 556 return SysInfoRes(value=value, state=state, human_value=human_value)
557 557
558 558
559 559 @register_sysinfo
560 560 def git_info():
561 561 from rhodecode.lib.vcs.backends import git
562 562 state = STATE_OK_DEFAULT
563 563 value = human_value = ''
564 564 try:
565 565 value = git.discover_git_version(raise_on_exc=True)
566 566 human_value = f'version reported from VCSServer: {value}'
567 567 except Exception as e:
568 568 state = {'message': str(e), 'type': STATE_ERR}
569 569
570 570 return SysInfoRes(value=value, state=state, human_value=human_value)
571 571
572 572
573 573 @register_sysinfo
574 574 def hg_info():
575 575 from rhodecode.lib.vcs.backends import hg
576 576 state = STATE_OK_DEFAULT
577 577 value = human_value = ''
578 578 try:
579 579 value = hg.discover_hg_version(raise_on_exc=True)
580 580 human_value = f'version reported from VCSServer: {value}'
581 581 except Exception as e:
582 582 state = {'message': str(e), 'type': STATE_ERR}
583 583 return SysInfoRes(value=value, state=state, human_value=human_value)
584 584
585 585
586 586 @register_sysinfo
587 587 def svn_info():
588 588 from rhodecode.lib.vcs.backends import svn
589 589 state = STATE_OK_DEFAULT
590 590 value = human_value = ''
591 591 try:
592 592 value = svn.discover_svn_version(raise_on_exc=True)
593 593 human_value = f'version reported from VCSServer: {value}'
594 594 except Exception as e:
595 595 state = {'message': str(e), 'type': STATE_ERR}
596 596 return SysInfoRes(value=value, state=state, human_value=human_value)
597 597
598 598
599 599 @register_sysinfo
600 600 def vcs_backends():
601 601 import rhodecode
602 602 value = rhodecode.CONFIG.get('vcs.backends')
603 603 human_value = 'Enabled backends in order: {}'.format(','.join(value))
604 604 return SysInfoRes(value=value, human_value=human_value)
605 605
606 606
607 607 @register_sysinfo
608 608 def vcs_server():
609 609 import rhodecode
610 610 from rhodecode.lib.vcs.backends import get_vcsserver_service_data
611 611
612 612 server_url = rhodecode.CONFIG.get('vcs.server')
613 613 enabled = rhodecode.CONFIG.get('vcs.server.enable')
614 614 protocol = rhodecode.CONFIG.get('vcs.server.protocol') or 'http'
615 615 state = STATE_OK_DEFAULT
616 616 version = None
617 617 workers = 0
618 618
619 619 try:
620 620 data = get_vcsserver_service_data()
621 621 if data and 'version' in data:
622 622 version = data['version']
623 623
624 624 if data and 'config' in data:
625 625 conf = data['config']
626 626 workers = conf.get('workers', 'NOT AVAILABLE')
627 627
628 628 connection = 'connected'
629 629 except Exception as e:
630 630 connection = 'failed'
631 631 state = {'message': str(e), 'type': STATE_ERR}
632 632
633 633 value = dict(
634 634 url=server_url,
635 635 enabled=enabled,
636 636 protocol=protocol,
637 637 connection=connection,
638 638 version=version,
639 639 text='',
640 640 )
641 641
642 642 human_value = value.copy()
643 643 human_value['text'] = \
644 644 '{url}@ver:{ver} via {mode} mode[workers:{workers}], connection:{conn}'.format(
645 645 url=server_url, ver=version, workers=workers, mode=protocol,
646 646 conn=connection)
647 647
648 648 return SysInfoRes(value=value, state=state, human_value=human_value)
649 649
650 650
651 651 @register_sysinfo
652 652 def vcs_server_config():
653 653 from rhodecode.lib.vcs.backends import get_vcsserver_service_data
654 654 state = STATE_OK_DEFAULT
655 655
656 656 value = {}
657 657 try:
658 658 data = get_vcsserver_service_data()
659 659 value = data['app_config']
660 660 except Exception as e:
661 661 state = {'message': str(e), 'type': STATE_ERR}
662 662
663 663 human_value = value.copy()
664 664 human_value['text'] = 'VCS Server config'
665 665
666 666 return SysInfoRes(value=value, state=state, human_value=human_value)
667 667
668 @register_sysinfo
669 def rhodecode_server_config():
670 import rhodecode
671
672 state = STATE_OK_DEFAULT
673 config = rhodecode.CONFIG.copy()
674
675 secrets_lits = [
676 f'rhodecode_{LicenseModel.LICENSE_DB_KEY}',
677 'sqlalchemy.db1.url',
678 'channelstream.secret',
679 'beaker.session.secret',
680 'rhodecode.encrypted_values.secret',
681 'appenlight.api_key',
682 'smtp_password',
683 'file_store.objectstore.secret',
684 'archive_cache.objectstore.secret',
685 'app.service_api.token',
686 ]
687 for k in secrets_lits:
688 if k in config:
689 config[k] = '**OBFUSCATED**'
690
691 value = human_value = config
692 return SysInfoRes(value=value, state=state, human_value=human_value)
693
668 694
669 695 @register_sysinfo
670 696 def rhodecode_app_info():
671 697 import rhodecode
672 698 edition = rhodecode.CONFIG.get('rhodecode.edition')
673 699
674 700 value = dict(
675 701 rhodecode_version=rhodecode.__version__,
676 702 rhodecode_lib_path=os.path.abspath(rhodecode.__file__),
677 703 text=''
678 704 )
679 705 human_value = value.copy()
680 706 human_value['text'] = 'RhodeCode {edition}, version {ver}'.format(
681 707 edition=edition, ver=value['rhodecode_version']
682 708 )
683 709 return SysInfoRes(value=value, human_value=human_value)
684 710
685 711
686 712 @register_sysinfo
687 713 def rhodecode_config():
688 714 import rhodecode
689 715 path = rhodecode.CONFIG.get('__file__')
690 716 rhodecode_ini_safe = rhodecode.CONFIG.copy()
691 717 cert_path = get_cert_path(path)
692 718
693 719 try:
694 720 config = configparser.ConfigParser()
695 721 config.read(path)
696 722 parsed_ini = config
697 723 if parsed_ini.has_section('server:main'):
698 724 parsed_ini = dict(parsed_ini.items('server:main'))
699 725 except Exception:
700 726 log.exception('Failed to read .ini file for display')
701 727 parsed_ini = {}
702 728
703 729 rhodecode_ini_safe['server:main'] = parsed_ini
704 730
705 731 blacklist = [
706 732 f'rhodecode_{LicenseModel.LICENSE_DB_KEY}',
707 733 'routes.map',
708 734 'sqlalchemy.db1.url',
709 735 'channelstream.secret',
710 736 'beaker.session.secret',
711 737 'rhodecode.encrypted_values.secret',
712 738 'rhodecode_auth_github_consumer_key',
713 739 'rhodecode_auth_github_consumer_secret',
714 740 'rhodecode_auth_google_consumer_key',
715 741 'rhodecode_auth_google_consumer_secret',
716 742 'rhodecode_auth_bitbucket_consumer_secret',
717 743 'rhodecode_auth_bitbucket_consumer_key',
718 744 'rhodecode_auth_twitter_consumer_secret',
719 745 'rhodecode_auth_twitter_consumer_key',
720 746
721 747 'rhodecode_auth_twitter_secret',
722 748 'rhodecode_auth_github_secret',
723 749 'rhodecode_auth_google_secret',
724 750 'rhodecode_auth_bitbucket_secret',
725 751
726 752 'appenlight.api_key',
727 753 ('app_conf', 'sqlalchemy.db1.url')
728 754 ]
729 755 for k in blacklist:
730 756 if isinstance(k, tuple):
731 757 section, key = k
732 758 if section in rhodecode_ini_safe:
733 759 rhodecode_ini_safe[section] = '**OBFUSCATED**'
734 760 else:
735 761 rhodecode_ini_safe.pop(k, None)
736 762
737 763 # TODO: maybe put some CONFIG checks here ?
738 764 return SysInfoRes(value={'config': rhodecode_ini_safe,
739 765 'path': path, 'cert_path': cert_path})
740 766
741 767
742 768 @register_sysinfo
743 769 def database_info():
744 770 import rhodecode
745 771 from sqlalchemy.engine import url as engine_url
746 772 from rhodecode.model import meta
747 773 from rhodecode.model.meta import Session
748 774 from rhodecode.model.db import DbMigrateVersion
749 775
750 776 state = STATE_OK_DEFAULT
751 777
752 778 db_migrate = DbMigrateVersion.query().filter(
753 779 DbMigrateVersion.repository_id == 'rhodecode_db_migrations').one()
754 780
755 781 db_url_obj = engine_url.make_url(rhodecode.CONFIG['sqlalchemy.db1.url'])
756 782
757 783 try:
758 784 engine = meta.get_engine()
759 785 db_server_info = engine.dialect._get_server_version_info(
760 786 Session.connection(bind=engine))
761 787 db_version = '.'.join(map(str, db_server_info))
762 788 except Exception:
763 789 log.exception('failed to fetch db version')
764 790 db_version = 'UNKNOWN'
765 791
766 792 db_info = dict(
767 793 migrate_version=db_migrate.version,
768 794 type=db_url_obj.get_backend_name(),
769 795 version=db_version,
770 796 url=repr(db_url_obj)
771 797 )
772 798 current_version = db_migrate.version
773 799 expected_version = rhodecode.__dbversion__
774 800 if state['type'] == STATE_OK and current_version != expected_version:
775 801 msg = 'Critical: database schema mismatch, ' \
776 802 'expected version {}, got {}. ' \
777 803 'Please run migrations on your database.'.format(
778 804 expected_version, current_version)
779 805 state = {'message': msg, 'type': STATE_ERR}
780 806
781 807 human_value = db_info.copy()
782 808 human_value['url'] = "{} @ migration version: {}".format(
783 809 db_info['url'], db_info['migrate_version'])
784 810 human_value['version'] = "{} {}".format(db_info['type'], db_info['version'])
785 811 return SysInfoRes(value=db_info, state=state, human_value=human_value)
786 812
787 813
788 814 @register_sysinfo
789 815 def server_info(environ):
790 816 import rhodecode
791 817 from rhodecode.lib.base import get_server_ip_addr, get_server_port
792 818
793 819 value = {
794 820 'server_ip': '{}:{}'.format(
795 821 get_server_ip_addr(environ, log_errors=False),
796 822 get_server_port(environ)
797 823 ),
798 824 'server_id': rhodecode.CONFIG.get('instance_id'),
799 825 }
800 826 return SysInfoRes(value=value)
801 827
802 828
803 829 @register_sysinfo
804 830 def usage_info():
805 831 from rhodecode.model.db import User, Repository, true
806 832 value = {
807 833 'users': User.query().count(),
808 834 'users_active': User.query().filter(User.active == true()).count(),
809 835 'repositories': Repository.query().count(),
810 836 'repository_types': {
811 837 'hg': Repository.query().filter(
812 838 Repository.repo_type == 'hg').count(),
813 839 'git': Repository.query().filter(
814 840 Repository.repo_type == 'git').count(),
815 841 'svn': Repository.query().filter(
816 842 Repository.repo_type == 'svn').count(),
817 843 },
818 844 }
819 845 return SysInfoRes(value=value)
820 846
821 847
822 848 def get_system_info(environ):
823 849 environ = environ or {}
824 850 return {
825 851 'rhodecode_app': SysInfo(rhodecode_app_info)(),
826 852 'rhodecode_config': SysInfo(rhodecode_config)(),
827 853 'rhodecode_usage': SysInfo(usage_info)(),
828 854 'python': SysInfo(python_info)(),
829 855 'py_modules': SysInfo(py_modules)(),
830 856
831 857 'platform': SysInfo(platform_type)(),
832 858 'locale': SysInfo(locale_info)(),
833 859 'server': SysInfo(server_info, environ=environ)(),
834 860 'database': SysInfo(database_info)(),
835 861 'ulimit': SysInfo(ulimit_info)(),
836 862 'storage': SysInfo(storage)(),
837 863 'storage_inodes': SysInfo(storage_inodes)(),
838 864 'storage_archive': SysInfo(storage_archives)(),
839 865 'storage_artifacts': SysInfo(storage_artifacts)(),
840 866 'storage_gist': SysInfo(storage_gist)(),
841 867 'storage_temp': SysInfo(storage_temp)(),
842 868
843 869 'search': SysInfo(search_info)(),
844 870
845 871 'uptime': SysInfo(uptime)(),
846 872 'load': SysInfo(machine_load)(),
847 873 'cpu': SysInfo(cpu)(),
848 874 'memory': SysInfo(memory)(),
849 875
850 876 'vcs_backends': SysInfo(vcs_backends)(),
851 877 'vcs_server': SysInfo(vcs_server)(),
852 878
853 879 'vcs_server_config': SysInfo(vcs_server_config)(),
880 'rhodecode_server_config': SysInfo(rhodecode_server_config)(),
854 881
855 882 'git': SysInfo(git_info)(),
856 883 'hg': SysInfo(hg_info)(),
857 884 'svn': SysInfo(svn_info)(),
858 885 }
859 886
860 887
861 888 def load_system_info(key):
862 889 """
863 890 get_sys_info('vcs_server')
864 891 get_sys_info('database')
865 892 """
866 893 return SysInfo(registered_helpers[key])()
@@ -1,89 +1,103 b''
1 1
2 2 <div id="update_notice" style="display: none; margin: 0px 0px 30px 0px">
3 3 <div>${_('Checking for updates...')}</div>
4 4 </div>
5 5
6 6
7 7 <div class="panel panel-default">
8 8 <div class="panel-heading">
9 9 <h3 class="panel-title">${_('System Info')}</h3>
10 10 % if c.allowed_to_snapshot:
11 11 <a href="${h.route_path('admin_settings_system', _query={'snapshot':1})}" class="panel-edit">${_('create summary snapshot')}</a>
12 12 % endif
13 13 </div>
14 14 <div class="panel-body">
15 15 <dl class="dl-horizontal settings dt-400">
16 16 % for dt, dd, warn in c.data_items:
17 17 <dt>${dt}${':' if dt else '---'}</dt>
18 18 <dd>${dd}${'' if dt else '---'}
19 19 % if warn and warn['message']:
20 20 <div class="alert-${warn['type']}">
21 21 <strong>${warn['message']}</strong>
22 22 </div>
23 23 % endif
24 24 </dd>
25 25 % endfor
26 26 </dl>
27 27 </div>
28 28 </div>
29 29
30 30 <div class="panel panel-default">
31 31 <div class="panel-heading">
32 <h3 class="panel-title">${_('VCS Server')}</h3>
32 <h3 class="panel-title">${_('RhodeCode Server Config')}</h3>
33 </div>
34 <div class="panel-body">
35 <dl class="dl-horizontal settings dt-400">
36 % for dt, dd in c.rhodecode_data_items:
37 <dt>${dt}${':' if dt else '---'}</dt>
38 <dd>${dd}${'' if dt else '---'}</dd>
39 % endfor
40 </dl>
41 </div>
42 </div>
43
44 <div class="panel panel-default">
45 <div class="panel-heading">
46 <h3 class="panel-title">${_('VCS Server Config')}</h3>
33 47 </div>
34 48 <div class="panel-body">
35 49 <dl class="dl-horizontal settings dt-400">
36 50 % for dt, dd in c.vcsserver_data_items:
37 51 <dt>${dt}${':' if dt else '---'}</dt>
38 52 <dd>${dd}${'' if dt else '---'}</dd>
39 53 % endfor
40 54 </dl>
41 55 </div>
42 56 </div>
43 57
44 58 <div class="panel panel-default">
45 59 <div class="panel-heading">
46 60 <h3 class="panel-title">${_('Python Packages')}</h3>
47 61 </div>
48 62 <div class="panel-body">
49 63 <table>
50 64 <th></th>
51 65 <th></th>
52 66 <th></th>
53 67 % for name, package_data in c.py_modules['human_value']:
54 68 <tr>
55 69 <td>${name.lower()}</td>
56 70 <td>${package_data['version']}</td>
57 71 <td>(${package_data['location']})</td>
58 72 </tr>
59 73 % endfor
60 74 </table>
61 75
62 76 </div>
63 77 </div>
64 78
65 79 <div class="panel panel-default">
66 80 <div class="panel-heading">
67 81 <h3 class="panel-title">${_('Env Variables')}</h3>
68 82 </div>
69 83 <div class="panel-body">
70 84 <table>
71 85 <th></th>
72 86 <th></th>
73 87 % for env_key, env_val in c.env_data:
74 88 <tr>
75 89 <td style="vertical-align: top">${env_key}</td>
76 90 <td>${env_val}</td>
77 91 </tr>
78 92 % endfor
79 93 </table>
80 94
81 95 </div>
82 96 </div>
83 97
84 98 <script>
85 99 $('#check_for_update').click(function(e){
86 100 $('#update_notice').show();
87 101 $('#update_notice').load("${h.route_path('admin_settings_system_update', _query={'ver': request.GET.get('ver')})}");
88 102 })
89 103 </script>
General Comments 0
You need to be logged in to leave comments. Login now