system_info.py
639 lines
| 19.6 KiB
| text/x-python
|
PythonLexer
r1111 | import os | |||
import sys | ||||
import time | ||||
import platform | ||||
import pkg_resources | ||||
import logging | ||||
r1112 | import string | |||
r1111 | ||||
log = logging.getLogger(__name__) | ||||
psutil = None | ||||
try: | ||||
# cygwin cannot have yet psutil support. | ||||
import psutil as psutil | ||||
except ImportError: | ||||
pass | ||||
_NA = 'NOT AVAILABLE' | ||||
STATE_OK = 'ok' | ||||
STATE_ERR = 'error' | ||||
STATE_WARN = 'warning' | ||||
STATE_OK_DEFAULT = {'message': '', 'type': STATE_OK} | ||||
# HELPERS | ||||
def percentage(part, whole): | ||||
whole = float(whole) | ||||
if whole > 0: | ||||
r1155 | return round(100 * float(part) / whole, 1) | |||
return 0.0 | ||||
r1111 | ||||
def get_storage_size(storage_path): | ||||
sizes = [] | ||||
for file_ in os.listdir(storage_path): | ||||
storage_file = os.path.join(storage_path, file_) | ||||
if os.path.isfile(storage_file): | ||||
try: | ||||
sizes.append(os.path.getsize(storage_file)) | ||||
except OSError: | ||||
log.exception('Failed to get size of storage file %s', | ||||
storage_file) | ||||
pass | ||||
return sum(sizes) | ||||
class SysInfoRes(object): | ||||
def __init__(self, value, state=STATE_OK_DEFAULT, human_value=None): | ||||
self.value = value | ||||
self.state = state | ||||
self.human_value = human_value or value | ||||
def __json__(self): | ||||
return { | ||||
'value': self.value, | ||||
'state': self.state, | ||||
'human_value': self.human_value, | ||||
} | ||||
def __str__(self): | ||||
return '<SysInfoRes({})>'.format(self.__json__()) | ||||
class SysInfo(object): | ||||
def __init__(self, func_name, **kwargs): | ||||
self.func_name = func_name | ||||
self.value = _NA | ||||
self.state = None | ||||
self.kwargs = kwargs or {} | ||||
def __call__(self): | ||||
computed = self.compute(**self.kwargs) | ||||
if not isinstance(computed, SysInfoRes): | ||||
raise ValueError( | ||||
'computed value for {} is not instance of ' | ||||
'{}, got {} instead'.format( | ||||
self.func_name, SysInfoRes, type(computed))) | ||||
return computed.__json__() | ||||
def __str__(self): | ||||
return '<SysInfo({})>'.format(self.func_name) | ||||
def compute(self, **kwargs): | ||||
return self.func_name(**kwargs) | ||||
# SysInfo functions | ||||
def python_info(): | ||||
r1112 | value = dict(version=' '.join(platform._sys_version()), | |||
executable=sys.executable) | ||||
r1111 | return SysInfoRes(value=value) | |||
def py_modules(): | ||||
mods = dict([(p.project_name, p.version) | ||||
for p in pkg_resources.working_set]) | ||||
value = sorted(mods.items(), key=lambda k: k[0].lower()) | ||||
return SysInfoRes(value=value) | ||||
def platform_type(): | ||||
r1115 | from rhodecode.lib.utils import safe_unicode, generate_platform_uuid | |||
value = dict( | ||||
name=safe_unicode(platform.platform()), | ||||
uuid=generate_platform_uuid() | ||||
) | ||||
r1111 | return SysInfoRes(value=value) | |||
def uptime(): | ||||
from rhodecode.lib.helpers import age, time_to_datetime | ||||
r1112 | value = dict(boot_time=0, uptime=0, text='') | |||
r1111 | state = STATE_OK_DEFAULT | |||
if not psutil: | ||||
r1112 | return SysInfoRes(value=value, state=state) | |||
r1111 | ||||
boot_time = psutil.boot_time() | ||||
r1112 | value['boot_time'] = boot_time | |||
value['uptime'] = time.time() - boot_time | ||||
r1111 | ||||
r1112 | human_value = value.copy() | |||
human_value['boot_time'] = time_to_datetime(boot_time) | ||||
human_value['uptime'] = age(time_to_datetime(boot_time), show_suffix=False) | ||||
human_value['text'] = 'Server started {}'.format( | ||||
r1111 | age(time_to_datetime(boot_time))) | |||
r1112 | return SysInfoRes(value=value, human_value=human_value) | |||
r1111 | ||||
def memory(): | ||||
from rhodecode.lib.helpers import format_byte_size_binary | ||||
r1116 | value = dict(available=0, used=0, used_real=0, cached=0, percent=0, | |||
percent_used=0, free=0, inactive=0, active=0, shared=0, | ||||
total=0, buffers=0, text='') | ||||
r1112 | ||||
r1111 | state = STATE_OK_DEFAULT | |||
if not psutil: | ||||
r1112 | return SysInfoRes(value=value, state=state) | |||
r1111 | ||||
r1112 | value.update(dict(psutil.virtual_memory()._asdict())) | |||
r1116 | value['used_real'] = value['total'] - value['available'] | |||
r1112 | value['percent_used'] = psutil._common.usage_percent( | |||
r1116 | value['used_real'], value['total'], 1) | |||
r1111 | ||||
r1112 | human_value = value.copy() | |||
human_value['text'] = '%s/%s, %s%% used' % ( | ||||
r1116 | format_byte_size_binary(value['used_real']), | |||
r1112 | format_byte_size_binary(value['total']), | |||
value['percent_used'],) | ||||
r1111 | ||||
r1112 | keys = value.keys()[::] | |||
keys.pop(keys.index('percent')) | ||||
keys.pop(keys.index('percent_used')) | ||||
keys.pop(keys.index('text')) | ||||
for k in keys: | ||||
human_value[k] = format_byte_size_binary(value[k]) | ||||
if state['type'] == STATE_OK and value['percent_used'] > 90: | ||||
r1111 | msg = 'Critical: your available RAM memory is very low.' | |||
state = {'message': msg, 'type': STATE_ERR} | ||||
r1112 | elif state['type'] == STATE_OK and value['percent_used'] > 70: | |||
r1111 | msg = 'Warning: your available RAM memory is running low.' | |||
state = {'message': msg, 'type': STATE_WARN} | ||||
r1112 | return SysInfoRes(value=value, state=state, human_value=human_value) | |||
r1111 | ||||
def machine_load(): | ||||
r1112 | value = {'1_min': _NA, '5_min': _NA, '15_min': _NA, 'text': ''} | |||
r1111 | state = STATE_OK_DEFAULT | |||
if not psutil: | ||||
r1112 | return SysInfoRes(value=value, state=state) | |||
r1111 | ||||
# load averages | ||||
if hasattr(psutil.os, 'getloadavg'): | ||||
r1112 | value.update(dict( | |||
zip(['1_min', '5_min', '15_min'], psutil.os.getloadavg()))) | ||||
r1111 | ||||
r1112 | human_value = value.copy() | |||
human_value['text'] = '1min: {}, 5min: {}, 15min: {}'.format( | ||||
value['1_min'], value['5_min'], value['15_min']) | ||||
r1111 | ||||
r1112 | if state['type'] == STATE_OK and value['15_min'] > 5: | |||
msg = 'Warning: your machine load is very high.' | ||||
state = {'message': msg, 'type': STATE_WARN} | ||||
return SysInfoRes(value=value, state=state, human_value=human_value) | ||||
r1111 | ||||
def cpu(): | ||||
r1112 | value = 0 | |||
r1111 | state = STATE_OK_DEFAULT | |||
r1112 | ||||
r1111 | if not psutil: | |||
r1112 | return SysInfoRes(value=value, state=state) | |||
r1111 | ||||
r1112 | value = psutil.cpu_percent(0.5) | |||
human_value = '{} %'.format(value) | ||||
return SysInfoRes(value=value, state=state, human_value=human_value) | ||||
r1111 | ||||
def storage(): | ||||
from rhodecode.lib.helpers import format_byte_size_binary | ||||
from rhodecode.model.settings import VcsSettingsModel | ||||
path = VcsSettingsModel().get_repos_location() | ||||
r1112 | value = dict(percent=0, used=0, total=0, path=path, text='') | |||
r1111 | state = STATE_OK_DEFAULT | |||
if not psutil: | ||||
r1112 | return SysInfoRes(value=value, state=state) | |||
r1111 | ||||
try: | ||||
r1112 | value.update(dict(psutil.disk_usage(path)._asdict())) | |||
r1111 | except Exception as e: | |||
log.exception('Failed to fetch disk info') | ||||
state = {'message': str(e), 'type': STATE_ERR} | ||||
r1112 | human_value = value.copy() | |||
human_value['used'] = format_byte_size_binary(value['used']) | ||||
human_value['total'] = format_byte_size_binary(value['total']) | ||||
r1111 | human_value['text'] = "{}/{}, {}% used".format( | |||
r1112 | format_byte_size_binary(value['used']), | |||
format_byte_size_binary(value['total']), | ||||
value['percent']) | ||||
r1111 | ||||
r1112 | if state['type'] == STATE_OK and value['percent'] > 90: | |||
r1111 | msg = 'Critical: your disk space is very low.' | |||
state = {'message': msg, 'type': STATE_ERR} | ||||
r1112 | elif state['type'] == STATE_OK and value['percent'] > 70: | |||
r1111 | msg = 'Warning: your disk space is running low.' | |||
state = {'message': msg, 'type': STATE_WARN} | ||||
r1112 | return SysInfoRes(value=value, state=state, human_value=human_value) | |||
r1111 | ||||
def storage_inodes(): | ||||
from rhodecode.model.settings import VcsSettingsModel | ||||
path = VcsSettingsModel().get_repos_location() | ||||
r1112 | value = dict(percent=0, free=0, used=0, total=0, path=path, text='') | |||
r1111 | state = STATE_OK_DEFAULT | |||
if not psutil: | ||||
r1112 | return SysInfoRes(value=value, state=state) | |||
r1111 | ||||
try: | ||||
i_stat = os.statvfs(path) | ||||
r1219 | value['free'] = i_stat.f_ffree | |||
value['used'] = i_stat.f_files-i_stat.f_favail | ||||
r1112 | value['total'] = i_stat.f_files | |||
r1155 | value['percent'] = percentage(value['used'], value['total']) | |||
r1111 | except Exception as e: | |||
log.exception('Failed to fetch disk inodes info') | ||||
state = {'message': str(e), 'type': STATE_ERR} | ||||
r1112 | human_value = value.copy() | |||
r1111 | human_value['text'] = "{}/{}, {}% used".format( | |||
r1112 | value['used'], value['total'], value['percent']) | |||
r1111 | ||||
r1112 | if state['type'] == STATE_OK and value['percent'] > 90: | |||
r1111 | msg = 'Critical: your disk free inodes are very low.' | |||
state = {'message': msg, 'type': STATE_ERR} | ||||
r1112 | elif state['type'] == STATE_OK and value['percent'] > 70: | |||
r1111 | msg = 'Warning: your disk free inodes are running low.' | |||
state = {'message': msg, 'type': STATE_WARN} | ||||
r1155 | return SysInfoRes(value=value, state=state, human_value=human_value) | |||
r1111 | ||||
def storage_archives(): | ||||
import rhodecode | ||||
from rhodecode.lib.utils import safe_str | ||||
from rhodecode.lib.helpers import format_byte_size_binary | ||||
msg = 'Enable this by setting ' \ | ||||
'archive_cache_dir=/path/to/cache option in the .ini file' | ||||
path = safe_str(rhodecode.CONFIG.get('archive_cache_dir', msg)) | ||||
r1112 | value = dict(percent=0, used=0, total=0, items=0, path=path, text='') | |||
r1111 | state = STATE_OK_DEFAULT | |||
try: | ||||
items_count = 0 | ||||
used = 0 | ||||
for root, dirs, files in os.walk(path): | ||||
if root == path: | ||||
r1112 | items_count = len(files) | |||
r1111 | ||||
for f in files: | ||||
try: | ||||
used += os.path.getsize(os.path.join(root, f)) | ||||
except OSError: | ||||
pass | ||||
r1112 | value.update({ | |||
r1111 | 'percent': 100, | |||
'used': used, | ||||
'total': used, | ||||
'items': items_count | ||||
}) | ||||
except Exception as e: | ||||
log.exception('failed to fetch archive cache storage') | ||||
state = {'message': str(e), 'type': STATE_ERR} | ||||
r1112 | human_value = value.copy() | |||
human_value['used'] = format_byte_size_binary(value['used']) | ||||
human_value['total'] = format_byte_size_binary(value['total']) | ||||
human_value['text'] = "{} ({} items)".format( | ||||
human_value['used'], value['items']) | ||||
r1111 | ||||
r1112 | return SysInfoRes(value=value, state=state, human_value=human_value) | |||
r1111 | ||||
def storage_gist(): | ||||
from rhodecode.model.gist import GIST_STORE_LOC | ||||
from rhodecode.model.settings import VcsSettingsModel | ||||
from rhodecode.lib.utils import safe_str | ||||
from rhodecode.lib.helpers import format_byte_size_binary | ||||
path = safe_str(os.path.join( | ||||
VcsSettingsModel().get_repos_location(), GIST_STORE_LOC)) | ||||
# gist storage | ||||
r1112 | value = dict(percent=0, used=0, total=0, items=0, path=path, text='') | |||
r1111 | state = STATE_OK_DEFAULT | |||
try: | ||||
items_count = 0 | ||||
used = 0 | ||||
for root, dirs, files in os.walk(path): | ||||
if root == path: | ||||
items_count = len(dirs) | ||||
for f in files: | ||||
try: | ||||
used += os.path.getsize(os.path.join(root, f)) | ||||
except OSError: | ||||
pass | ||||
r1112 | value.update({ | |||
r1111 | 'percent': 100, | |||
'used': used, | ||||
'total': used, | ||||
'items': items_count | ||||
}) | ||||
except Exception as e: | ||||
log.exception('failed to fetch gist storage items') | ||||
state = {'message': str(e), 'type': STATE_ERR} | ||||
r1112 | human_value = value.copy() | |||
human_value['used'] = format_byte_size_binary(value['used']) | ||||
human_value['total'] = format_byte_size_binary(value['total']) | ||||
human_value['text'] = "{} ({} items)".format( | ||||
human_value['used'], value['items']) | ||||
return SysInfoRes(value=value, state=state, human_value=human_value) | ||||
r1111 | ||||
r1124 | def storage_temp(): | |||
import tempfile | ||||
from rhodecode.lib.helpers import format_byte_size_binary | ||||
path = tempfile.gettempdir() | ||||
value = dict(percent=0, used=0, total=0, items=0, path=path, text='') | ||||
state = STATE_OK_DEFAULT | ||||
if not psutil: | ||||
return SysInfoRes(value=value, state=state) | ||||
try: | ||||
value.update(dict(psutil.disk_usage(path)._asdict())) | ||||
except Exception as e: | ||||
log.exception('Failed to fetch temp dir info') | ||||
state = {'message': str(e), 'type': STATE_ERR} | ||||
human_value = value.copy() | ||||
human_value['used'] = format_byte_size_binary(value['used']) | ||||
human_value['total'] = format_byte_size_binary(value['total']) | ||||
human_value['text'] = "{}/{}, {}% used".format( | ||||
format_byte_size_binary(value['used']), | ||||
format_byte_size_binary(value['total']), | ||||
value['percent']) | ||||
return SysInfoRes(value=value, state=state, human_value=human_value) | ||||
r1112 | def search_info(): | |||
r1111 | import rhodecode | |||
r1112 | from rhodecode.lib.index import searcher_from_config | |||
r1111 | ||||
r1112 | backend = rhodecode.CONFIG.get('search.module', '') | |||
location = rhodecode.CONFIG.get('search.location', '') | ||||
r1111 | try: | |||
r1112 | searcher = searcher_from_config(rhodecode.CONFIG) | |||
searcher = searcher.__class__.__name__ | ||||
except Exception: | ||||
searcher = None | ||||
r1111 | ||||
r1112 | value = dict( | |||
backend=backend, searcher=searcher, location=location, text='') | ||||
state = STATE_OK_DEFAULT | ||||
r1111 | ||||
r1112 | human_value = value.copy() | |||
human_value['text'] = "backend:`{}`".format(human_value['backend']) | ||||
return SysInfoRes(value=value, state=state, human_value=human_value) | ||||
r1111 | ||||
def git_info(): | ||||
from rhodecode.lib.vcs.backends import git | ||||
state = STATE_OK_DEFAULT | ||||
value = human_value = '' | ||||
try: | ||||
value = git.discover_git_version(raise_on_exc=True) | ||||
human_value = 'version reported from VCSServer: {}'.format(value) | ||||
except Exception as e: | ||||
state = {'message': str(e), 'type': STATE_ERR} | ||||
return SysInfoRes(value=value, state=state, human_value=human_value) | ||||
def hg_info(): | ||||
from rhodecode.lib.vcs.backends import hg | ||||
state = STATE_OK_DEFAULT | ||||
value = human_value = '' | ||||
try: | ||||
value = hg.discover_hg_version(raise_on_exc=True) | ||||
human_value = 'version reported from VCSServer: {}'.format(value) | ||||
except Exception as e: | ||||
state = {'message': str(e), 'type': STATE_ERR} | ||||
return SysInfoRes(value=value, state=state, human_value=human_value) | ||||
def svn_info(): | ||||
from rhodecode.lib.vcs.backends import svn | ||||
state = STATE_OK_DEFAULT | ||||
value = human_value = '' | ||||
try: | ||||
value = svn.discover_svn_version(raise_on_exc=True) | ||||
human_value = 'version reported from VCSServer: {}'.format(value) | ||||
except Exception as e: | ||||
state = {'message': str(e), 'type': STATE_ERR} | ||||
return SysInfoRes(value=value, state=state, human_value=human_value) | ||||
def vcs_backends(): | ||||
import rhodecode | ||||
r1112 | value = map( | |||
string.strip, rhodecode.CONFIG.get('vcs.backends', '').split(',')) | ||||
r1111 | human_value = 'Enabled backends in order: {}'.format(','.join(value)) | |||
return SysInfoRes(value=value, human_value=human_value) | ||||
def vcs_server(): | ||||
import rhodecode | ||||
from rhodecode.lib.vcs.backends import get_vcsserver_version | ||||
server_url = rhodecode.CONFIG.get('vcs.server') | ||||
enabled = rhodecode.CONFIG.get('vcs.server.enable') | ||||
r1182 | protocol = rhodecode.CONFIG.get('vcs.server.protocol') or 'http' | |||
r1111 | state = STATE_OK_DEFAULT | |||
version = None | ||||
try: | ||||
version = get_vcsserver_version() | ||||
connection = 'connected' | ||||
except Exception as e: | ||||
connection = 'failed' | ||||
state = {'message': str(e), 'type': STATE_ERR} | ||||
value = dict( | ||||
url=server_url, | ||||
enabled=enabled, | ||||
protocol=protocol, | ||||
connection=connection, | ||||
version=version, | ||||
text='', | ||||
) | ||||
r1112 | human_value = value.copy() | |||
r1111 | human_value['text'] = \ | |||
'{url}@ver:{ver} via {mode} mode, connection:{conn}'.format( | ||||
url=server_url, ver=version, mode=protocol, conn=connection) | ||||
r1112 | return SysInfoRes(value=value, state=state, human_value=human_value) | |||
r1111 | ||||
def rhodecode_app_info(): | ||||
import rhodecode | ||||
r1113 | edition = rhodecode.CONFIG.get('rhodecode.edition') | |||
r1112 | value = dict( | |||
rhodecode_version=rhodecode.__version__, | ||||
r1113 | rhodecode_lib_path=os.path.abspath(rhodecode.__file__), | |||
text='' | ||||
r1112 | ) | |||
r1113 | human_value = value.copy() | |||
human_value['text'] = 'RhodeCode {edition}, version {ver}'.format( | ||||
edition=edition, ver=value['rhodecode_version'] | ||||
) | ||||
return SysInfoRes(value=value, human_value=human_value) | ||||
r1111 | ||||
def rhodecode_config(): | ||||
import rhodecode | ||||
path = rhodecode.CONFIG.get('__file__') | ||||
rhodecode_ini_safe = rhodecode.CONFIG.copy() | ||||
blacklist = [ | ||||
'rhodecode_license_key', | ||||
'routes.map', | ||||
'pylons.h', | ||||
'pylons.app_globals', | ||||
'pylons.environ_config', | ||||
'sqlalchemy.db1.url', | ||||
'channelstream.secret', | ||||
'beaker.session.secret', | ||||
'rhodecode.encrypted_values.secret', | ||||
'rhodecode_auth_github_consumer_key', | ||||
'rhodecode_auth_github_consumer_secret', | ||||
'rhodecode_auth_google_consumer_key', | ||||
'rhodecode_auth_google_consumer_secret', | ||||
'rhodecode_auth_bitbucket_consumer_secret', | ||||
'rhodecode_auth_bitbucket_consumer_key', | ||||
'rhodecode_auth_twitter_consumer_secret', | ||||
'rhodecode_auth_twitter_consumer_key', | ||||
'rhodecode_auth_twitter_secret', | ||||
'rhodecode_auth_github_secret', | ||||
'rhodecode_auth_google_secret', | ||||
'rhodecode_auth_bitbucket_secret', | ||||
'appenlight.api_key', | ||||
('app_conf', 'sqlalchemy.db1.url') | ||||
] | ||||
for k in blacklist: | ||||
if isinstance(k, tuple): | ||||
section, key = k | ||||
if section in rhodecode_ini_safe: | ||||
rhodecode_ini_safe[section] = '**OBFUSCATED**' | ||||
else: | ||||
rhodecode_ini_safe.pop(k, None) | ||||
# TODO: maybe put some CONFIG checks here ? | ||||
return SysInfoRes(value={'config': rhodecode_ini_safe, 'path': path}) | ||||
def database_info(): | ||||
import rhodecode | ||||
from sqlalchemy.engine import url as engine_url | ||||
from rhodecode.model.meta import Base as sql_base, Session | ||||
from rhodecode.model.db import DbMigrateVersion | ||||
state = STATE_OK_DEFAULT | ||||
db_migrate = DbMigrateVersion.query().filter( | ||||
DbMigrateVersion.repository_id == 'rhodecode_db_migrations').one() | ||||
db_url_obj = engine_url.make_url(rhodecode.CONFIG['sqlalchemy.db1.url']) | ||||
try: | ||||
engine = sql_base.metadata.bind | ||||
db_server_info = engine.dialect._get_server_version_info( | ||||
Session.connection(bind=engine)) | ||||
db_version = '.'.join(map(str, db_server_info)) | ||||
except Exception: | ||||
log.exception('failed to fetch db version') | ||||
db_version = 'UNKNOWN' | ||||
db_info = dict( | ||||
migrate_version=db_migrate.version, | ||||
type=db_url_obj.get_backend_name(), | ||||
version=db_version, | ||||
url=repr(db_url_obj) | ||||
) | ||||
r1112 | human_value = db_info.copy() | |||
r1111 | human_value['url'] = "{} @ migration version: {}".format( | |||
db_info['url'], db_info['migrate_version']) | ||||
human_value['version'] = "{} {}".format(db_info['type'], db_info['version']) | ||||
return SysInfoRes(value=db_info, state=state, human_value=human_value) | ||||
def server_info(environ): | ||||
import rhodecode | ||||
from rhodecode.lib.base import get_server_ip_addr, get_server_port | ||||
value = { | ||||
'server_ip': '%s:%s' % ( | ||||
get_server_ip_addr(environ, log_errors=False), | ||||
get_server_port(environ) | ||||
), | ||||
'server_id': rhodecode.CONFIG.get('instance_id'), | ||||
} | ||||
return SysInfoRes(value=value) | ||||
def get_system_info(environ): | ||||
environ = environ or {} | ||||
return { | ||||
'rhodecode_app': SysInfo(rhodecode_app_info)(), | ||||
'rhodecode_config': SysInfo(rhodecode_config)(), | ||||
'python': SysInfo(python_info)(), | ||||
'py_modules': SysInfo(py_modules)(), | ||||
'platform': SysInfo(platform_type)(), | ||||
'server': SysInfo(server_info, environ=environ)(), | ||||
'database': SysInfo(database_info)(), | ||||
'storage': SysInfo(storage)(), | ||||
'storage_inodes': SysInfo(storage_inodes)(), | ||||
'storage_archive': SysInfo(storage_archives)(), | ||||
'storage_gist': SysInfo(storage_gist)(), | ||||
r1124 | 'storage_temp': SysInfo(storage_temp)(), | |||
r1111 | ||||
r1112 | 'search': SysInfo(search_info)(), | |||
r1111 | 'uptime': SysInfo(uptime)(), | |||
'load': SysInfo(machine_load)(), | ||||
'cpu': SysInfo(cpu)(), | ||||
'memory': SysInfo(memory)(), | ||||
'vcs_backends': SysInfo(vcs_backends)(), | ||||
'vcs_server': SysInfo(vcs_server)(), | ||||
'git': SysInfo(git_info)(), | ||||
'hg': SysInfo(hg_info)(), | ||||
'svn': SysInfo(svn_info)(), | ||||
} | ||||