#!/usr/bin/env python3
#
# Copyright (C) 2017-2020 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 os
import re

from copy import deepcopy
from jinja2 import FileSystemLoader, Environment
from sys import exit

from vyos.config import Config
from vyos.validate import is_addr_assigned,is_loopback_addr
from vyos.defaults import directories as vyos_data_dir
from vyos.version import get_version_data
from vyos import ConfigError

config_file = "/etc/default/lldpd"
vyos_config_file = "/etc/lldpd.d/01-vyos.conf"
base = ['service', 'lldp']

default_config_data = {
    "options": '',
    "interface_list": '',
    "location": ''
}

def get_options(config):
    options = {}
    config.set_level(base)

    options['listen_vlan'] = config.exists('listen-vlan')
    options['mgmt_addr'] = []
    for addr in config.return_values('management-address'):
        if is_addr_assigned(addr) and not is_loopback_addr(addr):
            options['mgmt_addr'].append(addr)
        else:
            message = 'WARNING: LLDP management address {0} invalid - '.format(addr)
            if is_loopback_addr(addr):
                message += '(loopback address).'
            else:
                message += 'address not found.'
            print(message)

    snmp = config.exists('snmp enable')
    options["snmp"] = snmp
    if snmp:
        config.set_level('')
        options["sys_snmp"] = config.exists('service snmp')
        config.set_level(base)

    config.set_level(base + ['legacy-protocols'])
    options['cdp'] = config.exists('cdp')
    options['edp'] = config.exists('edp')
    options['fdp'] = config.exists('fdp')
    options['sonmp'] = config.exists('sonmp')

    # start with an unknown version information
    version_data = get_version_data()
    options['description'] = version_data['version']
    options['listen_on'] = []

    return options

def get_interface_list(config):
    config.set_level(base)
    intfs_names = config.list_nodes(['interface'])
    if len(intfs_names) < 0:
        return 0

    interface_list = []
    for name in intfs_names:
        config.set_level(base + ['interface', name])
        disable = config.exists(['disable'])
        intf = {
            'name': name,
            'disable': disable
        }
        interface_list.append(intf)
    return interface_list


def get_location_intf(config, name):
    path = base + ['interface', name]
    config.set_level(path)

    config.set_level(path + ['location'])
    elin = ''
    coordinate_based = {}

    if config.exists('elin'):
        elin = config.return_value('elin')

    if config.exists('coordinate-based'):
        config.set_level(path + ['location', 'coordinate-based'])

        coordinate_based['latitude'] = config.return_value(['latitude'])
        coordinate_based['longitude'] = config.return_value(['longitude'])

        coordinate_based['altitude'] = '0'
        if config.exists(['altitude']):
            coordinate_based['altitude'] = config.return_value(['altitude'])

        coordinate_based['datum'] = 'WGS84'
        if config.exists(['datum']):
            coordinate_based['datum'] = config.return_value(['datum'])

    intf = {
        'name': name,
        'elin': elin,
        'coordinate_based': coordinate_based

    }
    return intf


def get_location(config):
    config.set_level(base)
    intfs_names = config.list_nodes(['interface'])
    if len(intfs_names) < 0:
        return 0

    if config.exists('disable'):
        return 0

    intfs_location = []
    for name in intfs_names:
        intf = get_location_intf(config, name)
        intfs_location.append(intf)

    return intfs_location


def get_config():
    lldp = deepcopy(default_config_data)
    conf = Config()
    if not conf.exists(base):
        return None
    else:
        lldp['options'] = get_options(conf)
        lldp['interface_list'] = get_interface_list(conf)
        lldp['location'] = get_location(conf)

        return lldp


def verify(lldp):
    # bail out early - looks like removal from running config
    if lldp is None:
        return

    # check location
    for location in lldp['location']:
        # check coordinate-based
        if len(location['coordinate_based']) > 0:
            # check longitude and latitude
            if not location['coordinate_based']['longitude']:
                raise ConfigError('Must define longitude for interface {0}'.format(location['name']))

            if not location['coordinate_based']['latitude']:
                raise ConfigError('Must define latitude for interface {0}'.format(location['name']))

            if not re.match(r'^(\d+)(\.\d+)?[nNsS]$', location['coordinate_based']['latitude']):
                raise ConfigError('Invalid location for interface {0}:\n' \
                                  'latitude should be a number followed by S or N'.format(location['name']))

            if not re.match(r'^(\d+)(\.\d+)?[eEwW]$', location['coordinate_based']['longitude']):
                raise ConfigError('Invalid location for interface {0}:\n' \
                                  'longitude should be a number followed by E or W'.format(location['name']))

            # check altitude and datum if exist
            if location['coordinate_based']['altitude']:
                if not re.match(r'^[-+0-9\.]+$', location['coordinate_based']['altitude']):
                    raise ConfigError('Invalid location for interface {0}:\n' \
                                      'altitude should be a positive or negative number'.format(location['name']))

            if location['coordinate_based']['datum']:
                if not re.match(r'^(WGS84|NAD83|MLLW)$', location['coordinate_based']['datum']):
                    raise ConfigError("Invalid location for interface {0}:\n' \
                                      'datum should be WGS84, NAD83, or MLLW".format(location['name']))

        # check elin
        elif location['elin']:
            if not re.match(r'^[0-9]{10,25}$', location['elin']):
                raise ConfigError('Invalid location for interface {0}:\n' \
                                  'ELIN number must be between 10-25 numbers'.format(location['name']))

    # check options
    if lldp['options']['snmp']:
        if not lldp['options']['sys_snmp']:
            raise ConfigError('SNMP must be configured to enable LLDP SNMP')


def generate(lldp):
    # bail out early - looks like removal from running config
    if lldp is None:
        return

    # Prepare Jinja2 template loader from files
    tmpl_path = os.path.join(vyos_data_dir['data'], 'templates', 'lldp')
    fs_loader = FileSystemLoader(tmpl_path)
    env = Environment(loader=fs_loader)

    # generate listen on interfaces
    for intf in lldp['interface_list']:
        tmp = ''
        # add exclamation mark if interface is disabled
        if intf['disable']:
            tmp = '!'

        tmp += intf['name']
        lldp['options']['listen_on'].append(tmp)

    # generate /etc/default/lldpd
    tmpl = env.get_template('lldpd.tmpl')
    config_text = tmpl.render(lldp)
    with open(config_file, 'w') as f:
        f.write(config_text)

    # generate /etc/lldpd.d/01-vyos.conf
    tmpl = env.get_template('vyos.conf.tmpl')
    config_text = tmpl.render(lldp)
    with open(vyos_config_file, 'w') as f:
        f.write(config_text)


def apply(lldp):
    if lldp:
        # start/restart lldp service
        os.system('sudo systemctl restart lldpd.service')
    else:
        # LLDP service has been terminated
        os.system('sudo systemctl stop lldpd.service')
        os.unlink(config_file)
        os.unlink(vyos_config_file)

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