#!/usr/bin/env python3
#
# Copyright (C) 2018 VyOS maintainers and contributors
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License version 2 or later as
# published by the Free Software Foundation.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
#
#

import sys
import os
import re
import jinja2

from vyos.config import Config
from vyos import ConfigError

########### config templates

#### /etc/rsyslog.d/vyos-rsyslog.conf ###
configs = '''
## generated by syslog.py ##
## file based logging
{% if files['global']['marker'] -%}
$ModLoad immark
{% if files['global']['marker-interval'] %}
$MarkMessagePeriod  {{files['global']['marker-interval']}} 
{% endif %}
{% endif -%}
{% if files['global']['preserver_fqdn'] -%}
$PreserveFQDN on
{% endif -%}
{% for file in files %}
$outchannel {{file}},{{files[file]['log-file']}},{{files[file]['max-size']}},{{files[file]['action-on-max-size']}}
{{files[file]['selectors']}} :omfile:${{file}}
{% endfor %}
{% if console %}
## console logging
{% for con in console %}
{{console[con]['selectors']}} /dev/console
{% endfor %}
{% endif %}
{% if hosts %}
## remote logging
{% for host in hosts %}
{% if hosts[host]['proto'] == 'tcp' %}
{{hosts[host]['selectors']}} @@{{host}}
{% else %}
{{hosts[host]['selectors']}} @{{host}}
{% endif %}
{% endfor %}
{% endif %}
{% if user %}
{% for u in user %}
{{user[u]['selectors']}} :omusrmsg:{{u}}
{% endfor %}
{% endif %}
'''

logrotate_configs = '''
{% for file in files %}
{{files[file]['log-file']}} {
  missingok
  notifempty
  create
  rotate {{files[file]['max-files']}}
  size={{files[file]['max-size']//1024}}k
  postrotate
    invoke-rc.d rsyslog rotate > /dev/null
  endscript
}
{% endfor %}
'''
############# config templates end

def get_config():
  c = Config()
  if not c.exists('system syslog'):
    return None
  c.set_level('system syslog')  

  config_data = {
    'files'   : {},
    'console' : {},
    'hosts'   : {},
    'user'    : {}
  }

  #####
  # /etc/rsyslog.d/vyos-rsyslog.conf
  # 'set system syslog global'
  #####
  config_data['files'].update(
    {
      'global'  : {
        'log-file'            : '/var/log/messages',
        'max-size'            : 262144,
        'action-on-max-size'  : '/usr/sbin/logrotate /etc/logrotate.d/vyos-rsyslog',
        'selectors'           : '*.notice;local7.debug',
        'max-files'           : '5',
        'preserver_fqdn'      : False
      }
    }
  )

  if c.exists('global marker'):
    config_data['files']['global']['marker'] = True
    if c.exists('global marker interval'):
      config_data['files']['global']['marker-interval'] = c.return_value('global marker interval') 
  if c.exists('global facility'):
    config_data['files']['global']['selectors'] = generate_selectors(c, 'global facility')
  if c.exists('global archive size'):
    config_data['files']['global']['max-size'] = int(c.return_value('global archive size'))* 1024
  if c.exists('global archive file'):
    config_data['files']['global']['max-files'] = c.return_value('global archive file')
  if c.exists('global preserve-fqdn'):
    config_data['files']['global']['preserver_fqdn'] = True

  ###
  # set system syslog file
  ###

  if c.exists('file'):
    filenames = c.list_nodes('file')
    for filename in filenames:
      config_data['files'].update(
        {
          filename  : { 
            'log-file' : '/var/log/user/' + filename,
            'max-files' : '5',
            'action-on-max-size'  : '/usr/sbin/logrotate /etc/logrotate.d/' + filename,
            'selectors'  : '*.err',
            'max-size'  : 262144     
          }
        }
      )

      if c.exists('file ' + filename + ' facility'):
        config_data['files'][filename]['selectors']  = generate_selectors(c, 'file ' + filename + ' facility')
      if c.exists('file ' + filename + ' archive size'):
       config_data['files'][filename]['max-size']  = int(c.return_value('file ' + filename + ' archive size'))* 1024
      if c.exists('file ' + filename + ' archive files'):
        config_data['files'][filename]['max-files'] = c.return_value('file ' + filename + ' archive files')

  ## set system syslog console
  if c.exists('console'):
    config_data['console']  = {
      '/dev/console' : {
        'selectors' : '*.err'
      }
    }
    
  for f in c.list_nodes('console facility'):
    if c.exists('console facility ' + f + ' level'):
      config_data['console'] = {
        '/dev/console' : {
          'selectors' : generate_selectors(c, 'console facility')
        }
      }
    
  ## set system syslog host
  if c.exists('host'):
    proto = 'udp'
    rhosts = c.list_nodes('host')
    for rhost in rhosts:
      for fac in c.list_nodes('host ' + rhost + ' facility'):
        if c.exists('host ' + rhost + ' facility ' + fac + ' protocol'):
          proto = c.return_value('host ' + rhost + ' facility ' + fac + ' protocol')

      config_data['hosts'].update(
        {
          rhost : {
           'selectors' : generate_selectors(c, 'host ' + rhost + ' facility'), 
            'proto' : proto
          } 
        }
      )

  ## set system syslog user
  if c.exists('user'):
    usrs = c.list_nodes('user')
    for usr in usrs:
      config_data['user'].update(
        {
          usr : {
            'selectors' : generate_selectors(c, 'user ' + usr + ' facility')
          }
        }
      )
  
  return config_data

