#!/usr/bin/env python3
#
# Copyright (C) 2019-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

from copy import deepcopy
from sys import exit
from netifaces import interfaces

from vyos.ifconfig import BridgeIf, Section
from vyos.ifconfig.stp import STP
from vyos.configdict import list_diff
from vyos.validate import is_member, has_address_configured
from vyos.config import Config
from vyos.util import cmd, get_bridge_member_config
from vyos import ConfigError

default_config_data = {
    'address': [],
    'address_remove': [],
    'aging': 300,
    'arp_cache_tmo': 30,
    'description': '',
    'deleted': False,
    'dhcp_client_id': '',
    'dhcp_hostname': '',
    'dhcp_vendor_class_id': '',
    'dhcpv6_prm_only': False,
    'dhcpv6_temporary': False,
    'disable': False,
    'disable_link_detect': 1,
    'forwarding_delay': 14,
    'hello_time': 2,
    'ip_disable_arp_filter': 1,
    'ip_enable_arp_accept': 0,
    'ip_enable_arp_announce': 0,
    'ip_enable_arp_ignore': 0,
    'ipv6_accept_ra': 1,
    'ipv6_autoconf': 0,
    'ipv6_eui64_prefix': [],
    'ipv6_eui64_prefix_remove': [],
    'ipv6_forwarding': 1,
    'ipv6_dup_addr_detect': 1,
    'igmp_querier': 0,
    'intf': '',
    'mac' : '',
    'max_age': 20,
    'member': [],
    'member_remove': [],
    'priority': 32768,
    'stp': 0,
    'vrf': ''
}

