File: //usr/syno/sbin/synologconfgen
#!/usr/bin/python2
import sys
import os
import re
import errno
import json
import shutil
import imp
import ConfigParser
DEV_MODE = False
class SynoException(Exception):
pass
class Path(object):
@staticmethod
def mkdir_p(dir_path):
try:
os.makedirs(dir_path)
except OSError as e:
if e.errno == errno.EEXIST and os.path.isdir(dir_path):
pass
else:
raise
@staticmethod
def rm_f(file_path):
try:
os.unlink(file_path)
except OSError as e:
if e.errno == errno.ENOENT:
pass
else:
raise
@staticmethod
def rm_rf(dir_path):
try:
shutil.rmtree(dir_path)
except OSError as e:
if e.errno == errno.ENOENT:
pass
else:
raise
@staticmethod
def ls(dir_path):
try:
filenames = os.listdir(dir_path)
except OSError:
filenames = []
return filenames
@staticmethod
def ls_file_paths(dir_rel_path, file_suffix=None):
dir_paths = [
os.path.join(Path.DSM_PREFIX, dir_rel_path),
os.path.join(Path.PKG_PREFIX, dir_rel_path),
]
file_paths = []
for dir_path in dir_paths:
for filename in Path.ls(dir_path):
if file_suffix and not filename.endswith(file_suffix):
continue
file_paths.append(os.path.join(dir_path, filename))
return file_paths
DSM_PREFIX = '/usr/syno'
PKG_PREFIX = '/usr/local'
class SubGenerator(object):
def __init__(self, file_path):
module_name = self._extract_module_name(file_path)
module = self._load_module(module_name, file_path)
self._set_module_functions(module)
self.name = module_name
self.path = file_path
@staticmethod
def load_all():
sub_gens = []
for file_path in Path.ls_file_paths(SubGenerator.DIR_REL_PATH, '.py'):
try:
sub_gen = SubGenerator(file_path)
except SynoException as e:
print 'ERROR: load sub-generator "{}" failed'.format(file_path)
print ' {}'.format(e)
continue
sub_gens.append(sub_gen)
print 'INFO: loaded sub-generator "{}" from "{}"'.format(sub_gen.name, sub_gen.path)
return sub_gens
def _extract_module_name(self, file_path):
filename = os.path.basename(file_path)
match = re.match(r'(.+)\.py$', filename)
if not match:
raise SynoException('invalid file path')
return match.group(1)
def _load_module(self, module_name, file_path):
try:
return imp.load_source(module_name, file_path)
except Exception as e:
raise SynoException(e)
def _set_module_functions(self, module):
try:
self.validate_synolog_conf = module.validate_synolog_conf
self.generate_syslog_ng_conf = module.generate_syslog_ng_conf
except AttributeError as e:
raise SynoException('missing required function, {}'.format(e))
DIR_REL_PATH = 'lib/synosyslog/subgens'
class SynoLogConf(object):
def __init__(self, conf_path):
self.app_id = self._extract_app_id(conf_path)
self.conf = self._load_conf(conf_path)
self.mappings = self._load_mappings(conf_path)
self.path = conf_path
def is_key_enabled(self, key):
if key not in self.conf:
return False
return bool(self.conf[key])
def validate(self, sub_gens):
valid_sub_gens = []
for sub_gen in sub_gens:
try:
success = sub_gen.validate_synolog_conf(self)
except Exception as e:
print 'ERROR: failed to run "validate_synolog_conf" in "{}" on app "{}"'.format(sub_gen.name, self.app_id)
print ' {}'.format(e)
if DEV_MODE:
raise
continue
if success:
valid_sub_gens.append(sub_gen)
else:
print 'WARN: synolog conf for app "{}" did not pass the validation in "{}"'.format(self.app_id, sub_gen.name)
return valid_sub_gens
def generate(self, sub_gens):
syslog_ng_conf = SyslogNgConf(self)
for sub_gen in sub_gens:
try:
sub_gen_result = sub_gen.generate_syslog_ng_conf(self)
except Exception as e:
print 'ERROR: failed to run "generate_syslog_ng_conf" in "{}" on app "{}"'.format(sub_gen.name, self.app_id)
print ' {}'.format(e)
if DEV_MODE:
raise
continue
syslog_ng_conf.add(sub_gen_result, sub_gen.name)
return syslog_ng_conf
@staticmethod
def load_all():
synolog_confs = []
for file_path in Path.ls_file_paths(SynoLogConf.DIR_REL_PATH):
try:
synolog_conf = SynoLogConf(file_path)
except SynoException as e:
print 'ERROR: load synolog config "{}" failed'.format(file_path)
print ' {}'.format(e)
continue
synolog_confs.append(synolog_conf)
print 'INFO: loaded synolog config "{}" from "{}"'.format(synolog_conf.app_id, synolog_conf.path)
return synolog_confs
def _extract_app_id(self, conf_path):
return os.path.basename(conf_path)
def _load_conf(self, conf_path):
try:
with open(conf_path) as f:
return json.load(f)
except IOError:
raise SynoException('open synolog config failed: {}'.format(conf_path))
except ValueError:
raise SynoException('parse json file failed: {}'.format(conf_path))
def _load_mappings(self, conf_path):
base_dir_path = os.path.dirname(os.path.dirname(conf_path))
event_path = os.path.join(base_dir_path, 'events', self.app_id)
parser = ConfigParser.ConfigParser()
try:
with open(event_path) as f:
parser.readfp(f)
except IOError:
raise SynoException('open synolog event file failed: {}'.format(event_path))
except ConfigParser.Error:
raise SynoException('parse synolog event file failed: {}'.format(event_path))
mappings = {}
for section in parser.sections():
mappings[section] = {}
for option in parser.options(section):
if option[0] != '@':
continue
try:
arg_seq = int(option[1:])
except ValueError:
continue
mappings[section][arg_seq] = parser.get(section, option)
return mappings
DIR_REL_PATH = 'share/synolog/configs'
class SyslogNgConf(object):
def __init__(self, synolog_conf):
self.synolog_conf = synolog_conf
self.path = self._get_dump_path()
self.confs = []
def add(self, sub_gen_result, sub_gen_name):
if not sub_gen_result:
return
try:
sub_gen_confs = self._make_sub_gen_confs(sub_gen_result)
except SynoException as e:
print 'ERROR: generated syslog-ng conf from "{}" on app "{}" is malformed, skipped'.format(sub_gen_name, self.synolog_conf.app_id)
print ' {}'.format(e)
return
try:
for sub_gen_conf in sub_gen_confs:
self._validate_sub_gen_conf(sub_gen_conf)
except SynoException as e:
print 'ERROR: generated syslog-ng conf from "{}" on app "{}" is invalid, skipped'.format(sub_gen_name, self.synolog_conf.app_id)
print ' {}'.format(e)
return
for idx, sub_gen_conf in enumerate(sub_gen_confs):
base_conf = self._gen_base_conf(sub_gen_name, idx)
self.confs.append(base_conf + sub_gen_conf)
def dump(self):
contents = []
contents.append(SyslogNgConf.GENERATED_WARNING)
for conf in self.confs:
contents.extend(self._render_components(conf))
for conf in self.confs:
contents.append(self._render_flow(conf))
contents = self._remove_duplicated(contents)
try:
with open(self.path, 'w') as f:
f.write('\n\n'.join(contents) + '\n')
except IOError:
Path.rm_f(self.path)
raise SynoException('write syslog-ng config failed: {}'.format(self.path))
@staticmethod
def clear_all():
Path.rm_rf(SyslogNgConf.CONF_DIR_PATH)
Path.mkdir_p(SyslogNgConf.CONF_DIR_PATH)
def _get_dump_path(self):
return os.path.join(SyslogNgConf.CONF_DIR_PATH, self.synolog_conf.app_id)
def _gen_base_conf(self, sub_gen_name, idx):
return [
{
'type': 'source',
'name': 's_syno_synosyslog',
'content': None
},
{
'type': 'filter',
'name': 'f_syno_{}'.format(self.synolog_conf.app_id),
'content': [
SyslogNgConf.FILTER_FMT.format(self.synolog_conf.app_id)
]
},
{
'type': 'parser',
'name': 'p_syno_sdata_{}_{}_{}'.format(self.synolog_conf.app_id, sub_gen_name, idx),
'content': [
SyslogNgConf.SYNOSDATA_FMT.format(sd_tag=SyslogNgConf.SYNO_SD_TAG)
]
},
{
'type': 'rewrite',
'name': 'r_syno_username',
'content': None
},
{
'type': 'rewrite',
'name': 'r_syno_args_{}'.format(self.synolog_conf.app_id),
'content': self._gen_args_rewrite_content()
},
]
def _gen_args_rewrite_content(self):
content = []
for event_id, mapping in self.synolog_conf.mappings.items():
for arg_seq, arg_name in mapping.items():
content.append(SyslogNgConf.ARGS_REWRITE_FMT.format(**{
'event_id': event_id,
'arg_seq': arg_seq,
'arg_name': arg_name,
}))
return content
def _make_sub_gen_confs(self, sub_gen_result):
if type(sub_gen_result) is not list:
raise SynoException('should be a list')
if len(sub_gen_result) == 0:
raise SynoException('should not be empty list')
first = sub_gen_result[0]
if type(first) is list:
return sub_gen_result
elif type(first) is dict:
return [sub_gen_result]
else:
raise SynoException('unknown list element')
def _validate_sub_gen_conf(self, sub_gen_conf):
if type(sub_gen_conf) is not list:
raise SynoException('should be a list')
for entry in sub_gen_conf:
if type(entry) is not dict:
raise SynoException('list element should be a dict')
for field in ['type', 'name', 'content']:
if field not in entry:
raise SynoException('each entry should contain the field "{}"'.format(field))
if 'destination' not in [entry['type'] for entry in sub_gen_conf]:
raise SynoException('should be at least one destination')
def _render_components(self, conf):
contents = []
for entry in conf:
if entry['content'] is None:
continue
component = self._render_component(entry)
contents.append(component)
return contents
def _render_component(self, entry):
lines = [SyslogNgConf.COMPONENT_ITEM_FMT.format(line) for line in entry['content']]
return SyslogNgConf.COMPONENT_FMT.format(lines='\n'.join(lines), **entry)
def _render_flow(self, conf):
lines = [SyslogNgConf.FLOW_ITEM_FMT.format(**entry) for entry in conf]
return SyslogNgConf.FLOW_FMT.format(lines='\n'.join(lines))
def _remove_duplicated(self, ori_items):
new_items = []
for item in ori_items:
if item in new_items:
continue
new_items.append(item)
return new_items
CONF_DIR_PATH = '/etc/syslog-ng/syno.d/synosyslog.conf.gen'
SYNO_SD_TAG = 'synolog@6574'
GENERATED_WARNING = '''# Generated by synologconfgen
# DO NOT EDIT THIS FILE BY HAND - YOUR CHANGES WILL BE OVERWRITTEN'''
COMPONENT_FMT = '''{type} {name} {{
{lines}
}};'''
FLOW_FMT = '''log {{
{lines}
}};'''
COMPONENT_ITEM_FMT = ' {}'
FLOW_ITEM_FMT = ' {type}({name});'
FILTER_FMT = 'program("^{}$");'
SYNOSDATA_FMT = 'json-parser(template("$(format-json --key .SDATA.{sd_tag}.* --replace-prefix .SDATA.{sd_tag}.=)") prefix("SYNOSDATA."));'
ARGS_REWRITE_FMT = 'set("${{SYNOSDATA.arg_{arg_seq}}}", value("ARGS.{arg_name}") condition(match("0x{event_id}" value("SYNOSDATA.event_id"))));'
def main():
SyslogNgConf.clear_all()
sub_gens = SubGenerator.load_all()
synolog_confs = SynoLogConf.load_all()
for synolog_conf in synolog_confs:
valid_sub_gens = synolog_conf.validate(sub_gens)
syslog_ng_conf = synolog_conf.generate(valid_sub_gens)
try:
syslog_ng_conf.dump()
print 'INFO: generated syslog-ng config for app "{}" on "{}"'.format(synolog_conf.app_id, syslog_ng_conf.path)
except SynoException as e:
print 'WARN: skipped generating syslog-ng config for app "{}"'.format(synolog_conf.app_id)
print ' {}'.format(e)
print 'INFO: syslog-ng config generation for synolog is done'
if __name__ == '__main__':
if len(sys.argv) > 1 and '--dev' in sys.argv:
DEV_MODE = True
main()