#!/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 jinja2
import ipaddress
import random
import binascii
import os

import vyos.version

from vyos.config import Config
from vyos import ConfigError

config_file_client = r'/etc/snmp/snmp.conf'
config_file_daemon = r'/etc/snmp/snmpd.conf'
config_file_access = r'/usr/share/snmp/snmpd.conf'
config_file_user   = r'/var/lib/snmp/snmpd.conf'

# SNMPS template - be careful if you edit the template.
client_config_tmpl = """
### Autogenerated by snmp.py ###
{% if trap_source -%}
clientaddr {{ trap_source }}
{% endif %}

"""

# SNMPS template - be careful if you edit the template.
access_config_tmpl = """
### Autogenerated by snmp.py ###
{% if v3_users %}
{% for u in v3_users %}
{{ u.mode }}user {{ u.name }}
{% endfor %}
{% endif -%}
rwuser {{ vyos_user }}

"""

# SNMPS template - be careful if you edit the template.
user_config_tmpl = """
### Autogenerated by snmp.py ###
# user
{% if v3_users %}
{% for u in v3_users %}
createUser {{ u.name }} {{ u.authProtocol | upper }} {% if u.authPassword %} "{{ u.authPassword }}" {% elif u.authMasterKey %} "{{ u.authMasterKey }}"{% endif %} {{ u.privProtocol | upper }}{% if u.privPassword %} {{ u.privPassword }}{% elif u.privMasterKey %} {{ u.privMasterKey }}{% endif %}
{% endfor %}
{% endif %}

createUser {{ vyos_user }} MD5 "{{ vyos_user_pass }}" DES
"""

# SNMPS template - be careful if you edit the template.
daemon_config_tmpl = """
### Autogenerated by snmp.py ###

# non configurable defaults
sysObjectID 1.3.6.1.4.1.44641
sysServices 14
master agentx
agentXPerms 0755 0755
pass .1.3.6.1.2.1.31.1.1.1.18 /opt/vyatta/sbin/if-mib-alias
smuxpeer .1.3.6.1.2.1.83
smuxpeer .1.3.6.1.2.1.157
smuxpeer .1.3.6.1.4.1.3317.1.2.2
smuxpeer .1.3.6.1.4.1.3317.1.2.3
smuxpeer .1.3.6.1.4.1.3317.1.2.5
smuxpeer .1.3.6.1.4.1.3317.1.2.8
smuxpeer .1.3.6.1.4.1.3317.1.2.9
smuxsocket localhost

# linkUp/Down configure the Event MIB tables to monitor
# the ifTable for network interfaces being taken up or down
# for making internal queries to retrieve any necessary information
iquerySecName {{ vyos_user }}

# Modified from the default linkUpDownNotification
# to include more OIDs and poll more frequently
notificationEvent  linkUpTrap    linkUp   ifIndex ifDescr ifType ifAdminStatus ifOperStatus
notificationEvent  linkDownTrap  linkDown ifIndex ifDescr ifType ifAdminStatus ifOperStatus
monitor  -r 10 -e linkUpTrap   "Generate linkUp" ifOperStatus != 2
monitor  -r 10 -e linkDownTrap "Generate linkDown" ifOperStatus == 2

########################
# configurable section #
########################

# Version
sysDescr VyOS {{ version }}

{% if description -%}
# Description
SysDescr {{ description }}
{% endif %}

# Listen
agentaddress unix:/run/snmpd.socket{% for li in listen_on %},{{ li }}{% endfor %}


# SNMP communities
{% if communities -%}
{% for c in communities %}
{% if c.network -%}
{% for network in c.network %}
{{ c.authorization }}community {{ c.name }} {{ network }}
{% endfor %}
{% else %}
{{ c.authorization }}community {{ c.name }}
{% endif %}
{% endfor %}
{% endif %}

{% if contact -%}
# system contact information
SysContact {{ contact }}
{% endif %}

{% if location -%}
# system location information
SysLocation {{ location }}
{% endif %}

{% if smux_peers -%}
# additional smux peers
{% for sp in smux_peers %}
smuxpeer {{ sp }}
{% endfor %}
{% endif %}

{% if trap_targets -%}
# if there is a problem - tell someone!
{% for t in trap_targets %}
trap2sink {{ t.target }}{% if t.port -%}:{{ t.port }}{% endif %} {{ t.community }}
{% endfor %}
{% endif %}

#
# SNMPv3 stuff goes here
#
{% if v3_enabled %}

# views
{% if v3_views -%}
{% for v in v3_views %}
{% for oid in v.oids %}
view {{ v.name }} included .{{ oid.oid }}
{% endfor %}
{% endfor %}
{% endif %}

# access
#             context sec.model sec.level match  read    write  notif
{% if v3_groups -%}
{% for g in v3_groups %}
access {{ g.name }} "" usm {{ g.seclevel }} exact {{ g.view }} none none
access {{ g.name }} "" tsm {{ g.seclevel }} exact {{ g.view }} none none
{% endfor -%}
{% endif %}

# trap-target
{% if v3_traps -%}
{% for t in v3_traps %}
trapsess -v 3 -e {{ t.engineID }} -u {{ t.secName }} -l {{ t.secLevel }} -a {{ t.authProtocol }} {% if t.authPassword %}-A {{ t.authPassword }}{% elif t.authMasterKey %}-3m {{ t.authMasterKey }}{% endif %} -x {{ t.privProtocol }} {% if t.privPassword %}-X {{ t.privPassword }}{% elif t.privMasterKey %}-3M {{ t.privMasterKey }}{% endif %} {{ t.ipProto }}:{{ t.ipAddr }}:{{ t.ipPort }}
{% endfor -%}
{% endif %}

# group
{% if v3_users -%}
{% for u in v3_users %}
group {{ u.group }} usm {{ u.name }}
group {{ u.group }} tsm {{ u.name }}
{% endfor %}
{% endif %}

{% endif %}

"""