def get_config():
    bridge = deepcopy(default_config_data)
    conf = Config()

    # determine tagNode instance
    if 'VYOS_TAGNODE_VALUE' not in os.environ:
        raise ConfigError('Interface (VYOS_TAGNODE_VALUE) not specified')

    bridge['intf'] = os.environ['VYOS_TAGNODE_VALUE']

    # Check if bridge has been removed
    if not conf.exists('interfaces bridge ' + bridge['intf']):
        bridge['deleted'] = True
        return bridge

    # set new configuration level
    conf.set_level('interfaces bridge ' + bridge['intf'])

    # retrieve configured interface addresses
    if conf.exists('address'):
        bridge['address'] = conf.return_values('address')

    # Determine interface addresses (currently effective) - to determine which
    # address is no longer valid and needs to be removed
    eff_addr = conf.return_effective_values('address')
    bridge['address_remove'] = list_diff(eff_addr, bridge['address'])

    # retrieve aging - how long addresses are retained
    if conf.exists('aging'):
        bridge['aging'] = int(conf.return_value('aging'))

    # retrieve interface description
    if conf.exists('description'):
        bridge['description'] = conf.return_value('description')

    # get DHCP client identifier
    if conf.exists('dhcp-options client-id'):
        bridge['dhcp_client_id'] = conf.return_value('dhcp-options client-id')

    # DHCP client host name (overrides the system host name)
    if conf.exists('dhcp-options host-name'):
        bridge['dhcp_hostname'] = conf.return_value('dhcp-options host-name')

    # DHCP client vendor identifier
    if conf.exists('dhcp-options vendor-class-id'):
        bridge['dhcp_vendor_class_id'] = conf.return_value('dhcp-options vendor-class-id')

    # DHCPv6 only acquire config parameters, no address
    if conf.exists('dhcpv6-options parameters-only'):
        bridge['dhcpv6_prm_only'] = True

    # DHCPv6 temporary IPv6 address
    if conf.exists('dhcpv6-options temporary'):
        bridge['dhcpv6_temporary'] = True

    # Disable this bridge interface
    if conf.exists('disable'):
        bridge['disable'] = True

    # Ignore link state changes
    if conf.exists('disable-link-detect'):
        bridge['disable_link_detect'] = 2

    # Forwarding delay
    if conf.exists('forwarding-delay'):
        bridge['forwarding_delay'] = int(conf.return_value('forwarding-delay'))

    # Hello packet advertisment interval
    if conf.exists('hello-time'):
        bridge['hello_time'] = int(conf.return_value('hello-time'))

    # Enable Internet Group Management Protocol (IGMP) querier
    if conf.exists('igmp querier'):
        bridge['igmp_querier'] = 1

    # ARP cache entry timeout in seconds
    if conf.exists('ip arp-cache-timeout'):
        bridge['arp_cache_tmo'] = int(conf.return_value('ip arp-cache-timeout'))

    # ARP filter configuration
    if conf.exists('ip disable-arp-filter'):
        bridge['ip_disable_arp_filter'] = 0

    # ARP enable accept
    if conf.exists('ip enable-arp-accept'):
        bridge['ip_enable_arp_accept'] = 1

    # ARP enable announce
    if conf.exists('ip enable-arp-announce'):
        bridge['ip_enable_arp_announce'] = 1

    # ARP enable ignore
    if conf.exists('ip enable-arp-ignore'):
        bridge['ip_enable_arp_ignore'] = 1

    # Enable acquisition of IPv6 address using stateless autoconfig (SLAAC)
    if conf.exists('ipv6 address autoconf'):
        bridge['ipv6_autoconf'] = 1

    # Get prefixes for IPv6 addressing based on MAC address (EUI-64)
    if conf.exists('ipv6 address eui64'):
        bridge['ipv6_eui64_prefix'] = conf.return_values('ipv6 address eui64')

    # Determine currently effective EUI64 addresses - to determine which
    # address is no longer valid and needs to be removed
    eff_addr = conf.return_effective_values('ipv6 address eui64')
    bridge['ipv6_eui64_prefix_remove'] = list_diff(eff_addr, bridge['ipv6_eui64_prefix'])

    # Remove the default link-local address if set.
    if conf.exists('ipv6 address no-default-link-local'):
        bridge['ipv6_eui64_prefix_remove'].append('fe80::/64')
    else:
        # add the link-local by default to make IPv6 work
        bridge['ipv6_eui64_prefix'].append('fe80::/64')

    # Disable IPv6 forwarding on this interface
    if conf.exists('ipv6 disable-forwarding'):
        bridge['ipv6_forwarding'] = 0

    # IPv6 Duplicate Address Detection (DAD) tries
    if conf.exists('ipv6 dup-addr-detect-transmits'):
        bridge['ipv6_dup_addr_detect'] = int(conf.return_value('ipv6 dup-addr-detect-transmits'))

    # Media Access Control (MAC) address
    if conf.exists('mac'):
        bridge['mac'] = conf.return_value('mac')

    # Find out if MAC has changed - if so, we need to delete all IPv6 EUI64 addresses
    # before re-adding them
    if ( bridge['mac'] and bridge['intf'] in Section.interfaces(section='bridge')
             and bridge['mac'] != BridgeIf(bridge['intf'], create=False).get_mac() ):
        bridge['ipv6_eui64_prefix_remove'] += bridge['ipv6_eui64_prefix']

    # to make IPv6 SLAAC and DHCPv6 work with forwarding=1,
    # accept_ra must be 2
    if bridge['ipv6_autoconf'] or 'dhcpv6' in bridge['address']:
        bridge['ipv6_accept_ra'] = 2

    # Interval at which neighbor bridges are removed
    if conf.exists('max-age'):
        bridge['max_age'] = int(conf.return_value('max-age'))

    # Determine bridge member interface (currently configured)
    for intf in conf.list_nodes('member interface'):
        # defaults are stored in util.py (they can't be here as all interface
        # scripts use the function)
        memberconf = get_bridge_member_config(conf, bridge['intf'], intf)
        if memberconf:
            memberconf['name'] = intf
            bridge['member'].append(memberconf)

    # Determine bridge member interface (currently effective) - to determine which
    # interfaces is no longer assigend to the bridge and thus can be removed
    eff_intf = conf.list_effective_nodes('member interface')
    act_intf = conf.list_nodes('member interface')
    bridge['member_remove'] = list_diff(eff_intf, act_intf)

    # Priority for this bridge
    if conf.exists('priority'):
        bridge['priority'] = int(conf.return_value('priority'))

    # Enable spanning tree protocol
    if conf.exists('stp'):
        bridge['stp'] = 1

    # retrieve VRF instance
    if conf.exists('vrf'):
        bridge['vrf'] = conf.return_value('vrf')

    return bridge

def verify(bridge):
    if bridge['dhcpv6_prm_only'] and bridge['dhcpv6_temporary']:
        raise ConfigError('DHCPv6 temporary and parameters-only options are mutually exclusive!')

    vrf_name = bridge['vrf']
    if vrf_name and vrf_name not in interfaces():
        raise ConfigError(f'VRF "{vrf_name}" does not exist')

    conf = Config()
    for intf in bridge['member']:
        # the interface must exist prior adding it to a bridge
        if intf['name'] not in interfaces():
            raise ConfigError((
                f'Cannot add nonexistent interface "{intf["name"]}" '
                f'to bridge "{bridge["intf"]}"'))

        if intf['name'] == 'lo':
            raise ConfigError('Loopback interface "lo" can not be added to a bridge')

        # bridge members aren't allowed to be members of another bridge
        for br in conf.list_nodes('interfaces bridge'):
            # it makes no sense to verify ourself in this case
            if br == bridge['intf']:
                continue

            tmp = conf.list_nodes(f'interfaces bridge {br} member interface')
            if intf['name'] in tmp:
                raise ConfigError((
                    f'Cannot add interface "{intf["name"]}" to bridge '
                    f'"{bridge["intf"]}", it is already a member of bridge "{br}"!'))

        # bridge members are not allowed to be bond members
        tmp = is_member(conf, intf['name'], 'bonding')
        if tmp:
            raise ConfigError((
                f'Cannot add interface "{intf["name"]}" to bridge '
                f'"{bridge["intf"]}", it is already a member of bond "{tmp}"!'))

        # bridge members must not have an assigned address
        if has_address_configured(conf, intf['name']):
            raise ConfigError((
                f'Cannot add interface "{intf["name"]}" to bridge '
                f'"{bridge["intf"]}", it has an address assigned!'))

    return None