def generate_selectors(c, config_node):
## protocols and security are being mapped here
## for backward compatibility with old configs
## security and protocol mappings can be removed later
  if c.is_tag(config_node):
    nodes = c.list_nodes(config_node)
    selectors = ""
    for node in nodes:
      lvl = c.return_value( config_node + ' ' + node + ' level')
      if lvl == None:
        lvl = "err"
      if lvl == 'all':
        lvl = '*'
      if node == 'all' and node != nodes[-1]:
        selectors += "*." + lvl + ";"
      elif node == 'all':
        selectors += "*." + lvl
      elif node != nodes[-1]:
        if node == 'protocols':
          node = 'local7'
        if node == 'security':
          node = 'auth'
        selectors += node + "." + lvl + ";"
      else:
        if node == 'protocols':
          node = 'local7'
        if node == 'security':
          node = 'auth'
        selectors += node + "." + lvl
    return selectors

def generate(c):
  if c == None:
    return None

  tmpl = jinja2.Template(configs, trim_blocks=True)
  config_text = tmpl.render(c)
  with open('/etc/rsyslog.d/vyos-rsyslog.conf', 'w') as f:
    f.write(config_text)

  ## eventually write for each file its own logrotate file, since size is defined it shouldn't matter
  tmpl = jinja2.Template(logrotate_configs, trim_blocks=True)
  config_text = tmpl.render(c)
  with open('/etc/logrotate.d/vyos-rsyslog', 'w') as f:
    f.write(config_text)

def verify(c):
  if c == None:
    return None
  #
  # /etc/rsyslog.conf is generated somewhere and copied over the original (exists in /opt/vyatta/etc/rsyslog.conf)
  # it interferes with the global logging, to make sure we are using a single base, template is enforced here 
  #
  if not os.path.islink('/etc/rsyslog.conf'):
    os.remove('/etc/rsyslog.conf')
    os.symlink('/usr/share/vyos/templates/rsyslog/rsyslog.conf', '/etc/rsyslog.conf')

  # /var/log/vyos-rsyslog were the old files, we may want to clean those up, but currently there
  # is a chance that someone still needs it, so I don't automatically remove them 

  if c == None:
    return None

  fac = ['*','auth','authpriv','cron','daemon','kern','lpr','mail','mark','news','protocols','security',\
        'syslog','user','uucp','local0','local1','local2','local3','local4','local5','local6','local7']
  lvl = ['emerg','alert','crit','err','warning','notice','info','debug','*']

  for conf in c:
    if c[conf]:
      for item in c[conf]:
        for s in c[conf][item]['selectors'].split(";"):
          f = re.sub("\..*$","",s)
          if f not in fac:
            print (c[conf])
            raise ConfigError('Invalid facility ' + s + ' set in '+ conf + ' ' + item)
          l = re.sub("^.+\.","",s)
          if l not in lvl:
            raise ConfigError('Invalid logging level ' + s + ' set in '+ conf + ' ' + item)

def apply(c):
  if not os.path.exists('/var/run/rsyslogd.pid'):
    os.system("sudo systemctl start rsyslog >/dev/null")
  else:
    os.system("sudo systemctl restart rsyslog >/dev/null")


if __name__ == '__main__':
  try:
    c = get_config()
    verify(c)
    generate(c)
    apply(c)
  except ConfigError as e:
    print(e)
    sys.exit(1)