default_config_data = {
    'listen_on': [],
    'communities': [],
    'smux_peers': [],
    'location' : '',
    'description' : '',
    'contact' : '',
    'trap_source': '',
    'trap_targets': [],
    'vyos_user': '',
    'vyos_user_pass': '',
    'version': '999',
    'v3_enabled': 'False',
    'v3_engineid': '',
    'v3_groups': [],
    'v3_traps': [],
    'v3_tsm_key': '',
    'v3_tsm_port': '10161',
    'v3_users': [],
    'v3_views': []
}

def get_config():
    snmp = default_config_data
    conf = Config()
    if not conf.exists('service snmp'):
        return None
    else:
        conf.set_level('service snmp')

    version_data = vyos.version.get_version_data()
    snmp['version'] = version_data['version']

    # create an internal snmpv3 user of the form 'vyattaxxxxxxxxxxxxxxxx'
    # os.urandom(8) returns 8 bytes of random data
    snmp['vyos_user'] = 'vyatta' + binascii.hexlify(os.urandom(8)).decode('utf-8')
    snmp['vyos_user_pass'] = binascii.hexlify(os.urandom(16)).decode('utf-8')

    if conf.exists('community'):
        for name in conf.list_nodes('community'):
            community = {
                'name': name,
                'authorization': 'ro',
                'network': []
            }

            if conf.exists('community {0} authorization'.format(name)):
                community['authorization'] = conf.return_value('community {0} authorization'.format(name))

            if conf.exists('community {0} network'.format(name)):
                community['network'] = conf.return_values('community {0} network'.format(name))

            snmp['communities'].append(community)

    if conf.exists('contact'):
        snmp['contact'] = conf.return_value('contact')

    if conf.exists('description'):
        snmp['description'] = conf.return_value('description')

    if conf.exists('listen-address'):
        for addr in conf.list_nodes('listen-address'):
            listen = ''
            port = '161'
            if conf.exists('listen-address {0} port'.format(addr)):
                port = conf.return_value('listen-address {0} port'.format(addr))

            if ipaddress.ip_address(addr).version == 4:
                # udp:127.0.0.1:161
                listen = 'udp:' + addr + ':' + port
            elif ipaddress.ip_address(addr).version == 6:
                # udp6:[::1]:161
                listen = 'udp6:' + '[' + addr + ']' + ':' + port
            else:
                raise ConfigError('Invalid IP address version')

            snmp['listen_on'].append(listen)

    if conf.exists('location'):
        snmp['location'] = conf.return_value('location')

    if conf.exists('smux-peer'):
        snmp['smux_peers'] = conf.return_values('smux-peer')

    if conf.exists('trap-source'):
        snmp['trap_source'] = conf.return_value('trap-source')

    if conf.exists('trap-target'):
        for target in conf.list_nodes('trap-target'):
            trap_tgt = {
                'target': target,
                'community': '',
                'port': ''
            }

            if conf.exists('trap-target {0} community'.format(target)):
               trap_tgt['community'] = conf.return_value('trap-target {0} community'.format(target))

            if conf.exists('trap-target {0} port'.format(target)):
                trap_tgt['port'] = conf.return_value('trap-target {0} port'.format(target))

            snmp['trap_targets'].append(trap_tgt)

    #########################################################################
    #                ____  _   _ __  __ ____          _____                 #
    #               / ___|| \ | |  \/  |  _ \  __   _|___ /                 #
    #               \___ \|  \| | |\/| | |_) | \ \ / / |_ \                 #
    #                ___) | |\  | |  | |  __/   \ V / ___) |                #
    #               |____/|_| \_|_|  |_|_|       \_/ |____/                 #
    #                                                                       #
    #     now take care about the fancy SNMP v3 stuff, or bail out eraly    #
    #########################################################################
    if not conf.exists('v3'):
        return snmp
    else:
        snmp['v3_enabled'] = True

    #
    # 'set service snmp v3 engineid'
    #
    if conf.exists('v3 engineid'):
        snmp['v3_engineid'] = conf.return_value('v3 engineid')

    #
    # 'set service snmp v3 group'
    #
    if conf.exists('v3 group'):
        for group in conf.list_nodes('v3 group'):
            v3_group = {
                'name': group,
                'mode': 'ro',
                'seclevel': 'auth',
                'view': ''
            }

            if conf.exists('v3 group {0} mode'.format(group)):
                v3_group['mode'] = conf.return_value('v3 group {0} mode'.format(group))

            if conf.exists('v3 group {0} seclevel'.format(group)):
                v3_group['seclevel'] = conf.return_value('v3 group {0} seclevel'.format(group))

            if conf.exists('v3 group {0} view'.format(group)):
                v3_group['view'] = conf.return_value('v3 group {0} view'.format(group))

            snmp['v3_groups'].append(v3_group)

    #
    # 'set service snmp v3 trap-target'
    #
    if conf.exists('v3 trap-target'):
        for trap in conf.list_nodes('v3 trap-target'):
            trap_cfg = {
                'ipAddr': trap,
                'engineID': '',
                'secName': '',
                'authProtocol': 'md5',
                'authPassword': '',
                'authMasterKey': '',
                'privProtocol': 'des',
                'privPassword': '',
                'privMasterKey': '',
                'ipProto': 'udp',
                'ipPort': '162',
                'type': '',
                'secLevel': 'noAuthNoPriv'
            }

            if conf.exists('v3 trap-target {0} engineid'.format(trap)):
                # Set the context engineID used for SNMPv3 REQUEST messages scopedPdu.
                # If not specified, this will default to the authoritative engineID.
                trap_cfg['engineID'] = conf.return_value('v3 trap-target {0} engineid'.format(trap))

            if conf.exists('v3 trap-target {0} user'.format(trap)):
                # Set the securityName used for authenticated SNMPv3 messages.
                trap_cfg['secName'] = conf.return_value('v3 trap-target {0} user'.format(trap))

            if conf.exists('v3 trap-target {0} auth type'.format(trap)):
                # Set the authentication protocol (MD5 or SHA) used for authenticated SNMPv3 messages
                # cmdline option '-a'
                trap_cfg['authProtocol'] = conf.return_value('v3 trap-target {0} auth type'.format(trap))

            if conf.exists('v3 trap-target {0} auth plaintext-key'.format(trap)):
                # Set the authentication pass phrase used for authenticated SNMPv3 messages.
                # cmdline option '-A'
                trap_cfg['authPassword'] = conf.return_value('v3 trap-target {0} auth plaintext-key'.format(trap))

            if conf.exists('v3 trap-target {0} auth encrypted-key'.format(trap)):
                # Sets the keys to be used for SNMPv3 transactions. These options allow you to set the master authentication keys.
                # cmdline option '-3m'
                trap_cfg['authMasterKey'] = conf.return_value('v3 trap-target {0} auth encrypted-key'.format(trap))

            if conf.exists('v3 trap-target {0} privacy type'.format(trap)):
                # Set the privacy protocol (DES or AES) used for encrypted SNMPv3 messages.
                # cmdline option '-x'
                trap_cfg['privProtocol'] = conf.return_value('v3 trap-target {0} privacy type'.format(trap))

            if conf.exists('v3 trap-target {0} privacy plaintext-key'.format(trap)):
                # Set the privacy pass phrase used for encrypted SNMPv3 messages.
                # cmdline option '-X'
                trap_cfg['privPassword'] = conf.return_value('v3 trap-target {0} privacy plaintext-key'.format(trap))

            if conf.exists('v3 trap-target {0} privacy encrypted-key'.format(trap)):
                # Sets the keys to be used for SNMPv3 transactions. These options allow you to set the master encryption keys.
                # cmdline option '-3M'
                trap_cfg['privMasterKey'] = conf.return_value('v3 trap-target {0} privacy encrypted-key'.format(trap))

            if conf.exists('v3 trap-target {0} protocol'.format(trap)):
                trap_cfg['ipProto'] = conf.return_value('v3 trap-target {0} protocol'.format(trap))

            if conf.exists('v3 trap-target {0} port'.format(trap)):
                trap_cfg['ipPort'] = conf.return_value('v3 trap-target {0} port'.format(trap))

            if conf.exists('v3 trap-target {0} type'.format(trap)):
                tmp = conf.return_value('v3 trap-target {0} type'.format(trap))
                # see http://www.net-snmp.org/docs/man/snmpd.conf.html
                # The option -Ci can be used (with -v2c or -v3) to generate an INFORM
                # notification rather than an unacknowledged TRAP.
                if tmp == 'inform':
                    trap_cfg['type'] = ' -Ci'

            # Determine securityLevel used for SNMPv3 messages (noAuthNoPriv|authNoPriv|authPriv).
            # Appropriate pass phrase(s) must provided when using any level higher than noAuthNoPriv.
            if trap_cfg['authPassword'] or trap_cfg['authMasterKey']:
                if trap_cfg['privProtocol'] or trap_cfg['privPassword']:
                    trap_cfg['secLevel'] = 'authPriv'
                else:
                    trap_cfg['secLevel'] = 'authNoPriv'

            snmp['v3_traps'].append(trap_cfg)

    #
    # 'set service snmp v3 tsm'
    #
    if conf.exists('v3 tsm'):
        if conf.exists('v3 tsm local-key'):
            snmp['v3_tsm_key'] = conf.return_value('v3 tsm local-key')

        if conf.exists('v3 tsm port'):
            snmp['v3_tsm_port'] = conf.return_value('v3 tsm port')

    #
    # 'set service snmp v3 user'
    #
    if conf.exists('v3 user'):
        for user in conf.list_nodes('v3 user'):
            user_cfg = {
                'name': user,
                'authMasterKey': '',
                'authPassword': '',
                'authProtocol': 'md5',
                'engineID': '',
                'group': '',
                'mode': 'ro',
                'privMasterKey': '',
                'privPassword': '',
                'privTsmKey': '',
                'privProtocol': 'des'
            }

            #
            # v3 user {0} auth
            #
            if conf.exists('v3 user {0} auth encrypted-key'.format(user)):
                user_cfg['authMasterKey'] = conf.return_value('v3 user {0} auth encrypted-key'.format(user))

            if conf.exists('v3 user {0} auth plaintext-key'.format(user)):
                user_cfg['authPassword'] = conf.return_value('v3 user {0} auth plaintext-key'.format(user))

            if conf.exists('v3 user {0} auth type'.format(user)):
                user_cfg['authProtocol'] = conf.return_value('v3 user {0} auth type'.format(user))

            #
            # v3 user {0} engineid
            #
            if conf.exists('v3 user {0} engineid'.format(user)):
                user_cfg['engineID'] = conf.return_value('v3 user {0} engineid'.format(user))

            #
            # v3 user {0} group
            #
            if conf.exists('v3 user {0} group'.format(user)):
                user_cfg['group'] = conf.return_value('v3 user {0} group'.format(user))

            #
            # v3 user {0} mode
            #
            if conf.exists('v3 user {0} mode'.format(user)):
                user_cfg['mode'] = conf.return_value('v3 user {0} mode'.format(user))

            #
            # v3 user {0} privacy
            #
            if conf.exists('v3 user {0} privacy encrypted-key'.format(user)):
                user_cfg['privMasterKey'] = conf.return_value('v3 user {0} privacy encrypted-key'.format(user))

            if conf.exists('v3 user {0} privacy plaintext-key'.format(user)):
                user_cfg['privPassword'] = conf.return_value('v3 user {0} privacy plaintext-key'.format(user))

            if conf.exists('v3 user {0} privacy tsm-key'.format(user)):
                user_cfg['privTsmKey'] = conf.return_value('v3 user {0} privacy tsm-key'.format(user))

            if conf.exists('v3 user {0} privacy type'.format(user)):
                user_cfg['privProtocol'] = conf.return_value('v3 user {0} privacy type'.format(user))

            snmp['v3_users'].append(user_cfg)

    #
    # 'set service snmp v3 view'
    #
    if conf.exists('v3 view'):
        for view in conf.list_nodes('v3 view'):
            view_cfg = {
                'name': view,
                'oids': []
            }

            if conf.exists('v3 view {0} oid'.format(view)):
                for oid in conf.list_nodes('v3 view {0} oid'.format(view)):
                    oid_cfg = {
                        'oid': oid
                    }
                    view_cfg['oids'].append(oid_cfg)
            snmp['v3_views'].append(view_cfg)

    return snmp