def generate(bridge):
    return None

def apply(bridge):
    br = BridgeIf(bridge['intf'])

    if bridge['deleted']:
        # delete interface
        br.remove()
    else:
        # enable interface
        br.set_admin_state('up')
        # set ageing time
        br.set_ageing_time(bridge['aging'])
        # set bridge forward delay
        br.set_forward_delay(bridge['forwarding_delay'])
        # set hello time
        br.set_hello_time(bridge['hello_time'])
        # configure ARP filter configuration
        br.set_arp_filter(bridge['ip_disable_arp_filter'])
        # configure ARP accept
        br.set_arp_accept(bridge['ip_enable_arp_accept'])
        # configure ARP announce
        br.set_arp_announce(bridge['ip_enable_arp_announce'])
        # configure ARP ignore
        br.set_arp_ignore(bridge['ip_enable_arp_ignore'])
        # IPv6 accept RA
        br.set_ipv6_accept_ra(bridge['ipv6_accept_ra'])
        # IPv6 address autoconfiguration
        br.set_ipv6_autoconf(bridge['ipv6_autoconf'])
        # IPv6 forwarding
        br.set_ipv6_forwarding(bridge['ipv6_forwarding'])
        # IPv6 Duplicate Address Detection (DAD) tries
        br.set_ipv6_dad_messages(bridge['ipv6_dup_addr_detect'])
        # set max message age
        br.set_max_age(bridge['max_age'])
        # set bridge priority
        br.set_priority(bridge['priority'])
        # turn stp on/off
        br.set_stp(bridge['stp'])
        # enable or disable IGMP querier
        br.set_multicast_querier(bridge['igmp_querier'])
        # update interface description used e.g. within SNMP
        br.set_alias(bridge['description'])

        if bridge['dhcp_client_id']:
            br.dhcp.v4.options['client_id'] = bridge['dhcp_client_id']

        if bridge['dhcp_hostname']:
            br.dhcp.v4.options['hostname'] = bridge['dhcp_hostname']

        if bridge['dhcp_vendor_class_id']:
            br.dhcp.v4.options['vendor_class_id'] = bridge['dhcp_vendor_class_id']

        if bridge['dhcpv6_prm_only']:
            br.dhcp.v6.options['dhcpv6_prm_only'] = True

        if bridge['dhcpv6_temporary']:
            br.dhcp.v6.options['dhcpv6_temporary'] = True

        # assign/remove VRF
        br.set_vrf(bridge['vrf'])

        # Delete old IPv6 EUI64 addresses before changing MAC
        # (adding members to a fresh bridge changes its MAC too)
        for addr in bridge['ipv6_eui64_prefix_remove']:
            br.del_ipv6_eui64_address(addr)

        # remove interface from bridge
        for intf in bridge['member_remove']:
            br.del_port(intf)

        # add interfaces to bridge
        for member in bridge['member']:
            # if we've come here we already verified the interface doesn't
            # have addresses configured so just flush any remaining ones
            cmd(f'ip addr flush dev "{member["name"]}"')
            br.add_port(member['name'])

        # Change interface MAC address
        if bridge['mac']:
            br.set_mac(bridge['mac'])

        # Add IPv6 EUI-based addresses (must be done after adding the
        # 1st bridge member or setting its MAC)
        for addr in bridge['ipv6_eui64_prefix']:
            br.add_ipv6_eui64_address(addr)

        # up/down interface
        if bridge['disable']:
            br.set_admin_state('down')

        # Configure interface address(es)
        # - not longer required addresses get removed first
        # - newly addresses will be added second
        for addr in bridge['address_remove']:
            br.del_addr(addr)
        for addr in bridge['address']:
            br.add_addr(addr)

        STPBridgeIf = STP.enable(BridgeIf)
        # configure additional bridge member options
        for member in bridge['member']:
            i = STPBridgeIf(member['name'])
            # configure ARP cache timeout
            i.set_arp_cache_tmo(member['arp_cache_tmo'])
            # ignore link state changes
            i.set_link_detect(member['disable_link_detect'])
            # set bridge port path cost
            i.set_path_cost(member['cost'])
            # set bridge port path priority
            i.set_path_priority(member['priority'])

    return None

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