def verify(snmp):
    if snmp is None:
        return None

    if 'v3_traps' in snmp.keys():
        for trap in snmp['v3_traps']:
            if trap['authPassword'] and trap['authMasterKey']:
                raise ConfigError('Can not mix "encrypted-key" and "plaintext-key" for trap auth')
            if trap['privPassword'] and trap['privMasterKey']:
                raise ConfigError('Can not mix "encrypted-key" and "plaintext-key" for trap privacy')

    if 'v3_users' in snmp.keys():
        for user in snmp['v3_users']:
            if user['authPassword'] and user['authMasterKey']:
                raise ConfigError('Can not mix "encrypted-key" and "plaintext-key" for user auth')
            if user['privPassword'] and user['privMasterKey']:
                raise ConfigError('Can not mix "encrypted-key" and "plaintext-key" for user privacy')

    if 'v3_group' in snmp.keys():
        for group in snmp['v3_group']:
            if not group['view']:
                raise ConfigError('You must create a view first')

    return None

def generate(snmp):
    #
    # As we are manipulating the snmpd user database we have to stop it first!
    # This is even save if service is going to be removed
    os.system("sudo systemctl stop snmpd.service")
    os.unlink(config_file_client)
    os.unlink(config_file_daemon)
    os.unlink(config_file_access)
    os.unlink(config_file_user)

    if snmp is None:
        return None

    # Write client config file
    tmpl = jinja2.Template(client_config_tmpl, trim_blocks=True)
    config_text = tmpl.render(snmp)
    with open(config_file_client, 'w') as f:
        f.write(config_text)

    # Write server config file
    tmpl = jinja2.Template(daemon_config_tmpl, trim_blocks=True)
    config_text = tmpl.render(snmp)
    with open(config_file_daemon, 'w') as f:
        f.write(config_text)

    # Write access rights config file
    tmpl = jinja2.Template(access_config_tmpl, trim_blocks=True)
    config_text = tmpl.render(snmp)
    with open(config_file_access, 'w') as f:
        f.write(config_text)

    # Write access rights config file
    tmpl = jinja2.Template(user_config_tmpl, trim_blocks=True)
    config_text = tmpl.render(snmp)
    with open(config_file_user, 'w') as f:
        f.write(config_text)

    return None

def apply(snmp):
    if snmp is not None:
        os.system("sudo systemctl restart snmpd.service")

    return None

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